@beyondwork/docx-react-component 1.0.41 → 1.0.43
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 +38 -37
- package/src/api/awareness-identity-types.ts +35 -0
- package/src/api/comment-negotiation-types.ts +130 -0
- package/src/api/comment-presentation-types.ts +106 -0
- package/src/api/editor-state-types.ts +110 -0
- package/src/api/external-custody-types.ts +74 -0
- package/src/api/participants-types.ts +18 -0
- package/src/api/public-types.ts +541 -5
- package/src/api/scope-metadata-resolver-types.ts +88 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +601 -9
- package/src/core/search/search-text.ts +15 -2
- package/src/index.ts +131 -1
- package/src/io/docx-session.ts +672 -2
- package/src/io/export/escape-xml-attribute.ts +26 -0
- package/src/io/export/external-send.ts +188 -0
- package/src/io/export/serialize-comments.ts +13 -16
- package/src/io/export/serialize-footnotes.ts +17 -24
- package/src/io/export/serialize-headers-footers.ts +17 -24
- package/src/io/export/serialize-main-document.ts +59 -62
- package/src/io/export/serialize-numbering.ts +20 -27
- package/src/io/export/serialize-runtime-revisions.ts +2 -9
- package/src/io/export/serialize-tables.ts +8 -15
- package/src/io/export/table-properties-xml.ts +25 -32
- package/src/io/import/external-reimport.ts +40 -0
- package/src/io/load-scheduler.ts +230 -0
- package/src/io/normalize/normalize-text.ts +83 -0
- package/src/io/ooxml/bw-xml.ts +244 -0
- package/src/io/ooxml/canonicalize-payload.ts +301 -0
- package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
- package/src/io/ooxml/comment-presentation-payload.ts +311 -0
- package/src/io/ooxml/external-custody-payload.ts +102 -0
- package/src/io/ooxml/participants-payload.ts +97 -0
- package/src/io/ooxml/payload-signature.ts +112 -0
- package/src/io/ooxml/workflow-payload-validator.ts +367 -0
- package/src/io/ooxml/workflow-payload.ts +317 -7
- package/src/runtime/awareness-identity.ts +173 -0
- package/src/runtime/collab/event-types.ts +27 -0
- package/src/runtime/collab-session-bridge.ts +157 -0
- package/src/runtime/collab-session-facet.ts +193 -0
- package/src/runtime/collab-session.ts +273 -0
- package/src/runtime/comment-negotiation-sync.ts +91 -0
- package/src/runtime/comment-negotiation.ts +158 -0
- package/src/runtime/comment-presentation.ts +223 -0
- package/src/runtime/document-runtime.ts +639 -124
- package/src/runtime/editor-state-channel.ts +544 -0
- package/src/runtime/editor-state-integration.ts +217 -0
- package/src/runtime/external-send-runtime.ts +117 -0
- package/src/runtime/layout/docx-font-loader.ts +11 -30
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +4 -0
- package/src/runtime/layout/layout-engine-instance.ts +139 -14
- package/src/runtime/layout/page-graph.ts +79 -7
- package/src/runtime/layout/paginated-layout-engine.ts +441 -48
- package/src/runtime/layout/public-facet.ts +585 -14
- package/src/runtime/layout/table-row-split.ts +316 -0
- package/src/runtime/markdown-sanitizer.ts +132 -0
- package/src/runtime/participants.ts +134 -0
- package/src/runtime/perf-counters.ts +28 -0
- package/src/runtime/render/render-frame-types.ts +17 -0
- package/src/runtime/render/render-kernel.ts +172 -29
- package/src/runtime/resign-payload.ts +120 -0
- package/src/runtime/surface-projection.ts +10 -5
- package/src/runtime/tamper-gate.ts +157 -0
- package/src/runtime/workflow-markup.ts +80 -16
- package/src/runtime/workflow-rail-segments.ts +244 -5
- package/src/ui/WordReviewEditor.tsx +654 -45
- package/src/ui/editor-command-bag.ts +14 -0
- package/src/ui/editor-runtime-boundary.ts +111 -11
- package/src/ui/editor-shell-view.tsx +21 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-helpers.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
- package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
- package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
- package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
- package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +106 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
- package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -1
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +167 -17
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +35 -7
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +10 -256
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
- package/src/ui-tailwind/index.ts +37 -1
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
- package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
- package/src/ui-tailwind/theme/editor-theme.css +25 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +455 -118
|
@@ -22,28 +22,32 @@ import type {
|
|
|
22
22
|
WorkflowCommentMarkup,
|
|
23
23
|
} from "../api/public-types";
|
|
24
24
|
import { MAIN_STORY_TARGET } from "../core/selection/mapping.ts";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
projectSurfaceText,
|
|
27
|
+
searchProjectedSurfaceText,
|
|
28
|
+
} from "../core/search/search-text.ts";
|
|
26
29
|
import { describeOpaqueFragment, isBlockedImportFeatureKey } from "../preservation/store.ts";
|
|
27
30
|
import type { CanonicalDocumentEnvelope } from "../core/state/editor-state.ts";
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Surface-derived markup (highlights + block-level opaque fragments).
|
|
34
|
+
*
|
|
35
|
+
* Pure function of `(surface, preservation)` — extracted from
|
|
36
|
+
* `collectWorkflowMarkupSnapshot` so callers can cache the expensive walk
|
|
37
|
+
* separately from the cheap reference-equal inputs (metadata, comments,
|
|
38
|
+
* revisions, protected ranges).
|
|
39
|
+
*/
|
|
40
|
+
export function collectWorkflowSurfaceMarkup(
|
|
41
|
+
surface: RuntimeRenderSnapshot["surface"],
|
|
42
|
+
preservation: CanonicalDocumentEnvelope["preservation"],
|
|
43
|
+
): { highlights: WorkflowHighlightMarkup[]; opaqueFragments: WorkflowOpaqueFragmentMarkup[] } {
|
|
36
44
|
const highlights: WorkflowHighlightMarkup[] = [];
|
|
37
|
-
const metadata = collectWorkflowMetadataMarkup(input.workflowMetadataSnapshot);
|
|
38
|
-
const fields: WorkflowFieldMarkup[] = [];
|
|
39
45
|
const opaqueFragments: WorkflowOpaqueFragmentMarkup[] = [];
|
|
40
|
-
const surface = input.renderSnapshot.surface;
|
|
41
|
-
|
|
42
46
|
if (surface) {
|
|
43
47
|
collectSurfaceMarkup(
|
|
44
48
|
surface.blocks,
|
|
45
49
|
MAIN_STORY_TARGET,
|
|
46
|
-
|
|
50
|
+
preservation,
|
|
47
51
|
highlights,
|
|
48
52
|
opaqueFragments,
|
|
49
53
|
);
|
|
@@ -51,16 +55,55 @@ export function collectWorkflowMarkupSnapshot(input: {
|
|
|
51
55
|
collectSurfaceMarkup(
|
|
52
56
|
story.blocks,
|
|
53
57
|
story.target,
|
|
54
|
-
|
|
58
|
+
preservation,
|
|
55
59
|
highlights,
|
|
56
60
|
opaqueFragments,
|
|
57
61
|
);
|
|
58
62
|
}
|
|
63
|
+
}
|
|
64
|
+
return { highlights, opaqueFragments };
|
|
65
|
+
}
|
|
59
66
|
|
|
67
|
+
export function collectWorkflowMarkupSnapshot(input: {
|
|
68
|
+
renderSnapshot: RuntimeRenderSnapshot;
|
|
69
|
+
fieldSnapshot: FieldSnapshot;
|
|
70
|
+
protectionSnapshot: ProtectionSnapshot;
|
|
71
|
+
preservation: CanonicalDocumentEnvelope["preservation"];
|
|
72
|
+
workflowMetadataSnapshot?: WorkflowMetadataSnapshot;
|
|
73
|
+
surfaceMarkupCache?: {
|
|
74
|
+
highlights: WorkflowHighlightMarkup[];
|
|
75
|
+
opaqueFragments: WorkflowOpaqueFragmentMarkup[];
|
|
76
|
+
};
|
|
77
|
+
perfStage?: (name: string, durationMs: number) => void;
|
|
78
|
+
}): WorkflowMarkupSnapshot {
|
|
79
|
+
const perf = input.perfStage;
|
|
80
|
+
const stageStart = perf ? () => performance.now() : () => 0;
|
|
81
|
+
const stageEnd = perf ? (name: string, t0: number) => perf(name, performance.now() - t0) : () => {};
|
|
82
|
+
|
|
83
|
+
const tMeta = stageStart();
|
|
84
|
+
const metadata = collectWorkflowMetadataMarkup(input.workflowMetadataSnapshot);
|
|
85
|
+
stageEnd("metadata", tMeta);
|
|
86
|
+
|
|
87
|
+
const fields: WorkflowFieldMarkup[] = [];
|
|
88
|
+
const surface = input.renderSnapshot.surface;
|
|
89
|
+
|
|
90
|
+
const tSurface = stageStart();
|
|
91
|
+
const surfaceMarkup = input.surfaceMarkupCache ??
|
|
92
|
+
collectWorkflowSurfaceMarkup(surface, input.preservation);
|
|
93
|
+
const highlights = surfaceMarkup.highlights.slice();
|
|
94
|
+
const opaqueFragments = surfaceMarkup.opaqueFragments.slice();
|
|
95
|
+
stageEnd("surface", tSurface);
|
|
96
|
+
|
|
97
|
+
if (surface) {
|
|
98
|
+
const tFields = stageStart();
|
|
60
99
|
fields.push(...collectFieldMarkup(surface, input.fieldSnapshot));
|
|
100
|
+
stageEnd("fields", tFields);
|
|
61
101
|
}
|
|
102
|
+
const tOpaqueRest = stageStart();
|
|
62
103
|
opaqueFragments.push(...collectOpaqueFragmentMarkup(input.preservation, opaqueFragments));
|
|
104
|
+
stageEnd("opaqueRest", tOpaqueRest);
|
|
63
105
|
|
|
106
|
+
const tCommentsEtc = stageStart();
|
|
64
107
|
const comments = input.renderSnapshot.comments.threads.map((thread): WorkflowCommentMarkup => ({
|
|
65
108
|
markupId: `comment:${thread.commentId}`,
|
|
66
109
|
kind: "comment",
|
|
@@ -113,6 +156,8 @@ export function collectWorkflowMarkupSnapshot(input: {
|
|
|
113
156
|
}),
|
|
114
157
|
);
|
|
115
158
|
|
|
159
|
+
stageEnd("commentsEtc", tCommentsEtc);
|
|
160
|
+
|
|
116
161
|
const items: WorkflowMarkupItem[] = [
|
|
117
162
|
...highlights,
|
|
118
163
|
...metadata,
|
|
@@ -168,6 +213,15 @@ function collectWorkflowMetadataMarkup(
|
|
|
168
213
|
value: entry.value,
|
|
169
214
|
scopeId: entry.scopeId,
|
|
170
215
|
workItemId: entry.workItemId,
|
|
216
|
+
// Schema 1.1 — copy external-mode fields through so card-layer code
|
|
217
|
+
// can detect external entries without re-consulting the entry snapshot.
|
|
218
|
+
...(entry.metadataPersistence !== undefined
|
|
219
|
+
? { metadataPersistence: entry.metadataPersistence }
|
|
220
|
+
: {}),
|
|
221
|
+
...(entry.storageRef !== undefined ? { storageRef: entry.storageRef } : {}),
|
|
222
|
+
...(entry.metadataVersion !== undefined
|
|
223
|
+
? { metadataVersion: entry.metadataVersion }
|
|
224
|
+
: {}),
|
|
171
225
|
} satisfies WorkflowMetadataMarkup];
|
|
172
226
|
});
|
|
173
227
|
}
|
|
@@ -294,11 +348,21 @@ function collectFieldMarkup(
|
|
|
294
348
|
return [];
|
|
295
349
|
}
|
|
296
350
|
|
|
351
|
+
// L7 Phase 1.5: project each story's text once up-front. The prior code
|
|
352
|
+
// called searchSurfaceBlocks per field, which re-projected the entire
|
|
353
|
+
// surface on every invocation. For the CCEP large-tables fixture this
|
|
354
|
+
// was ~220 ms per commit. Hoisting the projection out of the per-field
|
|
355
|
+
// loop collapses that to a single projection per story.
|
|
297
356
|
const stories = [
|
|
298
|
-
{
|
|
357
|
+
{
|
|
358
|
+
blocks: surface.blocks,
|
|
359
|
+
storyTarget: MAIN_STORY_TARGET,
|
|
360
|
+
projection: projectSurfaceText(surface.blocks),
|
|
361
|
+
},
|
|
299
362
|
...surface.secondaryStories.map((story) => ({
|
|
300
363
|
blocks: story.blocks,
|
|
301
364
|
storyTarget: story.target,
|
|
365
|
+
projection: projectSurfaceText(story.blocks),
|
|
302
366
|
})),
|
|
303
367
|
];
|
|
304
368
|
|
|
@@ -309,7 +373,7 @@ function collectFieldMarkup(
|
|
|
309
373
|
}
|
|
310
374
|
|
|
311
375
|
for (const story of stories) {
|
|
312
|
-
const matches =
|
|
376
|
+
const matches = searchProjectedSurfaceText(story.projection, displayText, { limit: 2 });
|
|
313
377
|
if (matches.length === 1) {
|
|
314
378
|
const match = matches[0]!;
|
|
315
379
|
return [
|
|
@@ -16,14 +16,18 @@ import type {
|
|
|
16
16
|
EditorAnchorProjection,
|
|
17
17
|
EditorStoryTarget,
|
|
18
18
|
IssueMetadataValue,
|
|
19
|
+
ReviewActionMetadataValue,
|
|
19
20
|
ScopeCardModel,
|
|
21
|
+
ScopeMetadataResolver,
|
|
22
|
+
SuggestionGroup,
|
|
23
|
+
SuggestionsSnapshot,
|
|
20
24
|
WorkflowBlockedCommandReason,
|
|
21
25
|
WorkflowCandidateRange,
|
|
22
26
|
WorkflowLockedZone,
|
|
23
27
|
WorkflowMetadataMarkup,
|
|
24
28
|
WorkflowScope,
|
|
25
29
|
} from "../api/public-types";
|
|
26
|
-
import { ISSUE_METADATA_ID } from "../api/public-types";
|
|
30
|
+
import { ISSUE_METADATA_ID, REVIEW_ACTION_METADATA_ID } from "../api/public-types";
|
|
27
31
|
import { MAIN_STORY_TARGET, storyTargetsEqual } from "../core/selection/mapping.ts";
|
|
28
32
|
import type { RuntimePageGraph } from "./layout/page-graph.ts";
|
|
29
33
|
import type {
|
|
@@ -316,6 +320,41 @@ export interface AttachScopeCardModelInput {
|
|
|
316
320
|
* rects — chrome consumers fall back to on-render positioning.
|
|
317
321
|
*/
|
|
318
322
|
anchorIndex?: RenderAnchorIndex | null;
|
|
323
|
+
/**
|
|
324
|
+
* R3 — suggestion snapshot. Groups whose `issueId` matches the
|
|
325
|
+
* scope's issue attach as `ScopeCardModel.suggestionGroups`; their
|
|
326
|
+
* ids land in `suggestionGroupIds` as a convenience for consumers
|
|
327
|
+
* that want a flat lookup.
|
|
328
|
+
*/
|
|
329
|
+
suggestions?: SuggestionsSnapshot | null;
|
|
330
|
+
/**
|
|
331
|
+
* K1-light — review-action markup. Entries with
|
|
332
|
+
* `metadataId === REVIEW_ACTION_METADATA_ID` whose coerced value's
|
|
333
|
+
* `issueId` matches the scope's issue attach newest-first as
|
|
334
|
+
* `ScopeCardModel.reviewActions`.
|
|
335
|
+
*/
|
|
336
|
+
reviewActionMetadata?: readonly WorkflowMetadataMarkup[];
|
|
337
|
+
/**
|
|
338
|
+
* K2 — candidate ranges. Any entry with `source: "ai"` whose
|
|
339
|
+
* offset range overlaps the segment flips `agentPending` to true.
|
|
340
|
+
*/
|
|
341
|
+
candidates?: readonly WorkflowCandidateRange[];
|
|
342
|
+
/**
|
|
343
|
+
* Pre-resolved external metadata values, keyed by entryId. When an
|
|
344
|
+
* entry in `metadata` or `reviewActionMetadata` has
|
|
345
|
+
* `metadataPersistence === "external"` and its inline `value` is
|
|
346
|
+
* undefined, this map supplies the resolved value (fetched by the caller
|
|
347
|
+
* via `ScopeMetadataResolver.resolve`).
|
|
348
|
+
*
|
|
349
|
+
* Entries absent from the map render as if the metadata had no value —
|
|
350
|
+
* the card still shows the scope but drops the issue row, consistent with
|
|
351
|
+
* how malformed metadata is handled today.
|
|
352
|
+
*
|
|
353
|
+
* NOTE: The facet's `getAllScopeCardModels()` does NOT populate this.
|
|
354
|
+
* Callers that need resolved values build the map via
|
|
355
|
+
* `buildExternalResolutions()` and call `attachScopeCardModel()` directly.
|
|
356
|
+
*/
|
|
357
|
+
externalResolutions?: ReadonlyMap<string, { value: Record<string, unknown>; version?: number }>;
|
|
319
358
|
}
|
|
320
359
|
|
|
321
360
|
/**
|
|
@@ -361,11 +400,23 @@ export function attachScopeCardModel(
|
|
|
361
400
|
segment.scopeId,
|
|
362
401
|
workItemId,
|
|
363
402
|
input.metadata,
|
|
403
|
+
input.externalResolutions,
|
|
364
404
|
);
|
|
365
405
|
const primaryAnchorRect = input.anchorIndex
|
|
366
406
|
? input.anchorIndex.bySelection(segment.fromOffset, segment.toOffset)
|
|
367
407
|
: null;
|
|
368
408
|
|
|
409
|
+
const suggestionGroups = resolveSuggestionGroupsForIssue(
|
|
410
|
+
issue?.issueId,
|
|
411
|
+
input.suggestions,
|
|
412
|
+
);
|
|
413
|
+
const reviewActions = resolveReviewActionsForIssue(
|
|
414
|
+
issue?.issueId,
|
|
415
|
+
input.reviewActionMetadata,
|
|
416
|
+
input.externalResolutions,
|
|
417
|
+
);
|
|
418
|
+
const agentPending = resolveAgentPending(segment, input.candidates);
|
|
419
|
+
|
|
369
420
|
models.push({
|
|
370
421
|
scopeId: segment.scopeId,
|
|
371
422
|
...(workItemId ? { workItemId } : {}),
|
|
@@ -373,19 +424,103 @@ export function attachScopeCardModel(
|
|
|
373
424
|
posture: segment.posture,
|
|
374
425
|
primaryAnchorRect,
|
|
375
426
|
...(issue ? { issue } : {}),
|
|
376
|
-
suggestionGroupIds:
|
|
377
|
-
|
|
378
|
-
|
|
427
|
+
suggestionGroupIds: suggestionGroups.map((group) => group.groupId),
|
|
428
|
+
suggestionGroups,
|
|
429
|
+
reviewActionCount: reviewActions.length,
|
|
430
|
+
reviewActions,
|
|
431
|
+
agentPending,
|
|
379
432
|
});
|
|
380
433
|
}
|
|
381
434
|
|
|
382
435
|
return models;
|
|
383
436
|
}
|
|
384
437
|
|
|
438
|
+
function resolveSuggestionGroupsForIssue(
|
|
439
|
+
issueId: string | undefined,
|
|
440
|
+
suggestions: SuggestionsSnapshot | null | undefined,
|
|
441
|
+
): SuggestionGroup[] {
|
|
442
|
+
if (!issueId || !suggestions?.groups) return [];
|
|
443
|
+
return suggestions.groups.filter((group) => group.issueId === issueId);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function resolveReviewActionsForIssue(
|
|
447
|
+
issueId: string | undefined,
|
|
448
|
+
metadata: readonly WorkflowMetadataMarkup[] | undefined,
|
|
449
|
+
externalResolutions?: ReadonlyMap<string, { value: Record<string, unknown>; version?: number }>,
|
|
450
|
+
): ReviewActionMetadataValue[] {
|
|
451
|
+
if (!issueId || !metadata || metadata.length === 0) return [];
|
|
452
|
+
const result: ReviewActionMetadataValue[] = [];
|
|
453
|
+
for (const entry of metadata) {
|
|
454
|
+
if (entry.metadataId !== REVIEW_ACTION_METADATA_ID) continue;
|
|
455
|
+
const coerced = coerceReviewActionValue(effectiveMetadataValue(entry, externalResolutions));
|
|
456
|
+
if (!coerced) continue;
|
|
457
|
+
if (coerced.issueId !== issueId) continue;
|
|
458
|
+
result.push(coerced);
|
|
459
|
+
}
|
|
460
|
+
// Newest-first by createdAt ISO string (lexicographic for 8601 UTC).
|
|
461
|
+
result.sort((a, b) => (a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0));
|
|
462
|
+
return result;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function coerceReviewActionValue(
|
|
466
|
+
value: unknown,
|
|
467
|
+
): ReviewActionMetadataValue | undefined {
|
|
468
|
+
if (!value || typeof value !== "object") return undefined;
|
|
469
|
+
const candidate = value as Partial<ReviewActionMetadataValue>;
|
|
470
|
+
if (
|
|
471
|
+
typeof candidate.reviewActionId !== "string" ||
|
|
472
|
+
typeof candidate.action !== "string" ||
|
|
473
|
+
typeof candidate.actor !== "string" ||
|
|
474
|
+
typeof candidate.actorRole !== "string" ||
|
|
475
|
+
typeof candidate.createdAt !== "string"
|
|
476
|
+
) {
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
return candidate as ReviewActionMetadataValue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function resolveAgentPending(
|
|
483
|
+
segment: ScopeRailSegment,
|
|
484
|
+
candidates: readonly WorkflowCandidateRange[] | undefined,
|
|
485
|
+
): boolean {
|
|
486
|
+
if (!candidates || candidates.length === 0) return false;
|
|
487
|
+
for (const candidate of candidates) {
|
|
488
|
+
if (candidate.source !== "ai") continue;
|
|
489
|
+
const range = anchorToRuntimeRange(candidate.anchor);
|
|
490
|
+
if (!range) continue;
|
|
491
|
+
// Strict-half-open overlap: [from, to).
|
|
492
|
+
if (range.to > segment.fromOffset && range.from < segment.toOffset) {
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Return the effective value for a metadata markup entry. For
|
|
501
|
+
* internal-mode entries (or entries with an inline value), returns
|
|
502
|
+
* `entry.value` directly. For external-mode entries whose inline
|
|
503
|
+
* `value` is undefined, looks up the resolved value from the
|
|
504
|
+
* caller-supplied `externalResolutions` map keyed by `entryId`.
|
|
505
|
+
*
|
|
506
|
+
* Returns `undefined` when no value is available — callers treat this the
|
|
507
|
+
* same as malformed metadata and drop the card row silently.
|
|
508
|
+
*/
|
|
509
|
+
function effectiveMetadataValue(
|
|
510
|
+
entry: WorkflowMetadataMarkup,
|
|
511
|
+
externalResolutions?: ReadonlyMap<string, { value: Record<string, unknown>; version?: number }>,
|
|
512
|
+
): Record<string, unknown> | undefined {
|
|
513
|
+
if (entry.value !== undefined) return entry.value;
|
|
514
|
+
// External-mode entry — inline body is empty. Look up the resolved value.
|
|
515
|
+
const resolved = externalResolutions?.get(entry.entryId);
|
|
516
|
+
return resolved?.value;
|
|
517
|
+
}
|
|
518
|
+
|
|
385
519
|
function resolveIssueForScope(
|
|
386
520
|
scopeId: string,
|
|
387
521
|
workItemId: string | undefined,
|
|
388
522
|
metadata: readonly WorkflowMetadataMarkup[] | undefined,
|
|
523
|
+
externalResolutions?: ReadonlyMap<string, { value: Record<string, unknown>; version?: number }>,
|
|
389
524
|
): IssueMetadataValue | undefined {
|
|
390
525
|
if (!metadata || metadata.length === 0) return undefined;
|
|
391
526
|
for (const entry of metadata) {
|
|
@@ -397,7 +532,7 @@ function resolveIssueForScope(
|
|
|
397
532
|
const matchesScope =
|
|
398
533
|
entry.scopeId !== undefined && entry.scopeId === scopeId;
|
|
399
534
|
if (!matchesWorkItem && !matchesScope) continue;
|
|
400
|
-
const coerced = coerceIssueValue(entry
|
|
535
|
+
const coerced = coerceIssueValue(effectiveMetadataValue(entry, externalResolutions));
|
|
401
536
|
if (coerced) return coerced;
|
|
402
537
|
}
|
|
403
538
|
return undefined;
|
|
@@ -426,3 +561,107 @@ function coerceIssueValue(
|
|
|
426
561
|
}
|
|
427
562
|
return candidate as IssueMetadataValue;
|
|
428
563
|
}
|
|
564
|
+
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
// buildExternalResolutions — caller utility for pre-resolving external refs
|
|
567
|
+
// ---------------------------------------------------------------------------
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Structured result from `buildExternalResolutions`. Callers that previously
|
|
571
|
+
* consumed the return value as a `Map` directly must now destructure `.resolutions`.
|
|
572
|
+
*/
|
|
573
|
+
export interface BuildExternalResolutionsResult {
|
|
574
|
+
/**
|
|
575
|
+
* Resolved values keyed by `entryId`. Suitable for passing into
|
|
576
|
+
* `AttachScopeCardModelInput.externalResolutions`.
|
|
577
|
+
*/
|
|
578
|
+
resolutions: Map<string, { value: Record<string, unknown>; version?: number }>;
|
|
579
|
+
/**
|
|
580
|
+
* Detected version mismatches. Non-empty only when the caller passes
|
|
581
|
+
* `detectConflicts: true`. Each record includes both sides' value +
|
|
582
|
+
* version so the caller can emit a `metadata_conflict_detected` event.
|
|
583
|
+
*/
|
|
584
|
+
conflicts: Array<{
|
|
585
|
+
scopeId?: string;
|
|
586
|
+
entryId?: string;
|
|
587
|
+
embedded: { value?: Record<string, unknown>; version?: number } | null;
|
|
588
|
+
external: { value?: Record<string, unknown>; version?: number } | null;
|
|
589
|
+
}>;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Pre-resolve every external-mode metadata entry's value via the supplied
|
|
594
|
+
* resolver. The returned `resolutions` map is suitable for passing into
|
|
595
|
+
* `AttachScopeCardModelInput.externalResolutions`.
|
|
596
|
+
*
|
|
597
|
+
* Internal-mode entries (where `entry.value` is already inline) are skipped.
|
|
598
|
+
* When the resolver returns `undefined` for a ref (unknown or revoked), the
|
|
599
|
+
* `entryId` is omitted from the map; the scope card then renders without that
|
|
600
|
+
* value.
|
|
601
|
+
*
|
|
602
|
+
* Both `metadata` and `reviewActionMetadata` arrays are walked so review-action
|
|
603
|
+
* rows stored externally are resolved alongside issue entries.
|
|
604
|
+
*
|
|
605
|
+
* When `detectConflicts` is `true`, the helper compares each entry's embedded
|
|
606
|
+
* `metadataVersion` against the resolver-returned version. Any mismatch is
|
|
607
|
+
* recorded in the returned `conflicts` array.
|
|
608
|
+
*
|
|
609
|
+
* NOTE: This helper is intentionally async so callers can `await` it in the
|
|
610
|
+
* React layer before passing the map to the synchronous `attachScopeCardModel`.
|
|
611
|
+
* The layout facet's `getAllScopeCardModels()` does NOT call this — it is a
|
|
612
|
+
* React-layer concern where async is normal.
|
|
613
|
+
*/
|
|
614
|
+
export async function buildExternalResolutions(input: {
|
|
615
|
+
metadata: readonly WorkflowMetadataMarkup[];
|
|
616
|
+
reviewActionMetadata?: readonly WorkflowMetadataMarkup[];
|
|
617
|
+
resolver: ScopeMetadataResolver;
|
|
618
|
+
/**
|
|
619
|
+
* When `true`, the helper compares each external entry's embedded
|
|
620
|
+
* `metadataVersion` against the resolver-returned version and records a
|
|
621
|
+
* conflict for every mismatch. Absent / `false` → `conflicts` is always
|
|
622
|
+
* empty (backwards-compatible default).
|
|
623
|
+
*/
|
|
624
|
+
detectConflicts?: boolean;
|
|
625
|
+
}): Promise<BuildExternalResolutionsResult> {
|
|
626
|
+
const externalEntries = [
|
|
627
|
+
...input.metadata,
|
|
628
|
+
...(input.reviewActionMetadata ?? []),
|
|
629
|
+
].filter(
|
|
630
|
+
(entry) =>
|
|
631
|
+
entry.metadataPersistence === "external" &&
|
|
632
|
+
entry.value === undefined &&
|
|
633
|
+
entry.storageRef !== undefined,
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
const results = await Promise.all(
|
|
637
|
+
externalEntries.map(async (entry) => {
|
|
638
|
+
const resolved = await input.resolver.resolve(entry.storageRef!);
|
|
639
|
+
return resolved ? { entry, resolved } : null;
|
|
640
|
+
}),
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
const resolutions = new Map<string, { value: Record<string, unknown>; version?: number }>();
|
|
644
|
+
const conflicts: BuildExternalResolutionsResult["conflicts"] = [];
|
|
645
|
+
|
|
646
|
+
for (const row of results) {
|
|
647
|
+
if (!row) continue;
|
|
648
|
+
resolutions.set(row.entry.entryId, row.resolved);
|
|
649
|
+
|
|
650
|
+
if (
|
|
651
|
+
input.detectConflicts &&
|
|
652
|
+
row.entry.metadataVersion !== undefined &&
|
|
653
|
+
row.resolved.version !== undefined &&
|
|
654
|
+
row.entry.metadataVersion !== row.resolved.version
|
|
655
|
+
) {
|
|
656
|
+
conflicts.push({
|
|
657
|
+
scopeId: row.entry.scopeId,
|
|
658
|
+
entryId: row.entry.entryId,
|
|
659
|
+
// External entries have no inline value — embedded side carries only version
|
|
660
|
+
embedded: { version: row.entry.metadataVersion },
|
|
661
|
+
external: { value: row.resolved.value, version: row.resolved.version },
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return { resolutions, conflicts };
|
|
667
|
+
}
|