method-ASK_bounded-generativity.figure.js
This view shows one file as text; the diagram front door links it. It is generated at build time, static and bounded: nothing here runs the code, and there is no browsing beyond this file.
/* method-ASK_bounded-generativity.figure.js — horizontal flow + Three Functions color (source-v5, ASK 2026-09-13) source-v5: conforms to Three Functions v0.2. The source-of-intent / normative-apex ROLE is the CONSTITUTIVE legislative source, so it now carries the role treatment; the grammar / brief box stays DELEGATED legislative apparatus. Both take the same role value — their rank is carried by topology (the source role sits outside the bounded-generativity bracket, the grammar / brief inside it), never a second hue, an opacity rank or a fourth function. The figure draws the role, not an occupying actor, so there is no actor envelope here. source-v4 (prior): even 36px spacing, taller frame (+72 split), aperture split labels, tightened grammar/governed boxes, compact chamber, 2-line source-of-intent copy. Consumes design-system-ASK Three Functions by reference (_dsa-tokens/three-functions.css): legislative = the source-of-intent role box (constitutive) + the grammar/brief box (delegated) · executive = the bounded-realization chamber (variance = negative cutouts) · judicial = selection ring + ratified node. Everything else neutral — including the aperture, its dimension arrow and iris geometry (the permission the grant produces, not apparatus) and every connecting beam. */ (function () { /* FAIL CLOSED BEFORE ANY LAYOUT. diagrams-fit.js is a DS-owned support file that must be vendored alongside this figure and loaded immediately BEFORE it. Checked here, at the top of the builder, so a missing helper stops this file before the SVG is constructed — rather than leaving a half-built, unpositioned artifact whose failure has to be inferred. The message names the figure builder, not the shared H engine. */ if (!window.DIAGRAM_FIT || typeof window.DIAGRAM_FIT.compute !== 'function') { throw new Error('Diagram fit support is missing. Load diagrams-fit.js before the figure builder.'); } /* The Fit's v3 placement options (balance, compactClearance) are not read by an older diagrams-fit.js, which would silently keep the old geometry. FAIL CLOSED instead. */ if (!(window.DIAGRAM_FIT.VERSION >= 3)) { throw new Error('diagrams-fit.js is older than v3. Re-vendor it from design-system-ASK with this figure builder.'); } /* FAIL-CLOSED on the design-system gesture carrier, on the same terms: diagrams-pointer.js (v2) is vendored alongside this figure and loaded BEFORE it. */ if (!window.DIAGRAM_POINTER || typeof window.DIAGRAM_POINTER.attach !== 'function' || !(window.DIAGRAM_POINTER.VERSION >= 2)) { throw new Error('Diagram gesture support is missing. Load diagrams-pointer.js (v2) before the figure builder.'); } const M = { apex: { label: 'source-of-intent role', sub: 'normative apex supplies', note1: 'purpose + governing standard' }, grammar:{ label: 'grammar / brief', note: 'grants the aperture' }, field: { tag: 'bounded realization · execution span · «the middle»', apertureLo: 'aperture', apertureHi: 'permitted variance', variance: 'variance', judgment: '· · · authorized judgment where present · · ·' }, select: { tag: 'selection + closure', note1: 'ratifies one candidate', note2: 'aperture closes' }, govern: { label: 'artifact governance', note: 'binds the accepted result' }, output: { label: 'governed artifact', note: '+ governance record' }, bracket:'bounded generativity', }; const svgNS = 'http://www.w3.org/2000/svg'; const W = 1600, H = 620, YM = 320; // YM = main horizontal axis (source → grammar → aperture → chamber → selection) const svg = document.getElementById('svg'); svg.setAttribute('width', W); svg.setAttribute('height', H); svg.setAttribute('viewBox', `0 0 ${W} ${H}`); while (svg.firstChild) svg.removeChild(svg.firstChild); function el(name, attrs = {}, kids = []) { const e = document.createElementNS(svgNS, name); for (const [k, v] of Object.entries(attrs)) if (v != null) e.setAttribute(k, v); for (const c of kids) e.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); return e; } const edges = el('g', { class: 'edges' }), nodes = el('g', { class: 'nodes' }); svg.append(edges, nodes); const AH = 9; const headR = (x, y) => el('path', { class: 'edge-arrowhead', d: `M ${x-AH} ${y-AH/1.6} L ${x-AH} ${y+AH/1.6} L ${x} ${y} Z` }); const headD = (x, y) => el('path', { class: 'edge-arrowhead', d: `M ${x-AH/1.6} ${y-AH} L ${x+AH/1.6} ${y-AH} L ${x} ${y} Z` }); const headU = (x, y) => el('path', { class: 'edge-arrowhead', d: `M ${x-AH/1.6} ${y+AH} L ${x+AH/1.6} ${y+AH} L ${x} ${y} Z` }); const box = (x, y, w, h, cls='node-box') => el('rect', { x, y, width:w, height:h, rx:4, ry:4, class:cls }); const line = (d, cls='edge', dash=null) => el('path', { class:cls, fill:'none', d, 'stroke-dasharray':dash }); const lbl = (x, y, t, cls='node-label', a='start') => el('text', { x, y, class:cls, 'text-anchor':a }, [t]); const note = (x, y, t, a='start') => el('text', { x, y, class:'node-note', 'text-anchor':a }, [t]); const tag = (x, y, t, a='middle') => el('text', { x, y, class:'flow-tag', 'text-anchor':a }, [t]); /* ===== source of intent / normative apex — OUTSIDE the frame, left · LEGISLATIVE (CONSTITUTIVE) ===== The role box sits ON the main axis so the source beam visibly leaves it. It carries the role label only, on the theme foreground; the two descriptive lines sit below it as neutral captions rather than on the role-bearing apparatus. */ nodes.append(box(124, YM-20, 172, 40, 'node-box fn-leg')); nodes.append(lbl(140, YM, M.apex.label)); nodes.append(note(140, YM+36, M.apex.sub)); nodes.append(note(140, YM+54, M.apex.note1)); /* ===== bounded generativity frame · NEUTRAL ===== */ const FX = 340, FY = 112, FW = 916, FH = 402; // x 340-1256, y 112-514; height +72 split equally (+36 top, +36 bottom) for extra vertical breathing room; contents held constant nodes.append(el('rect', { x:FX, y:FY, width:FW, height:FH, rx:14, ry:14, class:'flow-group' })); nodes.append(tag(FX + FW/2, FY - 14, M.bracket)); /* ===== source beam · NEUTRAL, from the source role → grammar/brief = LEGISLATIVE ===== Carriage takes no function color: the beam between two legislative elements stays neutral. */ edges.append(line(`M 296 ${YM} L 372 ${YM}`)); edges.append(headR(372, YM)); nodes.append(box(376, YM-29, 150, 58, 'node-box fn-leg')); // two-line node, 58px, centred on YM; 36px inset from the frame-left edge; width 150 (tight to the label) nodes.append(lbl(392, YM-6, M.grammar.label)); nodes.append(note(392, YM+12, M.grammar.note)); /* ===== grammar/brief grants the aperture → beam · NEUTRAL → chamber = EXECUTIVE apparatus ===== */ const CX = 576, CY = 184, CW = 428, CH = 250; // chamber; top aligned to artifact-governance (y=184). Right edge 1004 = 36px left of the governance box; the aperture ↕ arrow (CX-14 = 562) sits 36px right of the grammar box. edges.append(line(`M 526 ${YM} L 570 ${YM}`)); edges.append(headR(574, YM)); /* variance glyph geometry — used twice: as black holes in the executive mask, and as neutral outlines on top */ const glyphs = [ // 7 marks — plurality/heterogeneity/non-finality without a tidy matrix (top-left cube + one bottom circle removed per ASK) { t:'rect', x:639, y:226, w:30, h:30, rx:2, rot:'rotate(45 654 241)' }, // diamond, top { t:'circle', cx:746, cy:244, r:15 }, // circle, top { t:'path', d:'M 838 230 L 856 260 L 820 260 Z' }, // triangle, top { t:'rect', x:920, y:233, w:28, h:28, rx:4 }, // square, top { t:'rect', x:671, y:322, w:30, h:30, rx:4 }, // cube, bottom { t:'rect', x:783, y:326, w:26, h:26, rx:2, rot:'rotate(45 796 339)' }, // diamond, bottom { t:'circle', cx:906, cy:337, r:13 }, // circle, bottom ]; const glyphEl = (g, attrs) => g.t === 'rect' ? el('rect', Object.assign({ x:g.x, y:g.y, width:g.w, height:g.h, rx:g.rx, ry:g.rx, transform:g.rot }, attrs)) : g.t === 'circle' ? el('circle', Object.assign({ cx:g.cx, cy:g.cy, r:g.r }, attrs)) : el('path', Object.assign({ d:g.d }, attrs)); /* executive mask: chamber white (wash shows), glyphs black (punched out as holes) */ const defs = el('defs'); const mask = el('mask', { id:'exe-holes', maskUnits:'userSpaceOnUse', x:CX-4, y:CY-4, width:CW+8, height:CH+8 }); mask.append(el('rect', { x:CX, y:CY, width:CW, height:CH, rx:8, ry:8, fill:'#fff' })); glyphs.forEach(g => mask.append(glyphEl(g, { fill:'#000' }))); defs.append(mask); svg.insertBefore(defs, edges); /* chamber = the EXECUTIVE apparatus: executive fill (30%, from the consuming CSS) with the glyphs punched OUT as holes, + executive stroke */ nodes.append(el('rect', { x:CX, y:CY, width:CW, height:CH, rx:8, ry:8, class:'fn-exe-wash', mask:'url(#exe-holes)' })); nodes.append(el('rect', { x:CX, y:CY, width:CW, height:CH, rx:8, ry:8, class:'flow-group fn-exe-chamber' })); nodes.append(el('text', { x: CX + CW/2, y: CY + CH + 20, class:'section-tag', 'text-anchor':'middle' }, [M.field.tag])); /* ---- aperture: opening in the chamber's LEFT edge · NEUTRAL geometry (a quantity, not an actor) ---- */ const AP = 74; // aperture half-height (opens AP above and below the entry) edges.append(line(`M ${CX-14} ${YM-AP} L ${CX-14} ${YM+AP}`)); // ↕ permitted-variance dimension edges.append(headU(CX-14, YM-AP)); edges.append(headD(CX-14, YM+AP)); edges.append(line(`M ${CX} ${YM-AP} L ${CX+18} ${YM-10}`)); // iris blade (upper) edges.append(line(`M ${CX} ${YM+AP} L ${CX+18} ${YM+10}`)); // iris blade (lower) // aperture / permitted variance — two labels flanking the arrow's centre (YM); the ↕ passes through the empty gap between them (no "·"), words held in place const apLoY = YM + 38, apHiY = YM - 74; nodes.append(el('text', { x: CX-28, y: apLoY, class:'flow-tag', 'text-anchor':'middle', transform:`rotate(-90 ${CX-28} ${apLoY})` }, [M.field.apertureLo])); // "aperture" (lower) nodes.append(el('text', { x: CX-28, y: apHiY, class:'flow-tag', 'text-anchor':'middle', transform:`rotate(-90 ${CX-28} ${apHiY})` }, [M.field.apertureHi])); // "permitted variance" (upper) /* ---- variance candidates — NEGATIVE cutouts through the executive wash, neutral outlines (executive produces variance; the glyphs are content, NOT executive-function objects) ---- */ glyphs.forEach(g => nodes.append(glyphEl(g, { class:'node-box fn-cut' }))); nodes.append(tag(896, 208, M.field.variance, 'start')); edges.append(line(`M 606 396 L 974 396`, 'edge', '1 5')); // dotted authorized-judgment path · NEUTRAL nodes.append(note(CX + CW/2, 390, M.field.judgment, 'middle')); /* ===== selection + closure = JUDICIAL — ring + ratified candidate ===== */ const SC = { x: 1130, y: YM }; // pulled left with the chamber compression (tightens chamber → selection); centred under the governance box edges.append(line(`M ${CX+CW} ${YM} L ${SC.x-46} ${YM}`)); edges.append(headR(SC.x-46, YM)); edges.append(el('circle', { class:'edge fn-jud-ring', cx:SC.x, cy:SC.y, r:42, fill:'none' })); nodes.append(box(SC.x-26, SC.y-26, 52, 52, 'node-box fn-jud')); nodes.append(tag(SC.x, 394, M.select.tag)); nodes.append(note(SC.x, 412, M.select.note1, 'middle')); nodes.append(note(SC.x, 429, M.select.note2, 'middle')); /* ===== artifact governance — DOWNSTREAM, NEUTRAL (not a fourth color); two-line 58px node, top aligned to the chamber top (y=190) ===== */ edges.append(line(`M ${SC.x} ${SC.y-42} L ${SC.x} 246`)); edges.append(headU(SC.x, 242)); nodes.append(box(1040, 184, 180, 58)); // width 180 (centre 1130 = selection axis) so the two-line subtext fits; up 6 for the 36px selection→governance gap nodes.append(lbl(1056, 207, M.govern.label)); nodes.append(note(1056, 225, M.govern.note)); /* ===== governed artifact + record — OUTSIDE the frame · NEUTRAL; two-line 58px node, top-aligned to the artifact-governance box (y=190) ===== */ edges.append(line(`M 1220 213 L 1284 213`)); edges.append(headR(1284, 213)); // arrowhead to the governed box; the frame edge (1256) sits symmetric between governance (36px inside) and governed (36px outside) nodes.append(box(1292, 184, 150, 58)); // width 150 (tight to the label, was 178); up 6 with governance nodes.append(lbl(1308, 207, M.output.label)); nodes.append(note(1308, 225, M.output.note)); /* ===== tighten the viewBox to the drawn content (+ uniform margin) so the figure FILLS the export frame instead of the fixed 1600×620 canvas with dead padding — the content is width-limited, so a tighter box scales it up (bigger diagram, page margins held constant) ===== */ const VB_PAD = 28; const bb = svg.getBBox(); const vbX = Math.floor(bb.x - VB_PAD), vbY = Math.floor(bb.y - VB_PAD); const vbW = Math.ceil(bb.width + 2*VB_PAD), vbH = Math.ceil(bb.height + 2*VB_PAD); svg.setAttribute('viewBox', `${vbX} ${vbY} ${vbW} ${vbH}`); svg.setAttribute('width', vbW); svg.setAttribute('height', vbH); /* ---- pan / zoom / fit + drag + wheel (the camera is local; FIT and gestures are DS-owned) ---- */ const wrap = document.getElementById('canvasWrap'), stage = document.getElementById('stage'), pct = document.getElementById('zoomPct'); if (wrap && stage) { /* Fit comes from the design-system helper (diagrams-fit.js; presence is checked at the top of this file). This figure previously carried its own copy of the DS fit arithmetic — "local; mirrors the DS engines" — which is exactly why the panel-band defect had to be fixed per-figure. The duplicated arithmetic is retired; the figure's own geometry inputs are kept and passed as caller-owned values. */ let tx=0, ty=0, sc=1; /* Interaction floor. Ordinary zoom-out floor is this figure's historical 0.2 base, but the DS panel-aware fit can land BELOW it on a constrained viewport; a fixed floor above the fitted scale would make zoom-out ENLARGE the figure (direction reversal). So the live floor is the lower of the base and the most recent Fit. Every fit applied sets it from its result — the reader's Fit, and the automatic refits of a view at Fit. */ const BASE_MIN_SCALE = 0.2; let fittedMinScale = BASE_MIN_SCALE; /* FIT MODE (design-system-ASK responsive chrome). The view is at Fit after fit() and until the reader zooms, drags or wheels. Only a view at Fit follows a resize, the webfonts landing, or `diagram-chrome-change` from diagrams-chrome.js (a caption or legend opening or closing, the compact layout switching, its control band changing). A reader's own pan and zoom is not reset by a browser-toolbar resize or a disclosure. */ let atFit = true; const apply = () => { stage.style.transform = `translate(${tx}px,${ty}px) scale(${sc})`; if (pct) pct.textContent = Math.round(sc*100)+'%'; }; /* CALLER-OWNED, not DS defaults: this figure uses a 90px total clearance (DS default 80) and a 1.3 max scale (DS default 1.2). Bounds start at ZERO even though the tightened viewBox origin above is negative — the transform target is the stage div, whose box starts at 0 in CSS space, so the viewBox origin never enters it. Transform target decides this, not viewBox sign. */ const fitResult = () => window.DIAGRAM_FIT.compute({ wrap, bounds: { minX: 0, minY: 0, maxX: vbW, maxY: vbH }, clearanceX: 90, clearanceY: 90, maxScale: 1.3, gutter: 26, /* v3 (diagrams-fit.js): centre vertically between the chrome above and below the figure, and cap the clearance while the responsive chrome is compact. */ balance: true, compactClearance: 32 }); const applyFit = (f) => { fittedMinScale = Math.min(BASE_MIN_SCALE, f.scale); sc = f.scale; tx = f.tx; ty = f.ty; atFit = true; apply(); }; /* The page's responsive chrome (diagrams-chrome.js): its open compact panel, the Fit the reader returns to when that panel closes, and its one truthful way to close it. An open panel moves the drawing only where the drawing keeps its closed-panel size; otherwise the drawing keeps that closed-panel Fit and the panel overlays it until it closes. */ const chrome = window.DIAGRAM_CHROME || null; /* FAIL-CLOSED with the chrome: its edge declaration needs diagrams-fit.js v2. */ if (chrome && !(window.DIAGRAM_FIT.VERSION >= 2)) throw new Error('The responsive chrome needs the current diagrams-fit.js. Re-vendor it with diagrams-chrome.js.'); const panelOpen = () => !!(chrome && chrome.openPanel(wrap)); const closedFit = () => (chrome ? chrome.withoutOpenPanel(wrap, fitResult) : fitResult()); const keepsSize = (f, c) => f.clear && f.scale >= c.scale * (1 - 1e-6); const fitAround = () => { const f = fitResult(); if (!panelOpen()) return f; const c = closedFit(); return keepsSize(f, c) ? f : c; }; /* FIT, the reader's request, always ends at a usable fitted view: when the open compact panel would cover the drawing, Fit closes it through the chrome's state controller and fits against the chrome that remains. */ const fit = () => { if (panelOpen()) { const f = fitResult(); if (keepsSize(f, closedFit())) { applyFit(f); return; } chrome.close(wrap); } applyFit(fitResult()); }; /* A resize, the webfonts landing or a chrome change refits only a view at Fit, by the same open-panel rule. */ const refitAtFit = () => { if (atFit) applyFit(fitAround()); }; /* REFIT ONCE THE WEBFONTS LAND. The vendored faces use `font-display: swap`, so the page can lay out with fallback metrics and re-lay out when Inter / JetBrains Mono arrive. That now matters: the DS helper measures the LIVE caption, legend, and HUD rectangles, and those change size across the swap — so a fit computed against fallback-font chrome can be stale by the time the real fonts render. The H engine avoids this class of bug by gating its first render on the fonts it measures. Fit immediately so the figure is never left unpositioned, then refit once if the view is still at Fit. This is deliberately narrower than rebuilding the figure: the source geometry and the tightened viewBox above are computed from SVG text and stay as they are; only the panel-aware stage placement is refreshed. */ fit(); const fonts = document.fonts; if (fonts && fonts.ready && typeof fonts.ready.then === 'function') { fonts.ready.then(refitAtFit).catch(() => {}); } window.addEventListener('resize', refitAtFit); wrap.addEventListener('diagram-chrome-change', refitAtFit); const zi=document.getElementById('zoomIn'), zo=document.getElementById('zoomOut'), zf=document.getElementById('zoomFit'); /* The view leaves Fit only when the reader moves it: a zoom at its limit changes nothing, and a press that stays inside the pointer controller's tap slop is a tap. */ const zoomTo = (s) => { if (s === sc) return; sc = s; atFit = false; apply(); }; if (zi) zi.onclick=()=>zoomTo(Math.min(sc*1.2,4)); if (zo) zo.onclick=()=>zoomTo(Math.max(sc/1.2,fittedMinScale)); if (zf) zf.onclick=fit; /* GESTURES through the design-system pointer controller (diagrams-pointer.js, checked at the top of this file): one pointer pans from anywhere on the drawing, two pinch about their centroid, a wheel zooms about the pointer. The canvas carries `touch-action: none` (diagrams.css), so a touch gesture there moves the drawing, never the page. A press on the HUD or the chrome block stays native, and a wheel over the chrome block scrolls an open panel unless it is a trackpad pinch (ctrlKey). A moved gesture swallows its own click. */ const clampK = (k) => Math.max(fittedMinScale, Math.min(4, k)); window.DIAGRAM_POINTER.attach({ stage: wrap, exclude: '.hud, .legend, .caption, .diagram-info', wheelStep: 1.1, clampK, getView: () => ({ k: sc, x: tx, y: ty }), setView: (v) => { /* A gesture that moves the view takes it off Fit; float noise from a still pinch does not. */ if (Math.abs(v.k - sc) > 1e-9 * sc || Math.abs(v.x - tx) > 1e-6 || Math.abs(v.y - ty) > 1e-6) atFit = false; sc = v.k; tx = v.x; ty = v.y; apply(); }, zoomAt: (f, mx, my) => { const ns = clampK(sc*f); if (ns === sc) return; const k = ns/sc; tx = mx-(mx-tx)*k; ty = my-(my-ty)*k; sc = ns; atFit = false; apply(); } }); } })();