@beyondwork/docx-react-component 1.0.87 → 1.0.89

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.
Files changed (47) hide show
  1. package/package.json +1 -1
  2. package/src/api/v3/_runtime-handle.ts +5 -0
  3. package/src/api/v3/ai/replacement.ts +82 -0
  4. package/src/api/v3/runtime/content.ts +3 -0
  5. package/src/api/v3/runtime/formatting.ts +64 -0
  6. package/src/core/commands/formatting-commands.ts +107 -0
  7. package/src/core/state/text-transaction.ts +11 -4
  8. package/src/runtime/document-runtime.ts +293 -27
  9. package/src/runtime/scopes/_scope-dependencies.ts +1 -0
  10. package/src/runtime/scopes/action-validation.ts +12 -3
  11. package/src/runtime/scopes/audit-bundle.ts +2 -2
  12. package/src/runtime/scopes/compiler-service.ts +70 -0
  13. package/src/runtime/scopes/formatting/apply.ts +262 -0
  14. package/src/runtime/scopes/index.ts +12 -0
  15. package/src/runtime/scopes/replacement/propose.ts +2 -0
  16. package/src/runtime/scopes/scope-kinds/paragraph.ts +1 -0
  17. package/src/runtime/scopes/semantic-scope-types.ts +48 -4
  18. package/src/runtime/scopes/workflow-overlap.ts +9 -11
  19. package/src/shell/session-bootstrap.ts +1 -0
  20. package/src/ui/WordReviewEditor.tsx +277 -28
  21. package/src/ui/editor-command-bag.ts +11 -0
  22. package/src/ui/editor-shell-view.tsx +10 -0
  23. package/src/ui/headless/chrome-registry.ts +6 -6
  24. package/src/ui/headless/role-action-sets.ts +4 -10
  25. package/src/ui/headless/selection-tool-resolver.ts +11 -0
  26. package/src/ui-tailwind/chrome/editor-action-registry.ts +1 -1
  27. package/src/ui-tailwind/chrome/tw-context-band.tsx +7 -7
  28. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +13 -18
  29. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +8 -5
  30. package/src/ui-tailwind/chrome/tw-inline-find-bar.tsx +100 -0
  31. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +0 -40
  32. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +9 -7
  33. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +17 -1
  34. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +6 -1
  35. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +17 -7
  36. package/src/ui-tailwind/editor-surface/preserve-position.ts +61 -11
  37. package/src/ui-tailwind/editor-surface/scroll-anchor.ts +20 -5
  38. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +52 -6
  39. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +5 -1
  40. package/src/ui-tailwind/review/tw-review-rail.tsx +5 -0
  41. package/src/ui-tailwind/review/tw-workflow-tab.tsx +32 -38
  42. package/src/ui-tailwind/review-workspace/types.ts +2 -0
  43. package/src/ui-tailwind/theme/editor-theme.css +25 -12
  44. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +13 -4
  45. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +6 -15
  46. package/src/ui-tailwind/tw-review-workspace.tsx +28 -18
  47. package/src/ui-tailwind/workflow-scope-layers.ts +70 -0
@@ -63,12 +63,18 @@ import {
63
63
  applyScopeReplacement,
64
64
  type ApplyScopeReplacementResult,
65
65
  } from "./replacement/apply.ts";
66
+ import {
67
+ applyScopeFormatting,
68
+ type ApplyScopeFormattingResult,
69
+ } from "./formatting/apply.ts";
66
70
  import { proposeReplacement } from "./replacement/propose.ts";
