haltija 1.13.0 → 1.13.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.13.1 (2026-11-01)
4
+
5
+ ### Changed
6
+
7
+ - **Page text is neutralized by floorplan's own `neutralizeProvenance`** (tosijs-floorplan 0.7.0,
8
+ virta #3387), replacing haltija's hand copy of that policy. Output is identical.
9
+ - **Schematics use tosijs-floorplan 0.6.0** (virta #3388). Floorplan now draws haltija's ref pill
10
+ itself (`refStyle: 'pill'`), so haltija's own pill plugin is gone; the picture is unchanged.
11
+ - **A schematic can carry floorplan's `note`**, which says what the drawing can't be trusted to say:
12
+ nothing could be drawn for the region, or (on the tosi-agent tier) no record carries affordance
13
+ evidence. haltija now tells floorplan that its DOM map checked every element (`introspected`), so
14
+ a page with no controls is reported as having none rather than as a blind spot.
15
+
16
+ ### Fixed
17
+
18
+ - **MCP tools now tell the agent their argument types.** Every optional argument is typed
19
+ `["string", "null"]` in the API schema, and the MCP server's converter matched none of those, so
20
+ `click`'s `selector`, `ref`, `text` and every other optional field reached the agent untyped.
21
+ - **The "SCHEMATIC" banner is no longer cut off on narrow viewports** (virta #3381). The geometric
22
+ frame was sized to the page content alone; it is now at least as wide as the banner, as the
23
+ stacked layout's always was.
24
+
25
+ ### Security
26
+
27
+ - **The bundled MCP server (`dist/mcp.js`) carried two vulnerable libraries.** Affected: haltija
28
+ **1.13.0-beta.4 to 1.13.0**, the releases that ship that bundle. `fast-uri` 3.1.8 (via the MCP
29
+ SDK's `ajv`; GHSA-333x-w4r6-6crp, GHSA-w74x-v6f9-j3xg, GHSA-wjr9-j795-jxcr, GHSA-xhq7-xmqj-769h)
30
+ is now 3.1.9, and `zod` 3.25.76 (GHSA-mrhx-c7xc-m3w5, memory exhaustion on oversized
31
+ collections; no 3.x fix) is now 4.6.7. The MCP server's only caller is the local MCP client over
32
+ stdio, so neither was reachable from the network.
33
+ - **Development installs only, nothing shipped:** the desktop app's `electron` 43.8.6
34
+ (GHSA-jr5w-w6x2-84p9, compromised eMudhra intermediate certificates), its `undici` 7.30.1
35
+ (GHSA-xjj6-f33q-929c), and `body-parser` 2.4.0 (GHSA-27r3-jxmh-p828, via the MCP SDK's express,
36
+ which the bundle does not include). Lockfile updates; no other dependency moved.
37
+
3
38
  ## 1.13.0 (2026-10-18)
4
39
 
5
40
  The 1.13 betas, released. Read **Security** and **Upgrading from 1.12** before moving off 1.12.x.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haltija-desktop",
3
- "version": "1.13.0",
3
+ "version": "1.13.1",
4
4
  "private": true,
5
5
  "description": "Haltija Desktop - God Mode Browser for AI Agents",
6
6
  "homepage": "https://github.com/tonioloewald/haltija",
@@ -46,7 +46,7 @@
46
46
  });
47
47
 
48
48
  // src/version.ts
49
- var VERSION = "1.13.0";
49
+ var VERSION = "1.13.1";
50
50
 
51
51
  // src/ws-url.ts
52
52
  function httpBaseFromWsUrl(wsUrl2, fallback = "http://localhost:8700") {
@@ -237,7 +237,15 @@
237
237
  const at = Math.max(v.lastIndexOf(BOUND_TWO_WAY), v.lastIndexOf(BOUND_TO_DOM));
238
238
  return neutralizeArrows(at >= 0 ? v.slice(0, at).trim() : v);
239
239
  };
240
- var neutralizeArrows = (s) => s.replaceAll(BOUND_TWO_WAY, "<->").replaceAll(BOUND_TO_DOM, "<-");
240
+ var PROVENANCE_REWRITES = [
241
+ [BOUND_TWO_WAY, "<->"],
242
+ [BOUND_TO_DOM, "<-"]
243
+ ];
244
+ var neutralizeArrows = (s) => PROVENANCE_REWRITES.reduce((out, [token, neutral]) => out.replaceAll(token, neutral), s);
245
+ var PROVENANCE_TOKENS = Object.freeze(PROVENANCE_REWRITES.map(([token]) => token));
246
+ function neutralizeProvenance(text) {
247
+ return typeof text === "string" ? neutralizeArrows(text) : text;
248
+ }
241
249
  var boundTwoWay = (v) => {
242
250
  if (typeof v !== "string")
243
251
  return false;
@@ -287,7 +295,6 @@
287
295
  return width < targetSize || height < targetSize ? `${width}×${height} — below ${targetSize}×${targetSize} (WCAG 2.5.8)` : null;
288
296
  };
289
297
  var finiteBounds = (b) => Number.isFinite(b.x) && Number.isFinite(b.y) && Number.isFinite(b.width) && Number.isFinite(b.height);
290
- var printable = (value) => value == null || typeof value === "number";
291
298
  var intersects = (a, b) => a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
292
299
  var contains = (outer, inner) => inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
293
300
  var esc = (s) => typeof s !== "string" ? "" : s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
@@ -331,8 +338,8 @@
331
338
  };
