@superdoc/sdk 2.10.0-next.7 → 2.10.0-next.9
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.
- package/README.md +45 -0
- package/dist/action-primitives/doc-index.cjs +6 -4
- package/dist/action-primitives/doc-index.js +6 -4
- package/dist/action-primitives/engine.cjs +6 -2
- package/dist/action-primitives/engine.js +6 -2
- package/dist/action-primitives/receipt.d.ts +4 -0
- package/dist/action-primitives/tools/structure-insert.d.ts +1 -1
- package/dist/agent/actions.cjs +203 -223
- package/dist/agent/actions.d.ts +5 -0
- package/dist/agent/actions.js +203 -223
- package/dist/agent/catalog.cjs +15 -0
- package/dist/agent/catalog.js +15 -0
- package/dist/agent/doc-snapshot.cjs +22 -46
- package/dist/agent/doc-snapshot.js +22 -46
- package/dist/agent/execution-context.cjs +385 -0
- package/dist/agent/execution-context.d.ts +97 -0
- package/dist/agent/execution-context.js +376 -0
- package/dist/agent/runtime.cjs +116 -158
- package/dist/agent/runtime.d.ts +3 -0
- package/dist/agent/runtime.js +117 -159
- package/dist/agent/v2-preset-compat.cjs +5 -1
- package/dist/agent/v2-preset-compat.js +4 -1
- package/dist/embedded-tools.generated.cjs +5 -5
- package/dist/embedded-tools.generated.js +5 -5
- package/dist/generated/client.cjs +2 -0
- package/dist/generated/client.d.ts +47 -0
- package/dist/generated/client.js +2 -0
- package/dist/generated/contract.cjs +1605 -1297
- package/dist/generated/contract.js +1605 -1297
- package/dist/index.cjs +11 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +11 -0
- package/dist/presets/core.cjs +1 -1
- package/dist/presets/core.js +1 -1
- package/dist/runtime/document-evidence.cjs +40 -0
- package/dist/runtime/document-evidence.d.ts +13 -0
- package/dist/runtime/document-evidence.js +30 -0
- package/dist/runtime/host.cjs +30 -2
- package/dist/runtime/host.d.ts +2 -0
- package/dist/runtime/host.js +30 -2
- package/dist/runtime/process.cjs +6 -0
- package/dist/runtime/process.d.ts +2 -0
- package/dist/runtime/process.js +6 -0
- package/dist/runtime/sdk-version.generated.cjs +1 -1
- package/dist/runtime/sdk-version.generated.d.ts +1 -1
- package/dist/runtime/sdk-version.generated.js +1 -1
- package/package.json +10 -9
- package/tools/catalog.json +35 -0
- package/tools/tools-policy.json +1 -1
- package/tools/tools.anthropic.json +35 -0
- package/tools/tools.generic.json +35 -0
- package/tools/tools.openai.json +35 -0
- package/tools/tools.vercel.json +35 -0
package/dist/agent/catalog.js
CHANGED
|
@@ -143,6 +143,11 @@ export const ACTION_ARG_SCHEMA = {
|
|
|
143
143
|
},
|
|
144
144
|
finds: { type: 'array', items: { type: 'string' } },
|
|
145
145
|
caseSensitive: { type: 'boolean' },
|
|
146
|
+
evidence: {
|
|
147
|
+
type: 'string',
|
|
148
|
+
enum: ['required', 'full'],
|
|
149
|
+
description: 'Required evidence by default; full includes complete count summaries.',
|
|
150
|
+
},
|
|
146
151
|
changeMode: { type: 'string', enum: ['direct', 'tracked'] },
|
|
147
152
|
placement: { type: 'object', additionalProperties: true, properties: {} },
|
|
148
153
|
selector: SELECTOR_SCHEMA,
|
|
@@ -387,6 +392,11 @@ export const AGENT_TOOL_DEFINITIONS = [
|
|
|
387
392
|
additionalProperties: false,
|
|
388
393
|
required: ['plan'],
|
|
389
394
|
properties: {
|
|
395
|
+
evidence: {
|
|
396
|
+
type: 'string',
|
|
397
|
+
enum: ['required', 'full'],
|
|
398
|
+
description: 'Required evidence by default; full includes complete count summaries.',
|
|
399
|
+
},
|
|
390
400
|
plan: {
|
|
391
401
|
type: 'object',
|
|
392
402
|
additionalProperties: false,
|
|
@@ -425,6 +435,11 @@ export const AGENT_TOOL_DEFINITIONS = [
|
|
|
425
435
|
additionalProperties: false,
|
|
426
436
|
required: ['checks'],
|
|
427
437
|
properties: {
|
|
438
|
+
evidence: {
|
|
439
|
+
type: 'string',
|
|
440
|
+
enum: ['required', 'full'],
|
|
441
|
+
description: 'Required evidence by default; full includes complete count summaries.',
|
|
442
|
+
},
|
|
428
443
|
checks: {
|
|
429
444
|
type: 'array',
|
|
430
445
|
minItems: 1,
|
|
@@ -159,57 +159,33 @@ async function buildDocumentSnapshot(doc, options = {}) {
|
|
|
159
159
|
// first match so one call both LOCATES and READS the target section.
|
|
160
160
|
let finds;
|
|
161
161
|
if (typeof options.findText === 'string' && options.findText.trim().length > 0) {
|
|
162
|
-
const scanFn = maybeMethod(doc, ['blocks', '
|
|
162
|
+
const scanFn = maybeMethod(doc, ['blocks', 'findText']);
|
|
163
163
|
if (scanFn) {
|
|
164
|
-
// Paginated scan with a hard cap: bounded bridge payloads per page, and
|
|
165
|
-
// an explicit truncated flag instead of a silently-partial `total`.
|
|
166
|
-
const SCAN_PAGE = 2000;
|
|
167
|
-
const SCAN_CAP = 20000;
|
|
168
|
-
const needle = options.findText.toLowerCase();
|
|
169
164
|
const findLimit = options.findLimit != null && options.findLimit > 0 ? options.findLimit : 8;
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const scanBlocks = Array.isArray(scanRec?.blocks) ? scanRec.blocks : [];
|
|
177
|
-
scanBlocks.forEach((b, i) => {
|
|
178
|
-
const rec = asRecord(b) ?? {};
|
|
179
|
-
const text = asString(rec.text);
|
|
180
|
-
if (!text || !text.toLowerCase().includes(needle))
|
|
181
|
-
return;
|
|
182
|
-
matches.push({
|
|
183
|
-
ordinal: asNumber(rec.ordinal, pageOffset + i) + 1,
|
|
184
|
-
nodeId: asString(rec.nodeId),
|
|
185
|
-
nodeType: asString(rec.nodeType, 'paragraph'),
|
|
186
|
-
preview: text.slice(0, 100),
|
|
187
|
-
index: asNumber(rec.ordinal, pageOffset + i),
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
scanned += scanBlocks.length;
|
|
191
|
-
const totalBlocksReported = explicitCount(scanRec ?? {}, 'total');
|
|
192
|
-
if (scanBlocks.length < SCAN_PAGE)
|
|
193
|
-
break; // last page
|
|
194
|
-
if (totalBlocksReported != null && scanned >= totalBlocksReported)
|
|
195
|
-
break;
|
|
196
|
-
if (pageOffset + SCAN_PAGE >= SCAN_CAP) {
|
|
197
|
-
truncated = true;
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
165
|
+
const scanRaw = await safeCall(() => scanFn({ text: options.findText, limit: Math.min(20000, Math.trunc(findLimit)) }), null, recordError('blocks.findText'));
|
|
166
|
+
const scan = asRecord(scanRaw);
|
|
167
|
+
const scanError = asRecord(scan?.scanError);
|
|
168
|
+
if (typeof scanError?.message === 'string')
|
|
169
|
+
recordError('blocks.findText')(scanError.message);
|
|
170
|
+
const matches = Array.isArray(scan?.matches) ? scan.matches : [];
|
|
201
171
|
finds = {
|
|
202
172
|
query: options.findText,
|
|
203
|
-
total:
|
|
204
|
-
...(truncated ? { truncated: true } : {}),
|
|
205
|
-
scannedBlocks:
|
|
206
|
-
matches: matches.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
173
|
+
total: asNumber(scan?.total),
|
|
174
|
+
...(scan?.truncated === true ? { truncated: true } : {}),
|
|
175
|
+
scannedBlocks: asNumber(scan?.scannedBlocks),
|
|
176
|
+
matches: matches.map((match) => {
|
|
177
|
+
const rec = asRecord(match) ?? {};
|
|
178
|
+
const nodeId = asString(rec.nodeId);
|
|
179
|
+
return {
|
|
180
|
+
ordinal: asNumber(rec.ordinal) + 1,
|
|
181
|
+
...(nodeId ? { nodeId } : {}),
|
|
182
|
+
nodeType: asString(rec.nodeType, 'paragraph'),
|
|
183
|
+
preview: asString(rec.preview),
|
|
184
|
+
};
|
|
185
|
+
}),
|
|
210
186
|
};
|
|
211
|
-
if (
|
|
212
|
-
blockOffset = Math.max(0,
|
|
187
|
+
if (typeof scan?.firstMatchOrdinal === 'number' && options.blockOffset == null) {
|
|
188
|
+
blockOffset = Math.max(0, scan.firstMatchOrdinal - 2);
|
|
213
189
|
blockLimit = options.blockLimit != null && options.blockLimit > 0 ? options.blockLimit : 12;
|
|
214
190
|
}
|
|
215
191
|
}
|
|
@@ -157,57 +157,33 @@ export async function buildDocumentSnapshot(doc, options = {}) {
|
|
|
157
157
|
// first match so one call both LOCATES and READS the target section.
|
|
158
158
|
let finds;
|
|
159
159
|
if (typeof options.findText === 'string' && options.findText.trim().length > 0) {
|
|
160
|
-
const scanFn = maybeMethod(doc, ['blocks', '
|
|
160
|
+
const scanFn = maybeMethod(doc, ['blocks', 'findText']);
|
|
161
161
|
if (scanFn) {
|
|
162
|
-
// Paginated scan with a hard cap: bounded bridge payloads per page, and
|
|
163
|
-
// an explicit truncated flag instead of a silently-partial `total`.
|
|
164
|
-
const SCAN_PAGE = 2000;
|
|
165
|
-
const SCAN_CAP = 20000;
|
|
166
|
-
const needle = options.findText.toLowerCase();
|
|
167
162
|
const findLimit = options.findLimit != null && options.findLimit > 0 ? options.findLimit : 8;
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const scanBlocks = Array.isArray(scanRec?.blocks) ? scanRec.blocks : [];
|
|
175
|
-
scanBlocks.forEach((b, i) => {
|
|
176
|
-
const rec = asRecord(b) ?? {};
|
|
177
|
-
const text = asString(rec.text);
|
|
178
|
-
if (!text || !text.toLowerCase().includes(needle))
|
|
179
|
-
return;
|
|
180
|
-
matches.push({
|
|
181
|
-
ordinal: asNumber(rec.ordinal, pageOffset + i) + 1,
|
|
182
|
-
nodeId: asString(rec.nodeId),
|
|
183
|
-
nodeType: asString(rec.nodeType, 'paragraph'),
|
|
184
|
-
preview: text.slice(0, 100),
|
|
185
|
-
index: asNumber(rec.ordinal, pageOffset + i),
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
scanned += scanBlocks.length;
|
|
189
|
-
const totalBlocksReported = explicitCount(scanRec ?? {}, 'total');
|
|
190
|
-
if (scanBlocks.length < SCAN_PAGE)
|
|
191
|
-
break; // last page
|
|
192
|
-
if (totalBlocksReported != null && scanned >= totalBlocksReported)
|
|
193
|
-
break;
|
|
194
|
-
if (pageOffset + SCAN_PAGE >= SCAN_CAP) {
|
|
195
|
-
truncated = true;
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
163
|
+
const scanRaw = await safeCall(() => scanFn({ text: options.findText, limit: Math.min(20000, Math.trunc(findLimit)) }), null, recordError('blocks.findText'));
|
|
164
|
+
const scan = asRecord(scanRaw);
|
|
165
|
+
const scanError = asRecord(scan?.scanError);
|
|
166
|
+
if (typeof scanError?.message === 'string')
|
|
167
|
+
recordError('blocks.findText')(scanError.message);
|
|
168
|
+
const matches = Array.isArray(scan?.matches) ? scan.matches : [];
|
|
199
169
|
finds = {
|
|
200
170
|
query: options.findText,
|
|
201
|
-
total:
|
|
202
|
-
...(truncated ? { truncated: true } : {}),
|
|
203
|
-
scannedBlocks:
|
|
204
|
-
matches: matches.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
171
|
+
total: asNumber(scan?.total),
|
|
172
|
+
...(scan?.truncated === true ? { truncated: true } : {}),
|
|
173
|
+
scannedBlocks: asNumber(scan?.scannedBlocks),
|
|
174
|
+
matches: matches.map((match) => {
|
|
175
|
+
const rec = asRecord(match) ?? {};
|
|
176
|
+
const nodeId = asString(rec.nodeId);
|
|
177
|
+
return {
|
|
178
|
+
ordinal: asNumber(rec.ordinal) + 1,
|
|
179
|
+
...(nodeId ? { nodeId } : {}),
|
|
180
|
+
nodeType: asString(rec.nodeType, 'paragraph'),
|
|
181
|
+
preview: asString(rec.preview),
|
|
182
|
+
};
|
|
183
|
+
}),
|
|
208
184
|
};
|
|
209
|
-
if (
|
|
210
|
-
blockOffset = Math.max(0,
|
|
185
|
+
if (typeof scan?.firstMatchOrdinal === 'number' && options.blockOffset == null) {
|
|
186
|
+
blockOffset = Math.max(0, scan.firstMatchOrdinal - 2);
|
|
211
187
|
blockLimit = options.blockLimit != null && options.blockLimit > 0 ? options.blockLimit : 12;
|
|
212
188
|
}
|
|
213
189
|
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var documentEvidence = require('../runtime/document-evidence.cjs');
|
|
4
|
+
var errors = require('../runtime/errors.cjs');
|
|
5
|
+
var docSnapshot = require('./doc-snapshot.cjs');
|
|
6
|
+
var operationCatalog = require('./operation-catalog.cjs');
|
|
7
|
+
|
|
8
|
+
function supportsLocalSelector(selector) {
|
|
9
|
+
return (selector.kind === 'nodeId' ||
|
|
10
|
+
selector.kind === 'textSearch' ||
|
|
11
|
+
selector.kind === 'placement' ||
|
|
12
|
+
(selector.kind === 'ordinal' &&
|
|
13
|
+
['blockOrdinal', 'headingOrdinal', 'tableOrdinal'].includes(selector.ordinalKind)) ||
|
|
14
|
+
(selector.kind === 'relative' && supportsLocalSelector(selector.target)));
|
|
15
|
+
}
|
|
16
|
+
function localCheck(check) {
|
|
17
|
+
return (['revision-changed', 'revision-unchanged', 'block-text-contains', 'block-text-equals', 'table-shape'].includes(check.kind) ||
|
|
18
|
+
(check.kind === 'block-count-delta' && ['paragraph', 'heading', 'listItem', 'table'].includes(check.nodeType)));
|
|
19
|
+
}
|
|
20
|
+
async function readPublicRevision(doc) {
|
|
21
|
+
const revision = await documentEvidence.getDocumentRevisionReader(doc)?.();
|
|
22
|
+
if (revision !== undefined)
|
|
23
|
+
return revision;
|
|
24
|
+
const page = await doc.blocks.list({ offset: Number.MAX_SAFE_INTEGER, limit: 1 });
|
|
25
|
+
if (!page.revision || page.revision === 'unknown')
|
|
26
|
+
throw new errors.SuperDocCliError('Required document revision is unavailable.', { code: 'EVIDENCE_UNAVAILABLE' });
|
|
27
|
+
return page.revision;
|
|
28
|
+
}
|
|
29
|
+
class DocumentFacts {
|
|
30
|
+
doc;
|
|
31
|
+
revision;
|
|
32
|
+
complete;
|
|
33
|
+
summaryCounts;
|
|
34
|
+
blocks = new Map();
|
|
35
|
+
counts = new Map();
|
|
36
|
+
selections = new Map();
|
|
37
|
+
constructor(doc, revision, complete, summaryCounts = complete?.counts) {
|
|
38
|
+
this.doc = doc;
|
|
39
|
+
this.revision = revision;
|
|
40
|
+
this.complete = complete;
|
|
41
|
+
this.summaryCounts = summaryCounts;
|
|
42
|
+
}
|
|
43
|
+
get summary() {
|
|
44
|
+
return { revision: this.revision, ...(this.summaryCounts ? { counts: this.summaryCounts } : {}) };
|
|
45
|
+
}
|
|
46
|
+
async fence() {
|
|
47
|
+
if ((await readPublicRevision(this.doc)) !== this.revision)
|
|
48
|
+
throw new errors.SuperDocCliError('Document changed while acquiring execution evidence.', { code: 'REVISION_CONFLICT' });
|
|
49
|
+
}
|
|
50
|
+
async page(input) {
|
|
51
|
+
const result = await this.doc.blocks.list({ ...input, includeText: true });
|
|
52
|
+
if (result.revision !== this.revision)
|
|
53
|
+
throw new errors.SuperDocCliError('Document changed while resolving a target.', { code: 'REVISION_CONFLICT' });
|
|
54
|
+
const blocks = result.blocks.map((block) => ({
|
|
55
|
+
...block,
|
|
56
|
+
ordinal: block.ordinal + 1,
|
|
57
|
+
text: block.text ?? '',
|
|
58
|
+
textPreview: block.textPreview ?? null,
|
|
59
|
+
styleId: block.styleId ?? null,
|
|
60
|
+
}));
|
|
61
|
+
for (const block of blocks)
|
|
62
|
+
this.blocks.set(block.nodeId, block);
|
|
63
|
+
return blocks;
|
|
64
|
+
}
|
|
65
|
+
async count(nodeType) {
|
|
66
|
+
const key = nodeType ?? '*';
|
|
67
|
+
if (this.counts.has(key))
|
|
68
|
+
return this.counts.get(key);
|
|
69
|
+
if (this.complete)
|
|
70
|
+
return nodeType
|
|
71
|
+
? this.complete.blocks.filter((block) => block.nodeType === nodeType).length
|
|
72
|
+
: this.complete.blocks.length;
|
|
73
|
+
const result = await this.doc.blocks.list({
|
|
74
|
+
offset: Number.MAX_SAFE_INTEGER,
|
|
75
|
+
limit: 1,
|
|
76
|
+
...(nodeType ? { nodeTypes: [nodeType] } : {}),
|
|
77
|
+
});
|
|
78
|
+
if (result.revision !== this.revision || !Number.isSafeInteger(result.total))
|
|
79
|
+
throw new errors.SuperDocCliError('Required count is unavailable at the execution revision.', {
|
|
80
|
+
code: 'EVIDENCE_UNAVAILABLE',
|
|
81
|
+
});
|
|
82
|
+
this.counts.set(key, result.total);
|
|
83
|
+
return result.total;
|
|
84
|
+
}
|
|
85
|
+
async block(nodeId) {
|
|
86
|
+
if (this.complete)
|
|
87
|
+
return this.complete.blocks.find((block) => block.nodeId === nodeId);
|
|
88
|
+
if (!this.blocks.has(nodeId))
|
|
89
|
+
await this.page({ nodeIds: [nodeId], limit: 1 });
|
|
90
|
+
return this.blocks.get(nodeId);
|
|
91
|
+
}
|
|
92
|
+
async select(selector) {
|
|
93
|
+
if (this.complete)
|
|
94
|
+
return docSnapshot.resolveSnapshotSelector(this.complete, selector);
|
|
95
|
+
const key = JSON.stringify(selector);
|
|
96
|
+
if (this.selections.has(key))
|
|
97
|
+
return this.selections.get(key);
|
|
98
|
+
let blocks = [];
|
|
99
|
+
if (selector.kind === 'nodeId') {
|
|
100
|
+
const block = await this.block(selector.nodeId);
|
|
101
|
+
if (block)
|
|
102
|
+
blocks = [block];
|
|
103
|
+
}
|
|
104
|
+
else if (selector.kind === 'textSearch') {
|
|
105
|
+
const terms = selector.terms.filter((term) => term.trim());
|
|
106
|
+
if (terms.length && selector.nodeTypes?.length !== 0)
|
|
107
|
+
blocks = await this.page({
|
|
108
|
+
textSearch: { terms: [...terms], match: selector.match, caseSensitive: selector.caseSensitive },
|
|
109
|
+
nodeTypes: [...(selector.nodeTypes ?? ['paragraph', 'heading', 'listItem'])],
|
|
110
|
+
offset: (selector.occurrence ?? 1) - 1,
|
|
111
|
+
limit: 1,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
else if (selector.kind === 'placement') {
|
|
115
|
+
const offset = selector.at === 'document_start' ? 0 : (await this.count()) - 1;
|
|
116
|
+
if (offset >= 0)
|
|
117
|
+
blocks = await this.page({ offset, limit: 1 });
|
|
118
|
+
}
|
|
119
|
+
else if (selector.kind === 'ordinal') {
|
|
120
|
+
blocks = await this.page({
|
|
121
|
+
offset: selector.value - 1,
|
|
122
|
+
limit: 1,
|
|
123
|
+
...(selector.ordinalKind === 'headingOrdinal'
|
|
124
|
+
? { nodeTypes: ['heading'] }
|
|
125
|
+
: selector.ordinalKind === 'tableOrdinal'
|
|
126
|
+
? { nodeTypes: ['table'] }
|
|
127
|
+
: {}),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else if (selector.kind === 'relative') {
|
|
131
|
+
for (const id of await this.select(selector.target)) {
|
|
132
|
+
const target = await this.block(id);
|
|
133
|
+
const offset = target ? target.ordinal - 1 + (selector.position === 'before' ? -1 : 1) : -1;
|
|
134
|
+
if (offset >= 0)
|
|
135
|
+
blocks.push(...(await this.page({ offset, limit: 1 })));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else
|
|
139
|
+
throw new errors.SuperDocCliError('Selector requires complete evidence.', { code: 'EVIDENCE_UNAVAILABLE' });
|
|
140
|
+
const ids = [...new Set(blocks.map((block) => block.nodeId))];
|
|
141
|
+
this.selections.set(key, ids);
|
|
142
|
+
return ids;
|
|
143
|
+
}
|
|
144
|
+
async target(selector) {
|
|
145
|
+
const ids = await this.select(selector);
|
|
146
|
+
if (this.complete && selector.kind === 'tableCell' && ids.length === 1) {
|
|
147
|
+
const cell = this.complete.tables.flatMap((table) => table.cells).find((cell) => cell.nodeId === ids[0]);
|
|
148
|
+
if (cell?.nodeId)
|
|
149
|
+
return {
|
|
150
|
+
nodeId: cell.nodeId,
|
|
151
|
+
nodeType: 'paragraph',
|
|
152
|
+
text: cell.text,
|
|
153
|
+
textPreview: cell.text,
|
|
154
|
+
styleId: null,
|
|
155
|
+
ordinal: 0,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return ids.length === 1 ? this.block(ids[0]) : undefined;
|
|
159
|
+
}
|
|
160
|
+
async tableShape(nodeId) {
|
|
161
|
+
if (this.complete)
|
|
162
|
+
return this.complete.tables.find((table) => table.nodeId === nodeId);
|
|
163
|
+
return this.doc.tables.get({ nodeId });
|
|
164
|
+
}
|
|
165
|
+
async completeIndex(reason, acquire) {
|
|
166
|
+
const value = await acquire();
|
|
167
|
+
await this.fence();
|
|
168
|
+
return { value, coverage: 'complete', reason };
|
|
169
|
+
}
|
|
170
|
+
async prepare(checks) {
|
|
171
|
+
for (const check of checks)
|
|
172
|
+
if (check.kind === 'block-count-delta')
|
|
173
|
+
await this.count(check.nodeType);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
class ExecutionContext {
|
|
177
|
+
source;
|
|
178
|
+
requirements;
|
|
179
|
+
document;
|
|
180
|
+
executedOperations = [];
|
|
181
|
+
selectedTargets = [];
|
|
182
|
+
pre;
|
|
183
|
+
post;
|
|
184
|
+
fallbackReason;
|
|
185
|
+
currentRevision;
|
|
186
|
+
legacyRevisionOnly = false;
|
|
187
|
+
constructor(source, requirements = {}) {
|
|
188
|
+
this.source = source;
|
|
189
|
+
this.requirements = requirements;
|
|
190
|
+
if (requirements.evidence !== undefined && !['required', 'full'].includes(requirements.evidence))
|
|
191
|
+
throw new errors.SuperDocCliError('evidence must be required or full.', { code: 'INVALID_ARGUMENT' });
|
|
192
|
+
const wrap = (value, path) => new Proxy(value, {
|
|
193
|
+
get: (target, property, receiver) => {
|
|
194
|
+
const member = Reflect.get(target, property, receiver);
|
|
195
|
+
if (typeof property !== 'string')
|
|
196
|
+
return member;
|
|
197
|
+
const memberPath = [...path, property];
|
|
198
|
+
const operationId = `doc.${memberPath.join('.')}`;
|
|
199
|
+
if (typeof member === 'function' && operationCatalog.getOperationCatalogEntry(operationId)?.isMutating) {
|
|
200
|
+
return async (args = {}, options) => {
|
|
201
|
+
if (operationId === 'doc.format.apply' &&
|
|
202
|
+
documentEvidence.supportsDocumentFacts(this.source) &&
|
|
203
|
+
typeof args.blockId === 'string') {
|
|
204
|
+
const { blockId, start, end, ...rest } = args;
|
|
205
|
+
args = {
|
|
206
|
+
...rest,
|
|
207
|
+
target: {
|
|
208
|
+
kind: 'selection',
|
|
209
|
+
start: { kind: 'text', blockId, offset: start },
|
|
210
|
+
end: { kind: 'text', blockId, offset: end },
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (this.legacyRevisionOnly && this.executedOperations.length)
|
|
215
|
+
throw new errors.SuperDocCliError('This host cannot bind revisions across multiple writes.', {
|
|
216
|
+
code: 'EVIDENCE_UNAVAILABLE',
|
|
217
|
+
});
|
|
218
|
+
const guarded = (documentEvidence.supportsDocumentFacts(this.source) || this.legacyRevisionOnly) &&
|
|
219
|
+
!['doc.history.undo', 'doc.history.redo', 'doc.plan.execute'].includes(operationId);
|
|
220
|
+
const revision = guarded ? (this.currentRevision ?? (await readPublicRevision(this.source))) : undefined;
|
|
221
|
+
const result = await Reflect.apply(member, target, guarded
|
|
222
|
+
? [
|
|
223
|
+
{ ...args, expectedRevision: args.expectedRevision ?? revision },
|
|
224
|
+
{ ...options, expectedRevision: options?.expectedRevision ?? revision },
|
|
225
|
+
]
|
|
226
|
+
: [args, options]);
|
|
227
|
+
this.executedOperations.push({ operationId, result });
|
|
228
|
+
if (result?.success === false)
|
|
229
|
+
throw new errors.SuperDocCliError('Document operation did not apply.', {
|
|
230
|
+
code: result.failure?.code ?? 'APPLY_FAILED',
|
|
231
|
+
details: result,
|
|
232
|
+
});
|
|
233
|
+
if (documentEvidence.supportsDocumentFacts(this.source)) {
|
|
234
|
+
const applied = documentEvidence.getMutationRevision(result);
|
|
235
|
+
if (!applied ||
|
|
236
|
+
applied.before !== this.currentRevision ||
|
|
237
|
+
!applied.after ||
|
|
238
|
+
applied.after === 'unknown')
|
|
239
|
+
throw new errors.SuperDocCliError('Committed operation revision is unavailable or changed.', {
|
|
240
|
+
code: 'EVIDENCE_UNAVAILABLE',
|
|
241
|
+
});
|
|
242
|
+
this.currentRevision = applied.after;
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (member && typeof member === 'object')
|
|
248
|
+
return wrap(member, memberPath);
|
|
249
|
+
return member;
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
this.document = wrap(source, []);
|
|
253
|
+
documentEvidence.forwardDocumentEvidence(source, this.document);
|
|
254
|
+
}
|
|
255
|
+
async start() {
|
|
256
|
+
const { evidence, selectors = [], checks = [], completeReason } = this.requirements;
|
|
257
|
+
const compactRevision = this.requirements.revisionOnly && evidence !== 'full' && !completeReason
|
|
258
|
+
? await documentEvidence.getDocumentRevisionReader(this.source)?.()
|
|
259
|
+
: undefined;
|
|
260
|
+
this.legacyRevisionOnly = compactRevision !== undefined && !documentEvidence.supportsDocumentFacts(this.source);
|
|
261
|
+
this.fallbackReason =
|
|
262
|
+
completeReason ??
|
|
263
|
+
(!documentEvidence.supportsDocumentFacts(this.source) && !this.legacyRevisionOnly
|
|
264
|
+
? 'host lacks typed execution facts'
|
|
265
|
+
: selectors.some((selector) => !supportsLocalSelector(selector))
|
|
266
|
+
? 'selector requires complete index'
|
|
267
|
+
: checks.some((check) => !localCheck(check))
|
|
268
|
+
? 'verification requires complete evidence'
|
|
269
|
+
: undefined);
|
|
270
|
+
const complete = evidence === 'full' || this.fallbackReason ? await docSnapshot.buildMutationSnapshot(this.source) : undefined;
|
|
271
|
+
if (complete && (complete.revision === 'unknown' || complete.diagnostics.some((item) => item.section === 'info')))
|
|
272
|
+
throw new errors.SuperDocCliError('Required complete evidence is unavailable.', { code: 'EVIDENCE_UNAVAILABLE' });
|
|
273
|
+
this.pre = new DocumentFacts(this.source, complete?.revision ?? compactRevision ?? (await readPublicRevision(this.source)), complete);
|
|
274
|
+
this.currentRevision = this.pre.revision;
|
|
275
|
+
await this.pre.prepare(checks);
|
|
276
|
+
return this.pre;
|
|
277
|
+
}
|
|
278
|
+
async finishRevision() {
|
|
279
|
+
if (this.requirements.evidence === 'full')
|
|
280
|
+
return this.finish();
|
|
281
|
+
const summary = this.pre?.complete ? await docSnapshot.buildDocumentSnapshot(this.source, { countsOnly: true }) : undefined;
|
|
282
|
+
if (summary && (summary.revision === 'unknown' || summary.diagnostics.some((item) => item.section === 'info')))
|
|
283
|
+
throw new errors.SuperDocCliError('Required summary is unavailable.', { code: 'EVIDENCE_UNAVAILABLE' });
|
|
284
|
+
const revision = summary?.revision ?? (await readPublicRevision(this.source));
|
|
285
|
+
this.assertExecutionRevision(revision);
|
|
286
|
+
this.post = new DocumentFacts(this.source, revision, undefined, summary?.counts);
|
|
287
|
+
this.currentRevision = this.post.revision;
|
|
288
|
+
return this.post;
|
|
289
|
+
}
|
|
290
|
+
async finish() {
|
|
291
|
+
const complete = this.pre?.complete ? await docSnapshot.buildMutationSnapshot(this.source) : undefined;
|
|
292
|
+
if (complete && (complete.revision === 'unknown' || complete.diagnostics.some((item) => item.section === 'info')))
|
|
293
|
+
throw new errors.SuperDocCliError('Required complete evidence is unavailable.', { code: 'EVIDENCE_UNAVAILABLE' });
|
|
294
|
+
const revision = complete?.revision ?? (await readPublicRevision(this.source));
|
|
295
|
+
this.assertExecutionRevision(revision);
|
|
296
|
+
this.post = new DocumentFacts(this.source, revision, complete);
|
|
297
|
+
this.currentRevision = this.post.revision;
|
|
298
|
+
return this.post;
|
|
299
|
+
}
|
|
300
|
+
assertExecutionRevision(revision) {
|
|
301
|
+
if (documentEvidence.supportsDocumentFacts(this.source) && this.currentRevision !== revision)
|
|
302
|
+
throw new errors.SuperDocCliError('Document changed outside this execution.', { code: 'REVISION_CONFLICT' });
|
|
303
|
+
}
|
|
304
|
+
receipt(intent, verification, extra = {}) {
|
|
305
|
+
return {
|
|
306
|
+
status: verification.every((check) => check.passed) ? 'ok' : 'failed',
|
|
307
|
+
intent,
|
|
308
|
+
preSnapshot: this.pre?.summary,
|
|
309
|
+
postSnapshot: this.post?.summary,
|
|
310
|
+
selectedTargets: this.selectedTargets,
|
|
311
|
+
executedOperations: this.executedOperations,
|
|
312
|
+
verification,
|
|
313
|
+
...(this.fallbackReason ? { evidenceFallback: this.fallbackReason } : {}),
|
|
314
|
+
...extra,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
failure(intent, error) {
|
|
318
|
+
const err = error;
|
|
319
|
+
return this.receipt(intent, [], {
|
|
320
|
+
status: err?.code === 'AMBIGUOUS_SELECTOR' ? 'aborted' : 'failed',
|
|
321
|
+
errors: [
|
|
322
|
+
{ code: err?.code ?? 'ACTION_FAILED', message: err?.message ?? String(error), recovery: { kind: 'reinspect' } },
|
|
323
|
+
],
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function evaluateFactChecks(pre, post, checks) {
|
|
328
|
+
const results = [];
|
|
329
|
+
for (const check of checks) {
|
|
330
|
+
if ([
|
|
331
|
+
'revision-changed',
|
|
332
|
+
'revision-unchanged',
|
|
333
|
+
'block-count-delta',
|
|
334
|
+
'comment-count-delta',
|
|
335
|
+
'tracked-change-count-delta',
|
|
336
|
+
].includes(check.kind) &&
|
|
337
|
+
!pre) {
|
|
338
|
+
results.push({ check, passed: false, detail: `${check.kind} requires a baseline snapshot` });
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (check.kind === 'revision-changed' || check.kind === 'revision-unchanged')
|
|
342
|
+
results.push({
|
|
343
|
+
check,
|
|
344
|
+
passed: check.kind === 'revision-changed' ? pre.revision !== post.revision : pre.revision === post.revision,
|
|
345
|
+
detail: `pre=${pre.revision} post=${post.revision}`,
|
|
346
|
+
});
|
|
347
|
+
else if (check.kind === 'block-count-delta') {
|
|
348
|
+
const before = await pre.count(check.nodeType), after = await post.count(check.nodeType);
|
|
349
|
+
results.push({
|
|
350
|
+
check,
|
|
351
|
+
passed: after - before === check.delta,
|
|
352
|
+
detail: `pre=${before} post=${after} delta=${after - before}`,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
else if (check.kind === 'block-text-contains' || check.kind === 'block-text-equals') {
|
|
356
|
+
const block = await post.block(check.nodeId);
|
|
357
|
+
results.push({
|
|
358
|
+
check,
|
|
359
|
+
passed: !!block && (check.kind === 'block-text-equals' ? block.text === check.text : block.text.includes(check.text)),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
else if (check.kind === 'table-shape') {
|
|
363
|
+
const table = await post.tableShape(check.nodeId);
|
|
364
|
+
results.push({ check, passed: !!table && table.rows === check.rows && table.columns === check.columns });
|
|
365
|
+
}
|
|
366
|
+
else if (check.kind === 'comment-count-delta' || check.kind === 'tracked-change-count-delta') {
|
|
367
|
+
const key = check.kind === 'comment-count-delta' ? 'comments' : 'trackedChanges';
|
|
368
|
+
const before = pre?.complete?.counts[key], after = post.complete?.counts[key];
|
|
369
|
+
results.push({
|
|
370
|
+
check,
|
|
371
|
+
passed: before !== undefined && after !== undefined && after - before === check.delta,
|
|
372
|
+
detail: `pre=${before} post=${after}`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
else
|
|
376
|
+
results.push({ check, passed: false, detail: 'Check requires domain-specific evidence.' });
|
|
377
|
+
}
|
|
378
|
+
return results;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
exports.DocumentFacts = DocumentFacts;
|
|
382
|
+
exports.ExecutionContext = ExecutionContext;
|
|
383
|
+
exports.evaluateFactChecks = evaluateFactChecks;
|
|
384
|
+
exports.readPublicRevision = readPublicRevision;
|
|
385
|
+
exports.supportsLocalSelector = supportsLocalSelector;
|