@beyondwork/docx-react-component 1.0.58 → 1.0.60
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 +2 -2
- package/package.json +2 -1
- package/src/api/awareness-identity-types.ts +4 -2
- package/src/api/comment-negotiation-types.ts +4 -1
- package/src/api/external-custody-types.ts +16 -0
- package/src/api/internal/build-ref-projections.ts +108 -0
- package/src/api/package-version.ts +1 -1
- package/src/api/participants-types.ts +11 -1
- package/src/api/public-types.ts +980 -10
- package/src/api/scope-metadata-resolver-types.ts +6 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +225 -16
- package/src/core/commands/legacy-form-field-commands.ts +181 -0
- package/src/core/commands/table-structure-commands.ts +149 -31
- package/src/core/selection/mapping.ts +20 -0
- package/src/core/state/editor-state.ts +4 -1
- package/src/index.ts +28 -0
- package/src/io/docx-session.ts +22 -3
- package/src/io/export/export-session.ts +11 -7
- package/src/io/export/ooxml-namespaces.ts +47 -0
- package/src/io/export/reattach-preserved-parts.ts +4 -16
- package/src/io/export/serialize-comments.ts +3 -131
- package/src/io/export/serialize-ffdata.ts +89 -0
- package/src/io/export/serialize-headers-footers.ts +5 -0
- package/src/io/export/serialize-main-document.ts +224 -34
- package/src/io/export/serialize-numbering.ts +22 -2
- package/src/io/export/serialize-revisions.ts +99 -0
- package/src/io/export/serialize-tables.ts +9 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/export/table-properties-xml.ts +14 -0
- package/src/io/load-scheduler.ts +70 -28
- package/src/io/normalize/normalize-text.ts +13 -0
- package/src/io/ooxml/_mini-xml.ts +198 -0
- package/src/io/ooxml/canonicalize-payload.ts +1 -4
- package/src/io/ooxml/chart/chart-style-table.ts +4 -3
- package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
- package/src/io/ooxml/chart/parse-series.ts +2 -1
- package/src/io/ooxml/chart/resolve-color.ts +2 -2
- package/src/io/ooxml/chart/types.ts +6 -434
- package/src/io/ooxml/comment-presentation-payload.ts +6 -5
- package/src/io/ooxml/highlight-colors.ts +8 -5
- package/src/io/ooxml/parse-anchor.ts +68 -53
- package/src/io/ooxml/parse-comments.ts +14 -142
- package/src/io/ooxml/parse-complex-content.ts +3 -106
- package/src/io/ooxml/parse-drawing.ts +100 -195
- package/src/io/ooxml/parse-ffdata.ts +93 -0
- package/src/io/ooxml/parse-fields.ts +7 -146
- package/src/io/ooxml/parse-fill.ts +88 -8
- package/src/io/ooxml/parse-font-table.ts +5 -105
- package/src/io/ooxml/parse-footnotes.ts +28 -152
- package/src/io/ooxml/parse-headers-footers.ts +106 -212
- package/src/io/ooxml/parse-inline-media.ts +3 -200
- package/src/io/ooxml/parse-main-document.ts +180 -217
- package/src/io/ooxml/parse-numbering.ts +154 -335
- package/src/io/ooxml/parse-object.ts +147 -0
- package/src/io/ooxml/parse-ole-relationship.ts +82 -0
- package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
- package/src/io/ooxml/parse-picture-sdt.ts +85 -0
- package/src/io/ooxml/parse-picture.ts +72 -42
- package/src/io/ooxml/parse-revisions.ts +285 -51
- package/src/io/ooxml/parse-settings.ts +6 -99
- package/src/io/ooxml/parse-shapes.ts +25 -140
- package/src/io/ooxml/parse-styles.ts +3 -218
- package/src/io/ooxml/parse-tables.ts +76 -256
- package/src/io/ooxml/parse-theme.ts +1 -4
- package/src/io/ooxml/property-grab-bag.ts +5 -47
- package/src/io/ooxml/workflow-payload.ts +6 -1
- package/src/io/ooxml/xml-element-serialize.ts +32 -0
- package/src/io/ooxml/xml-parser.ts +183 -0
- package/src/legal/bookmarks.ts +1 -1
- package/src/legal/cross-references.ts +1 -1
- package/src/legal/defined-terms.ts +1 -1
- package/src/legal/{_document-root.ts → document-root.ts} +8 -0
- package/src/legal/signature-blocks.ts +1 -1
- package/src/model/canonical-document.ts +159 -6
- package/src/model/chart-types.ts +439 -0
- package/src/model/snapshot.ts +5 -1
- package/src/review/store/comment-remapping.ts +24 -11
- package/src/review/store/revision-actions.ts +482 -2
- package/src/review/store/revision-store.ts +15 -0
- package/src/review/store/revision-types.ts +76 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
- package/src/runtime/collab/runtime-collab-sync.ts +33 -0
- package/src/runtime/diagnostics/build-diagnostic.ts +153 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +230 -0
- package/src/runtime/document-runtime.ts +821 -54
- package/src/runtime/document-search.ts +115 -0
- package/src/runtime/edit-ops/index.ts +18 -2
- package/src/runtime/footnote-resolver.ts +130 -0
- package/src/runtime/layout/layout-engine-instance.ts +31 -4
- package/src/runtime/layout/layout-engine-version.ts +37 -1
- package/src/runtime/layout/page-graph.ts +14 -1
- package/src/runtime/layout/resolved-formatting-state.ts +21 -0
- package/src/runtime/numbering-prefix.ts +17 -0
- package/src/runtime/query-scopes.ts +108 -10
- package/src/runtime/resolved-numbering-geometry.ts +37 -6
- package/src/runtime/revision-runtime.ts +27 -1
- package/src/runtime/selection/post-edit-validator.ts +60 -6
- package/src/runtime/structure-ops/index.ts +20 -4
- package/src/runtime/surface-projection.ts +290 -21
- package/src/runtime/table-schema.ts +6 -0
- package/src/runtime/theme-color-resolver.ts +2 -2
- package/src/runtime/units.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +4 -0
- package/src/ui/WordReviewEditor.tsx +187 -43
- package/src/ui/editor-runtime-boundary.ts +10 -0
- package/src/ui/editor-shell-view.tsx +4 -1
- package/src/ui/headless/chrome-registry.ts +53 -0
- package/src/ui/headless/selection-tool-resolver.ts +11 -1
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
- package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
- package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +0 -9
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +1 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +87 -25
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +9 -0
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
- package/src/ui-tailwind/index.ts +9 -0
- package/src/ui-tailwind/page-chrome-model.ts +77 -5
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
- package/src/ui-tailwind/theme/tokens.ts +14 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +29 -87
- package/src/validation/diagnostics.ts +1 -0
|
@@ -88,6 +88,11 @@ import type {
|
|
|
88
88
|
WorkflowScopeSnapshot,
|
|
89
89
|
ScopeQueryFilter,
|
|
90
90
|
ScopeQueryResult,
|
|
91
|
+
ScopeVisibility,
|
|
92
|
+
ScopeChromeVisibilityState,
|
|
93
|
+
SearchOptions,
|
|
94
|
+
TextStyleFilter,
|
|
95
|
+
WorkflowScopeMode,
|
|
91
96
|
WorkspaceMode,
|
|
92
97
|
WordReviewEditorEvent,
|
|
93
98
|
ZoomLevel,
|
|
@@ -138,12 +143,14 @@ import {
|
|
|
138
143
|
} from "../review/store/revision-store.ts";
|
|
139
144
|
import { createSuggestionsSnapshot } from "./suggestions-snapshot.ts";
|
|
140
145
|
import { validateSelectionAgainstDocument } from "./selection/post-edit-validator.ts";
|
|
146
|
+
import { createSurfaceNodeSelectionProbe } from "./selection/post-edit-validator.ts";
|
|
141
147
|
import {
|
|
142
148
|
collectScopeLocations,
|
|
143
149
|
findAllScopesAt,
|
|
144
150
|
findScopesIntersecting,
|
|
145
151
|
resolveScope,
|
|
146
152
|
} from "./scope-resolver.ts";
|
|
153
|
+
import { buildDiagnosticFromLegacyWarningCode } from "./diagnostics/build-diagnostic.ts";
|
|
147
154
|
import {
|
|
148
155
|
projectScopeQueryResults,
|
|
149
156
|
queryScopes as runQueryScopes,
|
|
@@ -155,6 +162,10 @@ import {
|
|
|
155
162
|
import { buildCompatibilityReport } from "../validation/compatibility-engine.ts";
|
|
156
163
|
import { mergeCompatibilityReports } from "../validation/compatibility-report.ts";
|
|
157
164
|
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
165
|
+
import {
|
|
166
|
+
findTextMatches,
|
|
167
|
+
findTextWithStyleMatches,
|
|
168
|
+
} from "./document-search.ts";
|
|
158
169
|
import {
|
|
159
170
|
collectWorkflowMarkupSnapshot,
|
|
160
171
|
deriveWorkflowCandidateRangesFromMarkup,
|
|
@@ -420,6 +431,15 @@ export interface DocumentRuntime {
|
|
|
420
431
|
*/
|
|
421
432
|
applyRemoteCommandBatch(envelopes: ReadonlyArray<RemoteCommandEnvelope>): void;
|
|
422
433
|
emitBlockedCommand(command: string, reasons: WorkflowBlockedCommandReason[]): void;
|
|
434
|
+
/**
|
|
435
|
+
* Emit a fire-and-forget `warning_added` event + `onWarning` callback for
|
|
436
|
+
* a host-layer no-op (e.g. `applyRuntimeDeleteComment` invoked with an
|
|
437
|
+
* unknown id). The warning is NOT persisted to `state.warnings` or the
|
|
438
|
+
* compatibility report — same semantics as a reducer-emitted
|
|
439
|
+
* `effects.transientWarnings` entry, exposed here for host-side paths
|
|
440
|
+
* that never reach a reducer.
|
|
441
|
+
*/
|
|
442
|
+
emitTransientWarning(warning: InternalEditorWarning): void;
|
|
423
443
|
undo(): void;
|
|
424
444
|
redo(): void;
|
|
425
445
|
focus(): void;
|
|
@@ -430,11 +450,33 @@ export interface DocumentRuntime {
|
|
|
430
450
|
openComment(commentId: string): void;
|
|
431
451
|
resolveComment(commentId: string): void;
|
|
432
452
|
reopenComment(commentId: string): void;
|
|
433
|
-
|
|
453
|
+
/**
|
|
454
|
+
* Append a reply entry to an existing thread. Returns the minted
|
|
455
|
+
* `{ commentId, entryId }` on success. Returns `null` when the thread
|
|
456
|
+
* is unknown, resolved, or detached — in that case a
|
|
457
|
+
* `review_target_not_found` warning fires on `onWarning` / `warning_added`
|
|
458
|
+
* with structured `details.op = "addCommentReply"` and `details.reason`
|
|
459
|
+
* ∈ `{ "comment_unknown", "comment_status" }`.
|
|
460
|
+
*/
|
|
461
|
+
addCommentReply(
|
|
462
|
+
commentId: string,
|
|
463
|
+
body: string,
|
|
464
|
+
authorId?: string,
|
|
465
|
+
): AddCommentReplyResult | null;
|
|
434
466
|
editCommentBody(commentId: string, body: string): void;
|
|
435
467
|
addScope(params: AddScopeParams): AddScopeResult;
|
|
436
468
|
getScope(scopeId: string): WorkflowScope | null;
|
|
437
469
|
removeScope(scopeId: string): void;
|
|
470
|
+
/** §C8 — Add a scope with visibility: "invisible" atomically. */
|
|
471
|
+
addInvisibleScope(params: Omit<AddScopeParams, "mode"> & { mode?: WorkflowScopeMode }): AddScopeResult;
|
|
472
|
+
/** §C8 — Set a scope's visibility (collab-replicated). */
|
|
473
|
+
setScopeVisibility(scopeId: string, visibility: ScopeVisibility): void;
|
|
474
|
+
/** §C8 — Get a scope's current visibility (absent = "visible"). */
|
|
475
|
+
getScopeVisibility(scopeId: string): ScopeVisibility;
|
|
476
|
+
/** §C7 — Set local chrome visibility state (never collab-replicated). */
|
|
477
|
+
setScopeChromeVisibility(state: ScopeChromeVisibilityState): void;
|
|
478
|
+
/** §C7 — Get local chrome visibility state (default: { mode: "all" }). */
|
|
479
|
+
getScopeChromeVisibility(): ScopeChromeVisibilityState;
|
|
438
480
|
acceptChange(changeId: string): void;
|
|
439
481
|
rejectChange(changeId: string): void;
|
|
440
482
|
acceptAllChanges(): void;
|
|
@@ -520,6 +562,23 @@ export interface DocumentRuntime {
|
|
|
520
562
|
* `WordReviewEditorRef.queryScopes` for contract.
|
|
521
563
|
*/
|
|
522
564
|
queryScopes(filter?: ScopeQueryFilter): ScopeQueryResult[];
|
|
565
|
+
/** Phase C §C1 — live subscription; returns unsubscribe fn. */
|
|
566
|
+
subscribeToScopeQuery(
|
|
567
|
+
filter: ScopeQueryFilter,
|
|
568
|
+
callback: (results: ScopeQueryResult[]) => void,
|
|
569
|
+
): () => void;
|
|
570
|
+
/** Phase C §C4 — text search + style filter. */
|
|
571
|
+
findAllText(query: string, options?: SearchOptions): EditorAnchorProjection[];
|
|
572
|
+
findTextWithStyle(
|
|
573
|
+
query: string,
|
|
574
|
+
filter: TextStyleFilter,
|
|
575
|
+
options?: SearchOptions,
|
|
576
|
+
): EditorAnchorProjection[];
|
|
577
|
+
selectTextWithStyle(
|
|
578
|
+
query: string,
|
|
579
|
+
filter: TextStyleFilter,
|
|
580
|
+
options?: SearchOptions,
|
|
581
|
+
): number;
|
|
523
582
|
/**
|
|
524
583
|
* Phase C §C2 — geometric scope queries. See `WordReviewEditorRef`
|
|
525
584
|
* for contract. Non-range anchors yield `[]`.
|
|
@@ -814,7 +873,10 @@ export function createDocumentRuntime(
|
|
|
814
873
|
options.initialSessionState?.workflowMetadata?.entries
|
|
815
874
|
?? options.initialSnapshot?.workflowMetadata?.entries
|
|
816
875
|
?? [];
|
|
876
|
+
let markerBackedScopeIds = new Set<string>();
|
|
817
877
|
let hostAnnotationOverlay: HostAnnotationOverlay | null = null;
|
|
878
|
+
// §C7 — local view-state for scope chrome visibility; never collab-replicated.
|
|
879
|
+
let scopeChromeVisibilityState: ScopeChromeVisibilityState = { mode: "all" };
|
|
818
880
|
// P13 Slice B: shared workflow state from the collab Y.Map "workflow".
|
|
819
881
|
// Set via setSharedWorkflowState(); observed by evaluateWorkflowBlockedReasons.
|
|
820
882
|
let sharedWorkflowState: SharedWorkflowState | null = null;
|
|
@@ -849,6 +911,32 @@ export function createDocumentRuntime(
|
|
|
849
911
|
storySelections.set(storyTargetKey(MAIN_STORY_TARGET), state.selection);
|
|
850
912
|
lastHeadingFingerprint = computeHeadingFingerprint(state.document);
|
|
851
913
|
|
|
914
|
+
function syncMarkerBackedScopeIds(
|
|
915
|
+
document: CanonicalDocumentEnvelope,
|
|
916
|
+
overlay: WorkflowOverlay | null,
|
|
917
|
+
): void {
|
|
918
|
+
const presentScopeIds = new Set(collectScopeLocations(document).keys());
|
|
919
|
+
if (!overlay) {
|
|
920
|
+
markerBackedScopeIds = presentScopeIds;
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const overlayScopeIds = new Set(overlay.scopes.map((scope) => scope.scopeId));
|
|
924
|
+
const next = new Set<string>();
|
|
925
|
+
for (const scopeId of markerBackedScopeIds) {
|
|
926
|
+
if (overlayScopeIds.has(scopeId)) {
|
|
927
|
+
next.add(scopeId);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
for (const scopeId of presentScopeIds) {
|
|
931
|
+
if (overlayScopeIds.has(scopeId)) {
|
|
932
|
+
next.add(scopeId);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
markerBackedScopeIds = next;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
syncMarkerBackedScopeIds(state.document, workflowOverlay);
|
|
939
|
+
|
|
852
940
|
// Runtime-owned paginated layout engine (Phase 1+ of the layout facet work).
|
|
853
941
|
// The engine caches graph + resolved-formatting + fragment mapper keyed on
|
|
854
942
|
// (content, styles, subParts). It is the single internal source of truth
|
|
@@ -957,6 +1045,10 @@ export function createDocumentRuntime(
|
|
|
957
1045
|
let cachedCompatibility:
|
|
958
1046
|
| {
|
|
959
1047
|
revisionToken: string;
|
|
1048
|
+
/** Block count at the time of the last full rebuild. O(1) proxy for structural stability. */
|
|
1049
|
+
blockCount: number;
|
|
1050
|
+
/** Warning count at time of rebuild — warnings array gets remapped on every edit. */
|
|
1051
|
+
warningCount: number;
|
|
960
1052
|
warnings: EditorState["warnings"];
|
|
961
1053
|
fatalError: EditorState["fatalError"];
|
|
962
1054
|
report: RuntimeRenderSnapshot["compatibility"];
|
|
@@ -1107,6 +1199,14 @@ export function createDocumentRuntime(
|
|
|
1107
1199
|
snapshot: WorkflowMarkupSnapshot;
|
|
1108
1200
|
}
|
|
1109
1201
|
| undefined;
|
|
1202
|
+
// Keyed on block count + subParts identity (not revisionToken) — fields only
|
|
1203
|
+
// change when blocks are inserted/deleted or subParts (headers/footers) change,
|
|
1204
|
+
// NOT on text-only edits. Cleared explicitly by updateFields() for field-refresh.
|
|
1205
|
+
let cachedFieldSnapshotEntry: {
|
|
1206
|
+
blockCount: number;
|
|
1207
|
+
subParts: CanonicalDocumentEnvelope["subParts"];
|
|
1208
|
+
snapshot: FieldSnapshot;
|
|
1209
|
+
} | null = null;
|
|
1110
1210
|
const cachedContextAnalyticsSnapshots = new Map<
|
|
1111
1211
|
string,
|
|
1112
1212
|
{
|
|
@@ -1152,16 +1252,53 @@ export function createDocumentRuntime(
|
|
|
1152
1252
|
return snapshot;
|
|
1153
1253
|
}
|
|
1154
1254
|
|
|
1255
|
+
function getCachedFieldSnapshot(document: CanonicalDocumentEnvelope): FieldSnapshot {
|
|
1256
|
+
const blockCount = document.content.children.length;
|
|
1257
|
+
if (
|
|
1258
|
+
cachedFieldSnapshotEntry &&
|
|
1259
|
+
cachedFieldSnapshotEntry.blockCount === blockCount &&
|
|
1260
|
+
cachedFieldSnapshotEntry.subParts === document.subParts
|
|
1261
|
+
) {
|
|
1262
|
+
return cachedFieldSnapshotEntry.snapshot;
|
|
1263
|
+
}
|
|
1264
|
+
const snapshot = buildFieldSnapshot(document);
|
|
1265
|
+
cachedFieldSnapshotEntry = { blockCount, subParts: document.subParts, snapshot };
|
|
1266
|
+
return snapshot;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1155
1269
|
function getCachedCompatibilityReport(
|
|
1156
1270
|
nextState: EditorState,
|
|
1157
1271
|
): RuntimeRenderSnapshot["compatibility"] {
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1272
|
+
const blockCount = nextState.document.content.children.length;
|
|
1273
|
+
const warningCount = nextState.warnings?.length ?? 0;
|
|
1274
|
+
if (cachedCompatibility) {
|
|
1275
|
+
// Fast path 1: same revisionToken (selection move, surface-only refresh).
|
|
1276
|
+
if (
|
|
1277
|
+
cachedCompatibility.revisionToken === nextState.revisionToken &&
|
|
1278
|
+
cachedCompatibility.warnings === nextState.warnings &&
|
|
1279
|
+
cachedCompatibility.fatalError === nextState.fatalError
|
|
1280
|
+
) {
|
|
1281
|
+
return cachedCompatibility.report;
|
|
1282
|
+
}
|
|
1283
|
+
// Fast path 2: revisionToken changed but block structure and warning
|
|
1284
|
+
// count are stable (text-only edit). buildCompatibilityReport reads
|
|
1285
|
+
// block types, review counts, preservation, and warnings — NOT run text.
|
|
1286
|
+
// Block count + warning count + fatalError identity is an O(1) proxy:
|
|
1287
|
+
// if these are stable, the compatibility report output is unchanged.
|
|
1288
|
+
// `warnings` array gets remapped to a new reference on every commit via
|
|
1289
|
+
// remapReviewStateAfterContentChange, so we can't use reference equality.
|
|
1290
|
+
if (
|
|
1291
|
+
cachedCompatibility.blockCount === blockCount &&
|
|
1292
|
+
cachedCompatibility.warningCount === warningCount &&
|
|
1293
|
+
cachedCompatibility.fatalError === nextState.fatalError
|
|
1294
|
+
) {
|
|
1295
|
+
cachedCompatibility = {
|
|
1296
|
+
...cachedCompatibility,
|
|
1297
|
+
revisionToken: nextState.revisionToken,
|
|
1298
|
+
warnings: nextState.warnings,
|
|
1299
|
+
};
|
|
1300
|
+
return cachedCompatibility.report;
|
|
1301
|
+
}
|
|
1165
1302
|
}
|
|
1166
1303
|
|
|
1167
1304
|
const derived = createDerivedCompatibility(nextState);
|
|
@@ -1178,6 +1315,8 @@ export function createDocumentRuntime(
|
|
|
1178
1315
|
};
|
|
1179
1316
|
cachedCompatibility = {
|
|
1180
1317
|
revisionToken: nextState.revisionToken,
|
|
1318
|
+
blockCount,
|
|
1319
|
+
warningCount,
|
|
1181
1320
|
warnings: nextState.warnings,
|
|
1182
1321
|
fatalError: nextState.fatalError,
|
|
1183
1322
|
report,
|
|
@@ -1424,8 +1563,13 @@ export function createDocumentRuntime(
|
|
|
1424
1563
|
if (normalizedWorkflowOverlay) {
|
|
1425
1564
|
const matchingScope = getMatchingWorkflowScope(selection);
|
|
1426
1565
|
const activeScopes = getEffectiveWorkflowScopes(normalizedWorkflowOverlay);
|
|
1566
|
+
// §C8: invisible non-view scopes are transparent to the interaction guard.
|
|
1567
|
+
// Don't count them toward the "outside_workflow_scope" threshold.
|
|
1568
|
+
const guardingScopes = activeScopes.filter(
|
|
1569
|
+
(s) => !(s.visibility === "invisible" && s.mode !== "view"),
|
|
1570
|
+
);
|
|
1427
1571
|
|
|
1428
|
-
if (!matchingScope &&
|
|
1572
|
+
if (!matchingScope && guardingScopes.length > 0) {
|
|
1429
1573
|
reasons.push({
|
|
1430
1574
|
code: "outside_workflow_scope",
|
|
1431
1575
|
message: "Selection is outside any active workflow scope.",
|
|
@@ -1456,24 +1600,60 @@ export function createDocumentRuntime(
|
|
|
1456
1600
|
return reasons;
|
|
1457
1601
|
}
|
|
1458
1602
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1603
|
+
// §C6 — most-restrictive-wins ordering for overlap layering.
|
|
1604
|
+
const MODE_RESTRICTIVENESS: Record<WorkflowScopeMode, number> = {
|
|
1605
|
+
edit: 0,
|
|
1606
|
+
suggest: 1,
|
|
1607
|
+
comment: 2,
|
|
1608
|
+
view: 3,
|
|
1609
|
+
};
|
|
1465
1610
|
|
|
1611
|
+
/**
|
|
1612
|
+
* §C6 — Collect all guard-eligible scopes that contain `selection`,
|
|
1613
|
+
* sorted outermost→innermost (startPos ASC, endPos DESC, scopeId ASC).
|
|
1614
|
+
* Excludes invisible non-view scopes per §C8.
|
|
1615
|
+
*/
|
|
1616
|
+
function buildMatchingScopeStack(
|
|
1617
|
+
selection: EditorState["selection"],
|
|
1618
|
+
): WorkflowOverlay["scopes"] {
|
|
1619
|
+
if (!workflowOverlay) return [];
|
|
1466
1620
|
const selectionBounds = {
|
|
1467
1621
|
from: Math.min(selection.anchor, selection.head),
|
|
1468
1622
|
to: Math.max(selection.anchor, selection.head),
|
|
1469
1623
|
};
|
|
1470
1624
|
const activeScopes = getEffectiveWorkflowScopes(workflowOverlay);
|
|
1471
|
-
|
|
1625
|
+
const matching = activeScopes.filter((scope) => {
|
|
1626
|
+
// §C8
|
|
1627
|
+
if (scope.visibility === "invisible" && scope.mode !== "view") return false;
|
|
1472
1628
|
if (scope.anchor.kind === "detached") return false;
|
|
1473
1629
|
const scopeFrom = scope.anchor.kind === "range" ? scope.anchor.from : scope.anchor.at;
|
|
1474
1630
|
const scopeTo = scope.anchor.kind === "range" ? scope.anchor.to : scope.anchor.at;
|
|
1475
1631
|
return selectionBounds.from >= scopeFrom && selectionBounds.to <= scopeTo;
|
|
1476
|
-
})
|
|
1632
|
+
});
|
|
1633
|
+
// Outermost first: startPos ASC, endPos DESC (wider span = outer), scopeId ASC tiebreak
|
|
1634
|
+
matching.sort((a, b) => {
|
|
1635
|
+
const aFrom = a.anchor.kind === "range" ? a.anchor.from : (a.anchor as { at: number }).at;
|
|
1636
|
+
const bFrom = b.anchor.kind === "range" ? b.anchor.from : (b.anchor as { at: number }).at;
|
|
1637
|
+
if (aFrom !== bFrom) return aFrom - bFrom;
|
|
1638
|
+
const aTo = a.anchor.kind === "range" ? a.anchor.to : (a.anchor as { at: number }).at;
|
|
1639
|
+
const bTo = b.anchor.kind === "range" ? b.anchor.to : (b.anchor as { at: number }).at;
|
|
1640
|
+
if (aTo !== bTo) return bTo - aTo; // wider first
|
|
1641
|
+
return a.scopeId < b.scopeId ? -1 : a.scopeId > b.scopeId ? 1 : 0;
|
|
1642
|
+
});
|
|
1643
|
+
return matching;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function getMatchingWorkflowScope(
|
|
1647
|
+
selection: EditorState["selection"],
|
|
1648
|
+
): WorkflowOverlay["scopes"][number] | null {
|
|
1649
|
+
const stack = buildMatchingScopeStack(selection);
|
|
1650
|
+
if (stack.length === 0) return null;
|
|
1651
|
+
// §C6 — most-restrictive scope wins across all overlapping scopes.
|
|
1652
|
+
return stack.reduce((best, scope) =>
|
|
1653
|
+
(MODE_RESTRICTIVENESS[scope.mode] ?? 0) > (MODE_RESTRICTIVENESS[best.mode] ?? 0)
|
|
1654
|
+
? scope
|
|
1655
|
+
: best,
|
|
1656
|
+
);
|
|
1477
1657
|
}
|
|
1478
1658
|
|
|
1479
1659
|
function getEffectiveDocumentMode(
|
|
@@ -1772,19 +1952,37 @@ export function createDocumentRuntime(
|
|
|
1772
1952
|
return scope;
|
|
1773
1953
|
}
|
|
1774
1954
|
const location = locations.get(scope.scopeId);
|
|
1955
|
+
const isMarkerBacked = markerBackedScopeIds.has(scope.scopeId);
|
|
1956
|
+
let nextAnchor: EditorAnchorProjection | null = null;
|
|
1775
1957
|
if (
|
|
1776
|
-
|
|
1777
|
-
location.startPos
|
|
1778
|
-
location.endPos
|
|
1958
|
+
location &&
|
|
1959
|
+
location.startPos !== undefined &&
|
|
1960
|
+
location.endPos !== undefined
|
|
1779
1961
|
) {
|
|
1962
|
+
nextAnchor = {
|
|
1963
|
+
kind: "range",
|
|
1964
|
+
from: Math.min(location.startPos, location.endPos),
|
|
1965
|
+
to: Math.max(location.startPos, location.endPos),
|
|
1966
|
+
assoc: { start: -1, end: 1 },
|
|
1967
|
+
};
|
|
1968
|
+
} else if (isMarkerBacked) {
|
|
1969
|
+
const lastKnownRange =
|
|
1970
|
+
scope.anchor.kind === "range"
|
|
1971
|
+
? { from: scope.anchor.from, to: scope.anchor.to }
|
|
1972
|
+
: scope.anchor.kind === "node"
|
|
1973
|
+
? { from: scope.anchor.at, to: scope.anchor.at }
|
|
1974
|
+
: scope.anchor.lastKnownRange;
|
|
1975
|
+
nextAnchor = {
|
|
1976
|
+
kind: "detached",
|
|
1977
|
+
reason:
|
|
1978
|
+
location && (location.startPos !== undefined || location.endPos !== undefined)
|
|
1979
|
+
? "deleted"
|
|
1980
|
+
: "invalidatedByStructureChange",
|
|
1981
|
+
lastKnownRange,
|
|
1982
|
+
};
|
|
1983
|
+
} else {
|
|
1780
1984
|
return scope;
|
|
1781
1985
|
}
|
|
1782
|
-
const nextAnchor: EditorAnchorProjection = {
|
|
1783
|
-
kind: "range",
|
|
1784
|
-
from: Math.min(location.startPos, location.endPos),
|
|
1785
|
-
to: Math.max(location.startPos, location.endPos),
|
|
1786
|
-
assoc: { start: -1, end: 1 },
|
|
1787
|
-
};
|
|
1788
1986
|
if (workflowAnchorsEqual(scope.anchor, nextAnchor)) {
|
|
1789
1987
|
return scope;
|
|
1790
1988
|
}
|
|
@@ -1814,6 +2012,161 @@ export function createDocumentRuntime(
|
|
|
1814
2012
|
return normalizeWorkflowOverlayForDocument(state.document, workflowOverlay);
|
|
1815
2013
|
}
|
|
1816
2014
|
|
|
2015
|
+
function buildWarningSignature(warning: InternalEditorWarning): string {
|
|
2016
|
+
return JSON.stringify({
|
|
2017
|
+
code: warning.code,
|
|
2018
|
+
severity: warning.severity,
|
|
2019
|
+
message: warning.message,
|
|
2020
|
+
source: warning.source,
|
|
2021
|
+
featureEntryId: warning.featureEntryId ?? null,
|
|
2022
|
+
details: warning.details ?? null,
|
|
2023
|
+
affectedAnchor: warning.affectedAnchor ?? null,
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
function mergeDetachedWorkflowScopeWarnings(
|
|
2028
|
+
overlay: WorkflowOverlay | null,
|
|
2029
|
+
existingWarnings: readonly InternalEditorWarning[],
|
|
2030
|
+
): {
|
|
2031
|
+
nextWarnings: InternalEditorWarning[];
|
|
2032
|
+
added: InternalEditorWarning[];
|
|
2033
|
+
cleared: Array<{ warningId: string; code: InternalEditorWarning["code"] }>;
|
|
2034
|
+
} {
|
|
2035
|
+
const detachedScopesById = new Map<string, WorkflowScope>();
|
|
2036
|
+
for (const scope of overlay?.scopes ?? []) {
|
|
2037
|
+
if (scope.anchor.kind === "detached") {
|
|
2038
|
+
detachedScopesById.set(scope.scopeId, scope);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
const retainedWarnings = existingWarnings.filter(
|
|
2043
|
+
(warning) => warning.code !== "workflow_scope_invalidated",
|
|
2044
|
+
);
|
|
2045
|
+
const existingDetachedWarnings = existingWarnings.filter(
|
|
2046
|
+
(warning) => warning.code === "workflow_scope_invalidated",
|
|
2047
|
+
);
|
|
2048
|
+
const existingById = new Map(
|
|
2049
|
+
existingDetachedWarnings.map((warning) => [warning.warningId, warning] as const),
|
|
2050
|
+
);
|
|
2051
|
+
const desiredById = new Map(
|
|
2052
|
+
[...detachedScopesById.values()].map((scope) => {
|
|
2053
|
+
const warning = createInvalidatedWorkflowScopeWarning(scope);
|
|
2054
|
+
return [warning.warningId, warning] as const;
|
|
2055
|
+
}),
|
|
2056
|
+
);
|
|
2057
|
+
|
|
2058
|
+
const added: InternalEditorWarning[] = [];
|
|
2059
|
+
const cleared: Array<{ warningId: string; code: InternalEditorWarning["code"] }> = [];
|
|
2060
|
+
|
|
2061
|
+
for (const [warningId, existingWarning] of existingById) {
|
|
2062
|
+
const desiredWarning = desiredById.get(warningId);
|
|
2063
|
+
if (!desiredWarning) {
|
|
2064
|
+
cleared.push({ warningId, code: existingWarning.code });
|
|
2065
|
+
continue;
|
|
2066
|
+
}
|
|
2067
|
+
if (buildWarningSignature(existingWarning) !== buildWarningSignature(desiredWarning)) {
|
|
2068
|
+
cleared.push({ warningId, code: existingWarning.code });
|
|
2069
|
+
added.push(desiredWarning);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
for (const [warningId, desiredWarning] of desiredById) {
|
|
2074
|
+
if (!existingById.has(warningId)) {
|
|
2075
|
+
added.push(desiredWarning);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
return {
|
|
2080
|
+
nextWarnings: [...retainedWarnings, ...desiredById.values()],
|
|
2081
|
+
added,
|
|
2082
|
+
cleared,
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
function createInvalidatedWorkflowScopeWarning(
|
|
2087
|
+
scope: WorkflowScope,
|
|
2088
|
+
): InternalEditorWarning {
|
|
2089
|
+
const anchor = scope.anchor.kind === "detached" ? scope.anchor : null;
|
|
2090
|
+
const subject = scope.label
|
|
2091
|
+
? `Workflow scope "${scope.label}" (${scope.scopeId})`
|
|
2092
|
+
: `Workflow scope ${scope.scopeId}`;
|
|
2093
|
+
const reasonPhrase =
|
|
2094
|
+
anchor?.reason === "deleted"
|
|
2095
|
+
? "its anchored text was deleted"
|
|
2096
|
+
: anchor?.reason === "invalidatedByStructureChange"
|
|
2097
|
+
? "document structure changed around it"
|
|
2098
|
+
: "its anchor could not be resolved unambiguously";
|
|
2099
|
+
const modePhrase =
|
|
2100
|
+
scope.mode === "view"
|
|
2101
|
+
? "read-only enforcement"
|
|
2102
|
+
: `${scope.mode} enforcement`;
|
|
2103
|
+
|
|
2104
|
+
return {
|
|
2105
|
+
warningId: `warning:workflow-scope-invalidated:${scope.scopeId}`,
|
|
2106
|
+
code: "workflow_scope_invalidated",
|
|
2107
|
+
severity: "warning",
|
|
2108
|
+
message: `${subject} was invalidated because ${reasonPhrase}. Reapply the scope before relying on ${modePhrase}.`,
|
|
2109
|
+
source: "runtime",
|
|
2110
|
+
affectedAnchor: anchor ? toInternalAnchorProjection(anchor) : undefined,
|
|
2111
|
+
diagnostic: buildDiagnosticFromLegacyWarningCode("workflow_scope_invalidated", {
|
|
2112
|
+
diagnosticId: `warning-diag:workflow-scope-invalidated:${scope.scopeId}`,
|
|
2113
|
+
technical: {
|
|
2114
|
+
message: `${subject} lost its trusted anchor and is now detached.`,
|
|
2115
|
+
source: "runtime",
|
|
2116
|
+
},
|
|
2117
|
+
details: {
|
|
2118
|
+
scopeId: scope.scopeId,
|
|
2119
|
+
label: scope.label,
|
|
2120
|
+
mode: scope.mode,
|
|
2121
|
+
reason: anchor?.reason,
|
|
2122
|
+
lastKnownRange: anchor?.lastKnownRange,
|
|
2123
|
+
storyTarget: scope.storyTarget,
|
|
2124
|
+
reapplySuggested: true,
|
|
2125
|
+
},
|
|
2126
|
+
affectedAnchor: anchor ? scope.anchor : undefined,
|
|
2127
|
+
llmMetadata: {
|
|
2128
|
+
userSummary: `${subject} is no longer attached to trusted document content. Reapply the scope before relying on it.`,
|
|
2129
|
+
remediation: {
|
|
2130
|
+
kind: "fallback",
|
|
2131
|
+
suggestion:
|
|
2132
|
+
"Locate the intended text using warning.details.scopeId and warning.details.lastKnownRange, then call addScope again for the repaired range.",
|
|
2133
|
+
},
|
|
2134
|
+
recoveryClass: "requires-input",
|
|
2135
|
+
echoedInput: {
|
|
2136
|
+
scopeId: scope.scopeId,
|
|
2137
|
+
lastKnownRange: anchor?.lastKnownRange,
|
|
2138
|
+
},
|
|
2139
|
+
},
|
|
2140
|
+
}),
|
|
2141
|
+
details: {
|
|
2142
|
+
scopeId: scope.scopeId,
|
|
2143
|
+
label: scope.label,
|
|
2144
|
+
mode: scope.mode,
|
|
2145
|
+
reason: anchor?.reason,
|
|
2146
|
+
lastKnownRange: anchor?.lastKnownRange,
|
|
2147
|
+
storyTarget: scope.storyTarget,
|
|
2148
|
+
reapplySuggested: true,
|
|
2149
|
+
actionabilityNote:
|
|
2150
|
+
"Resolve the intended text again, then reapply the scope; the previous anchor is no longer trusted.",
|
|
2151
|
+
},
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
function syncDetachedWorkflowScopeWarningsInState(): {
|
|
2156
|
+
added: InternalEditorWarning[];
|
|
2157
|
+
cleared: Array<{ warningId: string; code: InternalEditorWarning["code"] }>;
|
|
2158
|
+
} {
|
|
2159
|
+
const { nextWarnings, added, cleared } = mergeDetachedWorkflowScopeWarnings(
|
|
2160
|
+
getNormalizedWorkflowOverlay(),
|
|
2161
|
+
state.warnings,
|
|
2162
|
+
);
|
|
2163
|
+
if (added.length === 0 && cleared.length === 0) {
|
|
2164
|
+
return { added, cleared };
|
|
2165
|
+
}
|
|
2166
|
+
state = { ...state, warnings: nextWarnings };
|
|
2167
|
+
return { added, cleared };
|
|
2168
|
+
}
|
|
2169
|
+
|
|
1817
2170
|
function deriveWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null {
|
|
1818
2171
|
const normalizedWorkflowOverlay = getNormalizedWorkflowOverlay();
|
|
1819
2172
|
if (!normalizedWorkflowOverlay) return null;
|
|
@@ -1892,6 +2245,7 @@ export function createDocumentRuntime(
|
|
|
1892
2245
|
|
|
1893
2246
|
const blockedReasons = evaluateWorkflowBlockedReasons(state.selection);
|
|
1894
2247
|
const matchingScope = getMatchingWorkflowScope(state.selection);
|
|
2248
|
+
const scopeStack = buildMatchingScopeStack(state.selection);
|
|
1895
2249
|
const primaryBlockedReason = blockedReasons[0];
|
|
1896
2250
|
const effectiveMode = primaryBlockedReason
|
|
1897
2251
|
? (
|
|
@@ -1904,10 +2258,19 @@ export function createDocumentRuntime(
|
|
|
1904
2258
|
: getEffectiveDocumentMode(state.selection) === "suggesting"
|
|
1905
2259
|
? "suggest"
|
|
1906
2260
|
: matchingScope?.mode ?? "edit";
|
|
2261
|
+
const matchedScopeStack: InteractionGuardSnapshot["matchedScopeStack"] =
|
|
2262
|
+
scopeStack.length > 0
|
|
2263
|
+
? scopeStack.map((s) => ({
|
|
2264
|
+
scopeId: s.scopeId,
|
|
2265
|
+
mode: s.mode,
|
|
2266
|
+
visibility: s.visibility ?? "visible",
|
|
2267
|
+
}))
|
|
2268
|
+
: undefined;
|
|
1907
2269
|
const snapshot: InteractionGuardSnapshot = {
|
|
1908
2270
|
effectiveMode,
|
|
1909
2271
|
...(matchingScope?.scopeId ? { matchedScopeId: matchingScope.scopeId } : {}),
|
|
1910
2272
|
...(matchingScope?.mode ? { matchedScopeMode: matchingScope.mode } : {}),
|
|
2273
|
+
...(matchedScopeStack ? { matchedScopeStack } : {}),
|
|
1911
2274
|
targetAccess:
|
|
1912
2275
|
effectiveMode === "edit"
|
|
1913
2276
|
? "direct-edit"
|
|
@@ -1992,7 +2355,7 @@ export function createDocumentRuntime(
|
|
|
1992
2355
|
|
|
1993
2356
|
const snapshot = collectWorkflowMarkupSnapshot({
|
|
1994
2357
|
renderSnapshot: cachedRenderSnapshot,
|
|
1995
|
-
fieldSnapshot:
|
|
2358
|
+
fieldSnapshot: getCachedFieldSnapshot(state.document),
|
|
1996
2359
|
protectionSnapshot,
|
|
1997
2360
|
preservation: state.document.preservation,
|
|
1998
2361
|
workflowMetadataSnapshot: deriveWorkflowMetadataSnapshot(),
|
|
@@ -2297,6 +2660,7 @@ export function createDocumentRuntime(
|
|
|
2297
2660
|
cachedWorkflowScopeSnapshot = undefined;
|
|
2298
2661
|
cachedNormalizedWorkflowOverlay = undefined;
|
|
2299
2662
|
cachedWorkflowMarkupSnapshot = undefined;
|
|
2663
|
+
cachedFieldSnapshotEntry = null;
|
|
2300
2664
|
cachedContextAnalyticsSnapshots.clear();
|
|
2301
2665
|
lastEmittedContextAnalyticsSnapshots = undefined;
|
|
2302
2666
|
}
|
|
@@ -2313,8 +2677,11 @@ export function createDocumentRuntime(
|
|
|
2313
2677
|
document,
|
|
2314
2678
|
};
|
|
2315
2679
|
if (previousDocument.subParts !== document.subParts) {
|
|
2316
|
-
|
|
2317
|
-
|
|
2680
|
+
const fontInput = collectFontLoaderInput(document);
|
|
2681
|
+
if (!fontFamiliesEqual(fontInput.families, collectFontLoaderInput(previousDocument).families)) {
|
|
2682
|
+
fontLoader.refresh(fontInput);
|
|
2683
|
+
layoutEngine.invalidateMeasurementCache();
|
|
2684
|
+
}
|
|
2318
2685
|
}
|
|
2319
2686
|
invalidateDerivedRuntimeCaches();
|
|
2320
2687
|
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
@@ -2363,6 +2730,7 @@ export function createDocumentRuntime(
|
|
|
2363
2730
|
return snapshot;
|
|
2364
2731
|
}
|
|
2365
2732
|
|
|
2733
|
+
syncDetachedWorkflowScopeWarningsInState();
|
|
2366
2734
|
let cachedRenderSnapshot = refreshRenderSnapshot();
|
|
2367
2735
|
|
|
2368
2736
|
emit({
|
|
@@ -2473,6 +2841,15 @@ export function createDocumentRuntime(
|
|
|
2473
2841
|
reasons,
|
|
2474
2842
|
});
|
|
2475
2843
|
},
|
|
2844
|
+
emitTransientWarning(warning) {
|
|
2845
|
+
const publicWarning = toPublicWarning(warning);
|
|
2846
|
+
emit({
|
|
2847
|
+
type: "warning_added",
|
|
2848
|
+
documentId: state.documentId,
|
|
2849
|
+
warning: publicWarning,
|
|
2850
|
+
});
|
|
2851
|
+
options.onWarning?.(publicWarning);
|
|
2852
|
+
},
|
|
2476
2853
|
dispatch(command) {
|
|
2477
2854
|
const commandSelection = getCommandSelection(command, state.selection);
|
|
2478
2855
|
if (isMutationCommand(command)) {
|
|
@@ -3083,7 +3460,7 @@ export function createDocumentRuntime(
|
|
|
3083
3460
|
});
|
|
3084
3461
|
},
|
|
3085
3462
|
addCommentReply(commentId, body, authorId) {
|
|
3086
|
-
const
|
|
3463
|
+
const priorCount =
|
|
3087
3464
|
state.document.review.comments[commentId]?.entries?.length ?? 0;
|
|
3088
3465
|
this.dispatch({
|
|
3089
3466
|
type: "comment.add-reply",
|
|
@@ -3092,8 +3469,16 @@ export function createDocumentRuntime(
|
|
|
3092
3469
|
authorId: authorId ?? defaultAuthorId,
|
|
3093
3470
|
origin: createOrigin("api", clock()),
|
|
3094
3471
|
});
|
|
3095
|
-
|
|
3096
|
-
|
|
3472
|
+
// Read post-dispatch state. The reducer skips silently on unknown /
|
|
3473
|
+
// resolved / detached threads and emits a `review_target_not_found`
|
|
3474
|
+
// transient warning from `effects.transientWarnings` — callers who
|
|
3475
|
+
// need to know about the skip listen on `onWarning`.
|
|
3476
|
+
const entries = state.document.review.comments[commentId]?.entries ?? [];
|
|
3477
|
+
if (entries.length <= priorCount) {
|
|
3478
|
+
return null;
|
|
3479
|
+
}
|
|
3480
|
+
const last = entries[entries.length - 1]!;
|
|
3481
|
+
return { commentId, entryId: last.entryId };
|
|
3097
3482
|
},
|
|
3098
3483
|
editCommentBody(commentId, body) {
|
|
3099
3484
|
this.dispatch({
|
|
@@ -3168,17 +3553,29 @@ export function createDocumentRuntime(
|
|
|
3168
3553
|
});
|
|
3169
3554
|
|
|
3170
3555
|
if (params.persistence && params.persistence !== "runtime-only") {
|
|
3556
|
+
const requestedMetadata = params.metadata ?? {};
|
|
3557
|
+
const entryPersistence =
|
|
3558
|
+
requestedMetadata.metadataPersistence ??
|
|
3559
|
+
(params.persistence === "session" ? "external" : "internal");
|
|
3171
3560
|
const entry: WorkflowMetadataEntry = {
|
|
3172
|
-
entryId: `scope-metadata-${scopeId}`,
|
|
3173
|
-
metadataId: "workflow.scope",
|
|
3561
|
+
entryId: requestedMetadata.entryId ?? `scope-metadata-${scopeId}`,
|
|
3562
|
+
metadataId: requestedMetadata.metadataId ?? "workflow.scope",
|
|
3174
3563
|
anchor: publicAnchor,
|
|
3564
|
+
...(params.storyTarget ? { storyTarget: params.storyTarget } : {}),
|
|
3175
3565
|
scopeId,
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3566
|
+
...(requestedMetadata.workItemId ? { workItemId: requestedMetadata.workItemId } : {}),
|
|
3567
|
+
...(requestedMetadata.value !== undefined
|
|
3568
|
+
? { value: requestedMetadata.value }
|
|
3569
|
+
: params.persistence === "document-metadata" && params.label
|
|
3570
|
+
? { value: { label: params.label } }
|
|
3571
|
+
: {}),
|
|
3572
|
+
metadataPersistence: entryPersistence,
|
|
3573
|
+
...(requestedMetadata.storageRef !== undefined
|
|
3574
|
+
? { storageRef: requestedMetadata.storageRef }
|
|
3575
|
+
: {}),
|
|
3576
|
+
...(requestedMetadata.metadataVersion !== undefined
|
|
3577
|
+
? { metadataVersion: requestedMetadata.metadataVersion }
|
|
3578
|
+
: {}),
|
|
3182
3579
|
};
|
|
3183
3580
|
this.dispatch({
|
|
3184
3581
|
type: "workflow.set-metadata-entries",
|
|
@@ -3250,6 +3647,38 @@ export function createDocumentRuntime(
|
|
|
3250
3647
|
}
|
|
3251
3648
|
}
|
|
3252
3649
|
},
|
|
3650
|
+
addInvisibleScope(params) {
|
|
3651
|
+
const result = this.addScope({
|
|
3652
|
+
...params,
|
|
3653
|
+
mode: params.mode ?? "comment",
|
|
3654
|
+
});
|
|
3655
|
+
this.setScopeVisibility(result.scopeId, "invisible");
|
|
3656
|
+
return result;
|
|
3657
|
+
},
|
|
3658
|
+
setScopeVisibility(scopeId, visibility) {
|
|
3659
|
+
if (!workflowOverlay) return;
|
|
3660
|
+
const idx = workflowOverlay.scopes.findIndex((s) => s.scopeId === scopeId);
|
|
3661
|
+
if (idx === -1) return;
|
|
3662
|
+
const nextScopes = workflowOverlay.scopes.map((s) =>
|
|
3663
|
+
s.scopeId === scopeId ? { ...s, visibility } : s,
|
|
3664
|
+
);
|
|
3665
|
+
this.dispatch({
|
|
3666
|
+
type: "workflow.set-overlay",
|
|
3667
|
+
overlay: { ...workflowOverlay, scopes: nextScopes },
|
|
3668
|
+
origin: createOrigin("api", clock()),
|
|
3669
|
+
});
|
|
3670
|
+
},
|
|
3671
|
+
getScopeVisibility(scopeId): ScopeVisibility {
|
|
3672
|
+
if (!workflowOverlay) return "visible";
|
|
3673
|
+
const scope = workflowOverlay.scopes.find((s) => s.scopeId === scopeId);
|
|
3674
|
+
return scope?.visibility ?? "visible";
|
|
3675
|
+
},
|
|
3676
|
+
setScopeChromeVisibility(state) {
|
|
3677
|
+
scopeChromeVisibilityState = state;
|
|
3678
|
+
},
|
|
3679
|
+
getScopeChromeVisibility(): ScopeChromeVisibilityState {
|
|
3680
|
+
return scopeChromeVisibilityState;
|
|
3681
|
+
},
|
|
3253
3682
|
acceptChange(changeId) {
|
|
3254
3683
|
this.dispatch({
|
|
3255
3684
|
type: "change.accept",
|
|
@@ -3382,7 +3811,11 @@ export function createDocumentRuntime(
|
|
|
3382
3811
|
getFootnoteResolver(): FootnoteResolver | undefined {
|
|
3383
3812
|
const collection = state.document.subParts?.footnoteCollection;
|
|
3384
3813
|
if (!collection) return undefined;
|
|
3385
|
-
return createFootnoteResolver(
|
|
3814
|
+
return createFootnoteResolver(
|
|
3815
|
+
collection,
|
|
3816
|
+
collectSectionPropertiesInOrder(state.document),
|
|
3817
|
+
state.document,
|
|
3818
|
+
);
|
|
3386
3819
|
},
|
|
3387
3820
|
layout: layoutFacet,
|
|
3388
3821
|
getCurrentLocation() {
|
|
@@ -3529,6 +3962,9 @@ export function createDocumentRuntime(
|
|
|
3529
3962
|
options,
|
|
3530
3963
|
);
|
|
3531
3964
|
if (refreshed.changed) {
|
|
3965
|
+
// Field display text changed — clear field snapshot cache so the
|
|
3966
|
+
// next getCachedFieldSnapshot call rebuilds with fresh displayText.
|
|
3967
|
+
cachedFieldSnapshotEntry = null;
|
|
3532
3968
|
this.dispatch({
|
|
3533
3969
|
type: "document.replace",
|
|
3534
3970
|
document: refreshed.document,
|
|
@@ -3729,10 +4165,98 @@ export function createDocumentRuntime(
|
|
|
3729
4165
|
overlay: workflowOverlay,
|
|
3730
4166
|
entries: workflowMetadataEntries,
|
|
3731
4167
|
document: state.document,
|
|
4168
|
+
markerBackedScopeIds,
|
|
3732
4169
|
},
|
|
3733
4170
|
filter,
|
|
3734
4171
|
);
|
|
3735
4172
|
},
|
|
4173
|
+
subscribeToScopeQuery(filter, callback) {
|
|
4174
|
+
const buildAnchorKey = (anchor: EditorAnchorProjection): string => {
|
|
4175
|
+
switch (anchor.kind) {
|
|
4176
|
+
case "range":
|
|
4177
|
+
return `range:${anchor.from}:${anchor.to}:${anchor.assoc.start}:${anchor.assoc.end}`;
|
|
4178
|
+
case "node":
|
|
4179
|
+
return `node:${anchor.at}`;
|
|
4180
|
+
case "detached":
|
|
4181
|
+
return `detached:${anchor.reason}:${anchor.lastKnownRange.from}:${anchor.lastKnownRange.to}`;
|
|
4182
|
+
default:
|
|
4183
|
+
return "unknown";
|
|
4184
|
+
}
|
|
4185
|
+
};
|
|
4186
|
+
|
|
4187
|
+
const buildKey = (results: ScopeQueryResult[]) =>
|
|
4188
|
+
results
|
|
4189
|
+
.map(
|
|
4190
|
+
(r) =>
|
|
4191
|
+
[
|
|
4192
|
+
r.scope.scopeId,
|
|
4193
|
+
r.scope.version ?? 0,
|
|
4194
|
+
r.scope.visibility ?? "visible",
|
|
4195
|
+
buildAnchorKey(r.scope.anchor),
|
|
4196
|
+
r.workItem?.workItemId ?? "",
|
|
4197
|
+
r.entries
|
|
4198
|
+
.map(
|
|
4199
|
+
(entry) =>
|
|
4200
|
+
`${entry.entryId}:${entry.metadataVersion ?? 0}:${buildAnchorKey(entry.anchor)}`,
|
|
4201
|
+
)
|
|
4202
|
+
.join("|"),
|
|
4203
|
+
].join(":"),
|
|
4204
|
+
)
|
|
4205
|
+
.join(",");
|
|
4206
|
+
|
|
4207
|
+
let lastKey = "";
|
|
4208
|
+
const fire = () => {
|
|
4209
|
+
const results = this.queryScopes(filter);
|
|
4210
|
+
const key = buildKey(results);
|
|
4211
|
+
if (key !== lastKey) {
|
|
4212
|
+
lastKey = key;
|
|
4213
|
+
callback(results);
|
|
4214
|
+
}
|
|
4215
|
+
};
|
|
4216
|
+
|
|
4217
|
+
// Immediate initial fire
|
|
4218
|
+
fire();
|
|
4219
|
+
|
|
4220
|
+
let pendingTimer: ReturnType<typeof setTimeout> | null = null;
|
|
4221
|
+
const unsub = this.subscribe(() => {
|
|
4222
|
+
if (pendingTimer !== null) return;
|
|
4223
|
+
pendingTimer = setTimeout(() => {
|
|
4224
|
+
pendingTimer = null;
|
|
4225
|
+
fire();
|
|
4226
|
+
}, 0);
|
|
4227
|
+
});
|
|
4228
|
+
|
|
4229
|
+
return () => {
|
|
4230
|
+
unsub();
|
|
4231
|
+
if (pendingTimer !== null) {
|
|
4232
|
+
clearTimeout(pendingTimer);
|
|
4233
|
+
pendingTimer = null;
|
|
4234
|
+
}
|
|
4235
|
+
};
|
|
4236
|
+
},
|
|
4237
|
+
findAllText(query, options) {
|
|
4238
|
+
const sel = makePublicSelectionSnapshot(state.selection.anchor, state.selection.head);
|
|
4239
|
+
return findTextMatches(state.document, sel, query, options ?? {});
|
|
4240
|
+
},
|
|
4241
|
+
findTextWithStyle(query, filter, options) {
|
|
4242
|
+
const sel = makePublicSelectionSnapshot(state.selection.anchor, state.selection.head);
|
|
4243
|
+
return findTextWithStyleMatches(state.document, sel, query, filter, options ?? {});
|
|
4244
|
+
},
|
|
4245
|
+
selectTextWithStyle(query, filter, options) {
|
|
4246
|
+
const sel = makePublicSelectionSnapshot(state.selection.anchor, state.selection.head);
|
|
4247
|
+
const hits = findTextWithStyleMatches(state.document, sel, query, filter, options ?? {});
|
|
4248
|
+
if (hits.length > 0 && hits[0]) {
|
|
4249
|
+
const first = hits[0];
|
|
4250
|
+
if (first.kind === "range") {
|
|
4251
|
+
this.dispatch({
|
|
4252
|
+
type: "selection.set",
|
|
4253
|
+
selection: createSelectionSnapshot(first.from, first.to),
|
|
4254
|
+
origin: createOrigin("api", clock()),
|
|
4255
|
+
});
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
return hits.length;
|
|
4259
|
+
},
|
|
3736
4260
|
findScopesAt(position, options) {
|
|
3737
4261
|
const pos =
|
|
3738
4262
|
position.kind === "range"
|
|
@@ -3743,7 +4267,12 @@ export function createDocumentRuntime(
|
|
|
3743
4267
|
if (pos === null) return [];
|
|
3744
4268
|
const hits = findAllScopesAt(state.document, pos);
|
|
3745
4269
|
return projectScopeQueryResults(
|
|
3746
|
-
{
|
|
4270
|
+
{
|
|
4271
|
+
overlay: workflowOverlay,
|
|
4272
|
+
entries: workflowMetadataEntries,
|
|
4273
|
+
document: state.document,
|
|
4274
|
+
markerBackedScopeIds,
|
|
4275
|
+
},
|
|
3747
4276
|
hits.map((h) => h.scopeId),
|
|
3748
4277
|
options,
|
|
3749
4278
|
);
|
|
@@ -3752,7 +4281,12 @@ export function createDocumentRuntime(
|
|
|
3752
4281
|
if (range.kind !== "range") return [];
|
|
3753
4282
|
const hits = findScopesIntersecting(state.document, range.from, range.to, options?.mode);
|
|
3754
4283
|
return projectScopeQueryResults(
|
|
3755
|
-
{
|
|
4284
|
+
{
|
|
4285
|
+
overlay: workflowOverlay,
|
|
4286
|
+
entries: workflowMetadataEntries,
|
|
4287
|
+
document: state.document,
|
|
4288
|
+
markerBackedScopeIds,
|
|
4289
|
+
},
|
|
3756
4290
|
hits.map((h) => h.scopeId),
|
|
3757
4291
|
options,
|
|
3758
4292
|
);
|
|
@@ -3925,6 +4459,38 @@ export function createDocumentRuntime(
|
|
|
3925
4459
|
}
|
|
3926
4460
|
|
|
3927
4461
|
function applyTransactionToState(transaction: EditorTransaction): void {
|
|
4462
|
+
// Pure-no-op short-circuit: when a reducer skipped without emitting any
|
|
4463
|
+
// observable effect AND the selection is identical, skip finalizeState
|
|
4464
|
+
// / invalidate / refresh / notify entirely. This keeps hot paths (e.g.
|
|
4465
|
+
// host loops that query invalid ids) cheap — no clock allocation, no
|
|
4466
|
+
// listener loop, no snapshot rebuild for nothing. Transient-warning
|
|
4467
|
+
// commits (the `review_target_not_found` path) carry
|
|
4468
|
+
// `transientWarnings.length > 0` and MUST fall through so `notify`
|
|
4469
|
+
// can emit them. Selection-only dispatches (e.g. `selection.set`) also
|
|
4470
|
+
// fall through because `nextState.selection !== state.selection`.
|
|
4471
|
+
const effects = transaction.effects;
|
|
4472
|
+
const selectionUnchanged = transaction.nextState.selection === state.selection;
|
|
4473
|
+
const isPureNoop =
|
|
4474
|
+
!transaction.markDirty &&
|
|
4475
|
+
transaction.historyBoundary === "skip" &&
|
|
4476
|
+
transaction.mapping.steps.length === 0 &&
|
|
4477
|
+
selectionUnchanged &&
|
|
4478
|
+
effects.warningsAdded.length === 0 &&
|
|
4479
|
+
effects.warningsCleared.length === 0 &&
|
|
4480
|
+
(effects.transientWarnings?.length ?? 0) === 0 &&
|
|
4481
|
+
!effects.commentAdded &&
|
|
4482
|
+
!effects.commentResolved &&
|
|
4483
|
+
!effects.commentReopened &&
|
|
4484
|
+
!effects.commentReplyAdded &&
|
|
4485
|
+
!effects.commentBodyEdited &&
|
|
4486
|
+
!effects.changeAccepted &&
|
|
4487
|
+
!effects.changeRejected &&
|
|
4488
|
+
!effects.revisionAuthored &&
|
|
4489
|
+
!effects.commandBlocked;
|
|
4490
|
+
if (isPureNoop) {
|
|
4491
|
+
return;
|
|
4492
|
+
}
|
|
4493
|
+
|
|
3928
4494
|
const previous = state;
|
|
3929
4495
|
|
|
3930
4496
|
const tApply0 = performance.now();
|
|
@@ -3933,6 +4499,8 @@ export function createDocumentRuntime(
|
|
|
3933
4499
|
state = finalizeState(transaction.nextState, transaction.markDirty, clock());
|
|
3934
4500
|
perfCounters.increment("commit.finalizeState.us", Math.round((performance.now() - tFinalize0) * 1000));
|
|
3935
4501
|
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
4502
|
+
syncMarkerBackedScopeIds(state.document, workflowOverlay);
|
|
4503
|
+
const detachedWorkflowScopeWarnings = syncDetachedWorkflowScopeWarningsInState();
|
|
3936
4504
|
|
|
3937
4505
|
const tInvalidate0 = performance.now();
|
|
3938
4506
|
if (transaction.markDirty && transaction.mapping.steps.length > 0) {
|
|
@@ -3948,8 +4516,11 @@ export function createDocumentRuntime(
|
|
|
3948
4516
|
}
|
|
3949
4517
|
}
|
|
3950
4518
|
if (previous.document.subParts !== state.document.subParts) {
|
|
3951
|
-
|
|
3952
|
-
|
|
4519
|
+
const fontInput = collectFontLoaderInput(state.document);
|
|
4520
|
+
if (!fontFamiliesEqual(fontInput.families, collectFontLoaderInput(previous.document).families)) {
|
|
4521
|
+
fontLoader.refresh(fontInput);
|
|
4522
|
+
layoutEngine.invalidateMeasurementCache();
|
|
4523
|
+
}
|
|
3953
4524
|
}
|
|
3954
4525
|
perfCounters.increment("commit.invalidate.us", Math.round((performance.now() - tInvalidate0) * 1000));
|
|
3955
4526
|
|
|
@@ -3979,10 +4550,16 @@ export function createDocumentRuntime(
|
|
|
3979
4550
|
// which is optional in the public API for shape-only reasons — the helper
|
|
3980
4551
|
// itself always returns a defined snapshot.
|
|
3981
4552
|
const surfaceForValidation = getCachedSurface(state.document, activeStory)!;
|
|
4553
|
+
const validationOptions = state.selection.activeRange.kind === "node"
|
|
4554
|
+
? {
|
|
4555
|
+
isValidNodeTarget: createSurfaceNodeSelectionProbe(surfaceForValidation),
|
|
4556
|
+
}
|
|
4557
|
+
: undefined;
|
|
3982
4558
|
const validatedSelection = validateSelectionAgainstDocument(
|
|
3983
4559
|
state.document,
|
|
3984
4560
|
state.selection,
|
|
3985
4561
|
surfaceForValidation.storySize,
|
|
4562
|
+
validationOptions,
|
|
3986
4563
|
);
|
|
3987
4564
|
if (validatedSelection !== state.selection) {
|
|
3988
4565
|
state = { ...state, selection: validatedSelection };
|
|
@@ -3994,7 +4571,20 @@ export function createDocumentRuntime(
|
|
|
3994
4571
|
perfCounters.increment("commit.refresh.us", Math.round((performance.now() - tRefresh0) * 1000));
|
|
3995
4572
|
|
|
3996
4573
|
const tNotify0 = performance.now();
|
|
3997
|
-
notify(previous, state,
|
|
4574
|
+
notify(previous, state, {
|
|
4575
|
+
...transaction,
|
|
4576
|
+
effects: {
|
|
4577
|
+
...transaction.effects,
|
|
4578
|
+
warningsAdded: [
|
|
4579
|
+
...transaction.effects.warningsAdded,
|
|
4580
|
+
...detachedWorkflowScopeWarnings.added,
|
|
4581
|
+
],
|
|
4582
|
+
warningsCleared: [
|
|
4583
|
+
...transaction.effects.warningsCleared,
|
|
4584
|
+
...detachedWorkflowScopeWarnings.cleared,
|
|
4585
|
+
],
|
|
4586
|
+
},
|
|
4587
|
+
});
|
|
3998
4588
|
perfCounters.increment("commit.notify.us", Math.round((performance.now() - tNotify0) * 1000));
|
|
3999
4589
|
perfCounters.increment("commit.total.us", Math.round((performance.now() - tApply0) * 1000));
|
|
4000
4590
|
}
|
|
@@ -4184,6 +4774,23 @@ export function createDocumentRuntime(
|
|
|
4184
4774
|
options.onWarning?.(publicWarning);
|
|
4185
4775
|
}
|
|
4186
4776
|
|
|
4777
|
+
if (transaction.effects.transientWarnings) {
|
|
4778
|
+
// Fire-and-forget diagnostics (e.g. `review_target_not_found` on a
|
|
4779
|
+
// silent-skip reducer). Surfaces on `warning_added` + `onWarning`
|
|
4780
|
+
// like any other warning, but never touches `state.warnings` or the
|
|
4781
|
+
// compatibility report, so repeated no-op calls (e.g. a host
|
|
4782
|
+
// looping `resolveComment` over stale ids) do not accumulate entries.
|
|
4783
|
+
for (const warning of transaction.effects.transientWarnings) {
|
|
4784
|
+
const publicWarning = toPublicWarning(warning);
|
|
4785
|
+
emit({
|
|
4786
|
+
type: "warning_added",
|
|
4787
|
+
documentId: next.documentId,
|
|
4788
|
+
warning: publicWarning,
|
|
4789
|
+
});
|
|
4790
|
+
options.onWarning?.(publicWarning);
|
|
4791
|
+
}
|
|
4792
|
+
}
|
|
4793
|
+
|
|
4187
4794
|
for (const cleared of transaction.effects.warningsCleared) {
|
|
4188
4795
|
emit({
|
|
4189
4796
|
type: "warning_cleared",
|
|
@@ -4433,9 +5040,16 @@ export function createDocumentRuntime(
|
|
|
4433
5040
|
base: EditorTransaction["effects"],
|
|
4434
5041
|
local: EditorTransaction["effects"],
|
|
4435
5042
|
): EditorTransaction["effects"] {
|
|
5043
|
+
const baseTransient = base.transientWarnings ?? [];
|
|
5044
|
+
const localTransient = local.transientWarnings ?? [];
|
|
5045
|
+
const mergedTransient =
|
|
5046
|
+
baseTransient.length + localTransient.length === 0
|
|
5047
|
+
? undefined
|
|
5048
|
+
: [...baseTransient, ...localTransient];
|
|
4436
5049
|
return {
|
|
4437
5050
|
warningsAdded: [...base.warningsAdded, ...local.warningsAdded],
|
|
4438
5051
|
warningsCleared: [...base.warningsCleared, ...local.warningsCleared],
|
|
5052
|
+
...(mergedTransient ? { transientWarnings: mergedTransient } : {}),
|
|
4439
5053
|
commentAdded: base.commentAdded ?? local.commentAdded,
|
|
4440
5054
|
commentResolved: base.commentResolved ?? local.commentResolved,
|
|
4441
5055
|
commentReopened: base.commentReopened ?? local.commentReopened,
|
|
@@ -4599,10 +5213,18 @@ export function createDocumentRuntime(
|
|
|
4599
5213
|
function applyRuntimeStateOverlayCommand(
|
|
4600
5214
|
command: RuntimeStateOverlayCommand,
|
|
4601
5215
|
): void {
|
|
5216
|
+
let detachedWorkflowScopeWarnings:
|
|
5217
|
+
| {
|
|
5218
|
+
added: InternalEditorWarning[];
|
|
5219
|
+
cleared: Array<{ warningId: string; code: InternalEditorWarning["code"] }>;
|
|
5220
|
+
}
|
|
5221
|
+
| null = null;
|
|
4602
5222
|
switch (command.type) {
|
|
4603
5223
|
case "workflow.set-overlay": {
|
|
4604
5224
|
workflowOverlay = structuredClone(command.overlay);
|
|
5225
|
+
syncMarkerBackedScopeIds(state.document, workflowOverlay);
|
|
4605
5226
|
cachedNormalizedWorkflowOverlay = undefined;
|
|
5227
|
+
detachedWorkflowScopeWarnings = syncDetachedWorkflowScopeWarningsInState();
|
|
4606
5228
|
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
4607
5229
|
const snapshot = deriveWorkflowScopeSnapshot()!;
|
|
4608
5230
|
emit({
|
|
@@ -4621,7 +5243,9 @@ export function createDocumentRuntime(
|
|
|
4621
5243
|
}
|
|
4622
5244
|
case "workflow.clear-overlay": {
|
|
4623
5245
|
workflowOverlay = null;
|
|
5246
|
+
syncMarkerBackedScopeIds(state.document, workflowOverlay);
|
|
4624
5247
|
cachedNormalizedWorkflowOverlay = undefined;
|
|
5248
|
+
detachedWorkflowScopeWarnings = syncDetachedWorkflowScopeWarningsInState();
|
|
4625
5249
|
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
4626
5250
|
emit({
|
|
4627
5251
|
type: "workflow_active_work_item_changed",
|
|
@@ -4696,6 +5320,25 @@ export function createDocumentRuntime(
|
|
|
4696
5320
|
break;
|
|
4697
5321
|
}
|
|
4698
5322
|
}
|
|
5323
|
+
if (detachedWorkflowScopeWarnings) {
|
|
5324
|
+
for (const warning of detachedWorkflowScopeWarnings.added) {
|
|
5325
|
+
const publicWarning = toPublicWarning(warning);
|
|
5326
|
+
emit({
|
|
5327
|
+
type: "warning_added",
|
|
5328
|
+
documentId: state.documentId,
|
|
5329
|
+
warning: publicWarning,
|
|
5330
|
+
});
|
|
5331
|
+
options.onWarning?.(publicWarning);
|
|
5332
|
+
}
|
|
5333
|
+
for (const cleared of detachedWorkflowScopeWarnings.cleared) {
|
|
5334
|
+
emit({
|
|
5335
|
+
type: "warning_cleared",
|
|
5336
|
+
documentId: state.documentId,
|
|
5337
|
+
warningId: cleared.warningId,
|
|
5338
|
+
code: cleared.code,
|
|
5339
|
+
});
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
4699
5342
|
for (const listener of listeners) {
|
|
4700
5343
|
listener();
|
|
4701
5344
|
}
|
|
@@ -4979,7 +5622,13 @@ function toRuntimeError(error: unknown): InternalEditorError {
|
|
|
4979
5622
|
function toStructuredRuntimeException<T extends InternalEditorError>(
|
|
4980
5623
|
error: T,
|
|
4981
5624
|
): Error & T {
|
|
4982
|
-
|
|
5625
|
+
const exception = Object.assign(new Error(error.message), error);
|
|
5626
|
+
// Set `name` so host wrappers that must branch across process / bundle
|
|
5627
|
+
// boundaries (e.g. the gRPC handlers in `vendor/beyondwork/src/ts/docx-api`)
|
|
5628
|
+
// can discriminate structured editor errors from generic `Error` objects
|
|
5629
|
+
// via `error.name === "EditorError"` — `instanceof` is unreliable there.
|
|
5630
|
+
exception.name = "EditorError";
|
|
5631
|
+
return exception;
|
|
4983
5632
|
}
|
|
4984
5633
|
|
|
4985
5634
|
function toPublicDocumentStats(state: Pick<EditorState, "document">) {
|
|
@@ -5077,11 +5726,14 @@ function extractSelectionFragment(
|
|
|
5077
5726
|
}
|
|
5078
5727
|
|
|
5079
5728
|
/**
|
|
5080
|
-
* Collect the stable ids of comment threads whose
|
|
5081
|
-
*
|
|
5082
|
-
*
|
|
5083
|
-
* the thread
|
|
5084
|
-
*
|
|
5729
|
+
* Collect the stable ids of comment threads whose host-observable state
|
|
5730
|
+
* differs between two commits. A reference-identity diff was too noisy —
|
|
5731
|
+
* any `remapCommentStore` pass (fired on every text edit that touches a
|
|
5732
|
+
* thread's anchor region) rebuilt the thread objects and would have marked
|
|
5733
|
+
* every thread as "changed" even when only their anchor numerics shifted
|
|
5734
|
+
* in a way that doesn't affect what the host renders. The semantic diff
|
|
5735
|
+
* below compares the fields hosts actually consume: status, anchor shape,
|
|
5736
|
+
* entry count + ordering, resolution metadata, and warning ids.
|
|
5085
5737
|
*/
|
|
5086
5738
|
function diffCommentMapKeys(
|
|
5087
5739
|
previous: CanonicalDocumentEnvelope["review"]["comments"],
|
|
@@ -5089,14 +5741,65 @@ function diffCommentMapKeys(
|
|
|
5089
5741
|
): string[] {
|
|
5090
5742
|
const changed = new Set<string>();
|
|
5091
5743
|
for (const id of Object.keys(previous)) {
|
|
5092
|
-
if (
|
|
5744
|
+
if (!next[id]) changed.add(id);
|
|
5093
5745
|
}
|
|
5094
5746
|
for (const id of Object.keys(next)) {
|
|
5095
|
-
|
|
5747
|
+
const prev = previous[id];
|
|
5748
|
+
if (!prev) {
|
|
5749
|
+
changed.add(id);
|
|
5750
|
+
continue;
|
|
5751
|
+
}
|
|
5752
|
+
if (prev === next[id]) continue;
|
|
5753
|
+
if (!semanticallyEqualCommentThreads(prev, next[id]!)) changed.add(id);
|
|
5096
5754
|
}
|
|
5097
5755
|
return Array.from(changed);
|
|
5098
5756
|
}
|
|
5099
5757
|
|
|
5758
|
+
function semanticallyEqualCommentThreads(
|
|
5759
|
+
prev: CanonicalDocumentEnvelope["review"]["comments"][string],
|
|
5760
|
+
next: CanonicalDocumentEnvelope["review"]["comments"][string],
|
|
5761
|
+
): boolean {
|
|
5762
|
+
if (prev.status !== next.status) return false;
|
|
5763
|
+
if (prev.isResolved !== next.isResolved) return false;
|
|
5764
|
+
if (prev.resolvedAt !== next.resolvedAt) return false;
|
|
5765
|
+
if (prev.resolution?.resolvedAt !== next.resolution?.resolvedAt) return false;
|
|
5766
|
+
if (prev.resolution?.resolvedBy !== next.resolution?.resolvedBy) return false;
|
|
5767
|
+
if (prev.body !== next.body) return false;
|
|
5768
|
+
if ((prev.entries?.length ?? 0) !== (next.entries?.length ?? 0)) return false;
|
|
5769
|
+
const prevEntries = prev.entries ?? [];
|
|
5770
|
+
const nextEntries = next.entries ?? [];
|
|
5771
|
+
for (let i = 0; i < prevEntries.length; i++) {
|
|
5772
|
+
const pe = prevEntries[i]!;
|
|
5773
|
+
const ne = nextEntries[i]!;
|
|
5774
|
+
if (pe.entryId !== ne.entryId) return false;
|
|
5775
|
+
if (pe.body !== ne.body) return false;
|
|
5776
|
+
if (pe.authorId !== ne.authorId) return false;
|
|
5777
|
+
}
|
|
5778
|
+
const prevWarnings = prev.warningIds ?? [];
|
|
5779
|
+
const nextWarnings = next.warningIds ?? [];
|
|
5780
|
+
if (prevWarnings.length !== nextWarnings.length) return false;
|
|
5781
|
+
for (let i = 0; i < prevWarnings.length; i++) {
|
|
5782
|
+
if (prevWarnings[i] !== nextWarnings[i]) return false;
|
|
5783
|
+
}
|
|
5784
|
+
const prevAnchor = prev.anchor;
|
|
5785
|
+
const nextAnchor = next.anchor;
|
|
5786
|
+
if (prevAnchor === nextAnchor) return true;
|
|
5787
|
+
if (prevAnchor.kind !== nextAnchor.kind) return false;
|
|
5788
|
+
if (prevAnchor.kind === "range" && nextAnchor.kind === "range") {
|
|
5789
|
+
return (
|
|
5790
|
+
prevAnchor.range.from === nextAnchor.range.from &&
|
|
5791
|
+
prevAnchor.range.to === nextAnchor.range.to
|
|
5792
|
+
);
|
|
5793
|
+
}
|
|
5794
|
+
if (prevAnchor.kind === "node" && nextAnchor.kind === "node") {
|
|
5795
|
+
return prevAnchor.at === nextAnchor.at;
|
|
5796
|
+
}
|
|
5797
|
+
if (prevAnchor.kind === "detached" && nextAnchor.kind === "detached") {
|
|
5798
|
+
return prevAnchor.reason === nextAnchor.reason;
|
|
5799
|
+
}
|
|
5800
|
+
return false;
|
|
5801
|
+
}
|
|
5802
|
+
|
|
5100
5803
|
function toPublicCompatibilityReport(
|
|
5101
5804
|
report: InternalCompatibilityReport,
|
|
5102
5805
|
): CompatibilityReport {
|
|
@@ -5124,11 +5827,42 @@ function toPublicCompatibilityFeatureEntry(
|
|
|
5124
5827
|
}
|
|
5125
5828
|
|
|
5126
5829
|
function toPublicWarning(warning: InternalEditorWarning): EditorWarning {
|
|
5830
|
+
const diagnostic =
|
|
5831
|
+
warning.diagnostic ??
|
|
5832
|
+
(() => {
|
|
5833
|
+
switch (warning.code) {
|
|
5834
|
+
case "unsupported_ooxml_preserved":
|
|
5835
|
+
case "unsupported_ooxml_locked":
|
|
5836
|
+
case "import_normalized":
|
|
5837
|
+
case "export_roundtrip_risk":
|
|
5838
|
+
case "comment_anchor_detached":
|
|
5839
|
+
case "revision_anchor_detached":
|
|
5840
|
+
case "workflow_scope_invalidated":
|
|
5841
|
+
case "large_document_degraded":
|
|
5842
|
+
case "font_substitution":
|
|
5843
|
+
case "image_missing":
|
|
5844
|
+
return buildDiagnosticFromLegacyWarningCode(warning.code, {
|
|
5845
|
+
diagnosticId: `warning-diag:${warning.warningId}`,
|
|
5846
|
+
emittedAt: new Date(0).toISOString(),
|
|
5847
|
+
technical: {
|
|
5848
|
+
message: warning.message,
|
|
5849
|
+
source: warning.source,
|
|
5850
|
+
},
|
|
5851
|
+
details: warning.details,
|
|
5852
|
+
affectedAnchor: warning.affectedAnchor
|
|
5853
|
+
? toPublicAnchorProjection(warning.affectedAnchor)
|
|
5854
|
+
: undefined,
|
|
5855
|
+
});
|
|
5856
|
+
default:
|
|
5857
|
+
return undefined;
|
|
5858
|
+
}
|
|
5859
|
+
})();
|
|
5127
5860
|
return {
|
|
5128
5861
|
...warning,
|
|
5129
5862
|
affectedAnchor: warning.affectedAnchor
|
|
5130
5863
|
? toPublicAnchorProjection(warning.affectedAnchor)
|
|
5131
5864
|
: undefined,
|
|
5865
|
+
diagnostic,
|
|
5132
5866
|
};
|
|
5133
5867
|
}
|
|
5134
5868
|
|
|
@@ -5510,6 +6244,22 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
5510
6244
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
5511
6245
|
}
|
|
5512
6246
|
|
|
6247
|
+
function makePublicSelectionSnapshot(anchor: number, head: number): SelectionSnapshot {
|
|
6248
|
+
const from = Math.min(anchor, head);
|
|
6249
|
+
const to = Math.max(anchor, head);
|
|
6250
|
+
return {
|
|
6251
|
+
anchor,
|
|
6252
|
+
head,
|
|
6253
|
+
isCollapsed: anchor === head,
|
|
6254
|
+
activeRange: {
|
|
6255
|
+
kind: "range",
|
|
6256
|
+
from,
|
|
6257
|
+
to,
|
|
6258
|
+
assoc: { start: -1, end: 1 },
|
|
6259
|
+
},
|
|
6260
|
+
};
|
|
6261
|
+
}
|
|
6262
|
+
|
|
5513
6263
|
/** Commands that are safe in viewing mode (no document mutation). */
|
|
5514
6264
|
const NON_MUTATION_COMMANDS = new Set([
|
|
5515
6265
|
"selection.set",
|
|
@@ -5526,6 +6276,9 @@ const NON_MUTATION_COMMANDS = new Set([
|
|
|
5526
6276
|
"workflow.clear-metadata-entries",
|
|
5527
6277
|
"host-annotation.set-overlay",
|
|
5528
6278
|
"host-annotation.clear-overlay",
|
|
6279
|
+
// API-level full-document replacement (scope markers, protection refresh,
|
|
6280
|
+
// collab replay). Not user-initiated — must bypass the workflow guard.
|
|
6281
|
+
"document.replace",
|
|
5529
6282
|
]);
|
|
5530
6283
|
|
|
5531
6284
|
/** Mutation commands that are not yet supported in suggesting mode. */
|
|
@@ -6626,6 +7379,17 @@ const fontLoaderInputCache = new WeakMap<
|
|
|
6626
7379
|
WeakMap<object, { families: readonly string[] }>
|
|
6627
7380
|
>();
|
|
6628
7381
|
|
|
7382
|
+
function fontFamiliesEqual(
|
|
7383
|
+
a: readonly string[],
|
|
7384
|
+
b: readonly string[],
|
|
7385
|
+
): boolean {
|
|
7386
|
+
if (a.length !== b.length) return false;
|
|
7387
|
+
// Both arrays are Set-derived (no duplicates); sort for order-independence.
|
|
7388
|
+
const sortedA = [...a].sort();
|
|
7389
|
+
const sortedB = [...b].sort();
|
|
7390
|
+
return sortedA.every((v, i) => v === sortedB[i]);
|
|
7391
|
+
}
|
|
7392
|
+
|
|
6629
7393
|
function collectFontLoaderInput(
|
|
6630
7394
|
document: CanonicalDocumentEnvelope,
|
|
6631
7395
|
): { families: readonly string[] } {
|
|
@@ -6678,6 +7442,9 @@ function collectFontLoaderInputUncached(
|
|
|
6678
7442
|
/** Test-only export of the uncached walk so memoization tests can spy on it. */
|
|
6679
7443
|
export const __collectFontLoaderInputUncached = collectFontLoaderInputUncached;
|
|
6680
7444
|
|
|
7445
|
+
/** Test-only export of the font-family set equality helper. */
|
|
7446
|
+
export const __fontFamiliesEqual = fontFamiliesEqual;
|
|
7447
|
+
|
|
6681
7448
|
/**
|
|
6682
7449
|
* Asynchronously upgrade the engine's measurement backend to canvas once
|
|
6683
7450
|
* the platform supports it and fonts have resolved. Errors are swallowed
|