@attentionform/astra-mcp-server 0.3.0 → 0.4.0

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/dist/index.js CHANGED
@@ -5566,6 +5566,23 @@ function writeCorePropertiesPart(adapter, xml) {
5566
5566
  } catch {
5567
5567
  }
5568
5568
  }
5569
+ function registerExistingCorePropertiesPart(adapter) {
5570
+ let present = false;
5571
+ try {
5572
+ present = adapter.readTextOrNull(CORE_PROPERTIES_PART) !== null;
5573
+ } catch {
5574
+ return;
5575
+ }
5576
+ if (!present) return;
5577
+ try {
5578
+ ensureContentTypeOverride(adapter);
5579
+ } catch {
5580
+ }
5581
+ try {
5582
+ ensurePackageRel(adapter);
5583
+ } catch {
5584
+ }
5585
+ }
5569
5586
  function patchToMetadata(patch) {
5570
5587
  const out = {};
5571
5588
  if (patch.title !== void 0) out.title = patch.title;
@@ -9811,6 +9828,41 @@ function hasLiveNumberingForSync(ast) {
9811
9828
  }
9812
9829
  }
9813
9830
 
9831
+ // ../engine/src/ooxml/quote-style.ts
9832
+ var QUOTE_STYLE_ID = "Quote";
9833
+ var QUOTE_INDENT_TWIPS = 720;
9834
+ var QUOTE_STYLE_BODY = `<w:style w:type="paragraph" w:styleId="Quote">
9835
+ <w:name w:val="Quote"/>
9836
+ <w:basedOn w:val="Normal"/>
9837
+ <w:next w:val="Normal"/>
9838
+ <w:uiPriority w:val="29"/>
9839
+ <w:qFormat/>
9840
+ <w:pPr>
9841
+ <w:pBdr>
9842
+ <w:left w:val="single" w:sz="18" w:space="10" w:color="94A3B8"/>
9843
+ </w:pBdr>
9844
+ <w:spacing w:before="120" w:after="120"/>
9845
+ <w:ind w:left="${QUOTE_INDENT_TWIPS}"/>
9846
+ </w:pPr>
9847
+ <w:rPr>
9848
+ <w:i/>
9849
+ <w:color w:val="475569"/>
9850
+ </w:rPr>
9851
+ </w:style>`;
9852
+ var QUOTE_STYLE_XML = QUOTE_STYLE_BODY.replace("<w:style ", '<w:style xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ');
9853
+ function ensureQuoteStyle(stylesDom) {
9854
+ if (!stylesDom) return false;
9855
+ const root = stylesDom.getElementsByTagName("w:styles")[0];
9856
+ if (!root) return false;
9857
+ for (const style of Array.from(stylesDom.getElementsByTagName("w:style"))) {
9858
+ if (style.getAttribute("w:styleId") === QUOTE_STYLE_ID) return false;
9859
+ }
9860
+ const fragment = new DOMParser().parseFromString(QUOTE_STYLE_XML, "text/xml").documentElement;
9861
+ if (!fragment) return false;
9862
+ root.appendChild(stylesDom.importNode(fragment, true));
9863
+ return true;
9864
+ }
9865
+
9814
9866
  // ../engine/src/parser.ts
