@beyondwork/docx-react-component 1.0.59 → 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/package.json +1 -1
- package/src/api/public-types.ts +2 -0
- package/src/core/state/editor-state.ts +2 -0
- package/src/io/ooxml/workflow-payload.ts +6 -1
- package/src/model/snapshot.ts +2 -0
- package/src/runtime/diagnostics/build-diagnostic.ts +2 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +9 -0
- package/src/runtime/document-runtime.ts +346 -21
- package/src/runtime/query-scopes.ts +103 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.60",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
6
|
"packageManager": "pnpm@10.30.3",
|
|
7
7
|
"type": "module",
|
package/src/api/public-types.ts
CHANGED
|
@@ -1257,6 +1257,7 @@ export type EditorWarningCode =
|
|
|
1257
1257
|
| "export_roundtrip_risk"
|
|
1258
1258
|
| "comment_anchor_detached"
|
|
1259
1259
|
| "revision_anchor_detached"
|
|
1260
|
+
| "workflow_scope_invalidated"
|
|
1260
1261
|
| "large_document_degraded"
|
|
1261
1262
|
| "font_substitution"
|
|
1262
1263
|
| "image_missing"
|
|
@@ -1350,6 +1351,7 @@ export type EditorDiagnosticCode =
|
|
|
1350
1351
|
| "export.roundtrip_risk"
|
|
1351
1352
|
| "review.comment_anchor_detached"
|
|
1352
1353
|
| "review.revision_anchor_detached"
|
|
1354
|
+
| "runtime.workflow_scope_invalidated"
|
|
1353
1355
|
| "runtime.large_document_degraded"
|
|
1354
1356
|
| "runtime.font_substitution"
|
|
1355
1357
|
| "runtime.image_missing"
|
|
@@ -31,6 +31,7 @@ export type EditorWarningCode =
|
|
|
31
31
|
| "export_roundtrip_risk"
|
|
32
32
|
| "comment_anchor_detached"
|
|
33
33
|
| "revision_anchor_detached"
|
|
34
|
+
| "workflow_scope_invalidated"
|
|
34
35
|
| "large_document_degraded"
|
|
35
36
|
| "font_substitution"
|
|
36
37
|
| "image_missing"
|
|
@@ -51,6 +52,7 @@ export interface EditorWarning {
|
|
|
51
52
|
affectedAnchor?: EditorAnchorProjection;
|
|
52
53
|
featureEntryId?: string;
|
|
53
54
|
details?: Record<string, unknown>;
|
|
55
|
+
diagnostic?: import("../../api/public-types.ts").EditorDiagnostic;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
export interface EditorError {
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
WorkflowWorkItem,
|
|
14
14
|
} from "../../api/public-types.ts";
|
|
15
15
|
import type { EditorStateNamespace, EditorStateLocation } from "../../api/editor-state-types.ts";
|
|
16
|
+
import { sha256Hex } from "../source-package-provenance.ts";
|
|
16
17
|
import {
|
|
17
18
|
validateWorkflowPayloadEnvelope,
|
|
18
19
|
type ValidatorIssue,
|
|
@@ -42,6 +43,8 @@ export interface EditorStatePayload {
|
|
|
42
43
|
unknownNamespaces?: Array<{ name: string; rawXml: string }>;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
const workflowAnchorHashEncoder = new TextEncoder();
|
|
47
|
+
|
|
45
48
|
// ---------------------------------------------------------------------------
|
|
46
49
|
// Schema 1.1 parser helpers (fail-closed per spec §8.2)
|
|
47
50
|
// ---------------------------------------------------------------------------
|
|
@@ -747,7 +750,9 @@ function createAnchorBindingHash(
|
|
|
747
750
|
from: number,
|
|
748
751
|
to: number,
|
|
749
752
|
): string {
|
|
750
|
-
return
|
|
753
|
+
return sha256Hex(
|
|
754
|
+
workflowAnchorHashEncoder.encode(`${serializeStoryTargetKey(storyTarget)}:${from}:${to}`),
|
|
755
|
+
);
|
|
751
756
|
}
|
|
752
757
|
|
|
753
758
|
function getPreservedExtensionsXml(sourcePackage: OpcPackage, payloadPartPath: string): string {
|
package/src/model/snapshot.ts
CHANGED
|
@@ -28,6 +28,7 @@ export type EditorWarningCode =
|
|
|
28
28
|
| "export_roundtrip_risk"
|
|
29
29
|
| "comment_anchor_detached"
|
|
30
30
|
| "revision_anchor_detached"
|
|
31
|
+
| "workflow_scope_invalidated"
|
|
31
32
|
| "large_document_degraded"
|
|
32
33
|
| "font_substitution"
|
|
33
34
|
| "image_missing"
|
|
@@ -235,6 +236,7 @@ const EDITOR_WARNING_CODES = new Set<EditorWarningCode>([
|
|
|
235
236
|
"export_roundtrip_risk",
|
|
236
237
|
"comment_anchor_detached",
|
|
237
238
|
"revision_anchor_detached",
|
|
239
|
+
"workflow_scope_invalidated",
|
|
238
240
|
"large_document_degraded",
|
|
239
241
|
"font_substitution",
|
|
240
242
|
"image_missing",
|
|
@@ -131,6 +131,7 @@ export function buildDiagnosticFromLegacyWarningCode(
|
|
|
131
131
|
| "export_roundtrip_risk"
|
|
132
132
|
| "comment_anchor_detached"
|
|
133
133
|
| "revision_anchor_detached"
|
|
134
|
+
| "workflow_scope_invalidated"
|
|
134
135
|
| "large_document_degraded"
|
|
135
136
|
| "font_substitution"
|
|
136
137
|
| "image_missing",
|
|
@@ -143,6 +144,7 @@ export function buildDiagnosticFromLegacyWarningCode(
|
|
|
143
144
|
export_roundtrip_risk: "export.roundtrip_risk",
|
|
144
145
|
comment_anchor_detached: "review.comment_anchor_detached",
|
|
145
146
|
revision_anchor_detached: "review.revision_anchor_detached",
|
|
147
|
+
workflow_scope_invalidated: "runtime.workflow_scope_invalidated",
|
|
146
148
|
large_document_degraded: "runtime.large_document_degraded",
|
|
147
149
|
font_substitution: "runtime.font_substitution",
|
|
148
150
|
image_missing: "runtime.image_missing",
|
|
@@ -80,6 +80,15 @@ export const CODE_METADATA_TABLE: Readonly<
|
|
|
80
80
|
"review.revision_anchor_detached": row("review", "info",
|
|
81
81
|
"A tracked change's anchor was invalidated by a structural edit.",
|
|
82
82
|
"retry-safe", { kind: "none" }, ["review", "tracked-change"], "review-revision-anchor-detached"),
|
|
83
|
+
"runtime.workflow_scope_invalidated": row("runtime", "warning",
|
|
84
|
+
"A workflow scope lost its trusted anchor and is no longer enforcing against live text.",
|
|
85
|
+
"requires-input",
|
|
86
|
+
{
|
|
87
|
+
kind: "fallback",
|
|
88
|
+
suggestion:
|
|
89
|
+
"Use warning.details.scopeId + warning.details.lastKnownRange to relocate the intended text, then reapply the scope.",
|
|
90
|
+
},
|
|
91
|
+
["runtime", "workflow", "scope"], "runtime-workflow-scope-invalidated"),
|
|
83
92
|
"runtime.large_document_degraded": row("runtime", "warning",
|
|
84
93
|
"The document is large enough that some rendering optimizations have been relaxed.",
|
|
85
94
|
"retry-safe", { kind: "none" }, ["runtime", "perf"], "runtime-large-document-degraded"),
|
|
@@ -150,6 +150,7 @@ import {
|
|
|
150
150
|
findScopesIntersecting,
|
|
151
151
|
resolveScope,
|
|
152
152
|
} from "./scope-resolver.ts";
|
|
153
|
+
import { buildDiagnosticFromLegacyWarningCode } from "./diagnostics/build-diagnostic.ts";
|
|
153
154
|
import {
|
|
154
155
|
projectScopeQueryResults,
|
|
155
156
|
queryScopes as runQueryScopes,
|
|
@@ -872,6 +873,7 @@ export function createDocumentRuntime(
|
|
|
872
873
|
options.initialSessionState?.workflowMetadata?.entries
|
|
873
874
|
?? options.initialSnapshot?.workflowMetadata?.entries
|
|
874
875
|
?? [];
|
|
876
|
+
let markerBackedScopeIds = new Set<string>();
|
|
875
877
|
let hostAnnotationOverlay: HostAnnotationOverlay | null = null;
|
|
876
878
|
// §C7 — local view-state for scope chrome visibility; never collab-replicated.
|
|
877
879
|
let scopeChromeVisibilityState: ScopeChromeVisibilityState = { mode: "all" };
|
|
@@ -909,6 +911,32 @@ export function createDocumentRuntime(
|
|
|
909
911
|
storySelections.set(storyTargetKey(MAIN_STORY_TARGET), state.selection);
|
|
910
912
|
lastHeadingFingerprint = computeHeadingFingerprint(state.document);
|
|
911
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
|
+
|
|
912
940
|
// Runtime-owned paginated layout engine (Phase 1+ of the layout facet work).
|
|
913
941
|
// The engine caches graph + resolved-formatting + fragment mapper keyed on
|
|
914
942
|
// (content, styles, subParts). It is the single internal source of truth
|
|
@@ -1924,19 +1952,37 @@ export function createDocumentRuntime(
|
|
|
1924
1952
|
return scope;
|
|
1925
1953
|
}
|
|
1926
1954
|
const location = locations.get(scope.scopeId);
|
|
1955
|
+
const isMarkerBacked = markerBackedScopeIds.has(scope.scopeId);
|
|
1956
|
+
let nextAnchor: EditorAnchorProjection | null = null;
|
|
1927
1957
|
if (
|
|
1928
|
-
|
|
1929
|
-
location.startPos
|
|
1930
|
-
location.endPos
|
|
1958
|
+
location &&
|
|
1959
|
+
location.startPos !== undefined &&
|
|
1960
|
+
location.endPos !== undefined
|
|
1931
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 {
|
|
1932
1984
|
return scope;
|
|
1933
1985
|
}
|
|
1934
|
-
const nextAnchor: EditorAnchorProjection = {
|
|
1935
|
-
kind: "range",
|
|
1936
|
-
from: Math.min(location.startPos, location.endPos),
|
|
1937
|
-
to: Math.max(location.startPos, location.endPos),
|
|
1938
|
-
assoc: { start: -1, end: 1 },
|
|
1939
|
-
};
|
|
1940
1986
|
if (workflowAnchorsEqual(scope.anchor, nextAnchor)) {
|
|
1941
1987
|
return scope;
|
|
1942
1988
|
}
|
|
@@ -1966,6 +2012,161 @@ export function createDocumentRuntime(
|
|
|
1966
2012
|
return normalizeWorkflowOverlayForDocument(state.document, workflowOverlay);
|
|
1967
2013
|
}
|
|
1968
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
|
+
|
|
1969
2170
|
function deriveWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null {
|
|
1970
2171
|
const normalizedWorkflowOverlay = getNormalizedWorkflowOverlay();
|
|
1971
2172
|
if (!normalizedWorkflowOverlay) return null;
|
|
@@ -2529,6 +2730,7 @@ export function createDocumentRuntime(
|
|
|
2529
2730
|
return snapshot;
|
|
2530
2731
|
}
|
|
2531
2732
|
|
|
2733
|
+
syncDetachedWorkflowScopeWarningsInState();
|
|
2532
2734
|
let cachedRenderSnapshot = refreshRenderSnapshot();
|
|
2533
2735
|
|
|
2534
2736
|
emit({
|
|
@@ -3351,17 +3553,29 @@ export function createDocumentRuntime(
|
|
|
3351
3553
|
});
|
|
3352
3554
|
|
|
3353
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");
|
|
3354
3560
|
const entry: WorkflowMetadataEntry = {
|
|
3355
|
-
entryId: `scope-metadata-${scopeId}`,
|
|
3356
|
-
metadataId: "workflow.scope",
|
|
3561
|
+
entryId: requestedMetadata.entryId ?? `scope-metadata-${scopeId}`,
|
|
3562
|
+
metadataId: requestedMetadata.metadataId ?? "workflow.scope",
|
|
3357
3563
|
anchor: publicAnchor,
|
|
3564
|
+
...(params.storyTarget ? { storyTarget: params.storyTarget } : {}),
|
|
3358
3565
|
scopeId,
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
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
|
+
: {}),
|
|
3365
3579
|
};
|
|
3366
3580
|
this.dispatch({
|
|
3367
3581
|
type: "workflow.set-metadata-entries",
|
|
@@ -3951,16 +4165,42 @@ export function createDocumentRuntime(
|
|
|
3951
4165
|
overlay: workflowOverlay,
|
|
3952
4166
|
entries: workflowMetadataEntries,
|
|
3953
4167
|
document: state.document,
|
|
4168
|
+
markerBackedScopeIds,
|
|
3954
4169
|
},
|
|
3955
4170
|
filter,
|
|
3956
4171
|
);
|
|
3957
4172
|
},
|
|
3958
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
|
+
|
|
3959
4187
|
const buildKey = (results: ScopeQueryResult[]) =>
|
|
3960
4188
|
results
|
|
3961
4189
|
.map(
|
|
3962
4190
|
(r) =>
|
|
3963
|
-
|
|
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(":"),
|
|
3964
4204
|
)
|
|
3965
4205
|
.join(",");
|
|
3966
4206
|
|
|
@@ -4027,7 +4267,12 @@ export function createDocumentRuntime(
|
|
|
4027
4267
|
if (pos === null) return [];
|
|
4028
4268
|
const hits = findAllScopesAt(state.document, pos);
|
|
4029
4269
|
return projectScopeQueryResults(
|
|
4030
|
-
{
|
|
4270
|
+
{
|
|
4271
|
+
overlay: workflowOverlay,
|
|
4272
|
+
entries: workflowMetadataEntries,
|
|
4273
|
+
document: state.document,
|
|
4274
|
+
markerBackedScopeIds,
|
|
4275
|
+
},
|
|
4031
4276
|
hits.map((h) => h.scopeId),
|
|
4032
4277
|
options,
|
|
4033
4278
|
);
|
|
@@ -4036,7 +4281,12 @@ export function createDocumentRuntime(
|
|
|
4036
4281
|
if (range.kind !== "range") return [];
|
|
4037
4282
|
const hits = findScopesIntersecting(state.document, range.from, range.to, options?.mode);
|
|
4038
4283
|
return projectScopeQueryResults(
|
|
4039
|
-
{
|
|
4284
|
+
{
|
|
4285
|
+
overlay: workflowOverlay,
|
|
4286
|
+
entries: workflowMetadataEntries,
|
|
4287
|
+
document: state.document,
|
|
4288
|
+
markerBackedScopeIds,
|
|
4289
|
+
},
|
|
4040
4290
|
hits.map((h) => h.scopeId),
|
|
4041
4291
|
options,
|
|
4042
4292
|
);
|
|
@@ -4249,6 +4499,8 @@ export function createDocumentRuntime(
|
|
|
4249
4499
|
state = finalizeState(transaction.nextState, transaction.markDirty, clock());
|
|
4250
4500
|
perfCounters.increment("commit.finalizeState.us", Math.round((performance.now() - tFinalize0) * 1000));
|
|
4251
4501
|
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
4502
|
+
syncMarkerBackedScopeIds(state.document, workflowOverlay);
|
|
4503
|
+
const detachedWorkflowScopeWarnings = syncDetachedWorkflowScopeWarningsInState();
|
|
4252
4504
|
|
|
4253
4505
|
const tInvalidate0 = performance.now();
|
|
4254
4506
|
if (transaction.markDirty && transaction.mapping.steps.length > 0) {
|
|
@@ -4319,7 +4571,20 @@ export function createDocumentRuntime(
|
|
|
4319
4571
|
perfCounters.increment("commit.refresh.us", Math.round((performance.now() - tRefresh0) * 1000));
|
|
4320
4572
|
|
|
4321
4573
|
const tNotify0 = performance.now();
|
|
4322
|
-
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
|
+
});
|
|
4323
4588
|
perfCounters.increment("commit.notify.us", Math.round((performance.now() - tNotify0) * 1000));
|
|
4324
4589
|
perfCounters.increment("commit.total.us", Math.round((performance.now() - tApply0) * 1000));
|
|
4325
4590
|
}
|
|
@@ -4948,10 +5213,18 @@ export function createDocumentRuntime(
|
|
|
4948
5213
|
function applyRuntimeStateOverlayCommand(
|
|
4949
5214
|
command: RuntimeStateOverlayCommand,
|
|
4950
5215
|
): void {
|
|
5216
|
+
let detachedWorkflowScopeWarnings:
|
|
5217
|
+
| {
|
|
5218
|
+
added: InternalEditorWarning[];
|
|
5219
|
+
cleared: Array<{ warningId: string; code: InternalEditorWarning["code"] }>;
|
|
5220
|
+
}
|
|
5221
|
+
| null = null;
|
|
4951
5222
|
switch (command.type) {
|
|
4952
5223
|
case "workflow.set-overlay": {
|
|
4953
5224
|
workflowOverlay = structuredClone(command.overlay);
|
|
5225
|
+
syncMarkerBackedScopeIds(state.document, workflowOverlay);
|
|
4954
5226
|
cachedNormalizedWorkflowOverlay = undefined;
|
|
5227
|
+
detachedWorkflowScopeWarnings = syncDetachedWorkflowScopeWarningsInState();
|
|
4955
5228
|
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
4956
5229
|
const snapshot = deriveWorkflowScopeSnapshot()!;
|
|
4957
5230
|
emit({
|
|
@@ -4970,7 +5243,9 @@ export function createDocumentRuntime(
|
|
|
4970
5243
|
}
|
|
4971
5244
|
case "workflow.clear-overlay": {
|
|
4972
5245
|
workflowOverlay = null;
|
|
5246
|
+
syncMarkerBackedScopeIds(state.document, workflowOverlay);
|
|
4973
5247
|
cachedNormalizedWorkflowOverlay = undefined;
|
|
5248
|
+
detachedWorkflowScopeWarnings = syncDetachedWorkflowScopeWarningsInState();
|
|
4974
5249
|
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
4975
5250
|
emit({
|
|
4976
5251
|
type: "workflow_active_work_item_changed",
|
|
@@ -5045,6 +5320,25 @@ export function createDocumentRuntime(
|
|
|
5045
5320
|
break;
|
|
5046
5321
|
}
|
|
5047
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
|
+
}
|
|
5048
5342
|
for (const listener of listeners) {
|
|
5049
5343
|
listener();
|
|
5050
5344
|
}
|
|
@@ -5533,11 +5827,42 @@ function toPublicCompatibilityFeatureEntry(
|
|
|
5533
5827
|
}
|
|
5534
5828
|
|
|
5535
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
|
+
})();
|
|
5536
5860
|
return {
|
|
5537
5861
|
...warning,
|
|
5538
5862
|
affectedAnchor: warning.affectedAnchor
|
|
5539
5863
|
? toPublicAnchorProjection(warning.affectedAnchor)
|
|
5540
5864
|
: undefined,
|
|
5865
|
+
diagnostic,
|
|
5541
5866
|
};
|
|
5542
5867
|
}
|
|
5543
5868
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
EditorAnchorProjection,
|
|
2
3
|
EditorStoryTarget,
|
|
3
4
|
ScopeQueryFilter,
|
|
4
5
|
ScopeQueryResult,
|
|
@@ -31,10 +32,97 @@ function storyTargetsEqual(a: EditorStoryTarget, b: EditorStoryTarget): boolean
|
|
|
31
32
|
|
|
32
33
|
const MAIN_STORY: EditorStoryTarget = { kind: "main" };
|
|
33
34
|
|
|
35
|
+
function workflowAnchorsEqual(
|
|
36
|
+
left: EditorAnchorProjection,
|
|
37
|
+
right: EditorAnchorProjection,
|
|
38
|
+
): boolean {
|
|
39
|
+
if (left.kind !== right.kind) return false;
|
|
40
|
+
switch (left.kind) {
|
|
41
|
+
case "range":
|
|
42
|
+
return (
|
|
43
|
+
right.kind === "range" &&
|
|
44
|
+
left.from === right.from &&
|
|
45
|
+
left.to === right.to &&
|
|
46
|
+
left.assoc.start === right.assoc.start &&
|
|
47
|
+
left.assoc.end === right.assoc.end
|
|
48
|
+
);
|
|
49
|
+
case "node":
|
|
50
|
+
return right.kind === "node" && left.at === right.at;
|
|
51
|
+
case "detached":
|
|
52
|
+
return (
|
|
53
|
+
right.kind === "detached" &&
|
|
54
|
+
left.reason === right.reason &&
|
|
55
|
+
left.lastKnownRange.from === right.lastKnownRange.from &&
|
|
56
|
+
left.lastKnownRange.to === right.lastKnownRange.to
|
|
57
|
+
);
|
|
58
|
+
default:
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildScopeIdCounts(overlay: WorkflowOverlay): Map<string, number> {
|
|
64
|
+
const counts = new Map<string, number>();
|
|
65
|
+
for (const scope of overlay.scopes) {
|
|
66
|
+
counts.set(scope.scopeId, (counts.get(scope.scopeId) ?? 0) + 1);
|
|
67
|
+
}
|
|
68
|
+
return counts;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeScopeAnchor(
|
|
72
|
+
scope: WorkflowScope,
|
|
73
|
+
scopeIdCounts: ReadonlyMap<string, number>,
|
|
74
|
+
locations: ReadonlyMap<string, { startPos?: number; endPos?: number }>,
|
|
75
|
+
markerBackedScopeIds: ReadonlySet<string>,
|
|
76
|
+
): WorkflowScope {
|
|
77
|
+
if ((scopeIdCounts.get(scope.scopeId) ?? 0) !== 1) {
|
|
78
|
+
return scope;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const location = locations.get(scope.scopeId);
|
|
82
|
+
let nextAnchor: EditorAnchorProjection | null = null;
|
|
83
|
+
if (
|
|
84
|
+
location &&
|
|
85
|
+
location.startPos !== undefined &&
|
|
86
|
+
location.endPos !== undefined
|
|
87
|
+
) {
|
|
88
|
+
nextAnchor = {
|
|
89
|
+
kind: "range",
|
|
90
|
+
from: Math.min(location.startPos, location.endPos),
|
|
91
|
+
to: Math.max(location.startPos, location.endPos),
|
|
92
|
+
assoc: { start: -1, end: 1 },
|
|
93
|
+
};
|
|
94
|
+
} else if (markerBackedScopeIds.has(scope.scopeId)) {
|
|
95
|
+
const lastKnownRange =
|
|
96
|
+
scope.anchor.kind === "range"
|
|
97
|
+
? { from: scope.anchor.from, to: scope.anchor.to }
|
|
98
|
+
: scope.anchor.kind === "node"
|
|
99
|
+
? { from: scope.anchor.at, to: scope.anchor.at }
|
|
100
|
+
: scope.anchor.lastKnownRange;
|
|
101
|
+
nextAnchor = {
|
|
102
|
+
kind: "detached",
|
|
103
|
+
reason:
|
|
104
|
+
location && (location.startPos !== undefined || location.endPos !== undefined)
|
|
105
|
+
? "deleted"
|
|
106
|
+
: "invalidatedByStructureChange",
|
|
107
|
+
lastKnownRange,
|
|
108
|
+
};
|
|
109
|
+
} else {
|
|
110
|
+
return scope;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return workflowAnchorsEqual(scope.anchor, nextAnchor)
|
|
114
|
+
? scope
|
|
115
|
+
: {
|
|
116
|
+
...scope,
|
|
117
|
+
anchor: nextAnchor,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
34
121
|
export interface ScopeQueryInputs {
|
|
35
122
|
readonly overlay: WorkflowOverlay | null;
|
|
36
123
|
readonly entries: readonly WorkflowMetadataEntry[];
|
|
37
124
|
readonly document: Pick<CanonicalDocument, "content"> | CanonicalDocumentEnvelope;
|
|
125
|
+
readonly markerBackedScopeIds?: ReadonlySet<string>;
|
|
38
126
|
}
|
|
39
127
|
|
|
40
128
|
/**
|
|
@@ -53,9 +141,17 @@ export function projectScopeQueryResults(
|
|
|
53
141
|
if (!overlay) return [];
|
|
54
142
|
const includeHidden = options.includeHidden === true;
|
|
55
143
|
const includeInvisible = options.includeInvisible === true;
|
|
144
|
+
const scopeIdCounts = buildScopeIdCounts(overlay);
|
|
145
|
+
const locations = collectScopeLocations(inputs.document);
|
|
146
|
+
const markerBackedScopeIds = inputs.markerBackedScopeIds ?? new Set<string>();
|
|
56
147
|
|
|
57
148
|
const scopesById = new Map<string, WorkflowScope>();
|
|
58
|
-
for (const scope of overlay.scopes)
|
|
149
|
+
for (const scope of overlay.scopes) {
|
|
150
|
+
scopesById.set(
|
|
151
|
+
scope.scopeId,
|
|
152
|
+
normalizeScopeAnchor(scope, scopeIdCounts, locations, markerBackedScopeIds),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
59
155
|
|
|
60
156
|
const entriesByScope = new Map<string, WorkflowMetadataEntry[]>();
|
|
61
157
|
for (const entry of inputs.entries) {
|
|
@@ -128,6 +224,8 @@ export function queryScopes(
|
|
|
128
224
|
}
|
|
129
225
|
|
|
130
226
|
const locations = collectScopeLocations(inputs.document);
|
|
227
|
+
const scopeIdCounts = buildScopeIdCounts(overlay);
|
|
228
|
+
const markerBackedScopeIds = inputs.markerBackedScopeIds ?? new Set<string>();
|
|
131
229
|
|
|
132
230
|
const candidates: Array<{ scope: WorkflowScope; startPos: number }> = [];
|
|
133
231
|
for (const scope of overlay.scopes) {
|
|
@@ -162,7 +260,10 @@ export function queryScopes(
|
|
|
162
260
|
const loc = locations.get(scope.scopeId);
|
|
163
261
|
const startPos =
|
|
164
262
|
loc?.startPos ?? loc?.endPos ?? Number.POSITIVE_INFINITY;
|
|
165
|
-
candidates.push({
|
|
263
|
+
candidates.push({
|
|
264
|
+
scope: normalizeScopeAnchor(scope, scopeIdCounts, locations, markerBackedScopeIds),
|
|
265
|
+
startPos,
|
|
266
|
+
});
|
|
166
267
|
}
|
|
167
268
|
|
|
168
269
|
candidates.sort((a, b) => {
|