332
339
  var schematic = (description, options = {}) => {
333
340
  const {
334
- minLabelHeight = 14,
335
- maxCaption = 36,
341
+ minLabelHeight: minLabelHeightIn = 14,
342
+ maxCaption: maxCaptionIn = 36,
336
343
  within: withinIn,
337
344
  index: showIndex = false,
338
345
  targetSize = TARGET_SIZE_DEFAULT,
@@ -340,9 +347,12 @@
340
347
  decorate
341
348
  } = options;
342
349
  const padIn = options.pad;
343
- const pad = padIn === undefined || !printable(padIn) ? 8 : padIn;
350
+ const pad = padIn === null || typeof padIn === "number" && Number.isFinite(padIn) && padIn >= 0 ? padIn : 8;
344
351
  const fontSizeIn = options.fontSize;
345
- const fontSize = fontSizeIn === undefined || !printable(fontSizeIn) ? 11 : fontSizeIn;
352
+ const fontSize = fontSizeIn === null || typeof fontSizeIn === "number" && Number.isFinite(fontSizeIn) && fontSizeIn > 0 ? fontSizeIn : 11;
353
+ const minLabelHeight = Number.isNaN(minLabelHeightIn) ? 14 : minLabelHeightIn;
354
+ const maxCaption = typeof maxCaptionIn === "number" && (Number.isNaN(maxCaptionIn) || maxCaptionIn < 1) ? 36 : maxCaptionIn;
355
+ const refStyle = options.refStyle === "pill" ? "pill" : "stamp";
346
356
  const legend = [];
347
357
  const within = withinIn == null ? undefined : {
348
358
  x: withinIn.x,
@@ -350,7 +360,7 @@
350
360
  width: withinIn.width,
351
361
  height: withinIn.height
352
362
  };
353
- const withinOk = within == null || printable(within.x) && printable(within.y) && printable(within.width) && printable(within.height);
363
+ const withinOk = within == null || finiteBounds(within);
354
364
  const geometry = new Map;
355
365
  const snapshotOf = (w) => {
356
366
  if (geometry.has(w))
@@ -370,22 +380,38 @@
370
380
  return g.width > 0 && g.height > 0 && (w.viewportFixed === true || g.x + g.width > 0 && g.y + g.height > 0) && (within == null || w.viewportFixed === true || intersects(g, within));
371
381
  });
372
382
  const flow = boxes.filter((w) => w.viewportFixed !== true);
