@xenosystem/blocks 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3255 @@
1
+ import { PanelModule, PanelManifest, LogSink } from '@xenosystem/panel-sdk';
2
+ import * as _xenosystem_data_core from '@xenosystem/data-core';
3
+ import { XenoColumn, XenoQuery, XenoResultSet, XenoValue, XenoRow, XenoAggFn, XenoValueFormat, XenoFilterGroup, XenoColumnRole, XenoSchemaCatalog, XenoFilter, XenoSourceSchema } from '@xenosystem/data-core';
4
+ import { ReactNode } from 'react';
5
+ import { C as ConnectorExecuteInput, a as ConnectorExecuteOutput, b as ConnectorsPanelState, S as SubstrateFacilities, c as ConnectorsViewState, d as XenoConnection, e as XenoCredentialFormSpec, f as XenoCredentialResult, g as XenoConnectionStatus, P as PushdownSplit, h as XenoConnectorQueryRequest, i as XenoRestPaging, j as XenoConnectorFetch, k as XenoResolverErrorCode, l as XenoConnectorFileReader, X as XenoSqlEngine, m as ConnectorTransport, n as XenoCredentialField } from '../transport-B1cdciP8.js';
6
+ export { B as BrowserFetchOptions, o as ConnectorTransportError, p as SECRET_HEADER_NAMES, q as XenoConnectionConfig, r as XenoConnectionHealth, s as XenoConnectionKind, t as XenoConnectionState, u as XenoConnectorCapabilities, v as XenoCredentialSubmission, w as XenoFetchRequest, x as XenoFetchResponse, y as XenoFileReadRequest, z as XenoFileReadResponse, A as XenoRefreshPolicy, D as XenoSqlTable, E as browserFetch, F as classifyBrowserFetchFailure, G as defaultCapabilitiesFor, H as errorResult, I as evaluateSupport, J as facilitiesOf, K as normalizePaging, L as secretHeaderNames } from '../transport-B1cdciP8.js';
7
+
8
+ /**
9
+ * Column layout — width, order, visibility and freezing.
10
+ *
11
+ * Layout is **panel state, not query state**: it persists through `serialize()` and never travels in
12
+ * a `XenoQuery`. Widths are keyed by `XenoColumn.id`, so a source that renames a column keeps its
13
+ * width and a source that returns two columns with the same display name keeps them apart (§6.2).
14
+ *
15
+ * All of it is pure so the geometry is unit-tested without a DOM — the same discipline that let the
16
+ * Transport panel's time math be tested without a clock.
17
+ *
18
+ * @module
19
+ */
20
+
21
+ /** Per-column layout overrides, keyed by column id. */
22
+ interface ColumnLayout {
23
+ /** Explicit widths in px. */
24
+ widths: Record<string, number>;
25
+ /** Explicit left-to-right order; ids absent from the result are ignored. */
26
+ order: string[];
27
+ /** Ids the user hid. */
28
+ hidden: string[];
29
+ /** How many leading columns are frozen (pinned) — counted in RENDER order. */
30
+ frozenCount: number;
31
+ }
32
+ /** An empty layout — every column visible, in source order, at the default width. */
33
+ declare const EMPTY_LAYOUT: ColumnLayout;
34
+ /** Default column width in px when neither the source nor the user set one. */
35
+ declare const DEFAULT_COLUMN_WIDTH = 140;
36
+ /** Minimum width a column can be dragged to — below this a header is unreadable and unclickable. */
37
+ declare const MIN_COLUMN_WIDTH = 40;
38
+ /** A column resolved for rendering: the source column plus its resolved geometry. */
39
+ interface ResolvedColumn {
40
+ /** The source column. */
41
+ column: XenoColumn;
42
+ /** Resolved width in px. */
43
+ width: number;
44
+ /** Distance from the left edge of the full column strip, in px. */
45
+ offset: number;
46
+ /** Frozen (pinned) — rendered in the sticky leading region. */
47
+ frozen: boolean;
48
+ /** Index in render order. */
49
+ index: number;
50
+ }
51
+ /**
52
+ * Resolve the render-order column list with widths and offsets.
53
+ *
54
+ * Ordering rule: ids named in `layout.order` come first in that order, then any remaining source
55
+ * columns in **source order**. A source that adds a column therefore appends it rather than
56
+ * silently dropping it — the failure mode of a layout that stores a closed list.
57
+ *
58
+ * @param columns - The result's columns.
59
+ * @param layout - Persisted layout state.
60
+ * @returns Visible columns, resolved.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const resolved = resolveColumns(result.columns, layout)
65
+ * const totalWidth = resolved.at(-1)!.offset + resolved.at(-1)!.width
66
+ * ```
67
+ */
68
+ declare function resolveColumns(columns: readonly XenoColumn[], layout?: ColumnLayout): ResolvedColumn[];
69
+ /** Total width of the resolved column strip, in px. */
70
+ declare function totalWidth(resolved: readonly ResolvedColumn[]): number;
71
+ /** Width of the frozen (sticky) leading region, in px. */
72
+ declare function frozenWidth(resolved: readonly ResolvedColumn[]): number;
73
+ /**
74
+ * Set a column's width, clamped to the minimum.
75
+ *
76
+ * @param layout - Current layout.
77
+ * @param columnId - Column to resize.
78
+ * @param width - Requested width in px.
79
+ * @returns A new layout (the input is not mutated).
80
+ */
81
+ declare function setColumnWidth(layout: ColumnLayout, columnId: string, width: number): ColumnLayout;
82
+ /**
83
+ * Move a column to a new index in render order.
84
+ *
85
+ * The order list is rewritten from the CURRENT resolved order, not patched, so a reorder is
86
+ * meaningful even the first time (when `layout.order` is still empty).
87
+ *
88
+ * @param layout - Current layout.
89
+ * @param columns - The result's columns (for the current order).
90
+ * @param columnId - Column to move.
91
+ * @param toIndex - Destination index among visible columns.
92
+ * @returns A new layout.
93
+ */
94
+ declare function moveColumn(layout: ColumnLayout, columns: readonly XenoColumn[], columnId: string, toIndex: number): ColumnLayout;
95
+ /**
96
+ * Toggle a column's visibility.
97
+ *
98
+ * @param layout - Current layout.
99
+ * @param columnId - Column to toggle.
100
+ * @param visible - Desired visibility; omitted flips.
101
+ * @returns A new layout.
102
+ */
103
+ declare function setColumnVisible(layout: ColumnLayout, columnId: string, visible?: boolean): ColumnLayout;
104
+ /**
105
+ * Set how many leading columns are frozen.
106
+ *
107
+ * @param layout - Current layout.
108
+ * @param count - Number of columns to pin; clamped to `>= 0`.
109
+ * @returns A new layout.
110
+ */
111
+ declare function setFrozenCount(layout: ColumnLayout, count: number): ColumnLayout;
112
+ /** Normalize an unknown (deserialized) layout, dropping anything malformed. */
113
+ declare function normalizeLayout(raw: unknown): ColumnLayout;
114
+
115
+ /**
116
+ * Panel-local types for `xeno.core.table`.
117
+ *
118
+ * The data contract itself lives in `@xenosystem/data-core` — this file holds only what the PANEL owns:
119
+ * view mode, selection, the cell-edit intent, and the serialized shape.
120
+ *
121
+ * @module
122
+ */
123
+
124
+ /**
125
+ * How the row set is laid out.
126
+ *
127
+ * v1 ships `grid` and `pivot`. `board` and `gallery` are reserved in the union — they are the same
128
+ * rows and the same cell editor with a different layout function, and reserving the values now means
129
+ * adding them later is not a config migration. See the README for why they are not in v1.
130
+ */
131
+ type TableViewMode = 'grid' | 'pivot' | 'board' | 'gallery';
132
+ /** Every view mode the panel currently implements. */
133
+ declare const IMPLEMENTED_VIEW_MODES: readonly TableViewMode[];
134
+ /** An edit the user performed on a cell. Emitted as an INTENT — the host writes. */
135
+ interface XenoCellEdit {
136
+ /** Row to edit. */
137
+ rowId: string;
138
+ /** Column to edit. */
139
+ columnId: string;
140
+ /** The new value. */
141
+ value: XenoValue;
142
+ /** The value before the edit, so a host can build an undo entry without re-reading. */
143
+ previousValue: XenoValue;
144
+ /**
145
+ * Gesture correlation id. Every preview and the final commit of one edit share it, so a host
146
+ * coalesces them into **one** undo entry.
147
+ */
148
+ gestureId: string;
149
+ }
150
+ /** A row activation (double-click / Enter) — the event that drives BACK-view execution. */
151
+ interface XenoRowActivate {
152
+ /** The activated row. */
153
+ rowId: string;
154
+ /** The column the activation happened in, when it was a cell. */
155
+ columnId?: string;
156
+ }
157
+ /** The panel's serialized state. `.xapp` stores this; it never stores a result. */
158
+ interface TablePanelState {
159
+ /** The query — **this persists**. */
160
+ query: XenoQuery;
161
+ /** View mode. */
162
+ viewMode: TableViewMode;
163
+ /** Column widths / order / hidden / frozen. */
164
+ layout: ColumnLayout;
165
+ /** Ids of collapsed groups, serialized from their structured keys. */
166
+ collapsedGroups: string[];
167
+ /** Selected row ids. */
168
+ selection: string[];
169
+ }
170
+ /** What the panel exposes to its view layer. */
171
+ interface TableViewState {
172
+ /** The newest result, or `null` before the first one lands. */
173
+ result: XenoResultSet | null;
174
+ /** The query the panel intends. Chrome renders from `result.query` when one exists. */
175
+ query: XenoQuery;
176
+ /** Current view mode. */
177
+ viewMode: TableViewMode;
178
+ /** Column layout. */
179
+ layout: ColumnLayout;
180
+ /** Serialized keys of collapsed groups. */
181
+ collapsedGroups: string[];
182
+ /** Selected row ids. */
183
+ selection: string[];
184
+ /** The focused cell, when the grid has a cursor. */
185
+ cursor: {
186
+ rowIndex: number;
187
+ columnIndex: number;
188
+ } | null;
189
+ /** The cell currently being edited. */
190
+ editing: {
191
+ rowId: string;
192
+ columnId: string;
193
+ draft: string;
194
+ } | null;
195
+ /** A request is outstanding. */
196
+ pending: boolean;
197
+ }
198
+
199
+ /** The host seam the controller emits through. */
200
+ interface TableHostBridge {
201
+ /** Emit on a declared output port. */
202
+ emit(portId: string, value: unknown): void;
203
+ /** Generate a correlation id. Injectable so tests are deterministic. */
204
+ makeId?: () => string;
205
+ }
206
+ /** Construction options. */
207
+ interface TableControllerOptions {
208
+ /** The host bridge. */
209
+ host: TableHostBridge;
210
+ /** Initial state (from `deserialize`). */
211
+ initial?: Partial<TablePanelState>;
212
+ /** Rows per page. Default 100. */
213
+ pageSize?: number;
214
+ /** Emit `editPreview` while typing. Default `false` — cell editing is commit-on-blur by nature. */
215
+ livePreview?: boolean;
216
+ }
217
+ /** Default page size. */
218
+ declare const DEFAULT_PAGE_SIZE = 100;
219
+ /** Structural equality for queries — the diff-before-emit gate. */
220
+ declare function queriesEqual(a: XenoQuery | undefined, b: XenoQuery | undefined): boolean;
221
+ /** The Table panel controller. */
222
+ declare class TableController {
223
+ private readonly host;
224
+ private readonly makeId;
225
+ private readonly pageSize;
226
+ private readonly livePreview;
227
+ private readonly listeners;
228
+ private result;
229
+ private query;
230
+ private viewMode;
231
+ private layout;
232
+ private collapsedGroups;
233
+ private selection;
234
+ private cursor;
235
+ private editing;
236
+ /** The newest emitted request id. Anything else that arrives is stale and dropped. */
237
+ private outstandingId;
238
+ /** The last query actually emitted, for diff-before-emit. */
239
+ private lastEmitted;
240
+ /** Cached view snapshot, so `useSyncExternalStore` sees a stable reference between changes. */
241
+ private snapshot;
242
+ constructor(options: TableControllerOptions);
243
+ /** Subscribe to state changes. Returns an unsubscribe function. */
244
+ subscribe: (listener: () => void) => (() => void);
245
+ /** The current view snapshot (stable reference until something changes). */
246
+ getState: () => TableViewState;
247
+ private notify;
248
+ /**
249
+ * Deliver a result.
250
+ *
251
+ * @param payload - `{requestId, result}` — the id must be echoed from the request.
252
+ * @returns `true` if the result was accepted, `false` if it was dropped as stale.
253
+ */
254
+ deliverResult(payload: {
255
+ requestId?: string;
256
+ result: XenoResultSet;
257
+ } | XenoResultSet): boolean;
258
+ /**
259
+ * Apply an incremental update from a live source.
260
+ *
261
+ * @param delta - The delta.
262
+ * @returns `true` if applied, `false` if there is nothing to apply it to.
263
+ */
264
+ deliverDelta(delta: _xenosystem_data_core.XenoResultDelta): boolean;
265
+ /** Selection may reference rows the newest page no longer contains — drop those. */
266
+ private pruneSelection;
267
+ /**
268
+ * Emit a query request, unless the query is structurally unchanged.
269
+ *
270
+ * @param reason - Why the query changed (telemetry + host debounce hints).
271
+ * @param force - Emit even if unchanged (an explicit refresh).
272
+ * @returns The emitted request id, or `null` if the emission was suppressed.
273
+ */
274
+ requestQuery(reason: _xenosystem_data_core.XenoQueryRequest['reason'], force?: boolean): string | null;
275
+ /** Replace the query wholesale (from a wired `fields` panel) and request it. */
276
+ setQuery(query: XenoQuery): void;
277
+ /**
278
+ * Cycle a column's sort: none → asc → desc → none.
279
+ *
280
+ * @param columnId - The column.
281
+ * @param additive - Keep existing terms and append/update this one (Shift-click).
282
+ */
283
+ toggleSort(columnId: string, additive?: boolean): void;
284
+ /** Replace the filter tree. */
285
+ setFilters(filters: XenoQuery['filters']): void;
286
+ /** Replace the grouping levels. */
287
+ setGroupBy(groupBy: XenoQuery['groupBy']): void;
288
+ /** Replace the pivot spec. Switching it on also switches the view mode. */
289
+ setPivot(pivot: XenoQuery['pivot']): void;
290
+ /** Go to a page by index (0-based). */
291
+ goToPage(index: number): void;
292
+ /** Step one page forward/backward, clamped to what the result says exists. */
293
+ stepPage(direction: 1 | -1): void;
294
+ /** Change the page size, returning to the first page. */
295
+ setPageSize(limit: number): void;
296
+ /** Re-run the current query unchanged. */
297
+ refresh(): void;
298
+ private firstPage;
299
+ /**
300
+ * The query the CHROME should render from.
301
+ *
302
+ * `result.query` when there is one and the result is settled; the local intent only while a
303
+ * request is in flight. This is invariant 2, in one method.
304
+ */
305
+ effectiveQuery(): XenoQuery;
306
+ /** Switch view mode. Unimplemented modes are refused rather than rendering blank. */
307
+ setViewMode(mode: TableViewMode): boolean;
308
+ /** Resolved columns for the current result + layout. */
309
+ resolvedColumns(): ResolvedColumn[];
310
+ /** Set a column width. */
311
+ resizeColumn(columnId: string, width: number): void;
312
+ /** Move a column in render order. */
313
+ reorderColumn(columnId: string, toIndex: number): void;
314
+ /** Show/hide a column. */
315
+ toggleColumn(columnId: string, visible?: boolean): void;
316
+ /** Freeze the leading `count` columns. */
317
+ freezeColumns(count: number): void;
318
+ /** Collapse/expand a group by its serialized key. */
319
+ toggleGroup(serializedKey: string): void;
320
+ /** Select rows. `additive` toggles membership instead of replacing. */
321
+ select(rowIds: readonly string[], additive?: boolean): void;
322
+ /** Move the cell cursor, clamped to the grid. */
323
+ moveCursor(delta: {
324
+ rows?: number;
325
+ columns?: number;
326
+ }): void;
327
+ /** Put the cursor at an explicit address. */
328
+ setCursor(address: {
329
+ rowIndex: number;
330
+ columnIndex: number;
331
+ } | null): void;
332
+ /** Emit a row activation (double-click / Enter) — the BACK-view execution trigger. */
333
+ activateRow(rowId: string, columnId?: string): void;
334
+ /**
335
+ * Begin editing a cell.
336
+ *
337
+ * @param rowId - Row.
338
+ * @param columnId - Column.
339
+ * @returns `true` if editing started; `false` when the column is not editable or the row is gone.
340
+ */
341
+ beginEdit(rowId: string, columnId: string): boolean;
342
+ /** Update the in-flight draft. Emits `editPreview` only when `livePreview` is on. */
343
+ updateDraft(text: string): void;
344
+ /**
345
+ * Commit the edit.
346
+ *
347
+ * Rejects invalid input by REVERTING rather than committing a coerced value — the whole point of
348
+ * inheriting the Inspector's parse-as-a-result contract. Also suppresses a no-op commit, so
349
+ * tabbing through a column does not write an undo entry per cell.
350
+ *
351
+ * @returns `true` if an `edit` intent was emitted.
352
+ */
353
+ commitEdit(): boolean;
354
+ /** Abandon the edit. Escape reverts; nothing is emitted. */
355
+ cancelEdit(): void;
356
+ private buildEdit;
357
+ /** Parse a draft according to the column's declared type. */
358
+ private parseDraft;
359
+ private rowById;
360
+ private columnById;
361
+ /** Format a cell for display — the column's format, honoured. */
362
+ displayValue(row: XenoRow, column: XenoColumn): string;
363
+ /**
364
+ * Serialize the panel's state.
365
+ *
366
+ * **The query persists; the result never does.** A result is transient and can be megabytes; an
367
+ * `.xapp` that embeds one is both stale and enormous the moment it is opened.
368
+ */
369
+ serialize(): TablePanelState;
370
+ /** Restore serialized state. Does NOT emit — the caller decides when to ask for data. */
371
+ deserialize(state: Partial<TablePanelState>): void;
372
+ /** Drop listeners and in-flight state. */
373
+ dispose(): void;
374
+ }
375
+
376
+ /**
377
+ * The `PanelModule` — what a host registers and mounts.
378
+ *
379
+ * Everything the panel does flows through the injected {@link PanelHost}: inputs arrive on
380
+ * `onInput`, commands on `onCommand`, and every output leaves through `host.emit`. There is no
381
+ * other door.
382
+ *
383
+ * @module
384
+ */
385
+
386
+ /** Everything a renderer needs: the controller plus the host's resolved config. */
387
+ interface TableRenderContext {
388
+ controller: TableController;
389
+ /**
390
+ * The host's config, already defaulted from the manifest.
391
+ *
392
+ * Passed in because a renderer that cannot see config makes every manifest-declared display
393
+ * option unreachable — the panel would document `rowHeight` and `zebra` and then ignore them.
394
+ */
395
+ config: {
396
+ rowHeight: number;
397
+ showFooter: boolean;
398
+ zebra: boolean;
399
+ showRowNumbers: boolean;
400
+ };
401
+ }
402
+ /** Options for {@link createTablePanel}. */
403
+ interface CreateTablePanelOptions {
404
+ /**
405
+ * Override the renderer.
406
+ *
407
+ * **Rarely needed.** The module wires {@link TablePanelView} itself, which is what keeps `react`
408
+ * and `react-dom` resolving to ONE copy: if the host calls `createRoot` against a view imported
409
+ * from the package, a `file:`/workspace link gives the host's `react-dom` and the package's
410
+ * `react` two different copies and every hook throws *"Invalid hook call"*. Vitest hides this
411
+ * (its React plugin dedupes implicitly); a real app does not.
412
+ *
413
+ * Supply this only for a non-React host or a test double.
414
+ */
415
+ render?: (root: HTMLElement, context: TableRenderContext) => () => void;
416
+ }
417
+ /**
418
+ * Build the Table panel module.
419
+ *
420
+ * @param options - Optional renderer.
421
+ * @returns The module.
422
+ */
423
+ declare function createTablePanel(options?: CreateTablePanelOptions): PanelModule;
424
+ /**
425
+ * The default Table panel module — view already wired.
426
+ *
427
+ * **Register THIS**, not `createTablePanel()` plus a hand-rolled renderer. Every other canonical
428
+ * panel exports the same shape, and the reason is not symmetry: it is that mounting the view inside
429
+ * the package is what guarantees a single React copy.
430
+ */
431
+ declare const tablePanel: PanelModule;
432
+
433
+ /**
434
+ * The `xeno.core.table` manifest.
435
+ *
436
+ * Ports follow the union spec §5 sketch: `result` + `query` in, `queryRequest` + `edit` +
437
+ * `selection` + `rowActivate` out. Every structured port carries a **shape tag**, so the builder
438
+ * cannot wire a Layers `op` into `result` (SDK 1.1's `PanelPort.schema`).
439
+ *
440
+ * **Capabilities: `storage.local` ONLY.** Zero network, zero fs, zero gpu — query execution stays
441
+ * host-side, so the panel is fully sandboxable under `iframe-quickjs`, web-exportable, and
442
+ * marketplace-shippable. That property is the whole point of the request/response seam.
443
+ *
444
+ * @module
445
+ */
446
+
447
+ /** The canonical manifest id. */
448
+ declare const TABLE_PANEL_ID = "xeno.core.table";
449
+ /** The `xeno.core.table` manifest. */
450
+ declare const tableManifest: PanelManifest;
451
+
452
+ /**
453
+ * Grid viewport math — the windowing that keeps a wide, tall table cheap.
454
+ *
455
+ * Rendering discipline taken from sheets' `Grid.tsx` as a **reference only** — its viewport
456
+ * derivation, its `ResizeObserver`-driven measurement, and its 2×`requestAnimationFrame` defer.
457
+ * None of its cell/formula/A1 semantics come along: this grid renders a `XenoResultSet`, which has
458
+ * ids and types, not a spreadsheet address space.
459
+ *
460
+ * Rows use the fixed-height virtualizer (`virtualize.ts`). Columns need their own pass because they
461
+ * are variable width, so the window is found by binary search over the resolved offsets — O(log n)
462
+ * per scroll instead of the O(n) scan a naive loop does on every frame of a horizontal drag.
463
+ *
464
+ * @module
465
+ */
466
+
467
+ /** The visible column window. */
468
+ interface ColumnWindow {
469
+ /** First rendered column index (inclusive). */
470
+ startIndex: number;
471
+ /** Last rendered column index (exclusive). */
472
+ endIndex: number;
473
+ /** Left spacer width in px — the offset of `startIndex`. */
474
+ offsetLeft: number;
475
+ }
476
+ /**
477
+ * Index of the last column whose offset is `<= x`, by binary search.
478
+ *
479
+ * @param columns - Resolved columns, ascending by offset.
480
+ * @param x - Horizontal position in px.
481
+ * @returns The index, or `0` when the strip is empty.
482
+ */
483
+ declare function columnIndexAt(columns: readonly ResolvedColumn[], x: number): number;
484
+ /**
485
+ * Compute the visible column window.
486
+ *
487
+ * Frozen columns are ALWAYS rendered — they are sticky, so scrolling them out of the window would
488
+ * make them disappear from a region that is still on screen. They are excluded from the spacer, and
489
+ * the window starts after them.
490
+ *
491
+ * @param columns - Resolved columns.
492
+ * @param scrollLeft - Horizontal scroll offset in px.
493
+ * @param viewportWidth - Visible width in px.
494
+ * @param overscan - Extra columns to render on each side. Default 1.
495
+ * @returns The window.
496
+ */
497
+ declare function computeColumnWindow(columns: readonly ResolvedColumn[], scrollLeft: number, viewportWidth: number, overscan?: number): ColumnWindow;
498
+ /**
499
+ * The scroll position that brings a column fully into view, or `null` if it already is.
500
+ *
501
+ * Returning `null` for the no-op case is deliberate: a caller that always assigns `scrollLeft`
502
+ * cancels a user's in-flight momentum scroll on every keystroke.
503
+ *
504
+ * @param columns - Resolved columns.
505
+ * @param index - Target column index.
506
+ * @param scrollLeft - Current scroll offset.
507
+ * @param viewportWidth - Visible width.
508
+ * @returns The new `scrollLeft`, or `null`.
509
+ */
510
+ declare function scrollLeftToReveal(columns: readonly ResolvedColumn[], index: number, scrollLeft: number, viewportWidth: number): number | null;
511
+ /**
512
+ * The scroll position that brings a row fully into view, or `null` if it already is.
513
+ *
514
+ * @param index - Row index.
515
+ * @param rowHeight - Fixed row height.
516
+ * @param scrollTop - Current vertical scroll offset.
517
+ * @param viewportHeight - Visible height.
518
+ * @returns The new `scrollTop`, or `null`.
519
+ */
520
+ declare function scrollTopToReveal(index: number, rowHeight: number, scrollTop: number, viewportHeight: number): number | null;
521
+ /** A cell address within the rendered page. */
522
+ interface CellAddress {
523
+ /** Row index within the page. */
524
+ rowIndex: number;
525
+ /** Column index in render order. */
526
+ columnIndex: number;
527
+ }
528
+ /**
529
+ * Move a cell cursor, clamped to the grid.
530
+ *
531
+ * Clamping rather than wrapping: an arrow key at the last row should stop, not jump to the first —
532
+ * wrapping in a grid loses the user's place and is not what any spreadsheet does.
533
+ *
534
+ * @param from - Current address.
535
+ * @param delta - Movement in rows/columns.
536
+ * @param bounds - Grid extent.
537
+ * @returns The new address.
538
+ */
539
+ declare function moveCell(from: CellAddress, delta: {
540
+ rows?: number;
541
+ columns?: number;
542
+ }, bounds: {
543
+ rowCount: number;
544
+ columnCount: number;
545
+ }): CellAddress;
546
+
547
+ /**
548
+ * Fixed-height row virtualization — Canvas's architecture (union spec §1.2).
549
+ *
550
+ * Canvas added this after a documented ~200 ms-at-5k-layers wall; Pixel has none, which is why the
551
+ * parity gate includes a perf floor Pixel itself does not meet today (spec §6). Fixed `rowHeight`
552
+ * keeps the index math O(1) — no measurement, no prefix sums (variable height is v1.1).
553
+ *
554
+ * @module
555
+ */
556
+ /** The slice of rows to render, plus the spacer geometry that preserves scroll extent. */
557
+ interface VirtualWindow {
558
+ /** First row index to render (inclusive), overscan included. */
559
+ startIndex: number;
560
+ /** Last row index to render (exclusive), overscan included. */
561
+ endIndex: number;
562
+ /** Top spacer height in px — `startIndex * rowHeight`. */
563
+ offsetTop: number;
564
+ /** Total scrollable height in px — `rowCount * rowHeight`. */
565
+ totalHeight: number;
566
+ }
567
+ /** Inputs for {@link computeWindow}. */
568
+ interface WindowInput {
569
+ rowCount: number;
570
+ rowHeight: number;
571
+ scrollTop: number;
572
+ viewportHeight: number;
573
+ /** Extra rows rendered above and below the viewport. Canvas ships 8. */
574
+ overscan?: number;
575
+ }
576
+ /**
577
+ * Compute the visible row window.
578
+ *
579
+ * Clamped and NaN-safe: a zero/unknown viewport (before the first `ResizeObserver` tick) yields an
580
+ * empty-but-valid window rather than rendering the whole list.
581
+ *
582
+ * @param input - Row count, fixed row height, scroll offset, viewport height, overscan.
583
+ * @returns The {@link VirtualWindow} to render.
584
+ */
585
+ declare function computeWindow(input: WindowInput): VirtualWindow;
586
+
587
+ /**
588
+ * Numeric cell parsing — **inherited wholesale from `@xenosystem/panel-inspector`'s `numeric.ts`**, not
589
+ * re-invented.
590
+ *
591
+ * The union spec (§6.3) names `parseFloat(e.target.value) || 0` in notes' number-cell editor as a
592
+ * live instance of checklist #91 (`x || default` where `0` is meaningful) *in the richest data
593
+ * surface in the ecosystem*: an empty box, a typo, or a half-typed `-` all commit a silent `0` over
594
+ * whatever the cell held. The spec's remedy is explicit — the cell editor **inherits the Inspector's
595
+ * numeric-field synthesis, not a re-implementation** — because a second implementation is a second
596
+ * chance to reintroduce the bug.
597
+ *
598
+ * Copied rather than imported: panels are standalone (zero cross-panel dependencies), the same rule
599
+ * under which `throttle` and `virtualize` are copied. The Inspector's file is the SOURCE OF TRUTH —
600
+ * a fix there must be mirrored here, and both are covered by the same assertions.
601
+ *
602
+ * Only the parse/clamp/precision half is carried over; the Inspector's slider, log-scale and scrub
603
+ * math have no cell-editor analogue.
604
+ *
605
+ * @module
606
+ */
607
+ /** Parse outcome. Invalid input is a first-class case, never a silent `0`. */
608
+ type ParseResult = {
609
+ ok: true;
610
+ value: number;
611
+ } | {
612
+ ok: false;
613
+ reason: 'empty' | 'nan';
614
+ };
615
+ /** Constraints a numeric cell can carry (read off the column's format). */
616
+ interface NumericConstraints {
617
+ min?: number;
618
+ max?: number;
619
+ step?: number;
620
+ precision?: number;
621
+ /** Reject a fractional result — an `integer` column cannot hold `1.5`. */
622
+ integer?: boolean;
623
+ }
624
+ /**
625
+ * Parse user text into a number.
626
+ *
627
+ * Accepts leading/trailing whitespace, a leading `+`/`-`, decimals, exponents, and a trailing unit
628
+ * suffix (`12px`, `45°`, `50 %`) so a suffix the panel itself rendered round-trips. Everything else
629
+ * is REJECTED — including empty input, a bare `-`, and `NaN`/`Infinity`.
630
+ *
631
+ * @param text - Raw input.
632
+ * @returns `{ok:true, value}` or `{ok:false, reason}` — never a coerced number.
633
+ */
634
+ declare function parseNumeric(text: string): ParseResult;
635
+ /** Clamp to `[min, max]` when declared. */
636
+ declare function clamp(value: number, { min, max }: NumericConstraints): number;
637
+ /** Round to the declared precision (decimal places). No precision ⇒ unchanged. */
638
+ declare function applyPrecision(value: number, precision?: number): number;
639
+ /**
640
+ * Clamp + round in one step — the canonical "make this value legal" helper.
641
+ *
642
+ * @param value - The parsed number.
643
+ * @param c - Constraints.
644
+ * @returns A value that satisfies every declared constraint.
645
+ */
646
+ declare function normalizeNumeric(value: number, c: NumericConstraints): number;
647
+ /**
648
+ * Read the numeric constraints a column implies.
649
+ *
650
+ * @param column - The column.
651
+ * @returns Constraints for {@link normalizeNumeric}.
652
+ */
653
+ declare function constraintsOfColumn(column: {
654
+ type: string;
655
+ format?: {
656
+ precision?: number;
657
+ };
658
+ }): NumericConstraints;
659
+
660
+ /**
661
+ * The Table panel's React view — grid + pivot.
662
+ *
663
+ * **First real consumer of `@xenosystem/workbench/primitives`.** Everything that is chrome (toolbar,
664
+ * status strip, empty/loading/error states, badges, icon buttons, segmented control) comes from the
665
+ * primitives; only the grid body itself — which is the panel's actual job — is local.
666
+ *
667
+ * The view is deliberately thin: all state lives in {@link TableController}, bound through
668
+ * `useSyncExternalStore`. Rendering is the only thing that happens here.
669
+ *
670
+ * @module
671
+ */
672
+
673
+ /** Props for {@link TablePanelView}. */
674
+ interface TablePanelViewProps {
675
+ /** The controller. */
676
+ controller: TableController;
677
+ /** Fixed row height in px (config `rowHeight`). */
678
+ rowHeight?: number;
679
+ /** Render the grand-totals footer. */
680
+ showFooter?: boolean;
681
+ /** Shade alternate rows. */
682
+ zebra?: boolean;
683
+ /**
684
+ * Render a leading ordinal gutter.
685
+ *
686
+ * U5: the manifest declared this from day one and the view never rendered it — declared config
687
+ * with no implementation. The numbers are 1-based over the CURRENT PAGE plus the query offset,
688
+ * so row 1 of page 2 reads as its absolute position rather than restarting.
689
+ */
690
+ showRowNumbers?: boolean;
691
+ }
692
+ /** The Table panel view. */
693
+ declare function TablePanelView({ controller, showRowNumbers, rowHeight, showFooter, zebra, }: TablePanelViewProps): ReactNode;
694
+
695
+ /**
696
+ * The `xeno.core.chart` contract.
697
+ *
698
+ * ## 🔴 Landmine §6.11, stated as a type
699
+ *
700
+ * sheets' chart is bound to a **viewport selection**, its type lives in component `useState` (lost
701
+ * on unmount), and its `ChartConfig` is written but never read. All three are the same mistake:
702
+ * chart state that is not part of the panel's serialized model.
703
+ *
704
+ * Here, data always arrives as a **`XenoResultSet`** — never a viewport range — and the encoding is
705
+ * the panel's persisted state, round-tripped through `serialize()`. A chart that forgets what it was
706
+ * showing is a chart nobody trusts.
707
+ *
708
+ * @module
709
+ */
710
+
711
+ /** Chart types v1 renders. */
712
+ type XenoChartType = 'bar' | 'horizontalBar' | 'line' | 'area' | 'scatter' | 'pie' | 'doughnut';
713
+ /** Every chart type, in menu order. */
714
+ declare const ALL_CHART_TYPES: readonly XenoChartType[];
715
+ /**
716
+ * How columns map to visual channels.
717
+ *
718
+ * **This is the panel's persisted state**, not component state — see the module note.
719
+ */
720
+ interface XenoChartEncoding {
721
+ /** The mark. */
722
+ type: XenoChartType;
723
+ /** Category / time axis. */
724
+ x?: string;
725
+ /** Measure(s). More than one produces multiple series. */
726
+ y?: string[];
727
+ /** Column that splits `y` into one series per distinct value. */
728
+ series?: string;
729
+ /** Column driving mark colour. */
730
+ color?: string;
731
+ /** Column driving mark size (scatter). */
732
+ size?: string;
733
+ /** Aggregate applied to `y` when the query groups. */
734
+ aggregate?: XenoAggFn;
735
+ /** Stack series rather than grouping them. */
736
+ stacked?: boolean;
737
+ /** Line smoothing, `0`–`1`. */
738
+ smoothing?: number;
739
+ }
740
+ /**
741
+ * The monochrome series palette — SIX steps of `--xeno-chart-series-<n>`, and the fill ramp that pairs
742
+ * with it, `--xeno-chart-fill-<n>`. The VALUES live one rung down (`@xenosystem/elements/tokens`
743
+ * `chart`, emitted by elements-react's theme); a block may carry no colour literal. Until 2026-09-18
744
+ * these were twelve white-alpha strings lifted from sheets' `ChartModel`.
745
+ *
746
+ * A `var()` cannot reach a `<canvas>`, so a RENDERER resolves them against the element it draws in —
747
+ * `resolveChartColors(element)` — right before handing the config to Chart.js. The model stays pure
748
+ * and testable: it assigns var strings, never computed colours.
749
+ */
750
+ declare const CHART_PALETTE_STEPS = 6;
751
+ declare const MONO_PALETTE: readonly string[];
752
+ /** The fill palette that pairs with it. */
753
+ declare const MONO_FILL_PALETTE: readonly string[];
754
+ /** The fill for a category with no value (a null slice). */
755
+ declare const CHART_EMPTY_FILL = "var(--xeno-border-subtle)";
756
+ /**
757
+ * A function that turns `var(--name)` strings into the computed value in scope at `element`. Anything
758
+ * that is not a `var()` passes through; a var the theme does not define resolves to `''` and Chart.js
759
+ * uses its own default for it — visible, never a wrong colour.
760
+ */
761
+ declare function resolveChartColors(element: {
762
+ ownerDocument?: {
763
+ defaultView?: {
764
+ getComputedStyle(el: unknown): {
765
+ getPropertyValue(name: string): string;
766
+ };
767
+ } | null;
768
+ } | null;
769
+ } | null | undefined): (css: string) => string;
770
+ /** One plotted series. */
771
+ interface XenoChartSeries {
772
+ /** Stable id — the `series` value, or the `y` column id when there is no split. */
773
+ id: string;
774
+ /** Display label. */
775
+ label: string;
776
+ /** Points, aligned to {@link XenoChartData.labels} by index. `null` is a genuine gap. */
777
+ data: (number | null)[];
778
+ /** Stroke colour. */
779
+ color: string;
780
+ /** Fill colour, for area and bar marks. */
781
+ fill: string;
782
+ }
783
+ /** The renderer-agnostic plot model. */
784
+ interface XenoChartData {
785
+ /** Category labels, one per point index. */
786
+ labels: string[];
787
+ /** The series. */
788
+ series: XenoChartSeries[];
789
+ /** The encoding that produced it. */
790
+ encoding: XenoChartEncoding;
791
+ /**
792
+ * Set when the source has more points than `maxPoints`.
793
+ *
794
+ * The panel then requests an AGGREGATED query rather than downsampling: **silent downsampling is a
795
+ * lie about the data**, and a user cannot tell a thinned line from a real one.
796
+ */
797
+ tooDense?: {
798
+ points: number;
799
+ limit: number;
800
+ };
801
+ }
802
+ /** A cross-filter highlight arriving from a sibling panel. */
803
+ type XenoChartHighlight = {
804
+ rowIds: string[];
805
+ } | {
806
+ columnId: string;
807
+ values: XenoValue[];
808
+ } | null;
809
+ /** What the panel emits when a mark is clicked. */
810
+ interface XenoChartDrillDown {
811
+ columnId: string;
812
+ value: XenoValue;
813
+ /** The series the mark belonged to, when there is one. */
814
+ seriesId?: string;
815
+ }
816
+ /** An export request. The HOST renders and writes — the panel never touches a filesystem. */
817
+ interface XenoChartExportRequest {
818
+ requestId: string;
819
+ format: 'png' | 'svg';
820
+ width: number;
821
+ height: number;
822
+ }
823
+ /** The panel's serialized state. **The encoding persists** — that is the §6.11 fix. */
824
+ interface ChartPanelState {
825
+ encoding: XenoChartEncoding;
826
+ }
827
+ /** What the controller exposes to its view. */
828
+ interface ChartViewState {
829
+ /** The plot model, or `null` before a usable result + encoding exist. */
830
+ data: XenoChartData | null;
831
+ /** The current encoding. */
832
+ encoding: XenoChartEncoding;
833
+ /** Columns available to encode. */
834
+ columns: XenoColumn[];
835
+ /** Chart types this host allows. */
836
+ allowedTypes: readonly XenoChartType[];
837
+ /** Highlighted row ids, from a sibling's cross-filter. */
838
+ highlightedRowIds: string[];
839
+ /** No result, or a result with no rows. */
840
+ empty: boolean;
841
+ /** The result was `partial`. */
842
+ partial: boolean;
843
+ /** A typed error from the result. */
844
+ errorMessage: string | null;
845
+ /** Set when the encoding cannot be plotted, with the reason. */
846
+ encodingProblem: string | null;
847
+ }
848
+ /** Marks that plot a single series against a category axis. */
849
+ declare function isCategorical(type: XenoChartType): boolean;
850
+ /** Marks that draw a filled region. */
851
+ declare function isFilled(type: XenoChartType): boolean;
852
+ /**
853
+ * Whether an encoding can be plotted, and if not, why.
854
+ *
855
+ * Returns a SENTENCE rather than a boolean: a chart that renders blank because the encoding is
856
+ * incomplete is indistinguishable from one that is broken.
857
+ */
858
+ declare function validateEncoding(encoding: XenoChartEncoding, columns: readonly XenoColumn[]): string | null;
859
+ /** The default encoding for a result — the first time column vs the first measure. */
860
+ declare function defaultEncoding(columns: readonly XenoColumn[], type?: XenoChartType): XenoChartEncoding;
861
+
862
+ /**
863
+ * The Chart controller.
864
+ *
865
+ * @module
866
+ */
867
+
868
+ /** The host seam. */
869
+ interface ChartHostBridge {
870
+ emit(portId: string, value: unknown): void;
871
+ makeId?: () => string;
872
+ }
873
+ /** Construction options. */
874
+ interface ChartControllerOptions {
875
+ host: ChartHostBridge;
876
+ initial?: Partial<ChartPanelState>;
877
+ /** Chart types this host permits. */
878
+ allowedTypes?: readonly XenoChartType[];
879
+ defaultType?: XenoChartType;
880
+ maxSeries?: number;
881
+ maxPoints?: number;
882
+ }
883
+ /** The Chart panel controller. */
884
+ declare class ChartController {
885
+ private readonly host;
886
+ private readonly makeId;
887
+ private readonly allowedTypes;
888
+ private readonly buildOptions;
889
+ private readonly listeners;
890
+ private result;
891
+ private encoding;
892
+ private highlight;
893
+ private snapshot;
894
+ /** Set once `tooDense` has been reported, so the request is not re-emitted every render. */
895
+ private densityReported;
896
+ constructor(options: ChartControllerOptions);
897
+ subscribe: (listener: () => void) => (() => void);
898
+ getState: () => ChartViewState;
899
+ private notify;
900
+ /**
901
+ * Receive a result.
902
+ *
903
+ * **Data always arrives as a result set** — never a viewport range. That is landmine §6.11: a
904
+ * chart bound to a selection cannot be saved, cannot be shared, and shows nothing the moment the
905
+ * selection moves.
906
+ */
907
+ setResult(result: XenoResultSet): void;
908
+ /** Receive an externally authored encoding (from a wired `fields` panel). */
909
+ setEncoding(encoding: XenoChartEncoding, emit?: boolean): void;
910
+ /** Patch one channel. */
911
+ patchEncoding(patch: Partial<XenoChartEncoding>): void;
912
+ /** Change the mark. Refuses a type this host disallows. */
913
+ setType(type: XenoChartType): boolean;
914
+ /** Receive a cross-filter highlight from a sibling panel. */
915
+ setHighlight(highlight: XenoChartHighlight): void;
916
+ /** The plot model, or `null` when there is nothing plottable. */
917
+ buildData(): XenoChartData | null;
918
+ /** Row ids the current highlight resolves to. */
919
+ private resolveHighlight;
920
+ /**
921
+ * Ask for an aggregated query when the result is too dense to plot honestly.
922
+ *
923
+ * **Never a silent downsample.** A thinned line is indistinguishable from a real one, so the panel
924
+ * asks the resolver to aggregate instead — and asks once per encoding/result, not every render.
925
+ */
926
+ private reportDensity;
927
+ /** A mark was clicked: emit the selection and a drill-down. */
928
+ activateMark(label: string, seriesId?: string): boolean;
929
+ /** A brush selected several categories. */
930
+ brush(labels: readonly string[]): boolean;
931
+ /**
932
+ * Ask the HOST to render an image.
933
+ *
934
+ * The panel never touches a filesystem — it declares `storage.local` and nothing else, which is
935
+ * only true because export is a request rather than a write.
936
+ */
937
+ requestExport(format: 'png' | 'svg', width?: number, height?: number): string;
938
+ /**
939
+ * Serialize.
940
+ *
941
+ * **The encoding persists.** sheets keeps its chart type in component `useState` and loses it on
942
+ * unmount, and writes a `ChartConfig` nothing ever reads — the same bug twice.
943
+ */
944
+ serialize(): ChartPanelState;
945
+ /** Restore. Does not emit. */
946
+ deserialize(state: Partial<ChartPanelState>): void;
947
+ /** Columns available to encode. */
948
+ columns(): XenoColumn[];
949
+ /** Drop listeners. */
950
+ dispose(): void;
951
+ }
952
+
953
+ /**
954
+ * The renderer SEAM.
955
+ *
956
+ * v1 renders with Chart.js — proven in sheets' 195-LOC `ChartView` — but the panel never imports it
957
+ * directly. A renderer receives the derived {@link XenoChartData} and a host element, and returns a
958
+ * disposer. That is the whole contract.
959
+ *
960
+ * The seam exists because spectra's future Canvas2D/WebGPU engine must be a **swap, not a rewrite**:
961
+ * everything expensive (deriving series, resolving labels, palette assignment, density detection)
962
+ * lives in `model.ts` and is renderer-agnostic. A second renderer implements ~40 lines.
963
+ *
964
+ * @module
965
+ */
966
+
967
+ /** What a renderer is handed. */
968
+ interface ChartRenderInput {
969
+ /** The element to draw into. */
970
+ element: HTMLElement;
971
+ /** The derived, renderer-agnostic model. */
972
+ data: XenoChartData;
973
+ /** Presentation toggles. */
974
+ options: ChartRenderOptions;
975
+ /** Row ids currently highlighted by a sibling's cross-filter. */
976
+ highlightedRowIds?: readonly string[];
977
+ /** Called when a mark is activated. */
978
+ onMarkActivate?: (label: string, seriesId?: string) => void;
979
+ }
980
+ /** Presentation toggles a renderer honours. */
981
+ interface ChartRenderOptions {
982
+ showLegend: boolean;
983
+ showGrid: boolean;
984
+ showTooltip: boolean;
985
+ stacked: boolean;
986
+ /** Line smoothing `0`–`1`. */
987
+ smoothing: number;
988
+ }
989
+ /** A renderer: draw, and hand back a disposer. */
990
+ type ChartRenderer = (input: ChartRenderInput) => () => void;
991
+ /**
992
+ * Install the renderer.
993
+ *
994
+ * @param next - The renderer, or `null` to uninstall.
995
+ *
996
+ * @example
997
+ * ```ts
998
+ * import { Chart } from 'chart.js/auto'
999
+ * setChartRenderer(createChartJsRenderer(Chart))
1000
+ * ```
1001
+ */
1002
+ declare function setChartRenderer(next: ChartRenderer | null): void;
1003
+ /** The installed renderer. */
1004
+ declare function getChartRenderer(): ChartRenderer | null;
1005
+ /**
1006
+ * Chart.js dataset shape — derived from the model so a host can wire Chart.js in a few lines
1007
+ * without the panel importing it.
1008
+ *
1009
+ * Exported rather than inlined so the mapping is testable without a DOM or a chart library.
1010
+ *
1011
+ * @param data - The derived model.
1012
+ * @param options - Presentation toggles.
1013
+ * @returns A Chart.js-compatible `{type, data, options}` config.
1014
+ */
1015
+ declare function toChartJsConfig(data: XenoChartData, options: ChartRenderOptions): Record<string, unknown>;
1016
+
1017
+ /**
1018
+ * The `PanelModule` — view wired inside the package (one React copy).
1019
+ *
1020
+ * @module
1021
+ */
1022
+
1023
+ /** Everything a renderer needs: the controller plus the resolved config. */
1024
+ interface ChartRenderContext {
1025
+ controller: ChartController;
1026
+ config: {
1027
+ options: ChartRenderOptions;
1028
+ emptyHint?: string;
1029
+ };
1030
+ }
1031
+ /** Options for {@link createChartPanel}. */
1032
+ interface CreateChartPanelOptions {
1033
+ /** Override the view. Rarely needed — mounting inside the package keeps React singular. */
1034
+ render?: (root: HTMLElement, context: ChartRenderContext) => () => void;
1035
+ }
1036
+ /**
1037
+ * Build the Chart panel module.
1038
+ *
1039
+ * @param options - Optional renderer override.
1040
+ * @returns The module.
1041
+ */
1042
+ declare function createChartPanel(options?: CreateChartPanelOptions): PanelModule;
1043
+ /** The default Chart panel module — view already wired. Register THIS. */
1044
+ declare const chartPanel: PanelModule;
1045
+
1046
+ /**
1047
+ * The `xeno.core.chart` manifest.
1048
+ *
1049
+ * Capabilities: `storage.local` only — export is a REQUEST to the host, never a write, which is what
1050
+ * keeps the capability list at one entry.
1051
+ *
1052
+ * @module
1053
+ */
1054
+
1055
+ /** The canonical manifest id. */
1056
+ declare const CHART_PANEL_ID = "xeno.core.chart";
1057
+ /** The `xeno.core.chart` manifest. */
1058
+ declare const chartManifest: PanelManifest;
1059
+
1060
+ /**
1061
+ * The plot model — `XenoResultSet` + `XenoChartEncoding` → `XenoChartData`.
1062
+ *
1063
+ * Pure, renderer-agnostic, and the whole reason the renderer is a **seam**: Chart.js consumes this,
1064
+ * and spectra's future Canvas2D/WebGPU engine consumes the same thing. Swapping the renderer must
1065
+ * never mean re-deriving the data.
1066
+ *
1067
+ * @module
1068
+ */
1069
+
1070
+ /** Options for {@link buildChartData}. */
1071
+ interface BuildChartOptions {
1072
+ /** Series ceiling. Beyond this the tail is dropped rather than rendering an unreadable legend. */
1073
+ maxSeries?: number;
1074
+ /**
1075
+ * Point ceiling. Above it the data is flagged `tooDense` and the panel requests an AGGREGATED
1076
+ * query — **never a silent downsample**, which is a lie about the data.
1077
+ */
1078
+ maxPoints?: number;
1079
+ }
1080
+ /**
1081
+ * Build the plot model.
1082
+ *
1083
+ * Two paths. A result carrying `groups` whose aggregates match the encoded measures is plotted from
1084
+ * the GROUPS; everything else is plotted from the rows. See {@link buildFromGroups} for why the row
1085
+ * path is a wrong answer for an aggregated query.
1086
+ *
1087
+ * @param result - The result set. Rows are THE PAGE — the model never invents rows the page lacks.
1088
+ * @param encoding - How columns map to channels.
1089
+ * @param options - Series and point ceilings.
1090
+ * @returns The renderer-agnostic model.
1091
+ *
1092
+ * @example
1093
+ * ```ts
1094
+ * const data = buildChartData(result, { type: 'line', x: 'day', y: ['revenue'], series: 'region' })
1095
+ * ```
1096
+ */
1097
+ declare function buildChartData(result: XenoResultSet, encoding: XenoChartEncoding, options?: BuildChartOptions): XenoChartData;
1098
+ /**
1099
+ * The row ids a mark corresponds to — what `selection` and `drillDown` are built from.
1100
+ *
1101
+ * @param result - The result the model was built from.
1102
+ * @param encoding - The encoding.
1103
+ * @param label - The category label that was clicked.
1104
+ * @param seriesId - The series, when the mark belongs to one.
1105
+ * @returns Matching row ids, in row order.
1106
+ */
1107
+ declare function rowsForMark(result: XenoResultSet, encoding: XenoChartEncoding, label: string, seriesId?: string): string[];
1108
+
1109
+ /**
1110
+ * The Chart panel view.
1111
+ *
1112
+ * Composes `@xenosystem/workbench/primitives`; the host must ensure `@xenosystem/workbench/primitives.css` is
1113
+ * present. The plot area delegates to the installed renderer (see `renderer.ts`) — this view owns
1114
+ * the chrome and the states, not the marks.
1115
+ *
1116
+ * **There is no shelf UI here.** The encoding editor is `xeno.core.fields` with chart shelves,
1117
+ * wired as a sibling panel; building a second shelf UI is exactly what sequencing chart after fields
1118
+ * was meant to avoid.
1119
+ *
1120
+ * @module
1121
+ */
1122
+
1123
+ /** Props for {@link ChartPanelView}. */
1124
+ interface ChartPanelViewProps {
1125
+ controller: ChartController;
1126
+ options?: Partial<ChartRenderOptions>;
1127
+ emptyHint?: string;
1128
+ }
1129
+ /** The Chart panel view. */
1130
+ declare function ChartPanelView({ controller, options, emptyHint }: ChartPanelViewProps): ReactNode;
1131
+
1132
+ /**
1133
+ * The `xeno.core.metrics` contract — KPI tiles over a `XenoResultSet`.
1134
+ *
1135
+ * The cheapest panel in the family: the tile primitives already landed in
1136
+ * `@xenosystem/workbench/primitives` (lifted from `xeno-post`'s kit), and the percentile math already
1137
+ * landed in `@xenosystem/data-core`. This package is the model that binds them.
1138
+ *
1139
+ * @module
1140
+ */
1141
+
1142
+ /**
1143
+ * Which direction of change is GOOD.
1144
+ *
1145
+ * Load-bearing: **a rising error rate is not good news.** A delta arrow that is green whenever the
1146
+ * number went up is worse than no arrow, because it reads as reassurance.
1147
+ */
1148
+ type DeltaDirection = 'higherIsBetter' | 'lowerIsBetter' | 'neutral';
1149
+ /** One KPI tile. */
1150
+ interface XenoMetricSpec {
1151
+ /** Stable id — the key in thresholds, alerts and `metricActivate`. */
1152
+ id: string;
1153
+ /** Display label. */
1154
+ label: string;
1155
+ /** Column to aggregate. Ignored by `count`. */
1156
+ columnId: string;
1157
+ /** The aggregate. Percentiles are R-7 / `PERCENTILE.INC`, as `@xenosystem/data-core` documents. */
1158
+ fn: XenoAggFn;
1159
+ /** Presentation. */
1160
+ format?: XenoValueFormat;
1161
+ /** Column supplying the sparkline series, when one is wanted. */
1162
+ sparklineColumnId?: string;
1163
+ /** Lucide icon name for the tile. */
1164
+ icon?: string;
1165
+ /** Which direction is good. Default `neutral` — no colour rather than a wrong colour. */
1166
+ deltaDirection?: DeltaDirection;
1167
+ /** Longer explanation, shown as a tooltip. */
1168
+ description?: string;
1169
+ }
1170
+ /** A warn/critical threshold for one metric. */
1171
+ interface XenoMetricThreshold {
1172
+ metricId: string;
1173
+ /** Crossing this raises `level: 'warn'`. */
1174
+ warn?: number;
1175
+ /** Crossing this raises `level: 'critical'`. */
1176
+ critical?: number;
1177
+ /**
1178
+ * Which side of the number is bad. Defaults from the metric's `deltaDirection`:
1179
+ * `lowerIsBetter` ⇒ `above`, `higherIsBetter` ⇒ `below`.
1180
+ */
1181
+ breach?: 'above' | 'below';
1182
+ }
1183
+ /** Threshold severity, mirroring the profiler's bottleneck model. */
1184
+ type XenoAlertLevel = 'ok' | 'warn' | 'critical';
1185
+ /** What the panel emits when a threshold is crossed. */
1186
+ interface XenoMetricAlert {
1187
+ metricId: string;
1188
+ level: XenoAlertLevel;
1189
+ value: number;
1190
+ threshold: number;
1191
+ }
1192
+ /** A tile, computed. */
1193
+ interface XenoMetricValue {
1194
+ spec: XenoMetricSpec;
1195
+ /** The aggregate, or `null` when the sample supports no answer. */
1196
+ value: XenoValue;
1197
+ /** Formatted for display. */
1198
+ display: string;
1199
+ /** The comparison-period value, when a comparison result was supplied. */
1200
+ previous?: XenoValue;
1201
+ /** Signed fractional change vs the previous period (`0.12` = +12%). */
1202
+ delta?: number;
1203
+ /**
1204
+ * Whether the change is GOOD, BAD or neither — derived from `deltaDirection`, never from the sign.
1205
+ */
1206
+ deltaTone: 'good' | 'bad' | 'neutral';
1207
+ /** Threshold state. */
1208
+ level: XenoAlertLevel;
1209
+ /** The sparkline series, oldest first. */
1210
+ spark?: number[];
1211
+ /**
1212
+ * `true` when the underlying result is `status: 'partial'`.
1213
+ *
1214
+ * A partial result means "these rows are real, the measures are still landing". Rendering it as
1215
+ * zero is a lie the user cannot detect; the tile shows the hint instead.
1216
+ */
1217
+ partial?: boolean;
1218
+ }
1219
+ /** A named time range offered in the toolbar. */
1220
+ interface XenoMetricRange {
1221
+ /** Stable id. */
1222
+ id: string;
1223
+ /** Display label (`'7d'`). */
1224
+ label: string;
1225
+ /** How far back. */
1226
+ unit: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
1227
+ /** How many units. Positive; the panel emits it as a negative relative filter. */
1228
+ n: number;
1229
+ }
1230
+ /**
1231
+ * The default ranges.
1232
+ *
1233
+ * **Config, not constants.** One product hardcoded 7/30/90 as tabs precisely because the filter
1234
+ * vocabulary had no `relative` primitive; now that it does, these are data.
1235
+ */
1236
+ declare const DEFAULT_RANGES: readonly XenoMetricRange[];
1237
+ /** The panel's serialized state. */
1238
+ interface MetricsPanelState {
1239
+ /** The active range id. */
1240
+ rangeId?: string;
1241
+ /** Per-metric threshold overrides the user set. */
1242
+ thresholds?: XenoMetricThreshold[];
1243
+ }
1244
+ /** What the controller exposes to its view. */
1245
+ interface MetricsViewState {
1246
+ /** Computed tiles, in config order. */
1247
+ metrics: XenoMetricValue[];
1248
+ /** The active range. */
1249
+ rangeId: string | null;
1250
+ /** Ranges to offer. */
1251
+ ranges: readonly XenoMetricRange[];
1252
+ /** No result yet. */
1253
+ empty: boolean;
1254
+ /** The result is `partial`. */
1255
+ partial: boolean;
1256
+ /** The result carried an error. */
1257
+ errorMessage: string | null;
1258
+ }
1259
+ /** Resolve which direction is bad for a threshold. */
1260
+ declare function breachSideOf(threshold: XenoMetricThreshold, spec: XenoMetricSpec | undefined): 'above' | 'below';
1261
+ /**
1262
+ * The tone a delta should render with.
1263
+ *
1264
+ * Derived from `deltaDirection`, **never from the sign**. `neutral` metrics get no tone at all,
1265
+ * because a colour that means nothing still reads as a judgement.
1266
+ */
1267
+ declare function deltaToneOf(delta: number | undefined, direction?: DeltaDirection): XenoMetricValue['deltaTone'];
1268
+
1269
+ /**
1270
+ * The Metrics controller — result in, tiles out.
1271
+ *
1272
+ * Every number comes from `@xenosystem/data-core`'s `applyAggregate`, so a percentile here is the same
1273
+ * percentile a table's footer shows (R-7 / `PERCENTILE.INC`). Definitions differ by a whole element
1274
+ * on small samples, and a dashboard whose KPI disagrees with its own table is worse than one with no
1275
+ * KPI at all.
1276
+ *
1277
+ * @module
1278
+ */
1279
+
1280
+ /** The host seam. */
1281
+ interface MetricsHostBridge {
1282
+ emit(portId: string, value: unknown): void;
1283
+ makeId?: () => string;
1284
+ }
1285
+ /** Construction options. */
1286
+ interface MetricsControllerOptions {
1287
+ host: MetricsHostBridge;
1288
+ /** The tiles to render. */
1289
+ metrics?: XenoMetricSpec[];
1290
+ /** Ranges to offer. Default 7/30/90 days. */
1291
+ ranges?: readonly XenoMetricRange[];
1292
+ /** Which time column a range filter applies to. */
1293
+ rangeColumnId?: string;
1294
+ /** IANA zone for the range filter. Default `'UTC'`. */
1295
+ timeZone?: string;
1296
+ /** Restored state. */
1297
+ initial?: Partial<MetricsPanelState>;
1298
+ }
1299
+ /** The Metrics panel controller. */
1300
+ declare class MetricsController {
1301
+ private readonly host;
1302
+ private readonly listeners;
1303
+ private readonly ranges;
1304
+ private readonly rangeColumnId?;
1305
+ private readonly timeZone;
1306
+ private specs;
1307
+ private result;
1308
+ private comparison;
1309
+ private thresholds;
1310
+ private rangeId;
1311
+ /** Last emitted level per metric, so an alert fires on TRANSITION and not on every render. */
1312
+ private readonly lastLevels;
1313
+ private snapshot;
1314
+ constructor(options: MetricsControllerOptions);
1315
+ subscribe: (listener: () => void) => (() => void);
1316
+ getState: () => MetricsViewState;
1317
+ private notify;
1318
+ /** Receive the current-period result. */
1319
+ setResult(result: XenoResultSet): void;
1320
+ /** Receive the prior-period result, which is what makes a delta possible. */
1321
+ setComparison(result: XenoResultSet | null): void;
1322
+ /** Receive thresholds. */
1323
+ setThresholds(thresholds: XenoMetricThreshold[]): void;
1324
+ /** Replace the tile set. */
1325
+ setMetrics(specs: XenoMetricSpec[]): void;
1326
+ /** Compute every tile. */
1327
+ compute(): XenoMetricValue[];
1328
+ private aggregate;
1329
+ private sparkFor;
1330
+ /** The threshold state of one metric. */
1331
+ levelFor(spec: XenoMetricSpec, value: XenoValue): XenoAlertLevel;
1332
+ /**
1333
+ * Emit an alert for every metric whose level CHANGED.
1334
+ *
1335
+ * On transition, not on every render: a dashboard that re-emits `critical` sixty times a second
1336
+ * is a dashboard whose alerts get muted.
1337
+ */
1338
+ private emitAlerts;
1339
+ /**
1340
+ * Select a time range, emitting a `queryRequest` carrying a RELATIVE filter.
1341
+ *
1342
+ * Relative, not two absolute timestamps: a saved dashboard must still mean "the last 7 days"
1343
+ * tomorrow.
1344
+ */
1345
+ setRange(rangeId: string): boolean;
1346
+ /** A tile was clicked — the drill-down trigger. */
1347
+ activate(metricId: string): boolean;
1348
+ /** Serialize. Range and thresholds — never a result. */
1349
+ serialize(): MetricsPanelState;
1350
+ /** Restore. Does not emit. */
1351
+ deserialize(state: Partial<MetricsPanelState>): void;
1352
+ /** Drop listeners. */
1353
+ dispose(): void;
1354
+ }
1355
+
1356
+ /**
1357
+ * The `PanelModule` — view wired inside the package (one React copy).
1358
+ *
1359
+ * @module
1360
+ */
1361
+
1362
+ /** Everything a renderer needs: the controller plus the resolved config. */
1363
+ interface MetricsRenderContext {
1364
+ controller: MetricsController;
1365
+ config: {
1366
+ layout: 'grid' | 'row' | 'list';
1367
+ columns: number;
1368
+ sparkline: boolean;
1369
+ showDelta: boolean;
1370
+ emptyHint?: string;
1371
+ partialHint: string;
1372
+ };
1373
+ }
1374
+ /** Options for {@link createMetricsPanel}. */
1375
+ interface CreateMetricsPanelOptions {
1376
+ /** Override the renderer. Rarely needed — mounting inside the package keeps React singular. */
1377
+ render?: (root: HTMLElement, context: MetricsRenderContext) => () => void;
1378
+ }
1379
+ /**
1380
+ * Build the Metrics panel module.
1381
+ *
1382
+ * @param options - Optional renderer override.
1383
+ * @returns The module.
1384
+ */
1385
+ declare function createMetricsPanel(options?: CreateMetricsPanelOptions): PanelModule;
1386
+ /** The default Metrics panel module — view already wired. Register THIS. */
1387
+ declare const metricsPanel: PanelModule;
1388
+
1389
+ /**
1390
+ * The `xeno.core.metrics` manifest.
1391
+ *
1392
+ * The cheapest panel in the family and a LEAF — it consumes a result and emits events, so nothing
1393
+ * downstream waits on it. Capabilities: `storage.local` only.
1394
+ *
1395
+ * @module
1396
+ */
1397
+
1398
+ /** The canonical manifest id. */
1399
+ declare const METRICS_PANEL_ID = "xeno.core.metrics";
1400
+ /** The `xeno.core.metrics` manifest. */
1401
+ declare const metricsManifest: PanelManifest;
1402
+
1403
+ /**
1404
+ * The Metrics panel view — tiles, deltas, sparklines, threshold tone.
1405
+ *
1406
+ * Composes `@xenosystem/workbench/primitives` (`StatTile`, `Badge`, `SegmentedControl`, `Sparkline`); the
1407
+ * host must ensure `@xenosystem/workbench/primitives.css` is present. Nothing here is a local re-cut —
1408
+ * the sparkline moved into the primitives so there is exactly one.
1409
+ *
1410
+ * @module
1411
+ */
1412
+
1413
+ /** Props for {@link MetricsPanelView}. */
1414
+ interface MetricsPanelViewProps {
1415
+ controller: MetricsController;
1416
+ layout?: 'grid' | 'row' | 'list';
1417
+ columns?: number;
1418
+ sparkline?: boolean;
1419
+ showDelta?: boolean;
1420
+ emptyHint?: string;
1421
+ partialHint?: string;
1422
+ }
1423
+ /** The Metrics panel view. */
1424
+ declare function MetricsPanelView({ controller, layout, columns, sparkline, showDelta, emptyHint, partialHint, }: MetricsPanelViewProps): ReactNode;
1425
+
1426
+ /**
1427
+ * The `xeno.core.fields` contract — the query AUTHOR.
1428
+ *
1429
+ * This is the panel that retires three surfaces at once: sheets' mock pivot shelves (three dashed
1430
+ * "Drag fields here" rectangles whose `computePivot` is never called), notes' inline filter builder,
1431
+ * and architect's *nonexistent* schedule field editor.
1432
+ *
1433
+ * ## The design bet: shelves are CONFIG
1434
+ *
1435
+ * One panel serves three consumers because a "shelf" is nothing but a named, typed bucket of column
1436
+ * references. Pivot wants `rows|columns|values|filters`; chart wants `x|y|series|color|size`; table
1437
+ * wants `columns|filters|sorts`. Those differ in *labels, arity and accepted roles* — not in
1438
+ * behaviour. Encoding them as config is what stops this becoming three panels that drift.
1439
+ *
1440
+ * @module
1441
+ */
1442
+
1443
+ /** What a shelf accepts and how many. */
1444
+ interface XenoShelfSpec {
1445
+ /** Stable id — also the key in {@link FieldsPanelState.shelves}. */
1446
+ id: string;
1447
+ /** Display label. */
1448
+ label: string;
1449
+ /**
1450
+ * Which roles may land here. A `measure` shelf refuses a dimension, so a user cannot build a
1451
+ * query the source will reject.
1452
+ */
1453
+ accepts: XenoColumnRole[];
1454
+ /** Maximum entries. `1` makes it a single-slot shelf (chart's `x`); omit for unlimited. */
1455
+ max?: number;
1456
+ /** Entries carry an aggregate function (a `values` shelf). */
1457
+ aggregated?: boolean;
1458
+ /** Time-bucket granularity is offered for `time` columns dropped here. */
1459
+ bucketed?: boolean;
1460
+ /** Help text. */
1461
+ hint?: string;
1462
+ }
1463
+ /** The three shelf layouts v1 ships, as data. */
1464
+ declare const SHELF_PRESETS: Readonly<Record<'pivot' | 'chart' | 'table', XenoShelfSpec[]>>;
1465
+ /** One column placed on a shelf. */
1466
+ interface XenoShelfEntry {
1467
+ /** The column. */
1468
+ columnId: string;
1469
+ /** Aggregate function, on an `aggregated` shelf. */
1470
+ fn?: XenoAggFn;
1471
+ /** Roll-up function for grand totals. Defaults to `fn` — never to sum. */
1472
+ totalFn?: XenoAggFn;
1473
+ /** Time bucket, on a `bucketed` shelf with a temporal column. */
1474
+ granularity?: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
1475
+ /** Sort direction, on a `sorts` shelf. */
1476
+ direction?: 'asc' | 'desc';
1477
+ /** Display label override. */
1478
+ label?: string;
1479
+ /**
1480
+ * `true` when the panel placed this entry by role inference rather than the user.
1481
+ *
1482
+ * Auto-assignment is a **suggestion, not a decision**: a suggested entry renders differently and
1483
+ * is cleared wholesale the moment the user touches the shelf, so the panel never silently decides
1484
+ * what a query means.
1485
+ */
1486
+ suggested?: boolean;
1487
+ }
1488
+ /** Live per-column statistics derived from a result — cardinality and nulls. */
1489
+ interface XenoFieldStat {
1490
+ columnId: string;
1491
+ /** Distinct values seen on the page. */
1492
+ distinct: number;
1493
+ /** Empty cells on the page. */
1494
+ nulls: number;
1495
+ /** Rows the statistic was computed over. */
1496
+ sampled: number;
1497
+ }
1498
+ /** The panel's serialized state. */
1499
+ interface FieldsPanelState {
1500
+ /** Entries per shelf id. */
1501
+ shelves: Record<string, XenoShelfEntry[]>;
1502
+ /** The filter tree. */
1503
+ filters?: XenoFilterGroup;
1504
+ /** Which preset the shelves came from. */
1505
+ preset: 'pivot' | 'chart' | 'table' | 'custom';
1506
+ }
1507
+ /** What the controller exposes to its view. */
1508
+ interface FieldsViewState {
1509
+ /** The catalog's columns for the active source, or `[]` before one lands. */
1510
+ columns: XenoColumn[];
1511
+ /** The shelf specs in play. */
1512
+ shelves: XenoShelfSpec[];
1513
+ /** Entries per shelf. */
1514
+ entries: Record<string, XenoShelfEntry[]>;
1515
+ /** The filter tree. */
1516
+ filters: XenoFilterGroup;
1517
+ /** Live stats, keyed by column id. */
1518
+ stats: Record<string, XenoFieldStat>;
1519
+ /** Aggregate functions the SOURCE can actually do. Empty ⇒ everything is local. */
1520
+ allowedAggregates: XenoAggFn[];
1521
+ /** Filter operators the source understands. */
1522
+ allowedFilterOps: string[];
1523
+ /**
1524
+ * Set when the catalog was inferred from a sample.
1525
+ *
1526
+ * `fields` must WARN that a column may be absent later — a shape derived from the first N rows is
1527
+ * a guess, and building a query on a guessed column is how a dashboard breaks next Tuesday.
1528
+ */
1529
+ sampleWarning: string | null;
1530
+ /** A query is pending an explicit Apply (only when `autoApply` is off). */
1531
+ dirty: boolean;
1532
+ /** The column the user is hovering/focused, mirrored on `fieldFocus`. */
1533
+ focusedColumnId: string | null;
1534
+ }
1535
+ /** A calculated-field request. The HOST validates and compiles it — never the panel. */
1536
+ interface XenoCalculatedFieldRequest {
1537
+ requestId: string;
1538
+ /** Proposed column id. */
1539
+ columnId: string;
1540
+ label: string;
1541
+ /** The expression, verbatim. Host-parsed; the panel never evaluates it. */
1542
+ expression: string;
1543
+ /** The user's declared result type, for the host to check against. */
1544
+ type?: XenoColumn['type'];
1545
+ }
1546
+ /** Every aggregate the family knows, in menu order. */
1547
+ declare const ALL_AGGREGATES: readonly XenoAggFn[];
1548
+ /**
1549
+ * The default aggregate for a column.
1550
+ *
1551
+ * A measure sums, a dimension counts. Anything else counts, because counting is the one aggregate
1552
+ * that is meaningful for every type.
1553
+ */
1554
+ declare function defaultAggregateFor(column: XenoColumn | undefined): XenoAggFn;
1555
+ /**
1556
+ * Which aggregates may be offered for a column, gated by what the SOURCE can push down.
1557
+ *
1558
+ * **Never offer a pushdown the source cannot do.** A menu that lists `p95` against a REST endpoint
1559
+ * produces a query the resolver must silently run locally over one page — which is a different,
1560
+ * wrong number.
1561
+ *
1562
+ * @param column - The column.
1563
+ * @param sourceAggregates - `capabilities.aggregates`, or empty when the source declares none.
1564
+ * @returns The offerable list.
1565
+ */
1566
+ declare function offerableAggregates(column: XenoColumn | undefined, sourceAggregates?: readonly string[]): XenoAggFn[];
1567
+ /** The shelf a column belongs on by role — a SUGGESTION, never a silent decision. */
1568
+ declare function suggestedShelfFor(column: XenoColumn, shelves: readonly XenoShelfSpec[]): XenoShelfSpec | undefined;
1569
+ /** An empty filter group. An empty group PASSES — a half-authored filter must not blank the table. */
1570
+ declare function emptyFilterGroup(): XenoFilterGroup;
1571
+ /** Operators offerable for a column type, intersected with what the source understands. */
1572
+ declare function offerableFilterOps(column: XenoColumn | undefined, sourceOps?: readonly string[]): string[];
1573
+ /** Read the columns of the active source out of a catalog. */
1574
+ declare function columnsOf(catalog: XenoSchemaCatalog | null, sourceId?: string): XenoColumn[];
1575
+ /** Narrow a filter-tree member to a leaf. */
1576
+ declare function isLeafFilter(f: XenoFilter | XenoFilterGroup): f is XenoFilter;
1577
+
1578
+ /**
1579
+ * The Fields controller — shelves in, `XenoQuery` out.
1580
+ *
1581
+ * Emitting a query is **the panel's whole reason to exist**, so the interesting decisions are all
1582
+ * about when *not* to emit: never on a structurally unchanged query, never mid-edit when `autoApply`
1583
+ * is off, and never a query the source cannot run.
1584
+ *
1585
+ * @module
1586
+ */
1587
+
1588
+ /** The host seam. */
1589
+ interface FieldsHostBridge {
1590
+ emit(portId: string, value: unknown): void;
1591
+ makeId?: () => string;
1592
+ }
1593
+ /** Construction options. */
1594
+ interface FieldsControllerOptions {
1595
+ host: FieldsHostBridge;
1596
+ initial?: Partial<FieldsPanelState>;
1597
+ /** Which shelf layout to serve. Default `'table'`. */
1598
+ preset?: 'pivot' | 'chart' | 'table';
1599
+ /** Custom shelves, overriding the preset. */
1600
+ shelves?: XenoShelfSpec[];
1601
+ /** Emit on every edit. Default `true`; when `false` the user presses Apply. */
1602
+ autoApply?: boolean;
1603
+ /** Rows per page in the emitted query. */
1604
+ pageSize?: number;
1605
+ }
1606
+ /** The Fields panel controller. */
1607
+ declare class FieldsController {
1608
+ private readonly host;
1609
+ private readonly makeId;
1610
+ private readonly autoApply;
1611
+ private readonly pageSize;
1612
+ private readonly listeners;
1613
+ private shelves;
1614
+ private preset;
1615
+ private entries;
1616
+ private filters;
1617
+ private catalog;
1618
+ private sourceId;
1619
+ private stats;
1620
+ private focusedColumnId;
1621
+ private dirty;
1622
+ private lastEmitted;
1623
+ private snapshot;
1624
+ constructor(options: FieldsControllerOptions);
1625
+ subscribe: (listener: () => void) => (() => void);
1626
+ getState: () => FieldsViewState;
1627
+ private notify;
1628
+ /** Receive a schema catalog. */
1629
+ setCatalog(catalog: XenoSchemaCatalog, sourceId?: string): void;
1630
+ /**
1631
+ * Receive a result — the source of LIVE per-column statistics.
1632
+ *
1633
+ * Cardinality and null counts come from the page, not from the schema, which is why they update on
1634
+ * every result rather than every catalog.
1635
+ */
1636
+ setResult(result: XenoResultSet): void;
1637
+ /** Adopt an externally authored query, so a user can edit what a table already shows. */
1638
+ setQuery(query: XenoQuery): void;
1639
+ /** The shelf spec with this id. */
1640
+ shelf(id: string): XenoShelfSpec | undefined;
1641
+ /**
1642
+ * Place a column on a shelf.
1643
+ *
1644
+ * Refuses when the shelf does not accept the column's role or is full — a shelf that silently
1645
+ * accepts anything is a shelf that builds queries the source rejects.
1646
+ *
1647
+ * @returns `true` when the column was placed.
1648
+ */
1649
+ addToShelf(shelfId: string, columnId: string, at?: number): boolean;
1650
+ /** Remove a column from a shelf. */
1651
+ removeFromShelf(shelfId: string, columnId: string): boolean;
1652
+ /** Move an entry within a shelf. */
1653
+ reorderShelf(shelfId: string, columnId: string, toIndex: number): boolean;
1654
+ /**
1655
+ * Change an entry's aggregate.
1656
+ *
1657
+ * Refuses a function the source cannot run — offering `p95` against a source with no percentile
1658
+ * support produces a silently different number.
1659
+ */
1660
+ setAggregate(shelfId: string, columnId: string, fn: XenoAggFn): boolean;
1661
+ /** Change an entry's time bucket. */
1662
+ setGranularity(shelfId: string, columnId: string, granularity: XenoShelfEntry['granularity']): void;
1663
+ /** Flip a sort direction. */
1664
+ toggleDirection(shelfId: string, columnId: string): void;
1665
+ /**
1666
+ * Fill empty shelves from column roles — a SUGGESTION the user can override.
1667
+ *
1668
+ * Every entry it creates is marked `suggested`, renders differently, and disappears the moment the
1669
+ * user touches that shelf. Auto-assignment that silently decides what a query means is how a
1670
+ * dashboard shows a number nobody chose.
1671
+ *
1672
+ * @returns How many entries were suggested.
1673
+ */
1674
+ autoAssign(): number;
1675
+ private clearSuggestions;
1676
+ /** Replace the whole filter tree. */
1677
+ setFilters(filters: XenoFilterGroup): void;
1678
+ /**
1679
+ * Add a leaf filter to a group, addressed by its path from the root.
1680
+ *
1681
+ * @param path - Indices into nested `filters` arrays; `[]` is the root group.
1682
+ * @param filter - The condition.
1683
+ */
1684
+ addFilter(path: readonly number[], filter: XenoFilter): boolean;
1685
+ /** Add a nested group (an and/or branch). */
1686
+ addFilterGroup(path: readonly number[], combinator?: 'and' | 'or'): boolean;
1687
+ /** Update a leaf in place. */
1688
+ updateFilter(path: readonly number[], index: number, patch: Partial<XenoFilter>): boolean;
1689
+ /** Remove a member (leaf or group). */
1690
+ removeFilter(path: readonly number[], index: number): boolean;
1691
+ /** Flip a group between AND and OR. */
1692
+ setCombinator(path: readonly number[], combinator: 'and' | 'or'): boolean;
1693
+ private groupAt;
1694
+ /**
1695
+ * Report the focused column, so a table can highlight the matching one.
1696
+ *
1697
+ * A direct lift of the Inspector's `fieldFocus` — the same idea, the same port shape, so a host
1698
+ * that already wired one gets the other for free.
1699
+ */
1700
+ focusColumn(columnId: string | null): void;
1701
+ /** Ask the host to validate and compile a calculated field. The panel never evaluates one. */
1702
+ requestCalculatedField(label: string, expression: string, type?: XenoColumn['type']): string;
1703
+ /**
1704
+ * The query the current shelves describe.
1705
+ *
1706
+ * Pivot shelves produce a `pivot` block; everything else produces `groupBy` + `aggregates`, which
1707
+ * is what makes one panel serve a cross-tab and a flat table without a second code path.
1708
+ */
1709
+ buildQuery(): XenoQuery;
1710
+ /** Emit the query, unless it is structurally unchanged. */
1711
+ apply(force?: boolean): XenoQuery | null;
1712
+ private afterEdit;
1713
+ private columnById;
1714
+ /** Serialize. Shelves and filters — never a catalog, never a result. */
1715
+ serialize(): FieldsPanelState;
1716
+ /** Restore. Does not emit. */
1717
+ deserialize(state: Partial<FieldsPanelState>): void;
1718
+ /** Drop listeners. */
1719
+ dispose(): void;
1720
+ }
1721
+
1722
+ /**
1723
+ * The `PanelModule` — view wired inside the package (one React copy).
1724
+ *
1725
+ * @module
1726
+ */
1727
+
1728
+ /** Everything a renderer needs: the controller plus the resolved config. */
1729
+ interface FieldsRenderContext {
1730
+ controller: FieldsController;
1731
+ config: {
1732
+ showStats: boolean;
1733
+ manualApply: boolean;
1734
+ };
1735
+ }
1736
+ /** Options for {@link createFieldsPanel}. */
1737
+ interface CreateFieldsPanelOptions {
1738
+ /** Override the renderer. Rarely needed — mounting inside the package keeps React singular. */
1739
+ render?: (root: HTMLElement, context: FieldsRenderContext) => () => void;
1740
+ }
1741
+ /**
1742
+ * Build the Fields panel module.
1743
+ *
1744
+ * @param options - Optional renderer override.
1745
+ * @returns The module.
1746
+ */
1747
+ declare function createFieldsPanel(options?: CreateFieldsPanelOptions): PanelModule;
1748
+ /** The default Fields panel module — view already wired. Register THIS. */
1749
+ declare const fieldsPanel: PanelModule;
1750
+
1751
+ /**
1752
+ * The `xeno.core.fields` manifest.
1753
+ *
1754
+ * Capabilities: `storage.local` only. The panel authors a query; the connectors panel runs it.
1755
+ *
1756
+ * @module
1757
+ */
1758
+
1759
+ /** The canonical manifest id. */
1760
+ declare const FIELDS_PANEL_ID = "xeno.core.fields";
1761
+ /** The `xeno.core.fields` manifest. */
1762
+ declare const fieldsManifest: PanelManifest;
1763
+
1764
+ /**
1765
+ * The Fields panel view — column list, shelves, filter builder.
1766
+ *
1767
+ * Composes `@xenosystem/workbench/primitives`; the host must ensure
1768
+ * `@xenosystem/workbench/primitives.css` is present.
1769
+ *
1770
+ * @module
1771
+ */
1772
+
1773
+ /** Props for {@link FieldsPanelView}. */
1774
+ interface FieldsPanelViewProps {
1775
+ controller: FieldsController;
1776
+ /** Render live cardinality/null counts. */
1777
+ showStats?: boolean;
1778
+ /** Show an explicit Apply (config `autoApply: false`). */
1779
+ manualApply?: boolean;
1780
+ }
1781
+ /** The Fields panel view. */
1782
+ declare function FieldsPanelView({ controller, showStats, manualApply, }: FieldsPanelViewProps): ReactNode;
1783
+
1784
+ /**
1785
+ * The Connectors controller — the data family's **resolver**.
1786
+ *
1787
+ * Owns connections, the credential lifecycle, pushdown negotiation, the single-flight cache, refresh
1788
+ * timers, and the typed error surface. Every log line leaves through the SDK's redacting sink, so a
1789
+ * credential cannot reach a log even by accident.
1790
+ *
1791
+ * @module
1792
+ */
1793
+
1794
+ /** The host seam. */
1795
+ interface ConnectorsHostBridge {
1796
+ emit(portId: string, value: unknown): void;
1797
+ makeId?: () => string;
1798
+ /** Structured logging. Wrapped in a redacting sink before use — never called directly. */
1799
+ log?: LogSink;
1800
+ /** Executes the pushed half of a query. Supplied by the host; the panel never opens a socket. */
1801
+ execute?: (input: ConnectorExecuteInput) => Promise<ConnectorExecuteOutput>;
1802
+ /** Wall clock, injectable for tests. */
1803
+ now?: () => number;
1804
+ /** Timer seam, injectable for tests. */
1805
+ setTimer?: (fn: () => void, ms: number) => unknown;
1806
+ clearTimer?: (handle: unknown) => void;
1807
+ }
1808
+ /** Construction options. */
1809
+ interface ConnectorsControllerOptions {
1810
+ host: ConnectorsHostBridge;
1811
+ initial?: Partial<ConnectorsPanelState>;
1812
+ /** What the substrate can do. Drives honest degradation. */
1813
+ facilities?: SubstrateFacilities;
1814
+ defaultPageSize?: number;
1815
+ maxRows?: number;
1816
+ cacheTtlMs?: number;
1817
+ /** Whether a panel-authored `raw` query is permitted. Default `false`. */
1818
+ allowRawQueries?: boolean;
1819
+ }
1820
+ /** The Connectors panel controller / resolver. */
1821
+ declare class ConnectorsController {
1822
+ private readonly host;
1823
+ private readonly makeId;
1824
+ private readonly log;
1825
+ private readonly now;
1826
+ private readonly setTimer;
1827
+ private readonly clearTimer;
1828
+ private readonly facilities;
1829
+ private readonly defaultPageSize;
1830
+ private readonly maxRows;
1831
+ private readonly cacheTtlMs;
1832
+ private readonly allowRawQueries;
1833
+ private readonly listeners;
1834
+ private connections;
1835
+ private statuses;
1836
+ private lastQueries;
1837
+ private selectedId;
1838
+ private form;
1839
+ /** Newest outstanding request per `(connectionId, requestorPanelId)`. */
1840
+ private readonly outstanding;
1841
+ /** In-flight executions, keyed by cache key — the single-flight map. */
1842
+ private readonly inFlight;
1843
+ private readonly cache;
1844
+ /** One timer per connection. */
1845
+ private readonly timers;
1846
+ /** Connections whose refresh is currently running — the re-entrancy guard. */
1847
+ private readonly refreshing;
1848
+ /** Last good result per connection, so a failed refresh can keep serving. */
1849
+ private readonly lastGood;
1850
+ /**
1851
+ * Introspected schema per connection — the catalog this panel PUBLISHES.
1852
+ *
1853
+ * Not persisted: a schema is host knowledge, re-pushed on load. A cached one written into `.xapp`
1854
+ * would outlive the table it describes.
1855
+ */
1856
+ private readonly schemas;
1857
+ /** Monotonic catalog revision, so `fields` re-reads without polling. */
1858
+ private catalogRev;
1859
+ /** Connections currently being read as a SQL source — the recursion guard for `sourceRows`. */
1860
+ private readonly resolvingSources;
1861
+ private hidden;
1862
+ private snapshot;
1863
+ constructor(options: ConnectorsControllerOptions);
1864
+ subscribe: (listener: () => void) => (() => void);
1865
+ getState: () => ConnectorsViewState;
1866
+ private notify;
1867
+ private stateOf;
1868
+ /**
1869
+ * Add or replace a connection.
1870
+ *
1871
+ * Health is **always rebuilt**, never taken from the input — a persisted "healthy" is a lie, and a
1872
+ * caller passing one must not be able to install it.
1873
+ */
1874
+ addConnection(connection: XenoConnection, emit?: boolean): void;
1875
+ /** Remove a connection, clearing its timer, cache and in-flight work. */
1876
+ removeConnection(id: string): boolean;
1877
+ /**
1878
+ * Receive an introspected schema from the host and **publish the catalog**.
1879
+ *
1880
+ * **Why this exists.** The `catalog` output port was declared, `introspect` documented "re-read
1881
+ * the schema and emit a catalog", and nothing in the package ever emitted on it — `introspect`
1882
+ * only emitted `sourceChanged` and deferred to "a host flow". But **a host cannot emit on a
1883
+ * panel's output port**, so `connectors.catalog → fields.catalog` — step one of `fields`' own
1884
+ * INTEGRATION.md — had no producer and `fields` was unusable in a builder.
1885
+ *
1886
+ * The division of labour that fixes it, unchanged from the rest of the family: **the host does
1887
+ * the IO, the panel owns the aggregation.** The host answers an `introspect` ask by pushing one
1888
+ * `XenoSourceSchema` in here; the panel merges it across connections, bumps `rev`, and emits the
1889
+ * catalog itself.
1890
+ *
1891
+ * @param schema - One source's schema, or a whole catalog to install at once.
1892
+ * @returns Whether a catalog was emitted.
1893
+ */
1894
+ setSchema(schema: XenoSourceSchema | XenoSchemaCatalog): boolean;
1895
+ /** The catalog as it currently stands. */
1896
+ catalog(): XenoSchemaCatalog;
1897
+ /** Publish it. */
1898
+ private emitCatalog;
1899
+ /** The connection with this id. */
1900
+ connection(id: string): XenoConnection | undefined;
1901
+ /** Select a connection in the UI. */
1902
+ select(id: string | null): void;
1903
+ /** The host asks for a credential. The panel renders the descriptor; it knows no platform names. */
1904
+ requireCredential(spec: XenoCredentialFormSpec): void;
1905
+ /**
1906
+ * Submit a credential form.
1907
+ *
1908
+ * The values leave immediately and are **cleared from local state right here** — not in a
1909
+ * `.then()`, not on unmount. `submit_credential` is declared `agentVisible: false`, so an agent can
1910
+ * neither read this form nor replay it.
1911
+ */
1912
+ submitCredential(values: Record<string, string | number | boolean>): boolean;
1913
+ /** Abandon the form. Values are dropped; nothing is emitted. */
1914
+ cancelCredential(): void;
1915
+ /**
1916
+ * The host's answer to a credential flow.
1917
+ *
1918
+ * **Never carries the values back.** A new `credentialRefRev` invalidates every cached result for
1919
+ * that connection, so a re-auth cannot serve data fetched with the old identity.
1920
+ */
1921
+ credentialChanged(result: XenoCredentialResult): void;
1922
+ /** A host-pushed health change. */
1923
+ healthChanged(connectionId: string, status: XenoConnectionStatus): void;
1924
+ /**
1925
+ * Split a query into what the source can run and what must run locally.
1926
+ *
1927
+ * The split is echoed in `result.stats` so a user can see *why* a filter was slow — a resolver that
1928
+ * silently runs everything locally looks identical to one that pushes everything down, right up
1929
+ * until the row count matters.
1930
+ *
1931
+ * @param query - The full query.
1932
+ * @param connection - The target connection.
1933
+ * @returns The split, with op names for telemetry.
1934
+ */
1935
+ splitQuery(query: XenoQuery, connection: XenoConnection): PushdownSplit;
1936
+ /** The effective row cap: the smallest of query, source ceiling and host budget. */
1937
+ clampLimit(query: XenoQuery, connection: XenoConnection): number;
1938
+ /** Cache key: connection + credential revision + the pushed query. */
1939
+ private cacheKey;
1940
+ /** Drop every cached result for a connection. */
1941
+ invalidateCache(connectionId: string): void;
1942
+ /**
1943
+ * Resolve a query.
1944
+ *
1945
+ * The whole of §5 lives here: per-`(connection, requestor)` correlation, the pushdown split,
1946
+ * single-flight, the clamped-limit echo, honest `page.total`, and keeping the last good result
1947
+ * when a refresh fails.
1948
+ *
1949
+ * @param request - The incoming query.
1950
+ * @returns The result set (also emitted on `result`).
1951
+ */
1952
+ resolve(request: XenoConnectorQueryRequest): Promise<XenoResultSet>;
1953
+ /**
1954
+ * The number of rows the source must return for this query to be answerable.
1955
+ *
1956
+ * **Not the same as the page size**, and conflating them is a real bug: when a source cannot push
1957
+ * `limit` down, the page is cut *locally*, so trimming the fetch to one page's worth leaves
1958
+ * nothing for the local cut to find and **every page after the first comes back empty**. A source
1959
+ * that pages for itself is asked for exactly one page; a source that cannot is asked for
1960
+ * everything up to the end of the requested window, capped by the row ceiling.
1961
+ *
1962
+ * @param split - The pushdown split.
1963
+ * @param limit - The clamped page size.
1964
+ * @returns The row budget, and whether the source paged for us.
1965
+ */
1966
+ private rowBudget;
1967
+ /** Run the pushed half through the host, then the local half through the pipeline. */
1968
+ private execute;
1969
+ /**
1970
+ * Another connection's rows, as plain objects — the in-sandbox SQL bridge's source of data.
1971
+ *
1972
+ * Reuses the ordinary resolution path, so a DuckDB query over a REST source gets that source's
1973
+ * cache, its credential and its error handling rather than a second, subtly different resolver.
1974
+ *
1975
+ * @param connectionId - The source to read.
1976
+ * @returns Its rows, or `null` when it is unknown or failed.
1977
+ */
1978
+ sourceRows(connectionId: string): Promise<{
1979
+ rows: Record<string, unknown>[];
1980
+ name?: string;
1981
+ } | null>;
1982
+ /**
1983
+ * Publish a **sampled** schema derived from a result, so `catalog` has a producer for every kind.
1984
+ *
1985
+ * Without this, `catalog` only ever emits for a host that answers `introspect` — which is exactly
1986
+ * one kind of host. A REST or CSV source has no DDL to read, but it does have a page of rows, and
1987
+ * §7 already names that path `derivedFrom: 'sample'` and requires `fields` to warn that a column
1988
+ * may be absent later.
1989
+ *
1990
+ * A host-supplied schema always wins: this never overwrites one that was genuinely introspected.
1991
+ * The revision is bumped **only when the derived shape actually changes**, so a 30-second refresh
1992
+ * does not force `fields` to re-read on every tick.
1993
+ *
1994
+ * @param connection - The source that answered.
1995
+ * @param result - Its result set.
1996
+ * @returns Whether a new catalog was published.
1997
+ */
1998
+ private noteSampledSchema;
1999
+ /** Apply the local half of the query and stamp the echoed query + split telemetry. */
2000
+ private finish;
2001
+ private echoQuery;
2002
+ /** Emit a result, unless the request has been superseded. */
2003
+ private deliver;
2004
+ /** Install the refresh timer for a connection, replacing any existing one. */
2005
+ scheduleRefresh(connection: XenoConnection): void;
2006
+ /** Clear a connection's timer. */
2007
+ clearRefresh(id: string): void;
2008
+ /** Suspend or resume every timer — a hidden panel must not poll. */
2009
+ setHidden(hidden: boolean): void;
2010
+ /** Re-run a connection's last query, bypassing the cache. */
2011
+ refresh(connectionId: string): Promise<XenoResultSet | null>;
2012
+ /**
2013
+ * Refresh every connection, reporting **per connection**.
2014
+ *
2015
+ * A partial failure must never collapse to one error: with five sources and one expired token, a
2016
+ * single rejected promise tells the user nothing about the four that worked.
2017
+ *
2018
+ * @returns One outcome per connection, in order.
2019
+ */
2020
+ refreshAll(): Promise<{
2021
+ connectionId: string;
2022
+ ok: boolean;
2023
+ code?: string;
2024
+ }[]>;
2025
+ /**
2026
+ * Serialize.
2027
+ *
2028
+ * Connections with their `credentialRef` and the last query. **Never a result, never `health`,
2029
+ * never a secret — and NO MASKED VALUES AT ALL.**
2030
+ *
2031
+ * A mask is not a security boundary, and a mask that survives a round-trip is data loss: one
2032
+ * shipped connector writes `password: '***'` on save and reads it back verbatim on load, so
2033
+ * opening and saving a document *replaces the real credential with three asterisks*. The only way
2034
+ * to make that unrepresentable is to never serialize a value field in the first place.
2035
+ */
2036
+ serialize(): ConnectorsPanelState;
2037
+ /** Restore. Health is rebuilt as `unknown` — never read from the document. */
2038
+ deserialize(state: Partial<ConnectorsPanelState>): void;
2039
+ /** Drop timers, listeners and in-flight state. */
2040
+ dispose(): void;
2041
+ }
2042
+
2043
+ /**
2044
+ * The `sql.*` executors — and the honest refusal that replaces a stub.
2045
+ *
2046
+ * **A sandboxed panel cannot open a TCP socket.** Not "does not yet"; cannot. QuickJS has no socket
2047
+ * API, a browser has no raw TCP at all, and no amount of work in this package changes either. The
2048
+ * capability matrix therefore marks `sql.postgres`, `sql.mysql`, `sql.sqlite` and file-mode
2049
+ * `sql.duckdb` as **`unsupported`** in those substrates, with one instruction attached to that
2050
+ * decision: *"Disabled card + reason. **Never a stub.**"*
2051
+ *
2052
+ * So this module has exactly two behaviours:
2053
+ *
2054
+ * 1. A **host resolver was supplied** — delegate to it. The host owns the driver, the connection
2055
+ * pool and the credential; the panel contributes the compiled statement and the paging truth.
2056
+ * 2. **No host resolver** — return `HOST_RESOLVER_REQUIRED` with a sentence naming what the user
2057
+ * can actually do about it. Not an empty grid, not a spinner, not a "coming soon".
2058
+ *
2059
+ * The second case is the whole point. A stub that returns `[]` renders as a working connection with
2060
+ * no data, and the user's next hour goes into debugging their WHERE clause.
2061
+ *
2062
+ * @module
2063
+ */
2064
+
2065
+ /** A host-side SQL resolver: the desktop main process, an IPC bridge, a server. */
2066
+ type HostSqlResolver = (input: ConnectorExecuteInput) => Promise<ConnectorExecuteOutput>;
2067
+ /** Options for {@link executeSql}. */
2068
+ interface ExecuteSqlOptions {
2069
+ /** The host's resolver. Absent means this substrate genuinely cannot reach a database. */
2070
+ hostResolver?: HostSqlResolver;
2071
+ }
2072
+ /**
2073
+ * Run a SQL query against a server or file database.
2074
+ *
2075
+ * @param input - Connection, the pushed query, and the clamped row budget.
2076
+ * @param options - The host resolver, when this substrate has one.
2077
+ * @returns Rows, or the typed refusal.
2078
+ *
2079
+ * @example
2080
+ * ```ts
2081
+ * // In a browser with no host: a visible, explained, disabled state.
2082
+ * await executeSql(input, {})
2083
+ * // → { rows: [], error: { code: 'HOST_RESOLVER_REQUIRED', message: 'Reading PostgreSQL … ' } }
2084
+ * ```
2085
+ */
2086
+ declare function executeSql(input: ConnectorExecuteInput, options: ExecuteSqlOptions): Promise<ConnectorExecuteOutput>;
2087
+ /**
2088
+ * The sentence shown on a disabled source card.
2089
+ *
2090
+ * Says the constraint, then the two real options. The spec's own remedy — *"convert to an inline
2091
+ * snapshot"* — is included because it is the one that works today without leaving the app.
2092
+ *
2093
+ * @param kind - The connection kind.
2094
+ * @returns An authored, user-facing sentence.
2095
+ */
2096
+ declare function unsupportedMessage(kind: string): string;
2097
+ /**
2098
+ * Compile the statement a host resolver would otherwise write itself.
2099
+ *
2100
+ * Offered, never imposed: a host with its own query builder ignores this and reads
2101
+ * `input.query` directly. A host without one gets correct identifier quoting, literal escaping and
2102
+ * `NULLS LAST` ordering for free, which is three classes of bug it does not have to rediscover.
2103
+ *
2104
+ * @param input - The executor input.
2105
+ * @param table - The table or view to read.
2106
+ * @param columns - Known columns, so unknown ids are refused rather than emitted.
2107
+ * @returns The statement or an authored refusal.
2108
+ *
2109
+ * @example
2110
+ * ```ts
2111
+ * const compiled = compileForHost(input, 'orders')
2112
+ * if ('sql' in compiled) rows = await pg.query(compiled.sql)
2113
+ * ```
2114
+ */
2115
+ declare function compileForHost(input: ConnectorExecuteInput, table: string, columns?: string[]): {
2116
+ sql: string;
2117
+ } | {
2118
+ error: string;
2119
+ };
2120
+ /**
2121
+ * The default entity (table/view) for a connection, when its config names one.
2122
+ *
2123
+ * @param input - The executor input.
2124
+ * @returns The entity name, or `null`.
2125
+ */
2126
+ declare function entityOf(input: ConnectorExecuteInput): string | null;
2127
+
2128
+ /**
2129
+ * The REST executor.
2130
+ *
2131
+ * Owns the protocol so no host has to: URL assembly, the four paging styles, JSONPath extraction,
2132
+ * HTTP-status→typed-error mapping, and the over-fetch that makes `hasMore` honest. The only thing it
2133
+ * does not own is the socket — that is {@link XenoConnectorFetch}, supplied by the host.
2134
+ *
2135
+ * @module
2136
+ */
2137
+
2138
+ /** Options for {@link executeRest}. */
2139
+ interface ExecuteRestOptions {
2140
+ fetch: XenoConnectorFetch;
2141
+ timeoutMs?: number;
2142
+ }
2143
+ /**
2144
+ * Run a REST query.
2145
+ *
2146
+ * @param input - Connection, the pushed query, and the already-clamped row limit.
2147
+ * @param options - The host's fetch and a deadline.
2148
+ * @returns Rows plus paging truth, or a typed error. **Never throws for a source-side failure.**
2149
+ *
2150
+ * @example
2151
+ * ```ts
2152
+ * const out = await executeRest(
2153
+ * { connection, query: {}, limit: 51 },
2154
+ * { fetch: browserFetch() },
2155
+ * )
2156
+ * ```
2157
+ */
2158
+ declare function executeRest(input: ConnectorExecuteInput, options: ExecuteRestOptions): Promise<ConnectorExecuteOutput>;
2159
+ /**
2160
+ * Assemble a request URL from the base, static params and the paging style.
2161
+ *
2162
+ * @param base - The configured URL, which may already carry a query string.
2163
+ * @param spec - Static params plus the paging position.
2164
+ * @returns The absolute URL to request.
2165
+ *
2166
+ * @example
2167
+ * ```ts
2168
+ * buildRestUrl('https://api.test/v1/rows?team=1', {
2169
+ * paging: { style: 'offset', limitParam: 'limit', offsetParam: 'offset' },
2170
+ * limit: 50,
2171
+ * offset: 100,
2172
+ * })
2173
+ * // → 'https://api.test/v1/rows?team=1&limit=50&offset=100'
2174
+ * ```
2175
+ */
2176
+ declare function buildRestUrl(base: string, spec: {
2177
+ params?: Record<string, string>;
2178
+ paging: XenoRestPaging;
2179
+ limit: number;
2180
+ offset: number;
2181
+ cursor?: string;
2182
+ }): string;
2183
+ /**
2184
+ * Work out how to reach the next page, per style.
2185
+ *
2186
+ * @param paging - The declared style.
2187
+ * @param body - The parsed response body.
2188
+ * @param response - The raw response, for header-based styles.
2189
+ * @param pageLength - Rows in the page just read.
2190
+ * @returns The next cursor or URL, or `exhausted`.
2191
+ */
2192
+ declare function nextPageToken(paging: XenoRestPaging, body: unknown, response: {
2193
+ headers: Record<string, string>;
2194
+ url?: string;
2195
+ }, pageLength: number): {
2196
+ cursor?: string;
2197
+ url?: string;
2198
+ exhausted?: boolean;
2199
+ };
2200
+ /**
2201
+ * Parse an RFC 8288 `Link` header.
2202
+ *
2203
+ * @param header - The raw header value.
2204
+ * @returns URLs by `rel`.
2205
+ *
2206
+ * @example
2207
+ * ```ts
2208
+ * parseLinkHeader('<https://api/x?page=2>; rel="next", <https://api/x?page=9>; rel="last"')
2209
+ * // → { next: 'https://api/x?page=2', last: 'https://api/x?page=9' }
2210
+ * ```
2211
+ */
2212
+ declare function parseLinkHeader(header: string): Record<string, string>;
2213
+
2214
+ /**
2215
+ * Turning failures into the typed vocabulary of §5 — **authored, never echoed**.
2216
+ *
2217
+ * Two rules from the spec are enforced here rather than remembered:
2218
+ *
2219
+ * 1. *"Never surface a 500."* A server's error body is written for the server's operator; it
2220
+ * routinely contains stack frames, internal hostnames and occasionally a connection string. The
2221
+ * status **number** is diagnostic and is kept; the body and the reason phrase are not.
2222
+ * 2. *"Never echo env-var names."* A misconfigured platform must say what the user can fix, not
2223
+ * which variable the operator forgot — one surveyed implementation renders
2224
+ * `PLATFORM_NOT_CONFIGURED` complete with the variable names, which tells an end user nothing and
2225
+ * tells an attacker something.
2226
+ *
2227
+ * @module
2228
+ */
2229
+
2230
+ /** A typed executor failure. */
2231
+ interface ExecuteError {
2232
+ code: XenoResolverErrorCode;
2233
+ message: string;
2234
+ retryable?: boolean;
2235
+ }
2236
+ /**
2237
+ * Map an HTTP status onto the typed vocabulary.
2238
+ *
2239
+ * @param status - The HTTP status code.
2240
+ * @param statusText - The reason phrase. **Deliberately unused in the message** — see the module doc.
2241
+ * @returns A typed error with an authored, user-facing sentence.
2242
+ *
2243
+ * @example
2244
+ * ```ts
2245
+ * classifyHttpStatus(401) // → { code: 'AUTH_EXPIRED', message: 'The credential … ', retryable: false }
2246
+ * classifyHttpStatus(503) // → { code: 'NETWORK', …, retryable: true }
2247
+ * ```
2248
+ */
2249
+ declare function classifyHttpStatus(status: number, statusText?: string): ExecuteError;
2250
+ /**
2251
+ * Parse a JSON body, reporting a `PARSE` failure rather than throwing.
2252
+ *
2253
+ * The offending text is **not** included: an HTML error page or a login redirect is the usual cause,
2254
+ * and pasting a kilobyte of markup into a panel error is neither readable nor safe. The first
2255
+ * meaningful characters are enough to recognise "that's HTML, not JSON".
2256
+ *
2257
+ * @param body - The raw response text.
2258
+ * @returns The parsed value, or a typed error.
2259
+ *
2260
+ * @example
2261
+ * ```ts
2262
+ * parseJsonBody('<!doctype html>…')
2263
+ * // → { error: { code: 'PARSE', message: 'The source returned HTML, not JSON. …' } }
2264
+ * ```
2265
+ */
2266
+ declare function parseJsonBody(body: string): {
2267
+ value: unknown;
2268
+ } | {
2269
+ error: ExecuteError;
2270
+ };
2271
+ /**
2272
+ * Convert anything thrown during execution into a typed error.
2273
+ *
2274
+ * A {@link ConnectorTransportError} already knows its code — that is the whole point of the
2275
+ * transport carrying one, and it is what keeps `CORS_BLOCKED` from collapsing into `NETWORK`
2276
+ * (§6 L9). Everything else is genuinely unknown and says so.
2277
+ *
2278
+ * @param error - The thrown value.
2279
+ * @returns A typed error.
2280
+ */
2281
+ declare function toExecuteError(error: unknown): ExecuteError;
2282
+
2283
+ /**
2284
+ * The GraphQL executor.
2285
+ *
2286
+ * **This file exists because of §6 L13, a live bug in shipped code.** One surveyed connector
2287
+ * declares `graphql` as a supported kind and then branches only on `rest`, so every other kind falls
2288
+ * through to the SQL path and a plain HTTP source dies with *"Database queries require main process
2289
+ * IPC"* — a message about the wrong subsystem entirely. The discriminated config union makes the
2290
+ * missing branch a compile error; this module is that branch.
2291
+ *
2292
+ * @module
2293
+ */
2294
+
2295
+ /** Options for {@link executeGraphql}. */
2296
+ interface ExecuteGraphqlOptions {
2297
+ fetch: XenoConnectorFetch;
2298
+ timeoutMs?: number;
2299
+ }
2300
+ /**
2301
+ * Run a GraphQL query.
2302
+ *
2303
+ * @param input - Connection, the pushed query, and the clamped row limit.
2304
+ * @param options - The host's fetch and a deadline.
2305
+ * @returns Rows plus paging truth, or a typed error.
2306
+ */
2307
+ declare function executeGraphql(input: ConnectorExecuteInput, options: ExecuteGraphqlOptions): Promise<ConnectorExecuteOutput>;
2308
+ /**
2309
+ * Read the first error out of a GraphQL `errors` array.
2310
+ *
2311
+ * An `UNAUTHENTICATED`/`FORBIDDEN` extension code is mapped onto the auth vocabulary so the
2312
+ * connection flips to `expired` and offers **Reconnect**, exactly as an HTTP 401 would. Without
2313
+ * that mapping a GraphQL auth failure is indistinguishable from a typo in the document.
2314
+ *
2315
+ * @param errors - The `errors` member of a GraphQL envelope.
2316
+ * @returns A typed error, or `null` when there are none.
2317
+ */
2318
+ declare function firstGraphqlError(errors: unknown): ExecuteError | null;
2319
+
2320
+ /**
2321
+ * The file executors — `file.csv` and `file.json`.
2322
+ *
2323
+ * **The panel never enumerates paths and never learns one.** `fileRef` is an opaque handle the host
2324
+ * minted from its own picker, exactly the discipline `xeno-shell` uses for host-folder mounts, where
2325
+ * raw paths deliberately never cross the bridge. The panel asks "read this handle"; the host decides
2326
+ * whether that handle is still valid, still consented, and still inside a granted mount.
2327
+ *
2328
+ * @module
2329
+ */
2330
+
2331
+ /** Options for the file executors. */
2332
+ interface ExecuteFileOptions {
2333
+ readFile: XenoConnectorFileReader;
2334
+ /** Byte ceiling for one read. Default 32 MiB. */
2335
+ maxBytes?: number;
2336
+ }
2337
+ /**
2338
+ * Read a delimited file into rows.
2339
+ *
2340
+ * @param input - Connection and the clamped row budget.
2341
+ * @param options - The host's file reader and a byte ceiling.
2342
+ * @returns Rows, or a typed error.
2343
+ *
2344
+ * @example
2345
+ * ```ts
2346
+ * await executeCsvFile({ connection, query: {}, limit: 500 }, { readFile })
2347
+ * ```
2348
+ */
2349
+ declare function executeCsvFile(input: ConnectorExecuteInput, options: ExecuteFileOptions): Promise<ConnectorExecuteOutput>;
2350
+ /**
2351
+ * Read a JSON or NDJSON file into rows.
2352
+ *
2353
+ * @param input - Connection and the clamped row budget.
2354
+ * @param options - The host's file reader and a byte ceiling.
2355
+ * @returns Rows, or a typed error.
2356
+ */
2357
+ declare function executeJsonFile(input: ConnectorExecuteInput, options: ExecuteFileOptions): Promise<ConnectorExecuteOutput>;
2358
+
2359
+ /**
2360
+ * The in-sandbox SQL bridge — **the honest bridge** of the capability matrix.
2361
+ *
2362
+ * Every other route to real SQL needs something a sandboxed panel cannot have: a TCP socket, a file
2363
+ * handle, a host process. This one needs nothing. DuckDB-WASM runs *inside* the sandbox over buffers
2364
+ * that were **already fetched** by connections that had their own permission to fetch them — so a
2365
+ * user gets joins, `GROUP BY`, window functions and correct `NULL` semantics over their REST and CSV
2366
+ * sources with **zero additional capabilities**, and it survives a web export.
2367
+ *
2368
+ * That is why the capability matrix lists this row as `requires: []` while every other `sql.*` row
2369
+ * requires a host resolver. The data crossed the boundary once, under an existing grant; SQL over it
2370
+ * afterwards is arithmetic.
2371
+ *
2372
+ * ## The engine is not a dependency
2373
+ *
2374
+ * `@duckdb/duckdb-wasm` is megabytes of wasm plus a worker. Rule 8 — no heavyweight runtime
2375
+ * dependency — is not negotiable for a package that twenty other panels sit beside, and the lucide
2376
+ * incident already proved what a static import costs every consumer who never uses the feature. So
2377
+ * the engine arrives through the three-method {@link XenoSqlEngine} seam, resolved from the host,
2378
+ * exactly as xterm does in `panel-terminal`. A reference adapter ships on the `./duckdb` subpath.
2379
+ *
2380
+ * **Absent engine ⇒ a visible `unsupported` state**, never a stub.
2381
+ *
2382
+ * @module
2383
+ */
2384
+
2385
+ /** Options for {@link executeDuckDb}. */
2386
+ interface ExecuteDuckDbOptions {
2387
+ /** The engine. Absent means "no in-sandbox SQL here", which is a state, not a failure. */
2388
+ engine?: XenoSqlEngine;
2389
+ /**
2390
+ * Fetch the rows of another connection, by id.
2391
+ *
2392
+ * Supplied by the controller, which already knows how to resolve a connection — including its
2393
+ * cache, its credential and its own error handling. Re-implementing that here would give the SQL
2394
+ * path a second, subtly different resolver.
2395
+ */
2396
+ resolveSource?: (connectionId: string) => Promise<{
2397
+ rows: Record<string, unknown>[];
2398
+ name?: string;
2399
+ } | null>;
2400
+ /** Whether a panel-authored `raw` statement may be executed. Mirrors the controller's config. */
2401
+ allowRawQueries?: boolean;
2402
+ }
2403
+ /**
2404
+ * Run SQL over already-fetched results.
2405
+ *
2406
+ * @param input - Connection, the pushed query, and the clamped row budget.
2407
+ * @param options - The engine seam, the source resolver, and the raw-query gate.
2408
+ * @returns Rows, or a typed error.
2409
+ *
2410
+ * @example
2411
+ * ```ts
2412
+ * // A duckdb source joining two REST connections:
2413
+ * // config = { kind: 'sql.duckdb', mode: 'wasm', sourceConnectionIds: ['orders', 'customers'] }
2414
+ * await executeDuckDb(input, { engine, resolveSource })
2415
+ * ```
2416
+ */
2417
+ declare function executeDuckDb(input: ConnectorExecuteInput, options: ExecuteDuckDbOptions): Promise<ConnectorExecuteOutput>;
2418
+ /**
2419
+ * Turn a connection name or id into a safe, unique SQL table name.
2420
+ *
2421
+ * Registered table names are not merely quoted — in most engines they also name a registered buffer,
2422
+ * where quoting protects nothing. So the name is *sanitized to an identifier*, not escaped: anything
2423
+ * outside `[A-Za-z0-9_]` becomes `_`, a leading digit gains a prefix, and collisions get a numeric
2424
+ * suffix so two sources called "Data" stay distinct.
2425
+ *
2426
+ * @param raw - The connection's display name or id.
2427
+ * @param taken - Names already assigned, as a map of sourceId → name.
2428
+ * @returns A safe, unused table name.
2429
+ *
2430
+ * @example
2431
+ * ```ts
2432
+ * uniqueTableName('Sales (EU)', new Map()) // → 'Sales__EU_'
2433
+ * uniqueTableName('2024', new Map()) // → 't_2024'
2434
+ * ```
2435
+ */
2436
+ declare function uniqueTableName(raw: string, taken: Map<string, string>): string;
2437
+
2438
+ /**
2439
+ * A dependency-free RFC 4180 CSV reader.
2440
+ *
2441
+ * Rule 8 forbids a runtime dependency, so PapaParse is not an option — but "split on commas" is not
2442
+ * a CSV parser either, and the difference shows up on the first export anyone actually has: a
2443
+ * quoted field containing a comma, a newline inside a quoted address, a doubled `""` escape, or a
2444
+ * UTF-8 BOM that turns the first column's name into `id` and makes every lookup by that name
2445
+ * silently miss.
2446
+ *
2447
+ * This is a proper character-scanning parser. It is ~90 lines because CSV genuinely is that, and
2448
+ * every one of the cases above has a test.
2449
+ *
2450
+ * @module
2451
+ */
2452
+ /** Options for {@link parseCsv}. */
2453
+ interface ParseCsvOptions {
2454
+ /** Field separator. Omit to sniff — see {@link sniffDelimiter}. */
2455
+ delimiter?: string;
2456
+ /** Quote character. Default `"`. */
2457
+ quote?: string;
2458
+ /** First (non-skipped) row is a header. Default `true`. */
2459
+ hasHeader?: boolean;
2460
+ /** Rows to drop before the header — banner lines above the table are common in exports. */
2461
+ skipRows?: number;
2462
+ }
2463
+ /** The parsed table. */
2464
+ interface ParsedCsv {
2465
+ /** Column names, either from the header row or synthesized as `column_1`, `column_2`, … */
2466
+ columns: string[];
2467
+ /** Records keyed by column name, values still as text. */
2468
+ rows: Record<string, string>[];
2469
+ }
2470
+ /**
2471
+ * Guess the field separator from the first meaningful line.
2472
+ *
2473
+ * Counts candidates **outside quotes only**, so a comma inside `"Smith, John"` does not vote for
2474
+ * comma in a semicolon-separated European export — which is the case that makes naive sniffers pick
2475
+ * the wrong character and produce a one-column table.
2476
+ *
2477
+ * @param text - The file's text.
2478
+ * @returns The most likely delimiter. Falls back to `,`.
2479
+ *
2480
+ * @example
2481
+ * ```ts
2482
+ * sniffDelimiter('a;b;c\n"x, y";2;3') // → ';'
2483
+ * ```
2484
+ */
2485
+ declare function sniffDelimiter(text: string): string;
2486
+ /**
2487
+ * Parse CSV text into records.
2488
+ *
2489
+ * @param text - The file contents.
2490
+ * @param options - Delimiter, quote, header and skip settings.
2491
+ * @returns Columns and records.
2492
+ *
2493
+ * @example
2494
+ * ```ts
2495
+ * parseCsv('id,name\n1,"Smith, John"\n')
2496
+ * // → { columns: ['id','name'], rows: [{ id: '1', name: 'Smith, John' }] }
2497
+ * ```
2498
+ */
2499
+ declare function parseCsv(text: string, options?: ParseCsvOptions): ParsedCsv;
2500
+ /**
2501
+ * Convert text cells into JSON-ish values.
2502
+ *
2503
+ * CSV has no types, so a reader must infer them — but **only where inference is safe**. A value is
2504
+ * numeric only if it round-trips exactly through `String(Number(v))`, which is what keeps a zip code
2505
+ * `01234`, a phone number `+15551234`, and a 20-digit account id from being silently mangled into
2506
+ * `1234`, `15551234`, and a float that lost its last four digits.
2507
+ *
2508
+ * @param rows - Text records from {@link parseCsv}.
2509
+ * @returns Records with numbers, booleans and nulls where they are unambiguous.
2510
+ *
2511
+ * @example
2512
+ * ```ts
2513
+ * coerceCsvValues([{ n: '42', zip: '01234', ok: 'true', gap: '' }])
2514
+ * // → [{ n: 42, zip: '01234', ok: true, gap: null }]
2515
+ * ```
2516
+ */
2517
+ declare function coerceCsvValues(rows: Record<string, string>[]): Record<string, unknown>[];
2518
+
2519
+ /**
2520
+ * JSONPath extraction — finding the row array inside a response envelope.
2521
+ *
2522
+ * Almost no API returns a bare array. It returns `{data: {items: [...], next: "..."}}`, and the
2523
+ * whole difference between a working REST source and a one-row table containing the word `[object
2524
+ * Object]` is knowing where to look. `rootPath` is that knowledge.
2525
+ *
2526
+ * ## A deliberate subset, and why
2527
+ *
2528
+ * This implements the *dotted-path* subset of JSONPath — `$.data.items`, `data.items`,
2529
+ * `results[0].rows`, `a["odd key"].b` — plus the `[*]` wildcard, and nothing else. No filter
2530
+ * expressions, no recursive descent, no script evaluation.
2531
+ *
2532
+ * That is not laziness. A full JSONPath engine is a third-party runtime dependency (rule 8 forbids
2533
+ * it) *and* the expression-evaluating half of the grammar is an injection surface pointed at the
2534
+ * response of a network call. The subset covers every shape the survey found in real connector
2535
+ * configs, and the parts left out are the parts that could bite.
2536
+ *
2537
+ * @module
2538
+ */
2539
+ /** One step in a parsed path. */
2540
+ type Step = {
2541
+ kind: 'key';
2542
+ key: string;
2543
+ } | {
2544
+ kind: 'index';
2545
+ index: number;
2546
+ } | {
2547
+ kind: 'wildcard';
2548
+ };
2549
+ /**
2550
+ * Parse a dotted/bracketed path into steps.
2551
+ *
2552
+ * @param path - e.g. `$.data.items[0]`, `data["odd key"]`, `rows[*].cells`.
2553
+ * @returns The steps, or `null` if the expression uses syntax this subset does not implement.
2554
+ *
2555
+ * @example
2556
+ * ```ts
2557
+ * parseJsonPath('$.data.items[0]')
2558
+ * // → [{kind:'key',key:'data'},{kind:'key',key:'items'},{kind:'index',index:0}]
2559
+ * ```
2560
+ */
2561
+ declare function parseJsonPath(path: string): Step[] | null;
2562
+ /**
2563
+ * Read a value out of parsed JSON at `path`.
2564
+ *
2565
+ * @param input - The parsed response body.
2566
+ * @param path - A dotted/bracketed path. Empty or `'$'` returns `input` unchanged.
2567
+ * @returns The value at the path, or `undefined` if any step is missing.
2568
+ *
2569
+ * @example
2570
+ * ```ts
2571
+ * extractJsonPath({ data: { items: [1, 2] } }, 'data.items') // → [1, 2]
2572
+ * extractJsonPath({ pages: [{ rows: [1] }, { rows: [2] }] }, 'pages[*].rows') // → [[1],[2]]
2573
+ * ```
2574
+ */
2575
+ declare function extractJsonPath(input: unknown, path: string | undefined): unknown;
2576
+ /**
2577
+ * Extract the row array from a response, with a documented fallback.
2578
+ *
2579
+ * When no `rootPath` is configured, an envelope is still the common case, so this looks for the
2580
+ * conventional wrappers (`data`, `items`, `results`, `records`, `rows`, `value`) before giving up.
2581
+ * **The fallback only fires when the top level is not already an array** — a source that returned
2582
+ * exactly what we wanted is never second-guessed.
2583
+ *
2584
+ * @param body - The parsed response body.
2585
+ * @param rootPath - The configured path, if any.
2586
+ * @returns The rows, and how they were found.
2587
+ *
2588
+ * @example
2589
+ * ```ts
2590
+ * extractRows({ items: [{ id: 1 }] }, undefined) // → { rows: [{id:1}], via: 'convention' }
2591
+ * ```
2592
+ */
2593
+ declare function extractRows(body: unknown, rootPath: string | undefined): {
2594
+ rows: unknown;
2595
+ via: 'path' | 'convention' | 'root';
2596
+ missing?: boolean;
2597
+ };
2598
+
2599
+ /**
2600
+ * Compiling a `XenoQuery` into SQL.
2601
+ *
2602
+ * Used by the in-sandbox DuckDB bridge, and offered to any host resolver that would otherwise write
2603
+ * this itself. It is the highest-risk file in the package, so the rules are absolute:
2604
+ *
2605
+ * - **Every identifier is quoted and escaped.** A column called `"; DROP TABLE x; --` becomes the
2606
+ * literal identifier `"""; DROP TABLE x; --"`, which is a column that does not exist — an error,
2607
+ * never an execution.
2608
+ * - **Every literal is escaped or refused.** Strings double their quotes; numbers must be finite;
2609
+ * anything else becomes a parameterless `NULL`. There is no path by which a value reaches the
2610
+ * statement uninspected.
2611
+ * - **`raw` is never assembled here.** A panel-authored raw string is the family's injection
2612
+ * surface and is gated on `allowRawQueries` in the controller, upstream of this file.
2613
+ *
2614
+ * The output targets DuckDB, whose dialect is PostgreSQL-compatible for everything used here.
2615
+ *
2616
+ * @module
2617
+ */
2618
+
2619
+ /**
2620
+ * Quote a SQL identifier.
2621
+ *
2622
+ * @param name - A table or column name from user data.
2623
+ * @returns The safely quoted identifier.
2624
+ *
2625
+ * @example
2626
+ * ```ts
2627
+ * quoteIdent('order') // → '"order"'
2628
+ * quoteIdent('a"; DROP b; --') // → '"a""; DROP b; --"' (one identifier, not a statement)
2629
+ * ```
2630
+ */
2631
+ declare function quoteIdent(name: string): string;
2632
+ /**
2633
+ * Render a value as a SQL literal.
2634
+ *
2635
+ * @param value - Any value from a filter or parameter.
2636
+ * @returns A literal safe to concatenate.
2637
+ *
2638
+ * @example
2639
+ * ```ts
2640
+ * quoteLiteral("O'Brien") // → "'O''Brien'"
2641
+ * quoteLiteral(NaN) // → 'NULL' (a non-finite number is not a number)
2642
+ * ```
2643
+ */
2644
+ declare function quoteLiteral(value: XenoValue | undefined): string;
2645
+ /** Is this a valid, safe table name to register with the engine? */
2646
+ declare function isSafeTableName(name: string): boolean;
2647
+ /** Options for {@link buildSelect}. */
2648
+ interface BuildSelectOptions {
2649
+ /** Table to read from. Must satisfy {@link isSafeTableName}. */
2650
+ table: string;
2651
+ /** The query to compile. */
2652
+ query: XenoQuery;
2653
+ /** Row ceiling. */
2654
+ limit: number;
2655
+ /** Row offset. */
2656
+ offset?: number;
2657
+ /** Columns known to exist. When supplied, unknown column ids are refused rather than emitted. */
2658
+ columns?: string[];
2659
+ }
2660
+ /** A compiled statement, or the reason it could not be compiled. */
2661
+ type BuildSelectResult = {
2662
+ sql: string;
2663
+ } | {
2664
+ error: string;
2665
+ };
2666
+ /**
2667
+ * Compile a query into a `SELECT`.
2668
+ *
2669
+ * @param options - Table, query, paging and the known column list.
2670
+ * @returns The statement, or an authored refusal.
2671
+ *
2672
+ * @example
2673
+ * ```ts
2674
+ * buildSelect({
2675
+ * table: 'orders',
2676
+ * query: { aggregates: [{ id: 'total', columnId: 'amount', fn: 'sum' }], groupBy: [{ columnId: 'region' }] },
2677
+ * limit: 100,
2678
+ * })
2679
+ * // → { sql: 'SELECT "region", sum("amount") AS "total" FROM "orders" GROUP BY "region" LIMIT 100' }
2680
+ * ```
2681
+ */
2682
+ declare function buildSelect(options: BuildSelectOptions): BuildSelectResult;
2683
+ /** Render a filter group into a WHERE fragment. */
2684
+ declare function renderFilterGroup(group: XenoFilterGroup, unknown?: (id: string) => boolean): {
2685
+ sql: string;
2686
+ } | {
2687
+ error: string;
2688
+ };
2689
+ /**
2690
+ * Escape `%`, `_` and `\` so user text is matched literally inside a LIKE pattern.
2691
+ *
2692
+ * @param text - Raw user input.
2693
+ * @returns The escaped text, for use with `ESCAPE '\'`.
2694
+ *
2695
+ * @example
2696
+ * ```ts
2697
+ * escapeLikePattern('50%') // → '50\\%' — searches for the character %, not "anything"
2698
+ * ```
2699
+ */
2700
+ declare function escapeLikePattern(text: string): string;
2701
+
2702
+ /**
2703
+ * The executor dispatcher — one function per connection kind, chosen by the discriminator.
2704
+ *
2705
+ * The `switch` below is exhaustive over `XenoConnectionConfig['kind']`, which is the entire reason
2706
+ * the config is a discriminated union rather than a flat bag. §6 L13 is a live bug produced by the
2707
+ * alternative: one surveyed connector branches on `rest` and lets *every other kind* fall through to
2708
+ * the SQL path, so a declared-but-unhandled `graphql` source dies with *"Database queries require
2709
+ * main process IPC"* — a message about a subsystem it never touched. Here a missing branch does not
2710
+ * compile.
2711
+ *
2712
+ * @module
2713
+ */
2714
+
2715
+ /** Options for {@link createConnectorExecutor}. */
2716
+ interface CreateConnectorExecutorOptions {
2717
+ /** The host's transport seams. Every field optional; absence is a state, not a bug. */
2718
+ transport?: ConnectorTransport;
2719
+ /**
2720
+ * A host-side resolver for `sql.*` and the first-party `xeno.*` kinds.
2721
+ *
2722
+ * Also consulted as a **fallback** for any kind whose transport is missing, so a desktop host can
2723
+ * supply one resolver and get every kind rather than wiring three seams.
2724
+ */
2725
+ hostResolver?: HostSqlResolver;
2726
+ /** Whether a panel-authored `raw` statement may execute. Default `false`. */
2727
+ allowRawQueries?: boolean;
2728
+ /** Fetch another connection's rows, for the in-sandbox SQL bridge. Supplied by the controller. */
2729
+ resolveSource?: (connectionId: string) => Promise<{
2730
+ rows: Record<string, unknown>[];
2731
+ name?: string;
2732
+ } | null>;
2733
+ }
2734
+ /**
2735
+ * Build the executor the controller calls for every non-`inline` source.
2736
+ *
2737
+ * `inline` never reaches here: it is resolved in the controller with no round-trip at all, which is
2738
+ * what makes the first `table` drag in the builder a live grid rather than a dead one.
2739
+ *
2740
+ * @param options - Transports, an optional host resolver, and the raw-query gate.
2741
+ * @returns An executor.
2742
+ *
2743
+ * @example
2744
+ * ```ts
2745
+ * const execute = createConnectorExecutor({
2746
+ * transport: { fetch: browserFetch(), readFile: hostReadFile },
2747
+ * hostResolver: ipcSqlResolver,
2748
+ * })
2749
+ * ```
2750
+ */
2751
+ declare function createConnectorExecutor(options?: CreateConnectorExecutorOptions): (input: ConnectorExecuteInput) => Promise<ConnectorExecuteOutput>;
2752
+
2753
+ /**
2754
+ * The `PanelModule` — what a host registers and mounts.
2755
+ *
2756
+ * Ships a wired default (`connectorsPanel`) with the view mounted INSIDE the package, so `react` and
2757
+ * `react-dom` resolve to one copy. A host that calls `createRoot` against a view imported from a
2758
+ * `file:`-linked package gets two React copies and every hook throws *"Invalid hook call"*.
2759
+ *
2760
+ * @module
2761
+ */
2762
+
2763
+ /** Everything a renderer needs: the controller plus the resolved config. */
2764
+ interface ConnectorsRenderContext {
2765
+ controller: ConnectorsController;
2766
+ config: {
2767
+ emptyHint?: string;
2768
+ };
2769
+ }
2770
+ /** Options for {@link createConnectorsPanel}. */
2771
+ interface CreateConnectorsPanelOptions {
2772
+ /**
2773
+ * A complete host-side executor, replacing the built-in one entirely.
2774
+ *
2775
+ * Use this when the host has its own resolver for **every** kind. To keep the built-in protocol
2776
+ * handling and only add what the substrate allows, pass {@link transport} instead — that is the
2777
+ * ordinary case.
2778
+ */
2779
+ execute?: ConnectorsHostBridge['execute'];
2780
+ /**
2781
+ * The host's transport seams: `fetch`, `readFile`, and an in-sandbox SQL engine.
2782
+ *
2783
+ * The panel owns the protocol — paging, extraction, parsing, error classification — and the host
2784
+ * owns the act. Every field is optional and **absence is meaningful**: no `fetch` means this
2785
+ * substrate has no network, which renders as a visible `unsupported` card rather than a source
2786
+ * that fails later.
2787
+ */
2788
+ transport?: ConnectorTransport;
2789
+ /**
2790
+ * A host-side resolver for `sql.*` and the first-party `xeno.*` kinds, which no sandboxed panel
2791
+ * can serve. Also the fallback for any kind whose transport is absent.
2792
+ */
2793
+ hostResolver?: HostSqlResolver;
2794
+ /**
2795
+ * What the substrate can do.
2796
+ *
2797
+ * **Derived from the transports when omitted**, which is the honest default: the controller's own
2798
+ * fallback assumes everything is available, and a source that reports healthy and then fails at
2799
+ * query time is precisely the "stub that fails later" the capability matrix forbids.
2800
+ */
2801
+ facilities?: SubstrateFacilities;
2802
+ /** Override the renderer. Rarely needed — see the module doc. */
2803
+ render?: (root: HTMLElement, context: ConnectorsRenderContext) => () => void;
2804
+ }
2805
+ /**
2806
+ * Build the Connectors panel module.
2807
+ *
2808
+ * @param options - Executor, substrate facilities, optional renderer.
2809
+ * @returns The module.
2810
+ */
2811
+ declare function createConnectorsPanel(options?: CreateConnectorsPanelOptions): PanelModule;
2812
+ /** The default Connectors panel module — view already wired. Register THIS. */
2813
+ declare const connectorsPanel: PanelModule;
2814
+
2815
+ /**
2816
+ * The `xeno.core.connectors` manifest.
2817
+ *
2818
+ * **The only panel in the data family holding a capability.** `table`, `chart`, `metrics`,
2819
+ * `calendar` and `fields` declare `storage.local` and nothing else *because this one declares
2820
+ * `net.fetch` and `fs.read`* — the seam concentrates every dangerous power in one auditable place.
2821
+ *
2822
+ * @module
2823
+ */
2824
+
2825
+ /** The canonical manifest id. */
2826
+ declare const CONNECTORS_PANEL_ID = "xeno.core.connectors";
2827
+ /**
2828
+ * Commands an agent must never be able to invoke.
2829
+ *
2830
+ * `submit_credential` is the whole list: an agent that can call it can read the form it answers and
2831
+ * replay a credential. The SDK has no `agentVisible` field yet, so the panel enforces it — the
2832
+ * command is absent from the manifest entirely and reachable only through the view.
2833
+ */
2834
+ declare const AGENT_HIDDEN_COMMANDS: readonly string[];
2835
+ /** The `xeno.core.connectors` manifest. */
2836
+ declare const connectorsManifest: PanelManifest;
2837
+
2838
+ /**
2839
+ * The credential form — the panel's half, and only the panel's half.
2840
+ *
2841
+ * The lock from §3 holds with one precision that is worth stating exactly, because it is the
2842
+ * difference between a rule people follow and a rule people quietly break: **the panel never
2843
+ * receives a *stored* secret, and holds an *entered* one only in transient component state that is
2844
+ * cleared at the moment of submission.** A user has to be able to type a password; they must never
2845
+ * be able to read one back.
2846
+ *
2847
+ * A counter-example is on the record. `xeno-hub`'s environment-secrets section reveals stored
2848
+ * secrets behind an eye-toggle. If this panel ever ships in Hub, **that affordance does not come
2849
+ * with it** — there is no code path here that can render a stored value, because no stored value
2850
+ * ever arrives.
2851
+ *
2852
+ * @module
2853
+ */
2854
+
2855
+ /** Values entered into a form, before submission. */
2856
+ type CredentialDraft = Record<string, string | number | boolean>;
2857
+ /**
2858
+ * Should this field be shown, given the current draft?
2859
+ *
2860
+ * `showWhen` is what lets one descriptor serve "API key *or* username+password" without the panel
2861
+ * knowing which platform it is rendering — the property that keeps a vendor registry out of this
2862
+ * package entirely.
2863
+ *
2864
+ * @param field - The field to test.
2865
+ * @param draft - Values entered so far.
2866
+ * @returns Whether to render it.
2867
+ *
2868
+ * @example
2869
+ * ```ts
2870
+ * isFieldVisible(
2871
+ * { key: 'pw', label: 'Password', type: 'password', showWhen: { key: 'mode', equals: ['basic'] } },
2872
+ * { mode: 'basic' },
2873
+ * ) // → true
2874
+ * ```
2875
+ */
2876
+ declare function isFieldVisible(field: XenoCredentialField, draft: CredentialDraft): boolean;
2877
+ /**
2878
+ * The fields a form is currently showing.
2879
+ *
2880
+ * @param spec - The host's form descriptor.
2881
+ * @param draft - Values entered so far.
2882
+ * @returns Visible fields, in declared order.
2883
+ */
2884
+ declare function visibleFields(spec: XenoCredentialFormSpec, draft: CredentialDraft): XenoCredentialField[];
2885
+ /** A per-field validation result, keyed by field key. */
2886
+ type CredentialErrors = Record<string, string>;
2887
+ /**
2888
+ * Validate a draft against its descriptor.
2889
+ *
2890
+ * **Only visible fields are required.** A required field hidden by its own `showWhen` would
2891
+ * otherwise block submission with an error pointing at a control that is not on screen — which
2892
+ * looks, to the user, exactly like a broken form.
2893
+ *
2894
+ * @param spec - The form descriptor.
2895
+ * @param draft - Values entered so far.
2896
+ * @returns Messages by field key. Empty means valid.
2897
+ *
2898
+ * @example
2899
+ * ```ts
2900
+ * validateCredentialDraft({ …, fields: [{ key: 'token', label: 'Token', type: 'password', required: true }] }, {})
2901
+ * // → { token: 'Token is required.' }
2902
+ * ```
2903
+ */
2904
+ declare function validateCredentialDraft(spec: XenoCredentialFormSpec, draft: CredentialDraft): CredentialErrors;
2905
+ /**
2906
+ * Drop values belonging to hidden fields.
2907
+ *
2908
+ * A user who fills in a password, switches the mode selector to "API key", and submits would
2909
+ * otherwise send the password too — a secret they believe they abandoned, travelling to a host that
2910
+ * will store it. Submitting only what is on screen is what makes "I changed my mind" mean it.
2911
+ *
2912
+ * @param spec - The form descriptor.
2913
+ * @param draft - Everything entered.
2914
+ * @returns Only the visible fields' values.
2915
+ */
2916
+ declare function pruneHiddenValues(spec: XenoCredentialFormSpec, draft: CredentialDraft): CredentialDraft;
2917
+ /**
2918
+ * Is this descriptor usable?
2919
+ *
2920
+ * §6 L2: a form-mode credential with no fields must be **rejected at registration, not at click** —
2921
+ * a card that offers "Connect", opens an empty modal, and can never be completed is a dead end the
2922
+ * user cannot diagnose. A host validating its own descriptors at registration time turns that into
2923
+ * a developer-visible error instead.
2924
+ *
2925
+ * @param spec - The descriptor.
2926
+ * @returns A reason it is unusable, or `null`.
2927
+ *
2928
+ * @example
2929
+ * ```ts
2930
+ * credentialSpecProblem({ requestId: '1', connectionId: 'c', title: 'X', mode: 'form' })
2931
+ * // → 'A form credential needs at least one field.'
2932
+ * ```
2933
+ */
2934
+ declare function credentialSpecProblem(spec: XenoCredentialFormSpec): string | null;
2935
+
2936
+ /**
2937
+ * The OAuth halves — loopback hardening, state, PKCE, and token refresh.
2938
+ *
2939
+ * ## Whose code is this?
2940
+ *
2941
+ * The panel's half of an OAuth flow is small and boring: render the redirect-URI hint verbatim, show
2942
+ * a button, wait for `credentialChanged`. The *host's* half is where every landmine in §6 lives —
2943
+ * and in `xeno-apps` the connectors panel **is** the host-side resolver, so this package is where
2944
+ * that logic belongs.
2945
+ *
2946
+ * Everything here is pure and injectable: no timers of its own, no global clock, no network. A host
2947
+ * that already owns a vault composes these functions into it; a host that does not gets a correct
2948
+ * implementation instead of the fourth independent one. Each landmine below was solved exactly once
2949
+ * somewhere in the ecosystem and nowhere twice — which is the definition of knowledge that should
2950
+ * live in one package.
2951
+ *
2952
+ * @module
2953
+ */
2954
+ /**
2955
+ * The loopback redirect URI, **derived from the port actually bound**.
2956
+ *
2957
+ * §6 L3 is a mismatch that costs an afternoon every time: one implementation falls back to
2958
+ * `localhost:3000` when three environment variables are unset — silently registering a URI the
2959
+ * server never listens on — while another derives it correctly from the loopback port. Deriving is
2960
+ * right, but the fix that actually stops the bug is **rendering the exact string to the user**, so
2961
+ * the value they paste into the provider's console and the value the token exchange sends are the
2962
+ * same characters.
2963
+ *
2964
+ * `127.0.0.1` is deliberate, not stylistic: RFC 8252 §7.3 requires the literal IP rather than
2965
+ * `localhost`, because `localhost` can resolve to an address another process is listening on.
2966
+ *
2967
+ * @param port - The port the loopback server actually bound.
2968
+ * @param path - The callback path. Default `/callback`.
2969
+ * @returns The exact URI to register with the provider.
2970
+ *
2971
+ * @example
2972
+ * ```ts
2973
+ * redirectUriFor(8730) // → 'http://127.0.0.1:8730/callback'
2974
+ * ```
2975
+ */
2976
+ declare function redirectUriFor(port: number, path?: string): string;
2977
+ /** A request arriving at the loopback callback server. */
2978
+ interface LoopbackRequest {
2979
+ /** The request target, e.g. `/callback?code=…&state=…`. */
2980
+ url: string;
2981
+ /** Request headers, any casing. */
2982
+ headers: Record<string, string | string[] | undefined>;
2983
+ method?: string;
2984
+ }
2985
+ /** The verdict on a loopback request. */
2986
+ type LoopbackVerdict = {
2987
+ ok: true;
2988
+ code: string;
2989
+ state: string;
2990
+ } | {
2991
+ ok: false;
2992
+ status: 404 | 400 | 403;
2993
+ reason: string;
2994
+ };
2995
+ /**
2996
+ * Validate a request hitting the one-shot loopback server.
2997
+ *
2998
+ * The engineering log's note on this is blunt: *"binding 127.0.0.1 is NOT enough."* A page on any
2999
+ * website can point a `<form>` or an `<img>` at `http://127.0.0.1:8730/callback?...`, and DNS
3000
+ * rebinding lets an attacker's domain resolve to 127.0.0.1 so the browser sends the request with
3001
+ * **their** hostname in the `Host` header. Binding the interface stops remote packets; it does not
3002
+ * stop the user's own browser being aimed at the port.
3003
+ *
3004
+ * The surveyed implementation is good on three counts — one-shot, no reflected parameters, 404s
3005
+ * non-callback paths — and **does not validate `Host`**. This adds it: the header must be a literal
3006
+ * loopback address with the expected port, so a rebound hostname is refused before the `code` is
3007
+ * ever read.
3008
+ *
3009
+ * @param request - The incoming request.
3010
+ * @param expected - The port and path the server bound.
3011
+ * @returns The extracted `code`/`state`, or a refusal with a status.
3012
+ *
3013
+ * @example
3014
+ * ```ts
3015
+ * validateLoopbackRequest(
3016
+ * { url: '/callback?code=a&state=b', headers: { host: 'evil.com:8730' } },
3017
+ * { port: 8730 },
3018
+ * )
3019
+ * // → { ok: false, status: 403, reason: 'unexpected Host header' }
3020
+ * ```
3021
+ */
3022
+ declare function validateLoopbackRequest(request: LoopbackRequest, expected: {
3023
+ port: number;
3024
+ path?: string;
3025
+ }): LoopbackVerdict;
3026
+ /**
3027
+ * Is this `Host` header a literal loopback address on the expected port?
3028
+ *
3029
+ * Only the literal forms pass. **`localhost` is rejected on purpose**: it is a name, and a name is
3030
+ * exactly what DNS rebinding controls.
3031
+ */
3032
+ declare function isLoopbackHost(host: string, port: number): boolean;
3033
+ /** One outstanding authorization attempt. */
3034
+ interface OAuthStateRecord {
3035
+ state: string;
3036
+ connectionId: string;
3037
+ /** PKCE verifier, held until the token exchange. */
3038
+ verifier?: string;
3039
+ /** Absolute epoch ms. */
3040
+ expiresAt: number;
3041
+ /** Anything the host needs on the way back. **Never a secret.** */
3042
+ meta?: Record<string, string>;
3043
+ }
3044
+ /** Options for {@link createOAuthStateStore}. */
3045
+ interface OAuthStateStoreOptions {
3046
+ now?: () => number;
3047
+ /** Time-to-live. Default 10 minutes, per the spec. */
3048
+ ttlMs?: number;
3049
+ /** Random state generator, injectable for tests. Must produce ≥192 bits of entropy. */
3050
+ randomState?: () => string;
3051
+ }
3052
+ /** An atomic, single-use OAuth state store. */
3053
+ interface OAuthStateStore {
3054
+ /** Mint a state for a connection. */
3055
+ issue(connectionId: string, options?: {
3056
+ verifier?: string;
3057
+ meta?: Record<string, string>;
3058
+ }): OAuthStateRecord;
3059
+ /**
3060
+ * **Atomically** take the record for a state. A second call with the same state returns `null`.
3061
+ *
3062
+ * There is deliberately no `peek`.
3063
+ */
3064
+ consume(state: string): OAuthStateRecord | null;
3065
+ /** Drop expired records. Returns how many went. */
3066
+ sweep(): number;
3067
+ /** Outstanding count, for tests and diagnostics. */
3068
+ size(): number;
3069
+ }
3070
+ /**
3071
+ * A CSRF state store whose only read is a **consume**.
3072
+ *
3073
+ * §6 L11: one surveyed implementation *peeks* at the state to look up the connection and then
3074
+ * consumes it in a second call. Two concurrent callbacks both pass the peek, and the window between
3075
+ * the two calls is exactly the race a CSRF state exists to close. The invariant is *one atomic
3076
+ * consume, checked at the point of use* — so this store **has no peek to misuse**. Anything the
3077
+ * caller wanted from a peek is returned by the consume itself.
3078
+ *
3079
+ * @param options - Clock, TTL and randomness, all injectable.
3080
+ * @returns The store.
3081
+ *
3082
+ * @example
3083
+ * ```ts
3084
+ * const store = createOAuthStateStore()
3085
+ * const { state } = store.issue('conn-1', { verifier })
3086
+ * store.consume(state) // → the record
3087
+ * store.consume(state) // → null. Always.
3088
+ * ```
3089
+ */
3090
+ declare function createOAuthStateStore(options?: OAuthStateStoreOptions): OAuthStateStore;
3091
+ /**
3092
+ * Base64url-encode bytes, without padding.
3093
+ *
3094
+ * Encoded by hand rather than through `btoa` or `Buffer`: this package targets the browser *and*
3095
+ * QuickJS *and* Node, `btoa` is absent in some of those, `Buffer` in others, and a panel that
3096
+ * assumes either one has quietly taken a runtime dependency on its substrate. Twelve lines removes
3097
+ * the question.
3098
+ *
3099
+ * @param bytes - Raw bytes.
3100
+ * @returns Unpadded base64url text, as RFC 7636 requires for a PKCE verifier.
3101
+ *
3102
+ * @example
3103
+ * ```ts
3104
+ * base64Url(new Uint8Array([255, 254, 253])) // → '__79'
3105
+ * ```
3106
+ */
3107
+ declare function base64Url(bytes: Uint8Array): string;
3108
+ /** A PKCE verifier/challenge pair. */
3109
+ interface PkcePair {
3110
+ verifier: string;
3111
+ challenge: string;
3112
+ method: 'S256';
3113
+ }
3114
+ /**
3115
+ * Generate a PKCE pair.
3116
+ *
3117
+ * **S256 only.** `plain` is still in RFC 7636 and is worthless: the "challenge" is the verifier, so
3118
+ * anything that can read the authorization request can complete the exchange. Offering it as an
3119
+ * option would only ever be a way to pick the broken one.
3120
+ *
3121
+ * @param digest - SHA-256 implementation. Defaults to WebCrypto; injectable for tests.
3122
+ * @returns The pair.
3123
+ *
3124
+ * @example
3125
+ * ```ts
3126
+ * const { verifier, challenge } = await createPkcePair()
3127
+ * ```
3128
+ */
3129
+ declare function createPkcePair(digest?: (input: Uint8Array) => Promise<Uint8Array>): Promise<PkcePair>;
3130
+ /** A stored OAuth credential. **Never rendered, never serialized into a document.** */
3131
+ interface StoredOAuthToken {
3132
+ accessToken: string;
3133
+ refreshToken?: string;
3134
+ /** Absolute epoch ms. Never a duration — see {@link mergeRefreshResponse}. */
3135
+ expiresAt?: number;
3136
+ tokenType?: string;
3137
+ scope?: string;
3138
+ }
3139
+ /** A token endpoint's response. */
3140
+ interface TokenEndpointResponse {
3141
+ access_token?: string;
3142
+ refresh_token?: string;
3143
+ /** Seconds from *now*, as OAuth defines it. */
3144
+ expires_in?: number;
3145
+ token_type?: string;
3146
+ scope?: string;
3147
+ }
3148
+ /** Clock skew allowance, ms. */
3149
+ declare const EXPIRY_SKEW_MS = 60000;
3150
+ /**
3151
+ * Merge a refresh response over the stored credential.
3152
+ *
3153
+ * **§6 L4, both halves:**
3154
+ *
3155
+ * 1. *Expiry is absolute epoch ms, with a 60 s skew.* A stored `expires_in` is a duration measured
3156
+ * from a moment nobody wrote down; after a restart it is unusable, and after a suspend it is a
3157
+ * lie. It is converted here, once, at the only point where "now" is known to be the right now.
3158
+ * 2. 🔴 *A response omitting `refresh_token` must **preserve** the prior one.* Most providers rotate
3159
+ * the refresh token on every use, but a large minority — and every provider using a
3160
+ * non-expiring refresh token — return only a new access token. Overwriting with `undefined`
3161
+ * destroys the only credential that can ever get another access token, and the failure does not
3162
+ * appear until the *next* refresh, long after the code that caused it.
3163
+ *
3164
+ * @param prior - What is stored today, if anything.
3165
+ * @param response - The token endpoint's answer.
3166
+ * @param now - Current epoch ms.
3167
+ * @returns The credential to store.
3168
+ *
3169
+ * @example
3170
+ * ```ts
3171
+ * mergeRefreshResponse({ accessToken: 'a', refreshToken: 'KEEP' }, { access_token: 'b' }, 0)
3172
+ * // → { accessToken: 'b', refreshToken: 'KEEP' }
3173
+ * ```
3174
+ */
3175
+ declare function mergeRefreshResponse(prior: StoredOAuthToken | undefined, response: TokenEndpointResponse, now: number): StoredOAuthToken;
3176
+ /**
3177
+ * Does this credential need refreshing?
3178
+ *
3179
+ * @param token - The stored credential.
3180
+ * @param now - Current epoch ms.
3181
+ * @param skewMs - Safety margin. Default {@link EXPIRY_SKEW_MS}.
3182
+ * @returns Whether to refresh before using it.
3183
+ *
3184
+ * @example
3185
+ * ```ts
3186
+ * needsRefresh({ accessToken: 'a', expiresAt: 100_000 }, 40_000) // → true (inside the 60s skew)
3187
+ * ```
3188
+ */
3189
+ declare function needsRefresh(token: StoredOAuthToken | undefined, now: number, skewMs?: number): boolean;
3190
+ /** Coordinates refreshes so N consumers produce one exchange. */
3191
+ interface RefreshCoordinator<T> {
3192
+ /**
3193
+ * Refresh `key`, or join the refresh already running for it.
3194
+ *
3195
+ * @param key - Credential reference.
3196
+ * @param run - Performs the exchange. Called at most once per concurrent burst.
3197
+ */
3198
+ refresh(key: string, run: () => Promise<T>): Promise<T>;
3199
+ /** How many exchanges are in flight. */
3200
+ inFlight(): number;
3201
+ /** Refresh many keys, reporting **per key** — §6 L6. */
3202
+ refreshAll(keys: string[], run: (key: string) => Promise<T>): Promise<{
3203
+ key: string;
3204
+ ok: boolean;
3205
+ value?: T;
3206
+ error?: string;
3207
+ }[]>;
3208
+ }
3209
+ /**
3210
+ * Dedupe concurrent refreshes of the same credential.
3211
+ *
3212
+ * §6 L5a: five panels sharing one connection all notice the expiry in the same tick and all start a
3213
+ * refresh. With a rotating refresh token that is not merely wasteful — the first exchange
3214
+ * invalidates the token the other four are holding, so four of them fail and, depending on the
3215
+ * provider, the whole family is revoked for reuse. One exchange, four joiners.
3216
+ *
3217
+ * @returns The coordinator.
3218
+ *
3219
+ * @example
3220
+ * ```ts
3221
+ * const coordinator = createRefreshCoordinator<Token>()
3222
+ * await Promise.all([a, b, c].map(() => coordinator.refresh('$cred:1', exchange)))
3223
+ * // `exchange` ran exactly once.
3224
+ * ```
3225
+ */
3226
+ declare function createRefreshCoordinator<T>(): RefreshCoordinator<T>;
3227
+
3228
+ /**
3229
+ * The Connectors panel view — connection list, health, and the credential form.
3230
+ *
3231
+ * Composes `@xenosystem/workbench/primitives`. **The host must import
3232
+ * `@xenosystem/workbench/primitives.css`** (or let the workbench inject it) or this renders unstyled with
3233
+ * no error.
3234
+ *
3235
+ * Two things this view deliberately does NOT have:
3236
+ *
3237
+ * - **No secret reveal.** `xeno-hub`'s `EnvironmentSecretsSection` exposes stored secrets behind an
3238
+ * eye-toggle; that affordance does not come with this panel. The panel never receives a stored
3239
+ * secret, so there is nothing to reveal.
3240
+ * - **No masked value round-trip.** A form field is uncontrolled-by-design local state that is
3241
+ * cleared at submit; nothing here reads a value back from a saved document.
3242
+ *
3243
+ * @module
3244
+ */
3245
+
3246
+ /** Props for {@link ConnectorsPanelView}. */
3247
+ interface ConnectorsPanelViewProps {
3248
+ controller: ConnectorsController;
3249
+ /** Hint shown when no source is configured. */
3250
+ emptyHint?: string;
3251
+ }
3252
+ /** The Connectors panel view. */
3253
+ declare function ConnectorsPanelView({ controller, emptyHint, }: ConnectorsPanelViewProps): ReactNode;
3254
+
3255
+ export { AGENT_HIDDEN_COMMANDS, ALL_AGGREGATES, ALL_CHART_TYPES, type BuildChartOptions, type BuildSelectOptions, type BuildSelectResult, CHART_EMPTY_FILL, CHART_PALETTE_STEPS, CHART_PANEL_ID, CONNECTORS_PANEL_ID, type CellAddress, ChartController, type ChartControllerOptions, type ChartHostBridge, type ChartPanelState, ChartPanelView, type ChartPanelViewProps, type ChartRenderContext, type ChartRenderInput, type ChartRenderOptions, type ChartRenderer, type ChartViewState, type ColumnLayout, type ColumnWindow, ConnectorExecuteInput, ConnectorExecuteOutput, ConnectorTransport, ConnectorsController, type ConnectorsControllerOptions, type ConnectorsHostBridge, ConnectorsPanelState, ConnectorsPanelView, type ConnectorsPanelViewProps, type ConnectorsRenderContext, ConnectorsViewState, type CreateChartPanelOptions, type CreateConnectorExecutorOptions, type CreateConnectorsPanelOptions, type CreateFieldsPanelOptions, type CreateMetricsPanelOptions, type CreateTablePanelOptions, type CredentialDraft, type CredentialErrors, DEFAULT_COLUMN_WIDTH, DEFAULT_PAGE_SIZE, DEFAULT_RANGES, type DeltaDirection, EMPTY_LAYOUT, EXPIRY_SKEW_MS, type ExecuteError, FIELDS_PANEL_ID, FieldsController, type FieldsControllerOptions, type FieldsHostBridge, type FieldsPanelState, FieldsPanelView, type FieldsPanelViewProps, type FieldsRenderContext, type FieldsViewState, type HostSqlResolver, IMPLEMENTED_VIEW_MODES, type LoopbackRequest, type LoopbackVerdict, METRICS_PANEL_ID, MIN_COLUMN_WIDTH, MONO_FILL_PALETTE, MONO_PALETTE, MetricsController, type MetricsControllerOptions, type MetricsHostBridge, type MetricsPanelState, MetricsPanelView, type MetricsPanelViewProps, type MetricsRenderContext, type MetricsViewState, type NumericConstraints, type OAuthStateRecord, type OAuthStateStore, type OAuthStateStoreOptions, type ParseCsvOptions, type ParseResult, type ParsedCsv, type PkcePair, PushdownSplit, type RefreshCoordinator, type ResolvedColumn, SHELF_PRESETS, type StoredOAuthToken, SubstrateFacilities, TABLE_PANEL_ID, TableController, type TableControllerOptions, type TableHostBridge, type TablePanelState, TablePanelView, type TablePanelViewProps, type TableRenderContext, type TableViewMode, type TableViewState, type TokenEndpointResponse, type VirtualWindow, type XenoAlertLevel, type XenoCalculatedFieldRequest, type XenoCellEdit, type XenoChartData, type XenoChartDrillDown, type XenoChartEncoding, type XenoChartExportRequest, type XenoChartHighlight, type XenoChartSeries, type XenoChartType, XenoConnection, XenoConnectionStatus, XenoConnectorFetch, XenoConnectorFileReader, XenoConnectorQueryRequest, XenoCredentialField, XenoCredentialFormSpec, XenoCredentialResult, type XenoFieldStat, type XenoMetricAlert, type XenoMetricRange, type XenoMetricSpec, type XenoMetricThreshold, type XenoMetricValue, XenoResolverErrorCode, XenoRestPaging, type XenoRowActivate, type XenoShelfEntry, type XenoShelfSpec, XenoSqlEngine, applyPrecision, base64Url, breachSideOf, buildChartData, buildRestUrl, buildSelect, chartManifest, chartPanel, clamp, classifyHttpStatus, coerceCsvValues, columnIndexAt, columnsOf, compileForHost, computeColumnWindow, computeWindow, connectorsManifest, connectorsPanel, constraintsOfColumn, createChartPanel, createConnectorExecutor, createConnectorsPanel, createFieldsPanel, createMetricsPanel, createOAuthStateStore, createPkcePair, createRefreshCoordinator, createTablePanel, credentialSpecProblem, defaultAggregateFor, defaultEncoding, deltaToneOf, emptyFilterGroup, entityOf, escapeLikePattern, executeCsvFile, executeDuckDb, executeGraphql, executeJsonFile, executeRest, executeSql, extractJsonPath, extractRows, fieldsManifest, fieldsPanel, firstGraphqlError, frozenWidth, getChartRenderer, isCategorical, isFieldVisible, isFilled, isLeafFilter, isLoopbackHost, isSafeTableName, mergeRefreshResponse, metricsManifest, metricsPanel, moveCell, moveColumn, needsRefresh, nextPageToken, normalizeLayout, normalizeNumeric, offerableAggregates, offerableFilterOps, parseCsv, parseJsonBody, parseJsonPath, parseLinkHeader, parseNumeric, pruneHiddenValues, queriesEqual, quoteIdent, quoteLiteral, redirectUriFor, renderFilterGroup, resolveChartColors, resolveColumns, rowsForMark, scrollLeftToReveal, scrollTopToReveal, setChartRenderer, setColumnVisible, setColumnWidth, setFrozenCount, sniffDelimiter, suggestedShelfFor, tableManifest, tablePanel, toChartJsConfig, toExecuteError, totalWidth, uniqueTableName, unsupportedMessage, validateCredentialDraft, validateEncoding, validateLoopbackRequest, visibleFields };