9815
9867
  function hasAncestorTag(el, tag) {
9816
9868
  for (let n = el.parentNode; n; n = n.parentNode) {
@@ -10475,6 +10527,7 @@ var DocxParser = class _DocxParser {
10475
10527
  } else {
10476
10528
  this.stylesDom = null;
10477
10529
  }
10530
+ registerExistingCorePropertiesPart(this.getZipAdapter());
10478
10531
  this.invalidateMediaCache();
10479
10532
  this.assignStableIds();
10480
10533
  this._bumpNodePagesRevision();
@@ -11040,6 +11093,27 @@ var DocxParser = class _DocxParser {
11040
11093
  return tracked;
11041
11094
  }
11042
11095
  // --- AST layer: stable id assignment ---
11096
+ /** Orders AST blocks by where their element sits in the body; blocks inside one body child keep their order. */
11097
+ sortBlocksInDocumentOrder(nodes) {
11098
+ const body = this.documentDom.getElementsByTagName("w:body")[0];
11099
+ if (!body || nodes.length < 2) return;
11100
+ const topIndex = /* @__PURE__ */ new Map();
11101
+ let position2 = 0;
11102
+ for (let child = body.firstChild; child; child = child.nextSibling) {
11103
+ if (child.nodeType === 1) topIndex.set(child, position2++);
11104
+ }
11105
+ const keyOf = (element) => {
11106
+ let current = element;
11107
+ while (current && current.parentNode !== body) current = current.parentNode;
11108
+ return current ? topIndex.get(current) ?? Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER;
11109
+ };
11110
+ const keyed = nodes.map((node, index) => ({ node, index, key: keyOf(node.xml) }));
11111
+ if (keyed.every((entry, i) => i === 0 || keyed[i - 1].key <= entry.key)) return;
11112
+ keyed.sort((a, b) => a.key - b.key || a.index - b.index);
11113
+ keyed.forEach((entry, i) => {
11114
+ nodes[i] = entry.node;
11115
+ });
11116
+ }
11043
11117
  async getAST() {
11044
11118
  const ast = {
11045
11119
  metadata: {},
@@ -11417,6 +11491,7 @@ var DocxParser = class _DocxParser {
11417
11491
  ast.sectPrFlags = { titlePg: false, evenAndOddHeaders: false };
11418
11492
  }
11419
11493
  ast.nextChangeId = firstFreeRevisionId(this.maxPackageRevisionId());
11494
+ this.sortBlocksInDocumentOrder(rootNodes);
11420
11495
  ast.sections.push(...splitIntoSections(rootNodes, fileSections));
11421
11496
  const prevNumberingAst = this.lastAstForNumbering;
11422
11497
  if (prevNumberingAst && prevNumberingAst !== ast) {
@@ -13191,6 +13266,7 @@ ${validation.errors.join("\n")}`, "repair the part XML or re-save the document i
13191
13266
  <w:spacing w:before="120" w:after="120" w:line="240" w:lineRule="auto"/>
13192
13267
  </w:pPr>
13193
13268
  </w:style>
13269
+ ${QUOTE_STYLE_BODY}
13194
13270
  <w:style w:type="character" w:styleId="Code">
13195
13271
  <w:name w:val="Code"/>
13196
13272
  <w:rPr>
@@ -13573,15 +13649,26 @@ var import_isomorphic_git = __toESM(require("isomorphic-git"));
13573
13649
  var import_fs3 = __toESM(require("fs"));
13574
13650
  var import_promises = __toESM(require("fs/promises"));
13575
13651
  var import_path2 = __toESM(require("path"));
13652
+ function hasUriScheme(ref) {
13653
+ if (/^[a-zA-Z]:[\\/]/.test(ref)) return false;
13654
+ return /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(ref);
13655
+ }
13656
+ function withoutTrailingSeparator(p, normalized) {
13657
+ const root = p.parse(normalized).root;
13658
+ let out = normalized;
13659
+ while (out.length > root.length && out.endsWith(p.sep)) out = out.slice(0, -1);
13660
+ return out;
13661
+ }
13576
13662
  function isPathInsideDir(candidate, dir, platform = process.platform) {
13577
- const normC = import_path2.default.normalize(candidate);
13578
- const normD = import_path2.default.normalize(dir);
13663
+ const p = platform === "win32" ? import_path2.default.win32 : import_path2.default.posix;
13664
+ let c = withoutTrailingSeparator(p, p.normalize(candidate));
13665
+ let d = withoutTrailingSeparator(p, p.normalize(dir));
13579
13666
  if (platform === "win32") {
13580
- const lc = normC.toLowerCase();
13581
- const ld = normD.toLowerCase();
13582
- return lc === ld || lc.startsWith(ld + import_path2.default.sep.toLowerCase());
13667
+ c = c.toLowerCase();
13668
+ d = d.toLowerCase();
13583
13669
  }
13584
- return normC === normD || normC.startsWith(normD + import_path2.default.sep);
13670
+ if (c === d) return true;
13671
+ return c.startsWith(d.endsWith(p.sep) ? d : d + p.sep);
13585
13672
  }
13586
13673
  function canonicalizeForAsset(resolved) {
13587
13674
  try {
@@ -14860,6 +14947,19 @@ function warnIfLowContrast(warnings, seen, fill, textColor, where) {
14860
14947
  warnings.push(msg);
14861
14948
  }
14862
14949
  }
14950
+ function autoTextForFill(fill, current) {
14951
+ if (!/^[0-9A-Fa-f]{6}$/.test(fill)) return void 0;
14952
+ if (contrastRatio((current ?? "000000").toUpperCase(), fill.toUpperCase()) >= TABLE_CONTRAST_MIN_RATIO) return void 0;
14953
+ return contrastRatio("FFFFFF", fill.toUpperCase()) > contrastRatio("000000", fill.toUpperCase()) ? "FFFFFF" : "000000";
14954
+ }
14955
+ function noteAutoText(warnings, seen, fill, text2, where) {
14956
+ const key = `auto:${fill}:${text2}`;
14957
+ if (seen.has(key)) return;
14958
+ seen.add(key);
14959
+ warnings.push(
14960
+ `Table note: text set to #${text2} at ${where} because black text is not readable on fill #${fill} (contrast ${contrastRatio(text2, fill).toFixed(2)}:1). Pass textColor to choose another colour.`
14961
+ );
14962
+ }
14863
14963
  function paintCellTextColor(tc, doc, normColor) {
14864
14964
  let ps = directChildren5(tc, "w:p");
14865
14965
  if (ps.length === 0) {
@@ -15486,6 +15586,13 @@ async function executeTable(opIn, ast, guide) {
15486
15586
  let effectiveText;
15487
15587
  if (rIdx === 0) {
15488
15588
  effectiveText = explicitTextColor ?? guideTextForFill(rowFill);
15589
+ if (effectiveText === void 0) {
15590
+ const auto = autoTextForFill(rowFill, firstExplicitCellTextColor(tc));
15591
+ if (auto) {
15592
+ effectiveText = auto;
15593
+ noteAutoText(warnings, warnedContrastPairs, rowFill, auto, `header cell (${rIdx + 1},${cIdx + 1}) of table '${tableNode.id}'`);
15594
+ }
15595
+ }
15489
15596
  if (effectiveText && tblEl.ownerDocument) {
15490
15597
  paintCellTextColor(tc, tblEl.ownerDocument, effectiveText);
15491
15598
  }
@@ -16012,7 +16119,14 @@ async function executeTable(opIn, ast, guide) {
16012
16119
  shd.setAttribute("w:color", "auto");
16013
16120
  shd.setAttribute("w:fill", normFill);
16014
16121
  sortChildrenBySchema(tcPr, TCPR_CHILD_ORDER);
16015
- const effectiveText = explicitTextColor ?? guideTextForFill(normFill);
16122
+ let effectiveText = explicitTextColor ?? guideTextForFill(normFill);
16123
+ if (effectiveText === void 0) {
16124
+ const auto = autoTextForFill(normFill, firstExplicitCellTextColor(tc));
16125
+ if (auto) {
16126
+ effectiveText = auto;
16127
+ noteAutoText(warnings, warnedContrastPairs, normFill, auto, `cell (${shown(rowIndex)},${shown(colIndex)}) of table '${tableNode.id}'`);
16128
+ }
16129
+ }
16016
16130
  if (effectiveText && tblEl.ownerDocument) {
16017
16131
  paintCellTextColor(tc, tblEl.ownerDocument, effectiveText);
16018
16132
  }
@@ -16296,7 +16410,7 @@ async function executeTable(opIn, ast, guide) {
16296
16410
  break;
16297
16411
  }
16298
16412
  case "setCellTextColor": {
16299
- const colorInput = raw.color ?? op.color;
16413
+ const colorInput = raw.color ?? op.color ?? raw.textColor ?? op.textColor;
16300
16414
  if (colorInput === void 0 || colorInput === null || String(colorInput).trim() === "") {
16301
16415
  throw new Error(
16302
16416
  `E_VALIDATION: setCellTextColor requires color (got no color). Remedy: pass color as 6 hex digits without '#', e.g. color:'FFFFFF' for white header text.`
@@ -16752,6 +16866,13 @@ function sanitizeXml(xmlStr) {
16752
16866
  // ../engine/src/operation-schema.ts
16753
16867
  var import_zod2 = require("zod");
16754
16868
 
16869
+ // ../engine/src/anchor-alias.ts
16870
+ function anchorAliasOf(value) {
16871
+ if (value === "start" || value === "@start") return "start";
16872
+ if (value === "end" || value === "@end") return "end";
16873
+ return null;
16874
+ }
16875
+
16755
16876
  // ../engine/src/operation-params.ts
16756
16877
  var STRUCTURAL = /* @__PURE__ */ new Set(["action", "target", "tableId", "operation", "op", "params", "trackChanges", "expectedVersion"]);
16757
16878
  var ROW = ["rowIndex", "row"];
@@ -16777,9 +16898,17 @@ var TABLE_SUBOP_KEYS = {
16777
16898
  setCellAlignment: ["align", "alignment", ...ROW, ...COL],
16778
16899
  setTableAlignment: ["align", "alignment"],
16779
16900
  setCellPadding: ["padding", ...ROW, ...COL],
16780
- setCellTextColor: ["color", ...ROW, ...COL],
16901
+ setCellTextColor: ["color", "textColor", ...ROW, ...COL],
16781
16902
  setKeepTogether: []
16782
16903
  };
16904
+ function describeTableSubOpKeys() {
16905
+ const aliases = /* @__PURE__ */ new Set(["rowIndex", "colIndex", "columnIndex"]);
16906
+ const parts = Object.entries(TABLE_SUBOP_KEYS).map(([sub, keys]) => {
16907
+ const shown2 = keys.filter((k) => !aliases.has(k));
16908
+ return shown2.length > 0 ? `${sub}(${shown2.join(",")})` : sub;
16909
+ });
16910
+ return `Parameters each operation reads (row and col count from 1): ${parts.join("; ")}.`;
16911
+ }
16783
16912
  function isRecord(value) {
16784
16913
  return value !== null && typeof value === "object" && !Array.isArray(value);
16785
16914
  }
@@ -20733,6 +20862,8 @@ var expectedVersion = import_zod2.z.number().int().nonnegative().optional();
20733
20862
  var nestedParams = import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).optional();
20734
20863
  var sectionNumber = import_zod2.z.number().int().positive("sections count from 1 (1 is the first section)").describe("Section number, counting from 1 (1 is the first section).");
20735
20864
  var position = import_zod2.z.enum(["before", "after"]);
20865
+ var ANCHOR_REF_DESCRIPTION = "A node id from inspect_document, or 'start' / 'end' for the first / last block of the document (no inspect needed; '@start' and '@end' work too). With 'start', position defaults to 'before'; with 'end', to 'after'.";
20866
+ var ANCHOR_REF_SHORT = "A node id, or 'start' / 'end' for the first / last block.";
20736
20867
  var operationInsertPosition = import_zod2.z.enum(["before", "after"]).optional();
20737
20868
  var FONT_SIZE_PT_REGEX = /^\s*(\d+(?:\.\d+)?)\s*pt\s*$/i;
20738
20869
  function parseFontSize(sz) {
@@ -20835,7 +20966,7 @@ var insertOp = import_zod2.z.object({
20835
20966
  // engine appends in call order without ever reading them) need no dummy
20836
20967
  // anchor. Body inserts still require both — enforced by the strict
20837
20968
  // validators below, not by this shape.
20838
- anchor: import_zod2.z.string().min(1).max(256).optional(),
20969
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_DESCRIPTION).optional(),
20839
20970
  position: operationInsertPosition,
20840
20971
  text: import_zod2.z.string().max(5e5).optional(),
20841
20972
  xml: import_zod2.z.string().max(5e5).optional(),
@@ -20957,7 +21088,7 @@ var clearFormatOp = import_zod2.z.object({
20957
21088
  var moveOp = import_zod2.z.object({
20958
21089
  action: import_zod2.z.literal("move"),
20959
21090
  target: nodeIdOrList,
20960
- anchor: import_zod2.z.string().min(1).max(256),
21091
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_SHORT),
20961
21092
  position,
20962
21093
  expectedVersion,
20963
21094
  params: nestedParams
@@ -20994,8 +21125,8 @@ var tableOp = import_zod2.z.object({
20994
21125
  action: import_zod2.z.literal("table"),
20995
21126
  target: import_zod2.z.string().min(1).max(256).optional(),
20996
21127
  tableId: import_zod2.z.string().min(1).max(256).optional(),
20997
- operation: tableSubOp.optional(),
20998
- op: tableSubOp.optional(),
21128
+ operation: tableSubOp.describe(describeTableSubOpKeys()).optional(),
21129
+ op: tableSubOp.describe("Alias of operation.").optional(),
20999
21130
  trackChanges: import_zod2.z.boolean().optional(),
21000
21131
  author: revisionAuthor,
21001
21132
  date: revisionDate,
@@ -21025,7 +21156,6 @@ var tableOp = import_zod2.z.object({
21025
21156
  border: import_zod2.z.string().max(32).optional(),
21026
21157
  color: import_zod2.z.string().max(16).optional(),
21027
21158
  sz: fontSizeOrSzSchema.optional(),
21028
- fontSize: fontSizeOrSzSchema.optional(),
21029
21159
  vAlign: import_zod2.z.string().max(16).optional(),
21030
21160
  align: tableAlignmentSchema.optional(),
21031
21161
  alignment: tableAlignmentSchema.optional(),
@@ -21087,7 +21217,7 @@ var namedStyleOp = import_zod2.z.object({
21087
21217
  var fieldOp = import_zod2.z.object({
21088
21218
  action: import_zod2.z.literal("field"),
21089
21219
  operation: import_zod2.z.enum(["insertTOC", "insertPageNumber", "insertWordCount", "updateFields"]),
21090
- anchor: import_zod2.z.string().min(1).max(256).optional(),
21220
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_SHORT).optional(),
21091
21221
  position: operationInsertPosition,
21092
21222
  title: import_zod2.z.string().max(1e3).optional(),
21093
21223
  levels: import_zod2.z.number().int().min(1).max(9).optional(),
@@ -21103,7 +21233,7 @@ var equationOp = import_zod2.z.object({
21103
21233
  action: import_zod2.z.literal("equation"),
21104
21234
  operation: import_zod2.z.enum(["insert", "replace"]),
21105
21235
  latex: import_zod2.z.string().min(1).max(5e4),
21106
- anchor: import_zod2.z.string().min(1).max(256).optional(),
21236
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_SHORT).optional(),
21107
21237
  position: operationInsertPosition,
21108
21238
  target: import_zod2.z.string().min(1).max(256).optional(),
21109
21239
  id: import_zod2.z.string().min(1).max(256).optional(),
@@ -21217,7 +21347,7 @@ var chartOp = import_zod2.z.object({
21217
21347
  categories: import_zod2.z.array(import_zod2.z.string().max(500)).max(1e3).optional(),
21218
21348
  series: import_zod2.z.array(chartSeries).max(100).optional(),
21219
21349
  style: chartStyle.optional(),
21220
- anchor: import_zod2.z.string().min(1).max(256).optional(),
21350
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_SHORT).optional(),
21221
21351
  id: import_zod2.z.string().min(1).max(256).optional(),
21222
21352
  widthEmu: import_zod2.z.number().positive().max(1e8).optional(),
21223
21353
  heightEmu: import_zod2.z.number().positive().max(1e8).optional(),
@@ -21227,7 +21357,7 @@ var chartOp = import_zod2.z.object({
21227
21357
  var imageOp = import_zod2.z.object({
21228
21358
  action: import_zod2.z.literal("image"),
21229
21359
  operation: import_zod2.z.enum(["insert", "resize", "reposition", "setWrap", "setCaption", "delete", "setAltText"]),
21230
- anchor: import_zod2.z.string().min(1).max(256).optional(),
21360
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_SHORT).optional(),
21231
21361
  target: import_zod2.z.string().min(1).max(256).optional(),
21232
21362
  fileData: import_zod2.z.string().max(2e7).optional(),
21233
21363
  filePath: import_zod2.z.string().min(1).max(1024).optional(),
@@ -21241,7 +21371,8 @@ var imageOp = import_zod2.z.object({
21241
21371
  var calloutOp = import_zod2.z.object({
21242
21372
  action: import_zod2.z.literal("callout"),
21243
21373
  text: import_zod2.z.string().min(1).max(1e5),
21244
- anchor: import_zod2.z.string().min(1).max(256).optional(),
21374
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_SHORT).optional(),
21375
+ id: import_zod2.z.string().min(1).max(256).optional(),
21245
21376
  title: import_zod2.z.string().max(1e3).optional(),
21246
21377
  theme: import_zod2.z.string().max(32).optional(),
21247
21378
  position: operationInsertPosition,
@@ -21251,7 +21382,7 @@ var calloutOp = import_zod2.z.object({
21251
21382
  var footnoteOp = import_zod2.z.object({
21252
21383
  action: import_zod2.z.literal("footnote"),
21253
21384
  operation: import_zod2.z.enum(["insert"]).optional(),
21254
- anchor: import_zod2.z.string().min(1).max(256),
21385
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_SHORT),
21255
21386
  text: import_zod2.z.string().min(1).max(5e4),
21256
21387
  id: import_zod2.z.string().min(1).max(256).optional(),
21257
21388
  offset: import_zod2.z.number().int().nonnegative().optional(),
@@ -21260,7 +21391,7 @@ var footnoteOp = import_zod2.z.object({
21260
21391
  var endnoteOp = import_zod2.z.object({
21261
21392
  action: import_zod2.z.literal("endnote"),
21262
21393
  operation: import_zod2.z.enum(["insert"]).optional(),
21263
- anchor: import_zod2.z.string().min(1).max(256),
21394
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_SHORT),
21264
21395
  text: import_zod2.z.string().min(1).max(5e4),
21265
21396
  id: import_zod2.z.string().min(1).max(256).optional(),
21266
21397
  offset: import_zod2.z.number().int().nonnegative().optional(),
@@ -21439,11 +21570,11 @@ function insertBodyAnchorDetail(op) {
21439
21570
  if (effectiveTarget === "header" || effectiveTarget === "footer") return null;
21440
21571
  const missing = [];
21441
21572
  if (typeof rec["anchor"] !== "string" || rec["anchor"].length < 1) {
21442
- missing.push("anchor: anchor is required for body inserts [invalid_type]");
21573
+ missing.push("anchor: anchor is required for body inserts: a node id from inspect_document, or 'start' / 'end' for the first / last block [invalid_type]");
21443
21574
  }
21444
21575
  const pos = rec["position"];
21445
- if (pos !== "before" && pos !== "after") {
21446
- missing.push("position: position is required for body inserts [invalid_type]");
21576
+ if (pos !== "before" && pos !== "after" && anchorAliasOf(rec["anchor"]) === null) {
21577
+ missing.push("position: position is required for body inserts (or use anchor 'start' / 'end', which imply it) [invalid_type]");
21447
21578
  }
21448
21579
  return missing.length > 0 ? missing.join("; ") : null;
21449
21580
  }
@@ -28157,7 +28288,7 @@ function resolveImageFilePath(allowedDir, filePath) {
28157
28288
  "use a plain relative file path inside the allowed directory"
28158
28289
  );
28159
28290
  }
28160
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) {
28291
+ if (hasUriScheme(trimmed)) {
28161
28292
  throw toError(
28162
28293
  "E_SANDBOX",
28163
28294
  "image filePath points outside the allowed directory",
@@ -28328,7 +28459,7 @@ function resolveAssetPath(allowedDir, refPath, line2, what) {
28328
28459
  if (trimmed.includes("\0") || /%00/i.test(trimmed) || trimmed.includes("\n") || trimmed.includes("\r")) {
28329
28460
  fail(line2, `${what} reference path is not usable`, "use a plain relative file path inside the allowed directory");
28330
28461
  }
28331
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) {
28462
+ if (hasUriScheme(trimmed)) {
28332
28463
  fail(line2, `${what} reference points outside the allowed directory`, "keep referenced files inside the allowed directory and use a relative path");
28333
28464
  }
28334
28465
  const candidate = import_path5.default.isAbsolute(trimmed) ? import_path5.default.resolve(trimmed) : import_path5.default.resolve(allowedDir, trimmed);
@@ -29598,7 +29729,8 @@ async function executeCallout(op, ast, parser2, guide) {
29598
29729
  if (!targetSection) throw new Error("No section available for callout box insertion");
29599
29730
  const ownerDoc = targetSection.nodes[0]?.xml?.ownerDocument || ownerDocumentOf(ast.sections);
29600
29731
  if (!ownerDoc) throw new Error("Cannot resolve XML DOM document");
29601
- const calloutId = isCalloutReproducible() ? (() => {
29732
+ const clientId = pick("id", op.id);
29733
+ const calloutId = typeof clientId === "string" && clientId.length > 0 ? clientId : isCalloutReproducible() ? (() => {
29602
29734
  const base = `callout_${calloutHash8(`${resolvedAnchor ?? ""}:${resolvedPosition}:${themeKey}:${resolvedTitle ?? ""}:${resolvedText}`)}`;
29603
29735
  const live = /* @__PURE__ */ new Set();
29604
29736
  try {
@@ -30909,6 +31041,208 @@ function executePageBorder(op, ast, parser2) {
30909
31041
  section.version += 1;
30910
31042
  }
30911
31043
 
31044
+ // ../engine/src/compose/pristine-seed.ts
31045
+ function pristineTemplateSeedIds(parser2, requestedTemplate) {
31046
+ if (!parser2.hasPristineBuiltinSeed()) return null;
31047
+ let summary;
31048
+ try {
31049
+ summary = parser2.getStructuralSummary();
31050
+ } catch {
31051
+ return null;
31052
+ }
31053
+ const nodes = summary.nodes ?? [];
31054
+ if (![1, 2, 4, 5, 6].includes(nodes.length)) return null;
31055
+ if (summary.hasTrackedChanges === true) return null;
31056
+ if (Object.keys(summary.mediaFiles ?? {}).length > 0) return null;
31057
+ if ((summary.headers ?? []).length > 0) return null;
31058
+ if ((summary.footers ?? []).length > 0) return null;
31059
+ if ((summary.charts ?? []).length > 0) return null;
31060
+ try {
31061
+ const zip = parser2.getZipAdapter?.();
31062
+ if (typeof zip?.hasFile === "function") {
31063
+ for (const part of [
31064
+ "word/footnotes.xml",
31065
+ "word/endnotes.xml",
31066
+ "word/comments.xml",
31067
+ "word/commentsExtended.xml"
31068
+ ]) {
31069
+ try {
31070
+ if (zip.hasFile(part)) return null;
31071
+ } catch {
31072
+ return null;
31073
+ }
31074
+ }
31075
+ } else {
31076
+ return null;
31077
+ }
31078
+ } catch {
31079
+ return null;
31080
+ }
31081
+ for (const n of nodes) {
31082
+ if (n.hasTrackedChanges === true) return null;
31083
+ const raw = n.rawXml ?? "";
31084
+ if (raw.includes("w:footnoteReference") || raw.includes("w:endnoteReference") || raw.includes("w:commentReference") || raw.includes("w:commentRangeStart") || raw.includes("w:commentRangeEnd")) {
31085
+ return null;
31086
+ }
31087
+ }
31088
+ const ids = nodes.map((n) => n.id ?? "").filter((id) => id.length > 0);
31089
+ if (ids.length !== nodes.length) return null;
31090
+ if (nodes.length === 1) {
31091
+ const node = nodes[0];
31092
+ if (node.type !== "paragraph") return null;
31093
+ if ((node.text ?? "").trim() !== "") return null;
31094
+ return ids;
31095
+ }
31096
+ if (nodes.length === 2) {
31097
+ const [first, second] = nodes;
31098
+ const t0 = (first.text ?? "").trim();
31099
+ const t1 = (second.text ?? "").trim();
31100
+ if (t0 === BLANK_SEED_TITLE && t1 === BLANK_SEED_HINT) {
31101
+ const firstRaw = first.rawXml ?? "";
31102
+ const secondRaw = second.rawXml ?? "";
31103
+ if (!firstRaw.includes('w:val="Title"') || !secondRaw.includes('w:val="Subtitle"')) return null;
31104
+ return ids;
31105
+ }
31106
+ const tmpl = requestedTemplate?.toLowerCase();
31107
+ if (t0 === "MEMORANDUM" && tmpl === "memo") return ids;
31108
+ if (t0 === "Letter" && tmpl === "letter") return ids;
31109
+ if (t0 === "Proposal" && tmpl === "proposal") return ids;
31110
+ if (t0 === "INVOICE" && tmpl === "invoice") return ids;
31111
+ return null;
31112
+ }
31113
+ if (nodes.length === 4) {
31114
+ const t0 = (nodes[0].text ?? "").trim();
31115
+ const tmpl = requestedTemplate?.toLowerCase();
31116
+ if (t0 === "Research Benchmark Report" && tmpl === "research") return ids;
31117
+ if (t0 === "Corporate Strategy Roadmap" && tmpl === "corporate") return ids;
31118
+ if (t0 === "Velocity Audit & Performance" && tmpl === "bar-chart-3d") return ids;
31119
+ return null;
31120
+ }
31121
+ if (nodes.length === 5) {
31122
+ const t0 = (nodes[0].text ?? "").trim();
31123
+ const tmpl = requestedTemplate?.toLowerCase();
31124
+ if (t0 === "Financial Report & Audit" && tmpl === "financial") return ids;
31125
+ return null;
31126
+ }
31127
+ if (nodes.length === 6) {
31128
+ const t0 = (nodes[0].text ?? "").trim();
31129
+ const t2 = (nodes[2].text ?? "").trim();
31130
+ const tmpl = requestedTemplate?.toLowerCase();
31131
+ if (t0 === "Q3 Executive Financial Performance Report" && t2 === "1. Executive Summary") {
31132
+ if (tmpl === "report") return ids;
31133
+ return null;
31134
+ }
31135
+ if (t0 === "Manuscript Title" && t2 === "Chapter 1: The Beginning" && tmpl === "novel") {
31136
+ return ids;
31137
+ }
31138
+ if (t0 === "Neural Document Architectures: A Comparative Benchmark" && tmpl === "academic") {
31139
+ return ids;
31140
+ }
31141
+ if (t0 === "The ASTra Dispatch \u2014 Monthly Engineering Gazette" && tmpl === "newsletter") return ids;
31142
+ if (t0 === "ASTra Document Engine API Specification" && (tmpl === "api" || tmpl === "api-spec")) return ids;
31143
+ return null;
31144
+ }
31145
+ return null;
31146
+ }
31147
+ var SEED_TEMPLATE_NAMES = [
31148
+ "memo",
31149
+ "letter",
31150
+ "proposal",
31151
+ "invoice",
31152
+ "research",
31153
+ "corporate",
31154
+ "bar-chart-3d",
31155
+ "financial",
31156
+ "report",
31157
+ "novel",
31158
+ "academic",
31159
+ "newsletter",
31160
+ "api-spec"
31161
+ ];
31162
+ function pristineSeedIdsOfAnyTemplate(parser2) {
31163
+ const blank = pristineTemplateSeedIds(parser2);
31164
+ if (blank !== null) return blank;
31165
+ for (const name2 of SEED_TEMPLATE_NAMES) {
31166
+ const ids = pristineTemplateSeedIds(parser2, name2);
31167
+ if (ids !== null) return ids;
31168
+ }
31169
+ return null;
31170
+ }
31171
+
31172
+ // ../engine/src/document-title.ts
31173
+ var TEMPLATE_NAMES = [
31174
+ "blank",
31175
+ "memo",
31176
+ "report",
31177
+ "letter",
31178
+ "proposal",
31179
+ "invoice",
31180
+ "research",
31181
+ "corporate",
31182
+ "financial",
31183
+ "novel",
31184
+ "academic",
31185
+ "newsletter",
31186
+ "api-spec",
31187
+ "bar-chart-3d"
31188
+ ];
31189
+ var PLACEHOLDER_TITLES = new Set(TEMPLATE_NAMES.map((name2) => newDocumentTitle(name2).toLowerCase()));
31190
+ var DERIVED_TITLE_MAX = 200;
31191
+ var DEFAULT_CREATOR = "ASTra";
31192
+ function isPlaceholderTitle(title) {
31193
+ if (typeof title !== "string") return true;
31194
+ const clean = title.replace(/\s+/g, " ").trim().toLowerCase();
31195
+ return clean === "" || PLACEHOLDER_TITLES.has(clean);
31196
+ }
31197
+ function cleanTitle(text2) {
31198
+ const plain = typeof text2 === "string" ? Array.from(text2, (ch) => ch.charCodeAt(0) < 32 || ch.charCodeAt(0) === 127 ? " " : ch).join("").replace(/\s+/g, " ").trim() : "";
31199
+ return plain.length > DERIVED_TITLE_MAX ? plain.slice(0, DERIVED_TITLE_MAX).trimEnd() : plain;
31200
+ }
31201
+ function deriveTitleFromContent(ast) {
31202
+ const nodes = ast.sections.flatMap((section) => section.nodes);
31203
+ const styleOf = (node) => {
31204
+ const pPr = Array.from(node.xml.childNodes).find((c) => c.nodeType === 1 && c.tagName === "w:pPr");
31205
+ const pStyle = pPr ? Array.from(pPr.childNodes).find((c) => c.nodeType === 1 && c.tagName === "w:pStyle") : void 0;
31206
+ return pStyle?.getAttribute("w:val") ?? node.metadata?.namedStyle ?? void 0;
31207
+ };
31208
+ const textOf4 = (node) => {
31209
+ const runs = node.xml.getElementsByTagName("w:t");
31210
+ let text2 = "";
31211
+ for (let i = 0; i < runs.length; i++) text2 += runs[i].textContent ?? "";
31212
+ return text2 !== "" ? text2 : node.metadata?.text ?? "";
31213
+ };
31214
+ const pick = (match, from) => {
31215
+ for (const node of nodes) {
31216
+ if (node.type !== "paragraph" || !match(node)) continue;
31217
+ const title = cleanTitle(textOf4(node));
31218
+ if (title !== "" && !isPlaceholderTitle(title)) return { title, from };
31219
+ }
31220
+ return null;
31221
+ };
31222
+ return pick((n) => n.id.startsWith("cover_title_"), "cover page") ?? pick((n) => styleOf(n) === "Title", "Title") ?? pick((n) => styleOf(n) === "Heading1", "Heading 1");
31223
+ }
31224
+ function applyDocumentIdentity(ast, parser2) {
31225
+ const result = {};
31226
+ const patch = {};
31227
+ if (isPlaceholderTitle(ast.metadata?.title)) {
31228
+ const derived = deriveTitleFromContent(ast);
31229
+ if (derived) {
31230
+ patch.title = derived.title;
31231
+ result.title = derived.title;
31232
+ result.titleSetFrom = derived.from;
31233
+ }
31234
+ }
31235
+ const author = typeof ast.settings?.defaultAuthor === "string" ? ast.settings.defaultAuthor.trim() : "";
31236
+ if (author !== "" && author !== DEFAULT_CREATOR && ast.metadata?.creator === DEFAULT_CREATOR) {
31237
+ patch.creator = author;
31238
+ patch.lastModifiedBy = author;
31239
+ result.creatorSet = author;
31240
+ }
31241
+ if (Object.keys(patch).length === 0) return result;
31242
+ new CorePropertiesManipulator(ast, parser2?.getZipAdapter()).update(patch);
31243
+ return result;
31244
+ }
31245
+
30912
31246
  // ../engine/src/operations/coverPage.ts
30913
31247
  function isCoverReproducible(explicit, env) {
30914
31248
  try {
@@ -30973,6 +31307,7 @@ function executeCoverPage(op, ast, parser2) {
30973
31307
  if (!firstSection || firstSection.nodes.length === 0) {
30974
31308
  throw new Error("Cannot insert cover page into an empty document");
30975
31309
  }
31310
+ const seedIds = parser2 ? pristineSeedIdsOfAnyTemplate(parser2) : null;
30976
31311
  const doc = firstSection.nodes[0].xml.ownerDocument;
30977
31312
  if (!doc) throw new Error("Document not found on first section");
30978
31313
  const sectPr = doc.getElementsByTagName("w:sectPr")[0];
@@ -31039,8 +31374,8 @@ function executeCoverPage(op, ast, parser2) {
31039
31374
  titleColor.setAttribute("w:val", accentColor);
31040
31375
  titleRPr.appendChild(titleFont);
31041
31376
  titleRPr.appendChild(titleBold);
31042
- titleRPr.appendChild(titleSz);
31043
31377
  titleRPr.appendChild(titleColor);
31378
+ titleRPr.appendChild(titleSz);
31044
31379
  titleRun.appendChild(titleRPr);
31045
31380
  titleRun.appendChild(createTextElement(doc, titleText));
31046
31381
  titleP.appendChild(titleRun);
@@ -31066,8 +31401,8 @@ function executeCoverPage(op, ast, parser2) {
31066
31401
  subSz.setAttribute("w:val", "28");
31067
31402
  const subColor = createWordElement(doc, "w:color");
31068
31403
  subColor.setAttribute("w:val", "64748B");
31069
- subRPr.appendChild(subSz);
31070
31404
  subRPr.appendChild(subColor);
31405
+ subRPr.appendChild(subSz);
31071
31406
  subRun.appendChild(subRPr);
31072
31407
  subRun.appendChild(createTextElement(doc, subtitleText));
31073
31408
  subP.appendChild(subRun);
@@ -31117,8 +31452,8 @@ function executeCoverPage(op, ast, parser2) {
31117
31452
  metaSz.setAttribute("w:val", "22");
31118
31453
  const metaColor = createWordElement(doc, "w:color");
31119
31454
  metaColor.setAttribute("w:val", "475569");
31120
- metaRPr.appendChild(metaSz);
31121
31455
  metaRPr.appendChild(metaColor);
31456
+ metaRPr.appendChild(metaSz);
31122
31457
  metaRun.appendChild(metaRPr);
31123
31458
  const metaLines = [authorText, orgText, dateText].filter(Boolean).join(" \u2022 ");
31124
31459
  metaRun.appendChild(createTextElement(doc, metaLines || dateText));
@@ -31163,6 +31498,13 @@ function executeCoverPage(op, ast, parser2) {
31163
31498
  }
31164
31499
  }
31165
31500
  }
31501
+ if (seedIds !== null && seedIds.length > 0) {
31502
+ executeDelete({ action: "delete", target: seedIds }, ast, parser2);
31503
+ }
31504
+ try {
31505
+ applyDocumentIdentity(ast, parser2);
31506
+ } catch {
31507
+ }
31166
31508
  }
31167
31509
 
31168
31510
  // ../engine/src/operations/metadata.ts
@@ -31334,8 +31676,9 @@ function collectBatchTouchedIds(ops, ast) {
31334
31676
  const touch = (raw) => {
31335
31677
  if (typeof raw !== "string" || raw.length === 0) return;
31336
31678
  let resolved = raw;
31337
- if (raw === "start") resolved = order[0] ?? raw;
31338
- else if (raw === "end") resolved = order[order.length - 1] ?? raw;
31679
+ const alias = anchorAliasOf(raw);
31680
+ if (alias === "start") resolved = order[0] ?? raw;
31681
+ else if (alias === "end") resolved = order[order.length - 1] ?? raw;
31339
31682
  else {
31340
31683
  const cell = parseCellAddress(raw);
31341
31684
  if (cell && order.includes(cell.tableId)) resolved = cell.tableId;
@@ -31928,13 +32271,13 @@ var OperationEngine = class _OperationEngine {
31928
32271
  /**
31929
32272
  * R-4 helper: true when the op mints a live node id from the client id.
31930
32273
  * Only these routes use `op.id` verbatim as the node id; other actions
31931
- * either ignore `id` (footnote/callout/field generate their own) or are
32274
+ * either ignore `id` (footnote/field generate their own) or are
31932
32275
  * rejected at the schema gate, so live collision applies here only.
31933
32276
  */
31934
32277
  static mintsNodeId(op) {
31935
32278
  const fields = op;
31936
32279
  const action = typeof fields["action"] === "string" ? String(fields["action"]) : "";
31937
- if (action === "insert") return true;
32280
+ if (action === "insert" || action === "callout") return true;
31938
32281
  if (action === "equation") {
31939
32282
  const sub = fields["operation"] ?? fields["params"]?.["operation"];
31940
32283
  return sub === "insert";
@@ -32034,7 +32377,7 @@ var OperationEngine = class _OperationEngine {
32034
32377
  * E_VALIDATION + remedy.
32035
32378
  */
32036
32379
  static isAnchorAlias(value) {
32037
- return value === "start" || value === "end";
32380
+ return anchorAliasOf(value) !== null;
32038
32381
  }
32039
32382
  resolveAnchorAlias(op) {
32040
32383
  const fields = op;
@@ -32050,6 +32393,10 @@ var OperationEngine = class _OperationEngine {
32050
32393
  }
32051
32394
  }
32052
32395
  if (aliasSeen === null) return;
32396
+ for (const h of holders) {
32397
+ const alias = anchorAliasOf(h["anchor"]);
32398
+ if (alias !== null && h["position"] === void 0) h["position"] = alias === "start" ? "before" : "after";
32399
+ }
32053
32400
  let first;
32054
32401
  let last;
32055
32402
  for (const section of this.ast.sections ?? []) {
@@ -32066,8 +32413,9 @@ var OperationEngine = class _OperationEngine {
32066
32413
  );
32067
32414
  }
32068
32415
  for (const h of holders) {
32069
- if (h["anchor"] === "start") h["anchor"] = first;
32070
- else if (h["anchor"] === "end") h["anchor"] = last;
32416
+ const alias = anchorAliasOf(h["anchor"]);
32417
+ if (alias === "start") h["anchor"] = first;
32418
+ else if (alias === "end") h["anchor"] = last;
32071
32419
  }
32072
32420
  }
32073
32421
  async routeOperation(op, allowedDir) {
@@ -33324,7 +33672,11 @@ ${textLines.map((l) => `> ${l}`).join("\n")}
33324
33672
  `);
33325
33673
  } else {
33326
33674
  const textLines = text2 ? text2.split("\n") : [];
33327
- lines.push(`${textLines.map((l) => `> ${l}`).join("\n")}
33675
+ const ind = node.xml?.getElementsByTagName?.("w:pPr")?.[0]?.getElementsByTagName?.("w:ind")?.[0] ?? null;
33676
+ const left = ind ? parseInt(ind.getAttribute("w:left") || "0", 10) : 0;
33677
+ const depth = style === "quote" && Number.isFinite(left) && left > 0 ? Math.min(3, Math.max(1, Math.round(left / 720))) : 1;
33678
+ const marker = ">".repeat(1).concat(" >".repeat(depth - 1));
33679
+ lines.push(`${textLines.map((l) => `${marker} ${l}`).join("\n")}
33328
33680
  `);
33329
33681
  }
33330
33682
  } else {
@@ -33828,17 +34180,19 @@ function directKids(parent, tag) {
33828
34180
  return out;
33829
34181
  }
33830
34182
  function cellTextOf(tc) {
33831
- let text2 = "";
34183
+ const parts = [];
33832
34184
  try {
33833
34185
  const paras = directKids(tc, "w:p");
33834
34186
  for (const p of paras) {
34187
+ let text2 = "";
33835
34188
  const ts = p.getElementsByTagName("w:t");
33836
34189
  for (let i = 0; i < ts.length; i++) text2 += ts[i].textContent || "";
34190
+ if (text2.trim().length > 0) parts.push(text2);
33837
34191
  }
33838
34192
  } catch {
33839
34193
  return "";
33840
34194
  }
33841
- return text2;
34195
+ return parts.join(" ");
33842
34196
  }
33843
34197
  function displayLen(value) {
33844
34198
  return [...value].length;
@@ -33937,6 +34291,23 @@ function renderTableGrid(grid) {
33937
34291
  const rows = [...byRow.keys()].sort((a, b) => a - b);
33938
34292
  return rows.map((r) => (byRow.get(r) ?? []).map(renderGridCell).join(" | ")).join(" ; ");
33939
34293
  }
34294
+ function tableGridPlainText(tbl) {
34295
+ try {
34296
+ const el = tbl;
34297
+ if (!el || typeof el.getElementsByTagName !== "function") return void 0;
34298
+ const grid = extractTableGrid(el);
34299
+ if (grid.cells.length === 0) return void 0;
34300
+ const byRow = /* @__PURE__ */ new Map();
34301
+ for (const c of grid.cells) {
34302
+ const list = byRow.get(c.row);
34303
+ if (list) list.push(c.text);
34304
+ else byRow.set(c.row, [c.text]);
34305
+ }
34306
+ return [...byRow.keys()].sort((a, b) => a - b).map((r) => (byRow.get(r) ?? []).join(" | ")).join("\n");
34307
+ } catch {
34308
+ return void 0;
34309
+ }
34310
+ }
33940
34311
  function renderChartText(node) {
33941
34312
  const kind = node.type === "chart3d" ? "3D Chart" : "Chart";
33942
34313
  const data = node.metadata.chartData;
@@ -38010,6 +38381,134 @@ var HeadlessVisualRenderer = class {
38010
38381
  };
38011
38382
  var headlessScreenshotProvider = new HeadlessVisualRenderer();
38012
38383
 
38384
+ // ../engine/src/stats.ts
38385
+ function nearestOwner(el, tagName) {
38386
+ let cur = el.parentNode;
38387
+ while (cur && typeof cur === "object") {
38388
+ const e = cur;
38389
+ if (e.tagName === tagName) return e;
38390
+ cur = e.parentNode;
38391
+ }
38392
+ return null;
38393
+ }
38394
+ function tableCellTextSpaced(node) {
38395
+ try {
38396
+ const xml = node.xml;
38397
+ if (!xml || typeof xml.getElementsByTagName !== "function") return void 0;
38398
+ const tcs = xml.getElementsByTagName("w:tc");
38399
+ if (!tcs || tcs.length === 0) return void 0;
38400
+ const cells = [];
38401
+ for (let i = 0; i < tcs.length; i++) {
38402
+ const tc = tcs[i];
38403
+ const paras = tc.getElementsByTagName("w:p");
38404
+ if (paras.length > 0) {
38405
+ const parts = [];
38406
+ for (let p = 0; p < paras.length; p++) {
38407
+ const para = paras[p];
38408
+ if (nearestOwner(para, "w:tc") !== tc) continue;
38409
+ const runs = para.getElementsByTagName("w:t");
38410
+ let t = "";
38411
+ for (let r = 0; r < runs.length; r++) {
38412
+ const run = runs[r];
38413
+ if (nearestOwner(run, "w:tc") !== tc) continue;
38414
+ if (nearestOwner(run, "w:p") !== para) continue;
38415
+ t += run.textContent || "";
38416
+ }
38417
+ if (t.trim().length > 0) parts.push(t.trim());
38418
+ }
38419
+ if (parts.length > 0) cells.push(parts.join(" "));
38420
+ } else {
38421
+ const runs = tc.getElementsByTagName("w:t");
38422
+ let t = "";
38423
+ for (let r = 0; r < runs.length; r++) {
38424
+ const run = runs[r];
38425
+ if (nearestOwner(run, "w:tc") !== tc) continue;
38426
+ t += run.textContent || "";
38427
+ }
38428
+ if (t.trim().length > 0) cells.push(t.trim());
38429
+ }
38430
+ }
38431
+ if (cells.length === 0) return "";
38432
+ return cells.join(" ");
38433
+ } catch {
38434
+ return void 0;
38435
+ }
38436
+ }
38437
+ function nestedTableCount(node) {
38438
+ try {
38439
+ const xml = node.xml;
38440
+ if (!xml || typeof xml.getElementsByTagName !== "function") return 0;
38441
+ return xml.getElementsByTagName("w:tbl").length;
38442
+ } catch {
38443
+ return 0;
38444
+ }
38445
+ }
38446
+ function isCalloutTable(node, dims) {
38447
+ try {
38448
+ const xml = node.xml;
38449
+ if (!xml || typeof xml.getElementsByTagName !== "function") return false;
38450
+ if (dims) {
38451
+ if (dims.rows !== 1 || dims.cols !== 1) return false;
38452
+ } else {
38453
+ const rows = Array.from(xml.childNodes).filter((n) => n.nodeType === 1 && n.tagName === "w:tr");
38454
+ if (rows.length !== 1) return false;
38455
+ const cells = Array.from(rows[0].childNodes).filter((n) => n.nodeType === 1 && n.tagName === "w:tc");
38456
+ if (cells.length !== 1) return false;
38457
+ }
38458
+ const fill = xml.getElementsByTagName("w:shd")[0]?.getAttribute("w:fill") ?? "";
38459
+ return /^[0-9A-Fa-f]{6}$/.test(fill);
38460
+ } catch {
38461
+ return false;
38462
+ }
38463
+ }
38464
+ function countExplicitPageBreaks(node) {
38465
+ if (node.type === "pageBreak") return 1;
38466
+ const text2 = node.metadata?.text || "";
38467
+ let count = text2 === "[Page Break]" ? 1 : 0;
38468
+ try {
38469
+ const xml = node.xml;
38470
+ if (xml && typeof xml.getElementsByTagName === "function") {
38471
+ const brs = xml.getElementsByTagName("w:br");
38472
+ let brCount = 0;
38473
+ for (let i = 0; i < brs.length; i++) {
38474
+ if (brs[i].getAttribute("w:type") === "page") brCount++;
38475
+ }
38476
+ if (brCount > count) count = brCount;
38477
+ }
38478
+ } catch {
38479
+ }
38480
+ return count;
38481
+ }
38482
+ function computeDocStats(ast) {
38483
+ let words = 0;
38484
+ let paragraphs = 0;
38485
+ let tables = 0;
38486
+ let callouts = 0;
38487
+ let images = 0;
38488
+ let pageBreaks = 0;
38489
+ for (const sec of ast.sections) {
38490
+ for (const node of sec.nodes) {
38491
+ if (node.type === "paragraph") paragraphs += 1;
38492
+ if (node.type === "table") {
38493
+ if (isCalloutTable(node)) callouts += 1;
38494
+ else tables += 1;
38495
+ tables += nestedTableCount(node);
38496
+ }
38497
+ if (node.type === "image") images += 1;
38498
+ pageBreaks += countExplicitPageBreaks(node);
38499
+ let text2 = node.metadata?.text ?? "";
38500
+ if (node.type === "table") {
38501
+ const spaced = tableCellTextSpaced(node);
38502
+ if (spaced !== void 0) text2 = spaced;
38503
+ }
38504
+ const hits = text2.match(/\S+/g);
38505
+ if (hits) words += hits.length;
38506
+ }
38507
+ }
38508
+ const pageBudget = words / 450 + (tables + callouts) * 0.45 + images * 0.5 + pageBreaks;
38509
+ return { words, paragraphs, pages: Math.max(1, Math.ceil(pageBudget)), tables, callouts, images };
38510
+ }
38511
+
38013
38512
  // ../engine/src/visual/telemetry.ts
38014
38513
  var VISUAL_TELEMETRY_TOKEN_BUDGET = 500;
38015
38514
  var VISUAL_WIREFRAME_WIDTH = 68;
@@ -38106,17 +38605,6 @@ var WIREFRAME_TABLE_ROWS = 3;
38106
38605
  function flatText(value) {
38107
38606
  return value.replace(/\s+/g, " ").trim();
38108
38607
  }
38109
- function isCalloutTable(node, grid) {
38110
- if (!grid || grid.rows !== 1 || grid.cols !== 1) return false;
38111
- try {
38112
- const xml = node.xml;
38113
- const shd = xml?.getElementsByTagName("w:shd")[0];
38114
- const fill = shd?.getAttribute("w:fill") ?? "";
38115
- return /^[0-9A-Fa-f]{6}$/.test(fill);
38116
- } catch {
38117
- return false;
38118
- }
38119
- }
38120
38608
  function tableRowLines(grid, width) {
38121
38609
  const byRow = /* @__PURE__ */ new Map();
38122
38610
  for (const cell of grid.cells) {
@@ -38339,7 +38827,7 @@ function generateVisualWireframe(ast, targetPage = 1, pageMap, maxNodes = 24, op
38339
38827
  } else if (node.type === "table") {
38340
38828
  lines.push(emptyLine);
38341
38829
  const grid = tableGridOf(node);
38342
- if (isCalloutTable(node, grid)) {
38830
+ if (isCalloutTable(node, grid ? { rows: grid.rows, cols: grid.cols } : null)) {
38343
38831
  const note = flatText(grid?.cells[0]?.text ?? "");
38344
38832
  const body = note.length > 48 ? note.slice(0, 45) + "..." : note;
38345
38833
  lines.push(`| [CALLOUT] ${body}`.slice(0, W2 - 2).padEnd(W2 - 1) + "|");
@@ -38527,6 +39015,10 @@ function buildLineIndex(src, tokens) {
38527
39015
  }
38528
39016
  return out;
38529
39017
  }
39018
+ var QUOTE_INDENT = QUOTE_INDENT_TWIPS;
39019
+ var QUOTE_LIST_INDENT = 360;
39020
+ var QUOTE_MAX_LEVEL = 3;
39021
+ var CODE_BLOCK_BASE_INDENT = 80;
38530
39022
  function subLine(blockRaw, blockLine, excerpt) {
38531
39023
  const found = blockRaw.indexOf(excerpt);
38532
39024
  if (found === -1) return blockLine;
@@ -38701,12 +39193,17 @@ function compileMarkdownDetailed(markdown, options) {
38701
39193
  const pendingNotes = [];
38702
39194
  let seq = 0;
38703
39195
  let lastInsertId = null;
39196
+ const idPrefix = options?.idPrefix ?? "md";
38704
39197
  const nextId = () => {
38705
39198
  seq += 1;
38706
- return `md_${String(seq).padStart(4, "0")}`;
39199
+ return `${idPrefix}_${String(seq).padStart(4, "0")}`;
39200
+ };
39201
+ const startAnchor = {
39202
+ anchor: options?.anchor ?? "end",
39203
+ position: options?.position ?? "after"
38707
39204
  };
38708
39205
  const insertAnchor = () => {
38709
- if (lastInsertId === null) return { anchor: "end", position: "after" };
39206
+ if (lastInsertId === null) return { ...startAnchor };
38710
39207
  return { anchor: lastInsertId, position: "after" };
38711
39208
  };
38712
39209
  const pushInsertParagraph = (text2, spans) => {
@@ -38834,12 +39331,12 @@ function compileMarkdownDetailed(markdown, options) {
38834
39331
  const inlineMime = inlineKind === "jpeg" ? "image/jpeg" : inlineKind === "svg" ? "image/svg+xml" : inlineKind === "tiff" ? "image/tiff" : `image/${inlineKind}`;
38835
39332
  const inlineFitted = fitImageEmus(decodeImagePixels(inlineBuf, inlineKind));
38836
39333
  const inlineId = nextId();
38837
- const inlineAnchor = lastInsertId === null ? "end" : lastInsertId;
39334
+ const inlineAnchor = insertAnchor();
38838
39335
  ops.push({
38839
39336
  action: "insert",
38840
39337
  type: "image",
38841
- anchor: inlineAnchor,
38842
- position: "after",
39338
+ anchor: inlineAnchor.anchor,
39339
+ position: inlineAnchor.position,
38843
39340
  id: inlineId,
38844
39341
  fileData: `data:${inlineMime};base64,${inlineBuf.toString("base64")}`,
38845
39342
  ...inlineFitted ? { width: inlineFitted.width, height: inlineFitted.height } : {},
@@ -38848,7 +39345,7 @@ function compileMarkdownDetailed(markdown, options) {
38848
39345
  lastInsertId = inlineId;
38849
39346
  return { id: inlineId, kind: "image" };
38850
39347
  }
38851
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(target)) {
39348
+ if (hasUriScheme(target)) {
38852
39349
  fail(line2, "image reference points outside the allowed directory", "keep referenced files inside the allowed directory and use a relative path");
38853
39350
  }
38854
39351
  if (allowedDir === void 0) {
@@ -38857,14 +39354,17 @@ function compileMarkdownDetailed(markdown, options) {
38857
39354
  const isChart = target.toLowerCase().endsWith(CHART_JSON_SUFFIX);
38858
39355
  const absPath = resolveAssetPath(allowedDir, target, line2, isChart ? "chart" : "image");
38859
39356
  const id = nextId();
38860
- const anchorId = lastInsertId === null ? "end" : lastInsertId;
39357
+ const chartAnchor = insertAnchor();
38861
39358
  if (isChart) {
39359
+ if (chartAnchor.position === "before") {
39360
+ fail(line2, "a chart cannot be the first block when the compose goes before a node", "start the Markdown with a paragraph, or place the compose after the node");
39361
+ }
38862
39362
  const asset2 = readChartAsset(absPath, line2, allowedDir);
38863
39363
  const title = asset2.title ?? (alt.trim().length > 0 ? alt.trim() : "Chart");
38864
39364
  ops.push({
38865
39365
  action: "chart",
38866
39366
  operation: "insert",
38867
- anchor: anchorId,
39367
+ anchor: chartAnchor.anchor,
38868
39368
  id,
38869
39369
  chartType: asset2.chartType,
38870
39370
  title,
@@ -38875,7 +39375,7 @@ function compileMarkdownDetailed(markdown, options) {
38875
39375
  return { id, kind: "chart" };
38876
39376
  }
38877
39377
  const asset = readImageAsset(absPath, line2, allowedDir);
38878
- const anchor = lastInsertId === null ? { anchor: "end", position: "after" } : { anchor: lastInsertId, position: "after" };
39378
+ const anchor = insertAnchor();
38879
39379
  ops.push({
38880
39380
  action: "insert",
38881
39381
  type: "image",
@@ -38893,15 +39393,11 @@ function compileMarkdownDetailed(markdown, options) {
38893
39393
  const walkAlert = (list) => {
38894
39394
  for (const tok of list) {
38895
39395
  const t = tok;
38896
- if (t.type === "strong" || t.type === "em" || t.type === "codespan") {
38897
- fail(line2, "rich text inside alerts is out of scope", "move bold, italic, or code into a body paragraph");
38898
- }
38899
39396
  if (t.type === "link") {
38900
39397
  const raw = t.raw;
38901
39398
  if (typeof raw === "string" && raw.startsWith("<")) {
38902
39399
  fail(line2, "raw HTML is out of scope in an alert", "use plain Markdown blocks instead of HTML tags");
38903
39400
  }
38904
- fail(line2, "links inside alerts are out of scope", "move the link into a body paragraph");
38905
39401
  }
38906
39402
  if (t.type === "image") {
38907
39403
  fail(line2, "images are out of scope in an alert", "put images on their own block line instead");
@@ -38919,7 +39415,7 @@ function compileMarkdownDetailed(markdown, options) {
38919
39415
  if (alertTokens) walkAlert(alertTokens);
38920
39416
  };
38921
39417
  const handleAlertBlockquote = (token, line2) => {
38922
- const inner = token.tokens ?? [];
39418
+ const inner = (token.tokens ?? []).filter((child) => child.type !== "space");
38923
39419
  if (inner.length === 0) {
38924
39420
  fail(line2, "blockquotes are out of scope", "use a plain paragraph");
38925
39421
  }
@@ -38945,6 +39441,7 @@ ${rest}` : sameLineBody;
38945
39441
  }
38946
39442
  const restLines = rest.split("\n");
38947
39443
  let title;
39444
+ let titleSegments;
38948
39445
  let bodySrc = rest;
38949
39446
  if (restLines.length > 0 && ALERT_TITLE_PATTERN.test(restLines[0].trim())) {
38950
39447
  const innerTitle = (ALERT_TITLE_PATTERN.exec(restLines[0].trim())?.[1] ?? "").trim();
@@ -38967,6 +39464,22 @@ ${rest}` : sameLineBody;
38967
39464
  CITATION_REF_PATTERN.lastIndex = 0;
38968
39465
  title = innerTitle;
38969
39466
  bodySrc = restLines.slice(1).join("\n");
39467
+ let titleTokens;
39468
+ try {
39469
+ titleTokens = lexMarkdown(restLines[0].trim(), { dollarMath: options?.dollarMath });
39470
+ } catch (err) {
39471
+ const detail = err instanceof Error ? err.message : String(err);
39472
+ throw toError("E_VALIDATION", `line ${line2}: alert title rejected (${detail})`, "keep the alert title to one line of inline text");
39473
+ }
39474
+ const titleBlocks = titleTokens.filter((t) => t.type !== "space");
39475
+ if (titleBlocks.length !== 1 || titleBlocks[0].type !== "paragraph") {
39476
+ fail(line2, `alert [!${alertType}] title must be one line of inline text`, "keep the title to one bold line");
39477
+ }
39478
+ const titleInlineTokens = titleBlocks[0].tokens;
39479
+ failOnSpanTokensInAlert(titleInlineTokens, line2);
39480
+ const parsedTitle = parseInlineTokens(titleInlineTokens, line2, "an alert title", citationDefs, citationOrder, citationNum, usedCitations);
39481
+ title = parsedTitle.plainText.length > 0 ? parsedTitle.plainText : innerTitle;
39482
+ titleSegments = parsedTitle.segments;
38970
39483
  }
38971
39484
  for (const bodyLine of bodySrc.split("\n")) {
38972
39485
  if (CONTAINER_FENCE_PATTERN.test(bodyLine)) {
@@ -38987,6 +39500,7 @@ ${rest}` : sameLineBody;
38987
39500
  throw toError("E_VALIDATION", `line ${line2}: alert body rejected (${detail})`, "keep alert bodies plain text");
38988
39501
  }
38989
39502
  const cleanParas = [];
39503
+ const bodySegments = [];
38990
39504
  for (const bodyToken of bodyTokens) {
38991
39505
  if (bodyToken.type === "space") continue;
38992
39506
  if (bodyToken.type !== "paragraph") {
@@ -39002,31 +39516,214 @@ ${rest}` : sameLineBody;
39002
39516
  FOOTNOTE_REF_PATTERN.lastIndex = 0;
39003
39517
  failOnImageOrHtml(paraText2, paraLine, "an alert");
39004
39518
  failOnSpanTokensInAlert(para.tokens, paraLine);
39005
- cleanParas.push(plainJoin(replaceCitationsInPlainCell(paraText2, paraLine)));
39519
+ const parsedPara = parseInlineTokens(para.tokens, paraLine, "an alert", citationDefs, citationOrder, citationNum, usedCitations);
39520
+ if (parsedPara.footnoteRefs.length > 0) {
39521
+ fail(paraLine, "footnote markers inside alerts are out of scope", "move the note reference into a body paragraph");
39522
+ }
39523
+ for (const key of parsedPara.citationKeys) usedCitations.add(key);
39524
+ if (cleanParas.length > 0) bodySegments.push({ text: " ", bold: false, italic: false, code: false });
39525
+ bodySegments.push(...parsedPara.segments);
39526
+ cleanParas.push(parsedPara.plainText);
39006
39527
  }
39007
39528
  const text2 = plainJoin(cleanParas.join(" "));
39008
39529
  if (text2.length === 0) {
39009
39530
  fail(line2, `alert [!${alertType}] needs body text`, "write the alert text on > lines after the marker");
39010
39531
  }
39011
- const anchorId = lastInsertId === null ? "end" : lastInsertId;
39532
+ const calloutAnchor = insertAnchor();
39012
39533
  const alertTag = alertType.toLowerCase();
39013
39534
  const tagThemeRaw = styleGuide?.callouts?.tagThemes?.[alertTag];
39014
39535
  const tagTheme = typeof tagThemeRaw === "string" && tagThemeRaw.length > 0 ? tagThemeRaw : void 0;
39015
39536
  const guideThemeRaw = styleGuide?.callouts?.defaultTheme;
39016
39537
  const guideTheme = typeof guideThemeRaw === "string" && guideThemeRaw.length > 0 ? guideThemeRaw : void 0;
39538
+ const calloutId = nextId();
39017
39539
  const op = {
39018
39540
  action: "callout",
39019
- anchor: anchorId,
39020
- position: "after",
39541
+ id: calloutId,
39542
+ anchor: calloutAnchor.anchor,
39543
+ position: calloutAnchor.position,
39021
39544
  text: text2,
39022
39545
  theme: tagTheme ?? guideTheme ?? ALERT_TYPE_TO_THEME[alertType] ?? "indigo"
39023
39546
  };
39024
39547
  if (title !== void 0) op["title"] = title;
39548
+ const spanParams = {};
39549
+ if (titleSegments !== void 0 && segmentsNeedSplit(titleSegments)) spanParams["mdTitleSpans"] = titleSegments;
39550
+ if (segmentsNeedSplit(bodySegments)) spanParams["mdBodySpans"] = bodySegments;
39551
+ if (Object.keys(spanParams).length > 0) op["params"] = spanParams;
39025
39552
  ops.push(op);
39026
- lastInsertId = null;
39553
+ lastInsertId = calloutId;
39027
39554
  };
39028
39555
  let underTitle = false;
39029
39556
  let lastBlockKind = "none";
39557
+ const compileList = (token, line2, quoteLevel = 0) => {
39558
+ const listType = token.ordered === true ? "numbered" : "bullet";
39559
+ const items = token.items ?? [];
39560
+ if (items.length === 0) {
39561
+ fail(line2, "empty list block", "add at least one list item");
39562
+ }
39563
+ const itemIds = [];
39564
+ const itemBaseLine = line2;
39565
+ for (const item of items) {
39566
+ const itemLine = subLine(token.raw, itemBaseLine, item.raw);
39567
+ if (item.task === true) {
39568
+ fail(itemLine, "task-list items are out of scope", "use a plain bullet or numbered item");
39569
+ }
39570
+ for (const child of item.tokens ?? []) {
39571
+ if (child.type === "list") {
39572
+ const nestedRaw = child.raw;
39573
+ const nestedLine = typeof nestedRaw === "string" && nestedRaw ? subLine(token.raw, itemBaseLine, nestedRaw) : itemLine;
39574
+ fail(nestedLine, "nested lists are out of scope", "flatten nested items into the top-level list");
39575
+ }
39576
+ if (child.type !== "text" && child.type !== "paragraph" && child.type !== "space") {
39577
+ fail(itemLine, `list items carry ${child.type} blocks, out of scope`, "keep list items plain text");
39578
+ }
39579
+ }
39580
+ if (item.text.split("\n").some((entry) => FOOTNOTE_DEF_PATTERN.test(entry))) {
39581
+ fail(itemLine, "footnote definition must stand alone as its own block", "move the definition out of the list");
39582
+ }
39583
+ if (item.text.split("\n").some((entry) => CITATION_DEF_PATTERN.test(entry))) {
39584
+ fail(itemLine, "citation definition must stand alone as its own block", "move the definition out of the list");
39585
+ }
39586
+ failOnImageOrHtml(item.text, itemLine, "a list item");
39587
+ const firstChild = (item.tokens ?? [])[0];
39588
+ const inlineTokens = firstChild?.tokens ?? [];
39589
+ if (inlineTokens.length === 1 && inlineTokens[0].type === "image") {
39590
+ fail(itemLine, "images are out of scope in a list item", "put images on their own block line instead of inside a list");
39591
+ }
39592
+ for (const it of inlineTokens) {
39593
+ if (it.type === "image") {
39594
+ fail(itemLine, "images are out of scope in a list item", "put images on their own block line instead of inside a list");
39595
+ }
39596
+ }
39597
+ const inline = parseInlineTokens(inlineTokens, itemLine, "a list item", citationDefs, citationOrder, citationNum, usedCitations);
39598
+ for (const key of inline.citationKeys) usedCitations.add(key);
39599
+ for (const label of inline.footnoteRefs) {
39600
+ if (!defs.has(label)) {
39601
+ fail(itemLine, `undefined footnote reference [^${label}]`, "add a matching [^label]: body definition");
39602
+ }
39603
+ }
39604
+ const id = pushInsertParagraph(inline.plainText, inline.segments);
39605
+ trackRefs(id, inline.footnoteEntries ?? inline.footnoteRefs, itemLine);
39606
+ itemIds.push(id);
39607
+ }
39608
+ ops.push({ action: "list", operation: "create", target: itemIds, listType });
39609
+ if (lastBlockKind === "table" && itemIds.length > 0) {
39610
+ ops.push({ action: "style", target: itemIds[0], spacingBefore: 240 });
39611
+ }
39612
+ if (quoteLevel > 0) {
39613
+ for (const itemId of itemIds) {
39614
+ ops.push({ action: "namedStyle", operation: "apply", styleName: "Quote", target: itemId });
39615
+ ops.push({ action: "style", target: itemId, indentLeft: quoteLevel * QUOTE_INDENT + QUOTE_LIST_INDENT, indentHanging: QUOTE_LIST_INDENT });
39616
+ }
39617
+ }
39618
+ };
39619
+ const compileCode = (token, line2, quoteLevel = 0) => {
39620
+ const lang = (token.lang ?? "").trim();
39621
+ if (CHART_FENCE_LANGUAGES.has(lang.toLowerCase())) {
39622
+ fail(line2, `fenced code tagged '${lang}' requests a chart`, "describe the chart with a chart path reference instead");
39623
+ }
39624
+ const id = nextId();
39625
+ const anchor = insertAnchor();
39626
+ ops.push({
39627
+ action: "insert",
39628
+ type: "paragraph",
39629
+ anchor: anchor.anchor,
39630
+ position: anchor.position,
39631
+ text: token.text ?? "",
39632
+ id,
39633
+ params: { codeBlock: true, ...lang ? { codeLanguage: lang } : {} }
39634
+ });
39635
+ lastInsertId = id;
39636
+ if (lastBlockKind === "table") {
39637
+ ops.push({ action: "style", target: id, spacingBefore: 240 });
39638
+ }
39639
+ if (quoteLevel > 0) {
39640
+ ops.push({ action: "style", target: id, indentLeft: quoteLevel * QUOTE_INDENT + CODE_BLOCK_BASE_INDENT });
39641
+ }
39642
+ };
39643
+ const compileQuote = (token, line2, level) => {
39644
+ const effective = Math.min(level, QUOTE_MAX_LEVEL);
39645
+ if (level > QUOTE_MAX_LEVEL) {
39646
+ warnings.push(`line ${line2}: quote nesting deeper than ${QUOTE_MAX_LEVEL} levels was written at level ${QUOTE_MAX_LEVEL}.`);
39647
+ }
39648
+ const children2 = (token.tokens ?? []).filter((child) => child.type !== "space");
39649
+ if (children2.length === 0) {
39650
+ fail(line2, "blockquote is empty", "write the quote text after the > marker");
39651
+ }
39652
+ const strippedRaw = token.raw.split("\n").map((entry) => entry.replace(/^ {0,3}>[ ]?/, "")).join("\n");
39653
+ for (const child of children2) {
39654
+ const childRaw = child.raw ?? "";
39655
+ const childLine = subLine(strippedRaw, line2, childRaw.split("\n")[0] ?? "");
39656
+ switch (child.type) {
39657
+ case "paragraph": {
39658
+ const para = child;
39659
+ const paraText2 = para.text ?? "";
39660
+ const paraLines = childRaw.replace(/\n+$/, "").split("\n");
39661
+ if (paraLines.some((entry) => FOOTNOTE_DEF_PATTERN.test(entry) || CITATION_DEF_PATTERN.test(entry))) {
39662
+ fail(childLine, "note and citation definitions must stand alone as their own block", "move the definition out of the quote");
39663
+ }
39664
+ if (CONTAINER_FENCE_PATTERN.test(paraLines[0] ?? "")) {
39665
+ fail(childLine, "container fences (:::) are out of scope as custom styles", "use plain Markdown blocks");
39666
+ }
39667
+ const paraTokens = para.tokens ?? [];
39668
+ for (const it of paraTokens) {
39669
+ if (it.type === "image") {
39670
+ fail(childLine, "images are out of scope in a quote", "put images on their own block line after the quote");
39671
+ }
39672
+ }
39673
+ failOnImageOrHtml(paraText2, childLine, "a quote");
39674
+ const inline = parseInlineTokens(para.tokens, childLine, "a quote", citationDefs, citationOrder, citationNum, usedCitations);
39675
+ for (const key of inline.citationKeys) usedCitations.add(key);
39676
+ for (const label of inline.footnoteRefs) {
39677
+ if (!defs.has(label)) {
39678
+ fail(childLine, `undefined footnote reference [^${label}]`, "add a matching [^label]: body definition");
39679
+ }
39680
+ }
39681
+ const id = pushInsertParagraph(inline.plainText, inline.segments);
39682
+ trackRefs(id, inline.footnoteEntries ?? inline.footnoteRefs, childLine);
39683
+ ops.push({ action: "namedStyle", operation: "apply", styleName: "Quote", target: id });
39684
+ if (effective > 1) {
39685
+ ops.push({ action: "style", target: id, indentLeft: effective * QUOTE_INDENT });
39686
+ }
39687
+ if (lastBlockKind === "table") {
39688
+ ops.push({ action: "style", target: id, spacingBefore: 240 });
39689
+ }
39690
+ lastBlockKind = "quote";
39691
+ underTitle = false;
39692
+ break;
39693
+ }
39694
+ case "blockquote":
39695
+ compileQuote(child, childLine, level + 1);
39696
+ break;
39697
+ case "list":
39698
+ compileList(child, childLine, effective);
39699
+ lastBlockKind = "list";
39700
+ break;
39701
+ case "code":
39702
+ compileCode(child, childLine, effective);
39703
+ lastBlockKind = "code";
39704
+ break;
39705
+ case "table":
39706
+ fail(childLine, "tables inside a quote are out of scope", "Word cannot indent a table as a quote: put the table after the quote");
39707
+ break;
39708
+ case "heading":
39709
+ fail(childLine, "headings inside a quote are out of scope", "put the heading before the quote");
39710
+ break;
39711
+ default:
39712
+ fail(childLine, `${child.type} blocks inside a quote are out of scope`, "keep quotes to paragraphs, flat lists and code blocks");
39713
+ }
39714
+ }
39715
+ };
39716
+ const quoteOpensWithAlertMarker = (token, line2) => {
39717
+ const first = (token.tokens ?? []).find((child) => child.type !== "space");
39718
+ if (!first || first.type !== "paragraph") return false;
39719
+ const head = (first.text ?? "").split("\n")[0]?.trim() ?? "";
39720
+ if (ALERT_MARKER_PATTERN.test(head)) return true;
39721
+ const unknown = /^\[!([A-Za-z][A-Za-z0-9_-]*)\]/.exec(head);
39722
+ if (unknown) {
39723
+ fail(line2, `unknown alert type [!${unknown[1]}]`, "use NOTE, TIP, IMPORTANT, WARNING or CAUTION, or drop the [!...] marker for a plain quote");
39724
+ }
39725
+ return false;
39726
+ };
39030
39727
  for (const rawToken of tokens) {
39031
39728
  const token = rawToken;
39032
39729
  const line2 = lineOf.get(rawToken) ?? 1;
@@ -39136,60 +39833,7 @@ ${rest}` : sameLineBody;
39136
39833
  break;
39137
39834
  }
39138
39835
  case "list": {
39139
- const listType = token.ordered === true ? "numbered" : "bullet";
39140
- const items = token.items ?? [];
39141
- if (items.length === 0) {
39142
- fail(line2, "empty list block", "add at least one list item");
39143
- }
39144
- const itemIds = [];
39145
- const itemBaseLine = line2;
39146
- for (const item of items) {
39147
- const itemLine = subLine(token.raw, itemBaseLine, item.raw);
39148
- if (item.task === true) {
39149
- fail(itemLine, "task-list items are out of scope", "use a plain bullet or numbered item");
39150
- }
39151
- for (const child of item.tokens ?? []) {
39152
- if (child.type === "list") {
39153
- const nestedRaw = child.raw;
39154
- const nestedLine = typeof nestedRaw === "string" && nestedRaw ? subLine(token.raw, itemBaseLine, nestedRaw) : itemLine;
39155
- fail(nestedLine, "nested lists are out of scope", "flatten nested items into the top-level list");
39156
- }
39157
- if (child.type !== "text" && child.type !== "paragraph" && child.type !== "space") {
39158
- fail(itemLine, `list items carry ${child.type} blocks, out of scope`, "keep list items plain text");
39159
- }
39160
- }
39161
- if (item.text.split("\n").some((entry) => FOOTNOTE_DEF_PATTERN.test(entry))) {
39162
- fail(itemLine, "footnote definition must stand alone as its own block", "move the definition out of the list");
39163
- }
39164
- if (item.text.split("\n").some((entry) => CITATION_DEF_PATTERN.test(entry))) {
39165
- fail(itemLine, "citation definition must stand alone as its own block", "move the definition out of the list");
39166
- }
39167
- failOnImageOrHtml(item.text, itemLine, "a list item");
39168
- const firstChild = (item.tokens ?? [])[0];
39169
- const inlineTokens = firstChild?.tokens ?? [];
39170
- if (inlineTokens.length === 1 && inlineTokens[0].type === "image") {
39171
- fail(itemLine, "images are out of scope in a list item", "put images on their own block line instead of inside a list");
39172
- }
39173
- for (const it of inlineTokens) {
39174
- if (it.type === "image") {
39175
- fail(itemLine, "images are out of scope in a list item", "put images on their own block line instead of inside a list");
39176
- }
39177
- }
39178
- const inline = parseInlineTokens(inlineTokens, itemLine, "a list item", citationDefs, citationOrder, citationNum, usedCitations);
39179
- for (const key of inline.citationKeys) usedCitations.add(key);
39180
- for (const label of inline.footnoteRefs) {
39181
- if (!defs.has(label)) {
39182
- fail(itemLine, `undefined footnote reference [^${label}]`, "add a matching [^label]: body definition");
39183
- }
39184
- }
39185
- const id = pushInsertParagraph(inline.plainText, inline.segments);
39186
- trackRefs(id, inline.footnoteEntries ?? inline.footnoteRefs, itemLine);
39187
- itemIds.push(id);
39188
- }
39189
- ops.push({ action: "list", operation: "create", target: itemIds, listType });
39190
- if (lastBlockKind === "table" && itemIds.length > 0) {
39191
- ops.push({ action: "style", target: itemIds[0], spacingBefore: 240 });
39192
- }
39836
+ compileList(token, line2);
39193
39837
  lastBlockKind = "list";
39194
39838
  break;
39195
39839
  }
@@ -39302,25 +39946,7 @@ ${rest}` : sameLineBody;
39302
39946
  break;
39303
39947
  }
39304
39948
  case "code": {
39305
- const lang = (token.lang ?? "").trim();
39306
- if (CHART_FENCE_LANGUAGES.has(lang.toLowerCase())) {
39307
- fail(line2, `fenced code tagged '${lang}' requests a chart`, "describe the chart with a chart path reference instead");
39308
- }
39309
- const id = nextId();
39310
- const anchor = insertAnchor();
39311
- ops.push({
39312
- action: "insert",
39313
- type: "paragraph",
39314
- anchor: anchor.anchor,
39315
- position: anchor.position,
39316
- text: token.text ?? "",
39317
- id,
39318
- params: { codeBlock: true, ...lang ? { codeLanguage: lang } : {} }
39319
- });
39320
- lastInsertId = id;
39321
- if (lastBlockKind === "table") {
39322
- ops.push({ action: "style", target: id, spacingBefore: 240 });
39323
- }
39949
+ compileCode(token, line2);
39324
39950
  lastBlockKind = "code";
39325
39951
  break;
39326
39952
  }
@@ -39339,8 +39965,12 @@ ${rest}` : sameLineBody;
39339
39965
  fail(line2, "raw HTML blocks are out of scope", "use plain Markdown blocks instead of HTML tags");
39340
39966
  break;
39341
39967
  case "blockquote":
39342
- handleAlertBlockquote(token, line2);
39343
- lastBlockKind = "callout";
39968
+ if (quoteOpensWithAlertMarker(token, line2)) {
39969
+ handleAlertBlockquote(token, line2);
39970
+ lastBlockKind = "callout";
39971
+ } else {
39972
+ compileQuote(token, line2, 1);
39973
+ }
39344
39974
  break;
39345
39975
  case "image": {
39346
39976
  const img = token;
@@ -39424,107 +40054,15 @@ ${rest}` : sameLineBody;
39424
40054
  }
39425
40055
 
39426
40056
  // ../engine/src/compose/markdown.ts
39427
- function pristineTemplateSeedIds(parser2, requestedTemplate) {
39428
- if (!parser2.hasPristineBuiltinSeed()) return null;
39429
- let summary;
39430
- try {
39431
- summary = parser2.getStructuralSummary();
39432
- } catch {
39433
- return null;
39434
- }
39435
- const nodes = summary.nodes ?? [];
39436
- if (![1, 2, 4, 5, 6].includes(nodes.length)) return null;
39437
- if (summary.hasTrackedChanges === true) return null;
39438
- if (Object.keys(summary.mediaFiles ?? {}).length > 0) return null;
39439
- if ((summary.headers ?? []).length > 0) return null;
39440
- if ((summary.footers ?? []).length > 0) return null;
39441
- if ((summary.charts ?? []).length > 0) return null;
39442
- try {
39443
- const zip = parser2.getZipAdapter?.();
39444
- if (typeof zip?.hasFile === "function") {
39445
- for (const part of [
39446
- "word/footnotes.xml",
39447
- "word/endnotes.xml",
39448
- "word/comments.xml",
39449
- "word/commentsExtended.xml"
39450
- ]) {
39451
- try {
39452
- if (zip.hasFile(part)) return null;
39453
- } catch {
39454
- return null;
39455
- }
39456
- }
39457
- } else {
39458
- return null;
39459
- }
39460
- } catch {
39461
- return null;
39462
- }
39463
- for (const n of nodes) {
39464
- if (n.hasTrackedChanges === true) return null;
39465
- const raw = n.rawXml ?? "";
39466
- if (raw.includes("w:footnoteReference") || raw.includes("w:endnoteReference") || raw.includes("w:commentReference") || raw.includes("w:commentRangeStart") || raw.includes("w:commentRangeEnd")) {
39467
- return null;
39468
- }
39469
- }
39470
- const ids = nodes.map((n) => n.id ?? "").filter((id) => id.length > 0);
39471
- if (ids.length !== nodes.length) return null;
39472
- if (nodes.length === 1) {
39473
- const node = nodes[0];
39474
- if (node.type !== "paragraph") return null;
39475
- if ((node.text ?? "").trim() !== "") return null;
39476
- return ids;
39477
- }
39478
- if (nodes.length === 2) {
39479
- const [first, second] = nodes;
39480
- const t0 = (first.text ?? "").trim();
39481
- const t1 = (second.text ?? "").trim();
39482
- if (t0 === BLANK_SEED_TITLE && t1 === BLANK_SEED_HINT) {
39483
- const firstRaw = first.rawXml ?? "";
39484
- const secondRaw = second.rawXml ?? "";
39485
- if (!firstRaw.includes('w:val="Title"') || !secondRaw.includes('w:val="Subtitle"')) return null;
39486
- return ids;
39487
- }
39488
- const tmpl = requestedTemplate?.toLowerCase();
39489
- if (t0 === "MEMORANDUM" && tmpl === "memo") return ids;
39490
- if (t0 === "Letter" && tmpl === "letter") return ids;
39491
- if (t0 === "Proposal" && tmpl === "proposal") return ids;
39492
- if (t0 === "INVOICE" && tmpl === "invoice") return ids;
39493
- return null;
39494
- }
39495
- if (nodes.length === 4) {
39496
- const t0 = (nodes[0].text ?? "").trim();
39497
- const tmpl = requestedTemplate?.toLowerCase();
39498
- if (t0 === "Research Benchmark Report" && tmpl === "research") return ids;
39499
- if (t0 === "Corporate Strategy Roadmap" && tmpl === "corporate") return ids;
39500
- if (t0 === "Velocity Audit & Performance" && tmpl === "bar-chart-3d") return ids;
39501
- return null;
39502
- }
39503
- if (nodes.length === 5) {
39504
- const t0 = (nodes[0].text ?? "").trim();
39505
- const tmpl = requestedTemplate?.toLowerCase();
39506
- if (t0 === "Financial Report & Audit" && tmpl === "financial") return ids;
39507
- return null;
39508
- }
39509
- if (nodes.length === 6) {
39510
- const t0 = (nodes[0].text ?? "").trim();
39511
- const t2 = (nodes[2].text ?? "").trim();
39512
- const tmpl = requestedTemplate?.toLowerCase();
39513
- if (t0 === "Q3 Executive Financial Performance Report" && t2 === "1. Executive Summary") {
39514
- if (tmpl === "report") return ids;
39515
- return null;
39516
- }
39517
- if (t0 === "Manuscript Title" && t2 === "Chapter 1: The Beginning" && tmpl === "novel") {
39518
- return ids;
39519
- }
39520
- if (t0 === "Neural Document Architectures: A Comparative Benchmark" && tmpl === "academic") {
39521
- return ids;
39522
- }
39523
- if (t0 === "TECH HORIZONS" && tmpl === "newsletter") return ids;
39524
- if (t0 === "Platform Core API Reference" && (tmpl === "api" || tmpl === "api-spec")) return ids;
39525
- return null;
39526
- }
39527
- return null;
40057
+ function nextComposeIdPrefix(ast) {
40058
+ const live = /* @__PURE__ */ new Set();
40059
+ for (const section of ast.sections ?? []) for (const node of section.nodes ?? []) live.add(node.id);
40060
+ const used = (prefix) => {
40061
+ for (const id of live) if (id.startsWith(`${prefix}_`)) return true;
40062
+ return false;
40063
+ };
40064
+ if (!used("md")) return "md";
40065
+ for (let n = 2; ; n++) if (!used(`md${n}`)) return `md${n}`;
39528
40066
  }
39529
40067
  var pristineBlankSeedIds = pristineTemplateSeedIds;
39530
40068
  function assertTemplateSeed(parser2, template) {
@@ -39568,15 +40106,26 @@ function assertTemplateSeed(parser2, template) {
39568
40106
  function applyInlineSpans(parser2, ast, operations) {
39569
40107
  const docParser = parser2;
39570
40108
  const liveAst = ast;
40109
+ const targets = [];
39571
40110
  for (const op of operations) {
39572
40111
  const rec = op;
39573
- if (rec["action"] !== "insert" || rec["type"] !== "paragraph") continue;
39574
40112
  const params = rec["params"];
39575
- const rawSpans = params?.["mdSpans"];
39576
- if (!Array.isArray(rawSpans) || !segmentsNeedSplit(rawSpans)) continue;
39577
- const spans = rawSpans;
39578
40113
  const id = String(rec["id"] ?? "");
39579
40114
  if (!id) continue;
40115
+ if (rec["action"] === "callout") {
40116
+ const titleSpans = params?.["mdTitleSpans"];
40117
+ const bodySpans = params?.["mdBodySpans"];
40118
+ if (Array.isArray(titleSpans) && segmentsNeedSplit(titleSpans)) targets.push({ id, spans: titleSpans, part: "calloutTitle" });
40119
+ if (Array.isArray(bodySpans) && segmentsNeedSplit(bodySpans)) targets.push({ id, spans: bodySpans, part: "calloutBody" });
40120
+ continue;
40121
+ }
40122
+ if (rec["action"] !== "insert" || rec["type"] !== "paragraph") continue;
40123
+ const rawSpans = params?.["mdSpans"];
40124
+ if (!Array.isArray(rawSpans) || !segmentsNeedSplit(rawSpans)) continue;
40125
+ targets.push({ id, spans: rawSpans, part: "paragraph" });
40126
+ }
40127
+ for (const target of targets) {
40128
+ const { id, spans, part } = target;
39580
40129
  let node;
39581
40130
  for (const section of liveAst.sections) {
39582
40131
  const found = section.nodes.find((n) => n.id === id);
@@ -39590,7 +40139,18 @@ function applyInlineSpans(parser2, ast, operations) {
39590
40139
  `E_VALIDATION: line 1: span target paragraph '${id}' is missing after compose. Remedy: retry the compose without manual edits in between.`
39591
40140
  );
39592
40141
  }
39593
- const para = node.xml;
40142
+ let para = node.xml;
40143
+ let baseRPr;
40144
+ if (part !== "paragraph") {
40145
+ const cellParas = Array.from(para.getElementsByTagName("w:p"));
40146
+ const hasTitle = cellParas.length > 1;
40147
+ const picked = part === "calloutTitle" ? hasTitle ? cellParas[0] : void 0 : cellParas[cellParas.length - 1];
40148
+ if (!picked) continue;
40149
+ para = picked;
40150
+ const firstRun = Array.from(para.childNodes).find((k) => k.nodeType === 1 && k.tagName === "w:r");
40151
+ const rPrEl = firstRun ? Array.from(firstRun.childNodes).find((k) => k.nodeType === 1 && k.tagName === "w:rPr") : void 0;
40152
+ baseRPr = rPrEl ? rPrEl : void 0;
40153
+ }
39594
40154
  const doc = para.ownerDocument;
39595
40155
  if (!doc) continue;
39596
40156
  const keptFootRefs = [];
@@ -39622,10 +40182,12 @@ function applyInlineSpans(parser2, ast, operations) {
39622
40182
  }
39623
40183
  const buildRun = (seg) => {
39624
40184
  const run = createWordElement(doc, "w:r");
39625
- const needsRPr = seg.bold || seg.italic || seg.strike || seg.code || seg.linkUrl !== void 0;
40185
+ const needsRPr = seg.bold || seg.italic || seg.strike || seg.code || seg.linkUrl !== void 0 || baseRPr !== void 0;
39626
40186
  if (needsRPr) {
39627
40187
  const rPr = createWordElement(doc, "w:rPr");
39628
- if (seg.bold) rPr.appendChild(createWordElement(doc, "w:b"));
40188
+ const baseBold = baseRPr !== void 0 && baseRPr.getElementsByTagName("w:b").length > 0;
40189
+ const baseColor = baseRPr?.getElementsByTagName("w:color")[0]?.getAttribute("w:val") ?? void 0;
40190
+ if (seg.bold || baseBold) rPr.appendChild(createWordElement(doc, "w:b"));
39629
40191
  if (seg.italic) rPr.appendChild(createWordElement(doc, "w:i"));
39630
40192
  if (seg.strike) rPr.appendChild(createWordElement(doc, "w:strike"));
39631
40193
  if (seg.code) {
@@ -39634,6 +40196,11 @@ function applyInlineSpans(parser2, ast, operations) {
39634
40196
  rFonts.setAttribute("w:hAnsi", COMPOSE_CODE_FONT);
39635
40197
  rPr.appendChild(rFonts);
39636
40198
  }
40199
+ if (seg.linkUrl === void 0 && baseColor) {
40200
+ const color = createWordElement(doc, "w:color");
40201
+ color.setAttribute("w:val", baseColor);
40202
+ rPr.appendChild(color);
40203
+ }
39637
40204
  if (seg.linkUrl !== void 0) {
39638
40205
  const color = createWordElement(doc, "w:color");
39639
40206
  color.setAttribute("w:val", "0066CC");
@@ -39719,35 +40286,76 @@ function applyInlineSpans(parser2, ast, operations) {
39719
40286
  );
39720
40287
  for (let i = 0; i < refSibs.length; i++) {
39721
40288
  const refEl = refSibs[i];
39722
- const op2 = noteOps[i];
39723
- insertRunAtOffset(para, refEl, op2?.offset);
40289
+ const op = noteOps[i];
40290
+ insertRunAtOffset(para, refEl, op?.offset);
39724
40291
  }
39725
40292
  node.version += 1;
39726
40293
  }
39727
40294
  }
40295
+ function validateComposedOperations(operations) {
40296
+ validateOperationsStrict(
40297
+ operations.map((op) => {
40298
+ const rec = op;
40299
+ const params = rec["params"];
40300
+ if (rec["action"] !== "callout" || !params) return op;
40301
+ const { mdTitleSpans: _t, mdBodySpans: _b, ...publicParams } = params;
40302
+ void _t;
40303
+ void _b;
40304
+ const { params: _p, ...rest } = rec;
40305
+ void _p;
40306
+ return Object.keys(publicParams).length > 0 ? { ...rest, params: publicParams } : rest;
40307
+ })
40308
+ );
40309
+ }
39728
40310
  async function composeDocument(parser2, markdown, options) {
39729
40311
  if (!parser2 || typeof parser2.getAST !== "function") {
39730
40312
  throw new Error(
39731
40313
  "E_VALIDATION: line 1: composeDocument needs an open document parser. Remedy: open a document first, then compose into it."
39732
40314
  );
39733
40315
  }
40316
+ const liveAst = await parser2.getAST();
40317
+ const idPrefix = options?.idPrefix ?? nextComposeIdPrefix(liveAst);
39734
40318
  if (options?.template !== void 0) {
40319
+ if (idPrefix !== "md") {
40320
+ throw new Error(
40321
+ "E_VALIDATION: line 1: template is accepted only on the first compose of a document. Remedy: omit template on this compose; the document already holds composed content."
40322
+ );
40323
+ }
39735
40324
  assertTemplateSeed(parser2, String(options.template));
39736
40325
  }
40326
+ if (options?.position !== void 0 && options.anchor === void 0) {
40327
+ throw new Error(
40328
+ "E_VALIDATION: line 1: position needs an anchor. Remedy: pass anchor (a node id, start or end) together with position, or omit position to append at the end."
40329
+ );
40330
+ }
40331
+ if (options?.anchor !== void 0 && anchorAliasOf(options.anchor) === null) {
40332
+ const known = liveAst.sections.some((section) => section.nodes.some((node) => node.id === options.anchor));
40333
+ if (!known) {
40334
+ throw new Error(
40335
+ `E_VALIDATION: line 1: anchor '${options.anchor}' is not a node of this document. Remedy: pass a node id from inspect_document, or start or end.`
40336
+ );
40337
+ }
40338
+ }
39737
40339
  const styleGuide = resolveComposeStyleGuide(options?.styleGuide);
39738
40340
  const { operations, warnings: compileWarnings } = compileMarkdownDetailed(markdown, {
40341
+ idPrefix,
40342
+ ...options?.anchor !== void 0 ? { anchor: options.anchor } : {},
40343
+ ...options?.position !== void 0 ? { position: options.position } : {},
39739
40344
  allowedDir: options?.allowedDir,
39740
40345
  ...options?.styleGuide !== void 0 ? { styleGuide: options.styleGuide } : {},
39741
40346
  ...options?.dollarMath ? { dollarMath: true } : {}
39742
40347
  });
39743
- validateOperationsStrict(operations);
40348
+ validateComposedOperations(operations);
40349
+ if (operations.some((op) => op.action === "namedStyle" && op.styleName === QUOTE_STYLE_ID)) {
40350
+ ensureQuoteStyle(parser2.getStylesDom());
40351
+ }
39744
40352
  const chunkSize = options?.chunkSize !== void 0 ? Math.floor(options.chunkSize) : ENGINE_MAX_OPS;
39745
40353
  if (!Number.isInteger(chunkSize) || chunkSize < 1 || chunkSize > ENGINE_MAX_OPS) {
39746
40354
  throw new Error(
39747
40355
  `E_VALIDATION: line 1: chunkSize must be an integer in [1, ${ENGINE_MAX_OPS}]. Remedy: omit chunkSize or pass a value inside that range.`
39748
40356
  );
39749
40357
  }
39750
- const ast = await parser2.getAST();
40358
+ const ast = liveAst;
39751
40359
  const blankSeedIds = pristineBlankSeedIds(parser2, options?.template);
39752
40360
  let preBatchSnapshot = null;
39753
40361
  try {
@@ -39800,7 +40408,17 @@ async function composeDocument(parser2, markdown, options) {
39800
40408
  }
39801
40409
  base += chunk.length;
39802
40410
  }
39803
- if (!aggregate.success) return aggregate;
40411
+ if (!aggregate.success) {
40412
+ if (aggregate.appliedCount > 0 && preBatchSnapshot !== null) {
40413
+ try {
40414
+ parser2.restorePackageSnapshot(preBatchSnapshot);
40415
+ aggregate.appliedCount = 0;
40416
+ aggregate.rollbackTriggered = true;
40417
+ } catch {
40418
+ }
40419
+ }
40420
+ return aggregate;
40421
+ }
39804
40422
  if (blankSeedIds !== null) {
39805
40423
  const seedCleanup = await engine.executeBatch({
39806
40424
  operations: [{ action: "delete", target: blankSeedIds }],
@@ -39871,119 +40489,19 @@ async function composeDocument(parser2, markdown, options) {
39871
40489
  if (/^E_VALIDATION/.test(detail)) throw err;
39872
40490
  throw new Error(`E_VALIDATION: line 1: inline formatting failed (${detail}). Remedy: simplify bold, italic, code, or link spans and retry.`);
39873
40491
  }
39874
- return aggregate;
39875
- }
39876
-
39877
- // ../engine/src/stats.ts
39878
- function nearestOwner(el, tagName) {
39879
- let cur = el.parentNode;
39880
- while (cur && typeof cur === "object") {
39881
- const e = cur;
39882
- if (e.tagName === tagName) return e;
39883
- cur = e.parentNode;
39884
- }
39885
- return null;
39886
- }
39887
- function tableCellTextSpaced(node) {
39888
40492
  try {
39889
- const xml = node.xml;
39890
- if (!xml || typeof xml.getElementsByTagName !== "function") return void 0;
39891
- const tcs = xml.getElementsByTagName("w:tc");
39892
- if (!tcs || tcs.length === 0) return void 0;
39893
- const cells = [];
39894
- for (let i = 0; i < tcs.length; i++) {
39895
- const tc = tcs[i];
39896
- const paras = tc.getElementsByTagName("w:p");
39897
- if (paras.length > 0) {
39898
- const parts = [];
39899
- for (let p = 0; p < paras.length; p++) {
39900
- const para = paras[p];
39901
- if (nearestOwner(para, "w:tc") !== tc) continue;
39902
- const runs = para.getElementsByTagName("w:t");
39903
- let t = "";
39904
- for (let r = 0; r < runs.length; r++) {
39905
- const run = runs[r];
39906
- if (nearestOwner(run, "w:tc") !== tc) continue;
39907
- if (nearestOwner(run, "w:p") !== para) continue;
39908
- t += run.textContent || "";
39909
- }
39910
- if (t.trim().length > 0) parts.push(t.trim());
39911
- }
39912
- if (parts.length > 0) cells.push(parts.join(" "));
39913
- } else {
39914
- const runs = tc.getElementsByTagName("w:t");
39915
- let t = "";
39916
- for (let r = 0; r < runs.length; r++) {
39917
- const run = runs[r];
39918
- if (nearestOwner(run, "w:tc") !== tc) continue;
39919
- t += run.textContent || "";
39920
- }
39921
- if (t.trim().length > 0) cells.push(t.trim());
39922
- }
39923
- }
39924
- if (cells.length === 0) return "";
39925
- return cells.join(" ");
39926
- } catch {
39927
- return void 0;
39928
- }
39929
- }
39930
- function nestedTableCount(node) {
39931
- try {
39932
- const xml = node.xml;
39933
- if (!xml || typeof xml.getElementsByTagName !== "function") return 0;
39934
- return xml.getElementsByTagName("w:tbl").length;
39935
- } catch {
39936
- return 0;
39937
- }
39938
- }
39939
- function countExplicitPageBreaks(node) {
39940
- if (node.type === "pageBreak") return 1;
39941
- const text2 = node.metadata?.text || "";
39942
- let count = text2 === "[Page Break]" ? 1 : 0;
39943
- try {
39944
- const xml = node.xml;
39945
- if (xml && typeof xml.getElementsByTagName === "function") {
39946
- const brs = xml.getElementsByTagName("w:br");
39947
- let brCount = 0;
39948
- for (let i = 0; i < brs.length; i++) {
39949
- if (brs[i].getAttribute("w:type") === "page") brCount++;
39950
- }
39951
- if (brCount > count) count = brCount;
39952
- }
40493
+ const identity = applyDocumentIdentity(ast, parser2);
40494
+ if (identity.titleSetFrom) aggregate.titleSetFrom = identity.titleSetFrom;
39953
40495
  } catch {
39954
40496
  }
39955
- return count;
39956
- }
39957
- function computeDocStats(ast) {
39958
- let words = 0;
39959
- let paragraphs = 0;
39960
- let tables = 0;
39961
- let images = 0;
39962
- let pageBreaks = 0;
39963
- for (const sec of ast.sections) {
39964
- for (const node of sec.nodes) {
39965
- if (node.type === "paragraph") paragraphs += 1;
39966
- if (node.type === "table") tables += 1 + nestedTableCount(node);
39967
- if (node.type === "image") images += 1;
39968
- pageBreaks += countExplicitPageBreaks(node);
39969
- let text2 = node.metadata?.text ?? "";
39970
- if (node.type === "table") {
39971
- const spaced = tableCellTextSpaced(node);
39972
- if (spaced !== void 0) text2 = spaced;
39973
- }
39974
- const hits = text2.match(/\S+/g);
39975
- if (hits) words += hits.length;
39976
- }
39977
- }
39978
- const pageBudget = words / 450 + tables * 0.45 + images * 0.5 + pageBreaks;
39979
- return { words, paragraphs, pages: Math.max(1, Math.ceil(pageBudget)), tables, images };
40497
+ return aggregate;
39980
40498
  }
39981
40499
 
39982
40500
  // ../engine/src/style-guide/resolve.ts
39983
40501
  var import_fs8 = __toESM(require("fs"));
39984
40502
  var import_path7 = __toESM(require("path"));
39985
40503
  function hasScheme(ref) {
39986
- return /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(ref);
40504
+ return hasUriScheme(ref);
39987
40505
  }
39988
40506
  function failValidation(message, remedy) {
39989
40507
  throw toError("E_VALIDATION", message, remedy);
@@ -40166,7 +40684,13 @@ function mcpErrorFrom(err, fallbackCode, fallbackRemedy) {
40166
40684
  const rawMessage = err instanceof Error ? err.message : String(err);
40167
40685
  const message = rewritePhantomMcpToolNames(sanitizeMcpClientMessage(rawMessage));
40168
40686
  const remedy = err?.remedy && typeof err.remedy === "string" ? rewritePhantomMcpToolNames(String(err.remedy)) : rewritePhantomMcpToolNames(fallbackRemedy);
40169
- return { ok: false, code: resolved, message, remedy };
40687
+ const shape = { ok: false, code: resolved, message, remedy };
40688
+ const carried = err;
40689
+ if (carried && Array.isArray(carried.errors) && carried.errors.length > 0 && typeof carried.errorCount === "number") {
40690
+ shape.errorCount = carried.errorCount;
40691
+ shape.errors = carried.errors;
40692
+ }
40693
+ return shape;
40170
40694
  }
40171
40695
  function sanitizeMcpClientMessage(raw) {
40172
40696
  let text2 = typeof raw === "string" ? raw : String(raw ?? "");
@@ -40352,7 +40876,8 @@ function collectAstIdTexts(ast) {
40352
40876
  for (const n of sec.nodes ?? []) {
40353
40877
  if (typeof n.id !== "string" || !n.id) continue;
40354
40878
  order.push(n.id);
40355
- texts.set(n.id, String(n.metadata?.text ?? ""));
40879
+ const grid = n.type === "table" ? tableGridPlainText(n.xml) : void 0;
40880
+ texts.set(n.id, grid ?? String(n.metadata?.text ?? ""));
40356
40881
  }
40357
40882
  }
40358
40883
  } catch {
@@ -40662,6 +41187,25 @@ function descriptionOf(e) {
40662
41187
  function firstMessages(result, limit = 5) {
40663
41188
  return (result.errors ?? []).slice(0, limit).map((e) => descriptionOf(e).slice(0, 300));
40664
41189
  }
41190
+ var AUDIT_VALIDATION_ERRORS_MAX = 10;
41191
+ function summarizeValidationErrors(errors, max = AUDIT_VALIDATION_ERRORS_MAX) {
41192
+ const raw = Array.isArray(errors) ? errors.slice(0, max) : [];
41193
+ return raw.map((e) => {
41194
+ const err = e;
41195
+ const description = String(
41196
+ typeof err.description === "string" && err.description || typeof err.message === "string" && err.message || "OOXML validation error"
41197
+ ).slice(0, 500);
41198
+ const xp = typeof err.xPath === "string" ? err.xPath : typeof err.path === "string" ? err.path : void 0;
41199
+ return xp ? { description, xPath: xp } : { description };
41200
+ });
41201
+ }
41202
+ function toAuditValidation(validation, max = AUDIT_VALIDATION_ERRORS_MAX) {
41203
+ return {
41204
+ valid: validation.ok,
41205
+ errorCount: Array.isArray(validation.errors) ? validation.errors.length : 0,
41206
+ errors: summarizeValidationErrors(validation.errors, max)
41207
+ };
41208
+ }
40665
41209
  function introducedError(toolName, verdict) {
40666
41210
  const first = verdict.introduced[0];
40667
41211
  const desc = descriptionOf(first);
@@ -40670,7 +41214,12 @@ function introducedError(toolName, verdict) {
40670
41214
  new Error(
40671
41215
  `E_VALIDATION: ${toolName} blocked: this edit adds ${verdict.introduced.length} validator error(s) (the file already had ${verdict.preExisting}): ${desc.slice(0, 2e3)}`
40672
41216
  ),
40673
- { code: "E_VALIDATION", remedy: remedyForValidationError(xp, desc) }
41217
+ {
41218
+ code: "E_VALIDATION",
41219
+ remedy: remedyForValidationError(xp, desc),
41220
+ errorCount: verdict.introduced.length,
41221
+ errors: summarizeValidationErrors(verdict.introduced)
41222
+ }
40674
41223
  );
40675
41224
  }
40676
41225
  function validationField(result, verdict) {
@@ -40680,10 +41229,12 @@ function validationField(result, verdict) {
40680
41229
  tolerated: true,
40681
41230
  preExistingErrors: verdict.preExisting,
40682
41231
  introducedErrors: 0,
40683
- messages: firstMessages(result)
41232
+ messages: firstMessages(result),
41233
+ valid: false,
41234
+ errorCount: Array.isArray(result.errors) ? result.errors.length : verdict.preExisting
40684
41235
  };
40685
41236
  }
40686
- return { ok: true };
41237
+ return { ok: true, valid: true, errorCount: 0 };
40687
41238
  }
40688
41239
  function withToleratedNote(message, preExisting) {
40689
41240
  const note = ` (saved with ${preExisting} existing validator errors, 0 introduced)`;
@@ -40955,6 +41506,13 @@ function getMcpAllowedRoots() {
40955
41506
  }
40956
41507
  return [...new Set(roots)];
40957
41508
  }
41509
+ function describeAllowedFolders() {
41510
+ const named = getExistingExtraRoots();
41511
+ if (named.length > 0) {
41512
+ return `inside the folder${named.length > 1 ? "s" : ""} this server allows: ${[...new Set(named)].join(", ")}`;
41513
+ }
41514
+ return "inside ./documents or the system temp folder, or start the server with --allowed-dir <folder> to allow another folder";
41515
+ }
40958
41516
  setTrustedWriteRootsProvider(getMcpAllowedRoots);
40959
41517
  function getExistingExtraRoots() {
40960
41518
  const raw = (process.env.ASTRA_EXTRA_DOC_ROOTS || "").trim();
@@ -40993,7 +41551,7 @@ function assertNoDotSegments(canonical, root) {
40993
41551
  if (seg.startsWith(".")) {
40994
41552
  throw Object.assign(new Error(`Document path traverses a hidden directory "${seg}"`), {
40995
41553
  code: "E_SANDBOX",
40996
- remedy: "Use a non-hidden path inside documents/ or the system temp dir."
41554
+ remedy: `Use a path with no hidden (dot-prefixed) folder, ${describeAllowedFolders()}.`
40997
41555
  });
40998
41556
  }
40999
41557
  }
@@ -41035,7 +41593,7 @@ function validateDocPath(rawPath) {
41035
41593
  if (segments.includes("..")) {
41036
41594
  throw Object.assign(new Error('Document path contains forbidden ".." traversal'), {
41037
41595
  code: "E_SANDBOX",
41038
- remedy: "Use a path inside the workspace documents directory."
41596
+ remedy: `Use a path ${describeAllowedFolders()}.`
41039
41597
  });
41040
41598
  }
41041
41599
  const resolved = path12.resolve(trimmed);
@@ -41043,7 +41601,7 @@ function validateDocPath(rawPath) {
41043
41601
  if (resolvedSegments.includes("..")) {
41044
41602
  throw Object.assign(new Error("Document path resolves outside its directory"), {
41045
41603
  code: "E_SANDBOX",
41046
- remedy: "Use a path inside the workspace documents directory."
41604
+ remedy: `Use a path ${describeAllowedFolders()}.`
41047
41605
  });
41048
41606
  }
41049
41607
  const canonical = canonicalizeForAsset(resolved);
@@ -41672,12 +42230,17 @@ var PARITY_TOOL_NAMES = [
41672
42230
  var MCP_ECMA_ESSENTIALS = "ECMA-376 essentials: colors are hex without # (e.g. 4F46E5, never #4F46E5); font sizes are half-points in w:sz (e.g. 28 = 14pt, 22 = 11pt); spacing/margins/cellMar use twips dxa (twentieths of a point, e.g. 240 = 12pt, 180 = 9pt); borders use eighths-of-a-point sz (e.g. 32 = 4pt left accent).";
41673
42231
  var MCP_WORKFLOW = "Workflow: open_document -> inspect_document -> search_content -> execute_batch -> save_document -> close_document. Always inspect first, batch second, save third. Single edit = 1-item batch. Never call twice when once suffices.";
41674
42232
  var MCP_ENVELOPE_NOTE = "";
41675
- function withSharedDocs(body) {
41676
- return `${body} ${MCP_ECMA_ESSENTIALS} ${MCP_WORKFLOW} ${MCP_ENVELOPE_NOTE}`;
42233
+ function withSharedDocs(body, shared = {}) {
42234
+ const parts = [body.trimEnd()];
42235
+ if (shared.ecma) parts.push(MCP_ECMA_ESSENTIALS);
42236
+ if (shared.workflow) parts.push(MCP_WORKFLOW);
42237
+ if (MCP_ENVELOPE_NOTE) parts.push(MCP_ENVELOPE_NOTE);
42238
+ return parts.join(" ");
41677
42239
  }
41678
42240
  var LEAN6_DESCRIPTIONS = {
41679
42241
  open_document: withSharedDocs(
41680
- 'Open a .docx file into a session (Map sessionId->DocHandle, LRU 10 docs / 200MB / 30min idle evict). Returns {ok:true,sessionId,revision,summary,evicted} where evicted:string[] lists sessions dropped by the capacity cap to make room (re-open them if needed; later calls with an evicted id fail with E_SESSION plus a close_document remedy). The default summary is compact: palette, counts, header/footer text, chart ids and pending-revision flag. Pass {"summaryOnly":false} for the full structural summary including styles XML. Example input {"path":"documents/report.docx"} -> {"ok":true,"sessionId":"sess_abc","revision":1,"evicted":[]}. Open first; keep the sessionId for every later call. The response also carries validation: {status:"valid"}, {status:"invalid",preExistingErrors,messages,baselineComparable,note} when the file already has validator errors (edits that add no new error can still be saved), {status:"timed-out"} or {status:"skipped"}. The check waits validationTimeoutMs (default 5000 ms; 0 skips it; server flag --open-validation-timeout, env ASTRA_OPEN_VALIDATION_TIMEOUT_MS) and never refuses the open. Macro/active content refuses by default (E_VALIDATION naming the parts): pass {"allowMacros":true} to preserve or {"stripMacros":true} to strip (never both). Session ids are server-minted (sess_<uuid>, DEPRECATED to supply sessionId: client-supplied values are rejected with E_VALIDATION; deterministic ids are test-only via --allow-client-session-ids). IDs are session-scoped: valid only in the issuing session, must not be stored across sessions, and duplicate client-supplied ids are rejected.'
42242
+ 'Open a .docx file into a session (Map sessionId->DocHandle, LRU 10 docs / 200MB / 30min idle evict). Returns {ok:true,sessionId,revision,summary,evicted} where evicted:string[] lists sessions dropped by the capacity cap to make room (re-open them if needed; later calls with an evicted id fail with E_SESSION plus a close_document remedy). The default summary is compact: palette, counts, header/footer text, chart ids and pending-revision flag. Pass {"summaryOnly":false} for the full structural summary including styles XML. Example input {"path":"documents/report.docx"} -> {"ok":true,"sessionId":"sess_abc","revision":1,"evicted":[]}. Open first; keep the sessionId for every later call. The response also carries validation: {status:"valid"}, {status:"invalid",preExistingErrors,messages,baselineComparable,note} when the file already has validator errors (edits that add no new error can still be saved), {status:"timed-out"} or {status:"skipped"}. The check waits validationTimeoutMs (default 5000 ms; 0 skips it; server flag --open-validation-timeout, env ASTRA_OPEN_VALIDATION_TIMEOUT_MS) and never refuses the open. Macro/active content refuses by default (E_VALIDATION naming the parts): pass {"allowMacros":true} to preserve or {"stripMacros":true} to strip (never both). Session ids are server-minted (sess_<uuid>, DEPRECATED to supply sessionId: client-supplied values are rejected with E_VALIDATION; deterministic ids are test-only via --allow-client-session-ids). IDs are session-scoped: valid only in the issuing session, must not be stored across sessions, and duplicate client-supplied ids are rejected.',
42243
+ { workflow: true }
41681
42244
  ),
41682
42245
  inspect_document: withSharedDocs(
41683
42246
  'Inspect a session: standard TOON outline (fidelity compact|standard|full, default standard) plus style palette passthrough {availableStyles,brandColors,primaryFont} parsed from word/styles.xml, plus document core properties `metadata` {title,creator,subject,description,keywords,category,lastModifiedBy,revision,created,modified} read from docProps/core.xml (absent when the docx has no core part). Use include:["comments","revisions"] for review records paged by reviewOffset/reviewLimit (max 50; offset/limit page nodes only); commentId or revisionId reads full detail and needs its include. Renders the in-memory session state (includes unsaved execute_batch edits); `dirty:true` means the session differs from the last saved file, `revision` is the in-memory revision. Node ids are verbatim: paragraphs carry 8-hex w14:paraId (e.g. 3F2A9C1B), tables t_001.., sections sec_root, sec_2, sec_3.. by position; use them as-is for targets/anchors. A repeat paragraph sharing one native paraId reports a `<paraId>_dup` session id instead (e.g. 3F2A9C1B_dup): saving refuses until the repeat is deleted and its text reinserted for a fresh id. Tables render one row per table with grid cells as rNcM coordinates (r1c1 value | r1c2 value, rows joined by ;); merged spans are marked (spanN); merge covers are tagged hidden-cover (not directly editable, address the restart cell). Cell edits: the replace, style, namedStyle, clearFormat, hyperlink, bookmark and comment actions accept target "<tableId>:r<row>c<col>" (1-based, as the grid prints it; add ":p<n>" to pick one paragraph of a multi-paragraph cell). Other actions, including table, take the table id. Every row and column number counts from 1 (cell addresses, rowIndex, colIndex, columnIndex, row, col, index, afterIndex, fromRow/toRow/fromCol/toCol), so r3c2 and {rowIndex:3,colIndex:2} are the same cell. Chart nodes render style chart|chart3d with title + categories + series in the text ([Chart column] Title | cats: Q1,Q2 | ARR: 10,25), read from the chart part. Flag legend: [TBL] table [IMG] image/drawing [EQ] equation [CITE] citation [CMT] comment [TRK] tracked [LNK] link [BKMK] bookmark [L#] list level (1 is the top level; list ops count the same way) [+ADDED]/[~MODIFIED] diff; standard tier adds [B] bold [I] italic [C:hex] color [SZ:half-points] size [RS:name] run style [J:just] [N:num] numbering [HDR]/[ZEBRA] table look [CH:type]/[RID:id] chart. Palette lists every style id verbatim with no trim (large templates return the full list). 3-tier workflow for large docs: (1) macro-overview inspect (compact, first window) -> (2) search_content to find a target node id -> (3) local zoom via {aroundNode:"<id>",window:5} or page with {offset,limit}. Windowing opts: offset (min 0, default 0), limit (1-200, default 50), aroundNode + window (1-20, default 5) returning [node-w,node+w], maxTokens (200-8000, cuts on a clean row boundary, always reported via truncated/hasMore). aroundNode and offset/limit are mutually exclusive (use one or the other). Windowed renders preserve the requested tier; the TOON fidelity marker stays lossy by design (TOON is a lossy projection). Docs under ~2000 tokens return the full TOON (unchanged); larger docs default to the first ~50-node window {totalNodes,returnedNodes,offset,limit,hasMore,nextOffset,toonText} with a navigation remedy. Full fidelity requires sectionId; sectionId is a section id or a node id (a node id prefix needs 4 characters or more), and an id that matches nothing is an error that names the available section ids. Example {"sessionId":"sess_abc","fidelity":"standard"} -> TOON text + palette. Use the palette style names verbatim (e.g. Corporate Callout); never invent styles. When the session carries a style guide, the envelope also includes its compact styleGuide summary {name,headerFill,headerText,chartPalette,calloutTheme}. IDs are session-scoped: valid only in the issuing session, must not be stored across sessions, and duplicate client-supplied ids are rejected.'
@@ -41686,10 +42249,12 @@ var LEAN6_DESCRIPTIONS = {
41686
42249
  'Search live paragraph/table w:t text; returns [{id,preview}] node hits for anchors/targets. Use scope:"comments" or scope:"all" to search comment text; the default scope is body. At most 50 matches come back; truncated:true says more exist. A table hit also lists `cells` (up to 5 addresses such as t_001:r3c1) for the cells that hold the match; pass one as the target of replace/style/namedStyle/clearFormat/hyperlink/bookmark/comment. Chart titles/series are NOT matched by search (they live in the chart part, not w:t); discover them in inspect TOON chart rows, then anchor by the chart node id. Image alt text and footnote/endnote bodies are NOT matched either (search covers body w:t text only); find images via inspect [IMG] rows and footnotes via their anchor ids. Search extension to those parts is a later-program candidate, not this surface. Default is an exact literal substring (case-insensitive); pass {"regex":true} to interpret the query as a regular expression (bounded: pattern <=200 chars, ~500ms evaluation budget, case-insensitive). Caps: first 50 hits, 120-char previews. Literal queries containing regex metacharacters return literal results plus a hint that regex:true exists. Example {"sessionId":"sess_abc","query":"Executive Summary"} -> {"ok":true,"matches":[{"id":"3F2A9C1B","preview":"Executive ..."}]}. Search before editing when the anchor id is unknown.'
41687
42250
  ),
41688
42251
  execute_batch: withSharedDocs(
41689
- 'Apply 1..500 strictly-typed operations atomically (strict discriminatedUnion per action; sequential:true default preserves document order; Tracked edits return revisionIds; trackedChange accept/reject selects by ids, author or target and reports revisionResolution {resolved,unresolved,removedBookmarks,removedComments,staleLinks}. Comment operations include resolve, unresolve and delete and take an id as 3 or comment:3. single edit = 1-item batch). Caps 500 ops / 200MB throw E_QUOTA with remedy (maxSteps is an optional positive integer, unbounded when omitted). Client-supplied op ids are honored where the schema allows (insert/equation/footnote/chart accept `id`; strict schemas reject it elsewhere). Style ops support word-level spans: `match` styles only the matched substring (run splitting; `matchAll` styles every occurrence, `matchCase:false` matches case-insensitively); an absent match fails with E_NOT_FOUND. Returns {ok:true,appliedCount,revision,skippedIds,createdNodeIds,results:[{index,action,createdNodeIds}],revisionsPending,revisionsUnresolved?,revisionsRemedy?} where createdNodeIds After a trackedChange that left revisions it could not resolve, revisionsUnresolved counts them and revisionsRemedy lists kind, node and reason instead of repeating acceptAll. lists new node ids in document order (use them as anchors in the next batch without re-inspecting). A batch that drops a non-empty redo stack signals it (redoCleared:true + redoClearedWarning naming the depth): a fresh edit invalidates redo, so redo_document afterwards reports an empty stack. Example {"sessionId":"sess_abc","operations":[{"action":"replace","target":"p_001","text":"Hello"}],"sequential":true} -> {"ok":true,"appliedCount":1,"revision":2}. On failure returns {ok:false,code,remedy} with a fix-and-retry hint. Image inserts take exactly one source: {action:"image",operation:"insert",anchor,fileData} for inline base64 bytes, or {action:"image",operation:"insert",anchor,filePath} for a file read from a directory jail (both together fail with E_VALIDATION). The jail resolves explicit allowedDir first, else the session anchor (dirname of the open document); outside-jail refs fail with E_SANDBOX, missing files with E_IO, oversize files with E_LIMIT; batch reads nothing otherwise. Callout themes (operations/callout.ts THEMES): indigo|emerald|amber|slate|rose (default indigo; unknown names render indigo). Optional styleGuide (bundled guide name or guide .md path, jailed like filePath: explicit allowedDir else the session anchor; remote URLs fail closed) overrides the session guide for this batch only; it never replaces or persists (to re-skin the session use compose_document with styleGuide). Omitted inherits the session guide. Guides cannot be detached: an empty styleGuide fails with E_VALIDATION; open a new session to work without a guide. Unknown names fail with E_VALIDATION naming the available set. Guide parse notes ride both server stderr and the client-visible warnings array. Layout operations: {action:"section",operation:"setColumns",columns,spacing,separator} (columns 1-12, spacing in twips, separator draws the dividing rule), {action:"section",operation:"split"|"merge",...}, and {action:"pageSetup",orientation:"portrait"|"landscape",paperSize:"A4"|"Letter"|"Legal"|"A3",marginTop,marginBottom,marginLeft,marginRight,columns,sectionIndex} (margins in twips and written as given, never rotated; orientation turns the page by swapping its width and height; sectionIndex counts sections from 1 and limits the change to one section, omit it to change every section). Section breaks: {action:"insert",type:"sectionBreak",anchor,position,breakType:"nextPage"|"continuous"|"evenPage"|"oddPage"|"nextColumn"}; the section before the break keeps the page setup and headers of the section it splits. Headers and footers: {action:"headerFooter",operation:"set",targetType:"header"|"footer",text,alignment,sectionIndex (from 1, default the first section)}; {PAGE} and {NUMPAGES} inside text become live page-number fields. Images: width and height are integers in EMU (914400 per inch, 12700 per point, 9525 per pixel at 96 dpi); give one and the other follows the image aspect ratio, give neither and the image keeps its own size fitted to the page. Tables created by insert type:"table" or by Markdown repeat their header row across pages by default. Equations: {action:"equation",operation:"insert"|"replace",latex,mode:"auto"|"latex"|"plain"}; LaTeX that contains a backslash, ^ or _ becomes a real Word equation, and mode:"plain" keeps the text exactly as written. IDs are session-scoped: valid only in the issuing session, must not be stored across sessions, and duplicate client-supplied ids are rejected.'
42252
+ 'Apply 1..500 strictly-typed operations atomically (strict discriminatedUnion per action; sequential:true default preserves document order; Tracked edits return revisionIds; trackedChange accept/reject selects by ids, author or target and reports revisionResolution {resolved,unresolved,removedBookmarks,removedComments,staleLinks}. Comment operations include resolve, unresolve and delete and take an id as 3 or comment:3. single edit = 1-item batch). Caps 500 ops / 200MB throw E_QUOTA with remedy (maxSteps is an optional positive integer, unbounded when omitted). Client-supplied op ids are honored where the schema allows (insert/equation/footnote/chart accept `id`; strict schemas reject it elsewhere). Style ops support word-level spans: `match` styles only the matched substring (run splitting; `matchAll` styles every occurrence, `matchCase:false` matches case-insensitively); an absent match fails with E_NOT_FOUND. Returns {ok:true,appliedCount,revision,skippedIds,createdNodeIds,results:[{index,action,createdNodeIds}],revisionsPending,revisionsUnresolved?,revisionsRemedy?} where createdNodeIds After a trackedChange that left revisions it could not resolve, revisionsUnresolved counts them and revisionsRemedy lists kind, node and reason instead of repeating acceptAll. lists new node ids in document order (use them as anchors in the next batch without re-inspecting). A batch that drops a non-empty redo stack signals it (redoCleared:true + redoClearedWarning naming the depth): a fresh edit invalidates redo, so redo_document afterwards reports an empty stack. Example {"sessionId":"sess_abc","operations":[{"action":"replace","target":"p_001","text":"Hello"}],"sequential":true} -> {"ok":true,"appliedCount":1,"revision":2}. On failure returns {ok:false,code,remedy} with a fix-and-retry hint. Image inserts take exactly one source: {action:"image",operation:"insert",anchor,fileData} for inline base64 bytes, or {action:"image",operation:"insert",anchor,filePath} for a file read from a directory jail (both together fail with E_VALIDATION). The jail resolves explicit allowedDir first, else the session anchor (dirname of the open document); outside-jail refs fail with E_SANDBOX, missing files with E_IO, oversize files with E_LIMIT; batch reads nothing otherwise. Callout themes (operations/callout.ts THEMES): indigo|emerald|amber|slate|rose (default indigo; unknown names render indigo). Optional styleGuide (bundled guide name or guide .md path, jailed like filePath: explicit allowedDir else the session anchor; remote URLs fail closed) overrides the session guide for this batch only; it never replaces or persists (to re-skin the session use compose_document with styleGuide). Omitted inherits the session guide. Guides cannot be detached: an empty styleGuide fails with E_VALIDATION; open a new session to work without a guide. Unknown names fail with E_VALIDATION naming the available set. Guide parse notes ride both server stderr and the client-visible warnings array. Layout operations: {action:"section",operation:"setColumns",columns,spacing,separator} (columns 1-12, spacing in twips, separator draws the dividing rule), {action:"section",operation:"split"|"merge",...}, and {action:"pageSetup",orientation:"portrait"|"landscape",paperSize:"A4"|"Letter"|"Legal"|"A3",marginTop,marginBottom,marginLeft,marginRight,columns,sectionIndex} (margins in twips and written as given, never rotated; orientation turns the page by swapping its width and height; sectionIndex counts sections from 1 and limits the change to one section, omit it to change every section). Section breaks: {action:"insert",type:"sectionBreak",anchor,position,breakType:"nextPage"|"continuous"|"evenPage"|"oddPage"|"nextColumn"}; the section before the break keeps the page setup and headers of the section it splits. Headers and footers: {action:"headerFooter",operation:"set",targetType:"header"|"footer",text,alignment,sectionIndex (from 1, default the first section)}; {PAGE} and {NUMPAGES} inside text become live page-number fields. Images: width and height are integers in EMU (914400 per inch, 12700 per point, 9525 per pixel at 96 dpi); give one and the other follows the image aspect ratio, give neither and the image keeps its own size fitted to the page. Tables created by insert type:"table" or by Markdown repeat their header row across pages by default. Equations: {action:"equation",operation:"insert"|"replace",latex,mode:"auto"|"latex"|"plain"}; LaTeX that contains a backslash, ^ or _ becomes a real Word equation, and mode:"plain" keeps the text exactly as written. IDs are session-scoped: valid only in the issuing session, must not be stored across sessions, and duplicate client-supplied ids are rejected.',
42253
+ { workflow: true, ecma: true }
41690
42254
  ),
41691
42255
  save_document: withSharedDocs(
41692
- `Validate then persist the session docx; the default policy permits pre-existing validation errors when no new normalized error signatures are detected. Save-blocked remedies are xpath-specific (drawing docPr/id, table tbl/tr/tc, relationships, header/footer parts, session tracking-attribute leaks each name their own fix). Optional commit params: message (commit subject, single-line, max 500 chars; newlines become spaces; blank/omitted falls back to \`save_document: rev <n> (<k> ops since last save)\`); author (max 100 chars; blank/omitted falls back to ASTra Agent). Over-long message/author fail at the handler with E_VALIDATION plus a shorten-and-retry remedy (never a bare MCP -32602: caps are enforced in the handler, not the wire schema). A successful save clears the session dirty flag (revision is re-synced to the file on disk). A file that already had validator errors when it was opened is saved when the edit adds none: the envelope then carries validation:{ok:false,tolerated:true,preExistingErrors,introducedErrors:0,messages}. An edit that adds an error is refused with E_VALIDATION naming both numbers. Pass {"strict":true} (or start the server with --strict-validation, or set ASTRA_STRICT_VALIDATION=1) to refuse every validator error as before. Paragraphs that share a w14:paraId are renumbered by the save: the first keeps its id, each later copy gets a new one and is listed in normalized, and the state before is recorded in the history (revert_document returns it); strict refuses such a document instead. The success envelope also carries revisionsPending (every revision element left in the saved package; a move counts as each of its markers) plus a revisionsRemedy when >0. The success envelope also carries advisory changedValues [{nodeId,before,after}] (old saved text -> new live text, bounded: at most ${CHANGED_VALUES_MAX} entries, each side at most ${CHANGED_VALUE_TEXT_MAX} chars, truncated flag honest; empty + reason when no baseline is computable). When the session carries a style guide (new_document/compose_document styleGuide), the envelope also carries advisory styleWarnings [{nodeId,rule,message}] (table contrast, chart duplicate colors, split-table orphans), never blocking. Example {"sessionId":"sess_abc"} -> {"ok":true,"path":"documents/report.docx"}. Always save after execute_batch; close afterwards. IDs are session-scoped: valid only in the issuing session, must not be stored across sessions, and duplicate client-supplied ids are rejected.`
42256
+ `Validate then persist the session docx; the default policy permits pre-existing validation errors when no new normalized error signatures are detected. Save-blocked remedies are xpath-specific (drawing docPr/id, table tbl/tr/tc, relationships, header/footer parts, session tracking-attribute leaks each name their own fix). Optional commit params: message (commit subject, single-line, max 500 chars; newlines become spaces; blank/omitted falls back to \`save_document: rev <n> (<k> ops since last save)\`); author (max 100 chars; blank/omitted falls back to ASTra Agent). Over-long message/author fail at the handler with E_VALIDATION plus a shorten-and-retry remedy (never a bare MCP -32602: caps are enforced in the handler, not the wire schema). A successful save clears the session dirty flag (revision is re-synced to the file on disk). When dc:title is still a template placeholder ("Blank Document", "Report") the save writes it from the cover title, the first Title or the first Heading 1 and reports title and titleSetFrom; a title already set is never changed. A file that already had validator errors when it was opened is saved when the edit adds none: the envelope then carries validation:{ok:false,valid:false,errorCount,tolerated:true,preExistingErrors,introducedErrors:0,messages}; a clean save carries validation:{ok:true,valid:true,errorCount:0}. An edit that adds an error is refused with E_VALIDATION naming both numbers, with errorCount and the first 10 errors as errors[{description,xPath}] (validate_document lists them all). Pass {"strict":true} (or start the server with --strict-validation, or set ASTRA_STRICT_VALIDATION=1) to refuse every validator error as before. Paragraphs that share a w14:paraId are renumbered by the save: the first keeps its id, each later copy gets a new one and is listed in normalized, and the state before is recorded in the history (revert_document returns it); strict refuses such a document instead. The success envelope also carries revisionsPending (every revision element left in the saved package; a move counts as each of its markers) plus a revisionsRemedy when >0. The success envelope also carries advisory changedValues [{nodeId,before,after}] (old saved text -> new live text, bounded: at most ${CHANGED_VALUES_MAX} entries, each side at most ${CHANGED_VALUE_TEXT_MAX} chars, truncated flag honest; empty + reason when no baseline is computable). When the session carries a style guide (new_document/compose_document styleGuide), the envelope also carries advisory styleWarnings [{nodeId,rule,message}] (table contrast, chart duplicate colors, split-table orphans), never blocking. Example {"sessionId":"sess_abc"} -> {"ok":true,"path":"documents/report.docx"}. Always save after execute_batch; close afterwards. IDs are session-scoped: valid only in the issuing session, must not be stored across sessions, and duplicate client-supplied ids are rejected.`,
42257
+ { workflow: true }
41693
42258
  ),
41694
42259
  close_document: withSharedDocs(
41695
42260
  'Evict a session and release its lock/memory. Returns {ok:true,sessionId,dirty,discardedRevisions,warning?}: a dirty close warns (dirty:true + warning naming the discarded unsaved revision count) instead of silently discarding; save first when the warning matters. Example {"sessionId":"sess_abc"} -> {"ok":true,"dirty":false,"discardedRevisions":0}. Close when finished; expired sessions (30min idle) must be re-opened.'
@@ -41697,7 +42262,7 @@ var LEAN6_DESCRIPTIONS = {
41697
42262
  };
41698
42263
  var GIT_HISTORY_DESCRIPTIONS = {
41699
42264
  get_document_history: withSharedDocs(
41700
- 'List the saved git history for a session (newest first, read-only: never mutates, never flips dirty). Paginate with limit (1-100, default 20) and offset (default 0). Returns {ok:true,sessionId,documentPath,currentCommit,totalCommits,history:[{oid,message,author,timestamp,timestampMs,opCount,affectedNodeIds,resultingHeadOid}]} (timestampMs = ms-precision epoch millis; commit-time clock for tracked commits, git seconds for older ones; opCount is always present (0 for pre-tracking commits and heals-only saves: it folds operations, not normalizer rewrites); affectedNodeIds is always present ([] when untracked); resultingHeadOid always equals oid (append-only; refs never move). Example {"sessionId":"sess_abc","limit":10} -> history slice. Use the oids with revert_document (or preview_revert first) to restore an earlier saved state. IDs are session-scoped: valid only in the issuing session, must not be stored across sessions, and duplicate client-supplied ids are rejected.'
42265
+ 'List the saved git history for a session (newest first, read-only: never mutates, never flips dirty). Paginate with limit (1-100, default 20) and offset (default 0). Returns {ok:true,sessionId,documentPath,currentCommit,totalCommits,history:[{oid,message,author,timestamp,timestampMs,opCount,affectedNodeIds,resultingHeadOid}]} (timestampMs = ms-precision epoch millis; commit-time clock for tracked commits, git seconds for older ones; opCount is always present (0 for pre-tracking commits and heals-only saves: it folds operations, not normalizer rewrites); affectedNodeIds is always present ([] when untracked); resultingHeadOid always equals oid (append-only; refs never move). Example {"sessionId":"sess_abc","limit":10} -> history slice. Use the oids with revert_document (or preview_revert first) to restore an earlier saved state. '
41701
42266
  ),
41702
42267
  preview_revert: withSharedDocs(
41703
42268
  `Read-only WOULD-restore preview for revert_document (never mutates: no revision bump, no disk write, no redo-stack change). Target one commit: {steps:N} walks back N saved commits from the head (1 = the previous saved commit, must be < totalCommits) OR {commitOid:"<40-hex>"} jumps to that commit (mutually exclusive). Returns {ok:true,sessionId,targetOid,targetMessage,targetAuthor,stepsBack,affectedFiles,changedValues,changedValuesTruncated,changedValuesPositional,dirty,dirtyHandling,currentHeadOid,totalCommits} where affectedFiles lists package paths that differ from the target (word/document.xml plus tracked extras). The preview also carries advisory changedValues [{nodeId,before,after}] (live current text -> target restore text, bounded: at most ${CHANGED_VALUES_MAX} entries, each side at most ${CHANGED_VALUE_TEXT_MAX} chars, truncated flag honest; changedValuesPositional:true means either side held orphan ids (no shared id across the boundary, common under duplicate native ids) so before/after pairing is positional, not identity; empty + changedValuesReason when the target range is not computable). Example {"sessionId":"sess_abc","steps":1} -> preview summary. Preview first when a revert would discard unsaved edits.`
@@ -41714,19 +42279,19 @@ var EXPORT_AUDIT_TRAIL_DESCRIPTION = withSharedDocs(
41714
42279
  );
41715
42280
  var PARITY_DESCRIPTIONS = {
41716
42281
  validate_document: withSharedDocs(
41717
- 'Preflight read-only OOXML audit for a session: runs the same validator gate save_document enforces, before anything is written. Detects schema violations while they are still cheap to fix. Strictly read-only: zero session mutation (revision and dirty flag untouched), zero disk writes, zero history commits. Returns {ok:true,sessionId,revision,dirty,validation:{valid,errorCount,errors}} where errors carry description + xPath (first 10, same shape as the save-blocked envelope). The verdict is on the document as a save would write it (children put in schema order first); when that save would repair something the result also carries pendingNormalizations (a list of repairs, absent when none), so a valid verdict is not mistaken for "valid exactly as it stands". Use before save_document when the package may be non-conforming; save_document re-gates before writing. Example {"sessionId":"sess_abc"} -> {"ok":true,"validation":{"valid":true,"errorCount":0,"errors":[]}}. IDs are session-scoped: valid only in the issuing session, must not be stored across sessions.'
42282
+ 'Preflight read-only OOXML audit for a session: runs the same validator gate save_document enforces, before anything is written. Detects schema violations while they are still cheap to fix. Strictly read-only: zero session mutation (revision and dirty flag untouched), zero disk writes, zero history commits. Returns {ok:true,sessionId,revision,dirty,validation:{valid,errorCount,errors}} where errors carry description + xPath (first 10, same shape as the save-blocked envelope). The verdict is on the document as a save would write it (children put in schema order first); when that save would repair something the result also carries pendingNormalizations (a list of repairs, absent when none), so a valid verdict is not mistaken for "valid exactly as it stands". Use before save_document when the package may be non-conforming; save_document re-gates before writing. Example {"sessionId":"sess_abc"} -> {"ok":true,"validation":{"valid":true,"errorCount":0,"errors":[]}}. '
41718
42283
  ),
41719
42284
  get_stats: withSharedDocs(
41720
- 'Token-cheap document census for a session: words, paragraphs, tables, images, and an estimated editorial page budget (layout-weighted by words, tables, images, and explicit page breaks) in a single small envelope. Counters are the exact engine counters behind CLI stats, so both surfaces always agree. Physical pagination is finalized when exporting to PDF via LibreOffice. Read-only: never mutates, never bumps the revision. Returns {ok:true,sessionId,revision,dirty,words,paragraphs,tables,images,pages}. Use instead of inspect_document when only counts are needed (a full TOON dump costs thousands of tokens for the same question). Example {"sessionId":"sess_abc"} -> {"ok":true,"words":42,"paragraphs":5}. IDs are session-scoped: valid only in the issuing session, must not be stored across sessions.'
42285
+ 'Token-cheap document census for a session: words, paragraphs, tables, callouts, images, and an estimated editorial page budget (layout-weighted by words, tables, images, and explicit page breaks) in a single small envelope. Counters are the exact engine counters behind CLI stats, so both surfaces always agree. Physical pagination is finalized when exporting to PDF via LibreOffice. Read-only: never mutates, never bumps the revision. Returns {ok:true,sessionId,revision,dirty,words,paragraphs,tables,callouts,images,pages}. A callout (a "> [!NOTE]" alert or a callout op: a one-cell table with a fill) is counted in callouts and not in tables. Use instead of inspect_document when only counts are needed (a full TOON dump costs thousands of tokens for the same question). Example {"sessionId":"sess_abc"} -> {"ok":true,"words":42,"paragraphs":5}. '
41721
42286
  ),
41722
42287
  sanitize_document: withSharedDocs(
41723
- `Scrub an open session document. By default it removes revision ids, authorship, embedded EXIF and hidden text PII. Tracked changes and comments are kept, with the text of every tracked deletion, unless you ask: acceptAllRevisions:true accepts all tracked changes (deleted text is removed) and stripComments:true removes comments. The CLI presets (bare sanitize, --client-safe, --metadata-only, --redact-pii) turn both on; this tool does not. Macro/active parts are removed on request. It uses the same engine scrub path as CLI sanitize. Body text is kept. Options mirror the engine scrub keys 1:1 (zeroRetention, purgeRSIDs, purgeAuthors, purgeExif, purgePII, acceptAllRevisions, stripComments, replacementAuthor, allowMacros, stripMacros); omitted options take engine defaults. With acceptAllRevisions it returns E_VALIDATION and no file when revision markup remains that cannot be resolved. allowMacros and stripMacros together fail with E_VALIDATION. ${piiCoverageSentence()} Returns {ok:true,sessionId,revision,dirty,removedRSIDs,purgedAuthors,strippedExifBytes,purgedPII,bodyPII,piiCoverage,acceptedRevisions,strippedComments,strippedMacros,macroParts,filesTouched,droppedRogue,...} plus the standard redoCleared signal when a redo chain is dropped. Example {"sessionId":"sess_abc","stripMacros":true} -> scrubbed session at a new revision (save after). IDs are session-scoped: valid only in the issuing session, must not be stored across sessions.`
42288
+ `Scrub an open session document. By default it removes revision ids, authorship, embedded EXIF and hidden text PII. Tracked changes and comments are kept, with the text of every tracked deletion, unless you ask: acceptAllRevisions:true accepts all tracked changes (deleted text is removed) and stripComments:true removes comments. The CLI presets (bare sanitize, --client-safe, --metadata-only, --redact-pii) turn both on; this tool does not. Macro/active parts are removed on request. It uses the same engine scrub path as CLI sanitize. Body text is kept. Options mirror the engine scrub keys 1:1 (zeroRetention, purgeRSIDs, purgeAuthors, purgeExif, purgePII, acceptAllRevisions, stripComments, replacementAuthor, allowMacros, stripMacros); omitted options take engine defaults. With acceptAllRevisions it returns E_VALIDATION and no file when revision markup remains that cannot be resolved. allowMacros and stripMacros together fail with E_VALIDATION. ${piiCoverageSentence()} Returns {ok:true,sessionId,revision,dirty,removedRSIDs,purgedAuthors,strippedExifBytes,purgedPII,bodyPII,piiCoverage,acceptedRevisions,strippedComments,strippedMacros,macroParts,filesTouched,droppedRogue,...} plus the standard redoCleared signal when a redo chain is dropped. Example {"sessionId":"sess_abc","stripMacros":true} -> scrubbed session at a new revision (save after). `
41724
42289
  ),
41725
42290
  normalize_document: withSharedDocs(
41726
- 'Canonical repair for an open session document: runs the same allowlist normalizer CLI normalize calls (verified tag renames, whitelisted default infills, schema-sequence sorts) over the live package, then refreshes the session view and proves the result with a read-only validation verdict. Output bytes match CLI normalize on the same input. When the package is still invalid afterwards, the session is restored untouched and the call fails with E_VALIDATION plus a fix-and-retry remedy (same contract as CLI normalize writing nothing on failure). After the normalizer it repairs known validator error kinds one rule at a time; a rule that adds a validator error or changes the visible text is dropped and listed in rolledBack. Returns {ok:true,sessionId,revision,dirty,normalized,remaining,rolledBack,validation:{valid:true,...}} where normalized lists every rewrite once and remaining counts the validator errors left (-1 when the validator did not run), plus the standard redoCleared signal when a redo chain is dropped. Example {"sessionId":"sess_abc"} -> {"ok":true,"normalized":[...]}. Save after to persist. IDs are session-scoped: valid only in the issuing session, must not be stored across sessions.'
42291
+ 'Canonical repair for an open session document: runs the same allowlist normalizer CLI normalize calls (verified tag renames, whitelisted default infills, schema-sequence sorts) over the live package, then refreshes the session view and proves the result with a read-only validation verdict. Output bytes match CLI normalize on the same input. When the package is still invalid afterwards, the session is restored untouched and the call fails with E_VALIDATION plus a fix-and-retry remedy (same contract as CLI normalize writing nothing on failure). After the normalizer it repairs known validator error kinds one rule at a time; a rule that adds a validator error or changes the visible text is dropped and listed in rolledBack. Returns {ok:true,sessionId,revision,dirty,normalized,remaining,rolledBack,validation:{valid:true,...}} where normalized lists every rewrite once and remaining counts the validator errors left (-1 when the validator did not run), plus the standard redoCleared signal when a redo chain is dropped. Example {"sessionId":"sess_abc"} -> {"ok":true,"normalized":[...]}. Save after to persist. '
41727
42292
  ),
41728
42293
  import_document: withSharedDocs(
41729
- 'Ingest an on-disk Markdown file into the OPEN session (the file-ingress half compose_document lacks: compose takes an inline string only). Reads the file under the workspace jail, then runs the identical compose pipeline (strict compile gate, quotas, template-seed check, inline-span splitting), so the result is byte-equal to composing the same Markdown inline. sessionId is strictly required: the tool never auto-creates sessions (fresh templated document: new_document first, then import; existing file: open_document first, then import). Accepts .md/.markdown/.txt; .html/.htm refuse with E_VALIDATION (convert to Markdown first, same verdict as CLI import); .docx refuses with E_VALIDATION naming open_document (open .docx files directly). Outside-jail reads fail E_SANDBOX, missing files E_IO, oversize files E_LIMIT. Optional allowedDir (image refs), template (report|memo|blank seed check), and styleGuide (same attach semantics as compose_document). Optional dollarMath (default false) treats $...$ as inline math; off by default because prices and shell text can look like math. Example {"sessionId":"sess_abc","path":"documents/notes.md"} -> compose envelope. IDs are session-scoped: valid only in the issuing session, must not be stored across sessions.'
42294
+ 'Ingest an on-disk Markdown file into the OPEN session (the file-ingress half compose_document lacks: compose takes an inline string only). Reads the file under the workspace jail, then runs the identical compose pipeline (strict compile gate, quotas, template-seed check, inline-span splitting), so the result is byte-equal to composing the same Markdown inline. sessionId is strictly required: the tool never auto-creates sessions (fresh templated document: new_document first, then import; existing file: open_document first, then import). Accepts .md/.markdown/.txt; .html/.htm refuse with E_VALIDATION (convert to Markdown first, same verdict as CLI import); .docx refuses with E_VALIDATION naming open_document (open .docx files directly). Outside-jail reads fail E_SANDBOX, missing files E_IO, oversize files E_LIMIT. Optional allowedDir (image refs), template (report|memo|blank seed check), and styleGuide (same attach semantics as compose_document). Optional dollarMath (default false) treats $...$ as inline math; off by default because prices and shell text can look like math. Example {"sessionId":"sess_abc","path":"documents/notes.md"} -> compose envelope. '
41730
42295
  )
41731
42296
  };
41732
42297
 
@@ -41768,7 +42333,7 @@ var InspectDocumentSchema = import_zod4.z.object({
41768
42333
  // defaults (offset 0, limit 50, window 5) are applied manually in
41769
42334
  // handleInspectDocument AFTER the explicit-presence check below.
41770
42335
  offset: import_zod4.z.number().int().min(0).optional(),
41771
- limit: import_zod4.z.number().int().min(1).max(200).optional(),
42336
+ limit: import_zod4.z.number().int().min(1).max(200, "limit is at most 200 per call; use offset and nextOffset to read more").optional(),
41772
42337
  aroundNode: import_zod4.z.string().min(1).max(256).optional(),
41773
42338
  window: import_zod4.z.number().int().min(1).max(20).optional(),
41774
42339
  maxTokens: import_zod4.z.number().int().min(200).max(8e3).optional(),
@@ -41776,7 +42341,7 @@ var InspectDocumentSchema = import_zod4.z.object({
41776
42341
  // FX-13: review records (include) page on their own window, so they never move the node window.
41777
42342
  // reviewLimit above 50 is clamped to 50 by the handler, as limit was before.
41778
42343
  reviewOffset: import_zod4.z.number().int().min(0).optional(),
41779
- reviewLimit: import_zod4.z.number().int().min(1).max(200).optional(),
42344
+ reviewLimit: import_zod4.z.number().int().min(1).max(200, "reviewLimit is at most 200 per call; use reviewOffset to read more").optional(),
41780
42345
  commentId: import_zod4.z.number().int().nonnegative().optional(),
41781
42346
  revisionId: import_zod4.z.number().int().nonnegative().optional()
41782
42347
  }).strict();
@@ -42213,7 +42778,7 @@ var import_zod5 = require("zod");
42213
42778
  var COMPOSE_MARKDOWN_MAX = 5e5;
42214
42779
  var COMPOSE_VALIDATION_ERRORS_MAX = 3;
42215
42780
  function hasGuideScheme(ref) {
42216
- return /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(ref.trim());
42781
+ return hasUriScheme(ref.trim());
42217
42782
  }
42218
42783
  function guideSandboxError(message, remedy) {
42219
42784
  return Object.assign(new Error(message), { code: "E_SANDBOX", remedy });
@@ -42251,9 +42816,13 @@ var ComposeDocumentSchema = import_zod5.z.object({
42251
42816
  // on purpose so the hint (not a bare schema error) reaches the caller.
42252
42817
  styleGuide: import_zod5.z.string().max(1024).optional(),
42253
42818
  // Read $...$ as inline math (off by default: prices and shell text can look like math).
42254
- dollarMath: import_zod5.z.boolean().optional()
42819
+ dollarMath: import_zod5.z.boolean().optional(),
42820
+ // Placement of a later compose: a node id, start or end. Absent = append after the last node.
42821
+ anchor: import_zod5.z.string().min(1).max(256).optional(),
42822
+ // Side of anchor the first block goes on (default after). Needs anchor.
42823
+ position: import_zod5.z.enum(["before", "after"]).optional()
42255
42824
  }).strict();
42256
- var COMPOSE_DOCUMENT_DESCRIPTION = 'Compose a Markdown string into the open session document (appends after existing content, except an unchanged built-in template created in this session has its sample content replaced). Runs the engine compose path, then applies the resulting operations through the same execute_batch path (strict schema gate, per-chunk quotas, structural checks, per-chunk atomic rollback, then inline-span run splitting), so every gate a batch meets applies here unchanged. Supported input: #/##/### headings (real Heading1..3 styles), blank-line paragraphs (single newlines join with a space), flat bullet (-, *, +) and ordered (1., 1)) lists with real numbering, GitHub-style tables with a header separator row (header row shaded and repeated), --- dividers, fenced or indented code blocks (monospace, line breaks kept), [^label] footnotes with definitions, inline spans (**bold**, *italic*, `code`, [text](url)) applied as runs with real hyperlink targets, and standalone block images/charts (![alt](path)) resolved from allowedDir. Out-of-scope input fails closed with E_VALIDATION plus a 1-based line number and writes nothing: raw HTML (except [!NOTE]-style alert callouts, which compile to callout containers), custom styles, general blockquotes, deeper headings, nested lists or tables, task items. Block image/chart refs need allowedDir pointing at the folder holding the files (refs stay jailed inside it; without allowedDir they fail closed); remote URL refs fail closed; inline data-URL block images bypass allowedDir by design (bytes decode in memory through the same image gates). Sources compiling past 500 operations apply as sequential 500-operation chunks; each chunk is atomic, but a later-chunk failure leaves earlier chunks applied (same exposure as any multi-batch caller). Compiled node ids are deterministic per source, so a second compose into the same session is rejected by the duplicate-id gate (compose once per session with the full source). Optional template (report|memo|blank) requires the open document to still hold that template seed, otherwise composition fails before any write (for a fresh templated document call new_document first, then compose_document with template), and placement is append after existing content except for an unchanged built-in template created in this session. Reopened documents and external templates keep existing content. Returns {ok:true,opCount,appliedCount,revision,createdNodeIds,results,validation:{valid,errorCount,errors}} where createdNodeIds lists new node ids in document order (use them as anchors without re-inspecting) and validation proves the post-compose package state via a read-only OOXML check (save_document re-gates before writing). A compose that drops a non-empty redo stack signals it (redoCleared:true + redoClearedWarning naming the depth): a fresh edit invalidates redo. Optional styleGuide (bundled guide name or guide .md path, jailed like image refs: explicit allowedDir else the session anchor; remote URLs fail closed) replaces the session guide (not a one-batch override: later batches inherit it until another compose replaces it) and skins this compose (table/header-fill, chart-palette, callout, and code defaults apply where ops omit params; explicit params still win). Guides cannot be detached: open a new session to work without a guide. With no guide, compose uses the built-in Default Corporate look (navy table header, bold white header text, striped rows). styleGuide "legacy" gives the older grey header for this call only and is not stored on the session. An italic-only line directly under the opening # title becomes the Subtitle; a [!NOTE]-style line outside a blockquote stays plain text and comes back in warnings. Later batches inherit it, inspect_document shows its compact summary, save_document reports advisory styleWarnings. Unknown names fail with E_VALIDATION naming the available set. Math: $$ ... $$ or \\[ ... \\] blocks become display equations and \\(...\\) becomes inline math (one line, up to 500 characters; refused inside links and table cells). Display blocks are top level only (inside a list item or alert body they fail closed). Optional dollarMath (default false) treats $...$ as inline math; off by default because prices and shell text can look like math. Workflow: open_document -> compose_document -> save_document. Example {"sessionId":"sess_abc","markdown":"# Plan\\n\\n- a\\n- b\\n"} (add "allowedDir":"<dir>" for image/chart refs, "template":"report|memo|blank" to verify the seed). IDs are session-scoped: valid only in the issuing session, must not be stored across sessions. persistentIds maps each created paragraph id (md_0003) to the paragraph id the saved file carries, so a reopened session can be addressed with it (tables are renumbered t_001.. by position on reopen).';
42825
+ var COMPOSE_DOCUMENT_DESCRIPTION = 'Compose a Markdown string into the open session document (appends after existing content, or at anchor with position before|after; an unchanged built-in template created in this session has its sample content replaced). A document can be built in several compose calls: the first call of a session mints ids md_0001..., later calls mint md2_0001..., md3_0001..., so ids never collide; pass anchor (a node id, start or end) and position to add a section in the middle without another tool. Runs the engine compose path, then applies the resulting operations through the same execute_batch path (strict schema gate, per-chunk quotas, structural checks, per-chunk atomic rollback, then inline-span run splitting), so every gate a batch meets applies here unchanged. Supported input: #/##/### headings (real Heading1..3 styles), blank-line paragraphs (single newlines join with a space), flat bullet (-, *, +) and ordered (1., 1)) lists with real numbering, GitHub-style tables with a header separator row (header row shaded and repeated), --- dividers, fenced or indented code blocks (monospace, line breaks kept), [^label] footnotes with definitions, inline spans (**bold**, *italic*, `code`, [text](url)) applied as runs with real hyperlink targets, plain `> quote` blocks (written in the Quote paragraph style: blank > lines start a new paragraph, > > nests up to 3 levels, flat lists and fenced code are kept inside, a table, heading or image inside a quote fails), and standalone block images/charts (![alt](path)) resolved from allowedDir. Out-of-scope input fails closed with E_VALIDATION plus a 1-based line number and writes nothing: raw HTML (except [!NOTE]-style alert callouts, which compile to callout containers and keep bold, italic, code and links), custom styles, deeper headings, nested lists or tables, task items. Block image/chart refs need allowedDir pointing at the folder holding the files (refs stay jailed inside it; without allowedDir they fail closed); remote URL refs fail closed; inline data-URL block images bypass allowedDir by design (bytes decode in memory through the same image gates). Sources compiling past 500 operations apply as sequential 500-operation chunks; the compose is atomic as a whole: when any chunk fails, every chunk before it is rewound and the document stays as it was. Citation numbers and the References list restart in each compose call. Optional template (report|memo|blank) is accepted on the first compose only and requires the open document to still hold that template seed, otherwise composition fails before any write (for a fresh templated document call new_document first, then compose_document with template), and placement is append after existing content except for an unchanged built-in template created in this session. Reopened documents and external templates keep existing content. Returns {ok:true,opCount,appliedCount,revision,createdNodeIds,results,validation:{valid,errorCount,errors}} where createdNodeIds lists new node ids in document order (use them as anchors without re-inspecting) and validation proves the post-compose package state via a read-only OOXML check (save_document re-gates before writing). A compose that drops a non-empty redo stack signals it (redoCleared:true + redoClearedWarning naming the depth): a fresh edit invalidates redo. Optional styleGuide (bundled guide name or guide .md path, jailed like image refs: explicit allowedDir else the session anchor; remote URLs fail closed) replaces the session guide (not a one-batch override: later batches inherit it until another compose replaces it) and skins this compose (table/header-fill, chart-palette, callout, and code defaults apply where ops omit params; explicit params still win). Guides cannot be detached: open a new session to work without a guide. With no guide, compose uses the built-in Default Corporate look (navy table header, bold white header text, striped rows). styleGuide "legacy" gives the older grey header for this call only and is not stored on the session. An italic-only line directly under the opening # title becomes the Subtitle; a [!NOTE]-style line outside a blockquote stays plain text and comes back in warnings. Later batches inherit it, inspect_document shows its compact summary, save_document reports advisory styleWarnings. Unknown names fail with E_VALIDATION naming the available set. Math: $$ ... $$ or \\[ ... \\] blocks become display equations and \\(...\\) becomes inline math (one line, up to 500 characters; refused inside links and table cells). Display blocks are top level only (inside a list item or alert body they fail closed). Optional dollarMath (default false) treats $...$ as inline math; off by default because prices and shell text can look like math. Workflow: open_document -> compose_document -> save_document. Example {"sessionId":"sess_abc","markdown":"# Plan\\n\\n- a\\n- b\\n"} (add "allowedDir":"<dir>" for image/chart refs, "template":"report|memo|blank" to verify the seed). IDs are session-scoped: valid only in the issuing session, must not be stored across sessions. When dc:title is still a template placeholder it is set from the first Title or Heading 1 (titleSetFrom says so); a title already set is kept. persistentIds maps each created paragraph id (md_0003) to the paragraph id the saved file carries, so a reopened session can be addressed with it (tables are renumbered t_001.. by position on reopen).';
42257
42826
  function persistentParagraphIds(ast, createdIds) {
42258
42827
  const wanted = new Set(createdIds);
42259
42828
  const out = {};
@@ -42316,7 +42885,7 @@ function assertPathAllowed(allowedDir, roots) {
42316
42885
  if (segments.includes("..")) {
42317
42886
  throw Object.assign(new Error('allowedDir contains forbidden ".." traversal'), {
42318
42887
  code: "E_SANDBOX",
42319
- remedy: "Use a folder inside the workspace documents directory or the system temp dir."
42888
+ remedy: `Use a folder ${describeAllowedFolders()}.`
42320
42889
  });
42321
42890
  }
42322
42891
  const resolved = path13.resolve(trimmed);
@@ -42341,7 +42910,7 @@ function assertPathAllowed(allowedDir, roots) {
42341
42910
  new Error(`allowedDir is outside the allowed directories (${roots.join(", ")})`),
42342
42911
  {
42343
42912
  code: "E_SANDBOX",
42344
- remedy: "Point allowedDir at a folder under documents/ or the system temp dir, or set ASTRA_EXTRA_DOC_ROOTS."
42913
+ remedy: `Point allowedDir at a folder ${describeAllowedFolders()}.`
42345
42914
  }
42346
42915
  );
42347
42916
  }
@@ -42366,7 +42935,7 @@ async function handleComposeDocument(store, rawArgs) {
42366
42935
  } catch (err) {
42367
42936
  return composeErrorFrom(
42368
42937
  err,
42369
- "Point allowedDir at a folder under documents/ or the system temp dir, or set ASTRA_EXTRA_DOC_ROOTS."
42938
+ `Point allowedDir at a folder ${describeAllowedFolders()}.`
42370
42939
  );
42371
42940
  }
42372
42941
  }
@@ -42382,7 +42951,7 @@ async function handleComposeDocument(store, rawArgs) {
42382
42951
  );
42383
42952
  }
42384
42953
  try {
42385
- validateOperationsStrict(operations);
42954
+ validateComposedOperations(operations);
42386
42955
  } catch (err) {
42387
42956
  return composeErrorFrom(
42388
42957
  err,
@@ -42435,10 +43004,15 @@ async function handleComposeDocument(store, rawArgs) {
42435
43004
  }
42436
43005
  }
42437
43006
  const sessionGuide = legacyLook ? "legacy" : handle.styleGuide;
42438
- if (sessionGuide !== void 0) {
43007
+ const idPrefix = nextComposeIdPrefix(handle.ast);
43008
+ if (sessionGuide !== void 0 || idPrefix !== "md" || parsed.data.anchor !== void 0) {
42439
43009
  operations = compileMarkdown(parsed.data.markdown, {
42440
43010
  ...canonicalAllowedDir !== void 0 ? { allowedDir: canonicalAllowedDir } : {},
42441
- styleGuide: sessionGuide
43011
+ ...sessionGuide !== void 0 ? { styleGuide: sessionGuide } : {},
43012
+ ...parsed.data.dollarMath ? { dollarMath: true } : {},
43013
+ idPrefix,
43014
+ ...parsed.data.anchor !== void 0 ? { anchor: parsed.data.anchor } : {},
43015
+ ...parsed.data.position !== void 0 ? { position: parsed.data.position } : {}
42442
43016
  });
42443
43017
  ({ shadows: opShadows, actions: opActions, targets: opTargets } = shadowsOf(operations));
42444
43018
  }
@@ -42446,7 +43020,10 @@ async function handleComposeDocument(store, rawArgs) {
42446
43020
  ...canonicalAllowedDir !== void 0 ? { allowedDir: canonicalAllowedDir } : {},
42447
43021
  ...template !== void 0 ? { template } : {},
42448
43022
  ...sessionGuide !== void 0 ? { styleGuide: sessionGuide } : {},
42449
- ...parsed.data.dollarMath ? { dollarMath: true } : {}
43023
+ ...parsed.data.dollarMath ? { dollarMath: true } : {},
43024
+ idPrefix,
43025
+ ...parsed.data.anchor !== void 0 ? { anchor: parsed.data.anchor } : {},
43026
+ ...parsed.data.position !== void 0 ? { position: parsed.data.position } : {}
42450
43027
  });
42451
43028
  if (!aggregate.success) {
42452
43029
  const first = aggregate.errors[0];
@@ -42515,6 +43092,7 @@ async function handleComposeDocument(store, rawArgs) {
42515
43092
  dirty: store.isDirty(handle),
42516
43093
  createdNodeIds,
42517
43094
  ...Object.keys(persistentIds).length > 0 ? { persistentIds } : {},
43095
+ ...aggregate.titleSetFrom ? { titleSetFrom: aggregate.titleSetFrom } : {},
42518
43096
  results,
42519
43097
  validation: {
42520
43098
  valid: validation.ok,
@@ -42802,6 +43380,13 @@ async function handleExecuteBatch(store, input) {
42802
43380
  const afterOrder = collectAstIdTexts(handle.ast).order;
42803
43381
  const createdNodeIds = afterOrder.filter((id) => !beforeIds.has(id));
42804
43382
  const results = attributeCreatedIds(opActions, opShadows, createdNodeIds);
43383
+ const coverIndex = opActions.indexOf("coverPage");
43384
+ if (coverIndex >= 0) {
43385
+ const afterSet = new Set(afterOrder);
43386
+ const named = new Set(opTargets);
43387
+ const removed = [...beforeIds].filter((id) => !afterSet.has(id) && !named.has(id));
43388
+ if (removed.length > 0) results[coverIndex].removedSeedNodes = removed;
43389
+ }
42805
43390
  store.accumulatePendingAudit(handle, res.appliedCount, [...opTargets, ...createdNodeIds]);
42806
43391
  const batchRevisionsPending = typeof res.revisionsPending === "number" ? res.revisionsPending : countPendingRevisionsInParser(handle.parser);
42807
43392
  const batchRevisionsRemedy = typeof res.revisionsRemedy === "string" ? String(res.revisionsRemedy) : revisionsRemedy(batchRevisionsPending, res.revisionResolution?.unresolved);
@@ -42911,6 +43496,11 @@ async function executeSaveDocument(store, input) {
42911
43496
  const strict = parsed.data.strict ?? strictValidationDefault();
42912
43497
  const paraIdPolicy = strict ? "refuse" : "renumber";
42913
43498
  const dupPreState = paraIdPolicy === "renumber" && handle.parser.getDuplicateParaIds().length > 0 ? { xml: handle.parser.readDocumentXml(), extras: collectTrackedExtrasForGit(handle.parser) } : null;
43499
+ let identity = {};
43500
+ try {
43501
+ identity = applyDocumentIdentity(handle.ast, handle.parser);
43502
+ } catch {
43503
+ }
42914
43504
  const validation = await handle.parser.validateOOXML({ maxErrors: BASELINE_MAX_ERRORS, duplicateParaIds: paraIdPolicy });
42915
43505
  const verdict = validation.ok ? { kind: "clean" } : strict ? { kind: "incomparable", reason: "no-baseline" } : judgeAgainstBaseline(validation, await resolveBaseline(handle));
42916
43506
  if (verdict.kind === "introduced") throw introducedError("save_document", verdict);
@@ -42924,7 +43514,9 @@ async function executeSaveDocument(store, input) {
42924
43514
  code: "E_VALIDATION",
42925
43515
  // Phase C-1c: xpath-specific remedy (docPr/@id, tbl children,
42926
43516
  // relationships each name their own fix).
42927
- remedy: remedyForValidationError(xp, desc)
43517
+ remedy: remedyForValidationError(xp, desc) + (count ? " validate_document lists every error with its location." : ""),
43518
+ errorCount: validation.errors.length,
43519
+ errors: summarizeValidationErrors(validation.errors)
42928
43520
  });
42929
43521
  }
42930
43522
  if (dupPreState) {
@@ -43060,6 +43652,8 @@ async function executeSaveDocument(store, input) {
43060
43652
  ...changedValuesReason ? { changedValuesReason } : {},
43061
43653
  // Display-only: the core save-path hook healed during the rezip above.
43062
43654
  normalized: saveNormalized,
43655
+ ...identity.titleSetFrom ? { title: identity.title, titleSetFrom: identity.titleSetFrom } : {},
43656
+ ...identity.creatorSet ? { creatorSet: identity.creatorSet } : {},
43063
43657
  // Advisory style pass under the session guide (read-only, best-effort,
43064
43658
  // never blocks the save). Absent without a guide. The stored chart
43065
43659
  // reader mirrors validate/document-manager so persisted twin colors
@@ -44498,7 +45092,7 @@ var NewDocumentSchema = import_zod8.z.object({
44498
45092
  "DEPRECATED: session ids are server-minted (sess_<uuid>); omit sessionId and use the returned id. Client-supplied values are rejected with E_VALIDATION (deterministic ids are test-only via --allow-client-session-ids)."
44499
45093
  )
44500
45094
  }).strict();
44501
- var NEW_DOCUMENT_DESCRIPTION = 'Create a new .docx from a template (report|memo|blank, default blank) at outPath inside an allowed directory (--allowed-dir / documents / system temp). Validates the fresh package before writing (E_VALIDATION when invalid, never writes corruption); refuses to overwrite without {force:true} (E_CONFLICT). Success returns {ok:true,path,sessionId,revision,template,bytes} with the file opened into a session (use sessionId for inspect/execute_batch/save). Example {"outPath":"documents/new-report.docx","template":"report"}. Session ids are server-minted (sess_<uuid>, DEPRECATED to supply sessionId: client-supplied values are rejected with E_VALIDATION; deterministic ids are test-only via --allow-client-session-ids). IDs are session-scoped: valid only in the issuing session, must not be stored across sessions. Optional styleGuide (bundled guide name or guide .md path, jailed to the document folder; remote URLs fail closed) attaches a house style to the session at creation: later batches inherit its table/chart/callout defaults, inspect_document shows its compact summary, save_document reports advisory styleWarnings. Guides cannot be detached afterwards: open a new session to work without a guide. Unknown names fail with E_VALIDATION naming the available set.';
45095
+ var NEW_DOCUMENT_DESCRIPTION = 'Create a new .docx from a template (report|memo|blank, default blank) at outPath inside an allowed folder (the folders named with --allowed-dir or ASTRA_EXTRA_DOC_ROOTS; only when none are named, ./documents and the system temp folder). Validates the fresh package before writing (E_VALIDATION when invalid, never writes corruption); refuses to overwrite without {force:true} (E_CONFLICT). Success returns {ok:true,path,sessionId,revision,template,bytes} with the file opened into a session (use sessionId for inspect/execute_batch/save). Example {"outPath":"documents/new-report.docx","template":"report"}. Session ids are server-minted (sess_<uuid>, DEPRECATED to supply sessionId: client-supplied values are rejected with E_VALIDATION; deterministic ids are test-only via --allow-client-session-ids). IDs are session-scoped: valid only in the issuing session, must not be stored across sessions. Optional styleGuide (bundled guide name or guide .md path, jailed to the document folder; remote URLs fail closed) attaches a house style to the session at creation: later batches inherit its table/chart/callout defaults, inspect_document shows its compact summary, save_document reports advisory styleWarnings. Guides cannot be detached afterwards: open a new session to work without a guide. Unknown names fail with E_VALIDATION naming the available set.';
44502
45096
  function newDocError(code, message, remedy) {
44503
45097
  return { ok: false, code, message, remedy };
44504
45098
  }
@@ -44659,24 +45253,7 @@ async function handleNewDocument(store, rawArgs) {
44659
45253
  // src/lib/mcp/handlers/parity.ts
44660
45254
  var fs17 = __toESM(require("fs"));
44661
45255
  var path19 = __toESM(require("path"));
44662
- var AUDIT_VALIDATION_ERRORS_MAX = 10;
44663
45256
  var IMPORT_FILE_MAX_BYTES = COMPOSE_MARKDOWN_MAX;
44664
- function toAuditValidation(validation, max = AUDIT_VALIDATION_ERRORS_MAX) {
44665
- const rawErrors = Array.isArray(validation.errors) ? validation.errors.slice(0, max) : [];
44666
- const errors = rawErrors.map((e) => {
44667
- const err = e;
44668
- const description = String(
44669
- typeof err.description === "string" && err.description || typeof err.message === "string" && err.message || "OOXML validation error"
44670
- ).slice(0, 500);
44671
- const xp = typeof err.xPath === "string" ? err.xPath : typeof err.path === "string" ? err.path : void 0;
44672
- return xp ? { description, xPath: xp } : { description };
44673
- });
44674
- return {
44675
- valid: validation.ok,
44676
- errorCount: Array.isArray(validation.errors) ? validation.errors.length : 0,
44677
- errors
44678
- };
44679
- }
44680
45257
  async function refreshSessionAst(handle) {
44681
45258
  await refreshAstInPlace(handle.ast, handle.parser);
44682
45259
  }
@@ -44758,6 +45335,7 @@ async function handleGetStats(store, input) {
44758
45335
  words: stats.words,
44759
45336
  paragraphs: stats.paragraphs,
44760
45337
  tables: stats.tables,
45338
+ callouts: stats.callouts,
44761
45339
  images: stats.images,
44762
45340
  pages: stats.pages
44763
45341
  };
@@ -44916,7 +45494,7 @@ async function handleNormalizeDocument(store, input) {
44916
45494
  }
44917
45495
  function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
44918
45496
  if (typeof rawPath !== "string" || !rawPath.trim()) {
44919
- throw toError("E_SANDBOX", "import path must be a non-empty string", "Pass a .md path inside documents/ or the system temp dir.");
45497
+ throw toError("E_SANDBOX", "import path must be a non-empty string", `Pass a .md path ${describeAllowedFolders()}.`);
44920
45498
  }
44921
45499
  if (rawPath.includes("\0") || /%00/i.test(rawPath)) {
44922
45500
  throw toError("E_SANDBOX", "import path contains a null byte", "Remove null bytes from the path and retry.");
@@ -45003,7 +45581,7 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
45003
45581
  return normC === normR || normC.startsWith(normR + path19.sep);
45004
45582
  });
45005
45583
  if (!stillInside) {
45006
- throw toError("E_SANDBOX", "import path points outside the allowed directory", "Keep Markdown files inside documents/ or the system temp dir.");
45584
+ throw toError("E_SANDBOX", "import path points outside the allowed directory", `Keep Markdown files ${describeAllowedFolders()}.`);
45007
45585
  }
45008
45586
  return buf.toString("utf8");
45009
45587
  } finally {
@@ -45080,7 +45658,7 @@ var import_zod9 = require("zod");
45080
45658
  // package.json
45081
45659
  var package_default = {
45082
45660
  name: "@attentionform/astra-mcp-server",
45083
- version: "0.3.0",
45661
+ version: "0.4.0",
45084
45662
  description: "Official Model Context Protocol (MCP) server for ASTra document IDE",
45085
45663
  main: "dist/index.js",
45086
45664
  types: "dist/index.d.ts",
@@ -45121,8 +45699,8 @@ var package_default = {
45121
45699
  "adm-zip-gate": "node ../../scripts/check-adm-zip-imports.mjs"
45122
45700
  },
45123
45701
  dependencies: {
45124
- "@attentionform/astra-engine": "^0.3.0",
45125
- "@attentionform/ooxml-validator": "^0.3.0",
45702
+ "@attentionform/astra-engine": "^0.4.0",
45703
+ "@attentionform/ooxml-validator": "^0.4.0",
45126
45704
  "@modelcontextprotocol/sdk": "^1.32.2",
45127
45705
  "@xmldom/xmldom": "0.8.15",
45128
45706
  "adm-zip": "0.6.1",
@@ -45528,6 +46106,10 @@ async function main(argv = process.argv) {
45528
46106
  return 0;
45529
46107
  }
45530
46108
  function isDirectEntry() {
46109
+ try {
46110
+ if (typeof require !== "undefined" && typeof module !== "undefined" && require.main === module) return true;
46111
+ } catch {
46112
+ }
45531
46113
  const entry = process.argv?.[1] ?? "";
45532
46114
  return entry.endsWith("/index.js") || entry.endsWith("\\index.js") || entry.endsWith("/index.ts") || entry.endsWith("astra-mcp-server");
45533
46115
  }