@superdoc/mcp 1.9.0-next.11 → 1.9.0-next.13

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 (2) hide show
  1. package/dist/index.js +686 -372
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -31620,7 +31620,7 @@ var init_host = __esm(() => {
31620
31620
  });
31621
31621
 
31622
31622
  // ../../packages/sdk/langs/node/src/runtime/sdk-version.generated.ts
31623
- var SDK_VERSION = "2.22.0-next.11";
31623
+ var SDK_VERSION = "2.22.0-next.13";
31624
31624
 
31625
31625
  // ../../packages/sdk/langs/node/src/runtime/embedded-platform.ts
31626
31626
  import { accessSync, chmodSync, constants, existsSync, statSync } from "node:fs";
@@ -87423,10 +87423,10 @@ var init_action_descriptors = __esm(() => {
87423
87423
  operations: ["create.heading"]
87424
87424
  },
87425
87425
  replace_text: {
87426
- hint: "edits[{find,replace}], optional selector to scope replacements to one inspected block, caseSensitive?, changeMode? — for EDITING existing wording only; the replaced span is rewritten as PLAIN text (intra-span bold/italic is dropped), so keep find/replace spans MINIMAL (the words that change, not the whole paragraph) and NEVER use it to append a new sentence/clause/paragraph (use insert_paragraphs)",
87426
+ hint: "edits[{find,replace,occurrence?}], optional selector to scope replacements to one inspected block, caseSensitive?, expectedRevision?, changeMode? — for EDITING existing wording only; keep find/replace spans MINIMAL (the words that change, not the whole paragraph). When the find appears more than once in the selected block and only one should change, set that edit's occurrence (1-based match number within the block; requires selector; this is not selector.occurrence, which picks a block). NEVER use it to append a new sentence/clause/paragraph (use insert_paragraphs)",
87427
87427
  group: "Text/structure actions",
87428
87428
  groupOrder: 2,
87429
- args: ["edits", "selector", "caseSensitive", "changeMode"],
87429
+ args: ["edits", "selector", "caseSensitive", "expectedRevision", "changeMode"],
87430
87430
  operations: ["replace"]
87431
87431
  },
87432
87432
  delete_text: {
@@ -93107,12 +93107,10 @@ function normalizeTitleLikeRewriteText(targetText, rewrittenText) {
93107
93107
  const displayTitle = toDisplayTitleCase(targetText);
93108
93108
  if (rewrittenText.includes(displayTitle))
93109
93109
  return rewrittenText;
93110
- const lowerTarget = targetText.toLocaleLowerCase();
93111
- const lowerRewrite = rewrittenText.toLocaleLowerCase();
93112
- const index = lowerRewrite.indexOf(lowerTarget);
93113
- if (index < 0)
93110
+ const match = findRanges(rewrittenText, targetText, false)[0];
93111
+ if (!match)
93114
93112
  return rewrittenText;
93115
- return `${rewrittenText.slice(0, index)}${displayTitle}${rewrittenText.slice(index + targetText.length)}`;
93113
+ return `${rewrittenText.slice(0, match.start)}${displayTitle}${rewrittenText.slice(match.end)}`;
93116
93114
  }
93117
93115
  async function executeCreateParagraph(doc2, text, placement, changeMode) {
93118
93116
  const fn = maybeMethod2(doc2, ["create", "paragraph"]);
@@ -93417,6 +93415,7 @@ async function runReplaceTextUnchecked(doc2, args) {
93417
93415
  const preIdentity = requiresBlockSnapshot ? null : await readDocumentIdentity(doc2);
93418
93416
  const pre = requiresBlockSnapshot ? await buildMutationSnapshot(doc2, { includeDomains: selectorDomains ?? ["blocks"] }) : snapshotFromIdentity(preIdentity);
93419
93417
  try {
93418
+ assertExpectedReplacementRevision(args.expectedRevision, pre.revision);
93420
93419
  assertReplacementTextPayloads(args.edits);
93421
93420
  if (args.edits.length === 0) {
93422
93421
  return failedReceipt("replace_text", new Error("edits must be non-empty"), pre);
@@ -93425,7 +93424,7 @@ async function runReplaceTextUnchecked(doc2, args) {
93425
93424
  const selectedTargets = [];
93426
93425
  let skippedEdits = [];
93427
93426
  let steps;
93428
- let applyExpectedRevision;
93427
+ let applyExpectedRevision = args.expectedRevision != null ? pre.revision : undefined;
93429
93428
  if (args.selector) {
93430
93429
  const target = selectorToBlockTarget(args.selector, pre);
93431
93430
  if (!target) {
@@ -95411,18 +95410,25 @@ function findRanges(haystack, needle, caseSensitive) {
95411
95410
  const ranges = [];
95412
95411
  if (needle.length === 0)
95413
95412
  return ranges;
95414
- const source = caseSensitive ? haystack : haystack.toLocaleLowerCase();
95415
- const target = caseSensitive ? needle : needle.toLocaleLowerCase();
95416
- let offset = 0;
95417
- while (offset < source.length) {
95418
- const idx = source.indexOf(target, offset);
95419
- if (idx < 0)
95420
- break;
95421
- ranges.push({ start: idx, end: idx + target.length });
95422
- offset = idx + target.length;
95413
+ if (caseSensitive) {
95414
+ let offset = 0;
95415
+ while (offset < haystack.length) {
95416
+ const idx = haystack.indexOf(needle, offset);
95417
+ if (idx < 0)
95418
+ break;
95419
+ ranges.push({ start: idx, end: idx + needle.length });
95420
+ offset = idx + needle.length;
95421
+ }
95422
+ return ranges;
95423
+ }
95424
+ for (const match of haystack.matchAll(caseInsensitivePattern(needle))) {
95425
+ ranges.push({ start: match.index, end: match.index + match[0].length });
95423
95426
  }
95424
95427
  return ranges;
95425
95428
  }
95429
+ function caseInsensitivePattern(needle) {
95430
+ return new RegExp(needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "giu");
95431
+ }
95426
95432
  async function runApplyLetterSpacing(doc2, args) {
95427
95433
  const pre = await buildMutationSnapshot(doc2, { includeDomains: ["blocks"] });
95428
95434
  try {
@@ -96168,29 +96174,59 @@ function trackedChangeRefIds(result) {
96168
96174
  return entityId ? [entityId] : [];
96169
96175
  }));
96170
96176
  }
96171
- function parseScopedReplaceArgs(args, changeMode) {
96172
- if (args.action !== "replace_text")
96173
- return null;
96177
+ function parseScopedReplaceArgs(args, edits, changeMode, expectedRevision) {
96174
96178
  if (!isRecord2(args.selector))
96175
96179
  return null;
96176
- if (!Array.isArray(args.edits) || args.edits.length === 0)
96177
- return null;
96178
- const edits = [];
96179
- for (const entry of args.edits) {
96180
- if (!isRecord2(entry) || typeof entry.find !== "string" || entry.find.length === 0)
96181
- return null;
96182
- edits.push({ find: entry.find, replace: typeof entry.replace === "string" ? entry.replace : "" });
96183
- }
96184
96180
  return {
96185
96181
  edits,
96186
96182
  selector: args.selector,
96187
96183
  caseSensitive: args.caseSensitive === true,
96184
+ expectedRevision,
96188
96185
  changeMode
96189
96186
  };
96190
96187
  }
96188
+ function assertExpectedReplacementRevision(expected, actual) {
96189
+ if (expected != null && expected !== actual) {
96190
+ throw new SuperDocCliError("replace_text: expectedRevision is stale. Inspect this live document again before editing.", {
96191
+ code: "REVISION_CONFLICT"
96192
+ });
96193
+ }
96194
+ }
96195
+ async function fieldResultRanges(doc2, nodeId, blockText) {
96196
+ const fn = maybeMethod2(doc2, ["fields", "list"]);
96197
+ if (!fn)
96198
+ return null;
96199
+ const results = [];
96200
+ const limit = 500;
96201
+ for (let offset = 0;offset < 1e5; offset += limit) {
96202
+ const page = await fn({ limit, offset });
96203
+ const items = isRecord2(page) && Array.isArray(page.items) ? page.items.filter(isRecord2) : [];
96204
+ for (const item of items) {
96205
+ const address = isRecord2(item.address) ? item.address : null;
96206
+ const inBody = typeof address?.storyId !== "string" || address.storyId.startsWith("main:");
96207
+ if (address?.blockId !== nodeId || !inBody)
96208
+ continue;
96209
+ if (typeof item.resolvedText !== "string" || item.resolvedText.length === 0) {
96210
+ return [{ start: 0, end: blockText.length }];
96211
+ }
96212
+ results.push(item.resolvedText);
96213
+ }
96214
+ const total = isRecord2(page) && typeof page.total === "number" ? page.total : null;
96215
+ if (items.length < limit || total != null && offset + limit >= total)
96216
+ break;
96217
+ }
96218
+ const ranges = [];
96219
+ for (const result of results) {
96220
+ for (let at = blockText.indexOf(result);at >= 0; at = blockText.indexOf(result, at + 1)) {
96221
+ ranges.push({ start: at, end: at + result.length });
96222
+ }
96223
+ }
96224
+ return ranges;
96225
+ }
96191
96226
  async function runScopedReplaceUnchecked(doc2, args) {
96192
96227
  assertReplacementTextPayloads(args.edits);
96193
96228
  const pre = await buildMutationSnapshot(doc2, { includeDomains: ["blocks", "tables"] });
96229
+ assertExpectedReplacementRevision(args.expectedRevision, pre.revision);
96194
96230
  const matched = resolveSnapshotSelector(pre, args.selector);
96195
96231
  if (matched.length !== 1) {
96196
96232
  const failure2 = describeSelectorFailure(args.selector, matched, pre, args.edits.map((e) => e.find));
@@ -96210,13 +96246,22 @@ async function runScopedReplaceUnchecked(doc2, args) {
96210
96246
  };
96211
96247
  }
96212
96248
  const caseSensitive = args.caseSensitive === true;
96249
+ const hasExplicitOccurrence = args.edits.some((edit) => edit.occurrence != null);
96213
96250
  const perEdit = [];
96214
96251
  const allSpans = [];
96215
96252
  for (const edit of args.edits) {
96216
- const spans = findRanges(block.text, edit.find, caseSensitive);
96217
- perEdit.push({ find: edit.find, occurrences: spans.length });
96253
+ const matches = findRanges(block.text, edit.find, caseSensitive);
96254
+ const spans = edit.occurrence == null ? matches : matches.slice(edit.occurrence - 1, edit.occurrence);
96255
+ if (hasExplicitOccurrence && spans.length === 0) {
96256
+ return failedReceipt("replace_text", new SuperDocCliError(edit.occurrence == null ? `replace_text: ${JSON.stringify(edit.find)} not found in the selected block. Inspect and choose a valid target.` : `replace_text: ${JSON.stringify(edit.find)} occurrence ${edit.occurrence} not found in the selected block (${matches.length} matches). Inspect and choose a valid target.`, { code: "MATCH_NOT_FOUND" }), pre);
96257
+ }
96258
+ perEdit.push({
96259
+ find: edit.find,
96260
+ occurrences: spans.length,
96261
+ ...edit.occurrence != null ? { occurrence: edit.occurrence } : {}
96262
+ });
96218
96263
  for (const span of spans)
96219
- allSpans.push({ ...span, replace: edit.replace ?? "", find: edit.find });
96264
+ allSpans.push({ ...span, replace: edit.replace, find: edit.find });
96220
96265
  }
96221
96266
  const appliedEdits = perEdit.filter((e) => e.occurrences > 0);
96222
96267
  const skippedEdits = perEdit.filter((e) => e.occurrences === 0);
@@ -96241,6 +96286,9 @@ async function runScopedReplaceUnchecked(doc2, args) {
96241
96286
  let lastEnd = -1;
96242
96287
  for (const span of allSpans) {
96243
96288
  if (span.start < lastEnd) {
96289
+ if (hasExplicitOccurrence) {
96290
+ return failedReceipt("replace_text", new SuperDocCliError("replace_text: explicit occurrence targets overlap. Choose non-overlapping edits in the pre-edit block.", { code: "INVALID_ARGUMENT" }), pre);
96291
+ }
96244
96292
  droppedOverlaps.push(span);
96245
96293
  continue;
96246
96294
  }
@@ -96249,24 +96297,45 @@ async function runScopedReplaceUnchecked(doc2, args) {
96249
96297
  }
96250
96298
  const expected = planned.reduceRight((text, span) => text.slice(0, span.start) + span.replace + text.slice(span.end), block.text);
96251
96299
  const tracked = (args.changeMode ?? "direct") === "tracked";
96252
- let result = null;
96253
96300
  const uniqueFinds = new Set(planned.map((s) => caseSensitive ? s.find : s.find.toLowerCase()));
96254
- if (tracked && uniqueFinds.size === planned.length) {
96255
- const spanSteps = planned.map((span, index) => ({
96301
+ const deletions = tracked ? [] : planned.filter((span) => span.replace.length === 0);
96302
+ const fieldRanges = deletions.length > 0 ? await fieldResultRanges(doc2, nodeId, block.text) : [];
96303
+ const fieldDeletionRisk = fieldRanges == null || deletions.some((span) => fieldRanges.some((range) => span.start < range.end && span.end > range.start));
96304
+ const exactTargets = !fieldDeletionRisk && (hasExplicitOccurrence || tracked && uniqueFinds.size === planned.length);
96305
+ let result = null;
96306
+ let wroteExactTargets = false;
96307
+ if (exactTargets) {
96308
+ const targetedSpans = [...planned].reverse().filter((span) => block.text.slice(span.start, span.end) !== span.replace);
96309
+ const steps = targetedSpans.map((span, index) => ({
96256
96310
  id: `scoped-replace-${index + 1}`,
96257
96311
  op: "text.rewrite",
96258
96312
  where: {
96259
- by: "select",
96260
- select: { type: "text", pattern: span.find, mode: "contains", caseSensitive },
96261
- require: "first",
96262
- within: { kind: "block", nodeType: block.nodeType, nodeId }
96313
+ by: "target",
96314
+ target: {
96315
+ kind: "selection",
96316
+ start: { kind: "text", blockId: nodeId, offset: span.start },
96317
+ end: { kind: "text", blockId: nodeId, offset: span.end }
96318
+ }
96263
96319
  },
96264
96320
  args: { replacement: { text: span.replace }, style: preserveRewriteStyle() }
96265
96321
  }));
96266
- try {
96267
- result = await executeMutations(doc2, spanSteps, args.changeMode, pre.revision);
96268
- } catch {
96269
- result = null;
96322
+ if (steps.length === 0) {
96323
+ result = { noop: true };
96324
+ } else {
96325
+ const preview = await previewMutations(doc2, steps, args.changeMode, pre.revision);
96326
+ const previewSteps = Array.isArray(preview.steps) ? preview.steps.filter(isRecord2) : [];
96327
+ const aligned = preview.valid === true && steps.every((step, index) => {
96328
+ const resolutions = previewSteps.find((candidate) => candidate.stepId === step.id)?.resolutions;
96329
+ const resolved = Array.isArray(resolutions) && isRecord2(resolutions[0]) ? resolutions[0].text : undefined;
96330
+ const span = targetedSpans[index];
96331
+ return resolved === block.text.slice(span.start, span.end);
96332
+ });
96333
+ if (aligned) {
96334
+ result = await executeMutations(doc2, steps, args.changeMode, pre.revision);
96335
+ wroteExactTargets = true;
96336
+ } else if (hasExplicitOccurrence) {
96337
+ return failedReceipt("replace_text", new SuperDocCliError("replace_text: the chosen occurrence does not resolve to the same text in the document. Inspect again before editing.", { code: "TARGET_NOT_FOUND" }), pre);
96338
+ }
96270
96339
  }
96271
96340
  }
96272
96341
  if (result == null) {
@@ -96280,9 +96349,20 @@ async function runScopedReplaceUnchecked(doc2, args) {
96280
96349
  ];
96281
96350
  result = await executeMutations(doc2, steps, args.changeMode, pre.revision);
96282
96351
  }
96283
- const post = await buildMutationSnapshot(doc2, { includeDomains: ["blocks", "tables"] });
96284
- const postBlock = findTextTarget(post, nodeId);
96285
- const passed = postBlock != null && (tracked ? planned.every((span) => span.replace.length === 0 || postBlock.text.includes(span.replace)) : postBlock.text === expected);
96352
+ let post = await buildMutationSnapshot(doc2, { includeDomains: ["blocks", "tables"] });
96353
+ let postBlock = findTextTarget(post, nodeId);
96354
+ let rolledBack = false;
96355
+ const appliedRevision = isRecord2(result) && isRecord2(result.revision) ? result.revision.after : undefined;
96356
+ if (wroteExactTargets && postBlock != null && postBlock.text !== expected && postBlock.text !== block.text && typeof appliedRevision === "string" && appliedRevision === post.revision) {
96357
+ const undoFn = maybeMethod2(doc2, ["history", "undo"]);
96358
+ if (undoFn) {
96359
+ await undoFn({});
96360
+ post = await buildMutationSnapshot(doc2, { includeDomains: ["blocks", "tables"] });
96361
+ postBlock = findTextTarget(post, nodeId);
96362
+ rolledBack = postBlock?.text === block.text;
96363
+ }
96364
+ }
96365
+ const passed = postBlock != null && !rolledBack && (tracked && !hasExplicitOccurrence && !wroteExactTargets ? planned.every((span) => span.replace.length === 0 || postBlock.text.includes(span.replace)) : postBlock.text === expected);
96286
96366
  const status = !passed ? "failed" : skippedEdits.length > 0 ? "partial" : "ok";
96287
96367
  return {
96288
96368
  status,
@@ -96291,6 +96371,14 @@ async function runScopedReplaceUnchecked(doc2, args) {
96291
96371
  nextStep: `${skippedEdits.length} of ${perEdit.length} edits were NOT applied — the find text is not in the selected block. ` + `Do not stop: call replace_text again for the remaining edits with a different selector (or no selector for a global replace).`,
96292
96372
  recovery: { kind: "retry" }
96293
96373
  } : {},
96374
+ ...rolledBack ? {
96375
+ errors: [
96376
+ {
96377
+ code: "ACTION_FAILED",
96378
+ message: "replace_text: the engine changed more than the requested text, so the edit was undone. Inspect the block and retry with a larger find or rewrite the whole block."
96379
+ }
96380
+ ]
96381
+ } : {},
96294
96382
  preSnapshot: { revision: pre.revision },
96295
96383
  postSnapshot: { revision: post.revision },
96296
96384
  selectedBlock: { nodeId, nodeType: block.nodeType },
@@ -97622,8 +97710,17 @@ async function dispatchSuperdocPerformAction(doc2, args) {
97622
97710
  code: "INVALID_ARGUMENT"
97623
97711
  });
97624
97712
  }
97713
+ const expectedRevision = args.expectedRevision;
97714
+ if (expectedRevision !== undefined && (typeof expectedRevision !== "string" || expectedRevision.length === 0)) {
97715
+ throw new SuperDocCliError("replace_text: expectedRevision must be a non-empty string from this live document handle", { code: "INVALID_ARGUMENT" });
97716
+ }
97717
+ if (edits.some((edit) => edit.occurrence != null) && !isRecord2(args.selector)) {
97718
+ throw new SuperDocCliError("replace_text: explicit occurrence requires a selector resolving to one block", {
97719
+ code: "INVALID_ARGUMENT"
97720
+ });
97721
+ }
97625
97722
  const changeMode = parseReplaceTextChangeMode(args.changeMode);
97626
- const scoped = parseScopedReplaceArgs(args, changeMode);
97723
+ const scoped = parseScopedReplaceArgs(args, edits, changeMode, expectedRevision);
97627
97724
  if (scoped) {
97628
97725
  return runScopedReplace(doc2, scoped);
97629
97726
  }
@@ -97636,6 +97733,7 @@ async function dispatchSuperdocPerformAction(doc2, args) {
97636
97733
  edits,
97637
97734
  selector,
97638
97735
  caseSensitive: args.caseSensitive === true,
97736
+ expectedRevision,
97639
97737
  changeMode
97640
97738
  });
97641
97739
  }
@@ -98281,7 +98379,11 @@ function parseEdits2(value) {
98281
98379
  const replace = asString2(entry.replace) ?? "";
98282
98380
  if (!find)
98283
98381
  return;
98284
- result.push({ find, replace });
98382
+ const occurrence = entry.occurrence;
98383
+ if (occurrence !== undefined && (typeof occurrence !== "number" || !Number.isSafeInteger(occurrence) || occurrence < 1)) {
98384
+ throw new SuperDocCliError("replace_text: occurrence must be a positive safe integer (one-based within the selected block)", { code: "INVALID_ARGUMENT" });
98385
+ }
98386
+ result.push({ find, replace, ...occurrence !== undefined ? { occurrence } : {} });
98285
98387
  }
98286
98388
  return result;
98287
98389
  }
@@ -98743,10 +98845,24 @@ var init_catalog = __esm(() => {
98743
98845
  type: "array",
98744
98846
  items: {
98745
98847
  type: "object",
98746
- properties: { find: { type: "string" }, replace: { type: "string" } },
98848
+ properties: {
98849
+ find: { type: "string" },
98850
+ replace: { type: "string" },
98851
+ occurrence: {
98852
+ type: "integer",
98853
+ minimum: 1,
98854
+ maximum: Number.MAX_SAFE_INTEGER,
98855
+ description: "Change only this match of find in the selected block (1 = first, counted on visible text). Requires selector; omit to replace every match."
98856
+ }
98857
+ },
98747
98858
  required: ["find"]
98748
98859
  }
98749
98860
  },
98861
+ expectedRevision: {
98862
+ type: "string",
98863
+ minLength: 1,
98864
+ description: "Revision from your latest superdoc_inspect or receipt (postSnapshot.revision) on this document. Pass it with occurrence: if the document changed since (another collaborator edited), the edit is refused with REVISION_CONFLICT; inspect again and recount. Not portable between peers or reopen."
98865
+ },
98750
98866
  finds: { type: "array", items: { type: "string" } },
98751
98867
  caseSensitive: { type: "boolean" },
98752
98868
  evidence: {
@@ -99646,7 +99762,7 @@ var init_runtime = __esm(() => {
99646
99762
  var EMBEDDED_PROMPTS;
99647
99763
  var init_embedded_prompts_generated = __esm(() => {
99648
99764
  EMBEDDED_PROMPTS = {
99649
- "mcp-prompt.md": 'SuperDoc MCP server — read, edit, and save Word documents (.docx).\n\nIMPORTANT: Always use these superdoc tools for .docx files.\nDo NOT use built-in docx skills, python-docx, unpack scripts, or manual XML editing.\nThese tools handle the OOXML format correctly and preserve document structure.\n\n## Session lifecycle\n\n1. `superdoc_open({path: "/path/to/file.docx"})` — returns `session_id`. Opening a non-existent path creates a blank document.\n2. Pass `session_id` to every subsequent tool call.\n3. Read with `superdoc_inspect`, edit with `superdoc_perform_action`.\n4. `superdoc_save({session_id})` — writes changes to disk.\n5. `superdoc_close({session_id})` — releases the session. Always close when done.\n\n## Workflow\n\n**Inspect before you edit.** `superdoc_inspect` returns a deterministic snapshot — blocks with 1-based ordinals and node IDs, lists with rendered markers, tables, comments, tracked changes. Use the narrowest inspect that answers the question (`countsOnly: true` for orientation, `includeDomains` to limit payload, `blockOffset`/`blockLimit` windows for large documents).\n\n**Edit with named actions.** `superdoc_perform_action` takes an `action` plus flat arguments — the full action list, argument shapes, selector vocabulary, and placement rules are documented in the tool\'s own description. Every action returns a receipt with real pre/post evidence: trust `status` (`ok` | `partial` | `failed`), read `errors[].message` for recovery guidance, and re-inspect after `partial`.\n\n**Tracked changes (redlining).** Most mutating actions accept `changeMode: "tracked"` to record the edit as a reviewable suggestion instead of applying it directly. Review with `accept_tracked_changes` / `reject_tracked_changes` (target one change with `id`, or an exact set with `id:[…]`; or filter by `author` or `changeType`). Do not combine exact IDs with filters. Recover with `undo_changes` / `redo_changes`.\n\n**Failures are safe.** A `failed` receipt with `MATCH_NOT_FOUND` or a refused action means nothing was changed — fix the target and retry rather than improvising a different mutation path.\n',
99765
+ "mcp-prompt.md": 'SuperDoc MCP server — read, edit, and save Word documents (.docx).\n\nIMPORTANT: Always use these superdoc tools for .docx files.\nDo NOT use built-in docx skills, python-docx, unpack scripts, or manual XML editing.\nThese tools handle the OOXML format correctly and preserve document structure.\n\n## Session lifecycle\n\n1. `superdoc_open({path: "/path/to/file.docx"})` — returns `session_id`. Opening a non-existent path creates a blank document.\n2. Pass `session_id` to every subsequent tool call.\n3. Read with `superdoc_inspect`, edit with `superdoc_perform_action`.\n4. `superdoc_save({session_id})` — writes changes to disk.\n5. `superdoc_close({session_id})` — releases the session. Always close when done.\n\n## Workflow\n\n**Inspect before you edit.** `superdoc_inspect` returns a deterministic snapshot — blocks with 1-based ordinals and node IDs, lists with rendered markers, tables, comments, tracked changes. Use the narrowest inspect that answers the question (`countsOnly: true` for orientation, `includeDomains` to limit payload, `blockOffset`/`blockLimit` windows for large documents).\n\n**Edit with named actions.** `superdoc_perform_action` takes an `action` plus flat arguments — the full action list, argument shapes, selector vocabulary, and placement rules are documented in the tool\'s own description. Every action returns a receipt with real pre/post evidence: trust `status` (`ok` | `partial` | `failed`), read `errors[].message` for recovery guidance, and re-inspect after `partial`.\n\n**Target one repeated match.** For `replace_text`, scope to one block with a selector and set `occurrence` on the edit (1 = first match in that block) to change only that match. Also pass `expectedRevision` from your latest inspect or receipt; on `REVISION_CONFLICT`, inspect again and recount.\n\n**Tracked changes (redlining).** Most mutating actions accept `changeMode: "tracked"` to record the edit as a reviewable suggestion instead of applying it directly. Review with `accept_tracked_changes` / `reject_tracked_changes` (target one change with `id`, or an exact set with `id:[…]`; or filter by `author` or `changeType`). Do not combine exact IDs with filters. Recover with `undo_changes` / `redo_changes`.\n\n**Failures are safe.** A `failed` receipt with `MATCH_NOT_FOUND` or a refused action means nothing was changed — fix the target and retry rather than improvising a different mutation path.\n',
99650
99766
  "system-prompt.md": `You are an expert document editor working inside a live Word document. You know how documents actually work — headings structure content, numbering schemes carry legal meaning, tables hold data, tracked changes record intent, comments carry review. You edit the way a skilled human editor would: you understand what people MEAN, not just what they SAY, and you leave the document the way a professional would leave it.
99651
99767
 
99652
99768
  ============================================================
@@ -99697,7 +99813,7 @@ ACTIONS (superdoc_perform_action with flat args)
99697
99813
  - undo_changes: untilMarker (rendered marker from the original state, e.g. "2.1.") or steps (1-25). Deterministic revert — steps history back until the marker reappears; the receipt proves it. NEVER use steps > 1 blindly: prefer untilMarker, and after ANY undo verify (superdoc_inspect) that content you meant to KEEP still exists — if you overshot, redo_changes {steps:N} steps forward again to recover it.
99698
99814
  - redo_changes: steps (1-25, default 1). Steps history FORWARD to re-apply edits a prior undo removed — THE recovery for an undo overshoot. Only reaches the forward branch until a NEW edit is made.
99699
99815
  - attach_numbering: anchorText (text of the block) or nodeId, likeMarker (rendered marker of a sibling clause, e.g. "10."). Makes an EXISTING block a numbered clause at the same scheme/level — "make this the section 11 heading" in a clause-numbered document is exactly this (it will render as the next number).
99700
- - replace_text: edits:[{find,replace}], caseSensitive default false. selector to scope. changeMode:"tracked" if asked. The receipt reports editsApplied and editsSkipped per find — READ IT: a skipped find is not in the selected block; re-target instead of assuming success.
99816
+ - replace_text: edits:[{find,replace,occurrence?}], caseSensitive default false. selector to scope. Keep find/replace spans MINIMAL. If the find appears more than once in the selected block and only one should change, give that edit occurrence (1 = first match in the block; needs selector; not selector.occurrence). With occurrence, also pass expectedRevision from your latest inspect or receipt; on REVISION_CONFLICT inspect again and recount. changeMode:"tracked" if asked. The receipt reports editsApplied and editsSkipped per find — READ IT: a skipped find is not in the selected block; re-target instead of assuming success.
99701
99817
  - delete_text: finds:[string]. selector to scope deletions to ONE block — REQUIRED for whitespace-only finds (unscoped whitespace matches document-wide and is refused). changeMode:"tracked" if asked. Deletes TEXT ONLY: the block survives, so a list item keeps its bullet/number and an accepted tracked deletion leaves an empty numbered item behind. To remove the item itself use delete_blocks.
99702
99818
  - delete_blocks: selectors:[…], each resolving to ONE block (list item, paragraph or heading). changeMode:"tracked" if asked. THE way to DELETE a whole LIST ITEM, paragraph or heading — the bullet/number goes with it and the remaining items renumber. "Delete the first item under Article II" / "remove that clause" is exactly this, NOT delete_text. Pass every target in ONE call. Use delete_table for a whole table.
99703
99819
  - rewrite_block: selector, text. Inspect first to gather current text; never ask the user to paste text already in the doc.
@@ -159330,7 +159446,7 @@ function buildCommentsCatalog(state) {
159330
159446
  const extendedBytes = state.packageBytes.get(COMMENTS_EXTENDED_PART_URI);
159331
159447
  let extendedByParaId = extendedBytes ? parseCommentsExtendedXml(TD12.decode(extendedBytes)) : new Map;
159332
159448
  if (entries.length > 1) {
159333
- extendedByParaId = mergeInferredThreading(extendedByParaId, inferThreadingFromStoryAnchors(state, entries));
159449
+ extendedByParaId = mergeInferredThreading(extendedByParaId, inferThreadingFromStoryAnchors(state, entries, extendedByParaId));
159334
159450
  }
159335
159451
  const idsBytes = state.packageBytes.get(COMMENTS_IDS_PART_URI);
159336
159452
  const durableByParaId = idsBytes ? parseCommentsIdsXml(TD12.decode(idsBytes)) : new Map;
@@ -159630,17 +159746,25 @@ function normalizeNullableCustomAttr(value) {
159630
159746
  return null;
159631
159747
  return decodeAttr(value);
159632
159748
  }
159633
- function inferThreadingFromStoryAnchors(state, entries) {
159749
+ function inferThreadingFromStoryAnchors(state, entries, explicit) {
159634
159750
  const entryByWId = new Map;
159635
159751
  for (const entry of entries) {
159636
159752
  if (!entryByWId.has(entry.wId))
159637
159753
  entryByWId.set(entry.wId, entry);
159638
159754
  }
159755
+ const paraIdOwners = new Map;
159756
+ for (const entry of entries) {
159757
+ for (const paraId of new Set(entry.bodyParaIds))
159758
+ paraIdOwners.set(paraId, (paraIdOwners.get(paraId) ?? 0) + 1);
159759
+ }
159639
159760
  const sessionParaIds = state.idReservations.comments.paraIds;
159640
159761
  const importedSourceIds = new Set(entries.filter((entry) => {
159641
159762
  const paraId = entry.bodyParaIds[0];
159642
- return paraId && !entry.isExplicitRoot && !sessionParaIds.has(paraId);
159763
+ return paraId && paraIdOwners.get(paraId) === 1 && !entry.isExplicitRoot && !sessionParaIds.has(paraId);
159643
159764
  }).map((entry) => entry.wId));
159765
+ const inferenceSourceIds = new Set([...entryByWId.values()].filter((entry) => importedSourceIds.has(entry.wId) && !entry.bodyParaIds.some((paraId) => explicit.has(paraId))).map((entry) => entry.wId));
159766
+ if (inferenceSourceIds.size === 0)
159767
+ return new Map;
159644
159768
  const sourceOrder = new Map;
159645
159769
  for (let i = 0;i < entries.length; i += 1) {
159646
159770
  const entry = entries[i];
@@ -159664,11 +159788,6 @@ function inferThreadingFromStoryAnchors(state, entries) {
159664
159788
  groups.push([anchor]);
159665
159789
  }
159666
159790
  const out = new Map;
159667
- for (const entry of entries) {
159668
- const paraId = entry.bodyParaIds[0];
159669
- if (paraId)
159670
- out.set(paraId, { parentParaId: null, done: false });
159671
- }
159672
159791
  for (const group of groups) {
159673
159792
  const uniqueSourceIds = Array.from(new Set(group.map((a) => a.sourceWId)));
159674
159793
  if (uniqueSourceIds.length < 2)
@@ -159680,7 +159799,7 @@ function inferThreadingFromStoryAnchors(state, entries) {
159680
159799
  if (!rootParaId)
159681
159800
  continue;
159682
159801
  for (const sourceWId of uniqueSourceIds) {
159683
- if (sourceWId === rootSourceId)
159802
+ if (sourceWId === rootSourceId || !inferenceSourceIds.has(sourceWId))
159684
159803
  continue;
159685
159804
  const replyEntry = entryByWId.get(sourceWId);
159686
159805
  const replyParaId = replyEntry?.bodyParaIds[0];
@@ -159688,12 +159807,25 @@ function inferThreadingFromStoryAnchors(state, entries) {
159688
159807
  continue;
159689
159808
  const parentSourceId = chooseContainingParentSourceId(sourceWId, uniqueSourceIds, envelopeBySourceId, sourceOrder) ?? rootSourceId;
159690
159809
  const parentEntry = entryByWId.get(parentSourceId);
159691
- const parentParaId = parentEntry?.bodyParaIds[0] ?? rootParaId;
159810
+ const parentParaId = parentEntry ? selectThreadingParaId(parentEntry, explicit) : rootParaId;
159811
+ if (wouldCreateThreadCycle(replyParaId, parentParaId, explicit, out))
159812
+ continue;
159692
159813
  out.set(replyParaId, { parentParaId, done: false });
159693
159814
  }
159694
159815
  }
159695
159816
  return out;
159696
159817
  }
159818
+ function wouldCreateThreadCycle(childParaId, parentParaId, explicit, inferred) {
159819
+ const seen = new Set([childParaId]);
159820
+ let cursor = parentParaId;
159821
+ while (cursor !== null) {
159822
+ if (seen.has(cursor))
159823
+ return true;
159824
+ seen.add(cursor);
159825
+ cursor = (explicit.get(cursor) ?? inferred.get(cursor))?.parentParaId ?? null;
159826
+ }
159827
+ return false;
159828
+ }
159697
159829
  function representativeEnvelopeBySourceId(group) {
159698
159830
  const out = new Map;
159699
159831
  for (const envelope of group) {
@@ -159758,17 +159890,8 @@ function mergeInferredThreading(explicit, inferred) {
159758
159890
  return explicit;
159759
159891
  const out = new Map(explicit);
159760
159892
  for (const [paraId, inferredExt] of inferred) {
159761
- const existing = out.get(paraId);
159762
- if (!existing) {
159893
+ if (!out.has(paraId))
159763
159894
  out.set(paraId, inferredExt);
159764
- continue;
159765
- }
159766
- if (existing.parentParaId === null && inferredExt.parentParaId !== null) {
159767
- out.set(paraId, {
159768
- parentParaId: inferredExt.parentParaId,
159769
- done: existing.done
159770
- });
159771
- }
159772
159895
  }
159773
159896
  return out;
159774
159897
  }
@@ -159779,25 +159902,38 @@ function collectCommentAnchorEnvelopes(state) {
159779
159902
  continue;
159780
159903
  const xml = TD12.decode(bytes);
159781
159904
  const starts = [];
159782
- const ends = [];
159905
+ const endsBySourceId = new Map;
159783
159906
  const tokenRe = /<[\w.-]+:(commentRangeStart|commentRangeEnd)\b[^>]*\bw:id="([^"]+)"[^>]*\/?>/g;
159784
159907
  for (let match = tokenRe.exec(xml);match; match = tokenRe.exec(xml)) {
159785
159908
  const item = { sourceWId: match[2] ?? "", pos: match.index };
159786
159909
  if (match[1] === "commentRangeStart")
159787
159910
  starts.push(item);
159788
- else
159789
- ends.push(item);
159911
+ else {
159912
+ const ends = endsBySourceId.get(item.sourceWId);
159913
+ if (ends)
159914
+ ends.push(item.pos);
159915
+ else
159916
+ endsBySourceId.set(item.sourceWId, [item.pos]);
159917
+ }
159790
159918
  }
159919
+ const endCursors = new Map;
159791
159920
  for (const start of starts) {
159792
- const end = ends.find((candidate2) => candidate2.sourceWId === start.sourceWId && candidate2.pos >= start.pos);
159793
- if (end)
159794
- out.push({ sourceWId: start.sourceWId, start: start.pos, end: end.pos });
159921
+ const ends = endsBySourceId.get(start.sourceWId);
159922
+ if (!ends)
159923
+ continue;
159924
+ let cursor = endCursors.get(start.sourceWId) ?? 0;
159925
+ while (cursor < ends.length && ends[cursor] < start.pos)
159926
+ cursor++;
159927
+ endCursors.set(start.sourceWId, cursor);
159928
+ const end = ends[cursor];
159929
+ if (end !== undefined)
159930
+ out.push({ partUri, sourceWId: start.sourceWId, start: start.pos, end });
159795
159931
  }
159796
159932
  }
159797
159933
  return out;
159798
159934
  }
159799
159935
  function envelopesOverlap(a, b) {
159800
- return a.start <= b.end && b.start <= a.end;
159936
+ return a.partUri === b.partUri && a.start <= b.end && b.start <= a.end;
159801
159937
  }
159802
159938
  function isStoryPartUri(partUri) {
159803
159939
  return partUri === "/word/document.xml" || /^\/word\/header\d+\.xml$/.test(partUri) || /^\/word\/footer\d+\.xml$/.test(partUri) || partUri === "/word/footnotes.xml" || partUri === "/word/endnotes.xml";
@@ -159961,27 +160097,42 @@ function shiftedMarker(marker, delta) {
159961
160097
  paragraph: marker.paragraph ? { ...marker.paragraph, range: shift(marker.paragraph.range) } : null
159962
160098
  };
159963
160099
  }
159964
- function getBodyCommentMarkerFacts(state, partUri) {
160100
+ function indexMarkersBySourceId(markers) {
160101
+ const index = new Map;
160102
+ for (const marker of markers) {
160103
+ const existing = index.get(marker.id);
160104
+ if (existing)
160105
+ existing.push(marker);
160106
+ else
160107
+ index.set(marker.id, [marker]);
160108
+ }
160109
+ return index;
160110
+ }
160111
+ function getBodyCommentByteFacts(state, partUri) {
159965
160112
  const bytes = state.packageBytes.get(partUri);
159966
160113
  const revision = state.parts.get(partUri)?.descriptor.revision;
159967
160114
  if (!bytes || !revision)
159968
- return [];
160115
+ return null;
159969
160116
  const cache = byteFacts(state);
159970
160117
  const prior = cache.get(partUri);
159971
160118
  if (prior?.bytes === bytes && prior.partRevision === revision)
159972
- return prior.markers;
160119
+ return prior;
159973
160120
  const bom = bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191 ? 3 : 0;
159974
160121
  const scanned = scanBodyCommentMarkers(TD13.decode(bytes));
159975
160122
  const markers = scanned.markers.map((marker) => shiftedMarker(marker, bom));
159976
- cache.set(partUri, { bytes, partRevision: revision, markers, complete: scanned.complete });
159977
- return markers;
160123
+ const facts = { bytes, partRevision: revision, markers, markersBySourceId: indexMarkersBySourceId(markers), complete: scanned.complete };
160124
+ cache.set(partUri, facts);
160125
+ return facts;
160126
+ }
160127
+ function getBodyCommentMarkerFacts(state, partUri) {
160128
+ return getBodyCommentByteFacts(state, partUri)?.markers ?? [];
160129
+ }
160130
+ function getBodyCommentMarkersForSourceId(state, partUri, commentId) {
160131
+ return getBodyCommentByteFacts(state, partUri)?.markersBySourceId.get(commentId) ?? [];
159978
160132
  }
159979
160133
  function readCompleteBodyCommentMarkerFacts(state, partUri) {
159980
- if (!state.packageBytes.has(partUri))
159981
- return null;
159982
- const markers = getBodyCommentMarkerFacts(state, partUri);
159983
- const facts = byteFacts(state).get(partUri);
159984
- return facts?.complete === true && facts.bytes === state.packageBytes.get(partUri) && facts.partRevision === state.parts.get(partUri)?.descriptor.revision ? markers : null;
160134
+ const facts = getBodyCommentByteFacts(state, partUri);
160135
+ return facts?.complete === true ? facts.markers : null;
159985
160136
  }
159986
160137
  function peekBodyCommentMarkerFacts(state, partUri) {
159987
160138
  const cache = state.partRegistries.get(BYTE_FACTS_KEY);
@@ -160011,22 +160162,22 @@ function refreshBodyCommentMarkerFactsAfterParagraphMutation(input) {
160011
160162
  }
160012
160163
  const scanned = scanBodyCommentMarkers(TD13.decode(bytes.subarray(input.beforeStart, input.afterEnd)), input.namespaceBindings);
160013
160164
  const current = scanned.markers.map((marker) => shiftedMarker(marker, input.beforeStart));
160165
+ const markers = [...unchanged, ...current].sort((a, b) => a.range.byteStart - b.range.byteStart);
160014
160166
  cache.set(input.partUri, {
160015
160167
  bytes,
160016
160168
  partRevision: revision,
160017
160169
  complete: prior.complete && scanned.complete,
160018
- markers: [...unchanged, ...current].sort((a, b) => a.range.byteStart - b.range.byteStart)
160170
+ markers,
160171
+ markersBySourceId: indexMarkersBySourceId(markers)
160019
160172
  });
160020
160173
  }
160021
160174
  function findRetainedBodyCommentAnchors(state, partUri, commentId, occurrenceIndex) {
160022
- return anchorsFromMarkers(getBodyCommentMarkerFacts(state, partUri), commentId, occurrenceIndex);
160175
+ return anchorsFromMarkers(getBodyCommentMarkersForSourceId(state, partUri, commentId), commentId, occurrenceIndex);
160023
160176
  }
160024
160177
  function countRetainedBodyCommentAnchorOccurrences(state, partUri, commentId) {
160025
160178
  const counts = { commentRangeStart: 0, commentRangeEnd: 0, commentReference: 0 };
160026
- for (const marker of getBodyCommentMarkerFacts(state, partUri)) {
160027
- if (marker.id === commentId)
160028
- counts[marker.kind] += 1;
160029
- }
160179
+ for (const marker of getBodyCommentMarkersForSourceId(state, partUri, commentId))
160180
+ counts[marker.kind] += 1;
160030
160181
  return Math.max(counts.commentRangeStart, counts.commentRangeEnd, counts.commentReference);
160031
160182
  }
160032
160183
  function collectBodyCommentMarkers(documentXml, namespaceBindings) {
@@ -185500,7 +185651,8 @@ function createReviewStagingState(state, targetBlockIds) {
185500
185651
  result[key3] = clone2(item);
185501
185652
  return result;
185502
185653
  };
185503
- Object.assign(staged, Object.fromEntries(Object.entries(staged).filter(([key3]) => !["packageBytes", "paragraphIndexes", "partRegistries"].includes(key3)).map(([key3, value]) => [key3, clone2(value)])));
185654
+ Object.assign(staged, Object.fromEntries(Object.entries(staged).filter(([key3]) => !["packageBytes", "paragraphIndexes", "partRegistries", "history"].includes(key3)).map(([key3, value]) => [key3, clone2(value)])));
185655
+ staged.history = { entries: new Map(state.history.entries), order: [...state.history.order] };
185504
185656
  staged.paragraphIndexes = new Map;
185505
185657
  staged.partRegistries = new Map;
185506
185658
  staged.revisions = new RevisionAllocator("review-preview");
@@ -202035,6 +202187,7 @@ function commitResidentText(input, view) {
202035
202187
  return null;
202036
202188
  const range = { byteStart: paragraph.byteStart, byteEnd: paragraph.byteEnd };
202037
202189
  const collapsed = op.on.startOffset === op.on.endOffset;
202190
+ const leadingBookmarkBoundary = collapsed && op.newText.length > 0 && edit.visibleStart === 0 && edit.before.model.leaves().some((piece) => piece.kind === "marker" && piece.visibleStart === 0 && (piece.markerKind === "bookmarkStart" || piece.markerKind === "bookmarkEnd"));
202038
202191
  const requiresCollaborationSessionSync = op.changeMode === "direct" && op.on.coordinateSpace === "tracked" && edit.visibleStart !== op.on.startOffset;
202039
202192
  return finalizeCommit({
202040
202193
  input,
@@ -202059,7 +202212,8 @@ function commitResidentText(input, view) {
202059
202212
  blockId: op.on.startBlockId,
202060
202213
  startOffset: edit.visibleStart,
202061
202214
  text: op.newText,
202062
- runPropertiesXml: leaf.runProperties ?? ""
202215
+ runPropertiesXml: leaf.runProperties ?? "",
202216
+ ...leadingBookmarkBoundary ? { afterLeadingMarkers: true } : {}
202063
202217
  } } : {},
202064
202218
  ...edit.insertionRsid && !collapsed ? { trackedTextRefinement: {
202065
202219
  kind: "trackedTextRefinement",
@@ -264694,8 +264848,7 @@ class TxContext {
264694
264848
  const documentBytes = this.state.packageBytes.get(op.on.partUri);
264695
264849
  if (!documentBytes)
264696
264850
  return rejected21(this.txId, "invalid-range");
264697
- const documentXml = TD78.decode(documentBytes);
264698
- const anchors = findBodyCommentAnchors(documentXml, markerWId, sourceStoryInfo.localOccurrenceIndex);
264851
+ const anchors = sourceStoryInfo.anchors;
264699
264852
  if (!anchors.rangeStart || !anchors.rangeEnd) {
264700
264853
  return rejected21(this.txId, "comment-anchor-not-found");
264701
264854
  }
@@ -264714,7 +264867,7 @@ class TxContext {
264714
264867
  continue;
264715
264868
  if (threadStory.partUri !== op.on.partUri)
264716
264869
  return rejected21(this.txId, "comment-move-cross-story");
264717
- const threadAnchors = findBodyCommentAnchors(documentXml, threadMarkerWId, threadStory.localOccurrenceIndex);
264870
+ const threadAnchors = threadStory.anchors;
264718
264871
  if (threadAnchors.rangeStart)
264719
264872
  removeRanges.push(threadAnchors.rangeStart);
264720
264873
  if (threadAnchors.rangeEnd)
@@ -264741,7 +264894,7 @@ class TxContext {
264741
264894
  };
264742
264895
  if (newRange.byteEnd < newRange.byteStart)
264743
264896
  return rejected21(this.txId, "invalid-range");
264744
- if (!op.allowMarkerCarrierOverlap && rangesOverlap(oldEnvelope, newRange)) {
264897
+ if (!op.allowMarkerCarrierOverlap && rangesOverlap(oldEnvelope, newRange) && !isUnchangedCommentRange(this.state, op.on, markerWId, anchors)) {
264745
264898
  return rejected21(this.txId, "comment-move-overlap");
264746
264899
  }
264747
264900
  const stripped = stripBytes2(documentBytes, mergedRemovals);
@@ -264749,7 +264902,7 @@ class TxContext {
264749
264902
  const newEnd = translateAfterRemovals(newRange.byteEnd, mergedRemovals);
264750
264903
  if (newStart == null || newEnd == null)
264751
264904
  return rejected21(this.txId, "comment-move-overlap");
264752
- const rewrite = rewriteDocumentXml(TD78.decode(stripped), { ...op.on, startByte: newStart, endByte: newEnd }, markerWId, undefined, {
264905
+ const rewrite = rewriteDocumentXml(TD_KEEP_BOM.decode(stripped), { ...op.on, startByte: newStart, endByte: newEnd }, markerWId, undefined, {
264753
264906
  start: relocatedAnchors.map((anchor) => commentRangeStart(anchor.markerWId)).join(""),
264754
264907
  end: relocatedAnchors.map((anchor) => commentRangeEnd(anchor.markerWId) + commentReferenceRun(anchor.markerWId, anchor.referenceRunXml)).join("")
264755
264908
  });
@@ -264778,28 +264931,31 @@ class TxContext {
264778
264931
  const commentsBytes = this.state.packageBytes.get(COMMENTS_PART_URI3);
264779
264932
  if (!commentsBytes)
264780
264933
  return rejected21(this.txId, "comments-part-missing");
264781
- const commentsXml = TD78.decode(commentsBytes);
264782
- let commentsNext = commentsXml;
264934
+ const bom = commentsBytes[0] === 239 && commentsBytes[1] === 187 && commentsBytes[2] === 191 ? 3 : 0;
264935
+ const commentsPatches = [];
264783
264936
  for (const threadEntry of threadEntries) {
264784
- const rewritten = rewriteCommentTrackedChangeMetadata(commentsNext, threadEntry.sourceWId || threadEntry.wId, threadEntry.sourceWIdOccurrenceIndex, op.trackedChangeMetadata);
264937
+ const startByte = threadEntry.byteStart + bom;
264938
+ const endByte = threadEntry.byteEnd + bom;
264939
+ const fragment3 = TD78.decode(commentsBytes.subarray(startByte, endByte));
264940
+ const rewritten = rewriteCommentTrackedChangeMetadata(fragment3, threadEntry.sourceWId || threadEntry.wId, 0, op.trackedChangeMetadata);
264785
264941
  if (!rewritten.ok)
264786
264942
  return rejected21(this.txId, rewritten.reason);
264787
- commentsNext = rewritten.xml;
264943
+ if (rewritten.xml !== fragment3)
264944
+ commentsPatches.push({
264945
+ kind: "replaceBytes",
264946
+ order: commentsPatches.length,
264947
+ startByte,
264948
+ endByte,
264949
+ bytes: TE44.encode(rewritten.xml)
264950
+ });
264788
264951
  }
264789
- if (commentsNext !== commentsXml) {
264790
- const commentsPatch = {
264791
- kind: "replaceBytes",
264792
- order: 0,
264793
- startByte: 0,
264794
- endByte: commentsBytes.length,
264795
- bytes: TE44.encode(commentsNext)
264796
- };
264797
- perPart[COMMENTS_PART_URI3] = { kind: "modify", patches: [commentsPatch] };
264952
+ if (commentsPatches.length > 0) {
264953
+ perPart[COMMENTS_PART_URI3] = { kind: "modify", patches: commentsPatches };
264798
264954
  preconditions.push(byteWindowPrecondition10(COMMENTS_PART_URI3, commentsBytes));
264799
264955
  const commentsMap = buildPatchMap({
264800
264956
  partUri: COMMENTS_PART_URI3,
264801
264957
  originalLength: commentsBytes.length,
264802
- patches: [commentsPatch]
264958
+ patches: commentsPatches
264803
264959
  });
264804
264960
  patchMaps[COMMENTS_PART_URI3] = commentsMap.ok ? commentsMap.value : { partUri: COMMENTS_PART_URI3, ranges: [] };
264805
264961
  }
@@ -265440,6 +265596,34 @@ function locateCommentAnchorStory(state, commentId, occurrenceIndex) {
265440
265596
  }
265441
265597
  return null;
265442
265598
  }
265599
+ function isUnchangedCommentRange(state, target, markerWId, anchors) {
265600
+ if (!anchors.rangeStart || !anchors.rangeEnd)
265601
+ return false;
265602
+ const markers = getBodyCommentMarkersForSourceId(state, target.partUri, markerWId);
265603
+ const bytes = state.packageBytes.get(target.partUri);
265604
+ if (!bytes)
265605
+ return false;
265606
+ const models = new Map;
265607
+ const matchesBoundary = (kind, anchor, blockId, offset) => {
265608
+ const marker = markers.find((item) => item.kind === kind && item.range.byteStart === anchor.byteStart);
265609
+ const paragraph = marker?.paragraph;
265610
+ if (!paragraph || anchor.byteStart < paragraph.range.byteStart || anchor.byteEnd > paragraph.range.byteEnd)
265611
+ return false;
265612
+ if (paragraph.blockId !== blockId) {
265613
+ const indexed = getStoryParagraphIndexFromState(state, target.storyId)?.byParaId(blockId);
265614
+ if (!indexed || indexed.partUri !== target.partUri || indexed.partRevision !== target.partRevision || indexed.byteStart !== paragraph.range.byteStart || indexed.byteEnd !== paragraph.range.byteEnd)
265615
+ return false;
265616
+ }
265617
+ let model = models.get(paragraph.range.byteStart);
265618
+ if (!model) {
265619
+ model = parseParagraphInlineMap(TD78.decode(bytes.subarray(paragraph.range.byteStart, paragraph.range.byteEnd)), paragraph.namespaceBindings);
265620
+ models.set(paragraph.range.byteStart, model);
265621
+ }
265622
+ const leaf = model.leaves().find((piece) => piece.kind === "marker" && piece.id === markerWId && piece.markerKind === kind && piece.byteStart + paragraph.range.byteStart === anchor.byteStart);
265623
+ return !!leaf && (target.coordinateSpace === "tracked" ? leaf.trackedStart : leaf.visibleStart) === offset;
265624
+ };
265625
+ return matchesBoundary("commentRangeStart", anchors.rangeStart, target.startBlockId, target.startOffset) && matchesBoundary("commentRangeEnd", anchors.rangeEnd, target.endBlockId, target.endOffset);
265626
+ }
265443
265627
  function findCommentAnchorsForStory(state, partUri, storyXml, commentId, occurrenceIndex) {
265444
265628
  const localOccurrenceIndex = localOccurrenceIndexForStory(state, partUri, storyXml, commentId, occurrenceIndex);
265445
265629
  if (localOccurrenceIndex === null) {
@@ -266206,22 +266390,25 @@ function materializeCurrentCommentThreadExtendedRows(input) {
266206
266390
  Object.assign(input.patchMaps, compiled.value.patchMaps);
266207
266391
  return { ok: true };
266208
266392
  }
266209
- let nextXml = TD78.decode(extendedBytes);
266210
- let changed = false;
266211
- for (const item of entries2) {
266212
- const next = upsertCommentsExtendedThreadRow(nextXml, {
266213
- paraId: item.paraId,
266214
- parentParaId: item.parentParaId,
266215
- done: item.done
266216
- });
266217
- if (!next.ok)
266218
- return next;
266219
- if (next.value !== nextXml) {
266220
- nextXml = next.value;
266221
- changed = true;
266222
- }
266393
+ const xml = TD_KEEP_BOM.decode(extendedBytes);
266394
+ const remaining = new Map(entries2.map((item) => [item.paraId, item]));
266395
+ let nextXml = xml.replace(/<[\w.-]+:commentEx\b[^>]*>/g, (fragment3) => {
266396
+ const paraId = /\b(?:w14|w15):paraId="([^"]*)"/.exec(fragment3)?.[1];
266397
+ const item = paraId ? remaining.get(paraId) : undefined;
266398
+ if (!item)
266399
+ return fragment3;
266400
+ remaining.delete(item.paraId);
266401
+ const next = upsertCommentsExtendedThreadRow(fragment3, item);
266402
+ return next.ok ? next.value : fragment3;
266403
+ });
266404
+ if (remaining.size > 0) {
266405
+ const fragments = [...remaining.values()].map(commentsExtendedRowXml).join("");
266406
+ const appended = appendToExistingXmlRoot(nextXml, fragments, "commentsEx");
266407
+ if (!appended.ok)
266408
+ return appended;
266409
+ nextXml = appended.value;
266223
266410
  }
266224
- if (changed)
266411
+ if (nextXml !== xml)
266225
266412
  addWholePartReplace(input, COMMENTS_EXTENDED_PART_URI3, extendedBytes, nextXml);
266226
266413
  return { ok: true };
266227
266414
  }
@@ -266277,11 +266464,13 @@ function threadRootParaId(catalog, entry) {
266277
266464
  }
266278
266465
  return entry.paraId;
266279
266466
  }
266280
- function appendCommentsExtendedXml(xml, input) {
266467
+ function commentsExtendedRowXml(input) {
266281
266468
  const parentAttr = input.parentParaId ? ` w15:paraIdParent="${escapeXmlAttribute(input.parentParaId)}"` : "";
266282
266469
  const doneAttr = input.done ? "1" : "0";
266283
- const fragment3 = `<w15:commentEx w15:paraId="${escapeXmlAttribute(input.paraId)}" w15:done="${doneAttr}"${parentAttr}/>`;
266284
- return appendToExistingXmlRoot(xml, fragment3, "commentsEx");
266470
+ return `<w15:commentEx w15:paraId="${escapeXmlAttribute(input.paraId)}" w15:done="${doneAttr}"${parentAttr}/>`;
266471
+ }
266472
+ function appendCommentsExtendedXml(xml, input) {
266473
+ return appendToExistingXmlRoot(xml, commentsExtendedRowXml(input), "commentsEx");
266285
266474
  }
266286
266475
  function appendCommentsIdsXml(xml, input) {
266287
266476
  const fragment3 = `<w16cid:commentId w16cid:paraId="${escapeXmlAttribute(input.paraId)}" w16cid:durableId="${escapeXmlAttribute(input.durableId)}"/>`;
@@ -266448,7 +266637,7 @@ function emptyHeaderFooterSkeleton(kind) {
266448
266637
  const root = kind === "header" ? "w:hdr" : "w:ftr";
266449
266638
  return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' + `<${root}` + ' xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"' + ' xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"' + ">" + "<w:p/>" + `</${root}>`;
266450
266639
  }
266451
- var TE44, TD78, nextTx = 0, CONTENT_TYPES_PART_URI12 = "/[Content_Types].xml", DOCUMENT_RELS_PART_URI10 = "/word/_rels/document.xml.rels", COMMENTS_PART_URI3 = "/word/comments.xml", COMMENTS_IDS_PART_URI3 = "/word/commentsIds.xml", COMMENTS_EXTENDED_PART_URI3 = "/word/commentsExtended.xml", SDCOM_COMMENTS_NS_URI = "http://superdoc.dev/2024/comments", COMMENT_ELEMENT_RE, RUN_HAS_TEXT_LEAF_VISIBLE_RE, RUN_HAS_TEXT_LEAF_TRACKED_RE, RUN_CONTROL_LEAF_RE, RUN_TEXT_LEAF_RE_VISIBLE, RUN_TEXT_LEAF_RE_TRACKED;
266640
+ var TE44, TD78, TD_KEEP_BOM, nextTx = 0, CONTENT_TYPES_PART_URI12 = "/[Content_Types].xml", DOCUMENT_RELS_PART_URI10 = "/word/_rels/document.xml.rels", COMMENTS_PART_URI3 = "/word/comments.xml", COMMENTS_IDS_PART_URI3 = "/word/commentsIds.xml", COMMENTS_EXTENDED_PART_URI3 = "/word/commentsExtended.xml", SDCOM_COMMENTS_NS_URI = "http://superdoc.dev/2024/comments", COMMENT_ELEMENT_RE, RUN_HAS_TEXT_LEAF_VISIBLE_RE, RUN_HAS_TEXT_LEAF_TRACKED_RE, RUN_CONTROL_LEAF_RE, RUN_TEXT_LEAF_RE_VISIBLE, RUN_TEXT_LEAF_RE_TRACKED;
266452
266641
  var init_tx_context = __esm(() => {
266453
266642
  init_resident_paragraph_ownership();
266454
266643
  init_canonical_xml_analysis_frame();
@@ -266494,6 +266683,7 @@ var init_tx_context = __esm(() => {
266494
266683
  init_compile_image_ops();
266495
266684
  TE44 = new TextEncoder;
266496
266685
  TD78 = new TextDecoder;
266686
+ TD_KEEP_BOM = new TextDecoder("utf-8", { ignoreBOM: true });
266497
266687
  COMMENT_ELEMENT_RE = /<[\w.-]+:comment(?=[\s>/])/;
266498
266688
  RUN_HAS_TEXT_LEAF_VISIBLE_RE = /<[\w.-]+:t\b/;
266499
266689
  RUN_HAS_TEXT_LEAF_TRACKED_RE = /<[\w.-]+:(t|delText)\b/;
@@ -377832,13 +378022,29 @@ function resolveCommentEntryByParaId(catalog, paraId) {
377832
378022
  return null;
377833
378023
  return catalog.commentsByCommentId.get(canonicalCommentId) ?? null;
377834
378024
  }
378025
+ function commentAnchorProjectionEntry(entry, catalog, anchorIndex) {
378026
+ let cursor = entry;
378027
+ const seen = new Set;
378028
+ while (cursor.parentParaId !== null) {
378029
+ if (seen.has(cursor.commentId))
378030
+ return entry;
378031
+ seen.add(cursor.commentId);
378032
+ const hits = anchorIndex.hitsBySourceWId.get(cursor.sourceWId) ?? [];
378033
+ if (catalog.commentsBySourceWId.get(cursor.sourceWId)?.length !== 1 || !hits.some((hit) => hit.hasReference) || hits.some((hit) => hit.hasRangeStart || hit.hasRangeEnd))
378034
+ break;
378035
+ const parent = resolveCommentEntryByParaId(catalog, cursor.parentParaId);
378036
+ if (!parent)
378037
+ break;
378038
+ cursor = parent;
378039
+ }
378040
+ return cursor;
378041
+ }
377835
378042
  function toCommentInfo(entry, catalog, anchorIndex, headerFooterRels, trackedAssociations) {
377836
378043
  const publicCommentId = publicCommentIdForEntry(entry);
377837
378044
  const parentParaId = entry.parentParaId;
377838
378045
  const parentEntry = parentParaId ? resolveCommentEntryByParaId(catalog, parentParaId) : null;
377839
378046
  const parentCommentId = parentEntry ? publicCommentIdForEntry(parentEntry) : undefined;
377840
378047
  const rootCommentId = parentCommentId ? rootCommentIdForCatalogEntry(entry, catalog) : null;
377841
- const anchorProjectionEntry = entry;
377842
378048
  const info = {
377843
378049
  address: { kind: "entity", entityType: "comment", entityId: publicCommentId },
377844
378050
  commentId: publicCommentId,
@@ -377860,10 +378066,9 @@ function toCommentInfo(entry, catalog, anchorIndex, headerFooterRels, trackedAss
377860
378066
  info.creatorImage = entry.authorImage;
377861
378067
  if (entry.importedId !== null && entry.importedId !== publicCommentId)
377862
378068
  info.importedId = entry.importedId;
377863
- let sourceSiblings;
377864
378069
  if (entry.sourceWId) {
377865
378070
  info.rawOoxmlId = entry.sourceWId;
377866
- sourceSiblings = catalog.commentsBySourceWId.get(entry.sourceWId);
378071
+ const sourceSiblings = catalog.commentsBySourceWId.get(entry.sourceWId);
377867
378072
  if (sourceSiblings && sourceSiblings.length > 1) {
377868
378073
  info.rawOoxmlOccurrenceIndex = entry.sourceWIdOccurrenceIndex;
377869
378074
  }
@@ -377901,11 +378106,12 @@ function toCommentInfo(entry, catalog, anchorIndex, headerFooterRels, trackedAss
377901
378106
  if (!info.trackedChange) {
377902
378107
  attachMetadataOnlyTrackedChangeLink(info, entry, anchorIndex ? null : trackedAssociations, metadataAssociation.status !== "unsafe");
377903
378108
  }
377904
- if (anchorIndex && anchorProjectionEntry) {
378109
+ if (anchorIndex) {
378110
+ const anchorProjectionEntry = commentAnchorProjectionEntry(entry, catalog, anchorIndex);
377905
378111
  const hits = anchorIndex.hitsBySourceWId.get(anchorProjectionEntry.sourceWId) ?? [];
377906
378112
  const projectable = hits.filter((h) => h.story.kind === "body" || h.story.kind === "header" || h.story.kind === "footer");
377907
378113
  if (projectable.length > 0) {
377908
- sourceSiblings ?? (sourceSiblings = catalog.commentsBySourceWId.get(anchorProjectionEntry.sourceWId));
378114
+ const sourceSiblings = catalog.commentsBySourceWId.get(anchorProjectionEntry.sourceWId);
377909
378115
  const globalSegmentIndex = sourceSiblings && sourceSiblings.length > 1 ? anchorProjectionEntry.sourceWIdOccurrenceIndex : undefined;
377910
378116
  const projected = projectAnchor(projectable, headerFooterRels, globalSegmentIndex);
377911
378117
  if (projected) {
@@ -385238,6 +385444,7 @@ class V2ListsAdapter {
385238
385444
  if (insertReceipt.status === "rejected") {
385239
385445
  return failure8(mapRejectionReason(insertReceipt.rejectedReason ?? "internal-error"), insertReceipt.rejectedReason ?? "list insert: paragraph insert rejected");
385240
385446
  }
385447
+ this.context.invalidate(insertReceipt);
385241
385448
  recordCommittedTxIdForHistory(this.context, insertReceipt.txId);
385242
385449
  const newParaId = readCreatedParagraphRefId(insertReceipt.createdRefs ?? [], insertReceipt.provisionalToFinal);
385243
385450
  if (!newParaId) {
@@ -385364,6 +385571,7 @@ class V2ListsAdapter {
385364
385571
  if (insertReceipt.status === "rejected") {
385365
385572
  return failure8(mapRejectionReason(insertReceipt.rejectedReason ?? "internal-error"), insertReceipt.rejectedReason ?? "list create: paragraph insert rejected");
385366
385573
  }
385574
+ this.context.invalidate(insertReceipt);
385367
385575
  recordCommittedTxIdForHistory(this.context, insertReceipt.txId);
385368
385576
  const newParaId = readCreatedParagraphRefId(insertReceipt.createdRefs ?? [], insertReceipt.provisionalToFinal);
385369
385577
  if (!newParaId) {
@@ -387095,6 +387303,7 @@ function applyListSeed(adapter, storyId, paraId, kind, expectedRevision, changeM
387095
387303
  if (receipt3.status === "rejected") {
387096
387304
  return failure8(mapRejectionReason(receipt3.rejectedReason ?? "internal-error"), receipt3.rejectedReason ?? "list create rejected");
387097
387305
  }
387306
+ adapter.context.invalidate(receipt3);
387098
387307
  recordCommittedTxIdForHistory(adapter.context, receipt3.txId);
387099
387308
  const summary = readListItemSummaryForSession(adapter.context.session, storyId, paraId);
387100
387309
  return {
@@ -419150,8 +419359,67 @@ async function compilePlan(context, input) {
419150
419359
  }
419151
419360
  detectTargetConflicts(context, steps, failures);
419152
419361
  detectUnsupportedRuntimeDrift(input.steps, failures);
419153
- detectTargetOffsetDriftRisk(steps, failures);
419154
- return { evaluatedRevision, steps, failures };
419362
+ const exactProgram = failures.length === 0 ? compileDescendingExactTargets(context, steps) : undefined;
419363
+ if (exactProgram) {
419364
+ if (exactProgram.edits.length > 0) {
419365
+ const preview = new V2SelectionMutationAdapter(context).executeResolvedTextRewrites(exactProgram.edits, { changeMode: changeMode ?? "direct", dryRun: true });
419366
+ if (!preview.success)
419367
+ failures.push({
419368
+ code: preview.failure.code,
419369
+ stepId: exactProgram.edits[0].ownerStepId,
419370
+ phase: "compile",
419371
+ message: preview.failure.message
419372
+ });
419373
+ }
419374
+ } else
419375
+ detectTargetOffsetDriftRisk(steps, failures);
419376
+ return {
419377
+ evaluatedRevision,
419378
+ steps,
419379
+ failures,
419380
+ ...exactProgram ? { atomicTextProgram: exactProgram } : {}
419381
+ };
419382
+ }
419383
+ function compileDescendingExactTargets(context, steps) {
419384
+ const mutating = steps.filter((step3) => step3.op !== "assert");
419385
+ if (mutating.length < 2)
419386
+ return;
419387
+ const edits = [];
419388
+ let blockId;
419389
+ let previousStart = Infinity;
419390
+ const index2 = getSDDocumentSessionInternalParagraphIndex(context.session);
419391
+ if (!index2)
419392
+ return;
419393
+ for (const step3 of mutating) {
419394
+ const source = step3.source;
419395
+ if (step3.op !== "text.rewrite" && step3.op !== "text.delete" || source.op === "assert" || !("where" in source) || source.where.by !== "target" || !step3.resolution || step3.request?.kind !== "replace")
419396
+ return;
419397
+ const target = source.where.target;
419398
+ if (target.kind !== "selection" || target.start.kind !== "text" || target.end.kind !== "text" || target.coordinateSpace === "tracked" || !isSupportedMutationStoryScope(target.story) || !isSupportedMutationStoryScope(target.start.story) || !isSupportedMutationStoryScope(target.end.story) || target.start.blockId !== target.end.blockId)
419399
+ return;
419400
+ blockId ?? (blockId = target.start.blockId);
419401
+ if (target.start.blockId !== blockId || step3.resolution.range.from >= step3.resolution.range.to || step3.resolution.range.to > previousStart)
419402
+ return;
419403
+ previousStart = step3.resolution.range.from;
419404
+ const on = resolveSelectionTargetToRange({
419405
+ target,
419406
+ index: index2,
419407
+ readPartBytes: (partUri) => getSDDocumentSessionInternalPartBytes(context.session, partUri),
419408
+ readPartRange: (partUri, start, end) => getSDDocumentSessionInternalPartRange(context.session, partUri, start, end),
419409
+ buildOffsetMap: buildOffsetMapForCoordinateSpace4
419410
+ });
419411
+ if ("error" in on)
419412
+ throw new DocumentApiValidationError2(on.error, on.message);
419413
+ if (step3.request.text !== step3.resolution.text)
419414
+ edits.push({
419415
+ on,
419416
+ newText: step3.request.text,
419417
+ resolution: step3.resolution,
419418
+ ownerStepId: step3.stepId,
419419
+ rewriteIntent: step3.op === "text.rewrite"
419420
+ });
419421
+ }
419422
+ return { edits };
419155
419423
  }
419156
419424
  function isOrderedAtomicTextProgram(steps) {
419157
419425
  return steps.length > 1 && steps.every((step3) => (step3.op === "text.rewrite" || step3.op === "text.delete") && step3.where.by === "select" && step3.where.select.type === "text" && step3.where.require === "all");
@@ -420748,6 +421016,7 @@ var init_engine3 = __esm(() => {
420748
421016
  init_sd_document_session_internal();
420749
421017
  init_search();
420750
421018
  init_raw_block_ref();
421019
+ init_selection_target();
420751
421020
  init_story_identity();
420752
421021
  init_story_surface();
420753
421022
  init_v2_search_ref();
@@ -475103,7 +475372,7 @@ function tryMirrorStoryText(prepared, extension2, options, admitted) {
475103
475372
  text: source.newText,
475104
475373
  txId: prepared.txId,
475105
475374
  directInsert: validation.directInsert,
475106
- preserveZeroLengthAtOffset: validation.inlineControl?.afterParagraphTail,
475375
+ preserveZeroLengthAtOffset: validation.directInsert.afterLeadingMarkers || validation.inlineControl?.afterParagraphTail,
475107
475376
  acceptedTextInsertionSplit: validation.acceptedTextInsertionSplit
475108
475377
  });
475109
475378
  } else {
@@ -475534,7 +475803,7 @@ function validateDirectTextInsert(source, extension2) {
475534
475803
  if (!directInsert) {
475535
475804
  return { ok: false, failureReason: "direct insert requires typed storySurface metadata" };
475536
475805
  }
475537
- if (directInsert.kind !== "directTextInsert" || directInsert.blockId !== source.on.startBlockId || directInsert.startOffset !== source.on.startOffset || directInsert.text !== source.newText) {
475806
+ if (directInsert.kind !== "directTextInsert" || directInsert.blockId !== source.on.startBlockId || directInsert.startOffset !== source.on.startOffset || directInsert.text !== source.newText || directInsert.afterLeadingMarkers !== undefined && (directInsert.afterLeadingMarkers !== true || directInsert.startOffset !== 0)) {
475538
475807
  return { ok: false, failureReason: "direct insert storySurface metadata does not match source" };
475539
475808
  }
475540
475809
  if (parsePreparedRunAttributes(directInsert.runPropertiesXml) === null) {
@@ -482861,6 +483130,25 @@ async function openV2CollaborativeDocumentSession(options) {
482861
483130
  } else {
482862
483131
  trace.mark("source-complete-deferred");
482863
483132
  }
483133
+ if (ownsPreparedBootstrap) {
483134
+ const current = readRoomManifest(rootConn.ydoc);
483135
+ if (current.snapshot?.meta.bootstrapState === "committed") {
483136
+ const committed = options.runtime.validateLateJoin({
483137
+ compatibility: { supports: COLLAB_V2_ROOM_SCHEMA_VERSION },
483138
+ readiness: READINESS_CLASS.bodyEditable
483139
+ });
483140
+ if (!committed.ok)
483141
+ throwLateJoinValidationFailure(committed, options.runtime.rootId);
483142
+ throw new CollabV2DiagnosticError(COLLAB_V2_DIAGNOSTICS.COLLAB_V2_ROOM_ALREADY_EXISTS, "another creator committed this v2 collaboration room during open; reopen with join", { rootId: options.runtime.rootId });
483143
+ }
483144
+ if (current.snapshot?.meta.bootstrapState !== "pending" || current.diagnostics.length > 0) {
483145
+ throw new CollabV2DiagnosticError(COLLAB_V2_DIAGNOSTICS.COLLAB_V2_ROOM_CORRUPT, "prepared v2 collaboration room is no longer a valid pending room; this open did not publish content", { rootId: options.runtime.rootId });
483146
+ }
483147
+ const leader = readBootstrapClaims(rootConn.ydoc).find((claim) => claim.status !== "aborted");
483148
+ if (leader?.claimerId !== expectedClaimerId || leader?.status !== "pending") {
483149
+ throw new CollabV2DiagnosticError(COLLAB_V2_DIAGNOSTICS.COLLAB_V2_ROOM_INITIALIZING, "another creator is initializing this v2 collaboration room", { rootId: options.runtime.rootId });
483150
+ }
483151
+ }
482864
483152
  const canonical = await buildCanonicalShardGraph({
482865
483153
  runtime: options.runtime,
482866
483154
  rootDoc: rootConn.ydoc,
@@ -483395,14 +483683,17 @@ async function buildCanonicalShardGraph(input) {
483395
483683
  for (const { story, shardId, bytes } of stories) {
483396
483684
  let unit;
483397
483685
  if (maySeedStoryShardsFromLocalSource && bytes) {
483398
- unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.story });
483399
- trySeedStoryShard(unit, {
483400
- shardId,
483401
- storyId: story.storyId,
483402
- storyKind: story.storyKind,
483403
- sourcePartUri: story.partUri,
483404
- bytes
483405
- });
483686
+ unit = rootDoc.transact(() => {
483687
+ const created = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.story });
483688
+ trySeedStoryShard(created, {
483689
+ shardId,
483690
+ storyId: story.storyId,
483691
+ storyKind: story.storyKind,
483692
+ sourcePartUri: story.partUri,
483693
+ bytes
483694
+ });
483695
+ return created;
483696
+ }, "collab-v2:bootstrap:seed-unit");
483406
483697
  } else {
483407
483698
  await waitForStoryShardSeeded(rootDoc, shardId);
483408
483699
  unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.story });
@@ -483475,13 +483766,16 @@ async function buildCanonicalShardGraph(input) {
483475
483766
  let commentsHandle = null;
483476
483767
  const commentsShardId = packageShardId2(input.runtime.rootId, SHARD_KIND.comments, COMMENTS_PART_URI6);
483477
483768
  {
483478
- const unit = createCollabUnitHandle({ rootDoc, shardId: commentsShardId, shardKind: SHARD_KIND.comments });
483479
- if (commentsFamily != null) {
483480
- const commentsCatalog = buildCommentsCatalogFromParts(new Map(commentsFamily.parts.flatMap((part) => part.bytes ? [[part.partUri, part.bytes]] : [])));
483481
- trySeedCommentsShard(unit, commentsShardId, commentsCatalog, commentsFamily.partKeys, commentsFamily.parts);
483482
- } else {
483483
- trySeedEmptyCommentsShard(unit, commentsShardId);
483484
- }
483769
+ const unit = rootDoc.transact(() => {
483770
+ const created = createCollabUnitHandle({ rootDoc, shardId: commentsShardId, shardKind: SHARD_KIND.comments });
483771
+ if (commentsFamily != null) {
483772
+ const commentsCatalog = buildCommentsCatalogFromParts(new Map(commentsFamily.parts.flatMap((part) => part.bytes ? [[part.partUri, part.bytes]] : [])));
483773
+ trySeedCommentsShard(created, commentsShardId, commentsCatalog, commentsFamily.partKeys, commentsFamily.parts);
483774
+ } else {
483775
+ trySeedEmptyCommentsShard(created, commentsShardId);
483776
+ }
483777
+ return created;
483778
+ }, "collab-v2:bootstrap:seed-unit");
483485
483779
  projectionHandles.set(commentsShardId, {
483486
483780
  shardId: commentsShardId,
483487
483781
  shardKind: SHARD_KIND.comments,
@@ -483609,243 +483903,259 @@ function subtreePackageHandle(rootDoc, unit, shardKind, bootstrap) {
483609
483903
  };
483610
483904
  }
483611
483905
  async function connectRelationshipsShard(rootDoc, shardId, part) {
483612
- const unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.relationships });
483613
- if (part.bytes) {
483614
- const ownerPartUri2 = ownerPartUriForRels3(part.partUri);
483615
- const xml = TEXT_DECODER12.decode(part.bytes);
483616
- const parsed = parseRelationshipsXml(xml, {
483617
- ownerPartUri: ownerPartUri2,
483618
- relsUri: part.partUri,
483619
- resolveTarget(target, targetMode) {
483620
- return resolveTargetUri(ownerPartUri2, target, targetMode);
483621
- }
483622
- });
483623
- try {
483624
- seedRelationshipsShard({
483625
- doc: unit,
483626
- shardId,
483627
- ownerPartKey: part.partKey,
483906
+ return rootDoc.transact(() => {
483907
+ const unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.relationships });
483908
+ if (part.bytes) {
483909
+ const ownerPartUri2 = ownerPartUriForRels3(part.partUri);
483910
+ const xml = TEXT_DECODER12.decode(part.bytes);
483911
+ const parsed = parseRelationshipsXml(xml, {
483628
483912
  ownerPartUri: ownerPartUri2,
483629
- relationships: parsed.relationships.map((relationship) => ({
483630
- rId: relationship.id,
483631
- type: relationship.type,
483632
- target: relationship.target,
483633
- targetMode: relationship.targetMode,
483634
- external: relationship.targetMode === "External"
483635
- }))
483913
+ relsUri: part.partUri,
483914
+ resolveTarget(target, targetMode) {
483915
+ return resolveTargetUri(ownerPartUri2, target, targetMode);
483916
+ }
483636
483917
  });
483637
- } catch (error50) {
483638
- if (!isAlreadySeededError(error50))
483639
- throw error50;
483918
+ try {
483919
+ seedRelationshipsShard({
483920
+ doc: unit,
483921
+ shardId,
483922
+ ownerPartKey: part.partKey,
483923
+ ownerPartUri: ownerPartUri2,
483924
+ relationships: parsed.relationships.map((relationship) => ({
483925
+ rId: relationship.id,
483926
+ type: relationship.type,
483927
+ target: relationship.target,
483928
+ targetMode: relationship.targetMode,
483929
+ external: relationship.targetMode === "External"
483930
+ }))
483931
+ });
483932
+ } catch (error50) {
483933
+ if (!isAlreadySeededError(error50))
483934
+ throw error50;
483935
+ }
483640
483936
  }
483641
- }
483642
- return subtreePackageHandle(rootDoc, unit, SHARD_KIND.relationships);
483937
+ return subtreePackageHandle(rootDoc, unit, SHARD_KIND.relationships);
483938
+ }, "collab-v2:bootstrap:seed-unit");
483643
483939
  }
483644
483940
  async function connectContentTypesShard(rootDoc, shardId, part) {
483645
- const unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.contentTypes });
483646
- if (part.bytes) {
483647
- const parsed = parseContentTypes(TEXT_DECODER12.decode(part.bytes));
483648
- try {
483649
- seedContentTypesShard({
483650
- doc: unit,
483651
- shardId,
483652
- ownerPartKey: part.partKey,
483653
- ownerPartUri: part.partUri,
483654
- defaults: Array.from(parsed.defaults.entries()).map(([extension2, contentType]) => ({
483655
- extension: extension2,
483656
- contentType
483657
- })),
483658
- overrides: Array.from(parsed.overrides.entries()).map(([partKey2, contentType]) => ({
483659
- partKey: partKey2,
483660
- contentType
483661
- }))
483662
- });
483663
- } catch (error50) {
483664
- if (!isAlreadySeededError(error50))
483665
- throw error50;
483941
+ return rootDoc.transact(() => {
483942
+ const unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.contentTypes });
483943
+ if (part.bytes) {
483944
+ const parsed = parseContentTypes(TEXT_DECODER12.decode(part.bytes));
483945
+ try {
483946
+ seedContentTypesShard({
483947
+ doc: unit,
483948
+ shardId,
483949
+ ownerPartKey: part.partKey,
483950
+ ownerPartUri: part.partUri,
483951
+ defaults: Array.from(parsed.defaults.entries()).map(([extension2, contentType]) => ({
483952
+ extension: extension2,
483953
+ contentType
483954
+ })),
483955
+ overrides: Array.from(parsed.overrides.entries()).map(([partKey2, contentType]) => ({
483956
+ partKey: partKey2,
483957
+ contentType
483958
+ }))
483959
+ });
483960
+ } catch (error50) {
483961
+ if (!isAlreadySeededError(error50))
483962
+ throw error50;
483963
+ }
483666
483964
  }
483667
- }
483668
- return subtreePackageHandle(rootDoc, unit, SHARD_KIND.contentTypes);
483965
+ return subtreePackageHandle(rootDoc, unit, SHARD_KIND.contentTypes);
483966
+ }, "collab-v2:bootstrap:seed-unit");
483669
483967
  }
483670
483968
  async function connectStylesShard(rootDoc, shardId, part) {
483671
- const unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.styles });
483672
- let bootstrap;
483673
- if (part.bytes) {
483674
- const xml = TEXT_DECODER12.decode(part.bytes);
483675
- const parsed = parseStylesPart3(xml);
483676
- bootstrap = bootstrapPartProjection(xml);
483677
- try {
483678
- seedStylesShard({
483679
- doc: unit,
483680
- shardId,
483681
- ownerPartKey: part.partKey,
483682
- ownerPartUri: part.partUri,
483683
- documentDefaults: parsed.documentDefaults,
483684
- styles: parsed.styles
483685
- });
483686
- } catch (error50) {
483687
- if (!isAlreadySeededError(error50))
483688
- throw error50;
483969
+ return rootDoc.transact(() => {
483970
+ const unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.styles });
483971
+ let bootstrap;
483972
+ if (part.bytes) {
483973
+ const xml = TEXT_DECODER12.decode(part.bytes);
483974
+ const parsed = parseStylesPart3(xml);
483975
+ bootstrap = bootstrapPartProjection(xml);
483976
+ try {
483977
+ seedStylesShard({
483978
+ doc: unit,
483979
+ shardId,
483980
+ ownerPartKey: part.partKey,
483981
+ ownerPartUri: part.partUri,
483982
+ documentDefaults: parsed.documentDefaults,
483983
+ styles: parsed.styles
483984
+ });
483985
+ } catch (error50) {
483986
+ if (!isAlreadySeededError(error50))
483987
+ throw error50;
483988
+ }
483689
483989
  }
483690
- }
483691
- return subtreePackageHandle(rootDoc, unit, SHARD_KIND.styles, bootstrap);
483990
+ return subtreePackageHandle(rootDoc, unit, SHARD_KIND.styles, bootstrap);
483991
+ }, "collab-v2:bootstrap:seed-unit");
483692
483992
  }
483693
483993
  async function connectNumberingShard(rootDoc, shardId, part, numbering) {
483694
- const unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.numbering });
483695
- let bootstrap;
483696
- if (part.bytes) {
483697
- bootstrap = bootstrapPartProjection(TEXT_DECODER12.decode(part.bytes));
483698
- }
483699
- if (part.bytes && numbering.present) {
483700
- try {
483701
- seedNumberingShard({
483702
- doc: unit,
483703
- shardId,
483704
- ownerPartKey: part.partKey,
483705
- ownerPartUri: part.partUri,
483706
- abstractNums: Array.from(numbering.abstracts.values()).map((entry) => ({
483707
- abstractNumId: entry.abstractNumId,
483708
- multiLevelType: entry.multiLevelType,
483709
- rawXml: entry.rawXml,
483710
- levels: Array.from(entry.levels.values()).map((level) => ({
483711
- ilvl: level.ilvl,
483712
- numFmt: level.numFmt,
483713
- lvlText: level.lvlText,
483714
- start: level.start,
483715
- opaqueXml: null
483716
- }))
483717
- })),
483718
- nums: Array.from(numbering.instances.values()).map((entry) => ({
483719
- numId: entry.numId,
483720
- abstractNumId: entry.abstractNumId,
483721
- rawXml: entry.rawXml,
483722
- lvlOverrides: Array.from(entry.lvlOverrides.entries()).map(([ilvl, value2]) => ({
483723
- ilvl,
483724
- startOverride: value2.startOverride
483994
+ return rootDoc.transact(() => {
483995
+ const unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.numbering });
483996
+ let bootstrap;
483997
+ if (part.bytes) {
483998
+ bootstrap = bootstrapPartProjection(TEXT_DECODER12.decode(part.bytes));
483999
+ }
484000
+ if (part.bytes && numbering.present) {
484001
+ try {
484002
+ seedNumberingShard({
484003
+ doc: unit,
484004
+ shardId,
484005
+ ownerPartKey: part.partKey,
484006
+ ownerPartUri: part.partUri,
484007
+ abstractNums: Array.from(numbering.abstracts.values()).map((entry) => ({
484008
+ abstractNumId: entry.abstractNumId,
484009
+ multiLevelType: entry.multiLevelType,
484010
+ rawXml: entry.rawXml,
484011
+ levels: Array.from(entry.levels.values()).map((level) => ({
484012
+ ilvl: level.ilvl,
484013
+ numFmt: level.numFmt,
484014
+ lvlText: level.lvlText,
484015
+ start: level.start,
484016
+ opaqueXml: null
484017
+ }))
484018
+ })),
484019
+ nums: Array.from(numbering.instances.values()).map((entry) => ({
484020
+ numId: entry.numId,
484021
+ abstractNumId: entry.abstractNumId,
484022
+ rawXml: entry.rawXml,
484023
+ lvlOverrides: Array.from(entry.lvlOverrides.entries()).map(([ilvl, value2]) => ({
484024
+ ilvl,
484025
+ startOverride: value2.startOverride
484026
+ }))
483725
484027
  }))
483726
- }))
483727
- });
483728
- } catch (error50) {
483729
- if (!isAlreadySeededError(error50))
483730
- throw error50;
484028
+ });
484029
+ } catch (error50) {
484030
+ if (!isAlreadySeededError(error50))
484031
+ throw error50;
484032
+ }
483731
484033
  }
483732
- }
483733
- return subtreePackageHandle(rootDoc, unit, SHARD_KIND.numbering, bootstrap);
484034
+ return subtreePackageHandle(rootDoc, unit, SHARD_KIND.numbering, bootstrap);
484035
+ }, "collab-v2:bootstrap:seed-unit");
483734
484036
  }
483735
484037
  async function connectSettingsShard(rootDoc, shardId, settingsPart, webSettingsPart) {
483736
- const unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.settings });
483737
- const settingsBootstrap = settingsPart?.bytes ? bootstrapPartProjection(TEXT_DECODER12.decode(settingsPart.bytes)) : null;
483738
- const webSettingsBootstrap = webSettingsPart?.bytes ? bootstrapPartProjection(TEXT_DECODER12.decode(webSettingsPart.bytes)) : null;
483739
- if (settingsPart?.bytes || webSettingsPart?.bytes) {
483740
- try {
483741
- seedSettingsShard({
483742
- doc: unit,
483743
- shardId,
483744
- ownerPartKey: settingsPart?.partKey ?? encodeLogicalOwnerKey(SETTINGS_PART_URI7),
483745
- ownerPartUri: settingsPart?.partUri ?? SETTINGS_PART_URI7,
483746
- pairedPartKeys: [settingsPart?.partKey, webSettingsPart?.partKey].filter(Boolean),
483747
- settings: {
483748
- fields: {},
483749
- webFields: {},
483750
- opaqueXml: settingsPart?.bytes ? settingsInnerXmlWithNamespaceScope(TEXT_DECODER12.decode(settingsPart.bytes)) : null,
483751
- webOpaqueXml: webSettingsPart?.bytes ? settingsInnerXmlWithNamespaceScope(TEXT_DECODER12.decode(webSettingsPart.bytes)) : null
483752
- }
483753
- });
483754
- } catch (error50) {
483755
- if (!isAlreadySeededError(error50))
483756
- throw error50;
484038
+ return rootDoc.transact(() => {
484039
+ const unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.settings });
484040
+ const settingsBootstrap = settingsPart?.bytes ? bootstrapPartProjection(TEXT_DECODER12.decode(settingsPart.bytes)) : null;
484041
+ const webSettingsBootstrap = webSettingsPart?.bytes ? bootstrapPartProjection(TEXT_DECODER12.decode(webSettingsPart.bytes)) : null;
484042
+ if (settingsPart?.bytes || webSettingsPart?.bytes) {
484043
+ try {
484044
+ seedSettingsShard({
484045
+ doc: unit,
484046
+ shardId,
484047
+ ownerPartKey: settingsPart?.partKey ?? encodeLogicalOwnerKey(SETTINGS_PART_URI7),
484048
+ ownerPartUri: settingsPart?.partUri ?? SETTINGS_PART_URI7,
484049
+ pairedPartKeys: [settingsPart?.partKey, webSettingsPart?.partKey].filter(Boolean),
484050
+ settings: {
484051
+ fields: {},
484052
+ webFields: {},
484053
+ opaqueXml: settingsPart?.bytes ? settingsInnerXmlWithNamespaceScope(TEXT_DECODER12.decode(settingsPart.bytes)) : null,
484054
+ webOpaqueXml: webSettingsPart?.bytes ? settingsInnerXmlWithNamespaceScope(TEXT_DECODER12.decode(webSettingsPart.bytes)) : null
484055
+ }
484056
+ });
484057
+ } catch (error50) {
484058
+ if (!isAlreadySeededError(error50))
484059
+ throw error50;
484060
+ }
483757
484061
  }
483758
- }
483759
- return subtreePackageHandle(rootDoc, unit, SHARD_KIND.settings, {
483760
- sourceXml: settingsBootstrap?.sourceXml ?? null,
483761
- rootOpenTagXml: settingsBootstrap?.rootOpenTagXml ?? null,
483762
- rootCloseTagXml: settingsBootstrap?.rootCloseTagXml ?? null,
483763
- containerOpenTagXml: webSettingsBootstrap?.rootOpenTagXml ?? null,
483764
- containerCloseTagXml: webSettingsBootstrap?.rootCloseTagXml ?? null
483765
- });
484062
+ return subtreePackageHandle(rootDoc, unit, SHARD_KIND.settings, {
484063
+ sourceXml: settingsBootstrap?.sourceXml ?? null,
484064
+ rootOpenTagXml: settingsBootstrap?.rootOpenTagXml ?? null,
484065
+ rootCloseTagXml: settingsBootstrap?.rootCloseTagXml ?? null,
484066
+ containerOpenTagXml: webSettingsBootstrap?.rootOpenTagXml ?? null,
484067
+ containerCloseTagXml: webSettingsBootstrap?.rootCloseTagXml ?? null
484068
+ });
484069
+ }, "collab-v2:bootstrap:seed-unit");
483766
484070
  }
483767
484071
  async function connectMediaManifestShard(rootDoc, shardId, parts, session, binaryMode) {
483768
- const unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.mediaManifest });
483769
- const assets = buildMediaAssets(parts, session, binaryMode);
483770
- if (assets.length > 0) {
483771
- try {
483772
- seedMediaManifestShard({
483773
- doc: unit,
483774
- shardId,
483775
- ownerPartKey: encodeLogicalOwnerKey(MEDIA_OWNER_URI2),
483776
- ownerPartUri: MEDIA_OWNER_URI2,
483777
- assets
483778
- });
483779
- } catch (error50) {
483780
- if (!isAlreadySeededError(error50))
483781
- throw error50;
484072
+ return rootDoc.transact(() => {
484073
+ const unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.mediaManifest });
484074
+ const assets = buildMediaAssets(parts, session, binaryMode);
484075
+ if (assets.length > 0) {
484076
+ try {
484077
+ seedMediaManifestShard({
484078
+ doc: unit,
484079
+ shardId,
484080
+ ownerPartKey: encodeLogicalOwnerKey(MEDIA_OWNER_URI2),
484081
+ ownerPartUri: MEDIA_OWNER_URI2,
484082
+ assets
484083
+ });
484084
+ } catch (error50) {
484085
+ if (!isAlreadySeededError(error50))
484086
+ throw error50;
484087
+ }
483782
484088
  }
483783
- }
483784
- return subtreePackageHandle(rootDoc, unit, SHARD_KIND.mediaManifest);
484089
+ return subtreePackageHandle(rootDoc, unit, SHARD_KIND.mediaManifest);
484090
+ }, "collab-v2:bootstrap:seed-unit");
483785
484091
  }
483786
484092
  async function connectBinaryShard(rootDoc, shardId, part, assetId, binaryMode, blobStore) {
483787
- const unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.binary });
483788
- if (part.bytes) {
483789
- try {
483790
- const payload = prepareSingleDocBinaryPayload(part.bytes, binaryMode, blobStore);
483791
- seedBinaryShard({
483792
- doc: unit,
483793
- shardId,
483794
- ownerPartKey: part.partKey,
483795
- ownerPartUri: part.partUri,
483796
- assetId,
483797
- mode: payload.mode,
483798
- contentType: part.contentType ?? "",
483799
- ...payload.mode === BINARY_MODE.blob ? {
483800
- blobRef: payload.blobRef,
483801
- sha256: payload.sha256,
483802
- byteLength: payload.byteLength
483803
- } : {
483804
- bytes: payload.bytes,
483805
- sha256: payload.sha256
483806
- }
483807
- });
483808
- } catch (error50) {
483809
- if (!isAlreadySeededError(error50))
483810
- throw error50;
484093
+ return rootDoc.transact(() => {
484094
+ const unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.binary });
484095
+ if (part.bytes) {
484096
+ try {
484097
+ const payload = prepareSingleDocBinaryPayload(part.bytes, binaryMode, blobStore);
484098
+ seedBinaryShard({
484099
+ doc: unit,
484100
+ shardId,
484101
+ ownerPartKey: part.partKey,
484102
+ ownerPartUri: part.partUri,
484103
+ assetId,
484104
+ mode: payload.mode,
484105
+ contentType: part.contentType ?? "",
484106
+ ...payload.mode === BINARY_MODE.blob ? {
484107
+ blobRef: payload.blobRef,
484108
+ sha256: payload.sha256,
484109
+ byteLength: payload.byteLength
484110
+ } : {
484111
+ bytes: payload.bytes,
484112
+ sha256: payload.sha256
484113
+ }
484114
+ });
484115
+ } catch (error50) {
484116
+ if (!isAlreadySeededError(error50))
484117
+ throw error50;
484118
+ }
483811
484119
  }
483812
- }
483813
- return subtreePackageHandle(rootDoc, unit, SHARD_KIND.binary);
484120
+ return subtreePackageHandle(rootDoc, unit, SHARD_KIND.binary);
484121
+ }, "collab-v2:bootstrap:seed-unit");
483814
484122
  }
483815
484123
  async function connectOpaqueShard(rootDoc, shardId, part, sourceZipSha256, binaryMode, blobStore) {
483816
- const unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.opaquePart });
483817
- if (part.bytes) {
483818
- try {
483819
- const payload = prepareSingleDocBinaryPayload(part.bytes, binaryMode, blobStore);
483820
- seedOpaquePartShard({
483821
- doc: unit,
483822
- shardId,
483823
- ownerPartKey: part.partKey,
483824
- ownerPartUri: part.partUri,
483825
- partKey: part.partKey,
483826
- partUri: part.partUri,
483827
- contentType: part.contentType,
483828
- mode: payload.mode,
483829
- ...payload.mode === BINARY_MODE.blob ? {
483830
- blobRef: payload.blobRef,
483831
- sha256: payload.sha256,
483832
- byteLength: payload.byteLength
483833
- } : {
483834
- bytes: payload.bytes,
483835
- sha256: payload.sha256
483836
- },
483837
- sourceProvenance: {
483838
- sha256Zip: sourceZipSha256,
483839
- committedAt: null,
483840
- committerId: null
483841
- }
483842
- });
483843
- } catch (error50) {
483844
- if (!isAlreadySeededError(error50))
483845
- throw error50;
484124
+ return rootDoc.transact(() => {
484125
+ const unit = createCollabUnitHandle({ rootDoc, shardId, shardKind: SHARD_KIND.opaquePart });
484126
+ if (part.bytes) {
484127
+ try {
484128
+ const payload = prepareSingleDocBinaryPayload(part.bytes, binaryMode, blobStore);
484129
+ seedOpaquePartShard({
484130
+ doc: unit,
484131
+ shardId,
484132
+ ownerPartKey: part.partKey,
484133
+ ownerPartUri: part.partUri,
484134
+ partKey: part.partKey,
484135
+ partUri: part.partUri,
484136
+ contentType: part.contentType,
484137
+ mode: payload.mode,
484138
+ ...payload.mode === BINARY_MODE.blob ? {
484139
+ blobRef: payload.blobRef,
484140
+ sha256: payload.sha256,
484141
+ byteLength: payload.byteLength
484142
+ } : {
484143
+ bytes: payload.bytes,
484144
+ sha256: payload.sha256
484145
+ },
484146
+ sourceProvenance: {
484147
+ sha256Zip: sourceZipSha256,
484148
+ committedAt: null,
484149
+ committerId: null
484150
+ }
484151
+ });
484152
+ } catch (error50) {
484153
+ if (!isAlreadySeededError(error50))
484154
+ throw error50;
484155
+ }
483846
484156
  }
483847
- }
483848
- return subtreePackageHandle(rootDoc, unit, SHARD_KIND.opaquePart);
484157
+ return subtreePackageHandle(rootDoc, unit, SHARD_KIND.opaquePart);
484158
+ }, "collab-v2:bootstrap:seed-unit");
483849
484159
  }
483850
484160
  function storyProjectionHandle(handle) {
483851
484161
  return {
@@ -485263,8 +485573,12 @@ function jsonSchemaPropertyToZod(prop, definitions = {}, resolvingReferences = n
485263
485573
  return describe3(intersection3);
485264
485574
  }
485265
485575
  switch (type) {
485266
- case "string":
485267
- return describe3(exports_external.string());
485576
+ case "string": {
485577
+ let schema = exports_external.string();
485578
+ if (typeof prop.minLength === "number")
485579
+ schema = schema.min(prop.minLength);
485580
+ return describe3(schema);
485581
+ }
485268
485582
  case "number": {
485269
485583
  let schema = exports_external.number();
485270
485584
  if (typeof prop.exclusiveMinimum === "number")