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 +35 -0
- package/apps/desktop/package.json +1 -1
- package/apps/desktop/resources/component.js +66 -42
- package/bin/version.mjs +1 -1
- package/dist/component.d.ts +1 -1
- package/dist/component.esm.js +66 -42
- package/dist/component.js +66 -42
- package/dist/floorplan-records.d.ts +1 -27
- package/dist/hj.js +2 -2
- package/dist/index.js +69 -45
- package/dist/mcp.js +289 -4026
- package/dist/server.js +69 -45
- package/dist/version.d.ts +1 -1
- package/package.json +3 -3
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.
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
// src/version.ts
|
|
49
|
-
var VERSION = "1.13.
|
|
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
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
@@ -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 ===
|
|
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 ===
|
|
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 ||
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
|
381
|
-
const
|
|
382
|
-
const
|
|
383
|
-
const
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 &&
|
|
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"
|
|
2497
|
-
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
package/dist/component.d.ts
CHANGED
|
@@ -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.
|
|
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;
|
package/dist/component.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/version.ts
|
|
2
|
-
var VERSION = "1.13.
|
|
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
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
@@ -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 ===
|
|
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 ===
|
|
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 ||
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
334
|
-
const
|
|
335
|
-
const
|
|
336
|
-
const
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 &&
|
|
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"
|
|
2450
|
-
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) => {
|