@beyondwork/docx-react-component 1.0.83 → 1.0.85
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/internal/build-ref-projections.ts +3 -0
- package/src/api/public-types.ts +86 -4
- package/src/api/v3/_runtime-handle.ts +15 -0
- package/src/api/v3/runtime/content.ts +148 -1
- package/src/api/v3/runtime/formatting.ts +41 -0
- package/src/api/v3/runtime/review.ts +98 -0
- package/src/api/v3/runtime/workflow.ts +154 -6
- package/src/core/commands/index.ts +81 -25
- package/src/core/state/editor-state.ts +15 -0
- package/src/io/export/serialize-main-document.ts +72 -6
- package/src/io/ooxml/header-footer-reference.ts +38 -0
- package/src/io/ooxml/parse-headers-footers.ts +11 -23
- package/src/io/ooxml/parse-main-document.ts +7 -10
- package/src/io/ooxml/workflow-payload-validator.ts +24 -0
- package/src/io/ooxml/workflow-payload.ts +12 -0
- package/src/model/canonical-document.ts +9 -0
- package/src/model/review/comment-types.ts +2 -0
- package/src/runtime/document-runtime.ts +718 -68
- package/src/runtime/formatting/field/resolver.ts +73 -8
- package/src/runtime/layout/layout-engine-version.ts +31 -12
- package/src/runtime/layout/paginated-layout-engine.ts +18 -11
- package/src/runtime/layout/public-facet.ts +119 -16
- package/src/runtime/layout/resolve-page-fields.ts +68 -6
- package/src/runtime/layout/resolve-page-previews.ts +1 -1
- 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/suggestions-snapshot.ts +24 -0
- package/src/runtime/surface-projection.ts +59 -2
- package/src/runtime/workflow/coordinator.ts +66 -14
- package/src/runtime/workflow/scope-writer.ts +83 -5
- package/src/shell/ref-commands.ts +3 -354
- package/src/shell/session-bootstrap.ts +10 -0
- package/src/ui/WordReviewEditor.tsx +99 -9
- package/src/ui/editor-command-bag.ts +3 -1
- package/src/ui/headless/revision-decoration-model.ts +13 -0
- package/src/ui/headless/selection-tool-types.ts +2 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +7 -3
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +175 -25
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -1
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +12 -0
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +18 -30
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +1 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +1 -1
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +20 -11
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +9 -4
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +12 -7
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +29 -10
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +1 -1
- package/src/ui-tailwind/review-workspace/types.ts +3 -2
- package/src/ui-tailwind/review-workspace/use-page-markers.ts +11 -1
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +2 -1
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +2 -1
- package/src/ui-tailwind/tw-review-workspace.tsx +18 -2
|
@@ -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
|
}
|
|
@@ -68,6 +68,26 @@ function summarizeActionability(
|
|
|
68
68
|
: "actionable";
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function collectLinkedCommentThreadIds(
|
|
72
|
+
revisions: TrackedChangeEntrySnapshot[],
|
|
73
|
+
): string[] | undefined {
|
|
74
|
+
const ids = new Set<string>();
|
|
75
|
+
for (const revision of revisions) {
|
|
76
|
+
for (const commentThreadId of revision.commentThreadIds ?? []) {
|
|
77
|
+
ids.add(commentThreadId);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return ids.size > 0 ? [...ids] : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function countLinkedReplies(revisions: TrackedChangeEntrySnapshot[]): number {
|
|
84
|
+
let count = 0;
|
|
85
|
+
for (const revision of revisions) {
|
|
86
|
+
count += revision.replyCount ?? 0;
|
|
87
|
+
}
|
|
88
|
+
return count;
|
|
89
|
+
}
|
|
90
|
+
|
|
71
91
|
export function createSuggestionsSnapshot(
|
|
72
92
|
trackedChanges: TrackedChangesSnapshot,
|
|
73
93
|
): SuggestionsSnapshot {
|
|
@@ -89,6 +109,8 @@ export function createSuggestionsSnapshot(
|
|
|
89
109
|
const kind = toSemanticKind(primary);
|
|
90
110
|
const status = summarizeStatus(revisions);
|
|
91
111
|
const actionability = summarizeActionability(revisions);
|
|
112
|
+
const commentThreadIds = collectLinkedCommentThreadIds(revisions);
|
|
113
|
+
const replyCount = countLinkedReplies(revisions);
|
|
92
114
|
return {
|
|
93
115
|
suggestionId,
|
|
94
116
|
kind,
|
|
@@ -108,6 +130,8 @@ export function createSuggestionsSnapshot(
|
|
|
108
130
|
preserveOnlyReason: primary.preserveOnlyReason,
|
|
109
131
|
excerpt: primary.excerpt,
|
|
110
132
|
detail: primary.detail,
|
|
133
|
+
...(commentThreadIds ? { commentThreadIds } : {}),
|
|
134
|
+
...(replyCount > 0 ? { replyCount } : {}),
|
|
111
135
|
};
|
|
112
136
|
})
|
|
113
137
|
.sort(compareSuggestions);
|
|
@@ -1536,7 +1536,34 @@ function appendInlineSegments(
|
|
|
1536
1536
|
node.fieldFamily === "NOTEREF" ||
|
|
1537
1537
|
node.fieldFamily === "TOC" ||
|
|
1538
1538
|
node.fieldFamily === "PAGE" ||
|
|
1539
|
-
node.fieldFamily === "NUMPAGES"
|
|
1539
|
+
node.fieldFamily === "NUMPAGES" ||
|
|
1540
|
+
node.fieldFamily === "SECTIONPAGES";
|
|
1541
|
+
const isPageScopedField =
|
|
1542
|
+
node.fieldFamily === "PAGE" ||
|
|
1543
|
+
node.fieldFamily === "NUMPAGES" ||
|
|
1544
|
+
node.fieldFamily === "SECTIONPAGES";
|
|
1545
|
+
if (isPageScopedField) {
|
|
1546
|
+
const fieldLabel =
|
|
1547
|
+
node.fieldFamily === "PAGE"
|
|
1548
|
+
? "Current page number"
|
|
1549
|
+
: node.fieldFamily === "NUMPAGES"
|
|
1550
|
+
? "Total pages"
|
|
1551
|
+
: "Section pages";
|
|
1552
|
+
const displayText = flattenSurfaceFieldDisplayText(node.children);
|
|
1553
|
+
paragraph.segments.push({
|
|
1554
|
+
segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
|
|
1555
|
+
kind: "field_ref",
|
|
1556
|
+
from: start,
|
|
1557
|
+
to: start + 1,
|
|
1558
|
+
fieldFamily: node.fieldFamily,
|
|
1559
|
+
fieldTarget: node.fieldTarget,
|
|
1560
|
+
instruction: node.instruction,
|
|
1561
|
+
refreshStatus: node.refreshStatus ?? "stale",
|
|
1562
|
+
label: fieldLabel,
|
|
1563
|
+
...(displayText ? { displayText } : {}),
|
|
1564
|
+
} as SurfaceInlineSegment);
|
|
1565
|
+
return { nextCursor: start + 1, lockedFragmentIds: [] };
|
|
1566
|
+
}
|
|
1540
1567
|
if (node.children && node.children.length > 0) {
|
|
1541
1568
|
// For REF \h, pass the bookmark as a hyperlink href so child text gets hyperlink styling
|
|
1542
1569
|
const refHyperlinkHref =
|
|
@@ -1574,7 +1601,9 @@ function appendInlineSegments(
|
|
|
1574
1601
|
? "Current page number"
|
|
1575
1602
|
: node.fieldFamily === "NUMPAGES"
|
|
1576
1603
|
? "Total pages"
|
|
1577
|
-
:
|
|
1604
|
+
: node.fieldFamily === "SECTIONPAGES"
|
|
1605
|
+
? "Section pages"
|
|
1606
|
+
: `${node.fieldFamily ?? "Field"}: ${node.fieldTarget ?? node.instruction.trim()}`;
|
|
1578
1607
|
paragraph.segments.push({
|
|
1579
1608
|
segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
|
|
1580
1609
|
kind: "field_ref",
|
|
@@ -1743,6 +1772,34 @@ function normalizeSafeCssHexColor(value: string | undefined): string | undefined
|
|
|
1743
1772
|
return trimmed.replace(/^#/, "").toUpperCase();
|
|
1744
1773
|
}
|
|
1745
1774
|
|
|
1775
|
+
function flattenSurfaceFieldDisplayText(
|
|
1776
|
+
children: readonly InlineNode[] | undefined,
|
|
1777
|
+
): string {
|
|
1778
|
+
if (!children || children.length === 0) return "";
|
|
1779
|
+
const parts: string[] = [];
|
|
1780
|
+
for (const child of children) {
|
|
1781
|
+
switch (child.type) {
|
|
1782
|
+
case "text":
|
|
1783
|
+
parts.push(child.text);
|
|
1784
|
+
break;
|
|
1785
|
+
case "tab":
|
|
1786
|
+
case "hard_break":
|
|
1787
|
+
parts.push(" ");
|
|
1788
|
+
break;
|
|
1789
|
+
case "symbol":
|
|
1790
|
+
parts.push(child.char ? String.fromCodePoint(parseInt(child.char, 16)) : "\uFFFD");
|
|
1791
|
+
break;
|
|
1792
|
+
case "field":
|
|
1793
|
+
case "hyperlink":
|
|
1794
|
+
parts.push(flattenSurfaceFieldDisplayText(child.children));
|
|
1795
|
+
break;
|
|
1796
|
+
default:
|
|
1797
|
+
break;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
return parts.join("");
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1746
1803
|
/**
|
|
1747
1804
|
* V2c.5 — Extract the first paragraph's plain text from a parsed
|
|
1748
1805
|
* `txbxBlocks` tree for the `txbxText` segment preview. The recursion
|
|
@@ -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
|
}
|