@svgrid/enterprise 3.1.21 → 3.1.23

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.
Files changed (89) hide show
  1. package/dist/SvGridBoard.svelte +39 -0
  2. package/dist/SvGridGallery.svelte +23 -0
  3. package/dist/SvGridRowCount.svelte +23 -0
  4. package/dist/SvGridRowCount.svelte.d.ts +6 -0
  5. package/dist/SvGridScheduler.svelte +25 -7
  6. package/dist/cdn/svgrid-enterprise.svelte-external.js +13838 -13554
  7. package/dist/designer/assets/GridFooter-DKMfZpL4.js +2 -0
  8. package/dist/designer/assets/GridMenus-DWDghydL.js +6 -0
  9. package/dist/designer/assets/{PinSplitter-C5AwA1xV.js → PinSplitter-BfIjd7pi.js} +1 -1
  10. package/dist/designer/assets/grid-extension.svelte-G1ng0xd2.js +1 -0
  11. package/dist/designer/assets/index-CHx3F4Ij.css +1 -0
  12. package/dist/designer/assets/{index-DLJjuVA4.js → index-CIxqRzKJ.js} +324 -324
  13. package/dist/designer/assets/merges-Bg-KqPEl.js +1 -0
  14. package/dist/designer/index.html +3 -3
  15. package/dist/expressions/compile.js +11 -7
  16. package/dist/expressions/evaluate.js +4 -0
  17. package/dist/expressions/expression-columns.d.ts +4 -1
  18. package/dist/expressions/expression-columns.js +12 -3
  19. package/dist/expressions/parse.js +19 -7
  20. package/dist/fields/RecordView.svelte +5 -2
  21. package/dist/fields/activity.svelte.d.ts +5 -2
  22. package/dist/fields/activity.svelte.js +4 -3
  23. package/dist/fields/grid-access.d.ts +1 -1
  24. package/dist/fields/grid-access.js +13 -3
  25. package/dist/fields/index.d.ts +18 -0
  26. package/dist/fields/index.js +31 -0
  27. package/dist/filter-labels.d.ts +16 -0
  28. package/dist/filter-labels.js +83 -0
  29. package/dist/index.d.ts +4 -0
  30. package/dist/index.js +7 -0
  31. package/dist/install.js +8 -0
  32. package/dist/node/studio.js +12 -5
  33. package/dist/relative-date-core.d.ts +40 -0
  34. package/dist/relative-date-core.js +85 -0
  35. package/dist/relative-dates.d.ts +28 -0
  36. package/dist/relative-dates.js +61 -0
  37. package/dist/review/GhostCell.svelte +60 -0
  38. package/dist/review/GhostCell.svelte.d.ts +14 -0
  39. package/dist/review/RichCellMark.svelte +72 -0
  40. package/dist/review/RichCellMark.svelte.d.ts +8 -0
  41. package/dist/review/SvReviewPanel.svelte +759 -0
  42. package/dist/review/SvReviewPanel.svelte.d.ts +16 -0
  43. package/dist/review/adapters.d.ts +79 -0
  44. package/dist/review/adapters.js +119 -0
  45. package/dist/review/agent.d.ts +52 -0
  46. package/dist/review/agent.js +232 -0
  47. package/dist/review/card-marks.svelte.d.ts +42 -0
  48. package/dist/review/card-marks.svelte.js +51 -0
  49. package/dist/review/cluster.d.ts +26 -0
  50. package/dist/review/cluster.js +377 -0
  51. package/dist/review/equal.d.ts +9 -0
  52. package/dist/review/equal.js +35 -0
  53. package/dist/review/formats.d.ts +31 -0
  54. package/dist/review/formats.js +37 -0
  55. package/dist/review/ghost.d.ts +17 -0
  56. package/dist/review/ghost.js +104 -0
  57. package/dist/review/index.d.ts +42 -0
  58. package/dist/review/index.js +36 -0
  59. package/dist/review/messages.d.ts +113 -0
  60. package/dist/review/messages.js +135 -0
  61. package/dist/review/model.svelte.d.ts +262 -0
  62. package/dist/review/model.svelte.js +883 -0
  63. package/dist/review/pending-filter.d.ts +26 -0
  64. package/dist/review/pending-filter.js +71 -0
  65. package/dist/review/rich-cells.d.ts +1 -0
  66. package/dist/review/rich-cells.js +36 -0
  67. package/dist/review/types.d.ts +83 -0
  68. package/dist/review/types.js +7 -0
  69. package/dist/row-count.d.ts +28 -0
  70. package/dist/row-count.js +39 -0
  71. package/dist/sort-blanks.d.ts +8 -0
  72. package/dist/sort-blanks.js +47 -0
  73. package/dist/studio/ui-components.generated.js +1 -1
  74. package/dist/upgrade-prompt.d.ts +1 -1
  75. package/dist/version.d.ts +2 -2
  76. package/dist/version.js +2 -2
  77. package/dist/wc/GridFooter-CKC4L36O.js +162 -0
  78. package/dist/wc/{GridMenus-DVGuqp2l.js → GridMenus-h7RsvcmI.js} +320 -303
  79. package/dist/wc/{PinSplitter-Btco0txr.js → PinSplitter-BN6ybmjs.js} +1 -1
  80. package/dist/wc/grid-extension.svelte-CDf5EUyz.js +27 -0
  81. package/dist/wc/merges-DQTOlMWM.js +109 -0
  82. package/dist/wc/sv-sheet-element.js +6753 -6792
  83. package/package.json +9 -3
  84. package/dist/designer/assets/GridFooter-CKBVItAU.js +0 -2
  85. package/dist/designer/assets/GridMenus-Dzwu7M9r.js +0 -6
  86. package/dist/designer/assets/grid-extension.svelte-BZ7AqyJI.js +0 -1
  87. package/dist/designer/assets/index-WCxj6WfJ.css +0 -1
  88. package/dist/wc/GridFooter-BgoA77TJ.js +0 -145
  89. package/dist/wc/grid-extension.svelte-CucBS4FL.js +0 -20
