@fenfill/mcp 0.2.1 → 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
@@ -136506,6 +136506,14 @@ function mapInternal(schema, input2, loadImage2) {
136506
136506
  }
136507
136507
  return out;
136508
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
+ }
136509
136517
  async function verifyImages(m) {
136510
136518
  if (m.images.length === 0) return;
136511
136519
  const { PDFDocument: PDFDocument2 } = await Promise.resolve().then(() => __toESM(require_cjs()));
@@ -136627,15 +136635,15 @@ var CheckCopyError = class extends Error {
136627
136635
  this.at = { ...at };
136628
136636
  }
136629
136637
  };
136630
- async function buildCheckPdf(stamped, entries, status, fonts, banner = true) {
136638
+ async function buildCheckPdf(stamped, entries, status, fonts, banner = true, tags = true) {
136631
136639
  const at = { stage: "load" };
136632
136640
  try {
136633
- return await drawCheckPdf(stamped, entries, status, fonts, banner, at);
136641
+ return await drawCheckPdf(stamped, entries, status, fonts, banner, tags, at);
136634
136642
  } catch (e4) {
136635
136643
  throw new CheckCopyError(at, e4);
136636
136644
  }
136637
136645
  }
136638
- async function drawCheckPdf(stamped, entries, status, fonts, banner, at) {
136646
+ async function drawCheckPdf(stamped, entries, status, fonts, banner, tags, at) {
136639
136647
  const step = (stage, id) => {
136640
136648
  at.stage = stage;
136641
136649
  at.id = id;
@@ -136684,7 +136692,7 @@ async function drawCheckPdf(stamped, entries, status, fonts, banner, at) {
136684
136692
  const headColor = rgb3(0.15, 0.35, 0.75);
136685
136693
  const wanted = [];
136686
136694
  for (const e4 of entries) {
136687
- if (e4.kind !== "table") continue;
136695
+ if (!tags || e4.kind !== "table") continue;
136688
136696
  step("table headers", e4.id);
136689
136697
  const g = e4.group;
136690
136698
  const texts = (v) => Array.isArray(v) ? v : [];
@@ -136783,7 +136791,7 @@ async function drawCheckPdf(stamped, entries, status, fonts, banner, at) {
136783
136791
  opacity: 0,
136784
136792
  borderOpacity: 0.9
136785
136793
  });
136786
- if (!b.tagged) continue;
136794
+ if (!tags || !b.tagged) continue;
136787
136795
  step("tag", b.id);
136788
136796
  const tag = asciiOnly(short.get(b.id) ?? b.id);
136789
136797
  const tw = helv.widthOfTextAtSize(tag, SIZE) + 1.6;
@@ -136874,7 +136882,8 @@ async function fillAgentPdf(a) {
136874
136882
  m.entries,
136875
136883
  (id) => skippedIds.has(id) ? "skipped" : warned.has(id) ? "warned" : m.filledIds.has(id) ? "filled" : "empty",
136876
136884
  a.fonts,
136877
- a.checkBanner !== false
136885
+ a.checkBanner !== false,
136886
+ a.checkTags !== false
136878
136887
  );
136879
136888
  res.checkPdf = built.pdf;
136880
136889
  warnings.push(...built.notes);
@@ -137478,19 +137487,24 @@ function listedPages(agent, requested) {
137478
137487
  return have.filter((p4) => want.has(p4));
137479
137488
  }
137480
137489
  function nestBySection(entries) {
137481
- const blocks = /* @__PURE__ */ new Map();
137490
+ const blocks = [];
137491
+ let run = null;
137482
137492
  for (const e4 of entries) {
137483
- const sid = e4.section?.id ?? "";
137484
- const key = `${String(e4.page)}\0${sid}`;
137485
- let b = blocks.get(key);
137486
- if (!b) {
137493
+ const sid = e4.section?.id;
137494
+ const key = `${String(e4.page)}\0${sid ?? ""}`;
137495
+ if (key !== run) {
137487
137496
  const title = e4.section?.title;
137488
- b = { page: e4.page, ...title ? { title } : {}, fields: [] };
137489
- 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;
137490
137504
  }
137491
- b.fields.push(compactEntry(e4));
137505
+ blocks[blocks.length - 1].fields.push(compactEntry(e4));
137492
137506
  }
137493
- return [...blocks.values()];
137507
+ return blocks;
137494
137508
  }
137495
137509
  function compactView(agent, requested, next, maxChars = COMPACT_MAX_CHARS) {
137496
137510
  const entries = selectEntries(agent, requested);
@@ -137784,7 +137798,7 @@ function loadFonts(dir = defaultFontsDir()) {
137784
137798
  }
137785
137799
 
137786
137800
  // src/version.ts
137787
- var VERSION = true ? "0.2.1" : "0.0.0-dev";
137801
+ var VERSION = true ? "0.2.2" : "0.0.0-dev";
137788
137802
  var USER_AGENT = `fenfill-mcp/${VERSION}`;
137789
137803
 
137790
137804
  // src/fill.ts
@@ -139307,67 +139321,208 @@ var ECHO_CONTAINS_MIN = 6;
139307
139321
  var MAX_REMEMBERED = 5e3;
139308
139322
  var MAX_LEN = 2e3;
139309
139323
  var FREE_TEXT = /* @__PURE__ */ new Set(["text", "multiline", "date", "comb"]);
139310
- 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
+ }
139311
139362
  var EchoGuard = class {
139312
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();
139313
139371
  add(v) {
139314
139372
  if (typeof v === "number" && Number.isFinite(v)) v = String(v);
139315
139373
  if (typeof v !== "string") return;
139316
139374
  const n = norm(v);
139317
139375
  if (!n || n.length > MAX_LEN) return;
139318
- this.seen.delete(n);
139319
- this.seen.add(n);
139320
- if (this.seen.size > MAX_REMEMBERED) {
139321
- const oldest = this.seen.values().next().value;
139322
- 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);
139323
139392
  }
139324
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));
139432
+ }
139433
+ for (const [, text] of shown) this.add(text);
139434
+ }
139325
139435
  /** Remember the free-text values of one fill/preview call. */
139326
139436
  remember(render, values2) {
139437
+ let schema;
139327
139438
  let entries;
139328
139439
  try {
139329
- entries = agentEntries(normalizeSchema(render));
139440
+ schema = normalizeSchema(render);
139441
+ entries = agentEntries(schema);
139330
139442
  } catch {
139443
+ this.addAny(values2);
139331
139444
  return;
139332
139445
  }
139333
- const isRec3 = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2);
139446
+ this.addShown(schema, entries, values2);
139334
139447
  for (const e4 of entries) {
139335
139448
  const v = values2[e4.id];
139336
139449
  if (v === void 0) continue;
139337
139450
  if (e4.kind === "field" || e4.kind === "comb") {
139338
139451
  if (FREE_TEXT.has(e4.type)) this.add(v);
139339
- } else if (e4.kind === "table" && isRec3(v)) {
139452
+ } else if (e4.kind === "table" && isRec(v)) {
139340
139453
  for (const c2 of e4.cells) if (FREE_TEXT.has(c2.type)) this.add(v[c2.id]);
139341
139454
  } else if (e4.kind === "table_rows" && Array.isArray(v)) {
139455
+ const ids = new Set(e4.columns.map((c2) => c2.id));
139342
139456
  for (const row of v) {
139343
- if (!isRec3(row)) continue;
139457
+ if (!isRec(row)) continue;
139344
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);
139345
139460
  }
139346
139461
  }
139347
139462
  }
139348
139463
  }
139349
- /** Wording that repeats a remembered answer: refusals (it contains one of
139350
- * 6+ chars, or equals one of 4+) and warnings (equals a shorter one). */
139351
- 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 = "") {
139352
139470
  const refuse = /* @__PURE__ */ new Set();
139353
139471
  const warn = /* @__PURE__ */ new Set();
139354
- 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 };
139355
139475
  for (const w of wording) {
139356
139476
  const n = norm(w.text);
139357
139477
  if (!n) continue;
139478
+ let hit = false;
139358
139479
  if (this.seen.has(n)) {
139359
- (n.length >= ECHO_REFUSE_MIN ? refuse : warn).add(w.id);
139360
- continue;
139361
- }
139362
- if (n.length < ECHO_CONTAINS_MIN) continue;
139363
- for (const v of this.seen) {
139364
- if (v.length >= ECHO_CONTAINS_MIN && n.includes(v)) {
139365
- refuse.add(w.id);
139366
- break;
139480
+ if (n.length < ECHO_REFUSE_MIN) {
139481
+ warn.add(w.id);
139482
+ continue;
139367
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)) });
139368
139506
  }