373
- if (boxes.length === 0) {
374
- return {
375
- svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 0 0"></svg>',
376
- legend
377
- };
383
+ const emptyMap = (why) => why == null ? {
384
+ svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 0 0"></svg>',
385
+ legend
386
+ } : {
387
+ svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 0 0">' + `<desc>${esc(why)}</desc></svg>`,
388
+ legend,
389
+ note: why
390
+ };
391
+ if (!withinOk) {
392
+ return emptyMap("nothing drawn: `within` is not a usable region (a field is not a " + "finite number) — this is not an empty region");
378
393
  }
394
+ if (boxes.length === 0)
395
+ return emptyMap();
379
396
  const fitBoxes = flow.length > 0 ? flow : boxes;
380
- const minX = within != null ? within.x - pad : Math.min(...fitBoxes.map((w) => geometry.get(w).x)) - pad;
381
- const minY = within != null ? within.y - pad : Math.min(...fitBoxes.map((w) => geometry.get(w).y)) - pad;
382
- const maxX = within != null ? within.x + within.width + pad : Math.max(...fitBoxes.map((w) => geometry.get(w).x + geometry.get(w).width)) + pad;
383
- const maxY = within != null ? within.y + within.height + pad : Math.max(...fitBoxes.map((w) => geometry.get(w).y + geometry.get(w).height)) + pad;
397
+ const minXRaw = within != null ? within.x - pad : Math.min(...fitBoxes.map((w) => geometry.get(w).x)) - pad;
398
+ const minYRaw = within != null ? within.y - pad : Math.min(...fitBoxes.map((w) => geometry.get(w).y)) - pad;
399
+ const maxXRaw = within != null ? within.x + within.width + pad : Math.max(...fitBoxes.map((w) => geometry.get(w).x + geometry.get(w).width)) + pad;
400
+ const maxYRaw = within != null ? within.y + within.height + pad : Math.max(...fitBoxes.map((w) => geometry.get(w).y + geometry.get(w).height)) + pad;
401
+ const overflow = ![
402
+ minXRaw,
403
+ minYRaw,
404
+ maxXRaw,
405
+ maxYRaw,
406
+ maxXRaw - minXRaw,
407
+ maxYRaw - minYRaw
408
+ ].every(Number.isFinite);
409
+ const [minX, minY, maxX, maxY] = overflow ? [0, 0, 0, 0] : [minXRaw, minYRaw, maxXRaw, maxYRaw];
384
410
  const parts = [
385
411
  `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${minX} ${minY} ${maxX - minX} ${maxY - minY}" width="${maxX - minX}" height="${maxY - minY}">`
386
412
  ];
387
413
  const drawOrder = [...boxes].sort((a, b) => Number(isGround(b)) - Number(isGround(a)));
388
- const blind = boxes.some((w) => !isGround(w)) && !description.wiring.some(isInteractive) && !description.wiring.some(hasCapabilityEvidence);
414
+ const blind = description.introspected !== true && boxes.some((w) => !isGround(w)) && !description.wiring.some(isInteractive) && !description.wiring.some(hasCapabilityEvidence);
389
415
  const note = blind ? "no record carries affordance evidence (on, href, contentEditable, " + "a two-way binding, or an interactive/editable assertion), and none " + "shows the producer can see wiring at all (no handler, assertion, " + 'or provenance arrow anywhere) — "nothing here is actionable" is ' + "NOT established; a producer that cannot introspect handlers " + "should assert `interactive`/`editable` per record (see README)" : undefined;
390
416
  const boxGeometry = boxes.map((b) => geometry.get(b));
391
417
  for (const w of drawOrder) {
@@ -535,9 +561,13 @@
535
561
  if (showIndex || w.ref != null || inLegend) {
536
562
  const shown = w.ref != null ? String(w.ref) : String(index);
537
563
  const indexX = toggle ? x - 3 : x + width - 2;
538
- parts.push(`<rect x="${indexX - shown.length * 5 - 1}" y="${y + 1}" ` + `width="${shown.length * 5 + 2}" height="8" fill="white" ` + `opacity="0.85" data-index-backdrop="true"/>`, `<text x="${indexX}" y="${y + 8}" font-size="8" ` + `text-anchor="end" font-family="monospace" fill="black" ` + `opacity="0.8">${esc(shown)}</text>`);
564
+ if (refStyle === "pill") {
565
+ parts.push(`<rect x="${indexX - shown.length * 5 - 2}" y="${y + 1}" ` + `width="${shown.length * 5 + 3}" height="9" rx="2" ` + `fill="#0f172a" data-index-backdrop="true"/>`, `<text x="${indexX - 1}" y="${y + 8}" font-size="8" ` + `text-anchor="end" font-family="monospace" font-weight="bold" ` + `fill="white">${esc(shown)}</text>`);
566
+ } else {
567
+ parts.push(`<rect x="${indexX - shown.length * 5 - 1}" y="${y + 1}" ` + `width="${shown.length * 5 + 2}" height="8" fill="white" ` + `opacity="0.85" data-index-backdrop="true"/>`, `<text x="${indexX}" y="${y + 8}" font-size="8" ` + `text-anchor="end" font-family="monospace" fill="black" ` + `opacity="0.8">${esc(shown)}</text>`);
568
+ }
539
569
  }
540
- if (decorate != null) {
570
+ if (decorate != null && !overflow) {
541
571
  decorate({
542
572
  record: w,
543
573
  index,
@@ -551,6 +581,9 @@
551
581
  }
552
582
  parts.push("</g>");
553
583
  }
584
+ if (overflow) {
585
+ return emptyMap("nothing drawn: the map's extent overflows a finite number (a " + "coordinate, size or pad near Number.MAX_VALUE); the legend still " + "carries every record's facts");
586
+ }
554
587
  const footerExtra = legendNote && legend.length > 0 ? 14 : 0;
555
588
  if (footerExtra > 0) {
556
589
  parts.push(`<text x="${minX + pad}" y="${maxY + 10}" font-size="8" ` + `font-family="monospace" fill="currentColor" opacity="0.75">` + `${legend.length} element${legend.length === 1 ? "" : "s"} with ` + `details in legend — match by stamped number</text>`);
@@ -570,23 +603,10 @@
570
603
  };
571
604
 
572
605
  // src/floorplan-records.ts
573
- function neutralizeArrows2(s) {
574
- return s.replaceAll("⟷", "<->").replaceAll("⟵", "<-");
575
- }
576
606
  var PAGE_TEXT_FIELDS = ["label", "placeholder", "text", "value"];
577
607
  function isPureLayout(n) {
578
608
  return !!n.children?.length && !n.label && !n.text && n.value === undefined && !n.href && !n.placeholder && !n.contrastFail && !n.interactive && !n.ownBg && !n.ownBorder;
579
609
  }
580
- function refPill(ctx) {
581
- const ref = ctx.record.ref;
582
- if (ref == null)
583
- return;
584
- const shown = String(ref).replace(/[&<>"]/g, "");
585
- const toggle = ctx.record.type === "checkbox" || ctx.record.type === "radio";
586
- const right = toggle ? ctx.x - 3 : ctx.x + ctx.width - 2;
587
- const w = shown.length * 5 + 3;
588
- ctx.emit(`<rect x="${right - w + 1}" y="${ctx.y + 1}" width="${w}" height="9" rx="2" fill="#0f172a" opacity="0.9"/>` + `<text x="${right}" y="${ctx.y + 8.5}" text-anchor="end" font-family="ui-monospace,Menlo,monospace" font-size="8" font-weight="bold" fill="#ffffff">${shown}</text>`);
589
- }
590
610
  var STRUCTURAL_TAGS = new Set([
591
611
  "main",
592
612
  "nav",
@@ -2045,14 +2065,18 @@
2045
2065
  const bannerSvg = (width) => banner ? `<rect x="0" y="0" width="${width}" height="${BANNER_H}" fill="#7f1d1d"/>` + `<text x="${PAD}" y="17" font-family="ui-monospace,Menlo,monospace" font-size="11" font-weight="bold" fill="#fee2e2">${esc(banner)}</text>` : "";
2046
2066
  if (geometric) {
2047
2067
  const within = fullPage ? undefined : { x: window.scrollX, y: window.scrollY, width: Math.max(1, window.innerWidth), height: Math.max(1, window.innerHeight) };
2048
- const result = schematic({ wiring: records }, { within, targetSize: 0, pad: PAD, maxCaption: 200, decorate: refPill });
2068
+ const result = schematic({ wiring: records, ...map.source === "tosi-agent" ? {} : { introspected: true } }, { within, targetSize: 0, pad: PAD, maxCaption: 200, refStyle: "pill" });
2049
2069
  const w = Number(/\bwidth="([\d.]+)"/.exec(result.svg)?.[1] || 0);
2050
2070
  const h = Number(/\bheight="([\d.]+)"/.exec(result.svg)?.[1] || 0);
2051
2071
  if (w > 0 && h > 0) {
2052
- const width = Math.ceil(w);
2072
+ const bannerCtx = banner ? document.createElement("canvas").getContext("2d") : null;
2073
+ if (bannerCtx)
2074
+ bannerCtx.font = "bold 11px ui-monospace, Menlo, monospace";
2075
+ const bannerW = bannerCtx ? Math.ceil(bannerCtx.measureText(banner).width) + PAD * 2 : 0;
2076
+ const width = Math.max(Math.ceil(w), bannerW);
2053
2077
  const height = Math.ceil(h + BANNER_H);
2054
2078
  const svg = `<?xml version="1.0" encoding="UTF-8"?>` + `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">` + `<rect width="${width}" height="${height}" fill="#ffffff"/>` + bannerSvg(width) + result.svg.replace("<svg ", `<svg x="0" y="${BANNER_H}" `) + `</svg>`;
2055
- return { svg, width, height, layout: "geometric", boundsCoverage };
2079
+ return { svg, width, height, layout: "geometric", boundsCoverage, ...result.note ? { note: result.note } : {} };
2056
2080
  }
2057
2081
  }
2058
2082
  const ROW = 22;
@@ -2070,7 +2094,7 @@
2070
2094
  if (typeof v === "string")
2071
2095
  bound.push(`${k}: ${v}`);
2072
2096
  }
2073
- if (w.text && /[⟷⟵]/.test(w.text))
2097
+ if (typeof w.text === "string" && PROVENANCE_TOKENS.some((t) => w.text.includes(t)))
2074
2098
  bound.push(String(w.text));
2075
2099
  const handlers = w.on ? Object.entries(w.on).map(([ev, path]) => `${ev} → ${Array.isArray(path) ? path.join(",") : path}`) : [];
2076
2100
  return {
@@ -2193,7 +2217,7 @@
2193
2217
  }
2194
2218
  async function buildSchematicResponse(map, payload2, banner) {
2195
2219
  const canvases = collectCanvasThumbnails();
2196
- const { svg, width, height, layout, boundsCoverage } = renderMapSchematic(map, canvases, banner, payload2?.fullPage === true, payload2?.layout || "auto");
2220
+ const { svg, width, height, layout, boundsCoverage, note } = renderMapSchematic(map, canvases, banner, payload2?.fullPage === true, payload2?.layout || "auto");
2197
2221
  const format = payload2?.format || "png";
2198
2222
  const raster = await rasterizeSchematic(svg, width, height, payload2?.scale || 1, {
2199
2223
  maxWidth: payload2?.maxWidth,
@@ -2209,6 +2233,7 @@
2209
2233
  source: "schematic",
2210
2234
  layout,
2211
2235
  boundsCoverage,
2236
+ ...note ? { note } : {},
2212
2237
  legend: buildLegend(map),
2213
2238
  canvasesRendered: canvases.filter((c) => c.image).length,
2214
2239
  map
@@ -2492,10 +2517,9 @@
2492
2517
  h: Math.round(r.height)
2493
2518
  };
2494
2519
  } catch {}
2495
- for (const k of PAGE_TEXT_FIELDS) {
2496
- if (typeof node[k] === "string" && /[⟷⟵]/.test(node[k]))
2497
- node[k] = neutralizeArrows2(node[k]);
2498
- }
2520
+ for (const k of PAGE_TEXT_FIELDS)
2521
+ if (typeof node[k] === "string")
2522
+ node[k] = neutralizeProvenance(node[k]);
2499
2523
  return node;
2500
2524
  };
2501
2525
  const walk = (el) => {
package/bin/version.mjs CHANGED
@@ -3,4 +3,4 @@
3
3
  * ⚠️ To change the version, update package.json and run: bun run build
4
4
  */
5
5
 
6
- export const HJ_VERSION = '1.13.0'
6
+ export const HJ_VERSION = '1.13.1'
@@ -20,7 +20,7 @@
20
20
  * - Option+Tab toggles visibility (but active state always shows briefly)
21
21
  * - Localhost only by default
22
22
  */
23
- export declare const VERSION = "1.13.0";
23
+ export declare const VERSION = "1.13.1";
24
24
  export declare class DevChannel extends HTMLElement {
25
25
  static get tagName(): string;
26
26
  static elementCreator(): () => DevChannel;
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "1.13.0";
2
+ var VERSION = "1.13.1";
3
3
 
4
4
  // src/ws-url.ts
5
5
  function httpBaseFromWsUrl(wsUrl2, fallback = "http://localhost:8700") {
@@ -190,7 +190,15 @@ var shownValue = (v) => {
190
190
  const at = Math.max(v.lastIndexOf(BOUND_TWO_WAY), v.lastIndexOf(BOUND_TO_DOM));
191
191
  return neutralizeArrows(at >= 0 ? v.slice(0, at).trim() : v);
192
192
  };
193
- var neutralizeArrows = (s) => s.replaceAll(BOUND_TWO_WAY, "<->").replaceAll(BOUND_TO_DOM, "<-");
193
+ var PROVENANCE_REWRITES = [
194
+ [BOUND_TWO_WAY, "<->"],
195
+ [BOUND_TO_DOM, "<-"]
196
+ ];
197
+ var neutralizeArrows = (s) => PROVENANCE_REWRITES.reduce((out, [token, neutral]) => out.replaceAll(token, neutral), s);
198
+ var PROVENANCE_TOKENS = Object.freeze(PROVENANCE_REWRITES.map(([token]) => token));
199
+ function neutralizeProvenance(text) {
200
+ return typeof text === "string" ? neutralizeArrows(text) : text;
201
+ }
194
202
  var boundTwoWay = (v) => {
195
203
  if (typeof v !== "string")
196
204
  return false;
@@ -240,7 +248,6 @@ var sizeFinding = (w, width, height, targetSize = TARGET_SIZE_DEFAULT, { honorPr
240
248
  return width < targetSize || height < targetSize ? `${width}×${height} — below ${targetSize}×${targetSize} (WCAG 2.5.8)` : null;
241
249
  };
242
250
  var finiteBounds = (b) => Number.isFinite(b.x) && Number.isFinite(b.y) && Number.isFinite(b.width) && Number.isFinite(b.height);
243
- var printable = (value) => value == null || typeof value === "number";
244
251
  var intersects = (a, b) => a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
245
252
  var contains = (outer, inner) => inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
246
253
  var esc = (s) => typeof s !== "string" ? "" : s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
@@ -284,8 +291,8 @@ var wrapCaption = (caption, maxChars, maxLines) => {
284
291
  };
285
292
  var schematic = (description, options = {}) => {
286
293
  const {
287
- minLabelHeight = 14,
288
- maxCaption = 36,
294
+ minLabelHeight: minLabelHeightIn = 14,
295
+ maxCaption: maxCaptionIn = 36,
289
296
  within: withinIn,
290
297
  index: showIndex = false,
291
298
  targetSize = TARGET_SIZE_DEFAULT,
@@ -293,9 +300,12 @@ var schematic = (description, options = {}) => {
293
300
  decorate
294
301
  } = options;
295
302
  const padIn = options.pad;
296
- const pad = padIn === undefined || !printable(padIn) ? 8 : padIn;
303
+ const pad = padIn === null || typeof padIn === "number" && Number.isFinite(padIn) && padIn >= 0 ? padIn : 8;
297
304
  const fontSizeIn = options.fontSize;
298
- const fontSize = fontSizeIn === undefined || !printable(fontSizeIn) ? 11 : fontSizeIn;
305
+ const fontSize = fontSizeIn === null || typeof fontSizeIn === "number" && Number.isFinite(fontSizeIn) && fontSizeIn > 0 ? fontSizeIn : 11;
306
+ const minLabelHeight = Number.isNaN(minLabelHeightIn) ? 14 : minLabelHeightIn;
307
+ const maxCaption = typeof maxCaptionIn === "number" && (Number.isNaN(maxCaptionIn) || maxCaptionIn < 1) ? 36 : maxCaptionIn;
308
+ const refStyle = options.refStyle === "pill" ? "pill" : "stamp";
299
309
  const legend = [];
300
310
  const within = withinIn == null ? undefined : {
301
311
  x: withinIn.x,
@@ -303,7 +313,7 @@ var schematic = (description, options = {}) => {
303
313
  width: withinIn.width,
304
314
  height: withinIn.height
305
315
  };
306
- const withinOk = within == null || printable(within.x) && printable(within.y) && printable(within.width) && printable(within.height);
316
+ const withinOk = within == null || finiteBounds(within);
307
317
  const geometry = new Map;
308
318
  const snapshotOf = (w) => {
309
319
  if (geometry.has(w))
@@ -323,22 +333,38 @@ var schematic = (description, options = {}) => {
323
333
  return g.width > 0 && g.height > 0 && (w.viewportFixed === true || g.x + g.width > 0 && g.y + g.height > 0) && (within == null || w.viewportFixed === true || intersects(g, within));
324
334
  });
325
335
  const flow = boxes.filter((w) => w.viewportFixed !== true);
326
- if (boxes.length === 0) {
327
- return {
328
- svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 0 0"></svg>',
329
- legend
330
- };
336
+ const emptyMap = (why) => why == null ? {
337
+ svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 0 0"></svg>',
338
+ legend
339
+ } : {
340
+ svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 0 0">' + `<desc>${esc(why)}</desc></svg>`,
341
+ legend,
342
+ note: why
343
+ };
344
+ if (!withinOk) {
345
+ return emptyMap("nothing drawn: `within` is not a usable region (a field is not a " + "finite number) — this is not an empty region");
331
346
  }
347
+ if (boxes.length === 0)
348
+ return emptyMap();
332
349
  const fitBoxes = flow.length > 0 ? flow : boxes;
333
- const minX = within != null ? within.x - pad : Math.min(...fitBoxes.map((w) => geometry.get(w).x)) - pad;
334
- const minY = within != null ? within.y - pad : Math.min(...fitBoxes.map((w) => geometry.get(w).y)) - pad;
335
- const maxX = within != null ? within.x + within.width + pad : Math.max(...fitBoxes.map((w) => geometry.get(w).x + geometry.get(w).width)) + pad;
336
- const maxY = within != null ? within.y + within.height + pad : Math.max(...fitBoxes.map((w) => geometry.get(w).y + geometry.get(w).height)) + pad;
350
+ const minXRaw = within != null ? within.x - pad : Math.min(...fitBoxes.map((w) => geometry.get(w).x)) - pad;
351
+ const minYRaw = within != null ? within.y - pad : Math.min(...fitBoxes.map((w) => geometry.get(w).y)) - pad;
352
+ const maxXRaw = within != null ? within.x + within.width + pad : Math.max(...fitBoxes.map((w) => geometry.get(w).x + geometry.get(w).width)) + pad;
353
+ const maxYRaw = within != null ? within.y + within.height + pad : Math.max(...fitBoxes.map((w) => geometry.get(w).y + geometry.get(w).height)) + pad;
354
+ const overflow = ![
355
+ minXRaw,
356
+ minYRaw,
357
+ maxXRaw,
358
+ maxYRaw,
359
+ maxXRaw - minXRaw,
360
+ maxYRaw - minYRaw
361
+ ].every(Number.isFinite);
362
+ const [minX, minY, maxX, maxY] = overflow ? [0, 0, 0, 0] : [minXRaw, minYRaw, maxXRaw, maxYRaw];
337
363
  const parts = [
338
364
  `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${minX} ${minY} ${maxX - minX} ${maxY - minY}" width="${maxX - minX}" height="${maxY - minY}">`
339
365
  ];
340
366
  const drawOrder = [...boxes].sort((a, b) => Number(isGround(b)) - Number(isGround(a)));
341
- const blind = boxes.some((w) => !isGround(w)) && !description.wiring.some(isInteractive) && !description.wiring.some(hasCapabilityEvidence);
367
+ const blind = description.introspected !== true && boxes.some((w) => !isGround(w)) && !description.wiring.some(isInteractive) && !description.wiring.some(hasCapabilityEvidence);
342
368
  const note = blind ? "no record carries affordance evidence (on, href, contentEditable, " + "a two-way binding, or an interactive/editable assertion), and none " + "shows the producer can see wiring at all (no handler, assertion, " + 'or provenance arrow anywhere) — "nothing here is actionable" is ' + "NOT established; a producer that cannot introspect handlers " + "should assert `interactive`/`editable` per record (see README)" : undefined;
343
369
  const boxGeometry = boxes.map((b) => geometry.get(b));
344
370
  for (const w of drawOrder) {
@@ -488,9 +514,13 @@ var schematic = (description, options = {}) => {
488
514
  if (showIndex || w.ref != null || inLegend) {
489
515
  const shown = w.ref != null ? String(w.ref) : String(index);
490
516
  const indexX = toggle ? x - 3 : x + width - 2;
491
- parts.push(`<rect x="${indexX - shown.length * 5 - 1}" y="${y + 1}" ` + `width="${shown.length * 5 + 2}" height="8" fill="white" ` + `opacity="0.85" data-index-backdrop="true"/>`, `<text x="${indexX}" y="${y + 8}" font-size="8" ` + `text-anchor="end" font-family="monospace" fill="black" ` + `opacity="0.8">${esc(shown)}</text>`);
517
+ if (refStyle === "pill") {
518
+ parts.push(`<rect x="${indexX - shown.length * 5 - 2}" y="${y + 1}" ` + `width="${shown.length * 5 + 3}" height="9" rx="2" ` + `fill="#0f172a" data-index-backdrop="true"/>`, `<text x="${indexX - 1}" y="${y + 8}" font-size="8" ` + `text-anchor="end" font-family="monospace" font-weight="bold" ` + `fill="white">${esc(shown)}</text>`);
519
+ } else {
520
+ parts.push(`<rect x="${indexX - shown.length * 5 - 1}" y="${y + 1}" ` + `width="${shown.length * 5 + 2}" height="8" fill="white" ` + `opacity="0.85" data-index-backdrop="true"/>`, `<text x="${indexX}" y="${y + 8}" font-size="8" ` + `text-anchor="end" font-family="monospace" fill="black" ` + `opacity="0.8">${esc(shown)}</text>`);
521
+ }
492
522
  }
493
- if (decorate != null) {
523
+ if (decorate != null && !overflow) {
494
524
  decorate({
495
525
  record: w,
496
526
  index,
@@ -504,6 +534,9 @@ var schematic = (description, options = {}) => {
504
534
  }
505
535
  parts.push("</g>");
506
536
  }
537
+ if (overflow) {
538
+ return emptyMap("nothing drawn: the map's extent overflows a finite number (a " + "coordinate, size or pad near Number.MAX_VALUE); the legend still " + "carries every record's facts");
539
+ }
507
540
  const footerExtra = legendNote && legend.length > 0 ? 14 : 0;
508
541
  if (footerExtra > 0) {
509
542
  parts.push(`<text x="${minX + pad}" y="${maxY + 10}" font-size="8" ` + `font-family="monospace" fill="currentColor" opacity="0.75">` + `${legend.length} element${legend.length === 1 ? "" : "s"} with ` + `details in legend — match by stamped number</text>`);
@@ -523,23 +556,10 @@ var schematic = (description, options = {}) => {
523
556
  };
524
557
 
525
558
  // src/floorplan-records.ts
526
- function neutralizeArrows2(s) {
527
- return s.replaceAll("⟷", "<->").replaceAll("⟵", "<-");
528
- }
529
559
  var PAGE_TEXT_FIELDS = ["label", "placeholder", "text", "value"];
530
560
  function isPureLayout(n) {
531
561
  return !!n.children?.length && !n.label && !n.text && n.value === undefined && !n.href && !n.placeholder && !n.contrastFail && !n.interactive && !n.ownBg && !n.ownBorder;
532
562
  }
533
- function refPill(ctx) {
534
- const ref = ctx.record.ref;
535
- if (ref == null)
536
- return;
537
- const shown = String(ref).replace(/[&<>"]/g, "");
538
- const toggle = ctx.record.type === "checkbox" || ctx.record.type === "radio";
539
- const right = toggle ? ctx.x - 3 : ctx.x + ctx.width - 2;
540
- const w = shown.length * 5 + 3;
541
- ctx.emit(`<rect x="${right - w + 1}" y="${ctx.y + 1}" width="${w}" height="9" rx="2" fill="#0f172a" opacity="0.9"/>` + `<text x="${right}" y="${ctx.y + 8.5}" text-anchor="end" font-family="ui-monospace,Menlo,monospace" font-size="8" font-weight="bold" fill="#ffffff">${shown}</text>`);
542
- }
543
563
  var STRUCTURAL_TAGS = new Set([
544
564
  "main",
545
565
  "nav",
@@ -1998,14 +2018,18 @@ function renderMapSchematic(map, canvases = [], banner, fullPage = false, layout
1998
2018
  const bannerSvg = (width) => banner ? `<rect x="0" y="0" width="${width}" height="${BANNER_H}" fill="#7f1d1d"/>` + `<text x="${PAD}" y="17" font-family="ui-monospace,Menlo,monospace" font-size="11" font-weight="bold" fill="#fee2e2">${esc(banner)}</text>` : "";
1999
2019
  if (geometric) {
2000
2020
  const within = fullPage ? undefined : { x: window.scrollX, y: window.scrollY, width: Math.max(1, window.innerWidth), height: Math.max(1, window.innerHeight) };
2001
- const result = schematic({ wiring: records }, { within, targetSize: 0, pad: PAD, maxCaption: 200, decorate: refPill });
2021
+ const result = schematic({ wiring: records, ...map.source === "tosi-agent" ? {} : { introspected: true } }, { within, targetSize: 0, pad: PAD, maxCaption: 200, refStyle: "pill" });
2002
2022
  const w = Number(/\bwidth="([\d.]+)"/.exec(result.svg)?.[1] || 0);
2003
2023
  const h = Number(/\bheight="([\d.]+)"/.exec(result.svg)?.[1] || 0);
2004
2024
  if (w > 0 && h > 0) {
2005
- const width = Math.ceil(w);
2025
+ const bannerCtx = banner ? document.createElement("canvas").getContext("2d") : null;
2026
+ if (bannerCtx)
2027
+ bannerCtx.font = "bold 11px ui-monospace, Menlo, monospace";
2028
+ const bannerW = bannerCtx ? Math.ceil(bannerCtx.measureText(banner).width) + PAD * 2 : 0;
2029
+ const width = Math.max(Math.ceil(w), bannerW);
2006
2030
  const height = Math.ceil(h + BANNER_H);
2007
2031
  const svg = `<?xml version="1.0" encoding="UTF-8"?>` + `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">` + `<rect width="${width}" height="${height}" fill="#ffffff"/>` + bannerSvg(width) + result.svg.replace("<svg ", `<svg x="0" y="${BANNER_H}" `) + `</svg>`;
2008
- return { svg, width, height, layout: "geometric", boundsCoverage };
2032
+ return { svg, width, height, layout: "geometric", boundsCoverage, ...result.note ? { note: result.note } : {} };
2009
2033
  }
2010
2034
  }
2011
2035
  const ROW = 22;
@@ -2023,7 +2047,7 @@ function renderMapSchematic(map, canvases = [], banner, fullPage = false, layout
2023
2047
  if (typeof v === "string")
2024
2048
  bound.push(`${k}: ${v}`);
2025
2049
  }
2026
- if (w.text && /[⟷⟵]/.test(w.text))
2050
+ if (typeof w.text === "string" && PROVENANCE_TOKENS.some((t) => w.text.includes(t)))
2027
2051
  bound.push(String(w.text));
2028
2052
  const handlers = w.on ? Object.entries(w.on).map(([ev, path]) => `${ev} → ${Array.isArray(path) ? path.join(",") : path}`) : [];
2029
2053
  return {
@@ -2146,7 +2170,7 @@ function buildLegend(map) {
2146
2170
  }
2147
2171
  async function buildSchematicResponse(map, payload2, banner) {
2148
2172
  const canvases = collectCanvasThumbnails();
2149
- const { svg, width, height, layout, boundsCoverage } = renderMapSchematic(map, canvases, banner, payload2?.fullPage === true, payload2?.layout || "auto");
2173
+ const { svg, width, height, layout, boundsCoverage, note } = renderMapSchematic(map, canvases, banner, payload2?.fullPage === true, payload2?.layout || "auto");
2150
2174
  const format = payload2?.format || "png";
2151
2175
  const raster = await rasterizeSchematic(svg, width, height, payload2?.scale || 1, {
2152
2176
  maxWidth: payload2?.maxWidth,
@@ -2162,6 +2186,7 @@ async function buildSchematicResponse(map, payload2, banner) {
2162
2186
  source: "schematic",
2163
2187
  layout,
2164
2188
  boundsCoverage,
2189
+ ...note ? { note } : {},
2165
2190
  legend: buildLegend(map),
2166
2191
  canvasesRendered: canvases.filter((c) => c.image).length,
2167
2192
  map
@@ -2445,10 +2470,9 @@ function buildDomAffordances(maxNodes = 400) {
2445
2470
  h: Math.round(r.height)
2446
2471
  };
2447
2472
  } catch {}
2448
- for (const k of PAGE_TEXT_FIELDS) {
2449
- if (typeof node[k] === "string" && /[⟷⟵]/.test(node[k]))
2450
- node[k] = neutralizeArrows2(node[k]);
2451
- }
2473
+ for (const k of PAGE_TEXT_FIELDS)
2474
+ if (typeof node[k] === "string")
2475
+ node[k] = neutralizeProvenance(node[k]);
2452
2476
  return node;
2453
2477
  };
2454
2478
  const walk = (el) => {