@@ -0,0 +1,16 @@
1
+ import { type ReviewMessages } from './messages.js';
2
+ import type { Review, ReviewOutcome } from './model.svelte.js';
3
+ type $$ComponentProps = {
4
+ review: Review<any>;
5
+ /** The heading; default `messages.title`. */
6
+ title?: string;
7
+ /** Wording for this panel, over the review's own `messages`. */
8
+ messages?: Partial<ReviewMessages>;
9
+ /** Ask before deciding more than this many proposals at once. */
10
+ confirmOver?: number;
11
+ /** Called after each decision with what happened. */
12
+ onDecide?: (outcome: ReviewOutcome) => void;
13
+ };
14
+ declare const SvReviewPanel: import("svelte").Component<$$ComponentProps, {}, "">;
15
+ type SvReviewPanel = ReturnType<typeof SvReviewPanel>;
16
+ export default SvReviewPanel;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Producers: turn what the AI helpers and imports return into proposals.
3
+ *
4
+ * The AI helpers key predictions by `rowIndex`, an index into `api.getData()`
5
+ * as it was when the helper read it. A sort, a filter or an edit during the
6
+ * await does not move data indices, but an insert or a new `data` array does,
7
+ * so the adapters resolve indices against a snapshot taken BEFORE the call,
8
+ * then key each proposal by row id. Indices the helper was not asked about,
9
+ * or that fall outside the snapshot, are dropped and counted.
10
+ */
11
+ import { type AIClassifyOptions, type AIClassifyResult, type AISmartFillOptions, type AISmartFillResult } from '@svgrid/grid';
12
+ import type { ReviewGridApi } from './model.svelte.js';
13
+ import type { ProposalInput, ProposalSource } from './types.js';
14
+ type RowIdApi = Pick<ReviewGridApi, 'getOption'>;
15
+ export type AdapterResult = {
16
+ source: ProposalSource;
17
+ title: string;
18
+ proposals: ProposalInput[];
19
+ /** Predictions dropped: an index outside the request or the snapshot. */
20
+ dropped: number;
21
+ };
22
+ /** Blank cells of `field`: what `aiSmartFill` targets when given no indices. */
23
+ export declare const blankTargets: (snapshot: ReadonlyArray<unknown>, field: string) => number[];
24
+ export declare function fromSmartFill<TData>(result: AISmartFillResult, snapshot: ReadonlyArray<TData>, opts: {
25
+ api: RowIdApi;
26
+ targets?: ReadonlyArray<number>;
27
+ source?: ProposalSource;
28
+ }): AdapterResult;
29
+ export declare function fromClassify<TData>(result: AIClassifyResult, snapshot: ReadonlyArray<TData>, opts: {
30
+ api: RowIdApi;
31
+ targets?: ReadonlyArray<number>;
32
+ source?: ProposalSource;
33
+ }): AdapterResult;
34
+ /**
35
+ * Diff incoming rows (an import, a sync) against the grid's data by `key`:
36
+ * one proposal per field that differs. Incoming rows with no match, and
37
+ * fields not in `fields` when it is given, are not proposed; unmatched rows
38
+ * are returned so the app can show them (adding rows comes in a later release).
39
+ */
40
+ export declare function fromRows<TData>(incoming: ReadonlyArray<Partial<TData>>, opts: {
41
+ api: Pick<ReviewGridApi, 'getOption' | 'getData'>;
42
+ key: keyof TData & string;
43
+ source: ProposalSource;
44
+ title?: string;
45
+ fields?: ReadonlyArray<keyof TData & string>;
46
+ }): AdapterResult & {
47
+ unmatched: Array<Partial<TData>>;
48
+ };
49
+ type Addable = {
50
+ add(input: {
51
+ source: ProposalSource;
52
+ title?: string;
53
+ proposals: ReadonlyArray<ProposalInput>;
54
+ }): unknown;
55
+ };
56
+ /** Snapshot, run `aiSmartFill`, and add the result to `review` as proposals. */
57
+ export declare function reviewSmartFill(review: Addable, api: ReviewGridApi, opts: AISmartFillOptions): Promise<AdapterResult>;
58
+ /** Snapshot, run `aiClassify`, and add the result to `review` as proposals. */
59
+ export declare function reviewClassify(review: Addable, api: ReviewGridApi, opts: AIClassifyOptions): Promise<AdapterResult>;
60
+ /**
61
+ * Lift a `createStagedEditing` buffer into proposals, to route a person's
62
+ * staged edits to someone else for approval instead of committing them.
63
+ *
64
+ * Staged editing has already written its values into the grid, and a
65
+ * proposal is checked against the value it saw (`before`). So revert the
66
+ * stage first (`stage.revert(...)`), then add these: the cells hold the
67
+ * original values again and the proposals carry the staged ones.
68
+ */
69
+ export declare function fromStaged(changes: ReadonlyArray<{
70
+ rowId: string;
71
+ columnId: string;
72
+ original: unknown;
73
+ staged: unknown;
74
+ }>, opts: {
75
+ api: Pick<ReviewGridApi, 'getColumns'>;
76
+ source: ProposalSource;
77
+ title?: string;
78
+ }): AdapterResult;
79
+ export {};
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Producers: turn what the AI helpers and imports return into proposals.
3
+ *
4
+ * The AI helpers key predictions by `rowIndex`, an index into `api.getData()`
5
+ * as it was when the helper read it. A sort, a filter or an edit during the
6
+ * await does not move data indices, but an insert or a new `data` array does,
7
+ * so the adapters resolve indices against a snapshot taken BEFORE the call,
8
+ * then key each proposal by row id. Indices the helper was not asked about,
9
+ * or that fall outside the snapshot, are dropped and counted.
10
+ */
11
+ import { aiClassify, aiSmartFill, } from '@svgrid/grid';
12
+ import { isBlank, sameValue } from './equal.js';
13
+ function rowIdOf(api) {
14
+ const fn = api.getOption('getRowId');
15
+ if (!fn)
16
+ throw new Error('[svgrid review] the grid needs a getRowId prop');
17
+ return fn;
18
+ }
19
+ function fromPredictions(predictions, field, snapshot, targets, api, reason) {
20
+ const id = rowIdOf(api);
21
+ const asked = new Set(targets);
22
+ const seen = new Set();
23
+ const proposals = [];
24
+ let dropped = 0;
25
+ for (const pr of predictions) {
26
+ const i = pr.rowIndex;
27
+ const row = snapshot[i];
28
+ if (!Number.isInteger(i) || !asked.has(i) || row === undefined || seen.has(i)) {
29
+ dropped++;
30
+ continue;
31
+ }
32
+ seen.add(i);
33
+ const before = row[field];
34
+ if (sameValue(before, pr.value))
35
+ continue;
36
+ proposals.push({ rowId: id(row, i), field, before, after: pr.value, confidence: pr.confidence, reason });
37
+ }
38
+ return { proposals, dropped };
39
+ }
40
+ /** Blank cells of `field`: what `aiSmartFill` targets when given no indices. */
41
+ export const blankTargets = (snapshot, field) => snapshot.flatMap((row, i) => (isBlank(row[field]) ? [i] : []));
42
+ export function fromSmartFill(result, snapshot, opts) {
43
+ const targets = opts.targets ?? blankTargets(snapshot, result.field);
44
+ const { proposals, dropped } = fromPredictions(result.predictions, result.field, snapshot, targets, opts.api, result.rationale || undefined);
45
+ const source = opts.source ?? { kind: 'ai', id: `smart-fill:${result.field}`, label: 'Smart fill' };
46
+ return { source, title: `Smart fill: ${result.field}`, proposals, dropped };
47
+ }
48
+ export function fromClassify(result, snapshot, opts) {
49
+ const targets = opts.targets ?? snapshot.map((_, i) => i);
50
+ const { proposals, dropped } = fromPredictions(result.predictions, result.outputField, snapshot, targets, opts.api, `classified from ${result.inputField}`);
51
+ const source = opts.source ?? { kind: 'ai', id: `classify:${result.outputField}`, label: 'Classify' };
52
+ return { source, title: `Classify: ${result.outputField}`, proposals, dropped };
53
+ }
54
+ /**
55
+ * Diff incoming rows (an import, a sync) against the grid's data by `key`:
56
+ * one proposal per field that differs. Incoming rows with no match, and
57
+ * fields not in `fields` when it is given, are not proposed; unmatched rows
58
+ * are returned so the app can show them (adding rows comes in a later release).
59
+ */
60
+ export function fromRows(incoming, opts) {
61
+ const id = rowIdOf(opts.api);
62
+ const data = opts.api.getData();
63
+ const byKey = new Map();
64
+ data.forEach((row, index) => byKey.set(row[opts.key], { row, index }));
65
+ const proposals = [];
66
+ const unmatched = [];
67
+ for (const next of incoming) {
68
+ const hit = byKey.get(next[opts.key]);
69
+ if (!hit) {
70
+ unmatched.push(next);
71
+ continue;
72
+ }
73
+ const rowId = id(hit.row, hit.index);
74
+ const fields = opts.fields ?? Object.keys(next);
75
+ for (const field of fields) {
76
+ if (field === opts.key || !(field in next))
77
+ continue;
78
+ const before = hit.row[field];
79
+ const after = next[field];
80
+ if (!sameValue(before, after))
81
+ proposals.push({ rowId, field, before, after });
82
+ }
83
+ }
84
+ return { source: opts.source, title: opts.title ?? opts.source.label, proposals, dropped: 0, unmatched };
85
+ }
86
+ /** Snapshot, run `aiSmartFill`, and add the result to `review` as proposals. */
87
+ export async function reviewSmartFill(review, api, opts) {
88
+ const snapshot = api.getData().slice();
89
+ const targets = opts.targetRowIndices ?? blankTargets(snapshot, opts.field);
90
+ const result = await aiSmartFill(api, { ...opts, targetRowIndices: [...targets] });
91
+ const set = fromSmartFill(result, snapshot, { api, targets });
92
+ review.add(set);
93
+ return set;
94
+ }
95
+ /** Snapshot, run `aiClassify`, and add the result to `review` as proposals. */
96
+ export async function reviewClassify(review, api, opts) {
97
+ const snapshot = api.getData().slice();
98
+ const targets = opts.targetRowIndices ?? snapshot.map((_, i) => i);
99
+ const result = await aiClassify(api, { ...opts, targetRowIndices: [...targets] });
100
+ const set = fromClassify(result, snapshot, { api, targets });
101
+ review.add(set);
102
+ return set;
103
+ }
104
+ /**
105
+ * Lift a `createStagedEditing` buffer into proposals, to route a person's
106
+ * staged edits to someone else for approval instead of committing them.
107
+ *
108
+ * Staged editing has already written its values into the grid, and a
109
+ * proposal is checked against the value it saw (`before`). So revert the
110
+ * stage first (`stage.revert(...)`), then add these: the cells hold the
111
+ * original values again and the proposals carry the staged ones.
112
+ */
113
+ export function fromStaged(changes, opts) {
114
+ const fieldOf = new Map(opts.api.getColumns().map((c) => [c.id, c.field ?? c.id]));
115
+ const proposals = changes
116
+ .filter((c) => !sameValue(c.original, c.staged))
117
+ .map((c) => ({ rowId: c.rowId, field: fieldOf.get(c.columnId) ?? c.columnId, before: c.original, after: c.staged }));
118
+ return { source: opts.source, title: opts.title ?? opts.source.label, proposals, dropped: 0 };
119
+ }
@@ -0,0 +1,52 @@
1
+ import type { ReviewGridApi } from './model.svelte.js';
2
+ import type { Proposal, ProposalGroup, ProposalInput, ProposalSource } from './types.js';
3
+ export type GridTool = {
4
+ name: string;
5
+ description: string;
6
+ /** JSON Schema of the input object. */
7
+ inputSchema: Record<string, unknown>;
8
+ execute: (input: any) => Promise<unknown>;
9
+ };
10
+ type ToolReview = {
11
+ add(input: {
12
+ source: ProposalSource;
13
+ title?: string;
14
+ proposals: ReadonlyArray<ProposalInput>;
15
+ }): {
16
+ id: string;
17
+ proposals: Proposal[];
18
+ };
19
+ groups(): ProposalGroup[];
20
+ conflicts(): Proposal[];
21
+ proposals(): Proposal[];
22
+ };
23
+ type ToolApi = Pick<ReviewGridApi, 'getData' | 'getColumns' | 'getOption'>;
24
+ export type GridToolsOptions = {
25
+ review: ToolReview;
26
+ /** The grid api, or a getter for it. */
27
+ api: ToolApi | (() => ToolApi | null | undefined);
28
+ /** Who the proposals are from. Default: an agent named "Agent". */
29
+ source?: ProposalSource;
30
+ /** Fields the agent may propose edits to. Default: every column with a field. */
31
+ fields?: ReadonlyArray<string>;
32
+ /** Most rows one query returns. Default 200. */
33
+ maxRows?: number;
34
+ /** Prefix for tool names. Default 'grid_'. */
35
+ prefix?: string;
36
+ /** Most edits one grid_propose_edits call may carry. Default 500. */
37
+ maxEdits?: number;
38
+ };
39
+ export declare function createGridTools(opts: GridToolsOptions): GridTool[];
40
+ /**
41
+ * Register tools with the browser's WebMCP API, so an agent driving the page
42
+ * can call them. Experimental: WebMCP is a draft (`document.modelContext`,
43
+ * earlier `navigator.modelContext`), and this does nothing where it is
44
+ * missing. Abort `signal`, or call `unregister`, to remove them.
45
+ */
46
+ export declare function registerWebMcpTools(tools: ReadonlyArray<GridTool>, opts?: {
47
+ signal?: AbortSignal;
48
+ }): {
49
+ supported: boolean;
50
+ unregister: () => void;
51
+ };
52
+ export {};
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Tools an AI agent can call to work on a grid's data, where every write is a
3
+ * proposal in a change review and a person decides. No tool writes the data.
4
+ *
5
+ * `createGridTools` returns plain tool definitions (name, description, JSON
6
+ * schema, execute) for any tool-use loop: an LLM SDK, an MCP server of your
7
+ * own, or the browser through `registerWebMcpTools`.
8
+ *
9
+ * ```ts
10
+ * const tools = createGridTools({ review, api: () => api, fields: ['region', 'phone'] })
11
+ * registerWebMcpTools(tools) // experimental: browsers with WebMCP
12
+ * ```
13
+ */
14
+ import { isBlank, sameValue } from './equal.js';
15
+ const OPS = ['equals', 'notEquals', 'contains', 'blank', 'notBlank', 'gt', 'lt'];
16
+ function matches(v, w) {
17
+ switch (w.op) {
18
+ case 'equals': return sameValue(v, w.value);
19
+ case 'notEquals': return !sameValue(v, w.value);
20
+ case 'contains': return String(v ?? '').toLowerCase().includes(String(w.value ?? '').toLowerCase());
21
+ case 'blank': return isBlank(v);
22
+ case 'notBlank': return !isBlank(v);
23
+ case 'gt': return v != null && v > w.value;
24
+ case 'lt': return v != null && v < w.value;
25
+ }
26
+ }
27
+ /**
28
+ * Why a value does not fit its column, or null. A model can send a string for
29
+ * a number or an object for a text cell; those are skipped, not proposed.
30
+ */
31
+ function wrongType(type, current, value) {
32
+ if (value === null || value === undefined || value === '')
33
+ return null; // clearing a cell
34
+ if (Array.isArray(current))
35
+ return Array.isArray(value) ? null : 'expects a list';
36
+ if (typeof value === 'object')
37
+ return 'expects a single value, not an object or a list';
38
+ if (type === 'number' && (typeof value !== 'number' || !Number.isFinite(value)))
39
+ return 'expects a number';
40
+ if (type === 'boolean' && typeof value !== 'boolean')
41
+ return 'expects true or false';
42
+ if ((type === 'date' || type === 'dateString') && (typeof value !== 'string' || Number.isNaN(Date.parse(value))))
43
+ return 'expects a date (YYYY-MM-DD)';
44
+ return null;
45
+ }
46
+ export function createGridTools(opts) {
47
+ const prefix = opts.prefix ?? 'grid_';
48
+ const source = opts.source ?? { kind: 'agent', id: 'agent', label: 'Agent' };
49
+ const maxRows = opts.maxRows ?? 200;
50
+ const maxEdits = opts.maxEdits ?? 500;
51
+ const api = () => {
52
+ const a = typeof opts.api === 'function' ? opts.api() : opts.api;
53
+ if (!a)
54
+ throw new Error('the grid is not ready yet');
55
+ return a;
56
+ };
57
+ const rowIdOf = (a) => {
58
+ const fn = a.getOption('getRowId');
59
+ if (!fn)
60
+ throw new Error('the grid needs a getRowId prop');
61
+ return fn;
62
+ };
63
+ const columns = (a) => a.getColumns().filter((c) => c.field);
64
+ const writable = (a) => new Set(opts.fields ?? columns(a).map((c) => c.field));
65
+ const describe = {
66
+ name: `${prefix}describe`,
67
+ description: 'Describe the table: its columns (field, header, data type), how many rows it has, and which fields you may propose edits to. ' +
68
+ 'Edits you propose are not applied: a person reviews them in groups and accepts or rejects them.',
69
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
70
+ async execute() {
71
+ const a = api();
72
+ const canWrite = writable(a);
73
+ return {
74
+ rowCount: a.getData().length,
75
+ columns: columns(a).map((c) => ({ field: c.field, header: c.header, type: c.cellDataType ?? 'text', writable: canWrite.has(c.field) })),
76
+ };
77
+ },
78
+ };
79
+ const query = {
80
+ name: `${prefix}query_rows`,
81
+ description: `Read rows of the table. Filter with "where" (all conditions must hold), pick fields, page with offset/limit (at most ${maxRows}). ` +
82
+ 'Every row comes with its rowId, which grid_propose_edits needs.',
83
+ inputSchema: {
84
+ type: 'object',
85
+ properties: {
86
+ where: {
87
+ type: 'array',
88
+ items: {
89
+ type: 'object',
90
+ properties: { field: { type: 'string' }, op: { type: 'string', enum: OPS }, value: {} },
91
+ required: ['field', 'op'],
92
+ },
93
+ },
94
+ fields: { type: 'array', items: { type: 'string' } },
95
+ offset: { type: 'integer', minimum: 0 },
96
+ limit: { type: 'integer', minimum: 1, maximum: maxRows },
97
+ },
98
+ additionalProperties: false,
99
+ },
100
+ async execute(input = {}) {
101
+ const a = api();
102
+ const id = rowIdOf(a);
103
+ const known = new Set(columns(a).map((c) => c.field));
104
+ for (const w of input.where ?? []) {
105
+ if (!known.has(w.field))
106
+ throw new Error(`unknown field "${w.field}"`);
107
+ if (!OPS.includes(w.op))
108
+ throw new Error(`unknown op "${w.op}"`);
109
+ }
110
+ const fields = (input.fields ?? [...known]).filter((f) => known.has(f));
111
+ const hits = a.getData().flatMap((row, i) => (input.where ?? []).every((w) => matches(row[w.field], w)) ? [{ row, i }] : []);
112
+ // The schema says integers; a model can still send "10" or -5.
113
+ const offset = Math.max(0, Math.trunc(Number(input.offset) || 0));
114
+ const limit = Math.min(Math.max(1, Math.trunc(Number(input.limit) || maxRows)), maxRows);
115
+ return {
116
+ total: hits.length,
117
+ offset,
118
+ rows: hits.slice(offset, offset + limit).map(({ row, i }) => ({
119
+ rowId: String(id(row, i)),
120
+ ...Object.fromEntries(fields.map((f) => [f, row[f]])),
121
+ })),
122
+ };
123
+ },
124
+ };
125
+ const propose = {
126
+ name: `${prefix}propose_edits`,
127
+ description: 'Propose new values for cells. Nothing is written: each edit becomes a proposal a person reviews. ' +
128
+ 'Give a short reason per edit, and a confidence from 0 to 1 when you are unsure. ' +
129
+ 'Edits to unknown rows, to fields you may not edit, or that change nothing are skipped and reported.',
130
+ inputSchema: {
131
+ type: 'object',
132
+ properties: {
133
+ title: { type: 'string', description: 'What this batch does, in a few words.' },
134
+ edits: {
135
+ type: 'array',
136
+ minItems: 1,
137
+ maxItems: maxEdits,
138
+ items: {
139
+ type: 'object',
140
+ properties: {
141
+ rowId: { type: 'string' },
142
+ field: { type: 'string' },
143
+ value: {},
144
+ reason: { type: 'string' },
145
+ confidence: { type: 'number', minimum: 0, maximum: 1 },
146
+ },
147
+ required: ['rowId', 'field', 'value'],
148
+ },
149
+ },
150
+ },
151
+ required: ['edits'],
152
+ additionalProperties: false,
153
+ },
154
+ async execute(input) {
155
+ const a = api();
156
+ const edits = Array.isArray(input?.edits) ? input.edits : [];
157
+ if (edits.length > maxEdits)
158
+ throw new Error(`at most ${maxEdits} edits per call; send the rest in another call`);
159
+ const id = rowIdOf(a);
160
+ const canWrite = writable(a);
161
+ const typeOf = new Map(a.getColumns().filter((c) => c.field).map((c) => [c.field, c.cellDataType ?? 'text']));
162
+ const rows = new Map(a.getData().map((row, i) => [String(id(row, i)), row]));
163
+ const skipped = [];
164
+ const proposals = [];
165
+ edits.forEach((e, index) => {
166
+ const row = rows.get(String(e?.rowId));
167
+ if (!row)
168
+ return skipped.push({ index, why: `no row with rowId "${e?.rowId}"` });
169
+ if (!canWrite.has(e.field))
170
+ return skipped.push({ index, why: `field "${e.field}" is not writable` });
171
+ const why = wrongType(typeOf.get(e.field), row[e.field], e.value);
172
+ if (why)
173
+ return skipped.push({ index, why: `${e.field} ${why}` });
174
+ if (sameValue(row[e.field], e.value))
175
+ return skipped.push({ index, why: 'the cell already holds this value' });
176
+ const confidence = typeof e.confidence === 'number' ? Math.min(1, Math.max(0, e.confidence)) : undefined;
177
+ proposals.push({ rowId: String(e.rowId), field: e.field, before: row[e.field], after: e.value, reason: e.reason, confidence });
178
+ });
179
+ const set = proposals.length ? opts.review.add({ source, title: input.title, proposals }) : null;
180
+ return {
181
+ proposed: set?.proposals.length ?? 0,
182
+ setId: set?.id ?? null,
183
+ skipped,
184
+ note: 'Proposed, not applied. A person will review these; call grid_review_status to see what was decided.',
185
+ };
186
+ },
187
+ };
188
+ const status = {
189
+ name: `${prefix}review_status`,
190
+ description: 'What happened to your proposals: how many are still pending, accepted, rejected, or stale (the cell changed before review), ' +
191
+ 'and the pending groups as the reviewer sees them.',
192
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
193
+ async execute() {
194
+ const mine = opts.review.proposals().filter((p) => p.source.id === source.id);
195
+ const count = (s) => mine.filter((p) => p.status === s).length;
196
+ return {
197
+ pending: count('pending'),
198
+ accepted: count('accepted'),
199
+ rejected: count('rejected'),
200
+ stale: count('stale'),
201
+ conflicts: count('conflict'),
202
+ groups: opts.review.groups().filter((g) => g.sourceId === source.id).map((g) => ({ label: g.label, kind: g.kind, count: g.count })),
203
+ };
204
+ },
205
+ };
206
+ return [describe, query, propose, status];
207
+ }
208
+ /**
209
+ * Register tools with the browser's WebMCP API, so an agent driving the page
210
+ * can call them. Experimental: WebMCP is a draft (`document.modelContext`,
211
+ * earlier `navigator.modelContext`), and this does nothing where it is
212
+ * missing. Abort `signal`, or call `unregister`, to remove them.
213
+ */
214
+ export function registerWebMcpTools(tools, opts = {}) {
215
+ const g = globalThis;
216
+ const mc = (g.document?.modelContext ?? g.navigator?.modelContext);
217
+ if (typeof mc?.registerTool !== 'function')
218
+ return { supported: false, unregister() { } };
219
+ const controller = new AbortController();
220
+ opts.signal?.addEventListener('abort', () => controller.abort(), { once: true });
221
+ for (const t of tools) {
222
+ const registered = mc.registerTool({
223
+ name: t.name,
224
+ description: t.description,
225
+ inputSchema: t.inputSchema,
226
+ execute: async (input) => ({ content: [{ type: 'text', text: JSON.stringify(await t.execute(input ?? {})) }] }),
227
+ }, { signal: controller.signal });
228
+ // A rejected registration (a duplicate name, a permission) must not break the page.
229
+ Promise.resolve(registered).catch(() => { });
230
+ }
231
+ return { supported: true, unregister: () => controller.abort() };
232
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Pending proposals on board and gallery cards. A card renderer gets the grid's
3
+ * rows and its `getRowId`, not the grid api, so a review registers itself by
4
+ * that function (the same object it reads through `api.getOption`; SvGrid
5
+ * Base keeps one per table) and a card asks for its row's mark.
6
+ *
7
+ * Small and on its own: the board and gallery import it without the review
8
+ * module. The registry is reactive: a card drawn before its review registered
9
+ * (the grid api arrives after the first draw) draws again when it does.
10
+ */
11
+ export type CardMark = {
12
+ count: number;
13
+ /** "2 proposed", in the review's wording. */
14
+ label: string;
15
+ /** "Region -> EMEA, Phone -> 555-123-4567", for the card's title. */
16
+ text: string;
17
+ };
18
+ /** A pending proposal in one cell, as a cell mark shows it. */
19
+ export type CellProposal = {
20
+ after: unknown;
21
+ source: string;
22
+ reason?: string;
23
+ /** The review's wording: "{source} proposes {value}" and the text for a blank value. */
24
+ proposes: string;
25
+ empty: string;
26
+ };
27
+ type Source = {
28
+ cardMark(rowId: string): CardMark | null;
29
+ cellProposal(rowId: string, field: string): CellProposal | null;
30
+ /** Whether a row object is one of this review's grid rows. */
31
+ owns(row: unknown): boolean;
32
+ };
33
+ /** Called by a review once its grid's `getRowId` is known. */
34
+ export declare function registerCardMarks(getRowId: object, source: Source): void;
35
+ /**
36
+ * The pending proposal in a grid cell, or null. `row` is the row object,
37
+ * `rowId` the grid's id for it; used by the cell mark of rich cells.
38
+ */
39
+ export declare function cellProposalFor(getRowId: object | undefined, row: unknown, rowId: string, field: string): CellProposal | null;
40
+ /** The mark for a card's row, or null (no review on this grid, or nothing pending). */
41
+ export declare function cardMarkFor(getRowId: ((row: any, index: number) => string) | undefined, row: unknown, index: number): CardMark | null;
42
+ export {};
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Pending proposals on board and gallery cards. A card renderer gets the grid's
3
+ * rows and its `getRowId`, not the grid api, so a review registers itself by
4
+ * that function (the same object it reads through `api.getOption`; SvGrid
5
+ * Base keeps one per table) and a card asks for its row's mark.
6
+ *
7
+ * Small and on its own: the board and gallery import it without the review
8
+ * module. The registry is reactive: a card drawn before its review registered
9
+ * (the grid api arrives after the first draw) draws again when it does.
10
+ */
11
+ // Several grids can share one getRowId function (Svelte hoists a static inline
12
+ // arrow), so a function maps to every review that registered it, and a card
13
+ // takes its mark from the review whose grid holds its row.
14
+ const sources = new WeakMap();
15
+ let generation = $state(0);
16
+ /** Called by a review once its grid's `getRowId` is known. */
17
+ export function registerCardMarks(getRowId, source) {
18
+ const list = sources.get(getRowId) ?? new Set();
19
+ list.add(source);
20
+ sources.set(getRowId, list);
21
+ // Registration runs while the grid reads its columns or formats, where state
22
+ // must not change; the cards hear about it a microtask later.
23
+ queueMicrotask(() => (generation += 1));
24
+ }
25
+ /**
26
+ * The pending proposal in a grid cell, or null. `row` is the row object,
27
+ * `rowId` the grid's id for it; used by the cell mark of rich cells.
28
+ */
29
+ export function cellProposalFor(getRowId, row, rowId, field) {
30
+ void generation;
31
+ const list = getRowId ? sources.get(getRowId) : undefined;
32
+ if (!list)
33
+ return null;
34
+ for (const source of list)
35
+ if (source.owns(row))
36
+ return source.cellProposal(rowId, field);
37
+ return null;
38
+ }
39
+ /** The mark for a card's row, or null (no review on this grid, or nothing pending). */
40
+ export function cardMarkFor(getRowId, row, index) {
41
+ void generation;
42
+ if (!getRowId)
43
+ return null;
44
+ const list = sources.get(getRowId);
45
+ if (!list)
46
+ return null;
47
+ for (const source of list)
48
+ if (source.owns(row))
49
+ return source.cardMark(String(getRowId(row, index)));
50
+ return null;
51
+ }
@@ -0,0 +1,26 @@
1
+ import { type ReviewMessages } from './messages.js';
2
+ import type { Proposal, ProposalGroup } from './types.js';
3
+ export type ClusterOptions = {
4
+ /** Wording of the group labels (the `label*`, `case*`, `punct*` keys); English by default. */
5
+ messages?: Partial<ReviewMessages>;
6
+ /** The header to show for a field in group labels. Default: the field itself. */
7
+ fieldLabel?: (field: string) => string;
8
+ /** Fewest members a `scale` group needs. Default 5. */
9
+ minScale?: number;
10
+ /** Below this, a judgment group sorts with the risky ones. Default 0.6. */
11
+ lowConfidence?: number;
12
+ };
13
+ export declare function clusterProposals(proposals: ReadonlyArray<Proposal>, opts?: ClusterOptions): ProposalGroup[];
14
+ /** A key that tells values apart by type as well as content. */
15
+ export declare function keyOf(v: unknown): string;
16
+ /**
17
+ * Spaces a reader cannot see, drawn as `·`: leading and trailing ones and runs
18
+ * of two or more. A trim proposal otherwise reads "Acme -> Acme".
19
+ */
20
+ export declare function visibleSpaces(s: string): string;
21
+ /**
22
+ * A value as a group label shows it: short, `(empty)` for blank, spaces
23
+ * visible. A string is quoted only where the quotes tell something: around
24
+ * spaces at its ends, or when it reads as a number, a boolean or the blank mark.
25
+ */
26
+ export declare function showValue(v: unknown, empty?: string): string;