139369
139507
  }
139370
- 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));
139371
139526
  }
139372
139527
  get size() {
139373
139528
  return this.seen.size;
@@ -139377,13 +139532,121 @@ var EchoGuard = class {
139377
139532
  // src/edits.ts
139378
139533
  import { randomUUID as randomUUID2 } from "node:crypto";
139379
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
+
139380
139643
  // src/doc.ts
139381
139644
  var OpError = class extends Error {
139382
139645
  };
139383
139646
  var bad = (msg) => {
139384
139647
  throw new OpError(msg);
139385
139648
  };
139386
- var isRec = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2);
139649
+ var isRec2 = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2);
139387
139650
  var isNum = (x2) => typeof x2 === "number" && Number.isFinite(x2);
139388
139651
  var round = (n, d = 4) => Math.round(n * 10 ** d) / 10 ** d;
139389
139652
  function index(s) {
@@ -139497,7 +139760,7 @@ function checkBoxOnPage(b) {
139497
139760
  if (!onPage(b)) bad(ON_PAGE);
139498
139761
  }
139499
139762
  function boxArg(v, name = "box") {
139500
- 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)`);
139501
139764
  const b = {
139502
139765
  x: checkFraction(v.x, `${name}.x`),
139503
139766
  y: checkFraction(v.y, `${name}.y`),
@@ -139575,7 +139838,7 @@ function patchForDataType(t, dateFormat) {
139575
139838
  }
139576
139839
  var DEFAULT_TEXT = { type: "text", format: { variant: "any" } };
139577
139840
  function variantOf(f3) {
139578
- 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";
139579
139842
  }
139580
139843
  function isDefaultText2(f3) {
139581
139844
  return f3.type === "text" && variantOf(f3) === "any";
@@ -139687,7 +139950,7 @@ function lineType(d, v, i2) {
139687
139950
  const w = cells.find((c2) => !isDefaultText2(c2)) ?? cells[0];
139688
139951
  return {
139689
139952
  type: String(w.type),
139690
- ...isRec(w.format) ? { format: structuredClone(w.format) } : {}
139953
+ ...isRec2(w.format) ? { format: structuredClone(w.format) } : {}
139691
139954
  };
139692
139955
  }
139693
139956
  var axisWord = (a) => a === "col" ? "column" : "row";
@@ -139962,7 +140225,7 @@ function tableInsert(d, op, w) {
139962
140225
  }
139963
140226
  let at;
139964
140227
  if (op.box !== void 0) {
139965
- if (!isRec(op.box)) return bad("box must be an object");
140228
+ if (!isRec2(op.box)) return bad("box must be an object");
139966
140229
  const b = op.box;
139967
140230
  const [p4, s] = axis === "row" ? ["y", "h"] : ["x", "w"];
139968
140231
  const pos = b[p4];
@@ -140033,8 +140296,8 @@ function tableDelete(d, op, w) {
140033
140296
  }
140034
140297
  if (v.cols === 0) v.grid = [];
140035
140298
  v.rows = v.grid.length;
140036
- const keep = op.keep_cells === true;
140037
- if (keep) {
140299
+ const keep2 = op.keep_cells === true;
140300
+ if (keep2) {
140038
140301
  for (const id of ids) {
140039
140302
  const f3 = d.field.get(id);
140040
140303
  if (f3) f3.group = null;
@@ -140043,9 +140306,9 @@ function tableDelete(d, op, w) {
140043
140306
  } else removeFields(d, ids);
140044
140307
  const alive = commit(d, v, hc, hr);
140045
140308
  if (!alive) w.push(`that was the last line of table ${v.g.id}, so the table was removed`);
140046
- const deleted = [...keep ? [] : ids, ...alive ? [] : [v.g.id]];
140309
+ const deleted = [...keep2 ? [] : ids, ...alive ? [] : [v.g.id]];
140047
140310
  return {
140048
- touched: [...alive ? [v.g.id] : [], ...keep ? ids : []],
140311
+ touched: [...alive ? [v.g.id] : [], ...keep2 ? ids : []],
140049
140312
  deleted,
140050
140313
  tables: alive ? [v.g.id] : []
140051
140314
  };
@@ -140096,7 +140359,7 @@ function tableAdopt(d, op, w) {
140096
140359
  const { r, c: c2 } = slotArgs(v, op);
140097
140360
  const tf = slotType(d, v, r, c2, {
140098
140361
  type: String(f3.type),
140099
- ...isRec(f3.format) ? { format: f3.format } : {}
140362
+ ...isRec2(f3.format) ? { format: f3.format } : {}
140100
140363
  });
140101
140364
  if (!sameType(f3, tf)) {
140102
140365
  w.push(
@@ -140335,7 +140598,7 @@ var FORMAT_KEYS = {
140335
140598
  date: /* @__PURE__ */ new Set(["date_format", "font_size", "text_anchor"]),
140336
140599
  signature: /* @__PURE__ */ new Set(["scale", "text_anchor"])
140337
140600
  };
140338
- var isRec2 = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2);
140601
+ var isRec3 = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2);
140339
140602
  var isNum2 = (x2) => typeof x2 === "number" && Number.isFinite(x2);
140340
140603
  var isInt = (x2) => isNum2(x2) && Number.isInteger(x2);
140341
140604
  function checkStr(out, v, max2, at, optional2) {
@@ -140368,7 +140631,7 @@ function checkAnchor(out, v, at) {
140368
140631
  function formatIssues(type, fmt3, id, path) {
140369
140632
  const out = [];
140370
140633
  if (fmt3 === void 0 || fmt3 === null) return out;
140371
- 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" }];
140372
140635
  const allowed = typeof type === "string" ? FORMAT_KEYS[type] : void 0;
140373
140636
  if (!allowed) return out;
140374
140637
  for (const k4 of Object.keys(fmt3)) {
@@ -140405,7 +140668,7 @@ function formatIssues(type, fmt3, id, path) {
140405
140668
  return out;
140406
140669
  }
140407
140670
  function fieldIssues(f3, path) {
140408
- if (!isRec2(f3)) return [{ path, reason: "must be an object" }];
140671
+ if (!isRec3(f3)) return [{ path, reason: "must be an object" }];
140409
140672
  const id = typeof f3.id === "string" ? f3.id : void 0;
140410
140673
  const out = [];
140411
140674
  const at = (k4) => ({ id, path: `${path}.${k4}` });
@@ -140428,7 +140691,7 @@ function fieldIssues(f3, path) {
140428
140691
  else {
140429
140692
  f3.cells.forEach((c2, i2) => {
140430
140693
  const cp = { id, path: `${path}.cells.${String(i2)}` };
140431
- if (!isRec2(c2)) {
140694
+ if (!isRec3(c2)) {
140432
140695
  out.push({ ...cp, reason: "must be an object" });
140433
140696
  return;
140434
140697
  }
@@ -140454,7 +140717,7 @@ function fieldIssues(f3, path) {
140454
140717
  return out;
140455
140718
  }
140456
140719
  function groupIssues(g, path) {
140457
- if (!isRec2(g)) return [{ path, reason: "must be an object" }];
140720
+ if (!isRec3(g)) return [{ path, reason: "must be an object" }];
140458
140721
  const id = typeof g.id === "string" ? g.id : void 0;
140459
140722
  const out = [];
140460
140723
  const at = (k4) => ({ id, path: `${path}.${k4}` });
@@ -140512,7 +140775,7 @@ function groupIssues(g, path) {
140512
140775
  return out;
140513
140776
  }
140514
140777
  function sectionIssues(s, path) {
140515
- if (!isRec2(s)) return [{ path, reason: "must be an object" }];
140778
+ if (!isRec3(s)) return [{ path, reason: "must be an object" }];
140516
140779
  const id = typeof s.id === "string" ? s.id : void 0;
140517
140780
  const out = [];
140518
140781
  const at = (k4) => ({ id, path: `${path}.${k4}` });
@@ -140537,7 +140800,7 @@ function schemaIssues(schema, limit = 50) {
140537
140800
  if (!Array.isArray(s.pages)) out.push({ path: "pages", reason: "must be a list" });
140538
140801
  pages.forEach((p4, i2) => {
140539
140802
  const path = `pages.${String(i2)}`;
140540
- if (!isRec2(p4)) {
140803
+ if (!isRec3(p4)) {
140541
140804
  out.push({ path, reason: "must be an object" });
140542
140805
  return;
140543
140806
  }
@@ -140638,7 +140901,9 @@ var OP_NAMES = [
140638
140901
  "set_orientation",
140639
140902
  "set_header",
140640
140903
  "set_headers",
140641
- "reorder"
140904
+ "reorder",
140905
+ "set_section",
140906
+ "set_section_title"
140642
140907
  ];
140643
140908
  function placeBox(fs, bb, nb) {
140644
140909
  if (nb.w <= 0 || nb.h <= 0) bad("the box must have a positive width and height");
@@ -140664,25 +140929,25 @@ function sectionIds(d, page) {
140664
140929
  return out;
140665
140930
  }
140666
140931
  function typeAndFormat(t, prev) {
140667
- const prevFmt = isRec(prev?.format) ? prev.format : {};
140668
- 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]]));
140669
140934
  switch (t) {
140670
140935
  case "text":
140671
140936
  return {
140672
140937
  type: "text",
140673
- format: { ...keep(["font_size", "text_anchor", "uppercase"]), variant: "any" }
140938
+ format: { ...keep2(["font_size", "text_anchor", "uppercase"]), variant: "any" }
140674
140939
  };
140675
140940
  case "multiline":
140676
140941
  return {
140677
140942
  type: "text",
140678
- format: { ...keep(["font_size", "uppercase"]), variant: "multiline" }
140943
+ format: { ...keep2(["font_size", "uppercase"]), variant: "multiline" }
140679
140944
  };
140680
140945
  case "date":
140681
140946
  return {
140682
140947
  type: "date",
140683
140948
  format: {
140684
140949
  date_format: typeof prevFmt.date_format === "string" ? prevFmt.date_format : "DD/MM/YYYY",
140685
- ...keep(["font_size", "text_anchor"])
140950
+ ...keep2(["font_size", "text_anchor"])
140686
140951
  }
140687
140952
  };
140688
140953
  case "checkbox":
@@ -140710,7 +140975,7 @@ function entryType(d, id) {
140710
140975
  }
140711
140976
  const f3 = d.field.get(id);
140712
140977
  if (!f3) return "?";
140713
- 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";
140714
140979
  if ((f3.type === "text" || f3.type === "date") && Array.isArray(f3.cells) && f3.cells.length > 1)
140715
140980
  return "comb";
140716
140981
  return String(f3.type);
@@ -140758,7 +141023,7 @@ function checkSnapTarget(g, op) {
140758
141023
  if (op.snap !== void 0 && g) bad("snap applies to a single field, not a group");
140759
141024
  }
140760
141025
  function applyOne(d, op, warnings, opts) {
140761
- 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");
140762
141027
  const raw = op;
140763
141028
  const table = (r) => r;
140764
141029
  switch (op.op) {
@@ -140916,29 +141181,36 @@ function applyOne(d, op, warnings, opts) {
140916
141181
  }
140917
141182
  case "set_format": {
140918
141183
  const { f: f3, g } = needId(d, op.id);
140919
- if (!isRec(op.format)) return bad("format must be an object");
141184
+ if (!isRec2(op.format)) return bad("format must be an object");
140920
141185
  if (g) {
140921
141186
  if (g.kind !== "comb") {
140922
141187
  return bad(
140923
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)"
140924
141189
  );
140925
141190
  }
141191
+ const comb = g;
141192
+ const dated = op.format.date_format !== void 0 && op.format.date_format !== null;
140926
141193
  for (const [k4, v] of Object.entries(op.format)) {
140927
141194
  if (k4 !== "cell_type" && k4 !== "date_format")
140928
141195
  bad(`a comb group's format keys are cell_type and date_format, not ${k4}`);
140929
141196
  if (k4 === "date_format" && v !== null) checkCombDateFormat(d, g, v);
140930
- if (v === null) delete g[k4];
140931
- else g[k4] = v;
141197
+ if (v === null) delete comb[k4];
141198
+ else comb[k4] = v;
140932
141199
  }
