@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.
- package/README.md +964 -75
- package/package.json +1 -1
- package/src/api/public-types.ts +280 -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/policy.ts +31 -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/chrome-preset-model.ts +6 -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/core/state/editor-state.ts +49 -6
- package/src/io/export/serialize-footnotes.ts +6 -0
- package/src/io/export/serialize-headers-footers.ts +7 -0
- package/src/io/export/serialize-main-document.ts +20 -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 +49 -2
- package/src/io/ooxml/parse-headers-footers.ts +31 -0
- package/src/io/ooxml/parse-main-document.ts +148 -7
- 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 +278 -1
- package/src/runtime/layout/paginated-layout-engine.ts +181 -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/action-validation.ts +30 -4
- 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 +50 -3
- package/src/runtime/scopes/scope-kinds/paragraph.ts +170 -7
- package/src/runtime/scopes/semantic-scope-types.ts +27 -0
- package/src/runtime/surface-projection.ts +77 -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/session/import/loader-types.ts +18 -0
- package/src/session/import/loader.ts +2 -0
- 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 +50 -4
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +6 -0
- 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 +114 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +12 -4
- 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 +54 -34
- 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 +8 -1
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +11 -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 +139 -10
- 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/theme/editor-theme.css +15 -16
- 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
|
+
}
|
package/src/api/v3/ai/policy.ts
CHANGED
|
@@ -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
|
+
}
|