@beyondwork/docx-react-component 1.0.88 → 1.0.90
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/v3/_runtime-handle.ts +5 -0
- package/src/api/v3/ai/replacement.ts +82 -0
- package/src/api/v3/runtime/content.ts +3 -0
- package/src/api/v3/runtime/formatting.ts +64 -0
- package/src/core/commands/formatting-commands.ts +107 -0
- package/src/core/state/text-transaction.ts +11 -4
- package/src/runtime/document-runtime.ts +51 -0
- package/src/runtime/scopes/_scope-dependencies.ts +1 -0
- package/src/runtime/scopes/action-validation.ts +12 -3
- package/src/runtime/scopes/audit-bundle.ts +2 -2
- package/src/runtime/scopes/compiler-service.ts +70 -0
- package/src/runtime/scopes/formatting/apply.ts +262 -0
- package/src/runtime/scopes/index.ts +12 -0
- package/src/runtime/scopes/replacement/propose.ts +2 -0
- package/src/runtime/scopes/scope-kinds/paragraph.ts +1 -0
- package/src/runtime/scopes/semantic-scope-types.ts +48 -4
- package/src/runtime/scopes/workflow-overlap.ts +9 -11
- package/src/shell/session-bootstrap.ts +1 -0
- package/src/ui/WordReviewEditor.tsx +277 -28
- package/src/ui/editor-command-bag.ts +11 -0
- package/src/ui/editor-shell-view.tsx +10 -0
- package/src/ui/headless/chrome-registry.ts +6 -6
- package/src/ui/headless/role-action-sets.ts +4 -10
- package/src/ui/headless/selection-tool-resolver.ts +11 -0
- package/src/ui-tailwind/chrome/editor-action-registry.ts +1 -1
- package/src/ui-tailwind/chrome/tw-context-band.tsx +7 -7
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +13 -18
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +8 -5
- package/src/ui-tailwind/chrome/tw-inline-find-bar.tsx +100 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +0 -40
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +9 -7
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +17 -1
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +6 -1
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +17 -7
- package/src/ui-tailwind/editor-surface/preserve-position.ts +30 -5
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +2 -1
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +5 -1
- package/src/ui-tailwind/review/tw-review-rail.tsx +5 -0
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +32 -38
- package/src/ui-tailwind/review-workspace/types.ts +2 -0
- package/src/ui-tailwind/theme/editor-theme.css +25 -12
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +20 -37
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +15 -27
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +24 -15
- package/src/ui-tailwind/tw-review-workspace.tsx +32 -18
- 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 {
|
|
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:
|
|
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:
|
|
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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")
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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,
|