@beyondwork/docx-react-component 1.0.82 → 1.0.84
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 +48 -4
- package/src/api/v3/_runtime-handle.ts +4 -0
- package/src/api/v3/runtime/workflow.ts +154 -6
- package/src/io/export/serialize-main-document.ts +72 -6
- package/src/io/ooxml/workflow-payload-validator.ts +24 -0
- package/src/io/ooxml/workflow-payload.ts +12 -0
- package/src/runtime/document-runtime.ts +41 -14
- package/src/runtime/scopes/_scope-dependencies.ts +1 -0
- package/src/runtime/scopes/action-validation.ts +54 -45
- package/src/runtime/scopes/workflow-overlap.ts +41 -9
- package/src/runtime/workflow/coordinator.ts +66 -14
- package/src/runtime/workflow/scope-writer.ts +83 -5
- package/src/shell/session-bootstrap.ts +2 -0
- package/src/ui/WordReviewEditor.tsx +176 -1
- package/src/ui-tailwind/review-workspace/use-chrome-policy.ts +32 -6
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +21 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +1 -0
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.84",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": [
|
package/src/api/public-types.ts
CHANGED
|
@@ -2512,6 +2512,16 @@ export interface AddScopeParams {
|
|
|
2512
2512
|
storyTarget?: EditorStoryTarget;
|
|
2513
2513
|
/** Optional display label for the scope card / rail. */
|
|
2514
2514
|
label?: string;
|
|
2515
|
+
/**
|
|
2516
|
+
* Optional per-scope visibility. Controls chrome/query presentation only;
|
|
2517
|
+
* edit enforcement is controlled by `guardPolicy`.
|
|
2518
|
+
*/
|
|
2519
|
+
visibility?: ScopeVisibility;
|
|
2520
|
+
/**
|
|
2521
|
+
* Optional edit-enforcement posture. Absent means advisory for
|
|
2522
|
+
* edit/suggest/comment scopes and read-only for `mode: "view"`.
|
|
2523
|
+
*/
|
|
2524
|
+
guardPolicy?: WorkflowScopeGuardPolicy;
|
|
2515
2525
|
}
|
|
2516
2526
|
|
|
2517
2527
|
export interface AddScopeResult {
|
|
@@ -2591,6 +2601,19 @@ export interface ExportResult {
|
|
|
2591
2601
|
|
|
2592
2602
|
export type WorkflowScopeMode = "edit" | "suggest" | "comment" | "view";
|
|
2593
2603
|
|
|
2604
|
+
/**
|
|
2605
|
+
* Explicit scope enforcement axis. Visibility is presentation-only.
|
|
2606
|
+
*
|
|
2607
|
+
* - `"none"`: advisory/metadata/chrome scope; direct editing is unaffected.
|
|
2608
|
+
* - `"insert-only"`: allowlist scope; when any active insert-only scope
|
|
2609
|
+
* exists, selection-driven edits outside insert-only scopes are blocked.
|
|
2610
|
+
* - `"read-only"`: deny edits inside this scope; outside content is unaffected.
|
|
2611
|
+
*
|
|
2612
|
+
* When absent, `mode: "view"` scopes default to `"read-only"` for
|
|
2613
|
+
* compatibility; all other modes default to `"none"`.
|
|
2614
|
+
*/
|
|
2615
|
+
export type WorkflowScopeGuardPolicy = "none" | "insert-only" | "read-only";
|
|
2616
|
+
|
|
2594
2617
|
/**
|
|
2595
2618
|
* §C7 — Local chrome visibility mode. Never collab-replicated.
|
|
2596
2619
|
* - `"all"`: show all scope rail entries + decorations (default).
|
|
@@ -2615,8 +2638,8 @@ export interface ScopeChromeVisibilityState {
|
|
|
2615
2638
|
*
|
|
2616
2639
|
* - `"visible"`: rail entry + card + inline decoration (current behavior).
|
|
2617
2640
|
* - `"hidden"`: in the rail but muted; card opens on explicit click; no decoration.
|
|
2618
|
-
* - `"invisible"`: never rendered; only queryable via API.
|
|
2619
|
-
* to InteractionGuard
|
|
2641
|
+
* - `"invisible"`: never rendered; only queryable via API. Visibility never
|
|
2642
|
+
* contributes to InteractionGuard.
|
|
2620
2643
|
*/
|
|
2621
2644
|
export type ScopeVisibility = "visible" | "hidden" | "invisible";
|
|
2622
2645
|
|
|
@@ -2639,6 +2662,12 @@ export interface WorkflowScope {
|
|
|
2639
2662
|
domain?: "legal" | "commercial" | "finance" | "other";
|
|
2640
2663
|
metadataRefs?: string[];
|
|
2641
2664
|
metadata?: WorkflowScopeMetadataField[];
|
|
2665
|
+
/**
|
|
2666
|
+
* Explicit edit-enforcement posture. Absent means:
|
|
2667
|
+
* - `mode: "view"` -> `"read-only"` (legacy read-only behavior).
|
|
2668
|
+
* - all other modes -> `"none"` (advisory/chrome/metadata only).
|
|
2669
|
+
*/
|
|
2670
|
+
guardPolicy?: WorkflowScopeGuardPolicy;
|
|
2642
2671
|
/**
|
|
2643
2672
|
* Schema 1.1 — override the overlay default for this scope.
|
|
2644
2673
|
* `"inherit"` defers to the overlay; absent is equivalent to
|
|
@@ -4571,8 +4600,9 @@ export interface WordReviewEditorRef {
|
|
|
4571
4600
|
removeScope(scopeId: string): void;
|
|
4572
4601
|
/**
|
|
4573
4602
|
* §C8 — Convenience: adds a scope with `visibility: "invisible"` atomically.
|
|
4574
|
-
* Mode defaults to `"comment"
|
|
4575
|
-
*
|
|
4603
|
+
* Mode defaults to `"comment"`. Visibility is presentation-only; edit
|
|
4604
|
+
* gating is controlled by `guardPolicy` (`mode: "view"` still defaults to
|
|
4605
|
+
* read-only for compatibility unless `guardPolicy: "none"` is set).
|
|
4576
4606
|
*/
|
|
4577
4607
|
addInvisibleScope(
|
|
4578
4608
|
params: Omit<AddScopeParams, "mode"> & { mode?: WorkflowScopeMode },
|
|
@@ -4587,6 +4617,20 @@ export interface WordReviewEditorRef {
|
|
|
4587
4617
|
* scopes or when the `visibility` field has never been set.
|
|
4588
4618
|
*/
|
|
4589
4619
|
getScopeVisibility(scopeId: string): ScopeVisibility;
|
|
4620
|
+
/**
|
|
4621
|
+
* Set a scope's edit-enforcement posture. Collab-replicated through the
|
|
4622
|
+
* workflow overlay; visibility remains purely presentational.
|
|
4623
|
+
*/
|
|
4624
|
+
setScopeGuardPolicy(
|
|
4625
|
+
scopeId: string,
|
|
4626
|
+
guardPolicy: WorkflowScopeGuardPolicy,
|
|
4627
|
+
): void;
|
|
4628
|
+
/**
|
|
4629
|
+
* Get a scope's effective guard policy. Returns `"read-only"` for legacy
|
|
4630
|
+
* `mode: "view"` scopes with no explicit policy and `"none"` for unknown
|
|
4631
|
+
* scopes.
|
|
4632
|
+
*/
|
|
4633
|
+
getScopeGuardPolicy(scopeId: string): WorkflowScopeGuardPolicy;
|
|
4590
4634
|
/**
|
|
4591
4635
|
* §C7 — Set the local chrome-visibility state. Local view-state only —
|
|
4592
4636
|
* never collab-replicated. Controls whether the scope rail / decorations
|
|
@@ -70,6 +70,8 @@ export type RuntimeApiHandle = Pick<
|
|
|
70
70
|
// live seam. `getWorkflowMetadataSnapshot` is the read side the
|
|
71
71
|
// metadata writer inspects before merging its entry.
|
|
72
72
|
| "addScope"
|
|
73
|
+
| "setScopeGuardPolicy"
|
|
74
|
+
| "getScopeGuardPolicy"
|
|
73
75
|
| "setWorkflowMetadataEntries"
|
|
74
76
|
| "getWorkflowMetadataSnapshot"
|
|
75
77
|
// W10 overlay-visibility policy (state-classes X1). Class-A canonical
|
|
@@ -155,6 +157,8 @@ export const RUNTIME_API_HANDLE_SHAPE_CHECK: Record<keyof RuntimeApiHandle, true
|
|
|
155
157
|
getInteractionGuardSnapshot: true,
|
|
156
158
|
getWorkflowOverlay: true,
|
|
157
159
|
addScope: true,
|
|
160
|
+
setScopeGuardPolicy: true,
|
|
161
|
+
getScopeGuardPolicy: true,
|
|
158
162
|
setWorkflowMetadataEntries: true,
|
|
159
163
|
getWorkflowMetadataSnapshot: true,
|
|
160
164
|
getVisibilityPolicy: true,
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* @endStateApi v3 — `runtime.workflow` family.
|
|
3
3
|
*
|
|
4
4
|
* queryScopes (live) / getMarkup (live) / getGuard (live) /
|
|
5
|
-
* createScope (live-with-adapter) /
|
|
5
|
+
* createScope (live-with-adapter) / setScopeGuardPolicy (live) /
|
|
6
|
+
* attachMetadata (live-with-adapter) /
|
|
6
7
|
* getVisibilityPolicy · getVisibilityPolicies · setVisibilityPolicy ·
|
|
7
8
|
* clearVisibilityPolicy (live — W10 state-classes X1) /
|
|
8
9
|
* scopeTags (live — coord-10 L11-4 tag-catalog read).
|
|
@@ -14,6 +15,8 @@ import type {
|
|
|
14
15
|
EditorStoryTarget,
|
|
15
16
|
OverlayKind,
|
|
16
17
|
OverlayVisibilityPolicy,
|
|
18
|
+
ScopeVisibility,
|
|
19
|
+
WorkflowScopeGuardPolicy,
|
|
17
20
|
WorkflowMarkupModePolicy,
|
|
18
21
|
} from "../../public-types.ts";
|
|
19
22
|
import { emitUxResponse } from "../_ux-response.ts";
|
|
@@ -82,6 +85,13 @@ export interface CreateScopeInput {
|
|
|
82
85
|
readonly blockId: string;
|
|
83
86
|
readonly mode?: "edit" | "suggest" | "comment" | "view";
|
|
84
87
|
readonly label?: string;
|
|
88
|
+
/** Per-scope visibility. Controls chrome/query presentation only. */
|
|
89
|
+
readonly visibility?: ScopeVisibility;
|
|
90
|
+
/**
|
|
91
|
+
* Explicit edit-enforcement posture. Use `"insert-only"` for
|
|
92
|
+
* template-slot allowlists; use `"read-only"` to lock the scope range.
|
|
93
|
+
*/
|
|
94
|
+
readonly guardPolicy?: WorkflowScopeGuardPolicy;
|
|
85
95
|
/**
|
|
86
96
|
* Coord-06 §13d — per-scope edge stickiness. Defaults to
|
|
87
97
|
* `{ start: 1, end: -1 }` (greedy — absorbs boundary inserts, the
|
|
@@ -112,10 +122,24 @@ export interface CreateScopeInput {
|
|
|
112
122
|
| "runtime-handle";
|
|
113
123
|
}
|
|
114
124
|
|
|
115
|
-
export
|
|
116
|
-
readonly scopeId: string;
|
|
117
|
-
readonly
|
|
118
|
-
|
|
125
|
+
export type CreateScopeResult =
|
|
126
|
+
| { readonly scopeId: string; readonly status: "created" }
|
|
127
|
+
| { readonly scopeId: ""; readonly status: "block-not-found" }
|
|
128
|
+
| {
|
|
129
|
+
readonly scopeId: "";
|
|
130
|
+
readonly status: "range-invalid";
|
|
131
|
+
readonly reason:
|
|
132
|
+
| "range-exceeds-story-length"
|
|
133
|
+
| "non-paragraph-target"
|
|
134
|
+
| "empty-document";
|
|
135
|
+
readonly from: number;
|
|
136
|
+
readonly to: number;
|
|
137
|
+
readonly storyLength: number;
|
|
138
|
+
readonly blockIndex?: number;
|
|
139
|
+
readonly blockKind?: string;
|
|
140
|
+
readonly message: string;
|
|
141
|
+
readonly nextStep: string;
|
|
142
|
+
};
|
|
119
143
|
|
|
120
144
|
/**
|
|
121
145
|
* Input for `createScopeFromAnchor` — sub-block marker-backed scope.
|
|
@@ -144,6 +168,8 @@ export interface CreateScopeFromAnchorInput {
|
|
|
144
168
|
};
|
|
145
169
|
readonly mode?: "edit" | "suggest" | "comment" | "view";
|
|
146
170
|
readonly label?: string;
|
|
171
|
+
readonly visibility?: ScopeVisibility;
|
|
172
|
+
readonly guardPolicy?: WorkflowScopeGuardPolicy;
|
|
147
173
|
readonly assoc?: { readonly start: -1 | 1; readonly end: -1 | 1 };
|
|
148
174
|
readonly stableRefHint?:
|
|
149
175
|
| "scope-id"
|
|
@@ -174,6 +200,22 @@ export type CreateScopeFromAnchorResult =
|
|
|
174
200
|
readonly nextStep: string;
|
|
175
201
|
};
|
|
176
202
|
|
|
203
|
+
export interface SetScopeGuardPolicyInput {
|
|
204
|
+
readonly scopeId: string;
|
|
205
|
+
readonly guardPolicy: WorkflowScopeGuardPolicy;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export type SetScopeGuardPolicyResult =
|
|
209
|
+
| {
|
|
210
|
+
readonly status: "updated";
|
|
211
|
+
readonly scopeId: string;
|
|
212
|
+
readonly guardPolicy: WorkflowScopeGuardPolicy;
|
|
213
|
+
}
|
|
214
|
+
| {
|
|
215
|
+
readonly status: "scope-not-found";
|
|
216
|
+
readonly scopeId: string;
|
|
217
|
+
};
|
|
218
|
+
|
|
177
219
|
export const createScopeMetadata: ApiV3FnMetadata = {
|
|
178
220
|
name: "runtime.workflow.createScope",
|
|
179
221
|
status: "live-with-adapter",
|
|
@@ -216,6 +258,51 @@ export const createScopeFromAnchorMetadata: ApiV3FnMetadata = {
|
|
|
216
258
|
"§Runtime API § runtime.workflow.createScopeFromAnchor. Companion to createScope({blockId}) for sub-block ranges. Consumes from/to once to plant inline scope_marker_start/end; the returned scopeId is the durable reference (KI-P9).",
|
|
217
259
|
};
|
|
218
260
|
|
|
261
|
+
export const getScopeGuardPolicyMetadata: ApiV3FnMetadata = {
|
|
262
|
+
name: "runtime.workflow.getScopeGuardPolicy",
|
|
263
|
+
status: "live",
|
|
264
|
+
sourceLayer: "workflow-review",
|
|
265
|
+
liveEvidence: {
|
|
266
|
+
runnerTest: "test/api/v3/workflow-create-scope-live.test.ts",
|
|
267
|
+
commit: "guard-policy-setter",
|
|
268
|
+
},
|
|
269
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
270
|
+
agentMetadata: {
|
|
271
|
+
readOrMutate: "read",
|
|
272
|
+
boundedScope: "scope",
|
|
273
|
+
auditCategory: "scope-policy",
|
|
274
|
+
},
|
|
275
|
+
stateClass: "A-canonical",
|
|
276
|
+
persistsTo: "customXml",
|
|
277
|
+
broadcastsVia: "crdt",
|
|
278
|
+
rwdReference: "§Runtime API § runtime.workflow.getScopeGuardPolicy",
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export const setScopeGuardPolicyMetadata: ApiV3FnMetadata = {
|
|
282
|
+
name: "runtime.workflow.setScopeGuardPolicy",
|
|
283
|
+
status: "live",
|
|
284
|
+
sourceLayer: "workflow-review",
|
|
285
|
+
liveEvidence: {
|
|
286
|
+
runnerTest: "test/api/v3/workflow-create-scope-live.test.ts",
|
|
287
|
+
commit: "guard-policy-setter",
|
|
288
|
+
},
|
|
289
|
+
uxIntent: {
|
|
290
|
+
uiVisible: true,
|
|
291
|
+
expectsUxResponse: "inline-change",
|
|
292
|
+
expectedDelta:
|
|
293
|
+
"scope edit-enforcement policy updates and interaction guard recalculates",
|
|
294
|
+
},
|
|
295
|
+
agentMetadata: {
|
|
296
|
+
readOrMutate: "mutate",
|
|
297
|
+
boundedScope: "scope",
|
|
298
|
+
auditCategory: "scope-policy",
|
|
299
|
+
},
|
|
300
|
+
stateClass: "A-canonical",
|
|
301
|
+
persistsTo: "customXml",
|
|
302
|
+
broadcastsVia: "crdt",
|
|
303
|
+
rwdReference: "§Runtime API § runtime.workflow.setScopeGuardPolicy",
|
|
304
|
+
};
|
|
305
|
+
|
|
219
306
|
export interface AttachMetadataInput {
|
|
220
307
|
readonly scopeId: string;
|
|
221
308
|
readonly metadataId: string;
|
|
@@ -476,6 +563,8 @@ export function createWorkflowFamily(runtime: RuntimeApiHandle) {
|
|
|
476
563
|
blockId: input.blockId,
|
|
477
564
|
mode: input.mode,
|
|
478
565
|
label: input.label,
|
|
566
|
+
...(input.visibility ? { visibility: input.visibility } : {}),
|
|
567
|
+
...(input.guardPolicy ? { guardPolicy: input.guardPolicy } : {}),
|
|
479
568
|
...(input.assoc ? { assoc: input.assoc } : {}),
|
|
480
569
|
...(input.stableRefHint
|
|
481
570
|
? { stableRefHint: input.stableRefHint }
|
|
@@ -493,11 +582,68 @@ export function createWorkflowFamily(runtime: RuntimeApiHandle) {
|
|
|
493
582
|
: undefined,
|
|
494
583
|
});
|
|
495
584
|
if (adapterResult.status !== "created") {
|
|
496
|
-
|
|
585
|
+
if (adapterResult.status === "block-not-found") {
|
|
586
|
+
return { scopeId: "", status: "block-not-found" };
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
scopeId: "",
|
|
590
|
+
status: "range-invalid",
|
|
591
|
+
reason: adapterResult.reason,
|
|
592
|
+
from: adapterResult.from,
|
|
593
|
+
to: adapterResult.to,
|
|
594
|
+
storyLength: adapterResult.storyLength,
|
|
595
|
+
...(adapterResult.blockIndex !== undefined
|
|
596
|
+
? { blockIndex: adapterResult.blockIndex }
|
|
597
|
+
: {}),
|
|
598
|
+
...(adapterResult.blockKind !== undefined
|
|
599
|
+
? { blockKind: adapterResult.blockKind }
|
|
600
|
+
: {}),
|
|
601
|
+
message: adapterResult.message,
|
|
602
|
+
nextStep: adapterResult.nextStep,
|
|
603
|
+
};
|
|
497
604
|
}
|
|
498
605
|
return { scopeId: adapterResult.scopeId, status: "created" };
|
|
499
606
|
},
|
|
500
607
|
|
|
608
|
+
getScopeGuardPolicy(scopeId: string): WorkflowScopeGuardPolicy {
|
|
609
|
+
// @endStateApi — live.
|
|
610
|
+
return runtime.getScopeGuardPolicy(scopeId);
|
|
611
|
+
},
|
|
612
|
+
|
|
613
|
+
setScopeGuardPolicy(
|
|
614
|
+
input: SetScopeGuardPolicyInput,
|
|
615
|
+
): SetScopeGuardPolicyResult {
|
|
616
|
+
// @endStateApi — live. Mutates the scope's explicit guard axis in the
|
|
617
|
+
// workflow overlay. Visibility stays presentation-only.
|
|
618
|
+
const overlay = runtime.getWorkflowOverlay();
|
|
619
|
+
const existingScope = overlay?.scopes.find(
|
|
620
|
+
(scope) => scope.scopeId === input.scopeId,
|
|
621
|
+
);
|
|
622
|
+
if (!existingScope) {
|
|
623
|
+
return { status: "scope-not-found", scopeId: input.scopeId };
|
|
624
|
+
}
|
|
625
|
+
runtime.setScopeGuardPolicy(input.scopeId, input.guardPolicy);
|
|
626
|
+
emitUxResponse(runtime, {
|
|
627
|
+
apiFn: setScopeGuardPolicyMetadata.name,
|
|
628
|
+
intent: setScopeGuardPolicyMetadata.uxIntent.expectedDelta ?? "",
|
|
629
|
+
mockOrLive: "live",
|
|
630
|
+
uiVisible: true,
|
|
631
|
+
expectedDelta: setScopeGuardPolicyMetadata.uxIntent.expectedDelta,
|
|
632
|
+
actualDelta: {
|
|
633
|
+
kind: "inline-change",
|
|
634
|
+
payload: {
|
|
635
|
+
scopeId: input.scopeId,
|
|
636
|
+
guardPolicy: input.guardPolicy,
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
return {
|
|
641
|
+
status: "updated",
|
|
642
|
+
scopeId: input.scopeId,
|
|
643
|
+
guardPolicy: input.guardPolicy,
|
|
644
|
+
};
|
|
645
|
+
},
|
|
646
|
+
|
|
501
647
|
createScopeFromAnchor(
|
|
502
648
|
input: CreateScopeFromAnchorInput,
|
|
503
649
|
): CreateScopeFromAnchorResult {
|
|
@@ -510,6 +656,8 @@ export function createWorkflowFamily(runtime: RuntimeApiHandle) {
|
|
|
510
656
|
anchor: input.anchor,
|
|
511
657
|
mode: input.mode,
|
|
512
658
|
label: input.label,
|
|
659
|
+
...(input.visibility ? { visibility: input.visibility } : {}),
|
|
660
|
+
...(input.guardPolicy ? { guardPolicy: input.guardPolicy } : {}),
|
|
513
661
|
...(input.assoc ? { assoc: input.assoc } : {}),
|
|
514
662
|
...(input.stableRefHint
|
|
515
663
|
? { stableRefHint: input.stableRefHint }
|
|
@@ -89,6 +89,14 @@ interface SerializationState {
|
|
|
89
89
|
usedTextIds: Set<string>;
|
|
90
90
|
/** Deterministic PRNG-less counter used when we mint fresh ids. */
|
|
91
91
|
mintedParaIdCounter: number;
|
|
92
|
+
/**
|
|
93
|
+
* Scope markers export as OOXML bookmarks. `w:bookmarkStart/@w:id` is a
|
|
94
|
+
* decimal number in WordprocessingML, so scope ids cannot be written into
|
|
95
|
+
* it directly.
|
|
96
|
+
*/
|
|
97
|
+
usedBookmarkIds: Set<string>;
|
|
98
|
+
scopeBookmarkIds: Map<string, string>;
|
|
99
|
+
nextScopeBookmarkId: number;
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
interface InlineSerializationResult {
|
|
@@ -117,6 +125,61 @@ function serializePerformanceNow(): number {
|
|
|
117
125
|
return Date.now();
|
|
118
126
|
}
|
|
119
127
|
|
|
128
|
+
function collectNumericBookmarkIds(content: DocumentRootNode): Set<string> {
|
|
129
|
+
const used = new Set<string>();
|
|
130
|
+
const visit = (node: unknown): void => {
|
|
131
|
+
if (!node || typeof node !== "object") return;
|
|
132
|
+
const typed = node as {
|
|
133
|
+
type?: string;
|
|
134
|
+
bookmarkId?: unknown;
|
|
135
|
+
children?: unknown;
|
|
136
|
+
rows?: unknown;
|
|
137
|
+
cells?: unknown;
|
|
138
|
+
};
|
|
139
|
+
if (
|
|
140
|
+
(typed.type === "bookmark_start" || typed.type === "bookmark_end") &&
|
|
141
|
+
typeof typed.bookmarkId === "string"
|
|
142
|
+
) {
|
|
143
|
+
const normalized = normalizeDecimalBookmarkId(typed.bookmarkId);
|
|
144
|
+
if (normalized !== null) used.add(normalized);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (Array.isArray(typed.children)) {
|
|
148
|
+
for (const child of typed.children) visit(child);
|
|
149
|
+
}
|
|
150
|
+
if (Array.isArray(typed.rows)) {
|
|
151
|
+
for (const row of typed.rows) visit(row);
|
|
152
|
+
}
|
|
153
|
+
if (Array.isArray(typed.cells)) {
|
|
154
|
+
for (const cell of typed.cells) visit(cell);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
visit(content);
|
|
159
|
+
return used;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function normalizeDecimalBookmarkId(id: string): string | null {
|
|
163
|
+
if (!/^\d+$/u.test(id)) return null;
|
|
164
|
+
const stripped = id.replace(/^0+/u, "");
|
|
165
|
+
return stripped.length > 0 ? stripped : "0";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function scopeBookmarkIdFor(state: SerializationState, scopeId: string): string {
|
|
169
|
+
const existing = state.scopeBookmarkIds.get(scopeId);
|
|
170
|
+
if (existing !== undefined) return existing;
|
|
171
|
+
|
|
172
|
+
let next = state.nextScopeBookmarkId;
|
|
173
|
+
while (state.usedBookmarkIds.has(String(next))) {
|
|
174
|
+
next += 1;
|
|
175
|
+
}
|
|
176
|
+
const bookmarkId = String(next);
|
|
177
|
+
state.nextScopeBookmarkId = next + 1;
|
|
178
|
+
state.usedBookmarkIds.add(bookmarkId);
|
|
179
|
+
state.scopeBookmarkIds.set(scopeId, bookmarkId);
|
|
180
|
+
return bookmarkId;
|
|
181
|
+
}
|
|
182
|
+
|
|
120
183
|
export function serializeMainDocument(
|
|
121
184
|
content: DocumentRootNode,
|
|
122
185
|
preservation: PreservationStore = { opaqueFragments: {}, packageParts: {} },
|
|
@@ -150,6 +213,9 @@ export function serializeMainDocument(
|
|
|
150
213
|
usedParaIds: new Set<string>(),
|
|
151
214
|
usedTextIds: new Set<string>(),
|
|
152
215
|
mintedParaIdCounter: 0,
|
|
216
|
+
usedBookmarkIds: collectNumericBookmarkIds(content),
|
|
217
|
+
scopeBookmarkIds: new Map<string, string>(),
|
|
218
|
+
nextScopeBookmarkId: 100000,
|
|
153
219
|
};
|
|
154
220
|
const suffix = `</w:body>\n</w:document>`;
|
|
155
221
|
const bodyPieces: string[] = [];
|
|
@@ -679,9 +745,9 @@ function serializeTableInlineNode(
|
|
|
679
745
|
return `<w:bookmarkEnd w:id="${escapeXmlAttribute(node.bookmarkId)}"/>`;
|
|
680
746
|
case "scope_marker_start": {
|
|
681
747
|
// S1 — scope markers export as w:bookmarkStart with the reserved
|
|
682
|
-
// `bw:scope:` name prefix. The synthetic w:id is
|
|
683
|
-
//
|
|
684
|
-
const bkId =
|
|
748
|
+
// `bw:scope:` name prefix. The synthetic w:id is numeric because
|
|
749
|
+
// WordprocessingML bookmark ids are ST_DecimalNumber values.
|
|
750
|
+
const bkId = scopeBookmarkIdFor(state, node.scopeId);
|
|
685
751
|
const name = `${SCOPE_MARKER_BOOKMARK_PREFIX}${node.scopeId}`;
|
|
686
752
|
return (
|
|
687
753
|
`<w:bookmarkStart w:id="${escapeXmlAttribute(bkId)}"` +
|
|
@@ -689,7 +755,7 @@ function serializeTableInlineNode(
|
|
|
689
755
|
);
|
|
690
756
|
}
|
|
691
757
|
case "scope_marker_end": {
|
|
692
|
-
const bkId =
|
|
758
|
+
const bkId = scopeBookmarkIdFor(state, node.scopeId);
|
|
693
759
|
return `<w:bookmarkEnd w:id="${escapeXmlAttribute(bkId)}"/>`;
|
|
694
760
|
}
|
|
695
761
|
case "footnote_ref": {
|
|
@@ -1258,7 +1324,7 @@ function serializeInlineNode(
|
|
|
1258
1324
|
case "scope_marker_start": {
|
|
1259
1325
|
// S1 — mirror the bookmark_start shape with the reserved `bw:scope:`
|
|
1260
1326
|
// name prefix. See serializeInline() above for the same convention.
|
|
1261
|
-
const bkId =
|
|
1327
|
+
const bkId = scopeBookmarkIdFor(state, node.scopeId);
|
|
1262
1328
|
const name = `${SCOPE_MARKER_BOOKMARK_PREFIX}${node.scopeId}`;
|
|
1263
1329
|
const xml =
|
|
1264
1330
|
`<w:bookmarkStart w:id="${escapeXmlAttribute(bkId)}"` +
|
|
@@ -1269,7 +1335,7 @@ function serializeInlineNode(
|
|
|
1269
1335
|
return { xml, cursor, boundaries };
|
|
1270
1336
|
}
|
|
1271
1337
|
case "scope_marker_end": {
|
|
1272
|
-
const bkId =
|
|
1338
|
+
const bkId = scopeBookmarkIdFor(state, node.scopeId);
|
|
1273
1339
|
const xml = `<w:bookmarkEnd w:id="${escapeXmlAttribute(bkId)}"/>`;
|
|
1274
1340
|
const boundaries = new Map<number, number>();
|
|
1275
1341
|
boundaries.set(cursor, xmlOffset);
|
|
@@ -40,6 +40,8 @@ export interface ValidatorIssue {
|
|
|
40
40
|
|
|
41
41
|
const OVERLAY_PERSISTENCE_VALUES = ["internal", "external"] as const;
|
|
42
42
|
const SCOPE_PERSISTENCE_VALUES = ["internal", "external", "inherit"] as const;
|
|
43
|
+
const SCOPE_VISIBILITY_VALUES = ["visible", "hidden", "invisible"] as const;
|
|
44
|
+
const SCOPE_GUARD_POLICY_VALUES = ["none", "insert-only", "read-only"] as const;
|
|
43
45
|
|
|
44
46
|
export function validateWorkflowPayloadEnvelope(xml: string): ValidatorIssue[] {
|
|
45
47
|
const issues: ValidatorIssue[] = [];
|
|
@@ -82,6 +84,28 @@ export function validateWorkflowPayloadEnvelope(xml: string): ValidatorIssue[] {
|
|
|
82
84
|
for (const scope of findAll(xml, /<bw:scope\b([^>]*?)>([\s\S]*?)<\/bw:scope>/gu)) {
|
|
83
85
|
const scopeAttrs = parseAttrs(scope.attrs);
|
|
84
86
|
const scopeId = scopeAttrs.id ?? "?";
|
|
87
|
+
if (
|
|
88
|
+
scopeAttrs.visibility !== undefined &&
|
|
89
|
+
!(SCOPE_VISIBILITY_VALUES as readonly string[]).includes(scopeAttrs.visibility)
|
|
90
|
+
) {
|
|
91
|
+
issues.push({
|
|
92
|
+
code: "unknown_enum_value",
|
|
93
|
+
path: `bw:scope[@id='${scopeId}']/@visibility`,
|
|
94
|
+
value: scopeAttrs.visibility,
|
|
95
|
+
severity: "warning",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (
|
|
99
|
+
scopeAttrs.guardPolicy !== undefined &&
|
|
100
|
+
!(SCOPE_GUARD_POLICY_VALUES as readonly string[]).includes(scopeAttrs.guardPolicy)
|
|
101
|
+
) {
|
|
102
|
+
issues.push({
|
|
103
|
+
code: "unknown_enum_value",
|
|
104
|
+
path: `bw:scope[@id='${scopeId}']/@guardPolicy`,
|
|
105
|
+
value: scopeAttrs.guardPolicy,
|
|
106
|
+
severity: "warning",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
85
109
|
let scopePersistence: string | undefined = scopeAttrs.metadataPersistence;
|
|
86
110
|
if (scopePersistence !== undefined) {
|
|
87
111
|
if (
|
|
@@ -927,6 +927,8 @@ function buildWorkflowScopeXml(scope: WorkflowScope): string {
|
|
|
927
927
|
scope.workItemId ? ` workItemRef="${escapeXml(scope.workItemId)}"` : "",
|
|
928
928
|
scope.label ? ` label="${escapeXml(scope.label)}"` : "",
|
|
929
929
|
scope.domain ? ` domain="${escapeXml(scope.domain)}"` : "",
|
|
930
|
+
scope.visibility ? ` visibility="${escapeXml(scope.visibility)}"` : "",
|
|
931
|
+
scope.guardPolicy ? ` guardPolicy="${escapeXml(scope.guardPolicy)}"` : "",
|
|
930
932
|
scope.metadataPersistence && scope.metadataPersistence !== "inherit"
|
|
931
933
|
? ` metadataPersistence="${escapeXml(scope.metadataPersistence)}"`
|
|
932
934
|
: "",
|
|
@@ -1166,6 +1168,14 @@ function parseWorkflowScope(attributesSource: string, body: string): WorkflowSco
|
|
|
1166
1168
|
attributes.metadataPersistence,
|
|
1167
1169
|
["internal", "external", "inherit"] as const,
|
|
1168
1170
|
);
|
|
1171
|
+
const visibility = parseClosedEnum(
|
|
1172
|
+
attributes.visibility,
|
|
1173
|
+
["visible", "hidden", "invisible"] as const,
|
|
1174
|
+
);
|
|
1175
|
+
const guardPolicy = parseClosedEnum(
|
|
1176
|
+
attributes.guardPolicy,
|
|
1177
|
+
["none", "insert-only", "read-only"] as const,
|
|
1178
|
+
);
|
|
1169
1179
|
|
|
1170
1180
|
return {
|
|
1171
1181
|
scopeId: attributes.id,
|
|
@@ -1177,6 +1187,8 @@ function parseWorkflowScope(attributesSource: string, body: string): WorkflowSco
|
|
|
1177
1187
|
label: attributes.label,
|
|
1178
1188
|
domain: attributes.domain as WorkflowScope["domain"],
|
|
1179
1189
|
metadata: parseWorkflowScopeMetadata(body),
|
|
1190
|
+
...(visibility !== undefined ? { visibility } : {}),
|
|
1191
|
+
...(guardPolicy !== undefined ? { guardPolicy } : {}),
|
|
1180
1192
|
...(scopeMetadataPersistence !== undefined ? { metadataPersistence: scopeMetadataPersistence } : {}),
|
|
1181
1193
|
};
|
|
1182
1194
|
}
|
|
@@ -94,6 +94,7 @@ import type {
|
|
|
94
94
|
ScopeQueryFilter,
|
|
95
95
|
ScopeQueryResult,
|
|
96
96
|
ScopeVisibility,
|
|
97
|
+
WorkflowScopeGuardPolicy,
|
|
97
98
|
ScopeChromeVisibilityState,
|
|
98
99
|
SearchOptions,
|
|
99
100
|
TextStyleFilter,
|
|
@@ -588,6 +589,13 @@ export interface DocumentRuntime {
|
|
|
588
589
|
setScopeVisibility(scopeId: string, visibility: ScopeVisibility): void;
|
|
589
590
|
/** §C8 — Get a scope's current visibility (absent = "visible"). */
|
|
590
591
|
getScopeVisibility(scopeId: string): ScopeVisibility;
|
|
592
|
+
/** Scope edit-enforcement posture (collab-replicated). */
|
|
593
|
+
setScopeGuardPolicy(
|
|
594
|
+
scopeId: string,
|
|
595
|
+
guardPolicy: WorkflowScopeGuardPolicy,
|
|
596
|
+
): void;
|
|
597
|
+
/** Effective scope edit-enforcement posture (unknown = "none"). */
|
|
598
|
+
getScopeGuardPolicy(scopeId: string): WorkflowScopeGuardPolicy;
|
|
591
599
|
/** §C7 — Set local chrome visibility state (never collab-replicated). */
|
|
592
600
|
setScopeChromeVisibility(state: ScopeChromeVisibilityState): void;
|
|
593
601
|
/** §C7 — Get local chrome visibility state (default: { mode: "all" }). */
|
|
@@ -3095,6 +3103,7 @@ export function createDocumentRuntime(
|
|
|
3095
3103
|
{
|
|
3096
3104
|
selection: createSelectionFromPublicAnchor(anchor),
|
|
3097
3105
|
blockedCommandName: "applyScopeReplacement",
|
|
3106
|
+
skipWorkflowGuard: true,
|
|
3098
3107
|
},
|
|
3099
3108
|
);
|
|
3100
3109
|
} catch (error) {
|
|
@@ -3129,6 +3138,7 @@ export function createDocumentRuntime(
|
|
|
3129
3138
|
{
|
|
3130
3139
|
selection: createSelectionFromPublicAnchor(anchor),
|
|
3131
3140
|
blockedCommandName: "applyScopeReplacement",
|
|
3141
|
+
skipWorkflowGuard: true,
|
|
3132
3142
|
},
|
|
3133
3143
|
);
|
|
3134
3144
|
} catch (error) {
|
|
@@ -3168,6 +3178,7 @@ export function createDocumentRuntime(
|
|
|
3168
3178
|
selection: createSelectionFromPublicAnchor(anchor),
|
|
3169
3179
|
blockedCommandName: "applyScopeReplacement",
|
|
3170
3180
|
documentModeOverride: "suggesting",
|
|
3181
|
+
skipWorkflowGuard: true,
|
|
3171
3182
|
},
|
|
3172
3183
|
);
|
|
3173
3184
|
} catch (error) {
|
|
@@ -3200,6 +3211,7 @@ export function createDocumentRuntime(
|
|
|
3200
3211
|
selection: createSelectionFromPublicAnchor(anchor),
|
|
3201
3212
|
blockedCommandName: "applyScopeReplacement",
|
|
3202
3213
|
documentModeOverride: "suggesting",
|
|
3214
|
+
skipWorkflowGuard: true,
|
|
3203
3215
|
},
|
|
3204
3216
|
);
|
|
3205
3217
|
} catch (error) {
|
|
@@ -3580,6 +3592,12 @@ export function createDocumentRuntime(
|
|
|
3580
3592
|
getScopeVisibility(scopeId) {
|
|
3581
3593
|
return workflowCoordinator.getScopeVisibility(scopeId);
|
|
3582
3594
|
},
|
|
3595
|
+
setScopeGuardPolicy(scopeId, guardPolicy) {
|
|
3596
|
+
workflowCoordinator.setScopeGuardPolicy(scopeId, guardPolicy);
|
|
3597
|
+
},
|
|
3598
|
+
getScopeGuardPolicy(scopeId) {
|
|
3599
|
+
return workflowCoordinator.getScopeGuardPolicy(scopeId);
|
|
3600
|
+
},
|
|
3583
3601
|
setScopeChromeVisibility(chromeVisibility) {
|
|
3584
3602
|
workflowCoordinator.setScopeChromeVisibility(chromeVisibility);
|
|
3585
3603
|
},
|
|
@@ -4939,6 +4957,13 @@ export function createDocumentRuntime(
|
|
|
4939
4957
|
* override to the context that `executeEditorCommand` reads.
|
|
4940
4958
|
*/
|
|
4941
4959
|
documentModeOverride?: DocumentMode;
|
|
4960
|
+
/**
|
|
4961
|
+
* Scope replacements are validated against their target scope by the
|
|
4962
|
+
* Layer-08 compiler before reaching this mechanical dispatch step.
|
|
4963
|
+
* Re-running the selection-scoped workflow guard here would make the
|
|
4964
|
+
* live cursor, not the target scope, decide whether the edit lands.
|
|
4965
|
+
*/
|
|
4966
|
+
skipWorkflowGuard?: boolean;
|
|
4942
4967
|
} = {},
|
|
4943
4968
|
): TextCommandAck {
|
|
4944
4969
|
emitStageToken(telemetryBus, "command", "command.dispatch.start", {
|
|
@@ -4980,20 +5005,22 @@ export function createDocumentRuntime(
|
|
|
4980
5005
|
blockedReasons: [{ code: "suggesting_unsupported", message }],
|
|
4981
5006
|
});
|
|
4982
5007
|
}
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
5008
|
+
if (!textOptions.skipWorkflowGuard) {
|
|
5009
|
+
const blockedReasons = workflowCoordinator.evaluateBlockedReasons(selection, command.type);
|
|
5010
|
+
if (blockedReasons.length > 0) {
|
|
5011
|
+
emit({
|
|
5012
|
+
type: "command_blocked",
|
|
5013
|
+
documentId: state.documentId,
|
|
5014
|
+
command: textOptions.blockedCommandName ?? command.type,
|
|
5015
|
+
reasons: blockedReasons,
|
|
5016
|
+
});
|
|
5017
|
+
return completeDispatch({
|
|
5018
|
+
kind: "rejected",
|
|
5019
|
+
opId,
|
|
5020
|
+
newRevisionToken: "",
|
|
5021
|
+
blockedReasons: blockedReasons.map((r) => ({ code: r.code, message: r.message })),
|
|
5022
|
+
});
|
|
5023
|
+
}
|
|
4997
5024
|
}
|
|
4998
5025
|
|
|
4999
5026
|
const timestamp = normalizeCommandTimestamp(command.origin?.timestamp) ?? clock();
|