@beyondwork/docx-react-component 1.0.83 → 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 +4 -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();
|
|
@@ -204,55 +204,64 @@ function collectGuardVerdict(
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
const guard = runtime.getInteractionGuardSnapshot();
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
207
|
+
// Scope-targeted writes target `scope.handle.scopeId`, not the current
|
|
208
|
+
// editor selection. The target scope's own overlay posture was handled
|
|
209
|
+
// above through `scope.workflow.effectiveMode`; this second guard read is
|
|
210
|
+
// only allowed to contribute global/session-wide blockers.
|
|
211
|
+
const isSelectionScopeMembershipReason = (reason: { readonly code: string; readonly scopeId?: string }): boolean => {
|
|
212
|
+
if (reason.code === "outside_workflow_scope") return true;
|
|
213
|
+
if (
|
|
214
|
+
(reason.code === "workflow_view_only" ||
|
|
215
|
+
reason.code === "workflow_comment_only") &&
|
|
216
|
+
typeof reason.scopeId === "string" &&
|
|
217
|
+
reason.scopeId.length > 0
|
|
218
|
+
) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
};
|
|
223
|
+
const rawReasons = guard.blockedReasons ?? [];
|
|
224
|
+
const nonSelectionScoped = rawReasons.filter(
|
|
225
|
+
(r) => !isSelectionScopeMembershipReason(r),
|
|
226
|
+
);
|
|
227
|
+
const pushTypedGuardBlocker = (code: string | undefined): void => {
|
|
228
|
+
const suffix = typeof code === "string" && code.length > 0
|
|
229
|
+
? code
|
|
230
|
+
: "unspecified";
|
|
231
|
+
const typedBlocker = `guard:block-${suffix}`;
|
|
232
|
+
if (!blockedReasons.some((existing) => existing === typedBlocker)) {
|
|
233
|
+
blockedReasons.push(typedBlocker);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
if (guard.effectiveMode === "view") {
|
|
237
|
+
const globalViewReason = nonSelectionScoped.find(
|
|
238
|
+
(reason) => reason.code === "workflow_view_only",
|
|
239
|
+
);
|
|
240
|
+
if (globalViewReason) {
|
|
241
|
+
pushTypedGuardBlocker(globalViewReason.code);
|
|
242
|
+
} else if (
|
|
243
|
+
rawReasons.length === 0 &&
|
|
244
|
+
!blockedReasons.includes("guard:view-mode-active")
|
|
245
|
+
) {
|
|
246
|
+
blockedReasons.push("guard:view-mode-active");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (guard.effectiveMode === "comment") {
|
|
250
|
+
const globalCommentReason = nonSelectionScoped.find(
|
|
251
|
+
(reason) => reason.code === "workflow_comment_only",
|
|
252
|
+
);
|
|
253
|
+
if (globalCommentReason) {
|
|
254
|
+
pushTypedGuardBlocker(globalCommentReason.code);
|
|
255
|
+
}
|
|
212
256
|
}
|
|
213
257
|
if (guard.effectiveMode === "blocked") {
|
|
214
258
|
// Coord-06 §13e — promote the bare `guard:blocked` blocker to a typed
|
|
215
|
-
// `guard:block-<reason>` suffix
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
// Scope-targeted-write carve-out (coord-09, TemplateViewer repro
|
|
221
|
-
// 2026-04-24): `applyReplacementScope`, `attachExplanation`, and
|
|
222
|
-
// `createIssue` target a scopeId, not the current editor selection.
|
|
223
|
-
// The scope's own `workflow.effectiveMode` already drove the
|
|
224
|
-
// scope-level arm of `collectGuardVerdict` above (lines 159–197).
|
|
225
|
-
// The selection-scoped coordinator guard, in contrast, evaluates
|
|
226
|
-
// against the live `state.selection` — which, for scope-targeted
|
|
227
|
-
// writes, may sit anywhere in the document. Reasons that depend on
|
|
228
|
-
// selection-scope membership (`outside_workflow_scope`,
|
|
229
|
-
// `workflow_view_only`, `workflow_comment_only`) are therefore
|
|
230
|
-
// double-counting and must not block. Globally-scoped reasons
|
|
231
|
-
// (`document_read_only`, `document_viewing_mode`) still apply — a
|
|
232
|
-
// read-only doc rejects every write, scope-targeted or not.
|
|
233
|
-
const SELECTION_SCOPE_MEMBERSHIP_CODES = new Set([
|
|
234
|
-
"outside_workflow_scope",
|
|
235
|
-
"workflow_view_only",
|
|
236
|
-
"workflow_comment_only",
|
|
237
|
-
]);
|
|
238
|
-
const rawReasons = guard.blockedReasons ?? [];
|
|
239
|
-
const nonSelectionScoped = rawReasons.filter(
|
|
240
|
-
(r) => !SELECTION_SCOPE_MEMBERSHIP_CODES.has(r.code),
|
|
241
|
-
);
|
|
242
|
-
// If every reason was selection-scope-membership for a scope-
|
|
243
|
-
// targeted write, emit no blocker — the scope-level arm above is
|
|
244
|
-
// authoritative. The defensive empty-array fallback
|
|
245
|
-
// (guard:block-unspecified) still fires when the coordinator
|
|
246
|
-
// produced effectiveMode:"blocked" without any reasons at all.
|
|
259
|
+
// `guard:block-<reason>` suffix. Selection-membership reasons are
|
|
260
|
+
// intentionally ignored here; global/session reasons such as read-only,
|
|
261
|
+
// protected ranges, shared workflow locks, and unsupported suggesting
|
|
262
|
+
// commands remain blockers.
|
|
247
263
|
if (nonSelectionScoped.length > 0 || rawReasons.length === 0) {
|
|
248
|
-
|
|
249
|
-
const suffix = typeof primaryCode === "string" && primaryCode.length > 0
|
|
250
|
-
? primaryCode
|
|
251
|
-
: "unspecified";
|
|
252
|
-
const typedBlocker = `guard:block-${suffix}`;
|
|
253
|
-
if (!blockedReasons.some((existing) => existing === typedBlocker)) {
|
|
254
|
-
blockedReasons.push(typedBlocker);
|
|
255
|
-
}
|
|
264
|
+
pushTypedGuardBlocker(nonSelectionScoped[0]?.code);
|
|
256
265
|
}
|
|
257
266
|
}
|
|
258
267
|
for (const reason of guard.blockedReasons ?? []) {
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Given a scope's canonical range and the workflow overlay, returns the
|
|
5
5
|
* `SemanticScopeWorkflow` projection: the ids of overlay scopes overlapping
|
|
6
|
-
* the range + the most-restrictive `effectiveMode` across
|
|
7
|
-
* `
|
|
6
|
+
* the range + the most-restrictive enforced `effectiveMode` across scopes
|
|
7
|
+
* whose `guardPolicy` participates in editing + `blockedReasons` when a
|
|
8
|
+
* read-only overlap blocks the scope.
|
|
8
9
|
*
|
|
9
10
|
* The most-restrictive rule matches layer 06's `InteractionGuardSnapshot`
|
|
10
|
-
* composition
|
|
11
|
+
* composition for guard-participating scopes (see
|
|
12
|
+
* `docs/architecture/06-workflow-review.md` §W3):
|
|
11
13
|
* view > comment > suggest > edit
|
|
12
14
|
*
|
|
13
15
|
* When no overlay is threaded, or no overlap exists, the returned shape
|
|
@@ -19,7 +21,11 @@
|
|
|
19
21
|
* overlap on top.
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
|
-
import type {
|
|
24
|
+
import type {
|
|
25
|
+
WorkflowOverlay,
|
|
26
|
+
WorkflowScope,
|
|
27
|
+
WorkflowScopeGuardPolicy,
|
|
28
|
+
} from "./_scope-dependencies.ts";
|
|
23
29
|
|
|
24
30
|
import type { ScopePositionMap, ScopePositionRange } from "./position-map.ts";
|
|
25
31
|
import { rangesOverlap } from "./scope-range.ts";
|
|
@@ -27,6 +33,14 @@ import type { SemanticScopeWorkflow } from "./semantic-scope-types.ts";
|
|
|
27
33
|
|
|
28
34
|
type WorkflowMode = "edit" | "suggest" | "comment" | "view";
|
|
29
35
|
|
|
36
|
+
function getScopeGuardPolicy(scope: WorkflowScope): WorkflowScopeGuardPolicy {
|
|
37
|
+
return scope.guardPolicy ?? (scope.mode === "view" ? "read-only" : "none");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getScopeGuardMode(scope: WorkflowScope): WorkflowMode {
|
|
41
|
+
return getScopeGuardPolicy(scope) === "read-only" ? "view" : scope.mode;
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
function modeRank(mode: WorkflowMode): number {
|
|
31
45
|
// Higher = more restrictive (wins the merge).
|
|
32
46
|
switch (mode) {
|
|
@@ -45,6 +59,21 @@ function mergeModes(a: WorkflowMode, b: WorkflowMode): WorkflowMode {
|
|
|
45
59
|
return modeRank(a) >= modeRank(b) ? a : b;
|
|
46
60
|
}
|
|
47
61
|
|
|
62
|
+
function rangeForWorkflowScope(
|
|
63
|
+
scope: WorkflowScope,
|
|
64
|
+
positionMap: ScopePositionMap,
|
|
65
|
+
): ScopePositionRange | null {
|
|
66
|
+
const markerRange = positionMap.markerScopes.get(scope.scopeId);
|
|
67
|
+
if (markerRange) return markerRange;
|
|
68
|
+
if (scope.anchor.kind === "range") {
|
|
69
|
+
return { from: scope.anchor.from, to: scope.anchor.to };
|
|
70
|
+
}
|
|
71
|
+
if (scope.anchor.kind === "node") {
|
|
72
|
+
return { from: scope.anchor.at, to: scope.anchor.at };
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
48
77
|
export interface WorkflowOverlapInputs {
|
|
49
78
|
readonly overlay: WorkflowOverlay | null | undefined;
|
|
50
79
|
readonly positionMap: ScopePositionMap;
|
|
@@ -73,12 +102,15 @@ export function resolveWorkflowOverlap(
|
|
|
73
102
|
let mode: WorkflowMode = "edit";
|
|
74
103
|
for (const scope of overlay.scopes as readonly WorkflowScope[]) {
|
|
75
104
|
if (selfScopeIds && selfScopeIds.has(scope.scopeId)) continue;
|
|
76
|
-
const
|
|
77
|
-
if (!
|
|
78
|
-
if (!rangesOverlap(range,
|
|
105
|
+
const scopeRange = rangeForWorkflowScope(scope, positionMap);
|
|
106
|
+
if (!scopeRange) continue;
|
|
107
|
+
if (!rangesOverlap(range, scopeRange)) continue;
|
|
79
108
|
overlappingIds.push(scope.scopeId);
|
|
80
|
-
|
|
81
|
-
if (
|
|
109
|
+
const guardPolicy = getScopeGuardPolicy(scope);
|
|
110
|
+
if (guardPolicy === "none") continue;
|
|
111
|
+
const guardMode = getScopeGuardMode(scope);
|
|
112
|
+
mode = mergeModes(mode, guardMode);
|
|
113
|
+
if (guardMode === "view") {
|
|
82
114
|
blockedReasons.push(`workflow-scope-view:${scope.scopeId}`);
|
|
83
115
|
}
|
|
84
116
|
}
|
|
@@ -64,6 +64,7 @@ import type {
|
|
|
64
64
|
WorkflowMetadataSnapshot,
|
|
65
65
|
WorkflowOverlay,
|
|
66
66
|
WorkflowScope,
|
|
67
|
+
WorkflowScopeGuardPolicy,
|
|
67
68
|
WorkflowScopeMode,
|
|
68
69
|
WorkflowScopeSnapshot,
|
|
69
70
|
} from "../../api/public-types.ts";
|
|
@@ -227,6 +228,11 @@ export interface WorkflowCoordinator {
|
|
|
227
228
|
addInvisibleScope(params: AddScopeParams): AddScopeResult;
|
|
228
229
|
setScopeVisibility(scopeId: string, visibility: ScopeVisibility): void;
|
|
229
230
|
getScopeVisibility(scopeId: string): ScopeVisibility;
|
|
231
|
+
setScopeGuardPolicy(
|
|
232
|
+
scopeId: string,
|
|
233
|
+
guardPolicy: WorkflowScopeGuardPolicy,
|
|
234
|
+
): void;
|
|
235
|
+
getScopeGuardPolicy(scopeId: string): WorkflowScopeGuardPolicy;
|
|
230
236
|
getScope(scopeId: string): WorkflowScope | null;
|
|
231
237
|
getMarkerBackedScopeIds(): ReadonlySet<string>;
|
|
232
238
|
/* --- scope chrome visibility (local view state) --- */
|
|
@@ -316,6 +322,18 @@ const MODE_RESTRICTIVENESS: Record<WorkflowScopeMode, number> = {
|
|
|
316
322
|
view: 3,
|
|
317
323
|
};
|
|
318
324
|
|
|
325
|
+
function resolveScopeGuardPolicy(scope: WorkflowScope): WorkflowScopeGuardPolicy {
|
|
326
|
+
return scope.guardPolicy ?? (scope.mode === "view" ? "read-only" : "none");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function getScopeGuardMode(scope: WorkflowScope): WorkflowScopeMode {
|
|
330
|
+
return resolveScopeGuardPolicy(scope) === "read-only" ? "view" : scope.mode;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function participatesInInteractionGuard(scope: WorkflowScope): boolean {
|
|
334
|
+
return resolveScopeGuardPolicy(scope) !== "none";
|
|
335
|
+
}
|
|
336
|
+
|
|
319
337
|
export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordinator {
|
|
320
338
|
const { overlayStore, clock } = deps;
|
|
321
339
|
|
|
@@ -413,8 +431,7 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
|
|
|
413
431
|
};
|
|
414
432
|
const activeScopes = getEffectiveWorkflowScopes(overlay);
|
|
415
433
|
const matching = activeScopes.filter((scope) => {
|
|
416
|
-
|
|
417
|
-
if (scope.visibility === "invisible" && scope.mode !== "view") return false;
|
|
434
|
+
if (!participatesInInteractionGuard(scope)) return false;
|
|
418
435
|
if (scope.anchor.kind === "detached") return false;
|
|
419
436
|
const scopeFrom =
|
|
420
437
|
scope.anchor.kind === "range" ? scope.anchor.from : scope.anchor.at;
|
|
@@ -450,8 +467,8 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
|
|
|
450
467
|
if (stack.length === 0) return null;
|
|
451
468
|
// §C6 — most-restrictive-wins across overlapping scopes.
|
|
452
469
|
return stack.reduce((best, scope) =>
|
|
453
|
-
(MODE_RESTRICTIVENESS[scope
|
|
454
|
-
(MODE_RESTRICTIVENESS[best
|
|
470
|
+
(MODE_RESTRICTIVENESS[getScopeGuardMode(scope)] ?? 0) >
|
|
471
|
+
(MODE_RESTRICTIVENESS[getScopeGuardMode(best)] ?? 0)
|
|
455
472
|
? scope
|
|
456
473
|
: best,
|
|
457
474
|
);
|
|
@@ -463,7 +480,9 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
|
|
|
463
480
|
const mode = deps.getDocumentMode();
|
|
464
481
|
if (mode === "viewing" || mode === "commenting") return mode;
|
|
465
482
|
const matchingScope = getMatchingWorkflowScope(selection);
|
|
466
|
-
if (matchingScope
|
|
483
|
+
if (matchingScope && getScopeGuardMode(matchingScope) === "suggest") {
|
|
484
|
+
return "suggesting";
|
|
485
|
+
}
|
|
467
486
|
return mode;
|
|
468
487
|
}
|
|
469
488
|
|
|
@@ -536,17 +555,18 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
|
|
|
536
555
|
if (normalized) {
|
|
537
556
|
const matchingScope = getMatchingWorkflowScope(selection);
|
|
538
557
|
const activeScopes = getEffectiveWorkflowScopes(normalized);
|
|
539
|
-
const
|
|
540
|
-
(s) =>
|
|
558
|
+
const insertOnlyScopes = activeScopes.filter(
|
|
559
|
+
(s) => resolveScopeGuardPolicy(s) === "insert-only",
|
|
541
560
|
);
|
|
542
561
|
|
|
543
|
-
if (!matchingScope &&
|
|
562
|
+
if (!matchingScope && insertOnlyScopes.length > 0) {
|
|
544
563
|
reasons.push({
|
|
545
564
|
code: "outside_workflow_scope",
|
|
546
|
-
message: "Selection is outside any active workflow scope.",
|
|
565
|
+
message: "Selection is outside any active insert-only workflow scope.",
|
|
547
566
|
});
|
|
548
567
|
} else if (matchingScope) {
|
|
549
|
-
|
|
568
|
+
const guardMode = getScopeGuardMode(matchingScope);
|
|
569
|
+
if (guardMode === "comment") {
|
|
550
570
|
const isCommentCommand = commandType?.startsWith("comment.") ?? false;
|
|
551
571
|
if (!isCommentCommand) {
|
|
552
572
|
reasons.push({
|
|
@@ -556,7 +576,7 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
|
|
|
556
576
|
workItemId: matchingScope.workItemId,
|
|
557
577
|
});
|
|
558
578
|
}
|
|
559
|
-
} else if (
|
|
579
|
+
} else if (guardMode === "view") {
|
|
560
580
|
reasons.push({
|
|
561
581
|
code: "workflow_view_only",
|
|
562
582
|
message: `Scope "${matchingScope.label ?? matchingScope.scopeId}" is view-only.`,
|
|
@@ -618,6 +638,9 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
|
|
|
618
638
|
const matchingScope = getMatchingWorkflowScope(state.selection);
|
|
619
639
|
const scopeStack = buildMatchingScopeStack(state.selection);
|
|
620
640
|
const primaryBlockedReason = blockedReasons[0];
|
|
641
|
+
const matchingGuardMode = matchingScope
|
|
642
|
+
? getScopeGuardMode(matchingScope)
|
|
643
|
+
: undefined;
|
|
621
644
|
const effectiveMode = primaryBlockedReason
|
|
622
645
|
? primaryBlockedReason.code === "workflow_comment_only"
|
|
623
646
|
? "comment"
|
|
@@ -626,19 +649,19 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
|
|
|
626
649
|
: "blocked"
|
|
627
650
|
: getEffectiveDocumentMode(state.selection) === "suggesting"
|
|
628
651
|
? "suggest"
|
|
629
|
-
:
|
|
652
|
+
: matchingGuardMode ?? "edit";
|
|
630
653
|
const matchedScopeStack: InteractionGuardSnapshot["matchedScopeStack"] =
|
|
631
654
|
scopeStack.length > 0
|
|
632
655
|
? scopeStack.map((s) => ({
|
|
633
656
|
scopeId: s.scopeId,
|
|
634
|
-
mode: s
|
|
657
|
+
mode: getScopeGuardMode(s),
|
|
635
658
|
visibility: s.visibility ?? "visible",
|
|
636
659
|
}))
|
|
637
660
|
: undefined;
|
|
638
661
|
const snapshot: InteractionGuardSnapshot = {
|
|
639
662
|
effectiveMode,
|
|
640
663
|
...(matchingScope?.scopeId ? { matchedScopeId: matchingScope.scopeId } : {}),
|
|
641
|
-
...(
|
|
664
|
+
...(matchingGuardMode ? { matchedScopeMode: matchingGuardMode } : {}),
|
|
642
665
|
...(matchedScopeStack ? { matchedScopeStack } : {}),
|
|
643
666
|
targetAccess:
|
|
644
667
|
effectiveMode === "edit"
|
|
@@ -888,6 +911,8 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
|
|
|
888
911
|
anchor: publicAnchor,
|
|
889
912
|
...(params.storyTarget ? { storyTarget: params.storyTarget } : {}),
|
|
890
913
|
...(params.label ? { label: params.label } : {}),
|
|
914
|
+
...(params.visibility ? { visibility: params.visibility } : {}),
|
|
915
|
+
...(params.guardPolicy ? { guardPolicy: params.guardPolicy } : {}),
|
|
891
916
|
...(params.scopeMetadataFields && params.scopeMetadataFields.length > 0
|
|
892
917
|
? { metadata: [...params.scopeMetadataFields] }
|
|
893
918
|
: {}),
|
|
@@ -1004,6 +1029,31 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
|
|
|
1004
1029
|
return scope?.visibility ?? "visible";
|
|
1005
1030
|
}
|
|
1006
1031
|
|
|
1032
|
+
function setScopeGuardPolicy(
|
|
1033
|
+
scopeId: string,
|
|
1034
|
+
guardPolicy: WorkflowScopeGuardPolicy,
|
|
1035
|
+
): void {
|
|
1036
|
+
const overlay = overlayStore.getOverlay();
|
|
1037
|
+
if (!overlay) return;
|
|
1038
|
+
const idx = overlay.scopes.findIndex((s) => s.scopeId === scopeId);
|
|
1039
|
+
if (idx === -1) return;
|
|
1040
|
+
const nextScopes = overlay.scopes.map((s) =>
|
|
1041
|
+
s.scopeId === scopeId ? { ...s, guardPolicy } : s,
|
|
1042
|
+
);
|
|
1043
|
+
deps.dispatch({
|
|
1044
|
+
type: "workflow.set-overlay",
|
|
1045
|
+
overlay: { ...overlay, scopes: nextScopes },
|
|
1046
|
+
origin: { source: "api", at: clock() },
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function getScopeGuardPolicy(scopeId: string): WorkflowScopeGuardPolicy {
|
|
1051
|
+
const overlay = overlayStore.getOverlay();
|
|
1052
|
+
if (!overlay) return "none";
|
|
1053
|
+
const scope = overlay.scopes.find((s) => s.scopeId === scopeId);
|
|
1054
|
+
return scope ? resolveScopeGuardPolicy(scope) : "none";
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1007
1057
|
function getScope(scopeId: string): WorkflowScope | null {
|
|
1008
1058
|
const normalized = getNormalizedOverlay();
|
|
1009
1059
|
const fromOverlay = normalized?.scopes.find((s) => s.scopeId === scopeId);
|
|
@@ -1360,6 +1410,8 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
|
|
|
1360
1410
|
addInvisibleScope,
|
|
1361
1411
|
setScopeVisibility,
|
|
1362
1412
|
getScopeVisibility,
|
|
1413
|
+
setScopeGuardPolicy,
|
|
1414
|
+
getScopeGuardPolicy,
|
|
1363
1415
|
getScope,
|
|
1364
1416
|
getMarkerBackedScopeIds: () => overlayStore.getMarkerBackedScopeIds(),
|
|
1365
1417
|
setScopeChromeVisibility,
|
|
@@ -37,6 +37,8 @@ import type {
|
|
|
37
37
|
EditorAnchorProjection,
|
|
38
38
|
EditorStoryTarget,
|
|
39
39
|
RuntimeRenderSnapshot,
|
|
40
|
+
ScopeVisibility,
|
|
41
|
+
WorkflowScopeGuardPolicy,
|
|
40
42
|
WorkflowMetadataEntry,
|
|
41
43
|
WorkflowMetadataPersistence,
|
|
42
44
|
WorkflowScopeMetadataField,
|
|
@@ -62,6 +64,8 @@ export interface CreateScopeFromBlockIdInput {
|
|
|
62
64
|
readonly persistence?: WorkflowMetadataPersistence;
|
|
63
65
|
readonly metadata?: Partial<WorkflowMetadataEntry>;
|
|
64
66
|
readonly storyTarget?: EditorStoryTarget;
|
|
67
|
+
readonly visibility?: ScopeVisibility;
|
|
68
|
+
readonly guardPolicy?: WorkflowScopeGuardPolicy;
|
|
65
69
|
/**
|
|
66
70
|
* Coord-06 §13d — per-scope edge stickiness for the range anchor.
|
|
67
71
|
* Defaults to `{ start: 1, end: -1 }` (greedy — absorbs boundary
|
|
@@ -98,7 +102,23 @@ export interface CreateScopeFromBlockIdInput {
|
|
|
98
102
|
|
|
99
103
|
export type CreateScopeFromBlockIdResult =
|
|
100
104
|
| { readonly status: "created"; readonly scopeId: string; readonly anchor: EditorAnchorProjection }
|
|
101
|
-
| { readonly status: "block-not-found"; readonly blockId: string }
|
|
105
|
+
| { readonly status: "block-not-found"; readonly blockId: string }
|
|
106
|
+
| {
|
|
107
|
+
readonly status: "range-invalid";
|
|
108
|
+
readonly scopeId: "";
|
|
109
|
+
readonly reason:
|
|
110
|
+
| "range-exceeds-story-length"
|
|
111
|
+
| "non-paragraph-target"
|
|
112
|
+
| "empty-document";
|
|
113
|
+
readonly from: number;
|
|
114
|
+
readonly to: number;
|
|
115
|
+
readonly storyLength: number;
|
|
116
|
+
/** Non-paragraph target only — the offending block's index and kind. */
|
|
117
|
+
readonly blockIndex?: number;
|
|
118
|
+
readonly blockKind?: string;
|
|
119
|
+
readonly message: string;
|
|
120
|
+
readonly nextStep: string;
|
|
121
|
+
};
|
|
102
122
|
|
|
103
123
|
function inlineLength(node: InlineNode): number {
|
|
104
124
|
switch (node.type) {
|
|
@@ -190,8 +210,9 @@ export function createScopeFromBlockId(
|
|
|
190
210
|
runtime: ScopeWriterRuntime,
|
|
191
211
|
input: CreateScopeFromBlockIdInput,
|
|
192
212
|
): CreateScopeFromBlockIdResult {
|
|
213
|
+
const document = runtime.getCanonicalDocument();
|
|
193
214
|
const anchor = resolveBlockAnchorFromCanonical(
|
|
194
|
-
|
|
215
|
+
document,
|
|
195
216
|
input.blockId,
|
|
196
217
|
input.assoc,
|
|
197
218
|
);
|
|
@@ -216,10 +237,63 @@ export function createScopeFromBlockId(
|
|
|
216
237
|
metadata: input.metadata,
|
|
217
238
|
storyTarget: input.storyTarget,
|
|
218
239
|
label: input.label,
|
|
240
|
+
...(input.visibility ? { visibility: input.visibility } : {}),
|
|
241
|
+
...(input.guardPolicy ? { guardPolicy: input.guardPolicy } : {}),
|
|
219
242
|
...(scopeMetadataFields.length > 0
|
|
220
243
|
? { scopeMetadataFields }
|
|
221
244
|
: {}),
|
|
222
245
|
});
|
|
246
|
+
if (result.plantStatus && result.plantStatus.planted === false) {
|
|
247
|
+
const ps = result.plantStatus;
|
|
248
|
+
const from = anchor.kind === "range" ? anchor.from : 0;
|
|
249
|
+
const to = anchor.kind === "range" ? anchor.to : from;
|
|
250
|
+
const storyLength = ps.storyLength ?? computeMainStoryLength(document);
|
|
251
|
+
if (ps.reason === "non-paragraph-target") {
|
|
252
|
+
return {
|
|
253
|
+
status: "range-invalid",
|
|
254
|
+
scopeId: "",
|
|
255
|
+
reason: "non-paragraph-target",
|
|
256
|
+
from,
|
|
257
|
+
to,
|
|
258
|
+
storyLength,
|
|
259
|
+
blockIndex: ps.blockIndex ?? -1,
|
|
260
|
+
blockKind: ps.blockKind ?? "unknown",
|
|
261
|
+
message:
|
|
262
|
+
`createScope refused blockId "${input.blockId}": it resolves to a ` +
|
|
263
|
+
`${ps.blockKind ?? "non-paragraph"} block (index ${ps.blockIndex}). ` +
|
|
264
|
+
`Marker-backed scopes only plant inside paragraphs today; choose a ` +
|
|
265
|
+
`paragraph block or create an overlay-only scope for table/SDT metadata.`,
|
|
266
|
+
nextStep: "pick-a-paragraph-block-or-use-overlay-only-scope",
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (ps.reason === "range-out-of-bounds") {
|
|
270
|
+
return {
|
|
271
|
+
status: "range-invalid",
|
|
272
|
+
scopeId: "",
|
|
273
|
+
reason: "range-exceeds-story-length",
|
|
274
|
+
from,
|
|
275
|
+
to,
|
|
276
|
+
storyLength,
|
|
277
|
+
message:
|
|
278
|
+
`createScope refused blockId "${input.blockId}": resolved range ` +
|
|
279
|
+
`[${from}, ${to}] exceeds the current story length (${storyLength}). ` +
|
|
280
|
+
`Re-query block ids from the current render snapshot before retrying.`,
|
|
281
|
+
nextStep: "re-query-current-block-id",
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
status: "range-invalid",
|
|
286
|
+
scopeId: "",
|
|
287
|
+
reason: "empty-document",
|
|
288
|
+
from,
|
|
289
|
+
to,
|
|
290
|
+
storyLength,
|
|
291
|
+
message:
|
|
292
|
+
`createScope refused blockId "${input.blockId}": the target document ` +
|
|
293
|
+
`has no blocks, so scope markers cannot be planted.`,
|
|
294
|
+
nextStep: "initialize-document-before-creating-scopes",
|
|
295
|
+
};
|
|
296
|
+
}
|
|
223
297
|
return { status: "created", scopeId: result.scopeId, anchor: result.anchor };
|
|
224
298
|
}
|
|
225
299
|
|
|
@@ -251,6 +325,8 @@ export interface CreateScopeFromAnchorInput {
|
|
|
251
325
|
readonly scopeId?: string;
|
|
252
326
|
readonly persistence?: WorkflowMetadataPersistence;
|
|
253
327
|
readonly metadata?: Partial<WorkflowMetadataEntry>;
|
|
328
|
+
readonly visibility?: ScopeVisibility;
|
|
329
|
+
readonly guardPolicy?: WorkflowScopeGuardPolicy;
|
|
254
330
|
/**
|
|
255
331
|
* Per-scope edge stickiness for the range anchor. Defaults to
|
|
256
332
|
* `{ start: 1, end: -1 }` (greedy — absorbs boundary inserts). See
|
|
@@ -423,6 +499,8 @@ export function createScopeFromAnchor(
|
|
|
423
499
|
metadata: input.metadata,
|
|
424
500
|
storyTarget,
|
|
425
501
|
label: input.label,
|
|
502
|
+
...(input.visibility ? { visibility: input.visibility } : {}),
|
|
503
|
+
...(input.guardPolicy ? { guardPolicy: input.guardPolicy } : {}),
|
|
426
504
|
...(scopeMetadataFields.length > 0 ? { scopeMetadataFields } : {}),
|
|
427
505
|
});
|
|
428
506
|
|
|
@@ -448,9 +526,9 @@ export function createScopeFromAnchor(
|
|
|
448
526
|
message:
|
|
449
527
|
`createScopeFromAnchor refused: range [${from}, ${to}] targets a ` +
|
|
450
528
|
`${ps.blockKind ?? "non-paragraph"} block (index ${ps.blockIndex}). ` +
|
|
451
|
-
`Marker scopes only plant inside paragraphs today. Pick a
|
|
452
|
-
`target, or
|
|
453
|
-
`
|
|
529
|
+
`Marker-backed scopes only plant inside paragraphs today. Pick a ` +
|
|
530
|
+
`paragraph target, or create an overlay-only scope for table/SDT ` +
|
|
531
|
+
`metadata until structural marker support lands.`,
|
|
454
532
|
nextStep: "pick-a-paragraph-target",
|
|
455
533
|
};
|
|
456
534
|
}
|
|
@@ -1289,6 +1289,8 @@ function createLoadingRuntimeBridge(input: {
|
|
|
1289
1289
|
addInvisibleScope: () => ({ scopeId: "", anchor: { kind: "range", from: 0, to: 0, assoc: { start: -1, end: 1 } } }),
|
|
1290
1290
|
setScopeVisibility: () => undefined,
|
|
1291
1291
|
getScopeVisibility: () => "visible" as const,
|
|
1292
|
+
setScopeGuardPolicy: () => undefined,
|
|
1293
|
+
getScopeGuardPolicy: () => "none" as const,
|
|
1292
1294
|
setScopeChromeVisibility: () => undefined,
|
|
1293
1295
|
getScopeChromeVisibility: () => ({ mode: "all" as const }),
|
|
1294
1296
|
subscribeToScopeQuery: (_filter, _callback) => () => undefined,
|
|
@@ -393,6 +393,8 @@ export function __createWordReviewEditorRefBridge(
|
|
|
393
393
|
addInvisibleScope: (params) => runtime.addInvisibleScope(params),
|
|
394
394
|
setScopeVisibility: (scopeId, visibility) => runtime.setScopeVisibility(scopeId, visibility),
|
|
395
395
|
getScopeVisibility: (scopeId) => runtime.getScopeVisibility(scopeId),
|
|
396
|
+
setScopeGuardPolicy: (scopeId, guardPolicy) => runtime.setScopeGuardPolicy(scopeId, guardPolicy),
|
|
397
|
+
getScopeGuardPolicy: (scopeId) => runtime.getScopeGuardPolicy(scopeId),
|
|
396
398
|
setScopeChromeVisibility: (state) => runtime.setScopeChromeVisibility(state),
|
|
397
399
|
getScopeChromeVisibility: () => runtime.getScopeChromeVisibility(),
|
|
398
400
|
subscribeToScopeQuery: (filter, callback) => runtime.subscribeToScopeQuery(filter, callback),
|
|
@@ -1654,6 +1656,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1654
1656
|
addInvisibleScope: (params) => activeRuntime.addInvisibleScope(params),
|
|
1655
1657
|
setScopeVisibility: (scopeId, visibility) => activeRuntime.setScopeVisibility(scopeId, visibility),
|
|
1656
1658
|
getScopeVisibility: (scopeId) => activeRuntime.getScopeVisibility(scopeId),
|
|
1659
|
+
setScopeGuardPolicy: (scopeId, guardPolicy) => activeRuntime.setScopeGuardPolicy(scopeId, guardPolicy),
|
|
1660
|
+
getScopeGuardPolicy: (scopeId) => activeRuntime.getScopeGuardPolicy(scopeId),
|
|
1657
1661
|
setScopeChromeVisibility: (state) => activeRuntime.setScopeChromeVisibility(state),
|
|
1658
1662
|
getScopeChromeVisibility: () => activeRuntime.getScopeChromeVisibility(),
|
|
1659
1663
|
subscribeToScopeQuery: (filter, callback) => activeRuntime.subscribeToScopeQuery(filter, callback),
|