140933
- 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 };
140934
141206
  }
140935
141207
  const fld = f3;
140936
141208
  const own2 = d.owner.get(fld.id);
140937
141209
  if (own2 && (own2.kind === "comb" || own2.kind === "choice")) {
140938
141210
  bad(`this is a member of ${String(own2.kind)} group ${own2.id}, whose kind fixes its format`);
140939
141211
  }
140940
- if (fld.type === "signature" && !isRec(fld.format)) fld.format = {};
140941
- 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 : {} };
140942
141214
  for (const [k4, v] of Object.entries(op.format)) {
140943
141215
  if (k4 === "date_format" && v !== null && !DATE_FORMATS.includes(v))
140944
141216
  bad(`date_format must be one of ${DATE_FORMATS.join(", ")}`);
@@ -140953,7 +141225,7 @@ function applyOne(d, op, warnings, opts) {
140953
141225
  delete fld.format;
140954
141226
  else fld.format = next;
140955
141227
  if (own2?.kind === "table") {
140956
- 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);
140957
141229
  return { touched: [fld.id, own2.id], tables: [own2.id] };
140958
141230
  }
140959
141231
  return { touched: [fld.id] };
@@ -141040,7 +141312,7 @@ function applyOne(d, op, warnings, opts) {
141040
141312
  });
