Pattern // diagram-static-peer
A reusable static-artifact scaffold for peer relations: three or four equal peers on a regular polygon, joined by declared relations, that inherits the design-system-ASK visual language. It has two presets, which fix placement only: a triangle of three positions and a square of four. Each ships a template: the triangle's joins its three peers in a clockwise directed cycle, and the square's joins four by a clockwise directed perimeter with its opposite pairs neutral.
This is a Class A static diagram scaffold, distinct from the Class B project-output artifact patterns. Its static siblings — patterns/diagram-static-H/, -V/, -SEQ/ and -FLOW/ — arrange hierarchy, succession and convergence, and share the window.DIAGRAMS.render interface and one PNG exporter. This pattern arranges peers. It has its own relation grammar, engine and exporter, and takes none of their chrome, fit, pointer or text-layout carriers: a fixed plate of three or four letters has no canvas to pan and no label to wrap. The surface pattern patterns/diagram-interactive-radial/ arranges a containment hierarchy on a live page; this one arranges equals on a sealed plate.
on this page
What this pattern is
A plate is a 1000-unit square: the foundation's 45-degree gradient field, one letter at each vertex, and a connector for each declared relation. A directed relation is a line with a filled, slightly notched head at its target. A neutral relation is a fitted dash with no head, lighter than a directed one, so two neutral diagonals cross as lines and never as a node. The keyed plate adds a band under the polygon: a drawn sample of each relation type the plate uses, with the instance's own word for it.
The spec owns the content: the labels, which pairs relate, and the words for each relation. The engine owns placement, connectors and presentation, and reads no meaning into a relation beyond its type. A-S-K and L-E-O-V are ASK's instances, read as a game; the templates carry numbered slots and no reading.
Eight files:
README.md// this filediagram-static-peer.html// the specimen: the instances, the neutral-opposites square beside the all-directed square, the templates, a relabeling and the checks, drawn from the relation sourcesdiagram-static-peer.source.js// the relation sources the specimen draws, as onewindow.PEER_DIAGRAMSarraydiagrams-static-peer-engine.js// DS-owned engine (window.DIAGRAM_PEER, or a Node module): validation, the derived checks, and the renderer, a pure function from a spec to a standalone SVG string. The page and the exporter run the same codediagrams-static-peer.css// the specimen's column, figure grid, plate frame and two mode selectors. Its text takes the document registerdiagrams-static-peer-glyphs.js// DS-owned, GENERATED bytools/gen-peer-font.pyfromfonts/InterVariable.woff2: each supported label's measured centering, and each printable Basic Latin character's advance at the key's weight. Never hand-edit itdiagrams-static-peer-font.js// DS-owned, GENERATED by the same tool: Inter subset to printable Basic Latin, with thewghtandopszaxes kept, as a base64@font-facerule, with Inter's copyright and the SIL Open Font License text. Only the exporter loads itexport-peer.mjs// DS-owned exporter (Node 18 or later, no dependency): writes each spec as standalone SVG files and, on request, PNG files
Load order is a contract. The page loads the stylesheets colors_and_type.css → surface-text-link.css → surface-document.css → diagrams-static-peer.css, then the scripts <source> → diagrams-static-peer-glyphs.js → diagrams-static-peer-engine.js, then its own script. The engine reads nothing global: the page passes it the glyph carrier, and render fails closed rather than centering letters by guesswork: without one, GLYPHS_MISSING; with one whose letter offsets or measured weight are not finite numbers, or that lacks a finite, non-negative advance for a printable Basic Latin character, GLYPHS_MALFORMED.
The pattern is not a component library, a generator, a build pipeline, an npm package or a project-specific diagram. It is a starting point for downstream peer diagrams that consume design-system-ASK foundations.
The relation contract
spec = {
id: letters, digits, '.', '_' and '-' 'triangle.A-S-K'
preset: 'triangle' | 'square'
role?: free text for the page; the engine ignores it
title?: the plate's accessible name, with no control character; defaults to id
nodes: [{ id, label }, ...] clockwise from the preset origin:
the triangle's top, the square's upper left
relations: [{ from, to, type }, ...] every pair exactly once
meaning?: { directed?, neutral?, verb?, neutralPlain? }
}
- Nodes. A triangle takes three nodes and a square four. A node id starts with a letter and continues in letters, digits,
_or-. A label is one character,A–Zor0–9: the glyph carrier measures the ink of exactly these, so every letter sits centered on its vertex. Labels are distinct. - Relations. Every pair of nodes is declared exactly once, in either orientation.
directedreads from → to and draws its head atto.neutraldraws the dash.unspecifieddeclares a pair open and draws nothing. An undeclared pair is an error, never a neutral pair: completeness is explicit, andunspecifiedis the one way to leave a pair open. The shipped relation sources — the templates, the instances and the specimen's comparison and relabeling — run every directed cycle clockwise, the order their labels read, and list its relations in that order, which the plate's description and the specimen's captions follow. That is a convention of these sources, not a rule of the engine, which draws whichever direction a spec declares. - Meaning. The instance's own words, all optional and all printable Basic Latin, the range the exported font covers.
directedandneutrallabel the key's samples; each must fit its slot, measured against the key's advance widths.verbjoins X verb Y in the plate's description (defaultto), andneutralPlainphrases X and Y neutralPlain (defaultare neutral). The engine reads no meaning from any of them. - Closed fields. A spec, a node, a relation, the meaning and the options each accept only the fields above. A misspelled field is an error, not an omission.
- Every entry is read. The nodes and relations are read at every index, so a hole in either array is an entry that is not an object, never one that is skipped.
validate(spec, glyphs) never throws: it returns { ok, errors }, each error a { code, path, message }, and a spec whose fields cannot even be read is SPEC_UNREADABLE. render throws a PeerError carrying code and every error. Membership tests are exact: a relation type, a node id or a preset is matched against its own declared set, so a name every object inherits, such as constructor or toString, is an ordinary unknown value and never passes as one that exists.
| Code | Raised when |
|---|---|
CodeSPEC_NOT_OBJECT · SPEC_UNREADABLE · SPEC_ID · SPEC_KEY · SPEC_ROLE · SPEC_TITLE |
Raised whenthe spec, its id, its title or its fields are malformed, or the spec cannot be read |
CodePRESET_UNKNOWN |
Raised whenthe preset is not triangle or square |
CodeNODES_NOT_ARRAY · NODE_COUNT · NODE_NOT_OBJECT · NODE_KEY · NODE_ID · NODE_ID_DUPLICATE |
Raised whenthe nodes are malformed, too many or too few, or repeat an id |
CodeLABEL_UNSUPPORTED · LABEL_DUPLICATE · LABEL_UNMEASURED |
Raised whena label is not one of A–Z, 0–9, repeats, or has no measured centering |
CodeRELATIONS_NOT_ARRAY · RELATION_NOT_OBJECT · RELATION_KEY · RELATION_TYPE · RELATION_ENDPOINT · RELATION_SELF · RELATION_PAIR_DUPLICATE |
Raised whena relation is malformed, has an unknown type or endpoint, relates a node to itself, or repeats a pair |
CodePAIR_UNDECLARED |
Raised whena pair has no relation |
CodeMEANING_NOT_OBJECT · MEANING_KEY · MEANING_TEXT · MEANING_KEY_WIDTH |
Raised whenthe wording is malformed, outside printable Basic Latin, or too wide for its key slot |
CodeOPTIONS_NOT_OBJECT · OPTION_KEY · GLYPHS_MISSING · GLYPHS_MALFORMED · OPTION_THEME · OPTION_KEYED · OPTION_ARROWHEADS · OPTION_ID_PREFIX · OPTION_FONT_FACE · OPTION_METADATA |
Raised whena render option, or the glyph carrier, is malformed or missing; a title, the metadata or a theme value carrying a character XML forbids, or markup or CSS punctuation in a theme value, is among them |
CodeMOUNT_HOST |
Raised whenmount is given no element |
The derived checks read only the declared relations: matrix (1 for row → column, −1 for the reverse, 0 neutral, null unspecified), signatures (each node's outgoing, incoming, neutral and unspecified partners), symmetry (the rotations that carry the pattern onto itself) and describe. They are structural: no score, payoff or ranking belongs to the contract. A reading such as the specimen's game is the instance's.
Rendering
render(spec, options) returns one standalone <svg>:
glyphs// required: theDIAGRAM_PEER_GLYPHScarrier.theme//'inherit'(default) draws with the page's foundation roles — the field from--bg-fromand--bg-to, ink from--fg-1, letters in--font-sansat--fw-extralight, the key at--fw-regular— so a plate on a page follows the page's theme. A resolved theme object —{ name, fieldFrom, fieldTo, ink, fontFamily, labelWeight, keyWeight }— draws a standalone file with those values written in. ItslabelWeightmust be the weight the glyph carrier measured the letters at, or they would sit off center.keyed//false(default) is the art plate;trueadds the key band, 150 units, with one item per drawn type.arrowheads//'target'is the default, the canonical presentation and the standard export.'midpoint'is a comparison mode: each head sits at its connector's midpoint, and where two directed connectors cross there, as on the all-directed square's diagonals, each head slides toward its own target so the crossing never reads as a node.idPrefix// every element id in the plate begins with it, and every rule in the plate's<style>is scoped to it. It defaults toidPrefixFor(spec.id), an encoding under which distinct texts never share a prefix. A plate carries three ids: the prefix itself,<prefix>_titleand<prefix>_field. On a page,mount(host, spec, options)takes a prefix none of whose three ids any element in the document carries, so the same spec can appear any number of times, beside plates rendered with prefixes of their own; a caller that names its own prefix keeps all three unique. The exporter gives each file a prefix of its own, so exported files can be inlined on one page; the@font-facerule they embed declaresInter, which joins a host page's Inter for the characters it covers.fontFaceCSS,metadata// embedded in a standalone file.
Each plate is role="img", named by its <title> and described by a <desc> derived from its relations. Heads are drawn as paths, not SVG markers, which rasterize unreliably. The geometry — the circumradius, the letter size, the clearance where connectors stop, the stroke widths, the fitted dash, the head's dimensions and the key's layout — belongs to the pattern and is not an option.
The specimen
diagram-static-peer.html draws every spec in the source. It follows the system's light or dark setting and has no theme selector. Its two mode selectors are quiet pills that report their state, the style guide's selector grammar:
- view //
art plateorwith key. With the key, the instances also show their relation tables. - arrowheads //
at target(default) orat midpoint, for comparison.
Nothing is stored: the page always opens at the art plate with heads at the target. The page's text takes the document register, and the checks table is derived from the relations when the page loads.
Export
node export-peer.mjs --out <dir> every spec: art plate and keyed, light and dark, SVG node export-peer.mjs --out <dir> --png the same, plus a PNG of each SVG
--tokens <file> names the token file (default _dsa-tokens/colors_and_type.css beside the script), --source <file> the relation sources, --spec <id> one spec (repeatable), --arrowheads midpoint the comparison mode, --chrome <path> the browser and --scale <n> the PNG's pixels per unit (1 to 4). From this repository's root, pass --tokens colors_and_type.css.
- Files.
<spec>_<plate|keyed>-<light|dark>.svg, and the same name in.png. Both themes are always written, each with the-lightor-darksuffix that Theme by embedding surface below names. Write each sealed generation to a directory of its own: the exporter overwrites a file of the same name, and the names carry no source or render version. A file's<metadata>records the token file's and the font subset's sha256, but not the relation source or the engine revision, so it is not a manifest to reproduce the file from: keep each sealed generation's sources and carriers with it. - Tokens. For each theme the exporter resolves
--bg-from,--bg-to,--fg-1,--font-sans,--fw-extralightand--fw-regularfrom the token file, followingvar()references, and writes the values into the file. It reads the light values on:rootand the dark overrides on the rule whose selector list includes:root[data-theme="dark"], which must declare the field and the ink itself, so a dark file never borrows a light value. It fails rather than guess on a name that does not resolve, an unbalanced block or an unclosed comment. Each file records the token file's sha256. - Font. Each file embeds the Inter subset, so its letters and key resolve with no font installed, and carries Inter's copyright and the full SIL Open Font License 1.1 text in
<metadata>. Subsetting is permitted because Inter declares no Reserved Font Name. - Checked first. Every spec the run exports is validated before any file is written, and the run is refused if a spec id is given twice or two differ only in letter case, such as
Triad.ASKandtriad.ask: a file system that ignores case, as macOS's and Windows' do by default, would store their files as one, the later replacing the earlier. Each of these failures writes no files. A failure once writing has begun — no browser found for--png, for example — leaves the files already written in place: a run that finds no browser writes the SVGs and no PNG. - Determinism. In one runtime the SVG is a pure function of the spec, the options, the token file and the carriers: two runs write the same bytes, and nothing in a file records the time.
- PNG.
--pngrasterizes each SVG with headless Chrome or Chromium at the SVG's own size times--scale, and checks the PNG's dimensions. The browser is--chrome, else$CHROME, else the platform's usual install paths (macOS, Windows) orgoogle-chrome,chromiumandchromium-browseronPATH; with none found it stops and names the paths it tried.
What is checked, and what is not. tests/peer-contract.test.mjs checks the SVG export on every run: the files written, their determinism, the embedded subset, the license text and the resolved tokens. When the pattern was admitted, the plates were also rendered in Chrome and the PNG route was run on macOS with Google Chrome; neither is part of the test. Not tested: the PNG route on Windows or Linux, and how other editors — Illustrator, Inkscape, Figma — import a file with live text, an embedded variable-font subset and CSS rules in <style>. Treat import into an editor as unverified.
Theme by embedding surface
Every diagram package generates and retains both theme variants (-light and -dark). The embedding surface selects the default:
- Repository documentation and operator-system diagrams default to dark.
- Substack and other published long-form editorial diagrams default to light.
- A stated local exception may override the default for a specific figure.
This rule selects which existing render is embedded. It does not suppress, rename, or replace the alternate-theme export.
The generated carriers
tools/gen-peer-font.py writes diagrams-static-peer-glyphs.js and diagrams-static-peer-font.js from fonts/InterVariable.woff2; --check regenerates them in memory and fails on any difference. It is owner-only and needs fontTools and brotli, which nothing else in this repository uses; another version of either may subset to other bytes, which --check reports. Each carrier records the source font's sha256, and tests/peer-contract.test.mjs fails when the canonical font changes and the carriers were not regenerated. A consumer vendors the carriers and never runs the tool.
How to use it
- Copy the pattern's files, except this README if you wish, into your consuming project, beside each other.
- Sync
colors_and_type.cssand the fonts from design-system-ASK into a local_dsa-tokens/mirror, andsurface-text-link.cssandsurface-document.cssinto a_dsa-surface/mirror, pinned to a known upstream commit. The page expects both mirrors beside it. - Replace
diagram-static-peer.source.jswith your own relation sources, and the page's title, lede, sections and figures with your own. Rename the page and the source if you wish, and update the<script src>reference. - Open the page in a browser to see the plates, then run
export-peer.mjsto write the standalone files you embed.
What downstream must replace
- The relation sources in
diagram-static-peer.source.js: ids, presets, titles, labels, relations and wording - The page's words: the
<title>, the description, the title and lede, each section, each figure's label, and each figure'sdata-columns, the column names of its relation table - The file names of the page and the source, if you prefer project-specific names
What not to edit
diagrams-static-peer-engine.js,diagrams-static-peer.css,export-peer.mjsand the two generated carriers: re-vendor them byte-identical- The load order, and the page's figure structure (
.peer-figurewith itsdata-spec,.peer-hostand thedata-relationscaption) - The light and dark theme model: the page sets no theme, and
colors_and_type.cssresolvesdata-themeandprefers-color-scheme
Static vs dynamic inheritance
Static diagram artifacts inherit at generation time and freeze for audit. The exporter resolves the Tier 1 + Tier 2 tokens from the local mirror and writes them into each file, with the embedded font subset, so an exported plate depends on nothing outside itself. The page takes the same tokens live from the mirror. There is no runtime fetch from the design-system-ASK repo and no font CDN. Re-sync the mirrors and re-vendor the bundle when upstream changes, and re-export.
Source-truth boundary
design-system-ASK supplies the foundations and the pattern. What the peers are, how they relate and what a relation means are the consuming project's: a plate illustrates its spec, and the spec's own source remains authoritative. Hosting this scaffold in design-system-ASK does not make this repo the owner of downstream content, and A-S-K and L-E-O-V remain ASK's instances, not the pattern's vocabulary.
Class A vs Class B
This is Class A (system / architecture diagram templates) — the peer grammar. Its static siblings patterns/diagram-static-H/, -V/, -SEQ/ and -FLOW/ share the window.DIAGRAMS.render interface; the interactive patterns/diagram-interactive-spine/ is a separate Class A pattern. Class B (project-output artifact templates) is patterns/output-artifact/ and patterns/message-archive/. The classes stay distinct. All inherit Tier 1 + Tier 2 from design-system-ASK, but they serve different artifact classes.