/*{ "parent": "utilities", "description": "EXPERIMENTAL schematic renderer: draw an agent-surface description as SVG — the app's affordance map at its true geometry, DOM-free." }*/ /*# # schematic (EXPERIMENTAL) `schematicSVG(description)` renders `agent.describe()` output as an SVG string — one rectangle per visible wired element at its **actual position and size** (`bounds` rides in the map), captioned from the map, optionally wearing the app's computed colors (`describe({ styles: true })`). import { enableAgentInterface, schematicSVG } from 'tosijs' const agent = enableAgentInterface() const svg = schematicSVG(agent.describe({ styles: true })) It is a **pure function over plain data** — no DOM, no layout engine, no screenshots — so it runs anywhere the description can travel: in the page, in a headless embodiment, or on the far side of a wire from an app nobody is viewing. Division of labor per consumer: **JSON for text reasoning**, **rasterized PNG for vision encoders** (rasterize the SVG at 2× so labels OCR cleanly — canvas in a browser, `@resvg/resvg-js` under bun), **SVG for humans and tools** (deterministic, diffable, and each `` carries `data-record=""` linking it back to `description.wiring[i]` — the image as index). > **EXPERIMENTAL.** Ships alongside the agent surface; shapes may change. */ // VENDORED from tosijs-floorplan@0.5.0 — the upstream package // is the source of truth. DO NOT EDIT below this line: edit // tosijs-floorplan and rebuild (this section regenerates at build time). // tosijs stays ZERO runtime dependencies — the core is inlined, not imported. /** * tosijs-floorplan — render an agent-surface map as a floorplan SVG. * * (Formerly tosijs-schematic — renamed to stop near-colliding with * tosijs-schema. Exported API names are unchanged.) * * A PURE FUNCTION over plain data: one record per wired element, drawn at * its true geometry, wearing the affordance grammar. No DOM, no framework, * no dependencies — the map travels as JSON, so this runs in the page, in * a headless embodiment, or on the far side of a wire from an app nobody * is viewing. * * The RECORD FORMAT is the contract (see README): tosijs's describe() * produces it, but anything that emits records gets the renderer — and * every consumer inherits the grammar's hard-won rules (geometry over * glyphs, hints are not content, ground is not figure). */ /** provenance tokens for bound values: "shown ⟵ path" (display-only) and * "shown ⟷ path" (two-way — user-writable). Part of the record format. */ export const BOUND_TO_DOM = '⟵' export const BOUND_TWO_WAY = '⟷' /** * One wired element, flat. Producers may include fields beyond these — * bound props ride as "value ⟷ path" strings under their own keys. */ export interface SchematicRecord { tag: string id?: string part?: string role?: string label?: string placeholder?: string type?: string checked?: boolean focused?: boolean invalid?: boolean required?: boolean disabled?: boolean contentEditable?: boolean description?: string text?: string on?: Record list?: { path: string; idPath?: string } bounds?: { x: number; y: number; width: number; height: number } viewportFixed?: boolean structural?: boolean style?: { background: string; borderColor: string; color: string } /** a DURABLE, actionable handle from the producer (haltija's `@42`) — * survives re-renders where a wiring index doesn't; rendered in the * index slot in preference to the index, and emitted as data-ref */ ref?: string /** computed verdicts about this element (WCAG contrast failures, etc.) — * drawn as severity-colored bars on the LEFT edge (the unclaimed slot), * with the first flag's label */ flags?: Array<{ kind: string; label: string; severity?: 'info' | 'warn' | 'error' }> /** pixels a pure renderer can't obtain: a data-URL snapshot of inline * media (serialized , .toDataURL()) drawn IN PLACE — on an * illustration-led page the picture IS the content */ image?: string /** a link's destination — the most actionable fact about a link, and * deliberately distinct from `text` ("the link says X" is not "the link * goes to Y"). Captions fall back to it only when nothing else names the * element; it ALWAYS rides the legend — URLs are the facts most often * too long to draw */ href?: string /** a filled control's value, distinct from label/placeholder — static * ("3") or bound ("3 ⟷ app.qty"). tosijs emits it as a bound prop; the * declared field gives plain-DOM producers the same home */ value?: string /** the producer's ASSERTION that this element can be acted on — for * producers that cannot introspect handlers (React delegates at a root; * vanilla addEventListener is not enumerable from page script). A binding * framework never needs it: `on` and two-way bindings already say so. * Asserting is truth-telling; fabricating `on` to unlock the styling * would be a lie in the payload. (issue #3, haltija) */ interactive?: boolean /** the producer's assertion that text goes in here — the DOM-side * counterpart of contentEditable/two-way bindings (issue #3) */ editable?: boolean /** the producer WITHHELD facts about this element (tosijs 1.11.0's * secret regions: a magic-link token lives in the href, so neither * label nor href is published). Drawn with a `[withheld]` caption when * nothing else names it, and the legend says redacted — "this link has * no destination" and "its destination was withheld" are different * facts (issue #15) */ secret?: boolean [boundProp: string]: unknown } /** the map: only `wiring` is read. The named optional fields are the * known producer extras (tosijs's describe() shape) — deliberately NOT an * index signature, which would stop interface-typed producers (TS gives * implicit index signatures to literals, never to interfaces) from * assigning without casts. */ export interface SchematicDescription { wiring: SchematicRecord[] roots?: unknown actions?: unknown exposure?: unknown contract?: unknown } export interface SchematicBounds { x: number y: number width: number height: number } export interface SchematicOptions { /** padding around the drawn region, px (default 8) */ pad?: number /** boxes shorter than this get no caption (default 14) */ minLabelHeight?: number /** caption length limit (default 36) */ maxCaption?: number /** caption font size, px (default 11) */ fontSize?: number /** * Scope the map SPATIALLY: only records whose bounds intersect this * page-coordinate rect are drawn, and the viewBox IS the rect — the * schematic becomes "this region of the page". Use `boundsOf(element)` * to scope to an element's region; omit for the whole map. */ within?: SchematicBounds /** * Stamp each box with its wiring index (top-right corner) — the raster * form of `data-record`: a vision consumer reads the number off the image * and looks the record up in `description.wiring[n]` — image as legend. */ index?: boolean /** * Interactive elements (handlers or editable, toggles exempt as * user-agent-sized) smaller than this on either axis are flagged * undersized — amber bar + legend fact. Default 24 (WCAG 2.5.8 AA); * raise to 44/48 for the AAA / platform touch-target bar. 0 disables. */ targetSize?: number /** draw the footer strip advertising the legend when it's non-empty * (default true) — the raster must confess what it couldn't carry */ legendNote?: boolean /** * EXPERIMENTAL plugin seam: called once per drawn record, just before * its closes — emit extra SVG into the record's group. The corner * slots already spoken for: top-left = invalid flag, top-right = index, * bottom-right = ↔ badge, outline = focus ring / emphasis. Claim empty * real estate; the first real plugins will shape the successor API. */ decorate?: (ctx: { record: SchematicRecord index: number x: number y: number width: number height: number structural: boolean emit: (svg: string) => void }) => void } /** what the drawing could not legibly carry, keyed back by index/ref — * the image's companion JSON. Pair every raster with this. */ export interface SchematicLegendEntry { index: number ref?: string tag: string /** the caption that would have been drawn (or its untruncated form) */ caption?: string editable?: boolean required?: boolean invalid?: boolean disabled?: boolean flags?: Array<{ kind: string; label: string; severity?: 'info' | 'warn' | 'error' }> /** the link's destination — carried whenever the record has one */ href?: string /** the control's held value (provenance stripped), when the drawing * elided or truncated it */ value?: string /** interactive element below the target-size floor, e.g. * "18×13 — below 24×24 (WCAG 2.5.8)" */ undersized?: string /** the producer withheld facts about this record (`secret: true`) — * a missing href here means "withheld", not "no destination" */ redacted?: boolean } export interface SchematicResult { svg: string legend: SchematicLegendEntry[] /** set when the map draws affordance-shaped boxes but NO record carries * any affordance evidence: "nothing here is actionable" and "the * producer couldn't tell" are different statements, and a consumer * acting on the first when the truth is the second is the * confident-wrong-answer case (issue #3). Also rides the svg's . */ note?: string } // strip provenance from a bound-value string: "shown ⟷ path" → "shown" // (empty when the binding holds no value yet); a plain string (no arrow) // is a live-but-unbound value and passes through whole. // The STRUCTURAL arrow is the LAST one in the string — the surface appends // it, so everything before it is data, and data can carry arrow tokens // (forged, or from a producer older than tosijs 1.8.0, which neutralizes // them at the source). A renderer consumes maps it did not generate, so it // parses defensively: split at the last arrow, and neutralize any arrow // left INSIDE the shown value (geometry over glyphs — a rare glyph must // never ride a caption run, and a fake arrow must never read as structure). const shownValue = (v: unknown): string | undefined => { if (typeof v !== 'string') return undefined const at = Math.max(v.lastIndexOf(BOUND_TWO_WAY), v.lastIndexOf(BOUND_TO_DOM)) return neutralizeArrows(at >= 0 ? v.slice(0, at).trim() : v) } // arrow tokens inside record data must neither ride a caption run // (geometry over glyphs) nor read as structure — neutralized the same way // tosijs ≥1.8.0 does at the source const neutralizeArrows = (s: string): string => s.replaceAll(BOUND_TWO_WAY, '<->').replaceAll(BOUND_TO_DOM, '<-') // is this string a live two-way binding? Only the arrow in STRUCTURAL // position (last) counts — a ⟷ buried inside the data must not confer an // affordance (a drawing that lies about what the page can do is worse than // no drawing). const boundTwoWay = (v: unknown): boolean => { if (typeof v !== 'string') return false const at = v.lastIndexOf(BOUND_TWO_WAY) return at >= 0 && at > v.lastIndexOf(BOUND_TO_DOM) } // fields the surface NEVER appends a binding arrow to: identity, naming, // hints, destinations. In these, any arrow is data (or forgery) — the // last-occurrence rule only protects fields that actually receive an // appended binding, so these are excluded from the binding scan entirely // (0.4.0 review B1: a lone forged arrow in a never-bindable field is // always in "last = structural" position). const NEVER_BOUND = new Set([ 'tag', 'id', 'part', 'role', 'label', 'placeholder', 'type', 'description', 'href', 'ref', 'image', ]) const hasTwoWayBinding = (w: SchematicRecord): boolean => Object.entries(w).some( ([key, v]) => !NEVER_BOUND.has(key) && boundTwoWay(v) ) // the two kinds of affordance evidence, split once and shared by the // renderer AND the exported predicate — three independently edited copies // of this logic is how the renderer and tosijs's audit drifted into // contradicting each other (issue #4); parity is now by construction const hasActEvidence = (w: SchematicRecord): boolean => w.on != null || w.interactive === true || (typeof w.href === 'string' && w.href !== '') const hasEditEvidence = (w: SchematicRecord): boolean => w.editable === true || w.contentEditable === true || hasTwoWayBinding(w) // can this producer SEE wiring at all? Any handler, any assertion, any // provenance arrow — including a display-only ⟵ — proves it can (#10: a // read-only dashboard from a binding framework is not a blind map, it's a // sighted map of a page with nothing actionable on it) const hasCapabilityEvidence = (w: SchematicRecord): boolean => w.on != null || w.interactive === true || w.editable === true || // arrows count only in bindable fields — an arrow in a never-bindable // identity/name field is page content (possibly forged), and must not // fabricate capability any more than it fabricates a binding Object.entries(w).some( ([key, v]) => !NEVER_BOUND.has(key) && typeof v === 'string' && (v.includes(BOUND_TWO_WAY) || v.includes(BOUND_TO_DOM)) ) /** * An element's page-coordinate bounds (the same space describe() records) — * the natural `within` argument for a region-scoped schematic. */ export const boundsOf = (element: Element): SchematicBounds => { const rect = element.getBoundingClientRect() return { x: Math.round(rect.x + ((globalThis as any).scrollX ?? 0)), y: Math.round(rect.y + ((globalThis as any).scrollY ?? 0)), width: Math.round(rect.width), height: Math.round(rect.height), } } // structure behind affordances — a LIST CONTAINER is ground too: it's // wired (the collection binds here), but its items are the affordances. // EVIDENCE BEATS CONTAINER ROLE (#7): an element that is both container // and control (a list-bound