67
71
  import type {
68
72
  ReplacementOperationKind,
69
73
  ReplacementPreservePolicy,
70
74
  ReplacementScope,
71
75
  RuntimeOperationPlan,
76
+ ScopeFormattingAction,
77
+ ScopeFormattingScope,
72
78
  ScopeBundle,
73
79
  ScopeHandle,
74
80
  SemanticScope,
@@ -95,6 +101,7 @@ export interface CompilerServiceRuntime {
95
101
  setWorkflowMetadataEntries(entries: readonly WorkflowMetadataEntry[]): void;
96
102
  getSessionState(): ScopeSessionState;
97
103
  applyScopeReplacement(plan: RuntimeOperationPlan): void;
104
+ applyScopeFormatting(plan: RuntimeOperationPlan): boolean;
98
105
  debug?: { bus: TelemetryBus };
99
106
  }
100
107
 
@@ -123,6 +130,7 @@ export interface ApplyReplacementRequest {
123
130
  * dispatch shipped (see `cross-layer-coord-08.md §8`).
124
131
  */
125
132
  readonly proposedContent?: ReplacementScope["proposedContent"];
133
+ readonly formatting?: ReplacementScope["formatting"];
126
134
  readonly preserve?: ReplacementPreservePolicy;
127
135
  readonly reason?: string;
128
136
  readonly actionId?: AIAction;
@@ -131,6 +139,16 @@ export interface ApplyReplacementRequest {
131
139
  readonly emittedAtUtc: string;
132
140
  }
133
141
 
142
+ export interface ApplyFormattingRequest {
143
+ readonly targetScopeId: string;
144
+ readonly action: ScopeFormattingAction;
145
+ readonly reason?: string;
146
+ readonly actionId?: AIAction;
147
+ readonly actorId: string;
148
+ readonly origin: "ui" | "agent" | "host";
149
+ readonly emittedAtUtc: string;
150
+ }
151
+
134
152
  export interface AttachExplanationRequest {
135
153
  readonly scopeId: string;
136
154
  readonly explanation: string;
@@ -181,6 +199,7 @@ export interface ScopeCompilerService {
181
199
  input: {
182
200
  readonly operation: ReplacementOperationKind;
183
201
  readonly proposedText?: string;
202
+ readonly formatting?: ReplacementScope["formatting"];
184
203
  readonly preserve?: ReplacementPreservePolicy;
185
204
  readonly reason?: string;
186
205
  readonly proposedAtUtc: string;
@@ -196,6 +215,13 @@ export interface ScopeCompilerService {
196
215
  */
197
216
  applyReplacement(request: ApplyReplacementRequest): ApplyScopeReplacementResult;
198
217
 
218
+ /**
219
+ * Apply an exact text-mark formatting action to a resolved scope. This
220
+ * is the supported route for agent/user scope edits such as "remove only
221
+ * highlight" because it does not replace the text payload.
222
+ */
223
+ applyFormatting(request: ApplyFormattingRequest): ApplyScopeFormattingResult;
224
+
199
225
  /**
200
226
  * Attach an agent-authored explanation via the Layer-06
201
227
  * `attachScopeMetadata` writer. Resolves against the workflow
@@ -315,6 +341,7 @@ export function createScopeCompilerService(
315
341
  typeof input.proposedText === "string"
316
342
  ? { kind: "text", text: input.proposedText }
317
343
  : { kind: "text", text: "" },
344
+ ...(input.formatting ? { formatting: input.formatting } : {}),
318
345
  ...(input.preserve ? { preserve: input.preserve } : {}),
319
346
  ...(input.reason ? { reason: input.reason } : {}),
320
347
  proposedAtUtc: input.proposedAtUtc,
@@ -350,6 +377,7 @@ export function createScopeCompilerService(
350
377
  targetHandle: compiled.scope.handle,
351
378
  operation: request.operation,
352
379
  proposedContent: request.proposedContent,
380
+ ...(request.formatting ? { formatting: request.formatting } : {}),
353
381
  ...(request.preserve ? { preserve: request.preserve } : {}),
354
382
  ...(request.reason ? { reason: request.reason } : {}),
355
383
  proposedAtUtc: request.emittedAtUtc,
@@ -359,6 +387,7 @@ export function createScopeCompilerService(
359
387
  ...(request.proposedText !== undefined
360
388
  ? { proposedText: request.proposedText }
361
389
  : {}),
390
+ ...(request.formatting ? { formatting: request.formatting } : {}),
362
391
  ...(request.preserve ? { preserve: request.preserve } : {}),
363
392
  ...(request.reason ? { reason: request.reason } : {}),
364
393
  proposedAtUtc: request.emittedAtUtc,
@@ -381,6 +410,47 @@ export function createScopeCompilerService(
381
410
  });
382
411
  },
383
412
 
413
+ applyFormatting(request: ApplyFormattingRequest): ApplyScopeFormattingResult {
414
+ const compiled = this.compileScopeById(request.targetScopeId);
415
+ if (!compiled) {
416
+ return {
417
+ applied: false,
418
+ reason: "scope-not-resolvable",
419
+ validation: {
420
+ safe: false,
421
+ blockedReasons: Object.freeze([
422
+ `scope-not-resolvable:${request.targetScopeId}`,
423
+ ]),
424
+ warnings: Object.freeze([]),
425
+ },
426
+ authoredRevisionIds: Object.freeze([]),
427
+ };
428
+ }
429
+ const proposed: ScopeFormattingScope = {
430
+ targetHandle: compiled.scope.handle,
431
+ operation: "formatting",
432
+ action: request.action,
433
+ ...(request.reason ? { reason: request.reason } : {}),
434
+ proposedAtUtc: request.emittedAtUtc,
435
+ };
436
+ return applyScopeFormatting({
437
+ sink: {
438
+ getCanonicalDocument: () => runtime.getCanonicalDocument(),
439
+ getWorkflowOverlay: () => runtime.getWorkflowOverlay(),
440
+ getInteractionGuardSnapshot: () =>
441
+ runtime.getInteractionGuardSnapshot(),
442
+ getCompatibilityReport: () => runtime.getCompatibilityReport(),
443
+ applyScopeFormatting: (plan) => runtime.applyScopeFormatting(plan),
444
+ },
445
+ proposed,
446
+ ...(request.actionId ? { actionId: request.actionId } : {}),
447
+ actorId: request.actorId,
448
+ origin: request.origin,
449
+ emittedAtUtc: request.emittedAtUtc,
450
+ ...(runtime.debug?.bus ? { bus: runtime.debug.bus } : {}),
451
+ });
452
+ },
453
+
384
454
  attachExplanation(
385
455
  request: AttachExplanationRequest,
386
456
  ): AttachExplanationResult {
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Scope-scoped formatting apply pipeline.
3
+ *
4
+ * This is intentionally parallel to replacement/apply.ts but narrower:
5
+ * the caller supplies a scope id plus an exact text-mark action, we resolve
6
+ * the scope on live state, validate global/scope guards, lower to one
7
+ * formatting-apply runtime step, dispatch once, and emit exactly one
8
+ * ScopeActionAudit when the document actually changed.
9
+ */
10
+
11
+ import type { TelemetryBus } from "../../debug/telemetry-bus.ts";
12
+ import type { AIAction } from "../../workflow/ai-action-policy.ts";
13
+ import type {
14
+ CompatibilityReport,
15
+ InteractionGuardSnapshot,
16
+ WorkflowOverlay,
17
+ } from "../_scope-dependencies.ts";
18
+ import type { CanonicalDocumentEnvelope } from "../../../core/state/editor-state.ts";
19
+
20
+ import { composeScopeValidation } from "../action-validation.ts";
21
+ import {
22
+ buildParagraphIndexMap,
23
+ buildSectionIndexByBlockIndex,
24
+ compileScope,
25
+ } from "../compile-scope.ts";
26
+ import { enumerateScopes, type EnumeratedScope } from "../enumerate-scopes.ts";
27
+ import { emitScopeActionAudit } from "../audit-bundle.ts";
28
+ import { buildScopePositionMap } from "../position-map.ts";
29
+ import { resolveScopeRange } from "../scope-range.ts";
30
+ import type {
31
+ RuntimeOperationPlan,
32
+ ScopeActionAudit,
33
+ ScopeFormattingScope,
34
+ SemanticScope,
35
+ ValidationResult,
36
+ } from "../semantic-scope-types.ts";
37
+
38
+ export interface ApplyScopeFormattingSink {
39
+ readonly getCanonicalDocument: () => CanonicalDocumentEnvelope;
40
+ readonly getWorkflowOverlay: () => WorkflowOverlay | null;
41
+ readonly getInteractionGuardSnapshot: () => InteractionGuardSnapshot;
42
+ readonly getCompatibilityReport: () => CompatibilityReport;
43
+ readonly applyScopeFormatting: (plan: RuntimeOperationPlan) => boolean;
44
+ }
45
+
46
+ export interface ApplyScopeFormattingInputs {
47
+ readonly sink: ApplyScopeFormattingSink;
48
+ readonly proposed: ScopeFormattingScope;
49
+ readonly actionId?: AIAction;
50
+ readonly actorId: string;
51
+ readonly origin: "ui" | "agent" | "host";
52
+ readonly emittedAtUtc: string;
53
+ readonly bus?: TelemetryBus;
54
+ }
55
+
56
+ export interface ApplyScopeFormattingResult {
57
+ readonly applied: boolean;
58
+ readonly reason?: string;
59
+ readonly validation: ValidationResult;
60
+ readonly audit?: ScopeActionAudit;
61
+ readonly plan?: RuntimeOperationPlan;
62
+ readonly scope?: SemanticScope;
63
+ readonly authoredRevisionIds: readonly string[];
64
+ }
65
+
66
+ function documentHash(doc: CanonicalDocumentEnvelope): string {
67
+ let textLength = 0;
68
+ let markSignatureLength = 0;
69
+ for (const block of doc.content.children) {
70
+ if (block.type !== "paragraph") continue;
71
+ for (const child of block.children) {
72
+ if (child.type !== "text") continue;
73
+ textLength += child.text.length;
74
+ markSignatureLength += JSON.stringify(child.marks ?? []).length;
75
+ }
76
+ }
77
+ return `blocks:${doc.content.children.length}|text:${textLength}|marks:${markSignatureLength}`;
78
+ }
79
+
80
+ function compileFormattingPlan(
81
+ scope: SemanticScope,
82
+ enumeratedScope: EnumeratedScope,
83
+ proposed: ScopeFormattingScope,
84
+ document: CanonicalDocumentEnvelope,
85
+ posture: "direct-edit" | "suggest-mode",
86
+ ): RuntimeOperationPlan | null {
87
+ if (posture === "suggest-mode") {
88
+ return null;
89
+ }
90
+
91
+ switch (enumeratedScope.kind) {
92
+ case "paragraph":
93
+ case "heading":
94
+ case "list-item":
95
+ case "scope":
96
+ break;
97
+ default:
98
+ return null;
99
+ }
100
+
101
+ const positionMap = buildScopePositionMap(document);
102
+ const range = resolveScopeRange(enumeratedScope, scope.handle, positionMap);
103
+ if (!range || range.to <= range.from) {
104
+ return null;
105
+ }
106
+ if (!Number.isInteger(range.from) || !Number.isInteger(range.to)) {
107
+ return null;
108
+ }
109
+
110
+ return {
111
+ scopeId: proposed.targetHandle.scopeId,
112
+ targetKind: scope.kind,
113
+ operation: "formatting",
114
+ steps: Object.freeze([
115
+ {
116
+ kind: "formatting-apply",
117
+ summary:
118
+ proposed.action.kind === "clear-mark"
119
+ ? `clear ${proposed.action.mark} mark in ${scope.kind} scope [${range.from}..${range.to}]`
120
+ : `set ${proposed.action.mark.type} mark in ${scope.kind} scope [${range.from}..${range.to}]`,
121
+ range: { from: range.from, to: range.to },
122
+ formattingAction: proposed.action,
123
+ },
124
+ ]),
125
+ posture,
126
+ };
127
+ }
128
+
129
+ export function applyScopeFormatting(
130
+ inputs: ApplyScopeFormattingInputs,
131
+ ): ApplyScopeFormattingResult {
132
+ const proposed = inputs.proposed;
133
+ const docBefore = inputs.sink.getCanonicalDocument();
134
+ const overlay = inputs.sink.getWorkflowOverlay() ?? undefined;
135
+ const paragraphIndexByBlockIndex = buildParagraphIndexMap(docBefore);
136
+ const sectionIndexByBlockIndex = buildSectionIndexByBlockIndex(docBefore);
137
+
138
+ let resolvedScope: SemanticScope | null = null;
139
+ let resolvedEnumerated: EnumeratedScope | null = null;
140
+ const enumerateInputs = overlay ? { overlay } : {};
141
+ for (const entry of enumerateScopes(docBefore, enumerateInputs)) {
142
+ if (entry.handle.scopeId !== proposed.targetHandle.scopeId) continue;
143
+ const compiled = compileScope(entry, {
144
+ document: docBefore,
145
+ ...(overlay ? { overlay } : {}),
146
+ paragraphIndexByBlockIndex,
147
+ sectionIndexByBlockIndex,
148
+ });
149
+ if (compiled) {
150
+ resolvedScope = compiled;
151
+ resolvedEnumerated = entry;
152
+ break;
153
+ }
154
+ }
155
+
156
+ if (!resolvedScope || !resolvedEnumerated) {
157
+ const validation: ValidationResult = {
158
+ safe: false,
159
+ blockedReasons: Object.freeze([
160
+ `scope-not-resolvable:${proposed.targetHandle.scopeId}`,
161
+ ]),
162
+ warnings: Object.freeze([]),
163
+ };
164
+ return {
165
+ applied: false,
166
+ reason: "scope-not-resolvable",
167
+ validation,
168
+ authoredRevisionIds: Object.freeze([]),
169
+ };
170
+ }
171
+
172
+ const verdict = composeScopeValidation({
173
+ scope: resolvedScope,
174
+ operation: "formatting",
175
+ proposedContent: { kind: "text", text: "" },
176
+ runtime: {
177
+ getInteractionGuardSnapshot: () => inputs.sink.getInteractionGuardSnapshot(),
178
+ getCompatibilityReport: () => inputs.sink.getCompatibilityReport(),
179
+ },
180
+ document: docBefore,
181
+ enumeratedScope: resolvedEnumerated,
182
+ skipPreservation: true,
183
+ ...(inputs.actionId ? { actionId: inputs.actionId } : {}),
184
+ });
185
+
186
+ if (!verdict.safe) {
187
+ return {
188
+ applied: false,
189
+ reason: "validation-blocked",
190
+ validation: verdict,
191
+ scope: resolvedScope,
192
+ authoredRevisionIds: Object.freeze([]),
193
+ };
194
+ }
195
+
196
+ const posture: "direct-edit" | "suggest-mode" =
197
+ verdict.warnings.some((w) => w.code === "guard:suggest-mode")
198
+ ? "suggest-mode"
199
+ : "direct-edit";
200
+ const plan = compileFormattingPlan(
201
+ resolvedScope,
202
+ resolvedEnumerated,
203
+ proposed,
204
+ docBefore,
205
+ posture,
206
+ );
207
+ if (!plan) {
208
+ const blocker =
209
+ posture === "suggest-mode"
210
+ ? `compile-refused:${resolvedScope.kind}:formatting-suggesting-not-implemented`
211
+ : `compile-refused:${resolvedScope.kind}:formatting-not-implemented`;
212
+ const refused: ValidationResult = {
213
+ safe: false,
214
+ blockedReasons: Object.freeze([blocker]),
215
+ warnings: verdict.warnings,
216
+ };
217
+ return {
218
+ applied: false,
219
+ reason: blocker,
220
+ validation: refused,
221
+ scope: resolvedScope,
222
+ authoredRevisionIds: Object.freeze([]),
223
+ };
224
+ }
225
+
226
+ const documentHashBefore = documentHash(docBefore);
227
+ const changed = inputs.sink.applyScopeFormatting(plan);
228
+ if (!changed) {
229
+ return {
230
+ applied: false,
231
+ reason: "no-op",
232
+ validation: verdict,
233
+ plan,
234
+ scope: resolvedScope,
235
+ authoredRevisionIds: Object.freeze([]),
236
+ };
237
+ }
238
+
239
+ const docAfter = inputs.sink.getCanonicalDocument();
240
+ const audit = emitScopeActionAudit({
241
+ actionId: inputs.actionId ?? "fix_formatting",
242
+ actorId: inputs.actorId,
243
+ origin: inputs.origin,
244
+ documentHashBefore,
245
+ documentHashAfter: documentHash(docAfter),
246
+ targetScopeSnapshot: resolvedScope,
247
+ proposed,
248
+ plan,
249
+ validation: verdict,
250
+ emittedAtUtc: inputs.emittedAtUtc,
251
+ ...(inputs.bus ? { bus: inputs.bus } : {}),
252
+ });
253
+
254
+ return {
255
+ applied: true,
256
+ validation: verdict,
257
+ audit,
258
+ plan,
259
+ scope: resolvedScope,
260
+ authoredRevisionIds: Object.freeze([]),
261
+ };
262
+ }
@@ -26,10 +26,15 @@ export type {
26
26
  ReplacementOperationKind,
27
27
  ReplacementPreservePolicy,
28
28
  ReplacementScope,
29
+ ScopeActionOperationKind,
30
+ ScopeActionProposal,
29
31
  RuntimeOperationPlan,
30
32
  RuntimeOperationStep,
31
33
  RuntimeOperationStepKind,
32
34
  ScopeActionAudit,
35
+ ScopeFormattingAction,
36
+ ScopeFormattingClearTarget,
37
+ ScopeFormattingScope,
33
38
  ScopeBundle,
34
39
  ScopeBundleEvidence,
35
40
  ScopeBundleNeighborhood,
@@ -128,6 +133,12 @@ export {
128
133
  type ApplyScopeReplacementResult,
129
134
  type ApplyScopeReplacementSink,
130
135
  } from "./replacement/apply.ts";
136
+ export {
137
+ applyScopeFormatting,
138
+ type ApplyScopeFormattingInputs,
139
+ type ApplyScopeFormattingResult,
140
+ type ApplyScopeFormattingSink,
141
+ } from "./formatting/apply.ts";
131
142
  export {
132
143
  attachExplanation,
133
144
  AI_EXPLANATION_METADATA_ID,
@@ -144,6 +155,7 @@ export {
144
155
  } from "./create-issue.ts";
145
156
  export {
146
157
  createScopeCompilerService,
158
+ type ApplyFormattingRequest,
147
159
  type ApplyReplacementRequest,
148
160
  type AttachExplanationRequest,
149
161
  type CompileScopeByIdResult,
@@ -23,6 +23,7 @@ export interface ReplacementProposalInput {
23
23
  readonly targetHandle: ScopeHandle;
24
24
  readonly operation: ReplacementOperationKind;
25
25
  readonly proposedContent: ReplacementScope["proposedContent"];
26
+ readonly formatting?: ReplacementScope["formatting"];
26
27
  readonly preserve?: ReplacementPreservePolicy;
27
28
  readonly reason?: string;
28
29
  readonly proposedAtUtc: string;
@@ -35,6 +36,7 @@ export function proposeReplacement(
35
36
  targetHandle: input.targetHandle,
36
37
  operation: input.operation,
37
38
  proposedContent: input.proposedContent,
39
+ ...(input.formatting ? { formatting: input.formatting } : {}),
38
40
  ...(input.preserve ? { preserve: input.preserve } : {}),
39
41
  ...(input.reason ? { reason: input.reason } : {}),
40
42
  proposedAtUtc: input.proposedAtUtc,
@@ -299,6 +299,7 @@ export function compileParagraphReplacement(
299
299
  : `suggest-mode ${summaryScope} text replace (len ${text.length})`,
300
300
  range: { from: effectiveRange.from, to: effectiveRange.to },
301
301
  text,
302
+ ...(proposed.formatting ? { formatting: proposed.formatting } : {}),
302
303
  },
303
304
  ]),
304
305
  ...(proposed.preserve ? { preserve: proposed.preserve } : {}),
@@ -24,7 +24,11 @@
24
24
  * compatibility event.
25
25
  */
26
26
 
27
- import type { EditorStoryTarget } from "./_scope-dependencies.ts";
27
+ import type { TextMark } from "../../model/canonical-document.ts";
28
+ import type {
29
+ EditorStoryTarget,
30
+ TextFormattingDirective,
31
+ } from "./_scope-dependencies.ts";
28
32
 
29
33
  /**
30
34
  * 13-value kind taxonomy — purely structural.
@@ -317,6 +321,24 @@ export interface ReplacementPreservePolicy {
317
321
  readonly opaqueFragments?: boolean;
318
322
  }
319
323
 
324
+ export type ScopeFormattingClearTarget = TextMark["type"] | "visualHighlight";
325
+
326
+ export type ScopeFormattingAction =
327
+ | {
328
+ readonly kind: "clear-mark";
329
+ /**
330
+ * Exact source-layer clear. Use `"highlight"` to remove only
331
+ * `w:highlight`, `"backgroundColor"` to remove only shading, and
332
+ * `"visualHighlight"` to remove both visual highlight layers.
333
+ */
334
+ readonly mark: ScopeFormattingClearTarget;
335
+ }
336
+ | {
337
+ readonly kind: "set-mark";
338
+ /** Full canonical mark to apply over the target scope range. */
339
+ readonly mark: TextMark;
340
+ };
341
+
320
342
  export interface ReplacementScope {
321
343
  readonly targetHandle: ScopeHandle;
322
344
  readonly operation: ReplacementOperationKind;
@@ -337,11 +359,26 @@ export interface ReplacementScope {
337
359
  */
338
360
  readonly structured?: unknown;
339
361
  };
362
+ /**
363
+ * Formatting directive for flat-text replacement. When absent, the
364
+ * runtime keeps the historic paragraph-default replacement behavior.
365
+ */
366
+ readonly formatting?: TextFormattingDirective;
340
367
  readonly preserve?: ReplacementPreservePolicy;
341
368
  readonly reason?: string;
342
369
  readonly proposedAtUtc: string;
343
370
  }
344
371
 
372
+ export interface ScopeFormattingScope {
373
+ readonly targetHandle: ScopeHandle;
374
+ readonly operation: "formatting";
375
+ readonly action: ScopeFormattingAction;
376
+ readonly reason?: string;
377
+ readonly proposedAtUtc: string;
378
+ }
379
+
380
+ export type ScopeActionProposal = ReplacementScope | ScopeFormattingScope;
381
+
345
382
  /**
346
383
  * Structural guard for `proposedContent.structured` when it carries a
347
384
  * fragment payload (L02's `CanonicalDocumentFragment`). Callers that
@@ -396,7 +433,8 @@ export type RuntimeOperationStepKind =
396
433
  | "text-replace"
397
434
  | "text-insert-tracked"
398
435
  | "text-delete-tracked"
399
- | "fragment-replace";
436
+ | "fragment-replace"
437
+ | "formatting-apply";
400
438
 
401
439
  export interface RuntimeOperationStep {
402
440
  readonly kind: RuntimeOperationStepKind;
@@ -412,6 +450,10 @@ export interface RuntimeOperationStep {
412
450
  };
413
451
  /** New text for text-replace / text-insert-tracked. */
414
452
  readonly text?: string;
453
+ /** Optional inserted-text formatting directive for text replacement. */
454
+ readonly formatting?: TextFormattingDirective;
455
+ /** Scope-bounded formatting action for formatting-apply. */
456
+ readonly formattingAction?: ScopeFormattingAction;
415
457
  /**
416
458
  * `CanonicalDocumentFragment`-shaped payload for `fragment-replace`.
417
459
  * Plain-value boundary (S9) — no live references; safe to serialise.
@@ -421,10 +463,12 @@ export interface RuntimeOperationStep {
421
463
  readonly fragment?: StructuredReplacementContent;
422
464
  }
423
465
 
466
+ export type ScopeActionOperationKind = ReplacementOperationKind | "formatting";
467
+
424
468
  export interface RuntimeOperationPlan {
425
469
  readonly scopeId: string;
426
470
  readonly targetKind: SemanticScopeKind;
427
- readonly operation: ReplacementOperationKind;
471
+ readonly operation: ScopeActionOperationKind;
428
472
  readonly steps: readonly RuntimeOperationStep[];
429
473
  readonly preserve?: ReplacementPreservePolicy;
430
474
  /** Posture the apply pipeline should dispatch under. */
@@ -438,7 +482,7 @@ export interface ScopeActionAudit {
438
482
  readonly documentHashBefore: string;
439
483
  readonly documentHashAfter?: string;
440
484
  readonly targetScopeSnapshot: SemanticScope;
441
- readonly proposed: ReplacementScope;
485
+ readonly proposed: ScopeActionProposal;
442
486
  readonly compiledOperations: readonly {
443
487
  readonly kind: string;
444
488
  readonly summary: string;
@@ -3,9 +3,9 @@
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 enforced `effectiveMode` across scopes
7
- * whose `guardPolicy` participates in editing + `blockedReasons` when a
8
- * read-only overlap blocks the scope.
6
+ * the range + the most-restrictive scope action posture. `guardPolicy`
7
+ * decides whether selection-driven edits are gated; it does not erase a
8
+ * target scope's own suggest/comment posture for scope-targeted writes.
9
9
  *
10
10
  * The most-restrictive rule matches layer 06's `InteractionGuardSnapshot`
11
11
  * composition for guard-participating scopes (see
@@ -37,10 +37,6 @@ function getScopeGuardPolicy(scope: WorkflowScope): WorkflowScopeGuardPolicy {
37
37
  return scope.guardPolicy ?? (scope.mode === "view" ? "read-only" : "none");
38
38
  }
39
39
 
40
- function getScopeGuardMode(scope: WorkflowScope): WorkflowMode {
41
- return getScopeGuardPolicy(scope) === "read-only" ? "view" : scope.mode;
42
- }
43
-
44
40
  function modeRank(mode: WorkflowMode): number {
45
41
  // Higher = more restrictive (wins the merge).
46
42
  switch (mode) {
@@ -107,10 +103,12 @@ export function resolveWorkflowOverlap(
107
103
  if (!rangesOverlap(range, scopeRange)) continue;
108
104
  overlappingIds.push(scope.scopeId);
109
105
  const guardPolicy = getScopeGuardPolicy(scope);
110
- if (guardPolicy === "none") continue;
111
- const guardMode = getScopeGuardMode(scope);
112
- mode = mergeModes(mode, guardMode);
113
- if (guardMode === "view") {
106
+ if (guardPolicy === "none" && scope.mode === "view") {
107
+ continue;
108
+ }
109
+ const scopeMode = guardPolicy === "read-only" ? "view" : scope.mode;
110
+ mode = mergeModes(mode, scopeMode);
111
+ if (scopeMode === "view") {
114
112
  blockedReasons.push(`workflow-scope-view:${scope.scopeId}`);
115
113
  }
116
114
  }
@@ -1100,6 +1100,7 @@ function createLoadingRuntimeBridge(input: {
1100
1100
  replaceText: () => undefined,
1101
1101
  applyFormattingOperation: () => undefined,
1102
1102
  applyScopeReplacement: () => undefined,
1103
+ applyScopeFormatting: () => false,
1103
1104
  insertFragment: () => undefined,
1104
1105
  copy: () => undefined,
1105
1106
  cut: () => undefined,