@superpanel-sdk/runtime 0.2.2 → 0.2.3

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 (39) hide show
  1. package/dist/builder/compile/compile-agenda.js +9 -4
  2. package/dist/cli/serve/index.js +60 -12
  3. package/dist/compute-host/index.d.ts +2 -0
  4. package/dist/compute-host/index.js +1 -0
  5. package/dist/compute-host/read-only-cache.d.ts +20 -0
  6. package/dist/compute-host/read-only-cache.js +61 -0
  7. package/dist/compute-host/source-compute-host.js +7 -2
  8. package/dist/core/context/types.d.ts +19 -1
  9. package/dist/core/functions/define/custom.d.ts +1 -1
  10. package/dist/core/functions/types.d.ts +4 -4
  11. package/dist/core/slot/index.d.ts +1 -1
  12. package/dist/core/slot/index.js +1 -1
  13. package/dist/core/slot/types.d.ts +2 -0
  14. package/dist/core/slot/write.d.ts +8 -1
  15. package/dist/core/slot/write.js +145 -38
  16. package/dist/core/stage/builders.d.ts +23 -5
  17. package/dist/core/stage/builders.js +3 -0
  18. package/dist/core/stage/drive.d.ts +1 -1
  19. package/dist/core/stage/drive.js +9 -8
  20. package/dist/core/stage/flatten.js +13 -11
  21. package/dist/core/stage/index.d.ts +8 -4
  22. package/dist/core/stage/index.js +5 -2
  23. package/dist/core/stage/interrupts.d.ts +28 -0
  24. package/dist/core/stage/interrupts.js +120 -0
  25. package/dist/core/stage/ivr-fields.d.ts +9 -0
  26. package/dist/core/stage/ivr-fields.js +75 -0
  27. package/dist/core/stage/transfer-fields.d.ts +6 -1
  28. package/dist/core/stage/transfer-fields.js +44 -3
  29. package/dist/core/stage/transfer-spec.d.ts +48 -0
  30. package/dist/core/stage/transfer-spec.js +113 -0
  31. package/dist/core/stage/types.d.ts +45 -14
  32. package/dist/core/stage/validate.js +105 -1
  33. package/dist/core/stage/walk.d.ts +5 -0
  34. package/dist/core/stage/walk.js +33 -10
  35. package/dist/fn-context/rpc/trpc-client.js +3 -0
  36. package/dist/index.d.ts +4 -4
  37. package/dist/index.js +2 -2
  38. package/dist/voice-service/types.d.ts +3 -0
  39. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { toEffectiveSchema } from '../../core/slot/compile-schema.js';
