@fenfill/mcp 0.2.0 → 0.2.2

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
@@ -135572,6 +135572,37 @@ function layoutMultiline({
135572
135572
  }
135573
135573
 
135574
135574
  // ../../app/t/[template_id]/stampPdf.ts
135575
+ async function embedStampFaces(pdfDoc, fontkit, fonts) {
135576
+ const interFont = await pdfDoc.embedFont(fonts.inter, { subset: true });
135577
+ const dejaFont = await pdfDoc.embedFont(fonts.deja, { subset: true });
135578
+ const interKit = fontkit.create(new Uint8Array(fonts.inter));
135579
+ const covers = (cp) => {
135580
+ try {
135581
+ return interKit.hasGlyphForCodePoint(cp);
135582
+ } catch {
135583
+ return false;
135584
+ }
135585
+ };
135586
+ const splitRuns = (t) => splitByCoverage(t, (cp) => covers(cp) ? interFont : dejaFont).map((r) => ({
135587
+ font: r.key,
135588
+ text: r.text
135589
+ }));
135590
+ const measure = (t, s) => splitRuns(t).reduce((sum2, r) => sum2 + r.font.widthOfTextAtSize(r.text, s), 0);
135591
+ const draw = (page, t, x2, y, s, color) => {
135592
+ let cx2 = x2;
135593
+ for (const r of splitRuns(t)) {
135594
+ page.drawText(r.text, { x: cx2, y, size: s, font: r.font, color });
135595
+ cx2 += r.font.widthOfTextAtSize(r.text, s);
135596
+ }
135597
+ };
135598
+ let dejaSet;
135599
+ const hasGlyph = (cp) => {
135600
+ if (covers(cp)) return true;
135601
+ dejaSet ??= new Set(dejaFont.getCharacterSet());
135602
+ return dejaSet.has(cp);
135603
+ };
135604
+ return { measure, draw, hasGlyph };
135605
+ }
135575
135606
  async function stampPdf({
135576
135607
  pdfBytes,
135577
135608
  marksByPage,
@@ -135605,29 +135636,12 @@ async function stampPdf({
135605
135636
  return r.arrayBuffer();
135606
135637
  })
135607
135638
  ]);
135608
- const interFont = await pdfDoc.embedFont(interBytes, { subset: true });
135609
- const dejaFont = await pdfDoc.embedFont(dejaBytes, { subset: true });
135639
+ const faces = await embedStampFaces(pdfDoc, fontkit, { inter: interBytes, deja: dejaBytes });
135610
135640
  const pdfPages = pdfDoc.getPages();
135611
135641
  const INK = rgb3(0.1, 0.1, 0.1);
135612
- const interKit = fontkit.create(new Uint8Array(interBytes));
135613
- const covers = (cp) => {
135614
- try {
135615
- return interKit.hasGlyphForCodePoint(cp);
135616
- } catch {
135617
- return false;
135618
- }
135619
- };
135620
- const splitRuns = (t) => splitByCoverage(t, (cp) => covers(cp) ? interFont : dejaFont).map((r) => ({
135621
- font: r.key,
135622
- text: r.text
135623
- }));
135624
- const measure = (t, s) => splitRuns(t).reduce((sum2, r) => sum2 + r.font.widthOfTextAtSize(r.text, s), 0);
135642
+ const { measure } = faces;
135625
135643
  const drawMixed = (page, t, x2, y, s) => {
135626
- let cx2 = x2;
135627
- for (const r of splitRuns(t)) {
135628
- page.drawText(r.text, { x: cx2, y, size: s, font: r.font, color: INK });
135629
- cx2 += r.font.widthOfTextAtSize(r.text, s);
135630
- }
135644
+ faces.draw(page, t, x2, y, s, INK);
135631
135645
  };
135632
135646
  const drawMarkPdf = (page, shape, r, sizePt, overflow) => {
135633
135647
  const fit = Math.min(r.w, r.h);
@@ -136492,6 +136506,14 @@ function mapInternal(schema, input2, loadImage2) {
136492
136506
  }
136493
136507
  return out;
136494
136508
  }
136509
+ function mapAgentValues(schema, input2, opts) {
136510
+ const { values: values2, rowCounts, filled, skipped, warnings } = mapInternal(
136511
+ schema,
136512
+ input2,
136513
+ opts.loadImage
136514
+ );
136515
+ return { values: values2, rowCounts, filled, skipped, warnings };
136516
+ }
136495
136517
  async function verifyImages(m) {
136496
136518
  if (m.images.length === 0) return;
136497
136519
  const { PDFDocument: PDFDocument2 } = await Promise.resolve().then(() => __toESM(require_cjs()));
@@ -136602,9 +136624,34 @@ function checkTagIds(entries) {
136602
136624
  return ids;
136603
136625
  }
136604
136626
  var hits = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
136605
- async function buildCheckPdf(stamped, entries, status, banner = true) {
136627
+ var causeOf = (e4) => e4 instanceof Error && e4.message ? e4.message : String(e4);
136628
+ var oneLine = (s) => s.replace(/[\s\p{Cc}]+/gu, " ").trim();
136629
+ var asciiOnly = (s) => s.replace(/[^\x20-\x7e]/g, "?");
136630
+ var CheckCopyError = class extends Error {
136631
+ at;
136632
+ constructor(at, cause) {
136633
+ super(causeOf(cause));
136634
+ this.name = "CheckCopyError";
136635
+ this.at = { ...at };
136636
+ }
136637
+ };
136638
+ async function buildCheckPdf(stamped, entries, status, fonts, banner = true, tags = true) {
136639
+ const at = { stage: "load" };
136640
+ try {
136641
+ return await drawCheckPdf(stamped, entries, status, fonts, banner, tags, at);
136642
+ } catch (e4) {
136643
+ throw new CheckCopyError(at, e4);
136644
+ }
136645
+ }
136646
+ async function drawCheckPdf(stamped, entries, status, fonts, banner, tags, at) {
136647
+ const step = (stage, id) => {
136648
+ at.stage = stage;
136649
+ at.id = id;
136650
+ };
136651
+ const notes = [];
136606
136652
  const { PDFDocument: PDFDocument2, StandardFonts: StandardFonts2, rgb: rgb3 } = await Promise.resolve().then(() => __toESM(require_cjs()));
136607
136653
  const doc = await PDFDocument2.load(stamped, { updateMetadata: false });
136654
+ step("fonts");
136608
136655
  const helv = await doc.embedFont(StandardFonts2.Helvetica);
136609
136656
  const pages = doc.getPages();
136610
136657
  const COLORS = {
@@ -136615,6 +136662,7 @@ async function buildCheckPdf(stamped, entries, status, banner = true) {
136615
136662
  };
136616
136663
  const boxes = [];
136617
136664
  for (const e4 of entries) {
136665
+ step("outline", e4.id);
136618
136666
  const st = status(e4.id);
136619
136667
  if (e4.kind === "field") {
136620
136668
  boxes.push({ id: e4.id, page: e4.field.page, box: e4.field, st, thin: false, tagged: true });
@@ -136631,6 +136679,7 @@ async function buildCheckPdf(stamped, entries, status, banner = true) {
136631
136679
  boxes.push({ id: c2.id, page: c2.field.page, box: c2.field, st, thin: true, tagged: false });
136632
136680
  }
136633
136681
  }
136682
+ step("tag");
136634
136683
  const short = shortIds(checkTagIds(entries));
136635
136684
  const SIZE = 5;
136636
136685
  const rectOf = (b, pw, ph) => {
@@ -136640,47 +136689,92 @@ async function buildCheckPdf(stamped, entries, status, banner = true) {
136640
136689
  };
136641
136690
  const placedTags = /* @__PURE__ */ new Map();
136642
136691
  const HEAD = 4;
136643
- const heads = [];
136644
136692
  const headColor = rgb3(0.15, 0.35, 0.75);
136693
+ const wanted = [];
136645
136694
  for (const e4 of entries) {
136646
- if (e4.kind !== "table") continue;
136695
+ if (!tags || e4.kind !== "table") continue;
136696
+ step("table headers", e4.id);
136647
136697
  const g = e4.group;
136648
136698
  const texts = (v) => Array.isArray(v) ? v : [];
136699
+ const lineOf = (raw) => typeof raw === "string" ? oneLine(raw) : "";
136649
136700
  const firstOf = (key, k4) => e4.cells.filter((c2) => c2[key] === k4).sort((a, b) => key === "row" ? a.col - b.col : a.row - b.row).at(0);
136650
- const draw = (prefix, raw, cell, below) => {
136651
- if (typeof raw !== "string" || !raw.trim() || !cell) return;
136701
+ const want = (axis, index2, raw, cell, below) => {
136702
+ const text = lineOf(raw);
136703
+ if (!text || !cell) return;
136652
136704
  const page = pages[cell.field.page - 1];
136653
- if (!page) return;
136654
- const { width: pw, height: ph } = page.getSize();
136655
- const cx2 = cell.field.xpct / 100 * pw;
136656
- const top = ph - cell.field.ypct / 100 * ph;
136657
- const w = cell.field.wpct / 100 * pw;
136658
- const h = cell.field.hpct / 100 * ph;
136659
- const inside = w >= 40 && h >= HEAD + 1;
136660
- const room = inside ? w - 1 : prefix === "R" ? Math.min(120, cx2 - 1) : Math.max(2 * w, 30);
136661
- let text = `${prefix}: ${raw.trim()}`;
136662
- while (text.length > 4 && helv.widthOfTextAtSize(text, HEAD) > room) {
136663
- text = `${text.slice(0, -2)}\u2026`;
136664
- }
136665
- const tw = helv.widthOfTextAtSize(text, HEAD);
136666
- const x2 = inside ? cx2 + 0.6 : prefix === "R" ? cx2 - tw - 0.8 : cx2;
136667
- const y = inside ? top - HEAD - 0.4 - below : prefix === "R" ? top - h / 2 - HEAD / 2 : top + 0.6;
136668
- heads.push({ page, text, opts: { x: x2, y, size: HEAD, font: helv, color: headColor } });
136669
- const taken = placedTags.get(cell.field.page) ?? [];
136670
- taken.push({ x: x2, y, w: tw, h: HEAD + 0.6 });
136671
- placedTags.set(cell.field.page, taken);
136705
+ if (page) wanted.push({ gid: e4.id, axis, index: index2, text, cell, page, below });
136672
136706
  };
136673
136707
  texts(g.header_rows).forEach((t, r) => {
136674
- draw("R", t, firstOf("row", r), 0);
136708
+ want("row", r, t, firstOf("row", r), 0);
136675
136709
  });
136676
136710
  texts(g.header_cols).forEach((t, c2) => {
136677
136711
  const cell = firstOf("col", c2);
136678
136712
  const shared = cell !== void 0 && cell.col === firstOf("row", cell.row)?.col;
136679
- const rowHead = cell ? texts(g.header_rows)[cell.row] : void 0;
136680
- draw("C", t, cell, shared && typeof rowHead === "string" && rowHead.trim() ? HEAD + 0.6 : 0);
136713
+ const rowHead = cell ? lineOf(texts(g.header_rows)[cell.row]) : "";
136714
+ want("column", c2, t, cell, shared && rowHead ? HEAD + 0.6 : 0);
136681
136715
  });
136682
136716
  }
136717
+ const heads = [];
136718
+ const dropped = (h, e4) => `${h.gid}: ${h.axis} header ${String(h.index)} could not be drawn in the check copy: ${causeOf(e4)}`;
136719
+ if (wanted.length > 0) {
136720
+ step("fonts");
136721
+ const { default: fontkit } = await Promise.resolve().then(() => __toESM(require_fontkit_umd()));
136722
+ doc.registerFontkit(fontkit);
136723
+ const faces = await embedStampFaces(doc, fontkit, fonts);
136724
+ const unreadable = /* @__PURE__ */ new Set();
136725
+ for (const h of wanted) {
136726
+ step("table headers", h.gid);
136727
+ try {
136728
+ const { cell, page } = h;
136729
+ const prefix = h.axis === "row" ? "R" : "C";
136730
+ const { width: pw, height: ph } = page.getSize();
136731
+ const cx2 = cell.field.xpct / 100 * pw;
136732
+ const top = ph - cell.field.ypct / 100 * ph;
136733
+ const w = cell.field.wpct / 100 * pw;
136734
+ const ht = cell.field.hpct / 100 * ph;
136735
+ const inside = w >= 40 && ht >= HEAD + 1;
136736
+ const room = inside ? w - 1 : prefix === "R" ? Math.min(120, cx2 - 1) : Math.max(2 * w, 30);
136737
+ const chars3 = Array.from(
136738
+ `${prefix}: ${h.text}`,
136739
+ (ch) => faces.hasGlyph(ch.codePointAt(0) ?? 0) ? ch : "?"
136740
+ );
136741
+ if (chars3.join("") !== `${prefix}: ${h.text}` && !unreadable.has(h.gid)) {
136742
+ unreadable.add(h.gid);
136743
+ notes.push(
136744
+ `${h.gid}: the check copy cannot draw some header characters (shown as "?"); the legend's header_rows / header_cols hold the full text`
136745
+ );
136746
+ }
136747
+ const cut = (n) => `${chars3.slice(0, n).join("")}\u2026`;
136748
+ let text = chars3.join("");
136749
+ if (chars3.length > 4 && faces.measure(text, HEAD) > room) {
136750
+ let lo = 3;
136751
+ let hi = chars3.length - 2;
136752
+ while (lo < hi) {
136753
+ const mid = Math.ceil((lo + hi) / 2);
136754
+ if (faces.measure(cut(mid), HEAD) <= room) lo = mid;
136755
+ else hi = mid - 1;
136756
+ }
136757
+ text = cut(lo);
136758
+ }
136759
+ const tw = faces.measure(text, HEAD);
136760
+ const x2 = inside ? cx2 + 0.6 : prefix === "R" ? cx2 - tw - 0.8 : cx2;
136761
+ const y = inside ? top - HEAD - 0.4 - h.below : prefix === "R" ? top - ht / 2 - HEAD / 2 : top + 0.6;
136762
+ heads.push({
136763
+ head: h,
136764
+ draw: () => {
136765
+ faces.draw(page, text, x2, y, HEAD, headColor);
136766
+ }
136767
+ });
136768
+ const taken = placedTags.get(cell.field.page) ?? [];
136769
+ taken.push({ x: x2, y, w: tw, h: HEAD + 0.6 });
136770
+ placedTags.set(cell.field.page, taken);
136771
+ } catch (e4) {
136772
+ notes.push(dropped(h, e4));
136773
+ }
136774
+ }
136775
+ }
136683
136776
  for (const b of boxes) {
136777
+ step("outline", b.id);
136684
136778
  const page = pages[b.page - 1];
136685
136779
  if (!page) continue;
136686
136780
  const { width: pw, height: ph } = page.getSize();
@@ -136697,8 +136791,9 @@ async function buildCheckPdf(stamped, entries, status, banner = true) {
136697
136791
  opacity: 0,
136698
136792
  borderOpacity: 0.9
136699
136793
  });
136700
- if (!b.tagged) continue;
136701
- const tag = short.get(b.id) ?? b.id;
136794
+ if (!tags || !b.tagged) continue;
136795
+ step("tag", b.id);
136796
+ const tag = asciiOnly(short.get(b.id) ?? b.id);
136702
136797
  const tw = helv.widthOfTextAtSize(tag, SIZE) + 1.6;
136703
136798
  const th = SIZE + 1.2;
136704
136799
  const right = x2 + w - tw;
@@ -136730,14 +136825,23 @@ async function buildCheckPdf(stamped, entries, status, banner = true) {
136730
136825
  });
136731
136826
  page.drawText(tag, { x: spot.x + 0.8, y: spot.y + 0.8, size: SIZE, font: helv, color });
136732
136827
  }
136733
- for (const h of heads) h.page.drawText(h.text, h.opts);
136828
+ for (const h of heads) {
136829
+ step("table headers", h.head.gid);
136830
+ try {
136831
+ h.draw();
136832
+ } catch (e4) {
136833
+ notes.push(dropped(h.head, e4));
136834
+ }
136835
+ }
136836
+ step("banner");
136734
136837
  const first = pages[0];
136735
136838
  if (first && banner) {
136736
136839
  const legend = "fenfill check copy: green = filled, orange = filled with a warning, red = skipped, grey = left empty. Tags are field-id prefixes.";
136737
136840
  const { height: ph } = first.getSize();
136738
136841
  first.drawText(legend, { x: 6, y: ph - 7, size: SIZE, font: helv, color: rgb3(0.3, 0.3, 0.35) });
136739
136842
  }
136740
- return doc.save();
136843
+ step("save");
136844
+ return { pdf: await doc.save(), notes };
136741
136845
  }
136742
136846
  async function fillAgentPdf(a) {
136743
136847
  if (!isRecord(a.render)) throw new TypeError("render must be the template's schema object");
@@ -136772,12 +136876,21 @@ async function fillAgentPdf(a) {
136772
136876
  const warned = new Set(
136773
136877
  placed.flatMap((w) => w.slice(0, w.indexOf(":")).split(" and ").map(topId))
136774
136878
  );
136775
- res.checkPdf = await buildCheckPdf(
136776
- pdf,
136777
- m.entries,
136778
- (id) => skippedIds.has(id) ? "skipped" : warned.has(id) ? "warned" : m.filledIds.has(id) ? "filled" : "empty",
136779
- a.checkBanner !== false
136780
- );
136879
+ try {
136880
+ const built = await buildCheckPdf(
136881
+ pdf,
136882
+ m.entries,
136883
+ (id) => skippedIds.has(id) ? "skipped" : warned.has(id) ? "warned" : m.filledIds.has(id) ? "filled" : "empty",
136884
+ a.fonts,
136885
+ a.checkBanner !== false,
136886
+ a.checkTags !== false
136887
+ );
136888
+ res.checkPdf = built.pdf;
136889
+ warnings.push(...built.notes);
136890
+ } catch (e4) {
136891
+ const at = e4 instanceof CheckCopyError ? e4.at : { stage: "build" };
136892
+ res.checkError = { stage: at.stage, ...at.id ? { id: at.id } : {}, message: causeOf(e4) };
136893
+ }
136781
136894
  }
136782
136895
  return res;
136783
136896
  }
@@ -137374,19 +137487,24 @@ function listedPages(agent, requested) {
137374
137487
  return have.filter((p4) => want.has(p4));
137375
137488
  }
137376
137489
  function nestBySection(entries) {
137377
- const blocks = /* @__PURE__ */ new Map();
137490
+ const blocks = [];
137491
+ let run = null;
137378
137492
  for (const e4 of entries) {
137379
- const sid = e4.section?.id ?? "";
137380
- const key = `${String(e4.page)}\0${sid}`;
137381
- let b = blocks.get(key);
137382
- if (!b) {
137493
+ const sid = e4.section?.id;
137494
+ const key = `${String(e4.page)}\0${sid ?? ""}`;
137495
+ if (key !== run) {
137383
137496
  const title = e4.section?.title;
137384
- b = { page: e4.page, ...title ? { title } : {}, fields: [] };
137385
- blocks.set(key, b);
137497
+ blocks.push({
137498
+ page: e4.page,
137499
+ ...sid ? { section_id: sid } : {},
137500
+ ...title ? { title } : {},
137501
+ fields: []
137502
+ });
137503
+ run = key;
137386
137504
  }
137387
- b.fields.push(compactEntry(e4));
137505
+ blocks[blocks.length - 1].fields.push(compactEntry(e4));
137388
137506
  }
137389
- return [...blocks.values()];
137507
+ return blocks;
137390
137508
  }
137391
137509
  function compactView(agent, requested, next, maxChars = COMPACT_MAX_CHARS) {
137392
137510
  const entries = selectEntries(agent, requested);
@@ -137680,7 +137798,7 @@ function loadFonts(dir = defaultFontsDir()) {
137680
137798
  }
137681
137799
 
137682
137800
  // src/version.ts
137683
- var VERSION = true ? "0.2.0" : "0.0.0-dev";
137801
+ var VERSION = true ? "0.2.2" : "0.0.0-dev";
137684
137802
  var USER_AGENT = `fenfill-mcp/${VERSION}`;
137685
137803
 
137686
137804
  // src/fill.ts
@@ -137864,6 +137982,11 @@ function fillForbiddenError() {
137864
137982
  hint: "fenfill can't fill this file; use a copy whose permissions allow form filling."
137865
137983
  });
137866
137984
  }
137985
+ function checkCopyFailed(err2) {
137986
+ const what = "The check copy could not be built";
137987
+ if (!err2) return `${what}.`;
137988
+ return `${what} (${[err2.stage, err2.id].filter(Boolean).join(", ")}): ${err2.message}`;
137989
+ }
137867
137990
  async function checkPathFor(outputAbs, opts) {
137868
137991
  return resolveOutputPath(outputAbs.replace(/\.pdf$/i, ".check.pdf"), opts);
137869
137992
  }
@@ -137889,17 +138012,22 @@ async function stampAndWrite(a) {
137889
138012
  await writeOutputAtomic(a.outputAbs, res.pdf, a.overwrite);
137890
138013
  await a.cache.registerOutput(sha256Hex(res.pdf));
137891
138014
  let checkPath;
138015
+ const warnings = [...res.warnings];
137892
138016
  if (a.checkAbs && res.checkPdf) {
137893
138017
  await writeOutputAtomic(a.checkAbs, res.checkPdf, a.overwrite);
137894
138018
  await a.cache.registerOutput(sha256Hex(res.checkPdf));
137895
138019
  checkPath = a.checkAbs;
138020
+ } else if (a.checkAbs) {
138021
+ warnings.push(
138022
+ `No check copy was written; the filled PDF is complete. ${checkCopyFailed(res.checkError)}`
138023
+ );
137896
138024
  }
137897
138025
  return {
137898
138026
  output_path: a.outputAbs,
137899
138027
  ...checkPath ? { check_path: checkPath } : {},
137900
138028
  filled: res.filled,
137901
138029
  skipped: res.skipped,
137902
- warnings: res.warnings
138030
+ warnings
137903
138031
  };
137904
138032
  }
137905
138033
 
@@ -139193,67 +139321,208 @@ var ECHO_CONTAINS_MIN = 6;
139193
139321
  var MAX_REMEMBERED = 5e3;
139194
139322
  var MAX_LEN = 2e3;
139195
139323
  var FREE_TEXT = /* @__PURE__ */ new Set(["text", "multiline", "date", "comb"]);
139196
- var norm = (s) => s.trim().replace(/\s+/g, " ").toLowerCase();
139324
+ var norm = (s) => s.normalize("NFC").trim().replace(/\s+/g, " ").toLowerCase();
139325
+ var isRec = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2);
139326
+ var NO_IMAGE = () => {
139327
+ throw new Error("not read by the echo guard");
139328
+ };
139329
+ function inPrintedFormat(digits2, fmt3) {
139330
+ const d = Array.from(digits2);
139331
+ const cells = Array.from(fmt3);
139332
+ const isLetter = (c2) => HAS_LETTER.test(c2);
139333
+ if (!d.length || !d.every((c2) => new RegExp("^\\p{Nd}$", "u").test(c2))) return "";
139334
+ if (d.length !== cells.filter(isLetter).length) return "";
139335
+ let i2 = 0;
139336
+ return cells.map((c2) => isLetter(c2) ? d[i2++] : c2).join("");
139337
+ }
139338
+ var LETTER = "[\\p{Lu}\\p{Ll}\\p{Lt}\\p{Lm}\\p{M}]";
139339
+ var HAS_LETTER = new RegExp(LETTER, "u");
139340
+ var DIGIT = "\\p{Nd}";
139341
+ var ENDS = [new RegExp(`${LETTER}$`, "u"), new RegExp(`${DIGIT}$`, "u")];
139342
+ var STARTS = [new RegExp(`^${LETTER}`, "u"), new RegExp(`^${DIGIT}`, "u")];
139343
+ var runsOn = (left, right) => ENDS.some((end, i2) => end.test(left) && STARTS[i2].test(right));
139344
+ function holds(n, v) {
139345
+ if (v.includes(" ") || !HAS_LETTER.test(v)) return true;
139346
+ for (let i2 = n.indexOf(v); i2 !== -1; i2 = n.indexOf(v, i2 + 1)) {
139347
+ if (!runsOn(n.slice(0, i2), v) && !runsOn(v, n.slice(i2 + v.length))) return true;
139348
+ }
139349
+ return false;
139350
+ }
139351
+ var entryKey = (w) => w.page === void 0 ? w.id : `${w.id}\0${String(w.page)}`;
139352
+ var keyOf = (r) => `${r.prop}\0${norm(r.text)}`;
139353
+ var placeOf = (r) => `${r.scope ?? ""}\0${entryKey(r)}\0${keyOf(r)}`;
139354
+ function keep(set2, key) {
139355
+ set2.delete(key);
139356
+ set2.add(key);
139357
+ if (set2.size > MAX_REMEMBERED) {
139358
+ const oldest = set2.values().next().value;
139359
+ if (oldest !== void 0) set2.delete(oldest);
139360
+ }
139361
+ }
139197
139362
  var EchoGuard = class {
139198
139363
  seen = /* @__PURE__ */ new Set();
139364
+ /** Refused wording the agent has been told about (property + text), and the
139365
+ * entries it was named on (working copy + entry + property + text). */
139366
+ told = /* @__PURE__ */ new Set();
139367
+ toldAt = /* @__PURE__ */ new Set();
139368
+ /** Wording the agent confirmed as the form's own (property + text: an id
139369
+ * does not survive group / ungroup, and the wording is what was vouched). */
139370
+ vouched = /* @__PURE__ */ new Set();
139199
139371
  add(v) {
139200
139372
  if (typeof v === "number" && Number.isFinite(v)) v = String(v);
139201
139373
  if (typeof v !== "string") return;
139202
139374
  const n = norm(v);
139203
139375
  if (!n || n.length > MAX_LEN) return;
139204
- this.seen.delete(n);
139205
- this.seen.add(n);
139206
- if (this.seen.size > MAX_REMEMBERED) {
139207
- const oldest = this.seen.values().next().value;
139208
- if (oldest !== void 0) this.seen.delete(oldest);
139376
+ keep(this.seen, n);
139377
+ if (!/[\r\n]/.test(v)) return;
139378
+ for (const line of v.split(/\r\n|\r|\n/)) {
139379
+ const l = norm(line);
139380
+ if (l.length >= ECHO_REFUSE_MIN && l.length <= MAX_LEN) keep(this.seen, l);
139381
+ }
139382
+ }
139383
+ /** Every text in `v`, whatever it was meant for (no entry tells a free-text
139384
+ * answer from a choice here), from the length a match refuses at. */
139385
+ addAny(v, depth = 0) {
139386
+ if (depth > 4) return;
139387
+ if (Array.isArray(v)) for (const x2 of v) this.addAny(x2, depth + 1);
139388
+ else if (isRec(v)) for (const x2 of Object.values(v)) this.addAny(x2, depth + 1);
139389
+ else {
139390
+ const s = typeof v === "number" && Number.isFinite(v) ? String(v) : v;
139391
+ if (typeof s === "string" && norm(s).length >= ECHO_REFUSE_MIN) this.add(s);
139392
+ }
139393
+ }
139394
+ /** Remember the values of a call that failed before its form was read (so
139395
+ * `remember` never saw them): the agent has typed them all the same, and one
139396
+ * can come back as wording on its next call. */
139397
+ rememberUnread(values2) {
139398
+ this.addAny(values2);
139399
+ }
139400
+ /** The same answers as the page shows them: the text of every mark the core
139401
+ * would stamp for these values (a date in its field's format, text as
139402
+ * normalized), a comb's cells joined into one answer. */
139403
+ addShown(schema, entries, values2) {
139404
+ const shown = /* @__PURE__ */ new Map();
139405
+ try {
139406
+ const mapped = mapAgentValues(schema, values2, { loadImage: NO_IMAGE });
139407
+ const marks = buildMarks({
139408
+ fields: schema.fields,
139409
+ groups: schema.groups,
139410
+ values: mapped.values,
139411
+ rowCounts: mapped.rowCounts
139412
+ });
139413
+ for (const page of marks.values()) {
139414
+ for (const m of page) {
139415
+ if (m.image || m.checkbox) continue;
139416
+ const key = m.key.split("#")[0];
139417
+ shown.set(key, (shown.get(key) ?? "") + m.text);
139418
+ }
139419
+ }
139420
+ } catch {
139421
+ return;
139422
+ }
139423
+ for (const e4 of entries) {
139424
+ if (e4.kind !== "comb") continue;
139425
+ let cells = "";
139426
+ for (const s of e4.slots) {
139427
+ cells += shown.get(s.key) ?? "";
139428
+ shown.delete(s.key);
139429
+ }
139430
+ this.add(cells);
139431
+ if (e4.dateFormat) this.add(inPrintedFormat(cells, e4.dateFormat));
139209
139432
  }
139433
+ for (const [, text] of shown) this.add(text);
139210
139434
  }
139211
139435
  /** Remember the free-text values of one fill/preview call. */
139212
139436
  remember(render, values2) {
139437
+ let schema;
139213
139438
  let entries;
139214
139439
  try {
139215
- entries = agentEntries(normalizeSchema(render));
139440
+ schema = normalizeSchema(render);
139441
+ entries = agentEntries(schema);
139216
139442
  } catch {
139443
+ this.addAny(values2);
139217
139444
  return;
139218
139445
  }
139219
- const isRec3 = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2);
139446
+ this.addShown(schema, entries, values2);
139220
139447
  for (const e4 of entries) {
139221
139448
  const v = values2[e4.id];
139222
139449
  if (v === void 0) continue;
139223
139450
  if (e4.kind === "field" || e4.kind === "comb") {
139224
139451
  if (FREE_TEXT.has(e4.type)) this.add(v);
139225
- } else if (e4.kind === "table" && isRec3(v)) {
139452
+ } else if (e4.kind === "table" && isRec(v)) {
139226
139453
  for (const c2 of e4.cells) if (FREE_TEXT.has(c2.type)) this.add(v[c2.id]);
139227
139454
  } else if (e4.kind === "table_rows" && Array.isArray(v)) {
139455
+ const ids = new Set(e4.columns.map((c2) => c2.id));
139228
139456
  for (const row of v) {
139229
- if (!isRec3(row)) continue;
139457
+ if (!isRec(row)) continue;
139230
139458
  for (const c2 of e4.columns) if (FREE_TEXT.has(c2.type)) this.add(row[c2.id]);
139459
+ for (const [k4, cell] of Object.entries(row)) if (!ids.has(k4)) this.add(cell);
139231
139460
  }
139232
139461
  }
139233
139462
  }
139234
139463
  }
139235
- /** Wording that repeats a remembered answer: refusals (it contains one of
139236
- * 6+ chars, or equals one of 4+) and warnings (equals a shorter one). */
139237
- check(wording) {
139464
+ /** Wording that repeats a remembered answer: refusals (it holds one of 6+
139465
+ * chars, or equals one of 4+) and warnings (equals a shorter one). Wording
139466
+ * the agent confirmed is listed apart and refuses nothing. `scope` is the
139467
+ * working copy the wording is in: an entry named in one copy is not thereby
139468
+ * named in another (section ids, and a copied template's ids, repeat). */
139469
+ check(wording, scope = "") {
139238
139470
  const refuse = /* @__PURE__ */ new Set();
139239
139471
  const warn = /* @__PURE__ */ new Set();
139240
- if (this.seen.size === 0) return { refuse: [], warn: [] };
139472
+ const refused = [];
139473
+ const confirmed = [];
139474
+ if (this.seen.size === 0) return { refuse: [], warn: [], refused, confirmed };
139241
139475
  for (const w of wording) {
139242
139476
  const n = norm(w.text);
139243
139477
  if (!n) continue;
139478
+ let hit = false;
139244
139479
  if (this.seen.has(n)) {
139245
- (n.length >= ECHO_REFUSE_MIN ? refuse : warn).add(w.id);
139246
- continue;
139247
- }
139248
- if (n.length < ECHO_CONTAINS_MIN) continue;
139249
- for (const v of this.seen) {
139250
- if (v.length >= ECHO_CONTAINS_MIN && n.includes(v)) {
139251
- refuse.add(w.id);
139252
- break;
139480
+ if (n.length < ECHO_REFUSE_MIN) {
139481
+ warn.add(w.id);
139482
+ continue;
139253
139483
  }
139484
+ hit = true;
139485
+ } else if (n.length >= ECHO_CONTAINS_MIN) {
139486
+ for (const v of this.seen) {
139487
+ if (v.length >= ECHO_CONTAINS_MIN && n.includes(v) && holds(n, v)) {
139488
+ hit = true;
139489
+ break;
139490
+ }
139491
+ }
139492
+ }
139493
+ if (!hit) continue;
139494
+ const r = {
139495
+ id: w.id,
139496
+ ...w.page === void 0 ? {} : { page: w.page },
139497
+ prop: w.prop ?? "",
139498
+ text: w.text,
139499
+ ...scope ? { scope } : {}
139500
+ };
139501
+ const key = keyOf(r);
139502
+ if (this.vouched.has(key)) confirmed.push({ ...r, shown: true, listed: true });
139503
+ else {
139504
+ refuse.add(w.id);
139505
+ refused.push({ ...r, shown: this.told.has(key), listed: this.toldAt.has(placeOf(r)) });
139254
139506
  }
139255
139507
  }
139256
- return { refuse: [...refuse], warn: [...warn].filter((id) => !refuse.has(id)) };
139508
+ return {
139509
+ refuse: [...refuse],
139510
+ warn: [...warn].filter((id) => !refuse.has(id)),
139511
+ refused,
139512
+ confirmed
139513
+ };
139514
+ }
139515
+ /** The agent has now been told about these refusals (an op's rejection, a
139516
+ * save's answer_in_template): from here on it can confirm them. */
139517
+ tell(refused) {
139518
+ for (const r of refused) {
139519
+ keep(this.told, keyOf(r));
139520
+ keep(this.toldAt, placeOf(r));
139521
+ }
139522
+ }
139523
+ /** The agent confirmed these as the form's own wording. */
139524
+ confirm(refused) {
139525
+ for (const r of refused) keep(this.vouched, keyOf(r));
139257
139526
  }
139258
139527
  get size() {
139259
139528
  return this.seen.size;
@@ -139263,13 +139532,121 @@ var EchoGuard = class {
139263
139532
  // src/edits.ts
139264
139533
  import { randomUUID as randomUUID2 } from "node:crypto";
139265
139534
 
139535
+ // ../../app/t/[template_id]/readOrder.ts
139536
+ var LINE_OVERLAP_FRAC = 0.5;
139537
+ var LEFT_OF_SLACK_PCT = 0.5;
139538
+ function boxEdges(box) {
139539
+ return {
139540
+ left: box.xpct,
139541
+ top: box.ypct,
139542
+ right: box.xpct + box.wpct,
139543
+ bottom: box.ypct + box.hpct
139544
+ };
139545
+ }
139546
+ function membersEdges(members3) {
139547
+ if (members3.length === 0) return null;
139548
+ let x0 = Infinity;
139549
+ let y0 = Infinity;
139550
+ let x1 = -Infinity;
139551
+ let y1 = -Infinity;
139552
+ for (const m of members3) {
139553
+ x0 = Math.min(x0, m.xpct);
139554
+ y0 = Math.min(y0, m.ypct);
139555
+ x1 = Math.max(x1, m.xpct + m.wpct);
139556
+ y1 = Math.max(y1, m.ypct + m.hpct);
139557
+ }
139558
+ return boxEdges({ xpct: x0, ypct: y0, wpct: x1 - x0, hpct: y1 - y0 });
139559
+ }
139560
+ function sameLine(a, b) {
139561
+ const overlap2 = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
139562
+ const small = Math.min(a.bottom - a.top, b.bottom - b.top);
139563
+ return small > 0 && overlap2 >= LINE_OVERLAP_FRAC * small;
139564
+ }
139565
+ function leftOf(a, b) {
139566
+ return a.left < b.left && a.right <= b.left + LEFT_OF_SLACK_PCT;
139567
+ }
139568
+ function readingLines(blocks) {
139569
+ const lines = [];
139570
+ let latest;
139571
+ for (const b of [...blocks].sort((p4, q) => p4.top - q.top || p4.left - q.left)) {
139572
+ if (latest?.every((o) => sameLine(b, o))) {
139573
+ latest.push(b);
139574
+ } else {
139575
+ latest = [b];
139576
+ lines.push(latest);
139577
+ }
139578
+ }
139579
+ return lines.map((ln2) => {
139580
+ const row = [];
139581
+ for (const b of ln2) {
139582
+ const at = row.findIndex((o) => leftOf(b, o));
139583
+ row.splice(at < 0 ? row.length : at, 0, b);
139584
+ }
139585
+ return row;
139586
+ });
139587
+ }
139588
+ function orderBlocks(blocks) {
139589
+ if (!blocks.some((b) => b.order != null)) return readingLines(blocks).flat();
139590
+ return [...blocks].sort((a, b) => {
139591
+ const oa = a.order ?? Number.POSITIVE_INFINITY;
139592
+ const ob = b.order ?? Number.POSITIVE_INFINITY;
139593
+ if (oa !== ob) return oa - ob;
139594
+ return a.top - b.top || a.left - b.left;
139595
+ });
139596
+ }
139597
+
139598
+ // ../../app/t/[template_id]/pageLayout.ts
139599
+ var orderOf = (b) => b.kind === "field" ? b.field.order : b.group.order;
139600
+ function buildPageItems(page, pageFields, pageGroups, resolve3) {
139601
+ const placed = [];
139602
+ const place = (block, edges) => {
139603
+ placed.push({ ...edges, block, order: orderOf(block) });
139604
+ };
139605
+ for (const f3 of pageFields) {
139606
+ place({ kind: "field", top: f3.ypct, left: f3.xpct, key: `f${f3.id}`, field: f3 }, boxEdges(f3));
139607
+ }
139608
+ for (const g of pageGroups) {
139609
+ const members3 = resolve3(g);
139610
+ const edges = membersEdges(members3);
139611
+ if (!edges) continue;
139612
+ const { top, left } = edges;
139613
+ place({ kind: "group", top, left, key: `g${g.id}`, group: g, members: members3 }, edges);
139614
+ }
139615
+ const sectionOf2 = (b) => b.kind === "field" ? b.field.section_id : b.members[0]?.section_id ?? null;
139616
+ const sorted = orderBlocks(placed).map((p4) => p4.block);
139617
+ const secById = new Map(page.sections.map((s) => [s.id, s]));
139618
+ const units = [];
139619
+ const sectionUnit = /* @__PURE__ */ new Map();
139620
+ for (const b of sorted) {
139621
+ const sid = sectionOf2(b);
139622
+ const sec = sid != null ? secById.get(sid) : void 0;
139623
+ if (!sec) {
139624
+ units.push({ section: null, blocks: [b] });
139625
+ continue;
139626
+ }
139627
+ let u = sectionUnit.get(sec.id);
139628
+ if (!u) {
139629
+ u = { section: sec, blocks: [] };
139630
+ sectionUnit.set(sec.id, u);
139631
+ units.push(u);
139632
+ }
139633
+ u.blocks.push(b);
139634
+ }
139635
+ const items2 = [];
139636
+ for (const u of units) {
139637
+ if (u.section) items2.push({ kind: "header", key: `h${u.section.id}`, section: u.section });
139638
+ for (const b of u.blocks) items2.push(b);
139639
+ }
139640
+ return { page, items: items2 };
139641
+ }
139642
+
139266
139643
  // src/doc.ts
139267
139644
  var OpError = class extends Error {
139268
139645
  };
139269
139646
  var bad = (msg) => {
139270
139647
  throw new OpError(msg);
139271
139648
  };
139272
- var isRec = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2);
139649
+ var isRec2 = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2);
139273
139650
  var isNum = (x2) => typeof x2 === "number" && Number.isFinite(x2);
139274
139651
  var round = (n, d = 4) => Math.round(n * 10 ** d) / 10 ** d;
139275
139652
  function index(s) {
@@ -139383,7 +139760,7 @@ function checkBoxOnPage(b) {
139383
139760
  if (!onPage(b)) bad(ON_PAGE);
139384
139761
  }
139385
139762
  function boxArg(v, name = "box") {
139386
- if (!isRec(v)) return bad(`${name} {x, y, w, h} is required (fractions of the page, top-left)`);
139763
+ if (!isRec2(v)) return bad(`${name} {x, y, w, h} is required (fractions of the page, top-left)`);
139387
139764
  const b = {
139388
139765
  x: checkFraction(v.x, `${name}.x`),
139389
139766
  y: checkFraction(v.y, `${name}.y`),
@@ -139461,7 +139838,7 @@ function patchForDataType(t, dateFormat) {
139461
139838
  }
139462
139839
  var DEFAULT_TEXT = { type: "text", format: { variant: "any" } };
139463
139840
  function variantOf(f3) {
139464
- return isRec(f3.format) && typeof f3.format.variant === "string" ? f3.format.variant : "any";
139841
+ return isRec2(f3.format) && typeof f3.format.variant === "string" ? f3.format.variant : "any";
139465
139842
  }
139466
139843
  function isDefaultText2(f3) {
139467
139844
  return f3.type === "text" && variantOf(f3) === "any";
@@ -139573,7 +139950,7 @@ function lineType(d, v, i2) {
139573
139950
  const w = cells.find((c2) => !isDefaultText2(c2)) ?? cells[0];
139574
139951
  return {
139575
139952
  type: String(w.type),
139576
- ...isRec(w.format) ? { format: structuredClone(w.format) } : {}
139953
+ ...isRec2(w.format) ? { format: structuredClone(w.format) } : {}
139577
139954
  };
139578
139955
  }
139579
139956
  var axisWord = (a) => a === "col" ? "column" : "row";
@@ -139848,7 +140225,7 @@ function tableInsert(d, op, w) {
139848
140225
  }
139849
140226
  let at;
139850
140227
  if (op.box !== void 0) {
139851
- if (!isRec(op.box)) return bad("box must be an object");
140228
+ if (!isRec2(op.box)) return bad("box must be an object");
139852
140229
  const b = op.box;
139853
140230
  const [p4, s] = axis === "row" ? ["y", "h"] : ["x", "w"];
139854
140231
  const pos = b[p4];
@@ -139919,8 +140296,8 @@ function tableDelete(d, op, w) {
139919
140296
  }
139920
140297
  if (v.cols === 0) v.grid = [];
139921
140298
  v.rows = v.grid.length;
139922
- const keep = op.keep_cells === true;
139923
- if (keep) {
140299
+ const keep2 = op.keep_cells === true;
140300
+ if (keep2) {
139924
140301
  for (const id of ids) {
139925
140302
  const f3 = d.field.get(id);
139926
140303
  if (f3) f3.group = null;
@@ -139929,9 +140306,9 @@ function tableDelete(d, op, w) {
139929
140306
  } else removeFields(d, ids);
139930
140307
  const alive = commit(d, v, hc, hr);
139931
140308
  if (!alive) w.push(`that was the last line of table ${v.g.id}, so the table was removed`);
139932
- const deleted = [...keep ? [] : ids, ...alive ? [] : [v.g.id]];
140309
+ const deleted = [...keep2 ? [] : ids, ...alive ? [] : [v.g.id]];
139933
140310
  return {
139934
- touched: [...alive ? [v.g.id] : [], ...keep ? ids : []],
140311
+ touched: [...alive ? [v.g.id] : [], ...keep2 ? ids : []],
139935
140312
  deleted,
139936
140313
  tables: alive ? [v.g.id] : []
139937
140314
  };
@@ -139982,7 +140359,7 @@ function tableAdopt(d, op, w) {
139982
140359
  const { r, c: c2 } = slotArgs(v, op);
139983
140360
  const tf = slotType(d, v, r, c2, {
139984
140361
  type: String(f3.type),
139985
- ...isRec(f3.format) ? { format: f3.format } : {}
140362
+ ...isRec2(f3.format) ? { format: f3.format } : {}
139986
140363
  });
139987
140364
  if (!sameType(f3, tf)) {
139988
140365
  w.push(
@@ -140221,7 +140598,7 @@ var FORMAT_KEYS = {
140221
140598
  date: /* @__PURE__ */ new Set(["date_format", "font_size", "text_anchor"]),
140222
140599
  signature: /* @__PURE__ */ new Set(["scale", "text_anchor"])
140223
140600
  };
140224
- var isRec2 = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2);
140601
+ var isRec3 = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2);
140225
140602
  var isNum2 = (x2) => typeof x2 === "number" && Number.isFinite(x2);
140226
140603
  var isInt = (x2) => isNum2(x2) && Number.isInteger(x2);
140227
140604
  function checkStr(out, v, max2, at, optional2) {
@@ -140254,7 +140631,7 @@ function checkAnchor(out, v, at) {
140254
140631
  function formatIssues(type, fmt3, id, path) {
140255
140632
  const out = [];
140256
140633
  if (fmt3 === void 0 || fmt3 === null) return out;
140257
- if (!isRec2(fmt3)) return [{ id, path, reason: "format must be an object" }];
140634
+ if (!isRec3(fmt3)) return [{ id, path, reason: "format must be an object" }];
140258
140635
  const allowed = typeof type === "string" ? FORMAT_KEYS[type] : void 0;
140259
140636
  if (!allowed) return out;
140260
140637
  for (const k4 of Object.keys(fmt3)) {
@@ -140291,7 +140668,7 @@ function formatIssues(type, fmt3, id, path) {
140291
140668
  return out;
140292
140669
  }
140293
140670
  function fieldIssues(f3, path) {
140294
- if (!isRec2(f3)) return [{ path, reason: "must be an object" }];
140671
+ if (!isRec3(f3)) return [{ path, reason: "must be an object" }];
140295
140672
  const id = typeof f3.id === "string" ? f3.id : void 0;
140296
140673
  const out = [];
140297
140674
  const at = (k4) => ({ id, path: `${path}.${k4}` });
@@ -140314,7 +140691,7 @@ function fieldIssues(f3, path) {
140314
140691
  else {
140315
140692
  f3.cells.forEach((c2, i2) => {
140316
140693
  const cp = { id, path: `${path}.cells.${String(i2)}` };
140317
- if (!isRec2(c2)) {
140694
+ if (!isRec3(c2)) {
140318
140695
  out.push({ ...cp, reason: "must be an object" });
140319
140696
  return;
140320
140697
  }
@@ -140340,7 +140717,7 @@ function fieldIssues(f3, path) {
140340
140717
  return out;
140341
140718
  }
140342
140719
  function groupIssues(g, path) {
140343
- if (!isRec2(g)) return [{ path, reason: "must be an object" }];
140720
+ if (!isRec3(g)) return [{ path, reason: "must be an object" }];
140344
140721
  const id = typeof g.id === "string" ? g.id : void 0;
140345
140722
  const out = [];
140346
140723
  const at = (k4) => ({ id, path: `${path}.${k4}` });
@@ -140398,7 +140775,7 @@ function groupIssues(g, path) {
140398
140775
  return out;
140399
140776
  }
140400
140777
  function sectionIssues(s, path) {
140401
- if (!isRec2(s)) return [{ path, reason: "must be an object" }];
140778
+ if (!isRec3(s)) return [{ path, reason: "must be an object" }];
140402
140779
  const id = typeof s.id === "string" ? s.id : void 0;
140403
140780
  const out = [];
140404
140781
  const at = (k4) => ({ id, path: `${path}.${k4}` });
@@ -140423,7 +140800,7 @@ function schemaIssues(schema, limit = 50) {
140423
140800
  if (!Array.isArray(s.pages)) out.push({ path: "pages", reason: "must be a list" });
140424
140801
  pages.forEach((p4, i2) => {
140425
140802
  const path = `pages.${String(i2)}`;
140426
- if (!isRec2(p4)) {
140803
+ if (!isRec3(p4)) {
140427
140804
  out.push({ path, reason: "must be an object" });
140428
140805
  return;
140429
140806
  }
@@ -140524,7 +140901,9 @@ var OP_NAMES = [
140524
140901
  "set_orientation",
140525
140902
  "set_header",
140526
140903
  "set_headers",
140527
- "reorder"
140904
+ "reorder",
140905
+ "set_section",
140906
+ "set_section_title"
140528
140907
  ];
140529
140908
  function placeBox(fs, bb, nb) {
140530
140909
  if (nb.w <= 0 || nb.h <= 0) bad("the box must have a positive width and height");
@@ -140550,25 +140929,25 @@ function sectionIds(d, page) {
140550
140929
  return out;
140551
140930
  }
140552
140931
  function typeAndFormat(t, prev) {
140553
- const prevFmt = isRec(prev?.format) ? prev.format : {};
140554
- const keep = (keys) => Object.fromEntries(keys.filter((k4) => prevFmt[k4] != null).map((k4) => [k4, prevFmt[k4]]));
140932
+ const prevFmt = isRec2(prev?.format) ? prev.format : {};
140933
+ const keep2 = (keys) => Object.fromEntries(keys.filter((k4) => prevFmt[k4] != null).map((k4) => [k4, prevFmt[k4]]));
140555
140934
  switch (t) {
140556
140935
  case "text":
140557
140936
  return {
140558
140937
  type: "text",
140559
- format: { ...keep(["font_size", "text_anchor", "uppercase"]), variant: "any" }
140938
+ format: { ...keep2(["font_size", "text_anchor", "uppercase"]), variant: "any" }
140560
140939
  };
140561
140940
  case "multiline":
140562
140941
  return {
140563
140942
  type: "text",
140564
- format: { ...keep(["font_size", "uppercase"]), variant: "multiline" }
140943
+ format: { ...keep2(["font_size", "uppercase"]), variant: "multiline" }
140565
140944
  };
140566
140945
  case "date":
140567
140946
  return {
140568
140947
  type: "date",
140569
140948
  format: {
140570
140949
  date_format: typeof prevFmt.date_format === "string" ? prevFmt.date_format : "DD/MM/YYYY",
140571
- ...keep(["font_size", "text_anchor"])
140950
+ ...keep2(["font_size", "text_anchor"])
140572
140951
  }
140573
140952
  };
140574
140953
  case "checkbox":
@@ -140596,7 +140975,7 @@ function entryType(d, id) {
140596
140975
  }
140597
140976
  const f3 = d.field.get(id);
140598
140977
  if (!f3) return "?";
140599
- if (f3.type === "text" && isRec(f3.format) && f3.format.variant === "multiline") return "multiline";
140978
+ if (f3.type === "text" && isRec2(f3.format) && f3.format.variant === "multiline") return "multiline";
140600
140979
  if ((f3.type === "text" || f3.type === "date") && Array.isArray(f3.cells) && f3.cells.length > 1)
140601
140980
  return "comb";
140602
140981
  return String(f3.type);
@@ -140644,7 +141023,7 @@ function checkSnapTarget(g, op) {
140644
141023
  if (op.snap !== void 0 && g) bad("snap applies to a single field, not a group");
140645
141024
  }
140646
141025
  function applyOne(d, op, warnings, opts) {
140647
- if (!isRec(op) || typeof op.op !== "string") return bad("each op needs an `op` name");
141026
+ if (!isRec2(op) || typeof op.op !== "string") return bad("each op needs an `op` name");
140648
141027
  const raw = op;
140649
141028
  const table = (r) => r;
140650
141029
  switch (op.op) {
@@ -140802,29 +141181,36 @@ function applyOne(d, op, warnings, opts) {
140802
141181
  }
140803
141182
  case "set_format": {
140804
141183
  const { f: f3, g } = needId(d, op.id);
140805
- if (!isRec(op.format)) return bad("format must be an object");
141184
+ if (!isRec2(op.format)) return bad("format must be an object");
140806
141185
  if (g) {
140807
141186
  if (g.kind !== "comb") {
140808
141187
  return bad(
140809
141188
  g.kind === "table" ? "a table's formats live on its cells: set_format a cell (it applies to the cell's whole column or row), or set_column_type" : "set_format applies to a field, or to a comb group (cell_type, date_format)"
140810
141189
  );
140811
141190
  }
141191
+ const comb = g;
141192
+ const dated = op.format.date_format !== void 0 && op.format.date_format !== null;
140812
141193
  for (const [k4, v] of Object.entries(op.format)) {
140813
141194
  if (k4 !== "cell_type" && k4 !== "date_format")
140814
141195
  bad(`a comb group's format keys are cell_type and date_format, not ${k4}`);
140815
141196
  if (k4 === "date_format" && v !== null) checkCombDateFormat(d, g, v);
140816
- if (v === null) delete g[k4];
140817
- else g[k4] = v;
141197
+ if (v === null) delete comb[k4];
141198
+ else comb[k4] = v;
140818
141199
  }
140819
- return { touched: [g.id] };
141200
+ if (dated) {
141201
+ if (op.format.cell_type !== void 0 && op.format.cell_type !== "date")
141202
+ bad('date_format needs cell_type "date": leave cell_type out, or pass "date"');
141203
+ comb.cell_type = "date";
141204
+ }
141205
+ return { touched: [g.id], comb: g.id };
140820
141206
  }
140821
141207
  const fld = f3;
140822
141208
  const own2 = d.owner.get(fld.id);
140823
141209
  if (own2 && (own2.kind === "comb" || own2.kind === "choice")) {
140824
141210
  bad(`this is a member of ${String(own2.kind)} group ${own2.id}, whose kind fixes its format`);
140825
141211
  }
140826
- if (fld.type === "signature" && !isRec(fld.format)) fld.format = {};
140827
- const next = { ...isRec(fld.format) ? fld.format : {} };
141212
+ if (fld.type === "signature" && !isRec2(fld.format)) fld.format = {};
141213
+ const next = { ...isRec2(fld.format) ? fld.format : {} };
140828
141214
  for (const [k4, v] of Object.entries(op.format)) {
140829
141215
  if (k4 === "date_format" && v !== null && !DATE_FORMATS.includes(v))
140830
141216
  bad(`date_format must be one of ${DATE_FORMATS.join(", ")}`);
@@ -140839,7 +141225,7 @@ function applyOne(d, op, warnings, opts) {
140839
141225
  delete fld.format;
140840
141226
  else fld.format = next;
140841
141227
  if (own2?.kind === "table") {
140842
- applyFormatToLine(d, own2, fld, isRec(fld.format) ? fld.format : void 0, warnings);
141228
+ applyFormatToLine(d, own2, fld, isRec2(fld.format) ? fld.format : void 0, warnings);
140843
141229
  return { touched: [fld.id, own2.id], tables: [own2.id] };
140844
141230
  }
140845
141231
  return { touched: [fld.id] };
@@ -140926,7 +141312,7 @@ function applyOne(d, op, warnings, opts) {
140926
141312
  });
140927
141313
  } else {
140928
141314
  for (const o of op.options) {
140929
- if (!isRec(o) || typeof o.id !== "string" || typeof o.label !== "string") {
141315
+ if (!isRec2(o) || typeof o.id !== "string" || typeof o.label !== "string") {
140930
141316
  return bad("options must be all strings, or all {id, label} pairs");
140931
141317
  }
140932
141318
  const oid = resolveId(d, o.id, ["field"], "option id");
@@ -140947,7 +141333,7 @@ function applyOne(d, op, warnings, opts) {
140947
141333
  const n = ms.length;
140948
141334
  if (op.index !== void 0 && (!Number.isInteger(op.index) || op.index < 0 || op.index > n))
140949
141335
  return bad(`index must be 0\u2013${String(n)} (${String(n)} = last)`);
140950
- const sib = ms.find((m) => m.type === "checkbox" && isRec(m.format));
141336
+ const sib = ms.find((m) => m.type === "checkbox" && isRec2(m.format));
140951
141337
  const f3 = {
140952
141338
  id: randomUUID2(),
140953
141339
  label: op.label.trim(),
@@ -141027,29 +141413,31 @@ function applyOne(d, op, warnings, opts) {
141027
141413
  case "set_headers":
141028
141414
  return table(setHeaders(d, raw, warnings));
141029
141415
  case "reorder":
141030
- return reorder(d, raw);
141416
+ return reorder(d, raw, warnings);
141417
+ case "set_section":
141418
+ return setSection(d, raw);
141419
+ case "set_section_title":
141420
+ return setSectionTitle(d, raw);
141031
141421
  default:
141032
141422
  return bad(`unknown op "${String(op.op)}"; ops: ${OP_NAMES.join(", ")}`);
141033
141423
  }
141034
141424
  }
141035
- function reorder(d, op) {
141425
+ function reorder(d, op, warnings) {
141036
141426
  const page = pageOf(d, op.page);
141037
141427
  if (!Array.isArray(op.ids) || op.ids.length === 0)
141038
141428
  return bad("ids must list the entries to order");
141039
- const blocks = [];
141429
+ const orderOf2 = (item) => typeof item.order === "number" ? item.order : void 0;
141430
+ const unordered = [];
141040
141431
  for (const f3 of d.s.fields) {
141041
- if (f3.page === page && !d.owner.has(f3.id)) blocks.push({ item: f3, top: f3.ypct, left: f3.xpct });
141432
+ if (f3.page === page && !d.owner.has(f3.id))
141433
+ unordered.push({ ...boxEdges(f3), item: f3, order: orderOf2(f3) });
141042
141434
  }
141043
141435
  for (const g of d.s.groups) {
141044
141436
  if (g.page !== page) continue;
141045
- const bb = bbox(members2(d, g));
141046
- if (bb) blocks.push({ item: g, top: bb.y, left: bb.x });
141437
+ const edges = membersEdges(members2(d, g));
141438
+ if (edges) unordered.push({ ...edges, item: g, order: orderOf2(g) });
141047
141439
  }
141048
- const orderOf = (b) => typeof b.item.order === "number" ? b.item.order : void 0;
141049
- const hasOrder = blocks.some((b) => orderOf(b) !== void 0);
141050
- blocks.sort(
141051
- (a, b) => (hasOrder ? (orderOf(a) ?? Infinity) - (orderOf(b) ?? Infinity) || 0 : 0) || a.top - b.top || a.left - b.left
141052
- );
141440
+ const blocks = orderBlocks(unordered);
141053
141441
  const byId = new Map(blocks.map((b) => [b.item.id, b]));
141054
141442
  const want = op.ids.map((raw) => {
141055
141443
  const id = resolveId(d, raw, ["field", "group"], "ids entry");
@@ -141070,10 +141458,132 @@ function reorder(d, op) {
141070
141458
  ...want.map((id) => byId.get(id)),
141071
141459
  ...rest.slice(before)
141072
141460
  ];
141461
+ const was = listedOrder(d, page);
141073
141462
  next.forEach((b, i2) => {
141074
141463
  b.item.order = i2;
141075
141464
  });
141076
- return { touched: want };
141465
+ const listed = listedOrder(d, page);
141466
+ const from = listed.indexOf(want[0]);
141467
+ const asAsked = from !== -1 && want.every((id, k4) => listed[from + k4] === id);
141468
+ if (!asAsked) {
141469
+ const why = `a section is listed as one run, standing where its first entry stands, and these ids span several (${sectionsOf(d, page, want)})`;
141470
+ const fix = "set_section {ids, section_id} moves an entry to another section; inside one section any order works";
141471
+ if (listed.every((id, k4) => id === was[k4])) {
141472
+ return bad(
141473
+ `this order would change nothing in the listing: ${why}. ${fix[0].toUpperCase()}${fix.slice(1)}`
141474
+ );
141475
+ }
141476
+ warnings.push(
141477
+ `reorder on page ${String(page)}: the ids are not all listed in the order asked, because ${why}. diff_summary.ops shows the order now listed; ${fix}`
141478
+ );
141479
+ }
141480
+ return { touched: want, order: listed };
141481
+ }
141482
+ function pageSections(d, page) {
141483
+ const pg = d.s.pages.find((p4) => p4.page === page);
141484
+ return (Array.isArray(pg?.sections) ? pg.sections : []).flatMap(
141485
+ (s) => isRec2(s) && typeof s.id === "string" ? [{ id: s.id, title: typeof s.title === "string" ? s.title : "" }] : []
141486
+ );
141487
+ }
141488
+ function readingMembers(d, g) {
141489
+ if (!Array.isArray(g.members)) return [];
141490
+ const byId = buildFieldById(d.s.fields);
141491
+ return resolveMembers(
141492
+ g,
141493
+ byId
141494
+ );
141495
+ }
141496
+ function listedOrder(d, page) {
141497
+ const info = d.s.pages.find((p4) => p4.page === page);
141498
+ const fields = d.s.fields.filter((f3) => f3.page === page && !d.owner.has(f3.id));
141499
+ const groups = d.s.groups.filter((g) => g.page === page && Array.isArray(g.members));
141500
+ const byId = buildFieldById(d.s.fields);
141501
+ const layout = buildPageItems(
141502
+ { ...info, page, sections: pageSections(d, page) },
141503
+ fields,
141504
+ groups,
141505
+ (g) => resolveMembers(g, byId)
141506
+ );
141507
+ return layout.items.flatMap(
141508
+ (it) => it.kind === "header" ? [] : [it.kind === "field" ? it.field.id : it.group.id]
141509
+ );
141510
+ }
141511
+ function sectionOf(d, page, id) {
141512
+ const g = d.group.get(id);
141513
+ const sid = g ? readingMembers(d, g)[0]?.section_id : d.field.get(id)?.section_id;
141514
+ if (typeof sid !== "string") return null;
141515
+ return pageSections(d, page).find((s) => s.id === sid) ?? null;
141516
+ }
141517
+ function sectionsOf(d, page, ids) {
141518
+ const by = /* @__PURE__ */ new Map();
141519
+ for (const id of ids) {
141520
+ const sec = sectionOf(d, page, id);
141521
+ const key = sec ? sec.id : "";
141522
+ const row = by.get(key) ?? {
141523
+ name: sec ? `section ${JSON.stringify(sec.title)} (${sec.id})` : "no section",
141524
+ ids: []
141525
+ };
141526
+ row.ids.push(id);
141527
+ by.set(key, row);
141528
+ }
141529
+ return [...by.values()].map(
141530
+ (r) => `${r.name}: ${r.ids.slice(0, 3).join(", ")}${r.ids.length > 3 ? ` and ${String(r.ids.length - 3)} more` : ""}`
141531
+ ).join("; ");
141532
+ }
141533
+ function setSection(d, op) {
141534
+ if (!Array.isArray(op.ids) || op.ids.length === 0)
141535
+ return bad("ids must list the fields or groups to move");
141536
+ const ids = [
141537
+ ...new Set(
141538
+ op.ids.map((raw) => {
141539
+ const id = resolveId(d, raw, ["field", "group"], "ids entry");
141540
+ const own2 = d.owner.get(id);
141541
+ if (own2) return bad(`${id} is an option/cell of group ${own2.id}; move the group`);
141542
+ return id;
141543
+ })
141544
+ )
141545
+ ];
141546
+ const pageOfEntry = (id) => (d.group.get(id) ?? d.field.get(id))?.page;
141547
+ const page = pageOfEntry(ids[0]);
141548
+ if (typeof page !== "number" || ids.some((id) => pageOfEntry(id) !== page))
141549
+ return bad("all ids must be on one page: a section belongs to one page");
141550
+ let section = null;
141551
+ if (op.section_id === void 0) {
141552
+ return bad("section_id is required (null takes the entries out of every section)");
141553
+ }
141554
+ if (op.section_id !== null) {
141555
+ const sid = resolveId(d, op.section_id, ["section"], "section_id");
141556
+ const found = pageSections(d, page).find((s) => s.id === sid);
141557
+ if (!found) {
141558
+ const have = pageSections(d, page).map((s) => `${s.id} ${JSON.stringify(s.title)}`);
141559
+ return bad(
141560
+ `section_id ${sid} is not a section of page ${String(page)} (${have.length ? `its sections: ${have.join(", ")}` : "the page has no sections"})`
141561
+ );
141562
+ }
141563
+ section = found;
141564
+ }
141565
+ for (const id of ids) {
141566
+ const g = d.group.get(id);
141567
+ for (const f3 of g ? members2(d, g) : [d.field.get(id)]) {
141568
+ f3.section_id = section ? section.id : null;
141569
+ }
141570
+ }
141571
+ return { touched: ids, section };
141572
+ }
141573
+ function setSectionTitle(d, op) {
141574
+ const page = pageOf(d, op.page);
141575
+ const sid = resolveId(d, op.section_id, ["section"], "section_id");
141576
+ const pg = d.s.pages.find((p4) => p4.page === page);
141577
+ const sec = (Array.isArray(pg?.sections) ? pg.sections : []).find(
141578
+ (s) => isRec2(s) && s.id === sid
141579
+ );
141580
+ if (!sec) return bad(`section_id ${sid} is not a section of page ${String(page)}`);
141581
+ if (typeof op.title !== "string" || !op.title.trim())
141582
+ return bad("title is required: the heading the form prints over these fields");
141583
+ sec.title = op.title.trim();
141584
+ const issues = sectionIssues(sec, `section ${sid}`);
141585
+ if (issues.length) return bad(issues.map((i2) => `${i2.path} ${i2.reason}`).join("; "));
141586
+ return { touched: [], section: { id: sid, title: sec.title } };
141077
141587
  }
141078
141588
  function checkCombDateFormat(d, g, v) {
141079
141589
  const slots = members2(d, g).reduce(
@@ -141086,6 +141596,18 @@ function checkCombDateFormat(d, g, v) {
141086
141596
  );
141087
141597
  }
141088
141598
  }
141599
+ function combFormat(render, id) {
141600
+ try {
141601
+ const schema = normalizeSchema(
141602
+ structuredClone(render)
141603
+ );
141604
+ const e4 = agentEntries(schema).find((x2) => x2.id === id);
141605
+ if (e4?.kind !== "comb") return null;
141606
+ return { cell_type: e4.cellType, date_format: e4.dateFormat, max_length: e4.maxLength };
141607
+ } catch {
141608
+ return null;
141609
+ }
141610
+ }
141089
141611
  function touchedIssues(d, ids, tables = []) {
141090
141612
  const out = [];
141091
141613
  const seen = /* @__PURE__ */ new Set();
@@ -141127,6 +141649,8 @@ function applyPass(render, ops, excluded, screen, opts) {
141127
141649
  const touchedIds = /* @__PURE__ */ new Set();
141128
141650
  const warnings = [];
141129
141651
  const outcomes = [];
141652
+ const told = [];
141653
+ const confirmed = [];
141130
141654
  let applied = 0;
141131
141655
  ops.forEach((raw, i2) => {
141132
141656
  const skip = excluded.get(i2);
@@ -141145,30 +141669,46 @@ function applyPass(render, ops, excluded, screen, opts) {
141145
141669
  issues.slice(0, 5).map((x2) => `${x2.path} ${x2.reason}`).join("; ")
141146
141670
  );
141147
141671
  }
141672
+ let vouched = [];
141148
141673
  if (screen) {
141149
141674
  const changed = changedWording(cur, draft);
141150
- const echo = changed.length ? screen(changed) : { refuse: [], warn: [] };
141151
- if (echo.refuse.length) {
141152
- bad(
141153
- `${describeWording(changed, echo.refuse)} matches an answer filled in this session. Labels, descriptions, placeholders, options and formats hold the form's own wording, never an answer: answers go only in fill values`
141154
- );
141675
+ const echo = changed.length ? screen(changed) : null;
141676
+ if (echo?.refused.length) {
141677
+ const known = index(cur);
141678
+ const name2 = (id) => known.field.has(id) || known.group.has(id) || known.section.has(id) ? id : "the new entry";
141679
+ if (opts.confirmWording === true && echo.refused.every((x2) => x2.shown)) {
141680
+ vouched = echo.refused;
141681
+ w.push(
141682
+ `${describeWording(echo.refused, name2)}: matches an answer filled in this session; applied because confirm_wording says it is the form's own wording`
141683
+ );
141684
+ } else {
141685
+ told.push(...echo.refused);
141686
+ bad(
141687
+ `${describeWording(echo.refused, name2)} matches an answer filled in this session. Labels, descriptions, placeholders, options and formats hold the form's own wording, never an answer: answers go only in fill values. If this is the form's printed wording, send the op again with confirm_wording: true on the call`
141688
+ );
141689
+ }
141155
141690
  }
141156
- for (const id of echo.warn) {
141691
+ for (const id of echo?.warn ?? []) {
141157
141692
  w.push(
141158
- `${describeWording(changed, [id])}: short wording that matches a short answer from this session (allowed under 4 characters); check it is the form's own text`
141693
+ `${describeWording(changed.filter((x2) => x2.id === id))}: short wording that matches a short answer from this session (allowed under 4 characters); check it is the form's own text`
141159
141694
  );
141160
141695
  }
141161
141696
  }
141162
141697
  const name = raw.op;
141163
141698
  const noop = JSON.stringify(draft) === JSON.stringify(cur);
141699
+ const comb = r.comb !== void 0 ? combFormat(draft, r.comb) : null;
141164
141700
  outcomes.push({
141165
141701
  op_index: i2,
141166
141702
  op: name,
141167
141703
  ...r.snapped !== void 0 ? { snapped: r.snapped } : {},
141704
+ ...comb ? { comb } : {},
141705
+ ...r.order ? { order: r.order } : {},
141706
+ ...r.section !== void 0 ? { section: r.section } : {},
141168
141707
  ...noop ? { noop: true } : {}
141169
141708
  });
141170
141709
  cur = draft;
141171
141710
  applied++;
141711
+ confirmed.push(...vouched);
141172
141712
  counts[name] = (counts[name] ?? 0) + 1;
141173
141713
  if (r.added) added.push({ op_index: i2, id: r.added });
141174
141714
  if (r.geometry) placed.set(i2, r.geometry);
@@ -141210,16 +141750,17 @@ function applyPass(render, ops, excluded, screen, opts) {
141210
141750
  applied,
141211
141751
  rejected,
141212
141752
  diff_summary: { counts, added, deleted, touched, ops: outcomes },
141213
- warnings
141753
+ warnings,
141754
+ screened: { told, confirmed }
141214
141755
  },
141215
141756
  offPage
141216
141757
  };
141217
141758
  }
141218
141759
  function wordingOf(s) {
141219
141760
  const out = [];
141220
- const add = (id, prop, v) => {
141761
+ const add = (id, prop, v, page) => {
141221
141762
  if (typeof v === "string" && v.trim() && typeof id === "string")
141222
- out.push({ id, prop, text: v });
141763
+ out.push({ id, ...typeof page === "number" ? { page } : {}, prop, text: v });
141223
141764
  };
141224
141765
  const rec = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2) ? x2 : null;
141225
141766
  for (const f3 of s.fields) {
@@ -141239,26 +141780,36 @@ function wordingOf(s) {
141239
141780
  for (const p4 of s.pages) {
141240
141781
  const secs = Array.isArray(p4.sections) ? p4.sections : [];
141241
141782
  for (const sec of secs)
141242
- for (const k4 of ["title", "description", "placeholder"]) add(sec.id, k4, sec[k4]);
141783
+ for (const k4 of ["title", "description", "placeholder"]) add(sec.id, k4, sec[k4], p4.page);
141243
141784
  }
141244
141785
  return out;
141245
141786
  }
141246
141787
  function changedWording(prev, next) {
141247
- const before = new Set(wordingOf(prev).map((w) => `${w.id}\0${w.prop}\0${w.text}`));
141248
- return wordingOf(next).filter((w) => !before.has(`${w.id}\0${w.prop}\0${w.text}`));
141788
+ const at = (w) => `${entryKey(w)}\0${w.prop}\0${w.text}`;
141789
+ const before = new Set(wordingOf(prev).map(at));
141790
+ return wordingOf(next).filter((w) => !before.has(at(w)));
141249
141791
  }
141250
141792
  function wordingChanges(changed) {
141251
141793
  const by = /* @__PURE__ */ new Map();
141252
141794
  for (const w of changed) {
141253
- const set2 = by.get(w.id) ?? /* @__PURE__ */ new Set();
141254
- set2.add(w.prop);
141255
- by.set(w.id, set2);
141256
- }
141257
- return [...by].map(([id, props]) => ({ id, properties: [...props].sort() }));
141795
+ const key = entryKey(w);
141796
+ const entry = by.get(key) ?? {
141797
+ id: w.id,
141798
+ ...w.page === void 0 ? {} : { page: w.page },
141799
+ props: /* @__PURE__ */ new Set()
141800
+ };
141801
+ entry.props.add(w.prop);
141802
+ by.set(key, entry);
141803
+ }
141804
+ return [...by.values()].map(({ props, ...entry }) => ({
141805
+ ...entry,
141806
+ properties: [...props].sort()
141807
+ }));
141258
141808
  }
141259
- function describeWording(changed, ids) {
141260
- const want = new Set(ids);
141261
- return wordingChanges(changed.filter((w) => want.has(w.id))).map((c2) => `the ${c2.properties.join("/")} of ${c2.id}`).join(", ");
141809
+ function describeWording(wording, name = (id) => id) {
141810
+ return wordingChanges(wording).map(
141811
+ (c2) => `the ${c2.properties.join("/")} of ${name(c2.id)}${c2.page === void 0 ? "" : ` on page ${String(c2.page)}`}`
141812
+ ).join(", ");
141262
141813
  }
141263
141814
 
141264
141815
  // src/render.ts
@@ -141339,14 +141890,14 @@ async function withPage(pdf, page, fn5) {
141339
141890
  p4.wasmExports.free(ptr);
141340
141891
  }
141341
141892
  }
141342
- async function renderPage(pdf, page, dpi, crop) {
141893
+ async function renderPage(pdf, page, dpi, crop, maxSide = MAX_SIDE_PX) {
141343
141894
  return withPage(pdf, page, (m, pg, heap) => {
141344
141895
  const wPt = m.FPDF_GetPageWidthF(pg);
141345
141896
  const hPt = m.FPDF_GetPageHeightF(pg);
141346
141897
  const c2 = crop ?? { x: 0, y: 0, w: 1, h: 1 };
141347
141898
  let scale2 = dpi / 72;
141348
141899
  const longest = Math.max(wPt * c2.w, hPt * c2.h) * scale2;
141349
- if (longest > MAX_SIDE_PX) scale2 *= MAX_SIDE_PX / longest;
141900
+ if (longest > maxSide) scale2 *= maxSide / longest;
141350
141901
  const fullW = Math.max(1, Math.round(wPt * scale2));
141351
141902
  const fullH = Math.max(1, Math.round(hPt * scale2));
141352
141903
  const x0 = Math.round(c2.x * fullW);
@@ -141375,7 +141926,7 @@ async function renderPage(pdf, page, dpi, crop) {
141375
141926
  const row = buf + y * stride;
141376
141927
  rgba.set(h8.subarray(row, row + width * 4), y * width * 4);
141377
141928
  }
141378
- return { width, height, rgba };
141929
+ return { width, height, rgba, scale: scale2 };
141379
141930
  } finally {
141380
141931
  m.FPDFBitmap_Destroy(bmp);
141381
141932
  }
@@ -141812,10 +142363,10 @@ var FONT_MIN_PT = 3;
141812
142363
  var OVERLAP_MIN_CHARS = 2;
141813
142364
  var CELL_TOL = 25e-4;
141814
142365
  var BLANK_CHARS = /^[_.\-‐-―…·•‧⋯─━┄┅┈┉_]$/u;
141815
- var multiline = (f3) => isRec(f3.format) && f3.format.variant === "multiline";
142366
+ var multiline = (f3) => isRec2(f3.format) && f3.format.variant === "multiline";
141816
142367
  var singleLine = (f3) => f3.type === "text" && !multiline(f3) || f3.type === "date" && !(f3.cells && f3.cells.length > 1);
141817
142368
  function pinned(f3) {
141818
- const s = isRec(f3.format) ? f3.format.font_size : void 0;
142369
+ const s = isRec2(f3.format) ? f3.format.font_size : void 0;
141819
142370
  return typeof s === "number" ? s : null;
141820
142371
  }
141821
142372
  function stampedSize(f3, pageHeightPt) {
@@ -141941,7 +142492,9 @@ async function placementWarnings2(pdf, render, ids, opts = {}) {
141941
142492
  }
141942
142493
 
141943
142494
  // src/preview.ts
142495
+ var OVERLAYS = ["full", "outline", "none"];
141944
142496
  var MAX_ZOOM = 8;
142497
+ var PREVIEW_MAX_SIDE_PX = 2e3;
141945
142498
  var r4 = (n) => Math.round(n * 1e4) / 1e4;
141946
142499
  function boxOf(fs) {
141947
142500
  if (!fs.length) return null;
@@ -141952,6 +142505,16 @@ function boxOf(fs) {
141952
142505
  return { x: r4(x0 / 100), y: r4(y0 / 100), w: r4((x1 - x0) / 100), h: r4((y1 - y0) / 100) };
141953
142506
  }
141954
142507
  var topId2 = (id) => id.split(/[.[]/, 1)[0];
142508
+ function idPages(entries) {
142509
+ const pages = /* @__PURE__ */ new Map();
142510
+ for (const e4 of entries) {
142511
+ const page = e4.kind === "field" ? e4.field.page : e4.group.page;
142512
+ pages.set(e4.id, page);
142513
+ const inner = e4.kind === "choice" ? e4.options : e4.kind === "table" ? e4.cells : e4.kind === "table_rows" ? e4.columns : [];
142514
+ for (const x2 of inner) if (!pages.has(x2.id)) pages.set(x2.id, page);
142515
+ }
142516
+ return pages;
142517
+ }
141955
142518
  function legendRows(render, page) {
141956
142519
  const schema = normalizeSchema(render);
141957
142520
  const entries = agentEntries(schema);
@@ -142087,6 +142650,7 @@ function legendChanges(before, now) {
142087
142650
  }
142088
142651
  async function previewPage(a) {
142089
142652
  const dpi = Math.min(MAX_DPI, Math.max(MIN_DPI, Math.round(a.dpi ?? DEFAULT_DPI)));
142653
+ const overlay = a.overlay ?? "full";
142090
142654
  const schema = normalizeSchema(a.render);
142091
142655
  const pages = schema.pages.map((p4) => p4.page);
142092
142656
  if (!pages.includes(a.page)) {
@@ -142104,27 +142668,41 @@ async function previewPage(a) {
142104
142668
  branding: null,
142105
142669
  logoBytes: null,
142106
142670
  loadImage,
142107
- check: true,
142671
+ // "none" shows the stamped PDF itself: no check copy is built, so none can fail.
142672
+ check: overlay !== "none",
142108
142673
  // preview_page's JSON carries the colour key; the banner would only
142109
142674
  // cover the top of page 1.
142110
- checkBanner: false
142675
+ checkBanner: false,
142676
+ checkTags: overlay === "full"
142111
142677
  });
142112
142678
  } catch (e4) {
142113
142679
  if (isPasswordError(e4)) throw passwordError();
142114
142680
  if (isFillForbiddenError(e4)) throw fillForbiddenError();
142115
142681
  throw e4;
142116
142682
  }
142117
- if (!res.checkPdf) throw new ToolError("internal_error", "The check copy could not be built.");
142683
+ const shown = overlay === "none" ? res.pdf : res.checkPdf;
142684
+ if (!shown) {
142685
+ const err2 = res.checkError;
142686
+ throw new ToolError("check_copy_failed", checkCopyFailed(err2), {
142687
+ hint: err2?.id ? `Fix or remove ${err2.id} with edit_template, then preview again.` : "Preview again; if it persists, fill_form / fill_template still write the filled PDF, without a check copy.",
142688
+ details: { stage: err2?.stage, id: err2?.id }
142689
+ });
142690
+ }
142118
142691
  const legend = legendRows(a.render, a.page);
142119
142692
  const ids = new Set(legend.map((r) => r.id));
142120
- const skipped = res.skipped.filter((s) => ids.has(topId2(s.id)));
142693
+ const where = idPages(agentEntries(schema));
142694
+ const skipped = res.skipped.filter((s) => {
142695
+ const top = topId2(s.id);
142696
+ const page = where.get(top);
142697
+ return ids.has(top) || page === void 0 || page === a.page;
142698
+ });
142121
142699
  const warnings = res.warnings.filter((w) => {
142122
142700
  const head = w.slice(0, Math.max(0, w.indexOf(":")));
142123
142701
  return head.split(" and ").some((id) => ids.has(topId2(id.trim())));
142124
142702
  });
142125
142703
  const zoom = a.crop ? Math.min(MAX_ZOOM, Math.max(1, a.zoom ?? 2)) : 1;
142126
- const raster = await renderPage(res.checkPdf, a.page, dpi * zoom, a.crop);
142127
- const shown = a.crop ? legend.filter((r) => {
142704
+ const raster = await renderPage(shown, a.page, dpi * zoom, a.crop, PREVIEW_MAX_SIDE_PX);
142705
+ const listed = a.crop ? legend.filter((r) => {
142128
142706
  const c2 = a.crop;
142129
142707
  return r.box.x < c2.x + c2.w && c2.x < r.box.x + r.box.w && r.box.y < c2.y + c2.h && c2.y < r.box.y + r.box.h;
142130
142708
  }) : legend;
@@ -142132,8 +142710,10 @@ async function previewPage(a) {
142132
142710
  png: encodePng2(raster),
142133
142711
  width: raster.width,
142134
142712
  height: raster.height,
142135
- dpi: dpi * zoom,
142136
- legend: shown,
142713
+ // What was rendered, not what was asked: a capped image is coarser, and
142714
+ // an agent reads sizes off it.
142715
+ dpi: Math.round(raster.scale * 72 * 10) / 10,
142716
+ legend: listed,
142137
142717
  filled: res.filled,
142138
142718
  skipped,
142139
142719
  warnings
@@ -142698,6 +143278,52 @@ async function sweepCache(cache3, now) {
142698
143278
  return removed;
142699
143279
  }
142700
143280
 
143281
+ // src/values.ts
143282
+ function expandKeys(values2, pool3) {
143283
+ const given = Object.keys(values2);
143284
+ const taken = /* @__PURE__ */ new Set();
143285
+ const pairs = [];
143286
+ for (const key of given) {
143287
+ let id = key;
143288
+ if (!pool3.includes(key) && key.length >= MIN_ID_PREFIX) {
143289
+ const hits2 = pool3.filter((p4) => p4.startsWith(key));
143290
+ if (hits2.length === 1 && !given.includes(hits2[0]) && !taken.has(hits2[0])) id = hits2[0];
143291
+ }
143292
+ taken.add(id);
143293
+ pairs.push([id, values2[key]]);
143294
+ }
143295
+ return Object.fromEntries(pairs);
143296
+ }
143297
+ function expandNested(e4, v) {
143298
+ if (e4.kind === "table" && isRec2(v)) {
143299
+ return expandKeys(
143300
+ v,
143301
+ e4.cells.map((c2) => c2.id)
143302
+ );
143303
+ }
143304
+ if (e4.kind === "table_rows" && Array.isArray(v)) {
143305
+ const columns = e4.columns.map((c2) => c2.id);
143306
+ return v.map((row) => isRec2(row) ? expandKeys(row, columns) : row);
143307
+ }
143308
+ return v;
143309
+ }
143310
+ function expandValueKeys(render, values2) {
143311
+ let entries;
143312
+ try {
143313
+ entries = agentEntries(normalizeSchema(render));
143314
+ } catch {
143315
+ return values2;
143316
+ }
143317
+ const byId = new Map(entries.map((e4) => [e4.id, e4]));
143318
+ const top = expandKeys(values2, [...byId.keys()]);
143319
+ return Object.fromEntries(
143320
+ Object.entries(top).map(([id, v]) => {
143321
+ const e4 = byId.get(id);
143322
+ return [id, e4 ? expandNested(e4, v) : v];
143323
+ })
143324
+ );
143325
+ }
143326
+
142701
143327
  // src/server.ts
142702
143328
  var VALUE_SHAPES = `values maps field ids to: text/multiline = string, stamped exactly as given (a date in a text field goes in the format its label or placeholder asks, e.g. "mmddyyyy" \u2192 "10042026"); date = "YYYY-MM-DD", printed in the field's date_format (shown on the field, e.g. DD/MM/YYYY); comb = string, one character per cell; checkbox = boolean; radio = option id or label, or the bare option text ("No" for "Smoke? No"); multiselect = array of those; table = {cellId: value}; table_rows = [{columnId: value}, \u2026]; signature = {"image_path": "/abs/sig.png"} (PNG or JPG). A signature with signing_requirement "external" is signed outside fenfill (on paper or by e-sign) and stays blank.`;
142703
143329
  var INSTRUCTIONS = [
@@ -142710,7 +143336,9 @@ var INSTRUCTIONS = [
142710
143336
  ].join(" ");
142711
143337
  var TEMPLATE_ID = external_exports.string().regex(/^[A-Za-z0-9-]{1,64}$/, "a fenfill template id").describe("Template id (from list_templates).");
142712
143338
  var PAGES = external_exports.string().max(2e3).optional().describe('1-based pages, e.g. "1-3,5". Omit for all.');
142713
- var VALUES = external_exports.record(external_exports.string(), external_exports.unknown()).describe("Field id \u2192 value (see description).");
143339
+ var VALUES = external_exports.record(external_exports.string(), external_exports.unknown()).describe(
143340
+ "Field id \u2192 value (see description). An id may be shortened to an unambiguous prefix of 6+ characters (the preview tags), a table's cell or column id inside its value too; a key that matches nothing is listed in skipped."
143341
+ );
142714
143342
  var OUTPUT_PATH = external_exports.string().min(1).max(4096).describe("Absolute path (or ~/\u2026) of the new .pdf to write.");
142715
143343
  var OVERWRITE = external_exports.boolean().optional().describe("Replace output_path if it exists.");
142716
143344
  var TARGET_PATH = external_exports.string().min(1).max(4096).optional().describe(
@@ -142757,7 +143385,14 @@ async function refreshSavedAnalyses(cache3, templateId, render, saved, who) {
142757
143385
  const agent = saved.result && Array.isArray(saved.result.fields) ? saved.result : e4.agent;
142758
143386
  await cache3.saveAnalysis({
142759
143387
  ...e4,
142760
- render: { ...render, pages: e4.render.pages },
143388
+ // The cached pages keep their own images; the sections are the saved ones.
143389
+ render: {
143390
+ ...render,
143391
+ pages: e4.render.pages.map((p4) => {
143392
+ const saved2 = render.pages.find((x2) => x2.page === p4.page);
143393
+ return saved2?.sections !== void 0 ? { ...p4, sections: saved2.sections } : p4;
143394
+ })
143395
+ },
142761
143396
  agent: { ...agent, template_id: templateId, updated_at: saved.updated_at }
142762
143397
  }).catch(() => void 0);
142763
143398
  }
@@ -142814,6 +143449,19 @@ function createFenfillServer(opts = {}) {
142814
143449
  };
142815
143450
  maybeSweep();
142816
143451
  const echo = new EchoGuard();
143452
+ const takenInputs = /* @__PURE__ */ new WeakSet();
143453
+ const taken = (render, values2) => {
143454
+ takenInputs.add(values2);
143455
+ const answers = expandValueKeys(render, values2);
143456
+ echo.remember(render, answers);
143457
+ return answers;
143458
+ };
143459
+ const untaken = (args) => {
143460
+ const given = args?.values;
143461
+ if (typeof given !== "object" || given === null || Array.isArray(given)) return;
143462
+ if (!takenInputs.has(given)) echo.rememberUnread(given);
143463
+ };
143464
+ const listedForeign = /* @__PURE__ */ new Map();
142817
143465
  const lastLegend = /* @__PURE__ */ new Map();
142818
143466
  const fileForm = async (path) => {
142819
143467
  const { abs: abs2, bytes } = await readInputPdf(path);
@@ -142889,6 +143537,7 @@ function createFenfillServer(opts = {}) {
142889
143537
  log("info", "tool ok", { tool: name, ms: Date.now() - t0 });
142890
143538
  return ok(result);
142891
143539
  } catch (e4) {
143540
+ untaken(args);
142892
143541
  const err2 = asToolError(e4);
142893
143542
  log("warn", "tool failed", { tool: name, code: err2.code, ms: Date.now() - t0 });
142894
143543
  return fail(err2);
@@ -142989,11 +143638,11 @@ function createFenfillServer(opts = {}) {
142989
143638
  const templateId = soleTemplateId(merged, null, await identity2(false));
142990
143639
  const wc = await fileEdits({ sha, templateId });
142991
143640
  const render = wc ? wc.render : merged.render;
142992
- echo.remember(render, values2);
143641
+ const answers = taken(render, values2);
142993
143642
  const res = await stampAndWrite({
142994
143643
  pdfBytes: bytes,
142995
143644
  render,
142996
- agentInput: values2,
143645
+ agentInput: answers,
142997
143646
  fonts: loadFonts(opts.fontsDir),
142998
143647
  branding: null,
142999
143648
  logoBytes: null,
@@ -143051,7 +143700,7 @@ function createFenfillServer(opts = {}) {
143051
143700
  }
143052
143701
  render = tpl.render;
143053
143702
  }
143054
- echo.remember(render, values2);
143703
+ const answers = taken(render, values2);
143055
143704
  const { body: account } = await client.getJson("/account", { signal, deadline });
143056
143705
  const blank = await templateBlank(template_id, signal, deadline);
143057
143706
  let logo = null;
@@ -143067,7 +143716,7 @@ function createFenfillServer(opts = {}) {
143067
143716
  const res = await stampAndWrite({
143068
143717
  pdfBytes: blank,
143069
143718
  render,
143070
- agentInput: values2,
143719
+ agentInput: answers,
143071
143720
  fonts: loadFonts(opts.fontsDir),
143072
143721
  branding,
143073
143722
  logoBytes: logo ? toArrayBuffer(logo) : null,
@@ -143183,24 +143832,31 @@ function createFenfillServer(opts = {}) {
143183
143832
  "preview_page",
143184
143833
  {
143185
143834
  title: "Preview a page with its field boxes",
143186
- description: `Render one page as a PNG with every field box outlined (green filled, orange warning, red skipped, grey empty) and tagged with its id prefix, plus a legend: tag \u2192 id, type, label, box (page fractions, top-left origin, as edit_template uses), format and font_pt; options, table cells (row, col) and table_rows columns with their own boxes. placement_warnings ([] = checked, clean) covers every box shown: much larger text than the page's other fields, a box over printed text (not on scans), a box past its ruled cell. Use it as a read-only placement audit. values (see that parameter) stamp test values. crop + zoom magnify a region; legend: "changed" lists only what changed since your last preview of the page (moved_into: fields now inside a table or group). It renders the local working copy (edit_template) when one exists, else the saved or analyzed version (edited: true/false). Rendered on this machine: nothing is written to disk and the values are never sent anywhere.`,
143835
+ description: `Render one page as a PNG with every field box outlined (green filled, orange warning, red skipped, grey empty) and tagged with its id prefix, plus a legend: tag \u2192 id, type, label, box (page fractions, top-left origin, as edit_template uses), format and font_pt; options, table cells (row, col) and table_rows columns with their own boxes. placement_warnings ([] = checked, clean) covers every box shown: much larger text than the page's other fields, a box over printed text (not on scans), a box past its ruled cell. Use it as a read-only placement audit. values (see that parameter) stamp test values. overlay: "outline" drops the tags, "none" shows the page with only the stamped values (to read print under a tag, or after check_copy_failed). crop + zoom magnify a region; legend: "changed" lists only what changed since your last preview of the page (moved_into: fields now inside a table or group). It renders the local working copy (edit_template) when one exists, else the saved or analyzed version (edited: true/false). Rendered on this machine: nothing is written to disk and the values are never sent anywhere.`,
143187
143836
  inputSchema: {
143188
143837
  path: TARGET_PATH,
143189
143838
  template_id: TARGET_TEMPLATE,
143190
143839
  page: external_exports.number().int().min(1).max(1e5).describe("1-based page number."),
143191
- values: VALUES.optional().describe(`Optional, exactly as for fill_form: ${VALUE_SHAPES}`),
143192
- dpi: external_exports.number().int().min(36).max(MAX_DPI).optional().describe(`Resolution (default ${String(DEFAULT_DPI)}, max ${String(MAX_DPI)}).`),
143840
+ values: VALUES.optional().describe(
143841
+ `Optional, exactly as for fill_form (ids may be shortened the same way): ${VALUE_SHAPES} Test values count as answers for the wording check of edit_template and save_template: prefer ones that are not words printed on the form (a caption that repeats one has to be confirmed there).`
143842
+ ),
143843
+ dpi: external_exports.number().int().min(36).max(MAX_DPI).optional().describe(
143844
+ `Resolution (default ${String(DEFAULT_DPI)}, max ${String(MAX_DPI)}). The image's longest side is capped at ${String(PREVIEW_MAX_SIDE_PX)} px; the result's dpi is what was rendered (use crop + zoom for more detail).`
143845
+ ),
143193
143846
  crop: external_exports.object({
143194
143847
  x: external_exports.number().min(0).max(1),
143195
143848
  y: external_exports.number().min(0).max(1),
143196
143849
  w: external_exports.number().gt(0).max(1),
143197
143850
  h: external_exports.number().gt(0).max(1)
143198
143851
  }).optional().describe(
143199
- "Optional: render only this region of the page (fractions, top-left origin, like the legend boxes), magnified by zoom. The legend then lists only what the region shows."
143852
+ "Optional: render only this region of the page (fractions, top-left origin, like the legend boxes), magnified by zoom. The legend then lists only what the region shows. Convert a position read off the image by proportion (x = crop.x + crop.w \xD7 pixel x / image width, the same for y), not by pixel count: a client may show the image smaller than width \xD7 height."
143200
143853
  ),
143201
143854
  zoom: external_exports.number().min(1).max(MAX_ZOOM).optional().describe(`With crop: magnification (default 2, max ${String(MAX_ZOOM)}).`),
143202
143855
  legend: external_exports.enum(["all", "changed", "none"]).optional().describe(
143203
143856
  `Which legend rows to return: "all" (default), "changed" (only what changed since your last preview of this page, or, the first time, since fenfill's version; plus removed ids), or "none".`
143857
+ ),
143858
+ overlay: external_exports.enum(OVERLAYS).optional().describe(
143859
+ 'What is drawn over the page: "full" (default) = every box outlined and tagged, table headers printed; "outline" = the boxes only, no id tags and no table header texts; "none" = nothing, the page as a fill stamps it. Use "outline" or "none" when a tag covers a caption, a tick box or a stamped value. The legend and placement_warnings are the same in all three.'
143204
143860
  )
143205
143861
  },
143206
143862
  annotations: { readOnlyHint: true, openWorldHint: true }
@@ -143230,16 +143886,17 @@ function createFenfillServer(opts = {}) {
143230
143886
  render = wc ? wc.render : (await fetchTemplate(id, signal)).render;
143231
143887
  bytes = await templateBlank(id, signal);
143232
143888
  }
143233
- echo.remember(render, values2);
143889
+ const answers = taken(render, values2);
143234
143890
  const pv = await previewPage({
143235
143891
  pdfBytes: bytes,
143236
143892
  render,
143237
143893
  page: args.page,
143238
- values: values2,
143894
+ values: answers,
143239
143895
  dpi: args.dpi,
143240
143896
  fonts: loadFonts(opts.fontsDir),
143241
143897
  crop: args.crop,
143242
- zoom: args.zoom
143898
+ zoom: args.zoom,
143899
+ overlay: args.overlay
143243
143900
  });
143244
143901
  const placement = await placementWarnings2(
143245
143902
  bytes,
@@ -143272,7 +143929,13 @@ function createFenfillServer(opts = {}) {
143272
143929
  edited: false,
143273
143930
  source: args.path ? "analysis (no local edits)" : "saved template (no local edits)"
143274
143931
  },
143275
- colours: "green = filled, orange = filled with a warning, red = skipped, grey dashed = empty; a radio/multiselect option has a thin box tagged below it",
143932
+ // The colour key of what is drawn: nothing with overlay "none".
143933
+ ...args.overlay === "none" ? { overlay: "none" } : args.overlay === "outline" ? {
143934
+ overlay: "outline",
143935
+ colours: "green = filled, orange = filled with a warning, red = skipped, grey dashed = empty; a radio/multiselect option has a thin box"
143936
+ } : {
143937
+ colours: "green = filled, orange = filled with a warning, red = skipped, grey dashed = empty; a radio/multiselect option has a thin box tagged below it"
143938
+ },
143276
143939
  ...args.crop ? { crop: args.crop } : {},
143277
143940
  ...mode === "none" ? {} : changes ? {
143278
143941
  legend: changes.changed,
@@ -143294,6 +143957,7 @@ function createFenfillServer(opts = {}) {
143294
143957
  ]
143295
143958
  };
143296
143959
  } catch (e4) {
143960
+ untaken(args);
143297
143961
  const err2 = asToolError(e4);
143298
143962
  log("warn", "tool failed", { tool: "preview_page", code: err2.code, ms: Date.now() - t0 });
143299
143963
  return fail(err2);
@@ -143308,10 +143972,15 @@ function createFenfillServer(opts = {}) {
143308
143972
  inputSchema: {
143309
143973
  path: TARGET_PATH,
143310
143974
  template_id: TARGET_TEMPLATE,
143311
- ops: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())).max(MAX_OPS).describe(
143312
- `Edit ops, applied in order (max ${String(MAX_OPS)}), each all-or-nothing; a box only has to lie on the page after the whole batch (so move then resize is fine). Fields: set_box {id, x, y, w, h, snap?} (absolute top-left + size; a group scales its members); move {id, dx, dy} or {id, x, y, snap?} (new top-left); resize {id, w, h}; add {page, type: text|multiline|date|checkbox|signature, box: {x, y, w, h}, label, description?, placeholder?, required?, section_id?, snap?} (diff_summary.added gives the new id); snap: "underline" fits the box onto the printed rule under it, "cell" into the ruled box around it, "answer" into that cell's blank part below (or after) its printed caption (read from the page on this machine; a miss keeps your box and warns); relabel {id, label}; retype {id, type} (a radio/multiselect group: radio|multiselect); delete {id}; set_format {id, format: {...}} (font_size, text_anchor, variant, date_format: DD/MM/YYYY|MM/DD/YYYY|YYYY-MM-DD|DD.MM.YYYY|DD-MM-YYYY; null removes a key; on a table cell it applies to the cell's whole column/row; a comb group: cell_type, date_format with one D/M/Y letter per cell); set_required {id, required}; set_description {id, description}; set_placeholder {id, placeholder} (null clears); set_autofill {id, autofill: a browser token (name, given-name, family-name, email, tel, street-address, postal-code, bday, \u2026) or null}. Groups: group {ids, kind: "choice"|"comb", label, multiple?} (standalone fields on one page; choice turns them into checkbox options); ungroup {id}; set_options {id, options: ["label", \u2026] in listed order, or [{id, label}]}; add_option {id: group, label, box, index?} and remove_option {id: group, option} keep the group's id. Tables: group {kind: "table", label, cells: [[id|null, \u2026], \u2026] (rows \xD7 columns) or ids (laid out from their boxes), header_cols?, header_rows?, orientation?: "col"|"row" (the axis whose lines carry the types; default col), format?: classic|expandable|checkbox_matrix}; table_insert {id, axis: "row"|"col", index, box?: {y, h} for a row / {x, w} for a column (default: one pitch past the edge, or centred in the gap), header?, type?}; table_delete {id, axis, index, keep_cells?}; table_add_cell {id, row, col, box?} (fills an empty slot); table_adopt {id, field, row, col} (moves a loose field into an empty slot); set_column_type {id, index, type: text|number|currency|date|checkbox, date_format?} (index along the typed axis); set_orientation {id, orientation} (resets every cell to text); set_header {id, axis, index, text}; set_headers {id, axis, texts: [...], start?} (several at once). Order: reorder {page, ids} moves those top-level entries together, in that order, to where the first sat (stamps order; geometry no longer decides). Every cell of one column (or row, when orientation is row) has one type.`
143975
+ ops: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())).max(MAX_OPS).optional().describe(
143976
+ `Edit ops, applied in order (max ${String(MAX_OPS)}), each all-or-nothing; a box only has to lie on the page after the whole batch (so move then resize is fine). Fields: set_box {id, x, y, w, h, snap?} (absolute top-left + size; a group scales its members); move {id, dx, dy} or {id, x, y, snap?} (new top-left); resize {id, w, h}; add {page, type: text|multiline|date|checkbox|signature, box: {x, y, w, h}, label, description?, placeholder?, required?, section_id?, snap?} (diff_summary.added gives the new id); snap: "underline" fits the box onto the printed rule under it, "cell" into the ruled box around it, "answer" into that cell's blank part below (or after) its printed caption (read from the page on this machine; a miss keeps your box and warns); relabel {id, label}; retype {id, type} (a radio/multiselect group: radio|multiselect); delete {id}; set_format {id, format: {...}} (font_size, text_anchor, variant, date_format: DD/MM/YYYY|MM/DD/YYYY|YYYY-MM-DD|DD.MM.YYYY|DD-MM-YYYY; null removes a key; on a table cell it applies to the cell's whole column/row; a comb group: cell_type char|integer|date, or date_format with one D/M/Y letter per cell, e.g. DDMMYYYY, which makes it a date comb; diff_summary.ops shows the comb's cell_type, date_format and max_length); set_required {id, required}; set_description {id, description}; set_placeholder {id, placeholder} (null clears); set_autofill {id, autofill: a browser token (name, given-name, family-name, email, tel, street-address, postal-code, bday, \u2026) or null}. Groups: group {ids, kind: "choice"|"comb", label, multiple?} (standalone fields on one page; choice turns them into checkbox options); ungroup {id}; set_options {id, options: ["label", \u2026] in listed order, or [{id, label}]}; add_option {id: group, label, box, index?} and remove_option {id: group, option} keep the group's id. Tables: group {kind: "table", label, cells: [[id|null, \u2026], \u2026] (rows \xD7 columns) or ids (laid out from their boxes), header_cols?, header_rows?, orientation?: "col"|"row" (the axis whose lines carry the types; default col), format?: classic|expandable|checkbox_matrix}; table_insert {id, axis: "row"|"col", index, box?: {y, h} for a row / {x, w} for a column (default: one pitch past the edge, or centred in the gap), header?, type?}; table_delete {id, axis, index, keep_cells?}; table_add_cell {id, row, col, box?} (fills an empty slot); table_adopt {id, field, row, col} (moves a loose field into an empty slot); set_column_type {id, index, type: text|number|currency|date|checkbox, date_format?} (index along the typed axis); set_orientation {id, orientation} (resets every cell to text); set_header {id, axis, index, text}; set_headers {id, axis, texts: [...], start?} (several at once). Order: reorder {page, ids} moves those top-level entries together, in that order, to where the first sat (stamps order; geometry no longer decides); diff_summary.ops shows the order now listed. A section is listed as one run where its first entry stands, so entries of different sections never stand together: set_section {ids, section_id | null} moves fields or groups of one page to another of its sections (get_template shows each section's section_id; null = no section), and set_section_title {page, section_id, title} renames one. A reorder whose ids cannot come out as asked and that would change nothing in the listing is rejected. Every cell of one column (or row, when orientation is row) has one type.`
143313
143977
  ),
143314
- discard: external_exports.boolean().optional().describe("Drop the local working copy first (any ops then apply to fenfill's version).")
143978
+ discard: external_exports.boolean().optional().describe(
143979
+ "Drop the local working copy first (any ops then apply to fenfill's version). ops may be left out with it."
143980
+ ),
143981
+ confirm_wording: external_exports.boolean().optional().describe(
143982
+ "Only after an op was rejected because its wording matches an answer of this session: true confirms the rejected wording is the form's own printed text, not an answer, so the same op applies. It covers only wording a rejection has already named (that text in that property); new wording is rejected and named first."
143983
+ )
143315
143984
  },
143316
143985
  annotations: {
143317
143986
  readOnlyHint: false,
@@ -143320,8 +143989,15 @@ function createFenfillServer(opts = {}) {
143320
143989
  openWorldHint: true
143321
143990
  }
143322
143991
  },
143323
- run("edit_template", async ({ path, template_id, ops, discard }, _extra, signal) => {
143992
+ run("edit_template", async (args, _extra, signal) => {
143993
+ const { path, template_id, ops: given, discard, confirm_wording } = args;
143324
143994
  oneTarget({ path, template_id });
143995
+ if (given === void 0 && discard !== true) {
143996
+ throw new ToolError("invalid_argument", "Pass ops, or discard: true (or both).", {
143997
+ hint: "ops: the edits to apply ([] only reports whether local edits exist); discard: true drops the local working copy."
143998
+ });
143999
+ }
144000
+ const ops = given ?? [];
143325
144001
  const who = await identity2(false);
143326
144002
  let key;
143327
144003
  let saveTarget;
@@ -143352,6 +144028,7 @@ function createFenfillServer(opts = {}) {
143352
144028
  if (discard === true) discarded = await cache3.deleteEdits(key);
143353
144029
  let wc = await cache3.readEdits(key);
143354
144030
  if (wc && saveTarget && !sameAccount(wc, who)) wc = null;
144031
+ const had = wc !== null;
143355
144032
  const nowIso = new Date(now()).toISOString();
143356
144033
  if (!wc && ops.length === 0) {
143357
144034
  return {
@@ -143393,7 +144070,10 @@ function createFenfillServer(opts = {}) {
143393
144070
  snap = () => ({ none: `no page render (${why})` });
143394
144071
  }
143395
144072
  }
143396
- const r = applyOps(wc.render, ops, (w) => echo.check(w), { snap });
144073
+ const r = applyOps(wc.render, ops, (w) => echo.check(w, key), {
144074
+ snap,
144075
+ confirmWording: confirm_wording === true
144076
+ });
143397
144077
  let placement = [];
143398
144078
  if (r.applied > 0) {
143399
144079
  const ids = [
@@ -143411,14 +144091,16 @@ function createFenfillServer(opts = {}) {
143411
144091
  writer,
143412
144092
  ...inherited ? { inherited_wording: true } : {}
143413
144093
  };
143414
- const existed = await cache3.readEdits(key) !== null;
143415
- if (r.applied > 0) {
144094
+ const changed = r.applied > 0 && JSON.stringify(r.render) !== JSON.stringify(wc.render);
144095
+ if (changed) {
143416
144096
  await cache3.writeEdits(out);
143417
- if (saveTarget && !existed) {
144097
+ if (saveTarget && !had) {
143418
144098
  await dropFileCopies(saveTarget, fileSha).catch(() => void 0);
143419
144099
  }
143420
144100
  }
143421
- const hasEdits = r.applied > 0 || existed;
144101
+ echo.tell(r.screened.told);
144102
+ echo.confirm(r.screened.confirmed);
144103
+ const hasEdits = changed || had;
143422
144104
  const left = hasEdits ? schemaIssues(out.render, 10) : [];
143423
144105
  return {
143424
144106
  target: saveTarget ? { template_id: saveTarget } : { path },
@@ -143434,7 +144116,7 @@ function createFenfillServer(opts = {}) {
143434
144116
  )
143435
144117
  ],
143436
144118
  has_edits: hasEdits,
143437
- ops_applied_total: out.ops_applied,
144119
+ ops_applied_total: changed ? out.ops_applied : wc.ops_applied,
143438
144120
  saveable: !!saveTarget,
143439
144121
  next: saveTarget ? "Check with preview_page (or fill_form with check_pdf), then save_template to keep the edits in the saved template." : "These edits stay on this machine (fill_form and preview_page use them). This is an unsaved analysis, so save_template can't keep them: to keep corrections, analyze_form with save_as_template: true and edit that template."
143440
144122
  };
@@ -143444,11 +144126,11 @@ function createFenfillServer(opts = {}) {
143444
144126
  "save_template",
143445
144127
  {
143446
144128
  title: "Save local edits to a template",
143447
- description: "Save edit_template's local working copy of a saved template to fenfill (only the layout and the form's wording are sent; never fill values). Fails safely if the template changed in fenfill since the edits began (template_conflict: the local edits are kept; discard and re-apply them), and refuses wording that repeats an answer given to fill_form / fill_template / preview_page in this session (answers never go into a template). Wording edited in an earlier session can't be checked that way: save_template then answers confirm_wording_needed with the changed ids and properties; review that wording (preview_page shows labels) and call again with confirm_wording: true. An unsaved analysis can't be saved: analyze it again with save_as_template: true (free on a native-field PDF; otherwise it costs scans again), then edit that template.",
144129
+ description: "Save edit_template's local working copy of a saved template to fenfill (only the layout and the form's wording are sent; never fill values). Fails safely if the template changed in fenfill since the edits began (template_conflict: the local edits are kept; discard and re-apply them), and refuses wording that repeats an answer given to fill_form / fill_template / preview_page in this session (answers never go into a template): answer_in_template lists the matched ids and properties. An answer: fix it with edit_template. The form's own wording (a caption that shares a word with a test value): call again with confirm_wording: true. Wording edited in an earlier session can't be checked that way: save_template then answers confirm_wording_needed with the changed ids and properties; review that wording (preview_page shows labels) and call again with confirm_wording: true. An unsaved analysis can't be saved: analyze it again with save_as_template: true (free on a native-field PDF; otherwise it costs scans again), then edit that template.",
143448
144130
  inputSchema: {
143449
144131
  template_id: TEMPLATE_ID,
143450
144132
  confirm_wording: external_exports.boolean().optional().describe(
143451
- "Only after a confirm_wording_needed error: true confirms you reviewed the listed wording (edited in an earlier session) and it is the form's own text, not an answer."
144133
+ "Only after an answer_in_template or confirm_wording_needed error: true confirms you compared the listed wording with the printed form and it is the form's own text, not an answer. An entry no error has listed yet is refused and listed first, on a copy from an earlier session too."
143452
144134
  )
143453
144135
  },
143454
144136
  annotations: {
@@ -143469,19 +144151,29 @@ function createFenfillServer(opts = {}) {
143469
144151
  };
143470
144152
  }
143471
144153
  const changed = changedWording(wc.base, wc.render);
143472
- const echoed = echo.check(changed);
143473
- if (echoed.refuse.length) {
144154
+ const echoed = echo.check(changed, wc.key);
144155
+ const foreign = wc.writer?.owner !== owner || wc.inherited_wording === true;
144156
+ const listing = sha256Hex(JSON.stringify(changed));
144157
+ const unvouched = foreign && changed.length > 0;
144158
+ const confirmable = confirm_wording === true && echoed.refused.every((x2) => x2.listed);
144159
+ if (echoed.refused.length && !confirmable) {
144160
+ echo.tell(echoed.refused);
144161
+ if (unvouched) listedForeign.set(wc.key, listing);
143474
144162
  throw new ToolError(
143475
144163
  "answer_in_template",
143476
- "Some edited wording matches an answer you filled in this session, so it was not sent.",
144164
+ "Some edited wording matches an answer you filled or previewed in this session, so it was not sent.",
143477
144165
  {
143478
- hint: "Labels, descriptions, placeholders and options hold the form's own wording, never an answer. Fix those items with edit_template (relabel / set_description / set_placeholder / set_options), then save again.",
143479
- details: { ids: echoed.refuse }
144166
+ hint: "Compare each property listed in `matched` with the printed form (preview_page's legend shows the wording). An answer: put the form's own wording back with edit_template (relabel / set_description / set_placeholder / set_options / set_section_title). The form's own text: call save_template again with confirm_wording: true.",
144167
+ details: {
144168
+ ids: echoed.refuse,
144169
+ matched: wordingChanges(echoed.refused),
144170
+ ...unvouched ? { changed: wordingChanges(changed) } : {}
144171
+ }
143480
144172
  }
143481
144173
  );
143482
144174
  }
143483
- const foreign = wc.writer?.owner !== owner || wc.inherited_wording === true;
143484
- if (foreign && changed.length && confirm_wording !== true) {
144175
+ if (unvouched && !(confirm_wording === true && listedForeign.get(wc.key) === listing)) {
144176
+ listedForeign.set(wc.key, listing);
143485
144177
  throw new ToolError(
143486
144178
  "confirm_wording_needed",
143487
144179
  "This working copy has wording edited in an earlier session, which this session's answer check can't cover, so it was not sent.",
@@ -143511,7 +144203,13 @@ function createFenfillServer(opts = {}) {
143511
144203
  }
143512
144204
  );
143513
144205
  }
143514
- const schema = { ...wc.render, pages: wc.base.pages };
144206
+ const schema = {
144207
+ ...wc.render,
144208
+ pages: wc.base.pages.map((p4, i2) => {
144209
+ const edited = wc.render.pages[i2];
144210
+ return edited?.page === p4.page && edited.sections !== void 0 ? { ...p4, sections: edited.sections } : p4;
144211
+ })
144212
+ };
143515
144213
  let saved;
143516
144214
  try {
143517
144215
  saved = await api().patchJson(
@@ -143553,6 +144251,7 @@ function createFenfillServer(opts = {}) {
143553
144251
  throw e4;
143554
144252
  }
143555
144253
  await cache3.deleteEdits(wc.key);
144254
+ listedForeign.delete(wc.key);
143556
144255
  await dropFileCopies(template_id, wc.sha256).catch(() => void 0);
143557
144256
  await refreshSavedAnalyses(cache3, template_id, schema, saved, await identity2(false));
143558
144257
  return {
@@ -143570,11 +144269,18 @@ function createFenfillServer(opts = {}) {
143570
144269
  return {};
143571
144270
  }
143572
144271
  })(),
143573
- ...echoed.warn.length ? {
143574
- warnings: [
143575
- `${echoed.warn.join(", ")}: short wording that matches a short answer from this session (allowed under 4 characters); check it is the form's own text`
143576
- ]
143577
- } : {},
144272
+ ...(() => {
144273
+ const vouched = [...echoed.confirmed, ...echoed.refused];
144274
+ const warnings = [
144275
+ ...vouched.length ? [
144276
+ `${describeWording(vouched)}: matches an answer filled in this session; saved because you confirmed it is the form's own wording`
144277
+ ] : [],
144278
+ ...echoed.warn.length ? [
144279
+ `${echoed.warn.join(", ")}: short wording that matches a short answer from this session (allowed under 4 characters); check it is the form's own text`
144280
+ ] : []
144281
+ ];
144282
+ return warnings.length ? { warnings } : {};
144283
+ })(),
143578
144284
  next: "fill_template, get_template and recipient links now use the saved layout."
143579
144285
  };
143580
144286
  })