@beyondwork/docx-react-component 1.0.71 → 1.0.73

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 (87) hide show
  1. package/README.md +964 -75
  2. package/package.json +1 -1
  3. package/src/api/public-types.ts +280 -1
  4. package/src/api/v3/_create.ts +16 -1
  5. package/src/api/v3/_runtime-handle.ts +2 -0
  6. package/src/api/v3/ai/evaluate.ts +113 -0
  7. package/src/api/v3/ai/outline.ts +140 -0
  8. package/src/api/v3/ai/policy.ts +31 -0
  9. package/src/api/v3/ai/replacement.ts +8 -0
  10. package/src/api/v3/ai/review.ts +342 -0
  11. package/src/api/v3/ai/stats.ts +62 -0
  12. package/src/api/v3/runtime/viewport.ts +181 -0
  13. package/src/api/v3/runtime/workflow.ts +114 -1
  14. package/src/api/v3/ui/_types.ts +35 -0
  15. package/src/api/v3/ui/chrome-preset-model.ts +6 -0
  16. package/src/api/v3/ui/index.ts +1 -0
  17. package/src/api/v3/ui/viewport.ts +112 -0
  18. package/src/compare/diff-engine.ts +2 -0
  19. package/src/core/commands/formatting-commands.ts +1 -0
  20. package/src/core/commands/table-structure-commands.ts +1 -0
  21. package/src/core/state/editor-state.ts +49 -6
  22. package/src/io/export/serialize-footnotes.ts +6 -0
  23. package/src/io/export/serialize-headers-footers.ts +7 -0
  24. package/src/io/export/serialize-main-document.ts +20 -0
  25. package/src/io/export/serialize-paragraph-formatting.ts +34 -0
  26. package/src/io/export/split-review-boundaries.ts +1 -0
  27. package/src/io/normalize/normalize-text.ts +49 -2
  28. package/src/io/ooxml/parse-headers-footers.ts +31 -0
  29. package/src/io/ooxml/parse-main-document.ts +148 -7
  30. package/src/io/ooxml/parse-paragraph-formatting.ts +105 -0
  31. package/src/model/canonical-document.ts +401 -1
  32. package/src/runtime/formatting/formatting-context.ts +2 -1
  33. package/src/runtime/geometry/overlay-rects.ts +7 -10
  34. package/src/runtime/layout/layout-engine-version.ts +278 -1
  35. package/src/runtime/layout/paginated-layout-engine.ts +181 -8
  36. package/src/runtime/layout/resolved-formatting-state.ts +108 -13
  37. package/src/runtime/markdown-sanitizer.ts +21 -4
  38. package/src/runtime/render/render-kernel.ts +21 -1
  39. package/src/runtime/scopes/action-validation.ts +30 -4
  40. package/src/runtime/scopes/audit-bundle.ts +8 -0
  41. package/src/runtime/scopes/compiler-service.ts +1 -0
  42. package/src/runtime/scopes/enumerate-scopes.ts +61 -3
  43. package/src/runtime/scopes/replacement/apply.ts +50 -3
  44. package/src/runtime/scopes/scope-kinds/paragraph.ts +170 -7
  45. package/src/runtime/scopes/semantic-scope-types.ts +27 -0
  46. package/src/runtime/surface-projection.ts +77 -0
  47. package/src/runtime/workflow/coordinator.ts +3 -0
  48. package/src/runtime/workflow/scope-writer.ts +34 -0
  49. package/src/session/export/embedded-reconstitute.ts +37 -3
  50. package/src/session/import/embedded-offload.ts +26 -1
  51. package/src/session/import/loader-types.ts +18 -0
  52. package/src/session/import/loader.ts +2 -0
  53. package/src/shell/media-previews.ts +8 -6
  54. package/src/ui/WordReviewEditor.tsx +1 -0
  55. package/src/ui/editor-surface-controller.tsx +11 -0
  56. package/src/ui/headless/selection-helpers.ts +2 -2
  57. package/src/ui/runtime-shortcut-dispatch.ts +4 -4
  58. package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +22 -4
  59. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +11 -11
  60. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -1
  61. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +5 -0
  62. package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +18 -1
  63. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +22 -6
  64. package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +18 -1
  65. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +98 -3
  66. package/src/ui-tailwind/editor-surface/pm-schema.ts +50 -4
  67. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +6 -0
  68. package/src/ui-tailwind/editor-surface/scroll-anchor.ts +8 -1
  69. package/src/ui-tailwind/editor-surface/search-plugin.ts +2 -4
  70. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +114 -0
  71. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +12 -4
  72. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +29 -4
  73. package/src/ui-tailwind/index.ts +4 -2
  74. package/src/ui-tailwind/page-chrome-model.ts +5 -7
  75. package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +54 -34
  76. package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +4 -1
  77. package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +4 -1
  78. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +10 -1
  79. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +8 -1
  80. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +11 -1
  81. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +7 -1
  82. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +139 -10
  83. package/src/ui-tailwind/review/comment-markdown-renderer.tsx +1 -1
  84. package/src/ui-tailwind/review-workspace/page-chrome.ts +4 -4
  85. package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +1 -1
  86. package/src/ui-tailwind/theme/editor-theme.css +15 -16
  87. package/src/ui-tailwind/tw-review-workspace.tsx +22 -14
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @endStateApi v3 — `ai.getDocumentOutline`.
3
+ *
4
+ * Composes the layer-04/07 navigation snapshot with the layer-07 outline
5
+ * builder to return a heading tree an agent can surface for "jump to
6
+ * section" / "summarize this section" flows.
7
+ *
8
+ * Each entry carries `headingId`, `level`, `text`, `offset`, `pageIndex`,
9
+ * `sectionIndex`, and `parentHeadingIds` (top-level headings have an
10
+ * empty array). `activeHeadingId` points at the heading containing the
11
+ * current selection head, if any.
12
+ *
13
+ * Read-family; no audit emission.
14
+ */
15
+
16
+ import type { RuntimeApiHandle } from "../_runtime-handle.ts";
17
+ import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
18
+ import type {
19
+ DocumentOutlineHeadingSnapshot,
20
+ DocumentOutlineSnapshot,
21
+ } from "../../public-types.ts";
22
+ import { createDocumentNavigationSnapshot } from "../../../runtime/document-navigation.ts";
23
+ import { createDocumentOutlineSnapshot } from "../../../runtime/document-outline.ts";
24
+ import {
25
+ computeBlockPositions,
26
+ createScopeCompilerService,
27
+ } from "../../../runtime/scopes/index.ts";
28
+
29
+ /**
30
+ * Gap B (coord-08 post-Slice-7 integration) — v3 extension of
31
+ * `DocumentOutlineHeadingSnapshot` that carries the L08 scope
32
+ * compiler's `scopeId` for each heading. Agents use it directly with
33
+ * `ai.applyReplacementScope` / `ai.attachExplanation` / etc. without a
34
+ * separate `ai.listScopes({kind:"heading"})` lookup + offset match.
35
+ *
36
+ * The field is populated when the compiler's heading enumeration has
37
+ * a matching entry by block-range start; omitted when no match is
38
+ * found (e.g. a heading outside the `main` story). Empty-docs /
39
+ * no-heading docs return `headings: []` — the enrichment is a no-op.
40
+ */
41
+ export type GetDocumentOutlineHeadingEntry = DocumentOutlineHeadingSnapshot & {
42
+ readonly scopeId?: string;
43
+ };
44
+
45
+ export type GetDocumentOutlineResult = Omit<
46
+ DocumentOutlineSnapshot,
47
+ "headings"
48
+ > & {
49
+ readonly headings: readonly GetDocumentOutlineHeadingEntry[];
50
+ };
51
+
52
+ export const getDocumentOutlineMetadata: ApiV3FnMetadata = {
53
+ name: "ai.getDocumentOutline",
54
+ status: "live-with-adapter",
55
+ sourceLayer: "workflow-review",
56
+ liveEvidence: {
57
+ runnerTest: "test/api/v3/ai/ai-document-read.test.ts",
58
+ commit: "refactor-09-post-closure-document-read",
59
+ },
60
+ uxIntent: { uiVisible: false, expectsUxResponse: "none" },
61
+ agentMetadata: {
62
+ readOrMutate: "read",
63
+ boundedScope: "document",
64
+ auditCategory: "document-outline-read",
65
+ contextPromptShape:
66
+ "Hierarchical heading outline — {headingId, level, text, offset, pageIndex, sectionIndex, parentHeadingIds} per entry, plus activeHeadingId when selection lives under a heading.",
67
+ },
68
+ stateClass: "A-canonical",
69
+ persistsTo: "canonical",
70
+ rwdReference:
71
+ "§AI API § ai.getDocumentOutline. Composes createDocumentNavigationSnapshot (L04/07) with createDocumentOutlineSnapshot (L07) to surface the heading tree. Read-only; no audit emission.",
72
+ };
73
+
74
+ export function createOutlineFamily(runtime: RuntimeApiHandle) {
75
+ const compiler = createScopeCompilerService(runtime);
76
+ return {
77
+ getDocumentOutline(): GetDocumentOutlineResult {
78
+ // @endStateApi — live-with-adapter. Composes L04/07 navigation
79
+ // snapshot with L07 outline builder to surface heading tree;
80
+ // enriches each heading entry with the L08 compiler's `scopeId`
81
+ // (Gap B, coord-08 post-Slice-7 integration).
82
+ const snapshot = runtime.getRenderSnapshot();
83
+ const document = runtime.getCanonicalDocument();
84
+ const selectionHead = snapshot.selection.head;
85
+ const navigation = createDocumentNavigationSnapshot(
86
+ document,
87
+ selectionHead,
88
+ snapshot.activeStory,
89
+ );
90
+ const outline = createDocumentOutlineSnapshot({
91
+ navigation,
92
+ activeStory: snapshot.activeStory,
93
+ selectionHead,
94
+ });
95
+
96
+ // Gap B enrichment — build an offset → scopeId map over the
97
+ // heading enumeration and join onto each outline entry. The
98
+ // offset-based join uses `computeBlockPositions` to translate
99
+ // the compiler's `blockIndex` (on the enumerated scope's
100
+ // `semanticPath`) back into a character offset equal to the
101
+ // outline's `offset` field.
102
+ const headingScopes = compiler
103
+ .compileAllScopes()
104
+ .filter((s) => s.kind === "heading");
105
+ const blockPositions = computeBlockPositions(document);
106
+ const blockOffsetByIndex = new Map<number, number>();
107
+ for (const entry of blockPositions) {
108
+ blockOffsetByIndex.set(entry.blockIndex, entry.from);
109
+ }
110
+ const scopeIdByOffset = new Map<number, string>();
111
+ for (const scope of headingScopes) {
112
+ // `semanticPath` is ["body", "heading", <level>, <blockIndex>]
113
+ // — the blockIndex is the last segment. Numeric parse + offset
114
+ // lookup gives us the heading paragraph's character offset.
115
+ const path = scope.handle.semanticPath;
116
+ const lastSegment = path[path.length - 1];
117
+ if (typeof lastSegment !== "string") continue;
118
+ const blockIndex = Number.parseInt(lastSegment, 10);
119
+ if (!Number.isFinite(blockIndex)) continue;
120
+ const offset = blockOffsetByIndex.get(blockIndex);
121
+ if (typeof offset === "number") {
122
+ scopeIdByOffset.set(offset, scope.handle.scopeId);
123
+ }
124
+ }
125
+
126
+ const enrichedHeadings: GetDocumentOutlineHeadingEntry[] =
127
+ outline.headings.map((heading) => {
128
+ const scopeId = scopeIdByOffset.get(heading.offset);
129
+ return scopeId ? { ...heading, scopeId } : heading;
130
+ });
131
+
132
+ return {
133
+ ...(outline.activeHeadingId
134
+ ? { activeHeadingId: outline.activeHeadingId }
135
+ : {}),
136
+ headings: enrichedHeadings,
137
+ };
138
+ },
139
+ };
140
+ }
@@ -38,6 +38,30 @@ export interface GetPolicyInput {
38
38
 
39
39
  export type GetPolicyResult = AIActionPolicy | readonly AIActionPolicy[];
40
40
 
41
+ export type ListAIActionsResult = readonly AIAction[];
42
+
43
+ export const listAIActionsMetadata: ApiV3FnMetadata = {
44
+ name: "ai.listAIActions",
45
+ status: "live-with-adapter",
46
+ sourceLayer: "workflow-review",
47
+ liveEvidence: {
48
+ runnerTest: "test/api/v3/ai/ai-list-actions.test.ts",
49
+ commit: "refactor-09-post-closure-ki-p5",
50
+ },
51
+ uxIntent: { uiVisible: false, expectsUxResponse: "none" },
52
+ agentMetadata: {
53
+ readOrMutate: "read",
54
+ boundedScope: "document",
55
+ auditCategory: "policy-list",
56
+ contextPromptShape:
57
+ "Discovery: returns the AIAction vocabulary with policy entries. Use before calling getPolicy/evaluateAction so ids aren't guessed (closes KI-P5).",
58
+ },
59
+ stateClass: "A-canonical",
60
+ persistsTo: "canonical",
61
+ rwdReference:
62
+ "§AI API § ai.listAIActions. Read-only adapter over AI_ACTION_POLICIES — returns every AIAction id with a shipped policy entry. Closes KI-P5 (AIAction discoverability) by giving agents a runtime-discoverable vocabulary.",
63
+ };
64
+
41
65
  export const getPolicyMetadata: ApiV3FnMetadata = {
42
66
  name: "ai.getPolicy",
43
67
  status: "live-with-adapter",
@@ -62,6 +86,13 @@ export const getPolicyMetadata: ApiV3FnMetadata = {
62
86
 
63
87
  export function createPolicyFamily(_runtime: RuntimeApiHandle) {
64
88
  return {
89
+ listAIActions(): ListAIActionsResult {
90
+ // @endStateApi — live-with-adapter. Projects AI_ACTION_POLICIES[]
91
+ // to the action-id list; every entry is guaranteed policy-backed
92
+ // (getPolicy on these ids returns support != 'unsupported').
93
+ return Object.freeze(AI_ACTION_POLICIES.map((p) => p.action));
94
+ },
95
+
65
96
  getPolicy(input?: GetPolicyInput): GetPolicyResult {
66
97
  // @endStateApi — live-with-adapter. Delegates to Layer-06's
67
98
  // getAIActionPolicy(action) for single-action lookups or returns
@@ -121,6 +121,13 @@ export interface ApplyResult {
121
121
  readonly reason?: string;
122
122
  readonly blockers?: readonly string[];
123
123
  readonly auditHint?: string;
124
+ /**
125
+ * Gap A (post-Slice-7 integration) — revision IDs authored during
126
+ * the apply. Populated for suggest-mode (tracked insert + delete);
127
+ * empty for direct-edit. Agents chain into `ai.acceptRevision` /
128
+ * `ai.rejectRevision` with each id to land or discard the proposal.
129
+ */
130
+ readonly authoredRevisionIds: readonly string[];
124
131
  }
125
132
 
126
133
  export interface ApplyReplacementScopeInput {
@@ -331,6 +338,7 @@ export function createReplacementFamily(runtime: RuntimeApiHandle) {
331
338
  ? { blockers: Object.freeze([...result.validation.blockedReasons]) }
332
339
  : {}),
333
340
  ...(result.audit ? { auditHint: result.audit.actionId } : {}),
341
+ authoredRevisionIds: result.authoredRevisionIds,
334
342
  };
335
343
  },
336
344
  };
@@ -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
+ }