2
- import { collectTransferBlocks, fatalAgendaIssues, flattenAgendaPlacements, flattenAgendaTree, isAgendaTree, isSequenceAgendas, normalizeSequenceAgendas, registerTransferSessionFields, validateCompiledAgenda, } from '../../core/stage/index.js';
2
+ import { collectIvrBlocks, collectTransferBlocks, collectTransferInterruptHeaders, fatalAgendaIssues, flattenAgendaPlacements, flattenAgendaTree, isAgendaTree, isSequenceAgendas, normalizeSequenceAgendas, registerIvrSessionFields, registerTransferSessionFields, validateCompiledAgenda, } from '../../core/stage/index.js';
3
3
  function asAgendaTrees(raw) {
4
4
  if (isAgendaTree(raw))
5
5
  return [raw];
@@ -27,8 +27,11 @@ export function compileSequenceAgendas(schema, raw, options = {}) {
27
27
  }
28
28
  const trees = asAgendaTrees(raw);
29
29
  if (trees) {
30
- const transferIds = collectTransferBlocks({}, trees.flatMap((tree) => tree.tree)).map((block) => block.id);
31
- const withTransfer = registerTransferSessionFields(schema, transferIds);
30
+ const nodes = trees.flatMap((tree) => tree.tree);
31
+ const transferIds = collectTransferBlocks({}, nodes).map((block) => block.id);
32
+ const withTransfer = registerIvrSessionFields(registerTransferSessionFields(schema, transferIds, {
33
+ lastResult: collectTransferInterruptHeaders(trees.map((tree) => tree.header)),
34
+ }), collectIvrBlocks({}, nodes).map((block) => block.id));
32
35
  const compiled = {};
33
36
  for (const tree of trees) {
34
37
  compiled[tree.id] = compileOneFlat(tree.id, tree.header, flattenAgendaTree(tree.tree, { functions: options.functions }), withTransfer, options.functions, issues);
@@ -48,7 +51,9 @@ export function compileSequenceAgendas(schema, raw, options = {}) {
48
51
  return { schema, effectiveSchema: toEffectiveSchema(schema), agendas: {}, issues };
49
52
  }
50
53
  const transferIds = collectTransferBlocks(agendasInput.blocks).map((block) => block.id);
51
- const withTransfer = registerTransferSessionFields(schema, transferIds);
54
+ const withTransfer = registerIvrSessionFields(registerTransferSessionFields(schema, transferIds, {
55
+ lastResult: collectTransferInterruptHeaders(Object.values(agendasInput.agendas).map((config) => config.header)),
56
+ }), collectIvrBlocks(agendasInput.blocks).map((block) => block.id));
52
57
  const compiled = {};
53
58
  for (const [agendaId, config] of Object.entries(agendasInput.agendas)) {
54
59
  compiled[agendaId] = compileOneFlat(agendaId, config.header, flattenAgendaPlacements(config.placements, agendasInput.blocks, { functions: options.functions }), withTransfer, options.functions, issues);
@@ -5,6 +5,7 @@ import { withCompanyImportGraph } from '../../builder/company-import-graph.js';
5
5
  import { findTsModule } from '../../builder/paths.js';
6
6
  import { listSequenceIds, loadSequenceCaseContextView, loadSequenceFunctions } from '../../builder/load.js';
7
7
  import { createSourceComputeHost } from '../../compute-host/source-compute-host.js';
8
+ import { formatRuntimeVersion } from '../../fn-context/rpc/request-id.js';
8
9
  import { encodeOverlayMessage, OVERLAY_REPLACED_CLOSE_CODE, parseOverlayMessage, } from '../../dev-overlay/protocol.js';
9
10
  const DEFAULT_BACKEND_URL = 'http://127.0.0.1:8100';
10
11
  function env(name) {
@@ -27,6 +28,20 @@ export function overlayWebSocketUrl(raw) {
27
28
  url.hash = '';
28
29
  return url.toString();
29
30
  }
31
+ function overlayRpcUrl(raw) {
32
+ const url = new URL(raw);
33
+ if (url.protocol === 'ws:')
34
+ url.protocol = 'http:';
35
+ else if (url.protocol === 'wss:')
36
+ url.protocol = 'https:';
37
+ else if (url.protocol !== 'http:' && url.protocol !== 'https:') {
38
+ throw new Error(`unsupported backend url: ${raw}`);
39
+ }
40
+ url.pathname = `${url.pathname.replace(/\/$/, '')}/trpc`;
41
+ url.search = '';
42
+ url.hash = '';
43
+ return url.toString();
44
+ }
30
45
  /** `sequences/<id>/…` → sequence id */
31
46
  function sequenceIdFromPath(repoRoot, file) {
32
47
  const rel = path.relative(repoRoot, file);
@@ -73,13 +88,19 @@ function send(ws, message) {
73
88
  function logField(label, value) {
74
89
  console.log(` ${label.padEnd(10)} ${value}`);
75
90
  }
76
- async function handleExecute(loaded, message) {
91
+ async function handleExecute(loaded, message, rpcUrl) {
92
+ const data = message.data
93
+ ? {
94
+ ...message.data,
95
+ config: { ...message.data.config, backendUrl: rpcUrl },
96
+ }
97
+ : undefined;
77
98
  const host = createSourceComputeHost({
78
99
  functions: loaded.functions,
79
100
  caseContextView: loaded.view,
80
- data: message.data,
101
+ data: data,
81
102
  runToken: message.runToken,
82
- runtimeVersion: loaded.build.runtimeVersion ?? loaded.build.engineVersion,
103
+ runtimeVersion: loaded.build.engineVersion,
83
104
  runtimeSource: loaded.build.runtimeSource ?? 'npm',
84
105
  getRequestId: () => message.logRequestId,
85
106
  getTags: () => message.logTags,
@@ -101,14 +122,36 @@ function pushOverlays(ws, sequences) {
101
122
  });
102
123
  }
103
124
  }
125
+ function closeDetail(event) {
126
+ return event.reason ? `${event.code} ${event.reason}` : String(event.code);
127
+ }
104
128
  function waitOpen(ws) {
105
129
  return new Promise((resolve, reject) => {
106
130
  if (ws.readyState === WebSocket.OPEN) {
107
131
  resolve();
108
132
  return;
109
133
  }
110
- ws.addEventListener('open', () => resolve(), { once: true });
111
- ws.addEventListener('error', () => reject(new Error('websocket error')), { once: true });
134
+ let settled = false;
135
+ const finish = (fn) => {
136
+ if (settled)
137
+ return;
138
+ settled = true;
139
+ cleanup();
140
+ fn();
141
+ };
142
+ const onOpen = () => finish(() => resolve());
143
+ const onClose = (event) => finish(() => reject(new Error(`websocket closed ${closeDetail(event)}`)));
144
+ const onError = () => {
145
+ setTimeout(() => finish(() => reject(new Error('websocket error'))), 250);
146
+ };
147
+ const cleanup = () => {
148
+ ws.removeEventListener('open', onOpen);
149
+ ws.removeEventListener('close', onClose);
150
+ ws.removeEventListener('error', onError);
151
+ };
152
+ ws.addEventListener('open', onOpen);
153
+ ws.addEventListener('close', onClose);
154
+ ws.addEventListener('error', onError);
112
155
  });
113
156
  }
114
157
  class OverlayReplacedError extends Error {
@@ -146,7 +189,7 @@ function waitAuthOk(ws) {
146
189
  cleanup();
147
190
  reject(event.code === OVERLAY_REPLACED_CLOSE_CODE
148
191
  ? new OverlayReplacedError()
149
- : new Error('websocket closed before auth.ok'));
192
+ : new Error(`websocket closed before auth.ok ${closeDetail(event)}`));
150
193
  };
151
194
  const cleanup = () => {
152
195
  ws.removeEventListener('message', onMessage);
@@ -156,7 +199,7 @@ function waitAuthOk(ws) {
156
199
  ws.addEventListener('close', onClose);
157
200
  });
158
201
  }
159
- function attachExecuteHandler(ws, sequences) {
202
+ function attachExecuteHandler(ws, sequences, rpcUrl) {
160
203
  ws.addEventListener('message', (event) => {
161
204
  void (async () => {
162
205
  let message;
@@ -185,7 +228,7 @@ function attachExecuteHandler(ws, sequences) {
185
228
  const started = Date.now();
186
229
  console.log(`execute ${call}`);
187
230
  try {
188
- const result = await handleExecute(loaded, message);
231
+ const result = await handleExecute(loaded, message, rpcUrl);
189
232
  console.log(`execute ${call} ok ${Date.now() - started}ms`);
190
233
  send(ws, { type: 'execute.result', requestId: message.requestId, ok: true, result });
191
234
  }
@@ -205,7 +248,7 @@ function attachExecuteHandler(ws, sequences) {
205
248
  function waitSessionEnd(ws) {
206
249
  return new Promise((resolve) => {
207
250
  if (ws.readyState === WebSocket.CLOSED) {
208
- resolve('closed');
251
+ resolve({ closed: 'already closed' });
209
252
  return;
210
253
  }
211
254
  const onMessage = (event) => {
@@ -222,7 +265,7 @@ function waitSessionEnd(ws) {
222
265
  };
223
266
  const onClose = (event) => {
224
267
  cleanup();
225
- resolve(event.code === OVERLAY_REPLACED_CLOSE_CODE ? 'replaced' : 'closed');
268
+ resolve(event.code === OVERLAY_REPLACED_CLOSE_CODE ? 'replaced' : { closed: closeDetail(event) });
226
269
  };
227
270
  const cleanup = () => {
228
271
  ws.removeEventListener('message', onMessage);
@@ -241,6 +284,7 @@ export async function runServe(repoRoot, options = {}) {
241
284
  throw new Error('SUPERPANEL_SDK_API_KEY or --api-key is required');
242
285
  const backendUrl = options.url ?? env('SUPERPANEL_SDK_BACKEND_URL') ?? DEFAULT_BACKEND_URL;
243
286
  const url = overlayWebSocketUrl(backendUrl);
287
+ const rpcUrl = overlayRpcUrl(backendUrl);
244
288
  const sequenceIds = options.sequenceIds ?? listSequenceIds(repoRoot);
245
289
  if (sequenceIds.length === 0) {
246
290
  throw new Error(`No sequences with slots found under ${path.join(repoRoot, 'sequences')}`);
@@ -249,9 +293,13 @@ export async function runServe(repoRoot, options = {}) {
249
293
  const sequences = await loadSequences(repoRoot, sequenceIds, generation);
250
294
  let socket = null;
251
295
  const names = [...sequences.keys()];
296
+ const firstBuild = sequences.values().next().value?.build;
252
297
  console.log('Local overlay');
253
298
  logField('company', companyId);
254
299
  logField('sequences', names.join(', '));
300
+ if (firstBuild) {
301
+ logField('runtime', formatRuntimeVersion(firstBuild.engineVersion, firstBuild.runtimeSource ?? 'npm'));
302
+ }
255
303
  logField('backend', backendUrl);
256
304
  logField('websocket', url);
257
305
  let timer;
@@ -328,14 +376,14 @@ export async function runServe(repoRoot, options = {}) {
328
376
  names: [...sequences.keys()],
329
377
  });
330
378
  await authOk;
331
- attachExecuteHandler(ws, sequences);
379
+ attachExecuteHandler(ws, sequences, rpcUrl);
332
380
  pushOverlays(ws, sequences);
333
381
  delayMs = 500;
334
382
  console.log(`connected overlay ${url}`);
335
383
  const end = await waitSessionEnd(ws);
336
384
  if (end === 'replaced')
337
385
  throw new OverlayReplacedError();
338
- console.log('disconnected retrying');
386
+ console.log(`disconnected retrying ${end.closed}`);
339
387
  }
340
388
  catch (error) {
341
389
  if (error instanceof OverlayReplacedError)
@@ -2,4 +2,6 @@ export { createLocalComputeHost, resolveComputeHost } from './compute-host.js';
2
2
  export type { ComputeFnMap, ComputeHost, ComputeHostOrFns, LocalComputeHostOptions } from './compute-host.js';
3
3
  export { createSourceComputeHost } from './source-compute-host.js';
4
4
  export type { SourceComputeHost, SourceComputeHostOptions } from './source-compute-host.js';
5
+ export { readOnlyComputeNames, withReadOnlyComputeCache } from './read-only-cache.js';
6
+ export type { ReadOnlyComputeCacheOptions } from './read-only-cache.js';
5
7
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,4 @@
1
1
  export { createLocalComputeHost, resolveComputeHost } from './compute-host.js';
2
2
  export { createSourceComputeHost } from './source-compute-host.js';
3
+ export { readOnlyComputeNames, withReadOnlyComputeCache } from './read-only-cache.js';
3
4
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,20 @@
1
+ import type { ComputeHost } from './compute-host.js';
2
+ /** Names safe to cache: `caseContextView` plus every compute / boolCompute. */
3
+ export declare function readOnlyComputeNames(functions?: Record<string, {
4
+ kind: string;
5
+ }>): Set<string>;
6
+ export type ReadOnlyComputeCacheOptions = {
7
+ /** Defaults to `caseContextView` only. Pass {@link readOnlyComputeNames} for compute / boolCompute too. */
8
+ readOnly?: Iterable<string> | ((name: string) => boolean);
9
+ };
10
+ /**
11
+ * Cache read-only `execute` results by function name + the single input object.
12
+ * Calls with extra args bypass the cache so a shared snapshot cannot alias
13
+ * two different argument lists.
14
+ *
15
+ * Do not wrap a host used by derived recompute: that path mutates one snapshot
16
+ * in place between `compute` calls. Stage walk / when-eval is safe (new snapshot
17
+ * after writes).
18
+ */
19
+ export declare function withReadOnlyComputeCache(host: ComputeHost, options?: ReadOnlyComputeCacheOptions): ComputeHost;
20
+ //# sourceMappingURL=read-only-cache.d.ts.map
@@ -0,0 +1,61 @@
1
+ const READ_ONLY_KINDS = new Set(['compute', 'boolCompute', 'caseContextView']);
2
+ /** Names safe to cache: `caseContextView` plus every compute / boolCompute. */
3
+ export function readOnlyComputeNames(functions) {
4
+ const names = new Set(['caseContextView']);
5
+ if (!functions)
6
+ return names;
7
+ for (const [name, entry] of Object.entries(functions)) {
8
+ if (READ_ONLY_KINDS.has(entry.kind))
9
+ names.add(name);
10
+ }
11
+ return names;
12
+ }
13
+ /**
14
+ * Cache read-only `execute` results by function name + the single input object.
15
+ * Calls with extra args bypass the cache so a shared snapshot cannot alias
16
+ * two different argument lists.
17
+ *
18
+ * Do not wrap a host used by derived recompute: that path mutates one snapshot
19
+ * in place between `compute` calls. Stage walk / when-eval is safe (new snapshot
20
+ * after writes).
21
+ */
22
+ export function withReadOnlyComputeCache(host, options) {
23
+ const allow = resolveAllow(options?.readOnly);
24
+ const cache = new WeakMap();
25
+ return {
26
+ async execute(name, args = []) {
27
+ const input = args[0];
28
+ if (!allow(name) || args.length !== 1 || input == null || typeof input !== 'object') {
29
+ return host.execute(name, args);
30
+ }
31
+ let byName = cache.get(input);
32
+ if (!byName) {
33
+ byName = new Map();
34
+ cache.set(input, byName);
35
+ }
36
+ const hit = byName.get(name);
37
+ if (hit)
38
+ return hit;
39
+ const pending = host.execute(name, args);
40
+ byName.set(name, pending);
41
+ try {
42
+ return await pending;
43
+ }
44
+ catch (error) {
45
+ if (byName.get(name) === pending)
46
+ byName.delete(name);
47
+ throw error;
48
+ }
49
+ },
50
+ dispose() {
51
+ host.dispose();
52
+ },
53
+ };
54
+ }
55
+ function resolveAllow(readOnly) {
56
+ if (typeof readOnly === 'function')
57
+ return readOnly;
58
+ const names = new Set(readOnly ?? ['caseContextView']);
59
+ return (name) => names.has(name);
60
+ }
61
+ //# sourceMappingURL=read-only-cache.js.map
@@ -49,11 +49,16 @@ export function createSourceComputeHost(options) {
49
49
  const caseContext = viewFn(requireSlots(name, args));
50
50
  const ctx = await systemContext();
51
51
  const fn = def.main;
52
- return fn(caseContext, ctx);
52
+ return fn(caseContext, { ...ctx, params: args[1] ?? {} });
53
53
  }
54
54
  const params = resolveSystemFnParams(args);
55
+ const contextStarted = Date.now();
55
56
  const ctx = await systemContext();
56
- return def.main({ ...ctx, params });
57
+ console.log(`[source timing] ${name} context ${Date.now() - contextStarted}ms`);
58
+ const functionStarted = Date.now();
59
+ const result = await def.main({ ...ctx, params });
60
+ console.log(`[source timing] ${name} function ${Date.now() - functionStarted}ms`);
61
+ return result;
57
62
  },
58
63
  dispose() { },
59
64
  };
@@ -19,7 +19,25 @@ export type Slots = Record<string, unknown>;
19
19
  * With `view.ts`, use `CaseContextOf<typeof caseContextView>` from that module.
20
20
  */
21
21
  export type CaseContext = Record<string, unknown>;
22
- export type Writes = Record<FieldPath, unknown>;
22
+ /** Flat writes for single-row tables, or the current-bound row of an entity table. */
23
+ export type FieldWrites = Record<FieldPath, unknown>;
24
+ /** Address one row of a multi-row table (`children`, `contact`, custom arrays). */
25
+ export type RowWrite = {
26
+ table: string;
27
+ rowId: string;
28
+ values: Record<string, unknown>;
29
+ };
30
+ /** Mix flat field writes with explicit row writes in one return. */
31
+ export type StructuredWrites = {
32
+ fields?: FieldWrites;
33
+ rows?: RowWrite[];
34
+ };
35
+ /**
36
+ * Function / platform write payload.
37
+ * Flat `{ '表.字段': 值 }` for single-row (or current-party) fields;
38
+ * `{ table, rowId, values }` to write a specific multi-row.
39
+ */
40
+ export type Writes = FieldWrites | RowWrite | StructuredWrites;
23
41
  export type Logger = {
24
42
  info: (message: string, extra?: Record<string, unknown>) => void;
25
43
  warn: (message: string, extra?: Record<string, unknown>) => void;
@@ -1,4 +1,4 @@
1
1
  import type { CustomFn, DefinedFunc, FuncOptions } from '../types.js';
2
2
  import type { CaseContext } from '../../context/types.js';
3
- export declare function defineCustomFunc<S = CaseContext>(main: CustomFn<S>, options?: FuncOptions): DefinedFunc;
3
+ export declare function defineCustomFunc<S = CaseContext, P extends object = object>(main: CustomFn<S, P>, options?: FuncOptions): DefinedFunc;
4
4
  //# sourceMappingURL=custom.d.ts.map
@@ -6,16 +6,16 @@ export type FuncOptions = {
6
6
  };
7
7
  /** First arg is CaseContext (or Slots when the sequence has no view). */
8
8
  export type ComputeFn<S = CaseContext, R = unknown> = (caseContext: S) => R | Promise<R>;
9
- export type CustomFn<S = CaseContext> = (caseContext: S, ctx: SystemContext) => Promise<Writes | null>;
9
+ export type FunctionCtx<Params extends object> = SystemContext & {
10
+ params: Params;
11
+ };
12
+ export type CustomFn<S = CaseContext, P extends object = object> = (caseContext: S, ctx: FunctionCtx<P>) => Promise<Writes | null>;
10
13
  /** Inbound webhook request payload. */
11
14
  export type WebHookFunctionParams = {
12
15
  headers: Record<string, string | string[] | undefined>;
13
16
  body: unknown;
14
17
  rawBody?: string;
15
18
  };
16
- export type FunctionCtx<Params extends object> = SystemContext & {
17
- params: Params;
18
- };
19
19
  export type FunctionCall<Params extends object, Return> = (ctx: FunctionCtx<Params>) => Promise<Return>;
20
20
  export type WebHookFunction = FunctionCall<WebHookFunctionParams, void>;
21
21
  export type GetFirstMessageParams<S = CaseContext> = {
@@ -9,7 +9,7 @@ export { isValidDataTypeValue, validateExtractedField, validateBareValue } from
9
9
  export { buildSlotFillingJsonSchema, buildFieldCatalog, type SfSchemaOptions } from './sf-schema.js';
10
10
  export { validateSlotFillingOutput, type ValidateSlotFillingOptions } from './sf-validate.js';
11
11
  export { snapshotFrom, cloneSnapshot, getSingleField, getSnapshotTable, type SnapshotSeed } from './snapshot.js';
12
- export { applyFieldPatch, applyWrites, applySlotFilling, markAskedUnknown, type ApplySlotFillingOptions, type FieldPatchInput, } from './write.js';
12
+ export { applyFieldPatch, applyWrites, applySlotFilling, markAskedUnknown, isRowWrite, isStructuredWrites, normalizeWrites, type ApplySlotFillingOptions, type FieldPatchInput, } from './write.js';
13
13
  export type { ComputeFnMap, ComputeHost, ComputeHostOrFns, LocalComputeHostOptions, RecomputeOptions, } from './derived.js';
14
14
  export { createLocalComputeHost, createSourceComputeHost, resolveComputeHost, recomputeDerived, applyFieldPatchAndRecompute, applyWritesAndRecompute, applySlotFillingAndRecompute, } from './derived.js';
15
15
  //# sourceMappingURL=index.d.ts.map
@@ -9,6 +9,6 @@ export { isValidDataTypeValue, validateExtractedField, validateBareValue } from
9
9
  export { buildSlotFillingJsonSchema, buildFieldCatalog } from './sf-schema.js';
10
10
  export { validateSlotFillingOutput } from './sf-validate.js';
11
11
  export { snapshotFrom, cloneSnapshot, getSingleField, getSnapshotTable } from './snapshot.js';
12
- export { applyFieldPatch, applyWrites, applySlotFilling, markAskedUnknown, } from './write.js';
12
+ export { applyFieldPatch, applyWrites, applySlotFilling, markAskedUnknown, isRowWrite, isStructuredWrites, normalizeWrites, } from './write.js';
13
13
  export { createLocalComputeHost, createSourceComputeHost, resolveComputeHost, recomputeDerived, applyFieldPatchAndRecompute, applyWritesAndRecompute, applySlotFillingAndRecompute, } from './derived.js';
14
14
  //# sourceMappingURL=index.js.map
@@ -23,6 +23,8 @@ export type FieldValue = {
23
23
  export type HistoryAppend = {
24
24
  path: FieldPath;
25
25
  entry: FieldHistoryEntry;
26
+ /** Set when the write targeted a specific snapshot row. */
27
+ rowId?: string;
26
28
  };
27
29
  export type ExtractedField = {
28
30
  reason: string;
@@ -1,9 +1,16 @@
1
- import type { FieldState, Writes } from '../context/types.js';
1
+ import type { FieldState, FieldWrites, RowWrite, StructuredWrites, Writes } from '../context/types.js';
2
2
  import type { FieldPath } from './path.js';
3
3
  import type { ApplyWritesOptions, ApplyWritesResult, CompiledSlotSchema, RuntimeSlotsSnapshot, SlotFillingOutput, UpdatedBy } from './types.js';
4
+ export declare function isRowWrite(value: unknown): value is RowWrite;
5
+ export declare function isStructuredWrites(value: unknown): value is StructuredWrites;
6
+ export declare function normalizeWrites(writes: Writes | null | undefined): {
7
+ fields: FieldWrites;
8
+ rows: RowWrite[];
9
+ } | null;
4
10
  /**
5
11
  * Apply bare Writes (function / human / import / platform) onto a snapshot.
6
12
  * Validates type/enum and write permissions. Detects C5 changes and appends history.
13
+ * Multi-row tables: pass `{ table, rowId, values }` (or `rows` on StructuredWrites).
7
14
  */
8
15
  export declare function applyWrites(schema: CompiledSlotSchema, snapshot: RuntimeSlotsSnapshot, writes: Writes | null | undefined, options: ApplyWritesOptions): ApplyWritesResult;
9
16
  export type FieldPatchInput = {