@beyondwork/docx-react-component 1.0.71 → 1.0.72
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 +964 -75
- package/package.json +1 -1
- package/src/api/public-types.ts +243 -1
- package/src/api/v3/_create.ts +16 -1
- package/src/api/v3/_runtime-handle.ts +2 -0
- package/src/api/v3/ai/evaluate.ts +113 -0
- package/src/api/v3/ai/outline.ts +140 -0
- package/src/api/v3/ai/replacement.ts +8 -0
- package/src/api/v3/ai/review.ts +342 -0
- package/src/api/v3/ai/stats.ts +62 -0
- package/src/api/v3/runtime/viewport.ts +181 -0
- package/src/api/v3/runtime/workflow.ts +114 -1
- package/src/api/v3/ui/_types.ts +35 -0
- package/src/api/v3/ui/index.ts +1 -0
- package/src/api/v3/ui/viewport.ts +112 -0
- package/src/compare/diff-engine.ts +2 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/table-structure-commands.ts +1 -0
- package/src/io/export/serialize-headers-footers.ts +1 -0
- package/src/io/export/serialize-main-document.ts +13 -0
- package/src/io/export/serialize-paragraph-formatting.ts +34 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/normalize/normalize-text.ts +11 -0
- package/src/io/ooxml/parse-main-document.ts +21 -5
- package/src/io/ooxml/parse-paragraph-formatting.ts +105 -0
- package/src/model/canonical-document.ts +401 -1
- package/src/runtime/formatting/formatting-context.ts +2 -1
- package/src/runtime/geometry/overlay-rects.ts +7 -10
- package/src/runtime/layout/layout-engine-version.ts +257 -1
- package/src/runtime/layout/paginated-layout-engine.ts +134 -8
- package/src/runtime/layout/resolved-formatting-state.ts +108 -13
- package/src/runtime/markdown-sanitizer.ts +21 -4
- package/src/runtime/render/render-kernel.ts +21 -1
- package/src/runtime/scopes/audit-bundle.ts +8 -0
- package/src/runtime/scopes/compiler-service.ts +1 -0
- package/src/runtime/scopes/enumerate-scopes.ts +61 -3
- package/src/runtime/scopes/replacement/apply.ts +49 -3
- package/src/runtime/scopes/semantic-scope-types.ts +8 -0
- package/src/runtime/surface-projection.ts +22 -0
- package/src/runtime/workflow/coordinator.ts +3 -0
- package/src/runtime/workflow/scope-writer.ts +34 -0
- package/src/session/export/embedded-reconstitute.ts +37 -3
- package/src/session/import/embedded-offload.ts +26 -1
- package/src/shell/media-previews.ts +8 -6
- package/src/ui/WordReviewEditor.tsx +1 -0
- package/src/ui/editor-surface-controller.tsx +11 -0
- package/src/ui/headless/selection-helpers.ts +2 -2
- package/src/ui/runtime-shortcut-dispatch.ts +4 -4
- package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +22 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +11 -11
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +5 -0
- package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +18 -1
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +22 -6
- package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +18 -1
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +98 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +18 -4
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +8 -1
- package/src/ui-tailwind/editor-surface/search-plugin.ts +2 -4
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +37 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +29 -4
- package/src/ui-tailwind/index.ts +4 -2
- package/src/ui-tailwind/page-chrome-model.ts +5 -7
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +5 -2
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +4 -1
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +4 -1
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +10 -1
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +4 -1
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +7 -1
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +7 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +73 -8
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +1 -1
- package/src/ui-tailwind/review-workspace/page-chrome.ts +4 -4
- package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +1 -1
- package/src/ui-tailwind/tw-review-workspace.tsx +1 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @endStateApi v3 — `ai` review-workflow family.
|
|
3
|
+
*
|
|
4
|
+
* acceptRevision / rejectRevision / resolveCommentThread.
|
|
5
|
+
*
|
|
6
|
+
* Thin adapters over Layer-06 review-workflow primitives exposed on
|
|
7
|
+
* `RuntimeApiHandle`:
|
|
8
|
+
*
|
|
9
|
+
* - `runtime.acceptChange(revisionId)` — dispatches `change.accept`
|
|
10
|
+
* through the /06 coordinator.
|
|
11
|
+
* - `runtime.rejectChange(revisionId)` — dispatches `change.reject`.
|
|
12
|
+
* - `runtime.resolveComment(commentId)` — dispatches `comment.resolve`.
|
|
13
|
+
*
|
|
14
|
+
* Refusal contract: when the id doesn't resolve (revision already
|
|
15
|
+
* accepted/rejected/detached, or unknown), the adapter returns
|
|
16
|
+
* `{accepted|rejected|resolved: false, reason: "<kind>-not-found:<id>" |
|
|
17
|
+
* "<kind>-not-actionable:<id>"}`. Success path is verified post-dispatch
|
|
18
|
+
* by re-reading `runtime.getRenderSnapshot().trackedChanges` /
|
|
19
|
+
* `.comments` and confirming the target entry transitioned.
|
|
20
|
+
*
|
|
21
|
+
* Architecture A4: these mutations emit exactly one `ScopeActionAudit`
|
|
22
|
+
* on the `scope` telemetry channel when the target carries a resolvable
|
|
23
|
+
* scope anchor. When no scope is resolvable (e.g. the revision anchors
|
|
24
|
+
* to a region that isn't an enumerable scope), the audit is skipped
|
|
25
|
+
* silently — mirrors `attach.ts`'s pre-scope-null arm. The mutation
|
|
26
|
+
* still commits; the contract is "at most one audit per mutation", and
|
|
27
|
+
* review-workflow mutations on non-scoped regions remain traceable via
|
|
28
|
+
* the primitive's own `change.*` dispatch records on the `command`
|
|
29
|
+
* telemetry channel.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { RuntimeApiHandle } from "../_runtime-handle.ts";
|
|
33
|
+
import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
|
|
34
|
+
import { emitUxResponse } from "../_ux-response.ts";
|
|
35
|
+
import {
|
|
36
|
+
captureScopeSnapshot,
|
|
37
|
+
emitScopeMetadataAudit,
|
|
38
|
+
snapshotDocumentHash,
|
|
39
|
+
} from "./_metadata-audit.ts";
|
|
40
|
+
|
|
41
|
+
export interface AcceptRevisionInput {
|
|
42
|
+
readonly revisionId: string;
|
|
43
|
+
/**
|
|
44
|
+
* Optional audit-target hint. When provided and resolvable, the A4
|
|
45
|
+
* `ScopeActionAudit` is emitted against this scope. Omitting it still
|
|
46
|
+
* accepts the revision; the audit is skipped silently.
|
|
47
|
+
*/
|
|
48
|
+
readonly scopeId?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface AcceptRevisionResult {
|
|
52
|
+
readonly revisionId: string;
|
|
53
|
+
readonly accepted: boolean;
|
|
54
|
+
readonly reason?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RejectRevisionInput {
|
|
58
|
+
readonly revisionId: string;
|
|
59
|
+
readonly scopeId?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface RejectRevisionResult {
|
|
63
|
+
readonly revisionId: string;
|
|
64
|
+
readonly rejected: boolean;
|
|
65
|
+
readonly reason?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ResolveCommentThreadInput {
|
|
69
|
+
readonly threadId: string;
|
|
70
|
+
readonly scopeId?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ResolveCommentThreadResult {
|
|
74
|
+
readonly threadId: string;
|
|
75
|
+
readonly resolved: boolean;
|
|
76
|
+
readonly reason?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const acceptRevisionMetadata: ApiV3FnMetadata = {
|
|
80
|
+
name: "ai.acceptRevision",
|
|
81
|
+
status: "live-with-adapter",
|
|
82
|
+
sourceLayer: "workflow-review",
|
|
83
|
+
liveEvidence: {
|
|
84
|
+
runnerTest: "test/api/v3/ai/ai-review-workflow.test.ts",
|
|
85
|
+
commit: "refactor-09-post-closure-review-workflow",
|
|
86
|
+
},
|
|
87
|
+
uxIntent: {
|
|
88
|
+
uiVisible: true,
|
|
89
|
+
expectsUxResponse: "inline-change",
|
|
90
|
+
expectedDelta: "tracked-change accepted; revision removed from pending list",
|
|
91
|
+
},
|
|
92
|
+
agentMetadata: {
|
|
93
|
+
readOrMutate: "mutate",
|
|
94
|
+
boundedScope: "document",
|
|
95
|
+
auditCategory: "revision-accept",
|
|
96
|
+
contextPromptShape:
|
|
97
|
+
"Accept a single tracked-change revision by revisionId. Optional scopeId hint binds the audit to a scope target.",
|
|
98
|
+
},
|
|
99
|
+
stateClass: "A-canonical",
|
|
100
|
+
persistsTo: "customXml",
|
|
101
|
+
broadcastsVia: "crdt",
|
|
102
|
+
rwdReference:
|
|
103
|
+
"§AI API § ai.acceptRevision. Adapter over runtime.acceptChange (Layer-06 review-workflow). A4 audit emitted when scopeId hint resolves; otherwise the primitive's /06 dispatch record on the command channel carries the trace.",
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const rejectRevisionMetadata: ApiV3FnMetadata = {
|
|
107
|
+
name: "ai.rejectRevision",
|
|
108
|
+
status: "live-with-adapter",
|
|
109
|
+
sourceLayer: "workflow-review",
|
|
110
|
+
liveEvidence: {
|
|
111
|
+
runnerTest: "test/api/v3/ai/ai-review-workflow.test.ts",
|
|
112
|
+
commit: "refactor-09-post-closure-review-workflow",
|
|
113
|
+
},
|
|
114
|
+
uxIntent: {
|
|
115
|
+
uiVisible: true,
|
|
116
|
+
expectsUxResponse: "inline-change",
|
|
117
|
+
expectedDelta: "tracked-change rejected; revision removed from pending list",
|
|
118
|
+
},
|
|
119
|
+
agentMetadata: {
|
|
120
|
+
readOrMutate: "mutate",
|
|
121
|
+
boundedScope: "document",
|
|
122
|
+
auditCategory: "revision-reject",
|
|
123
|
+
contextPromptShape:
|
|
124
|
+
"Reject a single tracked-change revision by revisionId. Optional scopeId hint binds the audit to a scope target.",
|
|
125
|
+
},
|
|
126
|
+
stateClass: "A-canonical",
|
|
127
|
+
persistsTo: "customXml",
|
|
128
|
+
broadcastsVia: "crdt",
|
|
129
|
+
rwdReference:
|
|
130
|
+
"§AI API § ai.rejectRevision. Adapter over runtime.rejectChange (Layer-06 review-workflow). A4 audit emitted when scopeId hint resolves.",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const resolveCommentThreadMetadata: ApiV3FnMetadata = {
|
|
134
|
+
name: "ai.resolveCommentThread",
|
|
135
|
+
status: "live-with-adapter",
|
|
136
|
+
sourceLayer: "workflow-review",
|
|
137
|
+
liveEvidence: {
|
|
138
|
+
runnerTest: "test/api/v3/ai/ai-review-workflow.test.ts",
|
|
139
|
+
commit: "refactor-09-post-closure-review-workflow",
|
|
140
|
+
},
|
|
141
|
+
uxIntent: {
|
|
142
|
+
uiVisible: true,
|
|
143
|
+
expectsUxResponse: "inline-change",
|
|
144
|
+
expectedDelta: "comment thread marked resolved; thread moves out of open list",
|
|
145
|
+
},
|
|
146
|
+
agentMetadata: {
|
|
147
|
+
readOrMutate: "mutate",
|
|
148
|
+
boundedScope: "document",
|
|
149
|
+
auditCategory: "comment-resolve",
|
|
150
|
+
contextPromptShape:
|
|
151
|
+
"Resolve an open comment thread by threadId. Optional scopeId hint binds the audit to the anchored scope.",
|
|
152
|
+
},
|
|
153
|
+
stateClass: "A-canonical",
|
|
154
|
+
persistsTo: "customXml",
|
|
155
|
+
broadcastsVia: "crdt",
|
|
156
|
+
rwdReference:
|
|
157
|
+
"§AI API § ai.resolveCommentThread. Adapter over runtime.resolveComment (Layer-06 review-workflow). A4 audit emitted when scopeId hint resolves.",
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export function createReviewFamily(runtime: RuntimeApiHandle) {
|
|
161
|
+
return {
|
|
162
|
+
acceptRevision(input: AcceptRevisionInput): AcceptRevisionResult {
|
|
163
|
+
// @endStateApi — live-with-adapter. Routes through Layer-06's
|
|
164
|
+
// runtime.acceptChange() dispatch; A4 audit emitted on success when
|
|
165
|
+
// caller supplied a resolvable scopeId hint.
|
|
166
|
+
const { revisionId } = input;
|
|
167
|
+
const before = runtime.getRenderSnapshot().trackedChanges;
|
|
168
|
+
const target = before.revisions.find((r) => r.revisionId === revisionId);
|
|
169
|
+
if (!target) {
|
|
170
|
+
return {
|
|
171
|
+
revisionId,
|
|
172
|
+
accepted: false,
|
|
173
|
+
reason: `revision-not-found:${revisionId}`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (!target.canAccept || target.status !== "active") {
|
|
177
|
+
return {
|
|
178
|
+
revisionId,
|
|
179
|
+
accepted: false,
|
|
180
|
+
reason: `revision-not-actionable:${revisionId}`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const preScope = input.scopeId
|
|
184
|
+
? captureScopeSnapshot(runtime, input.scopeId)
|
|
185
|
+
: null;
|
|
186
|
+
const documentHashBefore = snapshotDocumentHash(runtime);
|
|
187
|
+
runtime.acceptChange(revisionId);
|
|
188
|
+
const after = runtime.getRenderSnapshot().trackedChanges;
|
|
189
|
+
const accepted = !after.pendingChangeIds.includes(revisionId);
|
|
190
|
+
if (accepted && preScope && input.scopeId) {
|
|
191
|
+
emitScopeMetadataAudit({
|
|
192
|
+
runtime,
|
|
193
|
+
actionId: acceptRevisionMetadata.name,
|
|
194
|
+
scopeId: input.scopeId,
|
|
195
|
+
targetScopeSnapshot: preScope,
|
|
196
|
+
proposedContent: {
|
|
197
|
+
kind: "explanation",
|
|
198
|
+
payload: { revisionId, intent: "accept" },
|
|
199
|
+
},
|
|
200
|
+
compiledOperationKind: "metadata-attach-explanation",
|
|
201
|
+
compiledOperationSummary: `accept revision ${revisionId}`,
|
|
202
|
+
emittedAtUtc: new Date(0).toISOString(),
|
|
203
|
+
documentHashBefore,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
emitUxResponse(runtime, {
|
|
207
|
+
apiFn: acceptRevisionMetadata.name,
|
|
208
|
+
intent: acceptRevisionMetadata.uxIntent.expectedDelta ?? "",
|
|
209
|
+
mockOrLive: "live",
|
|
210
|
+
uiVisible: true,
|
|
211
|
+
expectedDelta: acceptRevisionMetadata.uxIntent.expectedDelta,
|
|
212
|
+
});
|
|
213
|
+
return accepted
|
|
214
|
+
? { revisionId, accepted: true }
|
|
215
|
+
: {
|
|
216
|
+
revisionId,
|
|
217
|
+
accepted: false,
|
|
218
|
+
reason: `revision-not-actionable:${revisionId}`,
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
rejectRevision(input: RejectRevisionInput): RejectRevisionResult {
|
|
223
|
+
// @endStateApi — live-with-adapter. Routes through Layer-06's
|
|
224
|
+
// runtime.rejectChange() dispatch.
|
|
225
|
+
const { revisionId } = input;
|
|
226
|
+
const before = runtime.getRenderSnapshot().trackedChanges;
|
|
227
|
+
const target = before.revisions.find((r) => r.revisionId === revisionId);
|
|
228
|
+
if (!target) {
|
|
229
|
+
return {
|
|
230
|
+
revisionId,
|
|
231
|
+
rejected: false,
|
|
232
|
+
reason: `revision-not-found:${revisionId}`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (!target.canReject || target.status !== "active") {
|
|
236
|
+
return {
|
|
237
|
+
revisionId,
|
|
238
|
+
rejected: false,
|
|
239
|
+
reason: `revision-not-actionable:${revisionId}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const preScope = input.scopeId
|
|
243
|
+
? captureScopeSnapshot(runtime, input.scopeId)
|
|
244
|
+
: null;
|
|
245
|
+
const documentHashBefore = snapshotDocumentHash(runtime);
|
|
246
|
+
runtime.rejectChange(revisionId);
|
|
247
|
+
const after = runtime.getRenderSnapshot().trackedChanges;
|
|
248
|
+
const rejected = !after.pendingChangeIds.includes(revisionId);
|
|
249
|
+
if (rejected && preScope && input.scopeId) {
|
|
250
|
+
emitScopeMetadataAudit({
|
|
251
|
+
runtime,
|
|
252
|
+
actionId: rejectRevisionMetadata.name,
|
|
253
|
+
scopeId: input.scopeId,
|
|
254
|
+
targetScopeSnapshot: preScope,
|
|
255
|
+
proposedContent: {
|
|
256
|
+
kind: "explanation",
|
|
257
|
+
payload: { revisionId, intent: "reject" },
|
|
258
|
+
},
|
|
259
|
+
compiledOperationKind: "metadata-attach-explanation",
|
|
260
|
+
compiledOperationSummary: `reject revision ${revisionId}`,
|
|
261
|
+
emittedAtUtc: new Date(0).toISOString(),
|
|
262
|
+
documentHashBefore,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
emitUxResponse(runtime, {
|
|
266
|
+
apiFn: rejectRevisionMetadata.name,
|
|
267
|
+
intent: rejectRevisionMetadata.uxIntent.expectedDelta ?? "",
|
|
268
|
+
mockOrLive: "live",
|
|
269
|
+
uiVisible: true,
|
|
270
|
+
expectedDelta: rejectRevisionMetadata.uxIntent.expectedDelta,
|
|
271
|
+
});
|
|
272
|
+
return rejected
|
|
273
|
+
? { revisionId, rejected: true }
|
|
274
|
+
: {
|
|
275
|
+
revisionId,
|
|
276
|
+
rejected: false,
|
|
277
|
+
reason: `revision-not-actionable:${revisionId}`,
|
|
278
|
+
};
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
resolveCommentThread(
|
|
282
|
+
input: ResolveCommentThreadInput,
|
|
283
|
+
): ResolveCommentThreadResult {
|
|
284
|
+
// @endStateApi — live-with-adapter. Routes through Layer-06's
|
|
285
|
+
// runtime.resolveComment() dispatch.
|
|
286
|
+
const { threadId } = input;
|
|
287
|
+
const before = runtime.getRenderSnapshot().comments;
|
|
288
|
+
const target = before.threads.find((t) => t.commentId === threadId);
|
|
289
|
+
if (!target) {
|
|
290
|
+
return {
|
|
291
|
+
threadId,
|
|
292
|
+
resolved: false,
|
|
293
|
+
reason: `thread-not-found:${threadId}`,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (target.status !== "open") {
|
|
297
|
+
return {
|
|
298
|
+
threadId,
|
|
299
|
+
resolved: false,
|
|
300
|
+
reason: `thread-not-open:${threadId}`,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
const preScope = input.scopeId
|
|
304
|
+
? captureScopeSnapshot(runtime, input.scopeId)
|
|
305
|
+
: null;
|
|
306
|
+
const documentHashBefore = snapshotDocumentHash(runtime);
|
|
307
|
+
runtime.resolveComment(threadId);
|
|
308
|
+
const after = runtime.getRenderSnapshot().comments;
|
|
309
|
+
const resolved = after.resolvedCommentIds.includes(threadId);
|
|
310
|
+
if (resolved && preScope && input.scopeId) {
|
|
311
|
+
emitScopeMetadataAudit({
|
|
312
|
+
runtime,
|
|
313
|
+
actionId: resolveCommentThreadMetadata.name,
|
|
314
|
+
scopeId: input.scopeId,
|
|
315
|
+
targetScopeSnapshot: preScope,
|
|
316
|
+
proposedContent: {
|
|
317
|
+
kind: "explanation",
|
|
318
|
+
payload: { threadId, intent: "resolve-comment" },
|
|
319
|
+
},
|
|
320
|
+
compiledOperationKind: "metadata-attach-explanation",
|
|
321
|
+
compiledOperationSummary: `resolve comment thread ${threadId}`,
|
|
322
|
+
emittedAtUtc: new Date(0).toISOString(),
|
|
323
|
+
documentHashBefore,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
emitUxResponse(runtime, {
|
|
327
|
+
apiFn: resolveCommentThreadMetadata.name,
|
|
328
|
+
intent: resolveCommentThreadMetadata.uxIntent.expectedDelta ?? "",
|
|
329
|
+
mockOrLive: "live",
|
|
330
|
+
uiVisible: true,
|
|
331
|
+
expectedDelta: resolveCommentThreadMetadata.uxIntent.expectedDelta,
|
|
332
|
+
});
|
|
333
|
+
return resolved
|
|
334
|
+
? { threadId, resolved: true }
|
|
335
|
+
: {
|
|
336
|
+
threadId,
|
|
337
|
+
resolved: false,
|
|
338
|
+
reason: `thread-not-resolvable:${threadId}`,
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @endStateApi v3 — `ai.getDocumentStatistics`.
|
|
3
|
+
*
|
|
4
|
+
* Passes `DocumentStats` derived inside the runtime through to agents.
|
|
5
|
+
* Counts: paragraphs, words, characters, comments, revisions.
|
|
6
|
+
*
|
|
7
|
+
* Read-family; no audit emission. Complements `ai.inspectDocument`
|
|
8
|
+
* (which reports scope-level counts) with flat document-level counts
|
|
9
|
+
* an agent can surface directly.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { RuntimeApiHandle } from "../_runtime-handle.ts";
|
|
13
|
+
import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
|
|
14
|
+
import { deriveDocumentStats } from "../../../core/state/editor-state.ts";
|
|
15
|
+
|
|
16
|
+
export interface DocumentStatisticsResult {
|
|
17
|
+
readonly paragraphCount: number;
|
|
18
|
+
readonly wordCount: number;
|
|
19
|
+
readonly characterCount: number;
|
|
20
|
+
readonly commentCount: number;
|
|
21
|
+
readonly revisionCount: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const getDocumentStatisticsMetadata: ApiV3FnMetadata = {
|
|
25
|
+
name: "ai.getDocumentStatistics",
|
|
26
|
+
status: "live-with-adapter",
|
|
27
|
+
sourceLayer: "canonical",
|
|
28
|
+
liveEvidence: {
|
|
29
|
+
runnerTest: "test/api/v3/ai/ai-document-read.test.ts",
|
|
30
|
+
commit: "refactor-09-post-closure-document-read",
|
|
31
|
+
},
|
|
32
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
33
|
+
agentMetadata: {
|
|
34
|
+
readOrMutate: "read",
|
|
35
|
+
boundedScope: "document",
|
|
36
|
+
auditCategory: "document-stats-read",
|
|
37
|
+
contextPromptShape:
|
|
38
|
+
"Flat document counts: paragraphs, words, characters, comments, revisions.",
|
|
39
|
+
},
|
|
40
|
+
stateClass: "A-canonical",
|
|
41
|
+
persistsTo: "canonical",
|
|
42
|
+
rwdReference:
|
|
43
|
+
"§AI API § ai.getDocumentStatistics. Read-through of the DocumentStats record derived from the canonical document. Complements ai.inspectDocument's scope-level counts.",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function createStatsFamily(runtime: RuntimeApiHandle) {
|
|
47
|
+
return {
|
|
48
|
+
getDocumentStatistics(): DocumentStatisticsResult {
|
|
49
|
+
// @endStateApi — live-with-adapter. Passes deriveDocumentStats
|
|
50
|
+
// through the canonical document for agent-facing counts.
|
|
51
|
+
const document = runtime.getCanonicalDocument();
|
|
52
|
+
const stats = deriveDocumentStats({ document });
|
|
53
|
+
return {
|
|
54
|
+
paragraphCount: stats.paragraphCount,
|
|
55
|
+
wordCount: stats.wordCount,
|
|
56
|
+
characterCount: stats.characterCount,
|
|
57
|
+
commentCount: stats.commentCount,
|
|
58
|
+
revisionCount: stats.revisionCount,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @endStateApi v3 — `runtime.viewport` family.
|
|
3
|
+
*
|
|
4
|
+
* Closes coord-07 §2.9 / coord-10 §γ — page-anchor + page-geometry
|
|
5
|
+
* primitives the visual-fidelity harness (and any future "go to page N"
|
|
6
|
+
* host) consumes through `ui.viewport.scrollToPage(n)`.
|
|
7
|
+
*
|
|
8
|
+
* Background (coord-07 §2.9 / coord-10 §γ filed 2026-04-23). The
|
|
9
|
+
* visual-fidelity harness approximated page navigation via PM
|
|
10
|
+
* `[data-page-frame='N']` boundary widgets + `scrollIntoView`. Three
|
|
11
|
+
* problems surfaced:
|
|
12
|
+
* 1. Widget naming drifted once already (`data-page-index` → `data-page-frame`).
|
|
13
|
+
* 2. Boundary widgets are zero-height strips between pages, not on
|
|
14
|
+
* page content — scrolling one to viewport-top parks the camera
|
|
15
|
+
* between pages.
|
|
16
|
+
* 3. Page 1 + final page have no boundary widget (only N-1 widgets for
|
|
17
|
+
* N pages).
|
|
18
|
+
*
|
|
19
|
+
* A runtime-owned primitive backed by the layout page graph eliminates
|
|
20
|
+
* both the chrome-drift risk and the gap-vs-content problem. Pure
|
|
21
|
+
* reads; `stateClass: "A-canonical"` + `persistsTo: "canonical"` — the
|
|
22
|
+
* layout facet is derived canonical state.
|
|
23
|
+
*
|
|
24
|
+
* `elementId` stays `null` until L11's per-page content wrapper (coord-11
|
|
25
|
+
* §19 / §P) lands with a stable `data-page-frame-start="page-<section>-<page>"`
|
|
26
|
+
* id. Until then callers rely on `scrollY` / `pageRect` for positioning.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { RuntimeApiHandle } from "../_runtime-handle.ts";
|
|
30
|
+
import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
|
|
31
|
+
import { DEFAULT_PX_PER_TWIP } from "../../public-types.ts";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolves a 1-based page number to a stable scroll target.
|
|
35
|
+
*
|
|
36
|
+
* - `elementId`: DOM id L11 populates on the per-page content wrapper.
|
|
37
|
+
* Returns `null` while the L11 wrapper contract (coord-11 §19 / §P)
|
|
38
|
+
* is still in flight — callers fall back to `scrollY`.
|
|
39
|
+
* - `scrollY`: scroll-container-relative Y offset of the page's top
|
|
40
|
+
* edge in CSS px. Computed as the sum of prior pages' heights via
|
|
41
|
+
* the layout facet's page graph.
|
|
42
|
+
* - `pageRect`: the page's content rectangle in the scroll container's
|
|
43
|
+
* coordinate space. `top === scrollY`; `left === 0`; `width/height`
|
|
44
|
+
* projected from the page's canonical `pageWidth` / `pageHeight`
|
|
45
|
+
* twips via `DEFAULT_PX_PER_TWIP`.
|
|
46
|
+
*/
|
|
47
|
+
export interface PageAnchor {
|
|
48
|
+
readonly elementId: string | null;
|
|
49
|
+
readonly scrollY: number;
|
|
50
|
+
readonly pageRect: {
|
|
51
|
+
readonly left: number;
|
|
52
|
+
readonly top: number;
|
|
53
|
+
readonly width: number;
|
|
54
|
+
readonly height: number;
|
|
55
|
+
} | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Per-page geometry for deterministic capture + truth-comparison.
|
|
60
|
+
* Stable across layout-recompute re-runs within the same
|
|
61
|
+
* `LAYOUT_ENGINE_VERSION`.
|
|
62
|
+
*/
|
|
63
|
+
export interface PageGeometry {
|
|
64
|
+
readonly widthPx: number;
|
|
65
|
+
readonly heightPx: number;
|
|
66
|
+
readonly marginsPx: {
|
|
67
|
+
readonly top: number;
|
|
68
|
+
readonly right: number;
|
|
69
|
+
readonly bottom: number;
|
|
70
|
+
readonly left: number;
|
|
71
|
+
};
|
|
72
|
+
/** CSS px-per-inch honoring the default kernel conversion (96 / 1440 × 1440 = 96). */
|
|
73
|
+
readonly dpi: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ================================================================== */
|
|
77
|
+
/* getPageAnchor */
|
|
78
|
+
/* ================================================================== */
|
|
79
|
+
|
|
80
|
+
export const getPageAnchorMetadata: ApiV3FnMetadata = {
|
|
81
|
+
name: "runtime.viewport.getPageAnchor",
|
|
82
|
+
status: "live-with-adapter",
|
|
83
|
+
sourceLayer: "layout-semantics",
|
|
84
|
+
liveEvidence: {
|
|
85
|
+
runnerTest: "test/api/v3/runtime/viewport-page-anchor.test.ts",
|
|
86
|
+
commit: "refactor-07-viewport-family-2026-04-24",
|
|
87
|
+
},
|
|
88
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
89
|
+
agentMetadata: {
|
|
90
|
+
readOrMutate: "read",
|
|
91
|
+
boundedScope: "document",
|
|
92
|
+
auditCategory: "viewport-read",
|
|
93
|
+
},
|
|
94
|
+
stateClass: "A-canonical",
|
|
95
|
+
persistsTo: "canonical",
|
|
96
|
+
rwdReference:
|
|
97
|
+
"§Runtime API § runtime.viewport.getPageAnchor. Reads the kernel-authoritative page frame from `handle.geometry.getPage(index).frame` — same source `ui.viewport.scrollToPage(n)` consumes (coord-10 §γ shipment `b8116b97`), so values stay consistent across the two surfaces. `elementId` stays null until L11's per-page content wrapper (coord-11 §19 / §P) lands; until then callers position off `scrollY` / `pageRect`. Promotes to `live` when the geometry facet surfaces a direct `getPageAnchor(pageNumber)` reader or when elementId is populated.",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/* ================================================================== */
|
|
101
|
+
/* getPageGeometry */
|
|
102
|
+
/* ================================================================== */
|
|
103
|
+
|
|
104
|
+
export const getPageGeometryMetadata: ApiV3FnMetadata = {
|
|
105
|
+
name: "runtime.viewport.getPageGeometry",
|
|
106
|
+
status: "live-with-adapter",
|
|
107
|
+
sourceLayer: "layout-semantics",
|
|
108
|
+
liveEvidence: {
|
|
109
|
+
runnerTest: "test/api/v3/runtime/viewport-page-anchor.test.ts",
|
|
110
|
+
commit: "refactor-07-viewport-family-2026-04-24",
|
|
111
|
+
},
|
|
112
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
113
|
+
agentMetadata: {
|
|
114
|
+
readOrMutate: "read",
|
|
115
|
+
boundedScope: "document",
|
|
116
|
+
auditCategory: "viewport-read",
|
|
117
|
+
},
|
|
118
|
+
stateClass: "A-canonical",
|
|
119
|
+
persistsTo: "canonical",
|
|
120
|
+
rwdReference:
|
|
121
|
+
"§Runtime API § runtime.viewport.getPageGeometry. Projects the selected page's `PageLayoutSnapshot` (pageWidth/Height + margins, all in twips) into CSS px via `DEFAULT_PX_PER_TWIP`. `dpi: 96` matches the kernel's default. Promotes to `live` when the layout facet projects PageLayoutSnapshot in px directly.",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/* ================================================================== */
|
|
125
|
+
/* family factory */
|
|
126
|
+
/* ================================================================== */
|
|
127
|
+
|
|
128
|
+
export function createViewportFamily(runtime: RuntimeApiHandle) {
|
|
129
|
+
const layout = runtime.layout;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
getPageAnchor(pageNumber: number): PageAnchor | null {
|
|
133
|
+
// @endStateApi — live-with-adapter. Reads the kernel-authoritative
|
|
134
|
+
// page frame from `handle.geometry.getPage(index)` — same source
|
|
135
|
+
// `ui.viewport.scrollToPage(n)` (coord-10 §γ shipment `b8116b97`)
|
|
136
|
+
// consumes. Geometry's `frame.topPx` includes page gaps + rounding
|
|
137
|
+
// the kernel applies, matching the DOM scroll-container's actual
|
|
138
|
+
// Y. Layout-only pageHeight summation would omit page gaps and
|
|
139
|
+
// diverge from both the DOM and the ui.viewport.scrollToPage return.
|
|
140
|
+
if (!layout) return null;
|
|
141
|
+
if (!Number.isInteger(pageNumber) || pageNumber < 1) return null;
|
|
142
|
+
if (pageNumber > layout.getPageCount()) return null;
|
|
143
|
+
const pageIndex = pageNumber - 1;
|
|
144
|
+
const geometry = runtime.geometry;
|
|
145
|
+
const page = geometry?.getPage(pageIndex);
|
|
146
|
+
if (!page) return null;
|
|
147
|
+
return {
|
|
148
|
+
elementId: null,
|
|
149
|
+
scrollY: page.frame.topPx,
|
|
150
|
+
pageRect: {
|
|
151
|
+
left: page.frame.leftPx,
|
|
152
|
+
top: page.frame.topPx,
|
|
153
|
+
width: page.frame.widthPx,
|
|
154
|
+
height: page.frame.heightPx,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
getPageGeometry(pageNumber: number): PageGeometry | null {
|
|
160
|
+
// @endStateApi — live-with-adapter.
|
|
161
|
+
if (!layout) return null;
|
|
162
|
+
if (!Number.isInteger(pageNumber) || pageNumber < 1) return null;
|
|
163
|
+
const pageCount = layout.getPageCount();
|
|
164
|
+
if (pageNumber > pageCount) return null;
|
|
165
|
+
const page = layout.getPage(pageNumber - 1);
|
|
166
|
+
if (!page) return null;
|
|
167
|
+
const lay = page.layout;
|
|
168
|
+
return {
|
|
169
|
+
widthPx: lay.pageWidth * DEFAULT_PX_PER_TWIP,
|
|
170
|
+
heightPx: lay.pageHeight * DEFAULT_PX_PER_TWIP,
|
|
171
|
+
marginsPx: {
|
|
172
|
+
top: lay.marginTop * DEFAULT_PX_PER_TWIP,
|
|
173
|
+
right: lay.marginRight * DEFAULT_PX_PER_TWIP,
|
|
174
|
+
bottom: lay.marginBottom * DEFAULT_PX_PER_TWIP,
|
|
175
|
+
left: lay.marginLeft * DEFAULT_PX_PER_TWIP,
|
|
176
|
+
},
|
|
177
|
+
dpi: 96,
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|