141041
141313
  } else {
141042
141314
  for (const o of op.options) {
141043
- if (!isRec(o) || typeof o.id !== "string" || typeof o.label !== "string") {
141315
+ if (!isRec2(o) || typeof o.id !== "string" || typeof o.label !== "string") {
141044
141316
  return bad("options must be all strings, or all {id, label} pairs");
141045
141317
  }
141046
141318
  const oid = resolveId(d, o.id, ["field"], "option id");
@@ -141061,7 +141333,7 @@ function applyOne(d, op, warnings, opts) {
141061
141333
  const n = ms.length;
141062
141334
  if (op.index !== void 0 && (!Number.isInteger(op.index) || op.index < 0 || op.index > n))
141063
141335
  return bad(`index must be 0\u2013${String(n)} (${String(n)} = last)`);
141064
- const sib = ms.find((m) => m.type === "checkbox" && isRec(m.format));
141336
+ const sib = ms.find((m) => m.type === "checkbox" && isRec2(m.format));
141065
141337
  const f3 = {
141066
141338
  id: randomUUID2(),
141067
141339
  label: op.label.trim(),
@@ -141141,29 +141413,31 @@ function applyOne(d, op, warnings, opts) {
141141
141413
  case "set_headers":
141142
141414
  return table(setHeaders(d, raw, warnings));
141143
141415
  case "reorder":
141144
- 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);
141145
141421
  default:
141146
141422
  return bad(`unknown op "${String(op.op)}"; ops: ${OP_NAMES.join(", ")}`);
141147
141423
  }
141148
141424
  }
141149
- function reorder(d, op) {
141425
+ function reorder(d, op, warnings) {
141150
141426
  const page = pageOf(d, op.page);
141151
141427
  if (!Array.isArray(op.ids) || op.ids.length === 0)
141152
141428
  return bad("ids must list the entries to order");
141153
- const blocks = [];
141429
+ const orderOf2 = (item) => typeof item.order === "number" ? item.order : void 0;
141430
+ const unordered = [];
141154
141431
  for (const f3 of d.s.fields) {
141155
- 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) });
141156
141434
  }
141157
141435
  for (const g of d.s.groups) {
141158
141436
  if (g.page !== page) continue;
141159
- const bb = bbox(members2(d, g));
141160
- 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) });
141161
141439
  }
141162
- const orderOf = (b) => typeof b.item.order === "number" ? b.item.order : void 0;
141163
- const hasOrder = blocks.some((b) => orderOf(b) !== void 0);
141164
- blocks.sort(
141165
- (a, b) => (hasOrder ? (orderOf(a) ?? Infinity) - (orderOf(b) ?? Infinity) || 0 : 0) || a.top - b.top || a.left - b.left
141166
- );
141440
+ const blocks = orderBlocks(unordered);
141167
141441
  const byId = new Map(blocks.map((b) => [b.item.id, b]));
141168
141442
  const want = op.ids.map((raw) => {
141169
141443
  const id = resolveId(d, raw, ["field", "group"], "ids entry");
@@ -141184,10 +141458,132 @@ function reorder(d, op) {
141184
141458
  ...want.map((id) => byId.get(id)),
141185
141459
  ...rest.slice(before)
141186
141460
  ];
141461
+ const was = listedOrder(d, page);
141187
141462
  next.forEach((b, i2) => {
141188
141463
  b.item.order = i2;
141189
141464
  });
141190
- 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 } };
141191
141587
  }
141192
141588
  function checkCombDateFormat(d, g, v) {
141193
141589
  const slots = members2(d, g).reduce(
@@ -141200,6 +141596,18 @@ function checkCombDateFormat(d, g, v) {
141200
141596
  );
141201
141597
  }
141202
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
+ }
141203
141611
  function touchedIssues(d, ids, tables = []) {
141204
141612
  const out = [];
141205
141613
  const seen = /* @__PURE__ */ new Set();
@@ -141241,6 +141649,8 @@ function applyPass(render, ops, excluded, screen, opts) {
141241
141649
  const touchedIds = /* @__PURE__ */ new Set();
141242
141650
  const warnings = [];
141243
141651
  const outcomes = [];
141652
+ const told = [];
141653
+ const confirmed = [];
141244
141654
  let applied = 0;
141245
141655
  ops.forEach((raw, i2) => {
141246
141656
  const skip = excluded.get(i2);
@@ -141259,30 +141669,46 @@ function applyPass(render, ops, excluded, screen, opts) {
141259
141669
  issues.slice(0, 5).map((x2) => `${x2.path} ${x2.reason}`).join("; ")
141260
141670
  );
141261
141671
  }
141672
+ let vouched = [];
141262
141673
  if (screen) {
141263
141674
  const changed = changedWording(cur, draft);
141264
- const echo = changed.length ? screen(changed) : { refuse: [], warn: [] };
141265
- if (echo.refuse.length) {
141266
- bad(
141267
- `${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`
141268
- );
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
+ }
141269
141690
  }
141270
- for (const id of echo.warn) {
141691
+ for (const id of echo?.warn ?? []) {
141271
141692
  w.push(
141272
- `${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`
141273
141694
  );
141274
141695
  }
141275
141696
  }
141276
141697
  const name = raw.op;
141277
141698
  const noop = JSON.stringify(draft) === JSON.stringify(cur);
141699
+ const comb = r.comb !== void 0 ? combFormat(draft, r.comb) : null;
141278
141700
  outcomes.push({
141279
141701
  op_index: i2,
141280
141702
  op: name,
141281
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 } : {},
141282
141707
  ...noop ? { noop: true } : {}
141283
141708
  });
141284
141709
  cur = draft;
141285
141710
  applied++;
141711
+ confirmed.push(...vouched);
141286
141712
  counts[name] = (counts[name] ?? 0) + 1;
141287
141713
  if (r.added) added.push({ op_index: i2, id: r.added });
141288
141714
  if (r.geometry) placed.set(i2, r.geometry);
@@ -141324,16 +141750,17 @@ function applyPass(render, ops, excluded, screen, opts) {
141324
141750
  applied,
141325
141751
  rejected,
141326
141752
  diff_summary: { counts, added, deleted, touched, ops: outcomes },
141327
- warnings
141753
+ warnings,
141754
+ screened: { told, confirmed }
141328
141755
  },
141329
141756
  offPage
141330
141757
  };
141331
141758
  }
141332
141759
  function wordingOf(s) {
141333
141760
  const out = [];
141334
- const add = (id, prop, v) => {
141761
+ const add = (id, prop, v, page) => {
141335
141762
  if (typeof v === "string" && v.trim() && typeof id === "string")
141336
- out.push({ id, prop, text: v });
141763
+ out.push({ id, ...typeof page === "number" ? { page } : {}, prop, text: v });
141337
141764
  };
141338
141765
  const rec = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2) ? x2 : null;
141339
141766
  for (const f3 of s.fields) {
@@ -141353,26 +141780,36 @@ function wordingOf(s) {
141353
141780
  for (const p4 of s.pages) {
141354
141781
  const secs = Array.isArray(p4.sections) ? p4.sections : [];
141355
141782
  for (const sec of secs)
141356
- 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);
141357
141784
  }
141358
141785
  return out;
141359
141786
  }
141360
141787
  function changedWording(prev, next) {
141361
- const before = new Set(wordingOf(prev).map((w) => `${w.id}\0${w.prop}\0${w.text}`));
141362
- 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)));
141363
141791
  }
141364
141792
  function wordingChanges(changed) {
141365
141793
  const by = /* @__PURE__ */ new Map();
141366
141794
  for (const w of changed) {
141367
- const set2 = by.get(w.id) ?? /* @__PURE__ */ new Set();
141368
- set2.add(w.prop);
141369
- by.set(w.id, set2);
141370
- }
141371
- 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
+ }));
141372
141808
  }
141373
- function describeWording(changed, ids) {
141374
- const want = new Set(ids);
141375
- 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(", ");
141376
141813
  }
141377
141814
 
141378
141815
  // src/render.ts
@@ -141453,14 +141890,14 @@ async function withPage(pdf, page, fn5) {
141453
141890
  p4.wasmExports.free(ptr);
141454
141891
  }
141455
141892
  }
141456
- async function renderPage(pdf, page, dpi, crop) {
141893
+ async function renderPage(pdf, page, dpi, crop, maxSide = MAX_SIDE_PX) {
141457
141894
  return withPage(pdf, page, (m, pg, heap) => {
141458
141895
  const wPt = m.FPDF_GetPageWidthF(pg);
141459
141896
  const hPt = m.FPDF_GetPageHeightF(pg);
141460
141897
  const c2 = crop ?? { x: 0, y: 0, w: 1, h: 1 };
141461
141898
  let scale2 = dpi / 72;
141462
141899
  const longest = Math.max(wPt * c2.w, hPt * c2.h) * scale2;
141463
- if (longest > MAX_SIDE_PX) scale2 *= MAX_SIDE_PX / longest;
141900
+ if (longest > maxSide) scale2 *= maxSide / longest;
141464
141901
  const fullW = Math.max(1, Math.round(wPt * scale2));
141465
141902
  const fullH = Math.max(1, Math.round(hPt * scale2));
141466
141903
  const x0 = Math.round(c2.x * fullW);
@@ -141489,7 +141926,7 @@ async function renderPage(pdf, page, dpi, crop) {
141489
141926
  const row = buf + y * stride;
141490
141927
  rgba.set(h8.subarray(row, row + width * 4), y * width * 4);
141491
141928
  }
141492
- return { width, height, rgba };
141929
+ return { width, height, rgba, scale: scale2 };
141493
141930
  } finally {
141494
141931
  m.FPDFBitmap_Destroy(bmp);
141495
141932
  }
@@ -141926,10 +142363,10 @@ var FONT_MIN_PT = 3;
141926
142363
  var OVERLAP_MIN_CHARS = 2;
141927
142364
  var CELL_TOL = 25e-4;
141928
142365
  var BLANK_CHARS = /^[_.\-‐-―…·•‧⋯─━┄┅┈┉_]$/u;
141929
- var multiline = (f3) => isRec(f3.format) && f3.format.variant === "multiline";
142366
+ var multiline = (f3) => isRec2(f3.format) && f3.format.variant === "multiline";
141930
142367
  var singleLine = (f3) => f3.type === "text" && !multiline(f3) || f3.type === "date" && !(f3.cells && f3.cells.length > 1);
141931
142368
  function pinned(f3) {
141932
- const s = isRec(f3.format) ? f3.format.font_size : void 0;
142369
+ const s = isRec2(f3.format) ? f3.format.font_size : void 0;
141933
142370
  return typeof s === "number" ? s : null;
141934
142371
  }
141935
142372
  function stampedSize(f3, pageHeightPt) {
@@ -142055,7 +142492,9 @@ async function placementWarnings2(pdf, render, ids, opts = {}) {
142055
142492
  }
142056
142493
 
142057
142494
  // src/preview.ts
142495
+ var OVERLAYS = ["full", "outline", "none"];
142058
142496
  var MAX_ZOOM = 8;
142497
+ var PREVIEW_MAX_SIDE_PX = 2e3;
142059
142498
  var r4 = (n) => Math.round(n * 1e4) / 1e4;
142060
142499
  function boxOf(fs) {
142061
142500
  if (!fs.length) return null;
@@ -142066,6 +142505,16 @@ function boxOf(fs) {
142066
142505
  return { x: r4(x0 / 100), y: r4(y0 / 100), w: r4((x1 - x0) / 100), h: r4((y1 - y0) / 100) };
142067
142506
  }
142068
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
+ }
142069
142518
  function legendRows(render, page) {
142070
142519
  const schema = normalizeSchema(render);
142071
142520
  const entries = agentEntries(schema);
@@ -142201,6 +142650,7 @@ function legendChanges(before, now) {
142201
142650
  }
142202
142651
  async function previewPage(a) {
142203
142652
  const dpi = Math.min(MAX_DPI, Math.max(MIN_DPI, Math.round(a.dpi ?? DEFAULT_DPI)));
142653
+ const overlay = a.overlay ?? "full";
142204
142654
  const schema = normalizeSchema(a.render);
142205
142655
  const pages = schema.pages.map((p4) => p4.page);
142206
142656
  if (!pages.includes(a.page)) {
@@ -142218,17 +142668,20 @@ async function previewPage(a) {
142218
142668
  branding: null,
142219
142669
  logoBytes: null,
142220
142670
  loadImage,
142221
- check: true,
142671
+ // "none" shows the stamped PDF itself: no check copy is built, so none can fail.
142672
+ check: overlay !== "none",
142222
142673
  // preview_page's JSON carries the colour key; the banner would only
142223
142674
  // cover the top of page 1.
142224
- checkBanner: false
142675
+ checkBanner: false,
142676
+ checkTags: overlay === "full"
142225
142677
  });
142226
142678
  } catch (e4) {
142227
142679
  if (isPasswordError(e4)) throw passwordError();
142228
142680
  if (isFillForbiddenError(e4)) throw fillForbiddenError();
142229
142681
  throw e4;
142230
142682
  }
142231
- if (!res.checkPdf) {
142683
+ const shown = overlay === "none" ? res.pdf : res.checkPdf;
142684
+ if (!shown) {
142232
142685
  const err2 = res.checkError;
142233
142686
  throw new ToolError("check_copy_failed", checkCopyFailed(err2), {
142234
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.",
@@ -142237,14 +142690,19 @@ async function previewPage(a) {
142237
142690
  }
142238
142691
  const legend = legendRows(a.render, a.page);
142239
142692
  const ids = new Set(legend.map((r) => r.id));
142240
- 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
+ });
142241
142699
  const warnings = res.warnings.filter((w) => {
142242
142700
  const head = w.slice(0, Math.max(0, w.indexOf(":")));
142243
142701
  return head.split(" and ").some((id) => ids.has(topId2(id.trim())));
142244
142702
  });
142245
142703
  const zoom = a.crop ? Math.min(MAX_ZOOM, Math.max(1, a.zoom ?? 2)) : 1;
142246
- const raster = await renderPage(res.checkPdf, a.page, dpi * zoom, a.crop);
142247
- 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) => {
142248
142706
  const c2 = a.crop;
142249
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;
142250
142708
  }) : legend;
@@ -142252,8 +142710,10 @@ async function previewPage(a) {
142252
142710
  png: encodePng2(raster),
142253
142711
  width: raster.width,
142254
142712
  height: raster.height,
142255
- dpi: dpi * zoom,
142256
- 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,
142257
142717
  filled: res.filled,
142258
142718
  skipped,
142259
142719
  warnings
@@ -142818,6 +143278,52 @@ async function sweepCache(cache3, now) {
142818
143278
  return removed;
142819
143279
  }
142820
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
+
142821
143327
  // src/server.ts
142822
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.`;
142823
143329
  var INSTRUCTIONS = [
@@ -142830,7 +143336,9 @@ var INSTRUCTIONS = [
142830
143336
  ].join(" ");
142831
143337
  var TEMPLATE_ID = external_exports.string().regex(/^[A-Za-z0-9-]{1,64}$/, "a fenfill template id").describe("Template id (from list_templates).");
142832
143338
  var PAGES = external_exports.string().max(2e3).optional().describe('1-based pages, e.g. "1-3,5". Omit for all.');
142833
- 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
+ );
142834
143342
  var OUTPUT_PATH = external_exports.string().min(1).max(4096).describe("Absolute path (or ~/\u2026) of the new .pdf to write.");
142835
143343
  var OVERWRITE = external_exports.boolean().optional().describe("Replace output_path if it exists.");
142836
143344
  var TARGET_PATH = external_exports.string().min(1).max(4096).optional().describe(
@@ -142877,7 +143385,14 @@ async function refreshSavedAnalyses(cache3, templateId, render, saved, who) {
142877
143385
  const agent = saved.result && Array.isArray(saved.result.fields) ? saved.result : e4.agent;
142878
143386
  await cache3.saveAnalysis({
142879
143387
  ...e4,
142880
- 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
+ },
142881
143396
  agent: { ...agent, template_id: templateId, updated_at: saved.updated_at }
142882
143397
  }).catch(() => void 0);
142883
143398
  }
@@ -142934,6 +143449,19 @@ function createFenfillServer(opts = {}) {
142934
143449
  };
142935
143450
  maybeSweep();
142936
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();
142937
143465
  const lastLegend = /* @__PURE__ */ new Map();
142938
143466
  const fileForm = async (path) => {
142939
143467
  const { abs: abs2, bytes } = await readInputPdf(path);
@@ -143009,6 +143537,7 @@ function createFenfillServer(opts = {}) {
143009
143537
  log("info", "tool ok", { tool: name, ms: Date.now() - t0 });
143010
143538
  return ok(result);
143011
143539
  } catch (e4) {
143540
+ untaken(args);
143012
143541
  const err2 = asToolError(e4);
143013
143542
  log("warn", "tool failed", { tool: name, code: err2.code, ms: Date.now() - t0 });
143014
143543
  return fail(err2);
@@ -143109,11 +143638,11 @@ function createFenfillServer(opts = {}) {
143109
143638
  const templateId = soleTemplateId(merged, null, await identity2(false));
143110
143639
  const wc = await fileEdits({ sha, templateId });
143111
143640
  const render = wc ? wc.render : merged.render;
143112
- echo.remember(render, values2);
143641
+ const answers = taken(render, values2);
143113
143642
  const res = await stampAndWrite({
143114
143643
  pdfBytes: bytes,
143115
143644
  render,
143116
- agentInput: values2,
143645
+ agentInput: answers,
143117
143646
  fonts: loadFonts(opts.fontsDir),
143118
143647
  branding: null,
143119
143648
  logoBytes: null,
@@ -143171,7 +143700,7 @@ function createFenfillServer(opts = {}) {
143171
143700
  }
143172
143701
  render = tpl.render;
143173
143702
  }
143174
- echo.remember(render, values2);
143703
+ const answers = taken(render, values2);
143175
143704
  const { body: account } = await client.getJson("/account", { signal, deadline });
143176
143705
  const blank = await templateBlank(template_id, signal, deadline);
143177
143706
  let logo = null;
@@ -143187,7 +143716,7 @@ function createFenfillServer(opts = {}) {
143187
143716
  const res = await stampAndWrite({
143188
143717
  pdfBytes: blank,
143189
143718
  render,
143190
- agentInput: values2,
143719
+ agentInput: answers,
143191
143720
  fonts: loadFonts(opts.fontsDir),
143192
143721
  branding,
143193
143722
  logoBytes: logo ? toArrayBuffer(logo) : null,
@@ -143303,24 +143832,31 @@ function createFenfillServer(opts = {}) {
143303
143832
  "preview_page",
143304
143833
  {
143305
143834
  title: "Preview a page with its field boxes",
143306
- 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.`,
143307
143836
  inputSchema: {
143308
143837
  path: TARGET_PATH,
143309
143838
  template_id: TARGET_TEMPLATE,
143310
143839
  page: external_exports.number().int().min(1).max(1e5).describe("1-based page number."),
143311
- values: VALUES.optional().describe(`Optional, exactly as for fill_form: ${VALUE_SHAPES}`),
143312
- 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
+ ),
143313
143846
  crop: external_exports.object({
143314
143847
  x: external_exports.number().min(0).max(1),
143315
143848
  y: external_exports.number().min(0).max(1),
143316
143849
  w: external_exports.number().gt(0).max(1),
143317
143850
  h: external_exports.number().gt(0).max(1)
143318
143851
  }).optional().describe(
143319
- "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."
143320
143853
  ),
143321
143854
  zoom: external_exports.number().min(1).max(MAX_ZOOM).optional().describe(`With crop: magnification (default 2, max ${String(MAX_ZOOM)}).`),
143322
143855
  legend: external_exports.enum(["all", "changed", "none"]).optional().describe(
143323
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.'
143324
143860
  )
143325
143861
  },
143326
143862
  annotations: { readOnlyHint: true, openWorldHint: true }
@@ -143350,16 +143886,17 @@ function createFenfillServer(opts = {}) {
143350
143886
  render = wc ? wc.render : (await fetchTemplate(id, signal)).render;
143351
143887
  bytes = await templateBlank(id, signal);
143352
143888
  }
143353
- echo.remember(render, values2);
143889
+ const answers = taken(render, values2);
143354
143890
  const pv = await previewPage({
143355
143891
  pdfBytes: bytes,
143356
143892
  render,
143357
143893
  page: args.page,
143358
- values: values2,
143894
+ values: answers,
143359
143895
  dpi: args.dpi,
143360
143896
  fonts: loadFonts(opts.fontsDir),
143361
143897
  crop: args.crop,
143362
- zoom: args.zoom
143898
+ zoom: args.zoom,
143899
+ overlay: args.overlay
143363
143900
  });
143364
143901
  const placement = await placementWarnings2(
143365
143902
  bytes,
@@ -143392,7 +143929,13 @@ function createFenfillServer(opts = {}) {
143392
143929
  edited: false,
143393
143930
  source: args.path ? "analysis (no local edits)" : "saved template (no local edits)"
143394
143931
  },
143395
- 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
+ },
143396
143939
  ...args.crop ? { crop: args.crop } : {},
143397
143940
  ...mode === "none" ? {} : changes ? {
143398
143941
  legend: changes.changed,
@@ -143414,6 +143957,7 @@ function createFenfillServer(opts = {}) {
143414
143957
  ]
143415
143958
  };
143416
143959
  } catch (e4) {
143960
+ untaken(args);
143417
143961
  const err2 = asToolError(e4);
143418
143962
  log("warn", "tool failed", { tool: "preview_page", code: err2.code, ms: Date.now() - t0 });
143419
143963
  return fail(err2);
@@ -143428,10 +143972,15 @@ function createFenfillServer(opts = {}) {
143428
143972
  inputSchema: {
143429
143973
  path: TARGET_PATH,
143430
143974
  template_id: TARGET_TEMPLATE,
143431
- ops: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())).max(MAX_OPS).describe(
143432
- `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.`
143433
143977
  ),
143434
- 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
+ )
143435
143984
  },
143436
143985
  annotations: {
143437
143986
  readOnlyHint: false,
@@ -143440,8 +143989,15 @@ function createFenfillServer(opts = {}) {
143440
143989
  openWorldHint: true
143441
143990
  }
143442
143991
  },
143443
- 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;
143444
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 ?? [];
143445
144001
  const who = await identity2(false);
143446
144002
  let key;
143447
144003
  let saveTarget;
@@ -143472,6 +144028,7 @@ function createFenfillServer(opts = {}) {
143472
144028
  if (discard === true) discarded = await cache3.deleteEdits(key);
143473
144029
  let wc = await cache3.readEdits(key);
143474
144030
  if (wc && saveTarget && !sameAccount(wc, who)) wc = null;
144031
+ const had = wc !== null;
143475
144032
  const nowIso = new Date(now()).toISOString();
143476
144033
  if (!wc && ops.length === 0) {
143477
144034
  return {
@@ -143513,7 +144070,10 @@ function createFenfillServer(opts = {}) {
143513
144070
  snap = () => ({ none: `no page render (${why})` });
143514
144071
  }
143515
144072
  }
143516
- 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
+ });
143517
144077
  let placement = [];
143518
144078
  if (r.applied > 0) {
143519
144079
  const ids = [
@@ -143531,14 +144091,16 @@ function createFenfillServer(opts = {}) {
143531
144091
  writer,
143532
144092
  ...inherited ? { inherited_wording: true } : {}
143533
144093
  };
143534
- const existed = await cache3.readEdits(key) !== null;
143535
- if (r.applied > 0) {
144094
+ const changed = r.applied > 0 && JSON.stringify(r.render) !== JSON.stringify(wc.render);
144095
+ if (changed) {
143536
144096
  await cache3.writeEdits(out);
143537
- if (saveTarget && !existed) {
144097
+ if (saveTarget && !had) {
143538
144098
  await dropFileCopies(saveTarget, fileSha).catch(() => void 0);
143539
144099
  }
143540
144100
  }
143541
- const hasEdits = r.applied > 0 || existed;
144101
+ echo.tell(r.screened.told);
144102
+ echo.confirm(r.screened.confirmed);
144103
+ const hasEdits = changed || had;
143542
144104
  const left = hasEdits ? schemaIssues(out.render, 10) : [];
143543
144105
  return {
143544
144106
  target: saveTarget ? { template_id: saveTarget } : { path },
@@ -143554,7 +144116,7 @@ function createFenfillServer(opts = {}) {
143554
144116
  )
143555
144117
  ],
143556
144118
  has_edits: hasEdits,
143557
- ops_applied_total: out.ops_applied,
144119
+ ops_applied_total: changed ? out.ops_applied : wc.ops_applied,
143558
144120
  saveable: !!saveTarget,
143559
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."
143560
144122
  };
@@ -143564,11 +144126,11 @@ function createFenfillServer(opts = {}) {
143564
144126
  "save_template",
143565
144127
  {
143566
144128
  title: "Save local edits to a template",
143567
- 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.",
143568
144130
  inputSchema: {
143569
144131
  template_id: TEMPLATE_ID,
143570
144132
  confirm_wording: external_exports.boolean().optional().describe(
143571
- "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."
143572
144134
  )
143573
144135
  },
143574
144136
  annotations: {
@@ -143589,19 +144151,29 @@ function createFenfillServer(opts = {}) {
143589
144151
  };
143590
144152
  }
143591
144153
  const changed = changedWording(wc.base, wc.render);
143592
- const echoed = echo.check(changed);
143593
- 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);
143594
144162
  throw new ToolError(
143595
144163
  "answer_in_template",
143596
- "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.",
143597
144165
  {
143598
- 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.",
143599
- 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
+ }
143600
144172
  }
143601
144173
  );
143602
144174
  }
143603
- const foreign = wc.writer?.owner !== owner || wc.inherited_wording === true;
143604
- if (foreign && changed.length && confirm_wording !== true) {
144175
+ if (unvouched && !(confirm_wording === true && listedForeign.get(wc.key) === listing)) {
144176
+ listedForeign.set(wc.key, listing);
143605
144177
  throw new ToolError(
143606
144178
  "confirm_wording_needed",
143607
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.",
@@ -143631,7 +144203,13 @@ function createFenfillServer(opts = {}) {
143631
144203
  }
143632
144204
  );
143633
144205
  }
143634
- 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
+ };
143635
144213
  let saved;
143636
144214
  try {
143637
144215
  saved = await api().patchJson(
@@ -143673,6 +144251,7 @@ function createFenfillServer(opts = {}) {
143673
144251
  throw e4;
143674
144252
  }
143675
144253
  await cache3.deleteEdits(wc.key);
144254
+ listedForeign.delete(wc.key);
143676
144255
  await dropFileCopies(template_id, wc.sha256).catch(() => void 0);
143677
144256
  await refreshSavedAnalyses(cache3, template_id, schema, saved, await identity2(false));
143678
144257
  return {
@@ -143690,11 +144269,18 @@ function createFenfillServer(opts = {}) {
143690
144269
  return {};
143691
144270
  }
143692
144271
  })(),
143693
- ...echoed.warn.length ? {
143694
- warnings: [
143695
- `${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`
143696
- ]
143697
- } : {},
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
+ })(),
143698
144284
  next: "fill_template, get_template and recipient links now use the saved layout."
143699
144285
  };
143700
144286
  })