diff --git a/.github/workflows/render.yml b/.github/workflows/render.yml
new file mode 100644
index 0000000..e019c9d
--- /dev/null
+++ b/.github/workflows/render.yml
@@ -0,0 +1,52 @@
+name: Deterministic Engineering Render
+on:
+ pull_request:
+ paths:
+ - 'packages/render/**'
+ - 'examples/render/**'
+ - 'docs/90-99-views/90-architecture-views/**'
+ - '.github/workflows/render.yml'
+ push:
+ branches: [main]
+ paths:
+ - 'packages/render/**'
+ - 'examples/render/**'
+ - 'docs/90-99-views/90-architecture-views/**'
+ - '.github/workflows/render.yml'
+permissions:
+ contents: read
+jobs:
+ renderer:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
+ - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
+ with:
+ bun-version: 1.3.11
+ - run: bun install --frozen-lockfile
+ working-directory: packages/render
+ - run: bunx playwright install --with-deps chromium
+ working-directory: packages/render
+ - run: bun run typecheck
+ working-directory: packages/render
+ - run: bun test
+ working-directory: packages/render
+ - run: bun run test:export
+ working-directory: packages/render
+ - run: bun install --frozen-lockfile
+ working-directory: examples/render
+ - run: bun run typecheck
+ working-directory: examples/render
+ - run: bun test
+ working-directory: examples/render
+ - run: bun run case
+ working-directory: examples/render
+ - run: bun run compare
+ working-directory: examples/render
+ - run: bun run native
+ working-directory: examples/render
diff --git a/.gitignore b/.gitignore
index f390869..0d94c65 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,7 @@
# Python transport caches are local artifacts.
__pycache__/
*.pyc
+
+# TypeScript packages and local rendering outputs.
+node_modules/
+.cache/
diff --git a/README.org b/README.org
index a39c3ce..cc49c4b 100644
--- a/README.org
+++ b/README.org
@@ -513,3 +513,8 @@ example evidence and decisions are hypothetical. The current
See the [[file:docs/10-19-design/10-architecture/10.03-upstream-capabilities.org::#inspection-scope][inspection scope]] for pinned sources and limitations. Their capabilities do
not automatically qualify the system proposed here.
+
+* Deterministic Engineering Render
+[[file:packages/render/README.org][Functional Deterministic Engineering rendering Library]] composes official D2 compilation and SVG rendering with
+independent upstream/default or downstream themes and an optional official
+OpenSysML SDK projection. [[file:docs/10-19-design/10-architecture/10.140-functional-architecture-rendering-and-opensysml.org][Design and ownership]]; [[file:examples/render/library-case.ts][dependency-only consumer example]].
diff --git a/docs/00-09-project/00-documentation/00.01-index.org b/docs/00-09-project/00-documentation/00.01-index.org
index cf6379e..ee17c43 100644
--- a/docs/00-09-project/00-documentation/00.01-index.org
+++ b/docs/00-09-project/00-documentation/00.01-index.org
@@ -344,3 +344,10 @@ source projection, behavioral checks and branch custody.
- [[file:../../10-19-design/10-architecture/10.138-native-package-cohort-and-ci-provenance.org][10.138 Native package cohort and CI provenance]]
- [[file:../../10-19-design/10-architecture/10.139-native-cohort-source-acquisition.org][10.139 Native cohort and source acquisition]]
+
+* Shared functional architecture rendering
+- [[file:../../10-19-design/10-architecture/10.140-functional-architecture-rendering-and-opensysml.org][10.140 Functional renderer, themes and OpenSysML SDK]]
+
+- [[file:../../10-19-design/10-architecture/10.141-cross-language-diagram-renderer-research.org][10.141 Cross-language renderer research and layout correction]].
+
+- [[file:../../10-19-design/10-architecture/10.142-declarative-render-extensibility-audit.org][10.142 Declarative static render extensibility audit]] — ownership, library reuse and cross-platform qualification.
diff --git a/docs/10-19-design/10-architecture/10.140-functional-architecture-rendering-and-opensysml.org b/docs/10-19-design/10-architecture/10.140-functional-architecture-rendering-and-opensysml.org
new file mode 100644
index 0000000..11658eb
--- /dev/null
+++ b/docs/10-19-design/10-architecture/10.140-functional-architecture-rendering-and-opensysml.org
@@ -0,0 +1,206 @@
+#+TITLE: 10.140 Question-driven D2 Rendering and OpenSysML Model Views
+#+LANGUAGE: en
+#+PROPERTY: JD_ID 10.140
+#+OPTIONS: toc:2 num:nil
+
+* Ownership and decision
+:PROPERTIES:
+:CUSTOM_ID: architecture-rendering-owner
+:END:
+The shared functional TypeScript rendering Library belongs to Deterministic
+Engineering. Gateway/Weaver consumes it and owns project acquisition, local
+presentation, review and publication. This work starts from main253a926 in a new
+branch; the original dirty workspace and active downstream checkout are preserved.
+The donor was Weaver's uncommitted dev/d2 working snapshot in a checkout at
+92f8848640ff53a360430178ba7adc0894793d83, not content published by that commit.
+
+D2 remains the language and rendering engine. Custom React scene reconstruction,
+manual ELK packing and text-outline machinery have been removed. Official D2
+compile/render owns layout and SVG. C4 and Structurizr are
+classification and communication references, not additional DSLs or dependencies.
+The Gerbil package, compiler and SDK are unchanged. The sole package version
+source is packages/render/package.json; namespaces stay stable.
+
+* Questions, model and evidence associations
+:PROPERTIES:
+:CUSTOM_ID: architecture-rendering-contracts
+:END:
+Every diagram answers an explicit question for an audience and scope. A useful
+association is Question -> model/obligation selection -> admitted query/evidence
+references -> view candidate -> review/publication decision. LLMs propose
+questions, model/view candidates and explanations. OpenSysML owns model operations;
+ASCENT owns deterministic queries; Core/Scenario own capture/currentness; Aitia
+owns review. The renderer cannot issue evidence or authorize publication.
+
+| Required view field | Meaning | Boundary |
+|---------------------+---------+----------|
+| question.ref / text | Existing question identity and wording | Included in output and fingerprint |
+| basis.captureRef | Existing capture or explicitly labeled design candidate | Does not create or authenticate C |
+| basis.modelRefs | Selected model/source references | Does not establish model validation |
+| basis.obligationRefs | Existing obligations | No second duty ledger |
+| basis.evidenceRefs | Existing evidence references; may be empty | Empty references cannot become a pass |
+| source/title/purpose/kind | Explicit view selection | Architecture/workflow kept separate |
+| sources/theme | Captured import texts and official D2 options | Full supplied input footprint |
+
+These TS reference fields project existing contracts. FormationProfile/Scenario
+in scenarios/project-formation already own intent, model root, catalog/mapping,
+policy and repository capture. Their capture may be blocked. This package does
+not invent a native Question Contract or pretend that a rendering snapshot issues
+C. Reference resolution, accepted Q, source trust and freshness remain caller
+responsibilities. Missing obligations, conflicts and counterevidence must survive
+selection; selecting a flattering image cannot discharge an obligation.
+
+Architecture answers ownership, components, interfaces, dependencies or deployment.
+Workflow answers ordering, branches, events and recovery. Explanatory overviews
+can refer to both as distinct titled sections. Dependency edges must not silently
+become execution order. Architecture kinds are system/data/assurance; workflow
+kinds are routing/flow/sequence/state. Kinds classify intended views; native D2
+notation supplies their actual semantics. Sequence diagrams use sequence_diagram,
+including its native actor lifelines. No TypeScript card layout replaces it.
+
+* Functional files and call boundaries
+:PROPERTIES:
+:CUSTOM_ID: architecture-rendering-functions
+:END:
+| File under packages/render | Responsibility |
+|-------------------------------------------+----------------|
+| src/source/types.ts | Readonly QuestionRef, ViewBasis and distinct purpose variants |
+| src/source/catalog.ts | Validate paths/selection and freeze supplied snapshot |
+| src/pipeline.ts | renderView composes explicit compile/SVG effects; retains native IDs |
+| src/worker.ts | Official D2 compile/render effects |
+| src/index.ts | Public API; at most two workers, always terminated; failure rejects result |
+| src/theme.ts and themes/ | Independent native options and optional D2 profiles |
+| src/opensysml.ts | Real SDK call and pure explicit reflow projection |
+| examples/render | Actual file-package consumer; project reads and candidate writes |
+
+The Library has no directory walker, global scene engine, semantic label guessing,
+custom compiler, FP runtime, evidence registry or publication database. Snapshots
+are immutable; ordinary functions, readonly records, unions and Promise suffice.
+Bun is the qualified runtime. No Node-only compatibility is claimed.
+
+* Independent themes
+:PROPERTIES:
+:CUSTOM_ID: architecture-rendering-themes
+:END:
+D2 profiles are optional defaults exposed by standardThemes(). Downstream can
+supply its own import closure, classes, styles, icons and layout. The ./theme
+export exposes immutable official D2 options; extendTheme composes overrides.
+Classes remain open strings rather than a closed semantic-role palette. Theme
+changes invalidate render reuse, not model identity or proof meaning.
+SVG is primary. The default PNG conversion was removed after real cases exposed font substitution
+and text overflow. Raster export requires separately qualified browser rendering. External fonts/images require separately qualified
+resource closure for hermetic publication. A digest is not authentication.
+
+* Actual OpenSysML SDK relationship
+:PROPERTIES:
+:CUSTOM_ID: architecture-rendering-opensysml
+:END:
+The optional peer/dev dependency is the published @openmbee/opensysml0.11.0.
+Research was pinned at d7e94f69bbb4324e4829afad1a40f67ad5b006d4; only fields
+present in the installed package are used.
+[[https://github.com/Open-MBEE/OpenSysML/tree/d7e94f69bbb4324e4829afad1a40f67ad5b006d4/client/node][Official TypeScript SDK]].
+
+captureOpenSysMLView invokes Connection.renderView(modelHash, viewName, options).
+projectOpenSysMLView explicitly reflows supported nodes/edges to D2 and retains
+model/view identity, original IDs, ownership, origins, full SDK response and its
+fingerprint. Ports, compartments, tables/notes, stand-ins, collapsed nodes,
+dangling edges and ownership cycles are refused rather than silently dropped.
+SDK errors propagate. There is no local SysML parser or alternative transport.
+Tests use the actual public SDK JSON decoder and real D2 rendering. They do not
+establish live service/WASM parse or model validation. A supplied modelHash is
+only a reference until existing project source checks authenticate its origin.
+
+* Current repository as the real Case
+:PROPERTIES:
+:CUSTOM_ID: architecture-rendering-cases
+:END:
+The cases command renders three original D2 files unchanged, with native ELK,
+theme0 and pad24 matching their documented convention. Receipts retain original
+source hashes, native element IDs and question/model/obligation links.
+
+| Original view | Review question | Content status |
+|---------------+-----------------+----------------|
+| 90.91 engineering-content-contracts | Who publishes, selects and captures architectural/document/decision content? | Existing design; rendering only |
+| 90.120 ascent-domain-owned-module-topology | Which domain owns each module and what remains transitional? | Existing target; not a fresh implementation audit |
+| 90.84 sysml-interoperability | How do observations reach reasoning and review without granting approval? | Existing design; no live model execution |
+
+Existing diagrams may mix architectural and workflow concerns. First-pass
+rendering preserves their source so a content problem is distinguishable from a
+renderer regression. A question-led redesign is a separate reviewed content change.
+The new 90.129 component architecture and 90.130 authoring workflow are separate
+outputs. Their capture references explicitly name design candidates and evidence
+references remain empty; they do not impersonate C, accepted Q or review R.
+
+* Verification, migration and open gates
+:PROPERTIES:
+:CUSTOM_ID: architecture-rendering-migration
+:END:
+Typecheck and tests cover immutable footprints, changed-question rejection,
+source/import refusal, nested D2, sequence notation, independent themes, SDK
+projection/refusal and worker completion. A real installed file dependency and
+an independent packed consumer exercise package boundaries. CI runs package tests
+and examples on Linux and macOS. Local and remote receipts must remain distinct.
+
+Correctness means native D2 semantics, preserved references and refusal of lossy
+SDK projection. Whether question-led LLM selection improves explanatory quality
+is a research hypothesis requiring human comparison, not a rendering pass claim.
+Open gates include live model validation; trusted C/accepted Q integration;
+question-to-element evidence coverage; authorized publication; external asset
+closure; downstream Gateway migration; and CI acceptance for this revision.
+No full automatic LLM orchestration, independent review or delivery is implemented.
+
+* References
+- [[https://d2lang.com/tour/themes][Native D2 themes]] and [[https://d2lang.com/tour/sequence-diagrams][sequence diagrams]].
+- [[https://c4model.com/diagrams][C4 scopes]] and [[https://c4model.com/diagrams/notation][notation guidance]], as references only.
+- [[https://docs.structurizr.com/dsl][Structurizr model/view/style separation]], as a reference only.
+
+* TS visual composition is a first-class responsibility
+The generic D2 adapter alone does not satisfy the visual framework requirement.
+src/presentation.ts implements composePresentation: pure SVG composition of the
+question, answer, native diagram, explanations, evidence context and limits.
+The separate presentation-theme export owns typography, surfaces, status colors
+and spacing. This is independent of themes/ and native D2 compile/render options.
+
+Three real cases produce two themes each: upstream blue/purple and downstream
+green/warm. Native D2 source and diagram bytes remain identical. These demonstrate
+TS-owned visual hierarchy rather than only returning engine SVG. Architecture
+and workflow remain separate page purposes. Explanation status is project-authored;
+a referenced supported label is not independently authenticated assurance. Current
+examples use design or pending. Question-to-element highlighting, further page
+compositions, multilingual shaping and verified evidence decoration remain open.
+The first compositor is a concrete contract, not every possible explanatory genre.
+
+* Local qualification receipt
+The final native D2 and TS composition suite passes19 cases, with strict typecheck
+and actionlint clean. A packed independent consumer passes using only the public
+package and presentation-theme exports. Three unchanged historical repository
+sources render successfully; three two-theme explanatory pages and one focused
+two-theme responsibility page are generated. Browser inspection confirms native
+embedded fonts and complete responsive page composition. The saved screenshot
+is a browser preview, not a separate runtime PNG export capability.
+
+Focused view90.131 is explicitly a concise content candidate derived from90.91
+and10.31. Its question is who publishes/selects/captures; its explanation associates
+Library, Profile and Scenario responsibilities. It does not infer real evidence
+or claim the live OpenSysML/ASCENT/Aitia chain has run. Remote CI is a distinct gate.
+
+* Icon-led infographic refinement
+The initial page compositor was too document-like. The current composition uses
+short labels, explicit node icons, visual grouping, icon-led responsibility tiles
+and a compact model/obligation/evidence strip. Full references remain in SVG
+metadata and sidecar receipts rather than occupying the main visual hierarchy.
+
+The ./icons export supplies official Lucide static SVG assets and an IconAsset
+contract. Project input explicitly binds icons to nodes and explanations; no label
+keyword inference assigns semantics. Downstream may replace any asset or the
+presentation icon set. Icons are embedded as data URIs, not fetched remotely.
+D2 still owns node geometry and connectors; TS owns the surrounding visual
+explanation, typography, icon selection and independent presentation theme.
+This compact composition replaces the initial prose-heavy report arrangement.
+
+The icon implementation uses [[https://github.com/lucide-icons/lucide/blob/main/docs/guide/installation.md][official Lucide static SVG exports]] and D2’s [[https://d2lang.com/tour/icons/][native icon support]]. SVG defaults are embedded with explicit bindings; no remote icon fetch is needed.
+
+* Layout research supersedes prototype acceptance
+[[file:10.141-cross-language-diagram-renderer-research.org][10.141]] records the cross-language architecture/renderer survey. The current
+compositor is a retained prototype, not the accepted visual architecture. D2 is a
+primary authoring language; its ecosystem does not bound rendering research.
diff --git a/docs/10-19-design/10-architecture/10.141-cross-language-diagram-renderer-research.org b/docs/10-19-design/10-architecture/10.141-cross-language-diagram-renderer-research.org
new file mode 100644
index 0000000..6af2fc1
--- /dev/null
+++ b/docs/10-19-design/10-architecture/10.141-cross-language-diagram-renderer-research.org
@@ -0,0 +1,360 @@
+#+TITLE: 10.141 Cross-language Diagram Rendering Research and Layout Correction
+#+LANGUAGE: en
+#+PROPERTY: JD_ID 10.141
+#+OPTIONS: toc:2 num:nil
+
+* Scope correction
+D2 is an authoring language and a primary project input. It does not delimit the
+research ecosystem or prescribe every downstream visual rendering decision.
+Architecture diagramming, graph visualization, constraint-based drawing, routing,
+icon systems and AI-assisted diagram design must be studied across languages.
+C4/Structurizr remain viewpoint references. No additional modeling DSL or proof
+ledger is selected by this research.
+
+The c8b95c0 implementation in10.140 is a functional prototype, not an accepted
+visual baseline. Its19 tests establish API/input properties, not layout quality.
+Its TS compositor primarily surrounds finished D2 geometry. Adding icons or
+shrinking margins cannot give that compositor control of graph-internal layout.
+The current samples expose poor spatial balance, small labels after fitting,
+overlarge fixed panels and repeated responsibility explanations.
+
+* Sources inspected through GitHub
+The inspection used general architecture-diagram and diagram-renderer/layout
+queries, not a D2-only query. Stars aided discovery only; they are not selection
+criteria. Third-party skill text was read as source material, not installed or
+executed as agent instructions. Project claims are not treated as local receipts.
+
+| Project | Actual inspected surface | Capability to learn | Selection status |
+|---------+--------------------------+--------------------+------------------|
+| [[https://github.com/antvis/G6][AntV G6]] | README; packages/g6/src/runtime/layout.ts | Separate graph data, layout stages, nodes/edges/combos, rendering and interactions | Candidate; no local runtime qualification |
+| [[https://github.com/penrose/penrose][Penrose]] | README Domain/Substance/Style example; official constraint documentation | Separate domain meaning, instance and visual constraints; express containment/overlap/objectives | Research reference; no solver dependency selected |
+| [[https://github.com/mingrammer/diagrams][Diagrams]] | README examples and provider structure | Icon-first node vocabulary, clusters and concise architectural labels | Visual reference; Python/Graphviz not automatically introduced |
+| [[https://github.com/awslabs/diagram-as-code/tree/4ec965cf8bdb309d714bc6202e7954fc43338bbe][AWS Diagram-as-code]] | doc/best-practices.md; introduction image | Orientations by diagram purpose, nested resource boundaries and grouped redundancy | Visual/layout reference |
+| [[https://github.com/likec4/likec4/tree/d1d61155215ceebd909cf217eab32ebeb6a35f1e][LikeC4]] | examples/cloud-system/views.c4; demo-rank-for-better-layout.c4 | Explicit include/exclude, scoped views and rank hints | Projection reference; its DSL is not a new dependency |
+| [[https://github.com/lukilabs/beautiful-mermaid/tree/2ac8bbbb060ca0a65a6a21f3200bd99b1587b488][Beautiful Mermaid]] | README; src/theme.ts | Small composable color contract; theme independently applied to SVG primitives | TS theme reference; Mermaid is not a replacement input requirement |
+| [[https://github.com/xyflow/xyflow][XYFlow]] | README package boundaries and usage | Custom nodes/edges and interactive inspection | Interactive candidate; editor does not alone solve offline layout |
+| [[https://github.com/Agents365-ai/drawio-skill/tree/57270820d3cd71cbdf4fb6b2f7091d4fb37bb019][drawio-skill]] | README; scripts/diagram_ir.py; architecture-studio example inventory | Separate meaning/provenance/geometry, multiple projections and visual self-check | Research reference; no copied source/evidence system |
+| [[https://github.com/Cocoon-AI/architecture-diagram-generator/tree/4b9087d55268c79a935105439dbcd37b630fc3f3][Architecture Diagram Generator]] | examples/web-app.html and web-app.png; template inventory | Study actual visual hierarchy and compact layer composition beyond one DSL ecosystem | Visual reference; fixed HTML template is not itself a reusable renderer |
+
+Penrose's [[https://penrose.ink/docs/tutorial/constraints/][constraint documentation]] describes optimization objectives; that is not a
+proof that its solver suits architecture edge routing. G6's documented render
+backends do not establish font, export or cold-start acceptance in our runtime.
+XYFlow's UI extensibility must be evaluated independently of automatic placement.
+
+* Correct responsibility split
+The intended Library has three independently testable concerns:
+1. Question/model projection: use actual OpenSysML and D2 structures plus existing
+ project query/evidence associations to select what the reader needs to see.
+2. Layout and visual grammar: group boundaries, choose topology, size nodes from
+ icons/labels, route edges, place callouts and keep useful whitespace.
+3. Rendering/presentation: draw the placed view through reusable SVG/Canvas
+ capabilities with downstream themes, icon assets and output profiles.
+
+The existing D2 compiler remains a real source adapter and a native comparison
+baseline. OpenSysML retains model semantics. ASCENT retains deterministic facts
+and inference; Aitia retains review. A view projection can carry their original
+IDs and references without creating a new modeling language, evidence database
+or self-appointed authority. A private renderer may use a mature engine object;
+public Library composition can remain functional and immutable.
+
+No engine is selected by README popularity. G6, existing layout engines and
+constraint-oriented alternatives must be compared on actual repository content
+and export/runtime constraints before adding a dependency. Pure static output
+and interactive inspection are separate requirements and may need distinct
+adapters rather than one mandatory browser application.
+
+* Same-case comparison protocol
+Use repository90.91,90.120 and90.84 as frozen semantic baselines. Preserve their
+original views for regression. Derive question-scoped architecture, workflow and
+explanation candidates explicitly; record any omitted/aggregated element and
+keep required obligations reachable. Do not improve density by deleting meaning.
+
+For each scope, compare:
+- The existing native rendering baseline.
+- The current TS compositor baseline, retaining its visible shortcomings.
+- Candidate visual grammars backed by mature layout/render capabilities.
+
+Keep the same question, model references, required nodes/relations and evidence
+status across a comparison. Theme and engine identity are independent recorded
+inputs. A compact static figure, an explanatory infographic and an interactive
+inspection view must not all be forced into the same tall report frame.
+
+* Acceptance and research questions
+| Category | Acceptance / observation |
+|----------+--------------------------|
+| Semantic correctness | Preserve identity, containment, relation direction and required obligations; disclose aggregation and unsupported notation |
+| Mechanical drawing checks | Detect clipped text, unintended overlaps, invalid geometry, invisible required content and routes through unrelated nodes |
+| Typography | Compare actual output at its intended viewing size; enlarging the canvas cannot compensate for unreadably small labels |
+| Routing | Compare crossings, shared paths, label collisions and unnecessarily long connectors; port semantics must survive |
+| Space | Compare occupied bounds, group padding and aspect ratio; use these as diagnostics, not a universal maximum-density rule |
+| Visual vocabulary | Icons encode actual kinds; shape, grouping, color and short labels have consistent meaning and an explicit legend where needed |
+| Evidence | Missing, conflict and pending remain distinct; visual status cannot establish an accepted Q or review R |
+| Reproduction | Record actual engine, theme/assets, source closure and layout options/seed; distinguish reproducibility from aesthetic preference |
+| Human comparison | Ask whether each view answers its question faster and more accurately; do not label subjective quality as a formal proof |
+
+LLMs may propose view scope, constraints and alternative candidates, then inspect
+rendered results for visual defects. Deterministic validation protects semantics
+and mechanical requirements. Human comparison establishes whether an explanation
+actually reduces reading burden. ASCENT is not an aesthetic oracle.
+
+* Implementation order
+1. Preserve the current prototype, original sources and screenshots as baselines;
+ stop treating the fixed report layout as the target framework.
+2. Write question/view fixtures with required identities and expected distinctions,
+ independent of the source language and the chosen renderer.
+3. Qualify focused layout/render candidates for nested groups, icon-rich nodes,
+ edge routing, text metrics and static export. Compare complete figures.
+4. Select the smallest reusable capabilities that pass those cases; replace the
+ prototype composition rather than stacking another wrapper over it.
+5. Apply independent themes/icons and connect actual model/evidence references;
+ qualify downstream use and publish only reviewed outputs.
+
+Open decisions: renderer/backend selection, exact layout constraint surface,
+static versus interactive adapters, reliable font measurement, multilingual text,
+asset closure and visual regression policy. No new engine has been installed by
+this research and no automatic orchestrator or visual-acceptance claim is issued.
+
+* D2 source identity observation
+The inspected official D2 checkout6a7b4837838a3907604a1d9eec57ee98b6f3cd01
+now declares @d2lang/d2 in d2js/js/package.json. The prototype uses the older
+@terrastruct/d2 published package. This is a dependency/API comparison obligation,
+not authorization to mix an unqualified version upgrade into visual experiments.
+Source-language and rendering-backend choices remain independently reviewable.
+
+* Visual inspection notes
+The actual Cocoon web-app image and HTML use a large fixed SVG canvas with a
+horizontal chain and a separate card row. Its color discipline is useful, but
+its large unused area and fixed placement are not a solution to our current
+layout complaint. It is not selected as the target template.
+The AWS example demonstrates icon-based resource identity and nested boundaries.
+The Diagrams advanced web-service example illustrates icon-rich clusters and
+relationships as the main figure rather than a document shell. These are visual
+references, not evidence that their default layout handles our Library models.
+
+* LLM/MLLM-led architecture and explanatory figures
+The next research scope is visual design by language and multimodal models, not
+only graph libraries. Historical scores, fixed templates and the current engine
+choice are references for comparison; they must not become a universal aesthetic
+objective. The user's uploaded infographic is a concrete visual reference for
+compact information grouping, meaningful icons and low reading burden. Its exact
+six-section format is not mandatory for every future question.
+
+| Primary project and inspected revision | Actual inspected material | Relevant method and limit |
+|---------------------------------------+---------------------------+---------------------------|
+| [[https://github.com/cathrynlavery/diagram-design/tree/0cf73c3fcc56667ad91a9a04c439ac6ccfabc9d1][Diagram Design]] | Official site before/after examples; repository type/example inventory and architecture screenshot | LLM-authored self-contained HTML plus inline SVG; choose a visual genre and hierarchy from the content rather than restyling one card layout |
+| [[https://github.com/nicobailon/visual-explainer/tree/2a34c38c8bf5d0affbf60700f7cf24245bb8ed07][Visual Explainer]] | templates/page.html and kit/render.mjs | A figure and a concise answer lead; semantic references connect prose to diagram parts; rendered pages receive browser/contact-sheet inspection |
+| [[https://github.com/google-research/papervizagent/tree/e088a8fff74cc363b6897c0843631fff76484908][PaperVizAgent / PaperBanana]] | Official repository identity; agents/critic_agent.py; author project pipeline | Reference retrieval, content/style planning and visual-feedback refinement; a research method, not an instruction to add its multi-agent runtime |
+| [[https://github.com/ResearAI/AutoFigure-Edit/tree/16f3749e9d512bdf7b7b55c162307bc289750b7a][AutoFigure-Edit]] | Official README, staged reconstruction and reference-style gallery | Use a reference image to guide composition and reconstruct editable SVG; its image generation/segmentation stack is not automatically a dependency |
+
+The independently maintained llmsresearch/paperbanana implementation is not the
+official Google project. Research attribution and locally verified capabilities
+must remain distinct. No reference system has been run against our case here.
+
+** HTML as a drawing artifact
+HTML/CSS and inline SVG are valid primary visual authoring surfaces. A standalone
+figure can contain iconography, spatial grouping, annotations, comparison panels
+and a compact legend without a navigation shell or long prose document. CSS grid
+and SVG each serve the visual problem that suits them. D2 remains available for
+model relationships or an embedded subview; it does not prescribe the entire
+composition. OpenSysML and existing C/Q/evidence references retain their semantic
+ownership across every output surface.
+
+Keep the editable HTML/SVG source. Export the intended figure region to PNG or
+other requested outputs through a real browser. [[https://playwright.dev/docs/screenshots][Playwright supports page and element screenshots]];
+this establishes an export capability, not our completed exporter acceptance.
+Record viewport, device scale, browser identity, theme/assets and source identity;
+wait for fonts/images and a deliberately selected animation state. Screenshot the
+figure rather than accidentally capturing controls or the full document. Native
+browser font rendering is particularly relevant after the observed resvg fallback.
+PDF/SVG export should be qualified separately when required.
+
+** Applying the method to this Library
+1. Start with the question, audience, actual model/evidence and the explanation's
+ intended takeaway. Keep architecture and workflow as distinct visual scopes.
+2. Select relevant reference figures and visual metaphors. LLM proposes compact
+ layouts, component groupings, icon placement and short annotations; no single
+ score or fixed card hierarchy defines the solution space.
+3. Render candidate HTML/SVG at the intended reading size and inspect its actual
+ image. MLLM critique addresses reading order, ambiguity, whitespace, collisions
+ and missing meaning, then the candidate is revised with the user's feedback.
+4. Check source/model/evidence fidelity through existing framework owners. These
+ semantic checks remain separate from optional aesthetic diagnostics.
+5. Retain editable sources, export images and record the selected candidate and
+ its associated references. Publication remains an explicit project decision.
+
+This is the revised design direction. The current fixed-width, fixed-section TS
+compositor is a baseline to replace, not the contract that future LLM-designed
+figures must fit. Public TS APIs can remain functional; themes, icons, source
+acquisition and export effects stay independently composable. Existing Agent
+capabilities should drive design iteration before adding another orchestration
+runtime. No implementation change or live backend/API invocation accompanies
+this research update.
+
+Visual inspection of Diagram Design's architecture.png still shows substantial
+unused canvas. Its method and output surface are relevant, but this individual
+sample is not accepted as our density or iconography baseline. Reference projects
+are sources of techniques, not automatic aesthetic winners or fixed score targets.
+
+* Implementation: Deterministic Engineering Render
+The package is now @deterministic-engineering/render in packages/render. The
+package version remains1.0.0 and is read only from package.json. The old fixed
+composePresentation SVG/card compositor is removed; composeFigure accepts a pure
+HTML/SVG composition. The host supplies a Playwright browser to exportFigure;
+the Library owns and closes its own context and returns PNG plus rendering facts.
+It writes no project files and issues no Core/Aitia authority.
+
+** D2 owns Questions and authored content
+D2 already has the [[https://d2lang.com/tour/vars/][vars.d2-config.data plugin metadata mechanism]].
+The existing compiler exports it through graph.data. The installed SDK returns
+null for nested map values; a native JSON block scalar carries the nested figure
+declaration correctly. Official D2 parses source/imports; standard JSON.parse
+reads that scalar. No second D2 parser or TS Questions registry is introduced.
+
+The90.132/90.133 D2 sources contain Questions, focus/source associations, explanatory
+copy, language, purpose, theme layers and model relations. TS contains visual
+composition and source reading adapters. A changed D2 question changes the image
+without editing TS. Native relation IDs/directions are retained, and the compositor
+refuses requests for undeclared relations. The case is a scoped projection with
+explicit model/source links, not a complete SysML semantic validation.
+
+** Language and own visual system
+This case uses English because its source architecture, D2 and SysML are English.
+No renderer-wide language translation is applied. Upstream maintains Blueprint,
+Field Notes and Graphite theme data plus a Compact spacing layer. Declared ordered
+layers compose through pure functions; downstream may supply its own layers.
+The previously used Lucide default dependency is removed. The Engineering glyph
+pack uses project-authored SVG assets for definitions, selection, capture, model,
+document, decision, evidence and source references. It remains independently
+replaceable. The uploaded infographic guides density and information grouping,
+not the copying of its decorative assets or a fixed universal template.
+
+** Actual case and remaining trust boundary
+The Case reads actual organization Library objects, Profile declarations, Scenario
+functions, project.sysml and Org design/dossier sources. Explicit source excerpts
+and their line positions are observed; this is not a Scheme or SysML interpreter.
+Current prepare-formation-capture still returns capture-blocked and capture=#f,
+which remains visible in both views. No C, accepted Q, independent R or delivery
+is claimed by this image-generation run.
+
+Architecture and Workflow are separate HTML/PNG artifacts. The main relationship
+region uses content-driven height rather than fitting the whole graph to the full
+page width. Browser-measured region bounds are recorded as diagnostics, not a
+universal density score or automatic aesthetic acceptance. Positive/negative
+checks cover actual D2 metadata, missing/changed questions, source footprint,
+undeclared/reversed relations, theme/icon replacement, clipping, invalid images,
+HTML modification and browser-context cleanup.
+
+An initial export-control run stalled at document.fonts.ready before layout in a
+script-disabled browser context. The corrected path uses the standard context,
+explicitly settles layout, then waits for fonts/images. The same clipping and
+invalid-image controls now return the intended refusals. Previous logs are kept
+locally; a repeated run alone is not the claimed fix.
+
+* Visual language, diagram organization and palette are independent
+A theme is a visual language: stroke treatment, typography, node/port shapes,
+connector appearance, boundary treatment and spacing. Palette layers remain
+composable but are not a complete theme on their own. =sketchbook= uses actual
+Rough.js strokes and embedded Kalam fonts; =node-editor= adds node ports,
+precise borders and a node-canvas treatment. It does not embed React Flow.
+
+Swimlane is an organization of responsibility and order, not a palette.
+The D2 figure declaration owns =flow.lanes=, =flow.steps= and =layout=.
+Steps bind existing native D2 node IDs; their consecutive relations must exist.
+A missing lane, node or edge refuses composition. Responsibility labels are
+explicit project design declarations, not verified human role mandates.
+A business view similarly needs its own business entities/relations and Question;
+changing a theme cannot invent these semantics.
+
+Downstream selects ordered theme layers and a layout in the D2 declaration.
+Custom layers and functional composers remain replaceable; the package does
+not prescribe numbered sections or a universal card layout. Architecture and
+Workflow retain independent purposes. Current source/model language stays English.
+
+HTML export measures settled browser text and routes declared connectors against
+actual node boxes. Sketch surfaces are marked explicitly with =data-surface=;
+font assets and deterministic Rough.js SVG are baked into offline HTML.
+Receipts distinguish original composed HTML from materialized HTML, record font
+hashes and actual browser identity. These are rendering receipts, not C/Q/R evidence.
+
+Native browser controls cover deterministic sketch output, changed drawing
+parameters, missing connector endpoints, clipping and broken images. The real
+case generates architecture and workflow separately, plus sketchbook,
+node-editor and workflow-swimlane variants. Actual current source still returns
+=capture-blocked / capture = #f=; no accepted inference or published model is claimed.
+
+References: [[https://roughjs.com/][Rough.js drawing primitives]] and
+[[https://reactflow.dev/learn/customization/custom-nodes][React Flow node customization]].
+The user-provided Zhihu reference returned HTTP 403; its article contents were
+not inspected. Its hand-drawn example is used as the requested visual direction.
+
+* Static style implementation and open view organization
+The current package boundary is static rendering. Rough.js supplies sketch
+primitives and hachure strokes. XYFlow's public =getSmoothStepPath= supplies
+connector geometry; the package does not initialize an editor, interactions or
+pan/zoom state. The complete Excalidraw package and React are excluded. There is
+no editor-scene adapter, runtime bundler or additional build entry.
+
+** Three independent choices
+| Concern | Owner | Current implementation |
+|---------+-------+------------------------|
+| Meaning and Questions | D2/SysML/project | Canonical =90.132=/=90.133= declarations and captured sources |
+| View organization | Project compositor | Native graph, responsibility swimlane, dependency matrix and open =layout=/=presentation= |
+| Visual language | Replaceable theme layers | Independent =styles/sketchbook.ts= and =styles/node-editor.ts=, palettes, spacing, fonts and icons |
+
+Node and swimlane are examples, not the framework's complete catalogue. Native
+D2 classifications retain their existing language/rendering path. New domain
+organizations use a functional composer and validate their own presentation data;
+there is no mandatory card count or global enumeration of all future diagram kinds.
+The new dependency-matrix Case shows the same native architecture relationships
+in a genuinely different organization, without converting dependencies to workflow.
+
+** Source and evidence invariants
+Questions, explanatory copy, current language and source links remain declared
+in D2. The model association remains the captured =project.sysml= and native D2
+projection. Changing a theme/layout changes the rendering footprint, not the model
+or evidence status. Every rendered connector is checked against its captured
+relation and direction. Matrix axes must reference native nodes. Workflow lane
+assignments are design declarations, not role-authorization evidence.
+
+Font assets are explicit theme data. Rendering records composed HTML separately
+from materialized offline HTML, actual font hashes, drawing/routing package pins,
+browser identity and output fingerprints. Rough.js deterministic-seed controls
+apply within the qualified browser/font environment; no cross-platform pixel
+identity is claimed. The current blocked capture and empty evidence remain visible.
+
+** Verification and artifacts
+The standard package checks cover source/Question capture, native D2 syntax and
+notation, saved SDK projection, open custom presentation, immutable style
+composition and unknown-layer refusal. Native browser controls cover actual
+Rough.js output and font embedding, repeated seeded output, modified inputs,
+clipping, broken images, missing endpoints and reversed model relations.
+A separate packed-package consumer checks installed worker/font/dependency paths.
+
+Eight full HTML/PNG candidates plus rendering receipts are generated through
+=examples/render/library-case.ts=. =examples/render/comparison.ts= compares only
+the main diagram regions, separately for architecture and Workflow, without
+stretching a Workflow to fill the whole figure. Document snapshots are explicit
+design candidates; rendering does not submit review or publish SysML.
+
+References: [[https://roughjs.com/][Rough.js official primitives]],
+[[https://reactflow.dev/api-reference/utils/get-smooth-step-path][XYFlow public connector utility]],
+[[https://docs.excalidraw.com/docs/@excalidraw/excalidraw/installation][Excalidraw editor boundary]].
+
+** Local qualification recorded on 2026-11-01
+- Package and real consumer native TypeScript checks: passed.
+- Bun unit/native D2/saved SDK controls: 29 passed, 0 failed.
+- Native Playwright export controls: 7 passed, 0 failed; worker limit remains two.
+- Eight complete candidates and two separate comparison sheets: generated and visually inspected.
+- Final packed consumer: =PACKED-CONSUMER-OK=, including owned D2 worker,
+ actual Rough.js SVG, embedded font resources and selected-region PNG export.
+- Qualified tarball SHA256: =ded011a886bb9ad47b69340a89bec268d1365a8b0ac79a7fe8ea9883e4f00bf5=.
+- Runtime dependencies: D2 0.1.33, Rough.js 4.6.6, Kalam 5.3.0,
+ XYFlow system 0.0.83. Neither editor nor React remains in the declarations/locks.
+- Actual local export browser: Chromium =156.0.8078.4=.
+- CI workflow syntax: actionlint passed. Remote Linux/macOS execution is not
+ established by these local results; no live SysML validation or C/Q/R is issued.
+
+* Extensibility audit follow-up
+[[file:10.142-declarative-render-extensibility-audit.org][10.142]] supersedes the earlier
+closed kind selection, framework-owned linear flow helper and programmatic-only
+icon selection. It records the current owners and executable positive/negative controls.
diff --git a/docs/10-19-design/10-architecture/10.142-declarative-render-extensibility-audit.org b/docs/10-19-design/10-architecture/10.142-declarative-render-extensibility-audit.org
new file mode 100644
index 0000000..1b8ff7f
--- /dev/null
+++ b/docs/10-19-design/10-architecture/10.142-declarative-render-extensibility-audit.org
@@ -0,0 +1,102 @@
+#+TITLE: 10.142 Declarative Static Render Extensibility Audit
+#+LANGUAGE: en
+#+PROPERTY: JD_ID 10.142
+
+* Scope and resulting ownership
+This audit follows 10.141 and inspects the actual TS package, D2 declarations,
+Library/Profile/Scenario Case, native APIs and CI entrypoints. The package remains
+=@deterministic-engineering/render=, version 1.0.0 read from =package.json=.
+It is a static rendering library. No whiteboard/editor or React runtime is added.
+
+| Owner | Publishes / consumes | Does not own |
+|-------+----------------------+--------------|
+| Project D2/SysML | Questions, copy, model IDs/edges, sources, kind, layout, theme/icon selection and variants | Generic render implementation |
+| Project composer | Domain presentation schema, selected content organization, source observations | Another HTML/D2 parser, graph layout engine or evidence ledger |
+| Render core | Capture/fingerprint, immutable composition context, resource resolution and purpose boundary | Linear-workflow semantics, finite catalogue of all diagram kinds or mandatory page template |
+| Mature libraries | D2 compilation/layout/SVG, parse5 HTML parsing, Rough.js strokes/options, XYFlow connector paths, Playwright export | Project truth, evidence issuance or review authority |
+
+* Findings and disposition
+| Finding in previous implementation | Change / exact owner | Verification |
+|------------------------------------+----------------------+--------------|
+| Seven names acted as the whole diagram taxonomy; purpose checks were repeated | =source/types.ts= has open kind identifiers and one =validatePurposeKind=; reserved standard kinds retain their purpose | Custom kind through actual D2 worker and native SVG render; blank kind/reserved mismatch refuse |
+| Framework parsed/validated every workflow as one ordered preparation path with lanes | Moved schema and checks to =examples/render/library-case/workflow-declaration.ts=; D2 places it in project-owned =presentation.flow= | Case lane/node/edge negatives, actual source declarations |
+| Compiler forced ELK regardless of project selection | Both worker entry and default native theme leave compile options empty; RenderTheme accepts the complete official CompileOptions | Actual native D2 DAGRE declaration compiles |
+| Variant consumer re-resolved themes through upstream defaults and lost downstream resources | =readD2Figure= resolves each variant once against supplied =FigureResources=; =selectFigureVariant= retains it | Downstream theme remains in variant and HTML; missing/duplicate variants refuse |
+| Icons were programmatic overrides; D2 could not select packs, metadata always said engineering | Ordered immutable =composeIconPacks=, D2 =icons= and variant overrides; actual SVG hashes in metadata | Custom real Case glyphs; unknown/prototype-name pack and invalid asset refuse |
+| Question markers used a double-quote regex | parse5 parses real HTML; browser independently checks actual visibility | Single/unquoted attributes pass; comments/template/hidden/CSS invisibility refuse |
+| Captured model and project presentation could be modified by a composer | Deep-freeze cloned inputs before composition | Mutation negatives; caller input remains unchanged |
+| Drawing hardcoded rectangle/hachure/pen parameters | Reuse Rough.js =Options= directly; explicitly declared rectangle/ellipse/diamond surfaces call its primitives | Options/geometry change output; unsupported surface refuses |
+| HTML connections assumed horizontal endpoints | Declared =data-source-position=/=data-target-position= select XYFlow positions | Vertical connection passes, invalid side and reversed relation refuse |
+| Matrix variant replaced part of Case HTML via substring search | =architecture-diagram.ts= is an ordinary Case-owned pure-function table | Real default/matrix/custom resource compositions and unsupported Case layout refusal |
+| Source path checking was duplicated | Figure capture reuses =source/catalog.ts= source boundary | Source law and changed/uncaptured input controls |
+| Source excerpts were hardcoded in TS | D2 =presentation.observations= declares assertions; Case effect checks actual files | Missing/changed excerpts stop output; observations do not become execution evidence |
+| Unused SVG data-URI wrapper duplicated the existing inline SVG path | Removed; =IconAsset= remains the single payload type | Same real glyph/export tests |
+
+* Downstream declaration contract
+=compileFigureSource(sources, inputPath, resources?)= accepts data-only resources:
+=themeLayers= and =iconPacks=. Defaults are immutable. A downstream extends them
+with ordinary module exports, then D2 selects ordered names. No global registration,
+new manifest, cache or automatic discovery mechanism is required.
+
+A declared variant may select =kind=, =layout=, =themes= and =icons=. Unspecified
+resources inherit the view's resolved selection. =selectFigureVariant= preserves
+the captured sources, model, Questions and evidence links; it does not recompile
+or rewrite the model. Unknown or duplicate variants fail. Explicit icon packs are
+complete selected maps, not implicit word-based icon inference.
+
+=kind= is open. =purpose= remains architecture or workflow. Reserved standard
+kinds keep their original purpose, so allowing a new kind cannot silently turn
+an existing architecture view into a Workflow. Project =presentation= stays
+unknown at the generic boundary and is specifically validated by its compositor.
+
+The Case's =project-owned= variants declare =responsibility-map= and
+=capture-handoff= kinds, compose =field-notes + workbench + compact= and select
+=engineering + repo= glyphs. These names are absent from framework source.
+The Case remains explicitly English and still displays =capture-blocked= with
+=capture = #f= and no issued C/Q/R. Source assertions remain source observations.
+
+* Intentional retained defaults and limits
+Upstream palettes/styles and the Engineering glyph pack are optional maintained
+resources. Their existence does not limit downstream kinds or assets. The Case
+has an authored explanatory layout; its headings, role placement and section
+organization belong to that Case, not to =composeFigure=.
+
+Native D2 notation keeps D2 layout/SVG. The browser adapter measures explicitly
+authored HTML node boxes and delegates only connectors to XYFlow; it does not
+replace D2 with another graph layout solver. Rough.js adapters expose the selected
+primitives; other explicit SVG markup can be supplied by a compositor. Unknown
+primitives do not silently render as rectangles.
+
+OpenSysML's supported reflow adapter retains capability refusals for unsupported
+notation. This audit does not claim live SysML service validation. Static HTML
+export still bounds width/scale, closes its browser context, records actual fonts,
+backend pins and output hashes, and rejects stale inputs and detected clipping.
+Visual quality remains a review judgment separate from correctness controls.
+
+* Acceptance and execution entrypoints
+The normal CI entrypoints run package =bun install --frozen-lockfile=,
+=typecheck=, =bun test=, =bun run test:export=; then the actual consumer's frozen
+install, typecheck, tests, Case export, comparisons and native audit view.
+Playwright owns browser fixtures; tests do not start another test runner. Worker
+limits remain two and existing job/resource bounds are unchanged.
+
+Required positives include native D2 notation, project-only kinds/themes/icons,
+variant retention, real Library/Profile/Scenario sources, source fingerprints,
+independent architecture/Workflow artifacts and actual font/image export.
+Negatives retain unknown resources, hidden Questions, source/model mutation,
+invalid lanes/relations/ports, unsupported primitives, stale input and broken images.
+
+Ten full candidates and two comparison sheets are derived from 90.132/90.133.
+90.134 is the native D2 ownership/dependency view for this audit. CI is authoritative
+for Linux/macOS and its exact commit; local results alone do not establish both.
+No artifact-upload action is added; generated image candidates are tracked as
+requested documentation. No model publication, review or delivery is performed.
+
+* Local qualification before PR
+Package typecheck passed; 35 unit/native-D2/saved-SDK tests passed. The native
+Playwright entry passed 10 positive/negative controls. The actual consumer passed
+its typecheck and two domain integration tests, exported ten full candidate
+figures and two comparison sheets, and rendered the native D2 audit view.
+Remote Linux/macOS qualification must be read from the new PR's checks; local
+results do not substitute for those runs. The workflow keeps its ten-minute job
+bound and two-worker configuration and installs only declared packages/browser.
diff --git a/docs/90-99-views/90-architecture-views/90.01-index.org b/docs/90-99-views/90-architecture-views/90.01-index.org
index 7d5b972..517420a 100644
--- a/docs/90-99-views/90-architecture-views/90.01-index.org
+++ b/docs/90-99-views/90-architecture-views/90.01-index.org
@@ -508,3 +508,31 @@ Lean proof, benchmark execution or operational effect is implemented by them.
- [[file:90.127-native-package-cohort-and-ci-provenance.d2][90.127 Native package cohort and CI provenance]]
- [[file:90.128-native-cohort-source-acquisition.d2][90.128 Native cohort and source acquisition]]
+
+- [[file:90.129-functional-architecture-rendering.d2][90.129 Functional architecture rendering]] — [[file:90.129-functional-architecture-rendering.svg][SVG]]; owner: [[file:../../10-19-design/10-architecture/10.140-functional-architecture-rendering-and-opensysml.org][10.140]].
+
+- [[file:90.130-llm-assisted-model-view-workflow.d2][90.130 Model-to-view authoring workflow]] — [[file:90.130-llm-assisted-model-view-workflow.svg][SVG]]; separate from component architecture; owner: [[file:../../10-19-design/10-architecture/10.140-functional-architecture-rendering-and-opensysml.org][10.140]].
+
+- [[file:90.131-library-responsibility-explanation.d2][90.131 Question-led Library responsibilities]] — concise candidate derived from 90.91; [[file:../../10-19-design/10-architecture/10.140-functional-architecture-rendering-and-opensysml.org][TS page composition design]].
+
+- [[file:90.132-library-profile-scenario-architecture.d2][90.132 Library / Profile / Scenario architecture]] — [[file:90.132-library-profile-scenario-architecture.html][editable HTML]] · [[file:90.132-library-profile-scenario-architecture.png][PNG]]. Questions and presentation selection live in D2.
+- [[file:90.133-capture-preparation-workflow.d2][90.133 Capture preparation workflow]] — [[file:90.133-capture-preparation-workflow.html][editable HTML]] · [[file:90.133-capture-preparation-workflow.png][PNG]]. Separate workflow, current capture-blocked result preserved.
+
+** Static visual styles and open view organizations
+The following are design candidates of the same captured Library/Profile/Scenario
+case, not acceptance of C/Q/R. Full figures retain their Questions and source links.
+Comparison sheets show the main diagram regions at readable size.
+
+- [[file:90.132-library-profile-scenario-architecture-comparison.png][Architecture comparison]] — [[file:90.132-library-profile-scenario-architecture-comparison.html][standalone HTML]]. Hand-drawn, node style and dependency matrix.
+- [[file:90.133-capture-preparation-workflow-comparison.png][Workflow comparison]] — [[file:90.133-capture-preparation-workflow-comparison.html][standalone HTML]]. Hand-drawn, node style and responsibility swimlane.
+- [[file:90.132-library-profile-scenario-architecture-sketchbook.png][Full hand-drawn architecture]] · [[file:90.132-library-profile-scenario-architecture-node-editor.png][Full node-style architecture]] · [[file:90.132-library-profile-scenario-architecture-dependency-matrix.png][Full dependency matrix]].
+- [[file:90.133-capture-preparation-workflow-sketchbook.png][Full hand-drawn Workflow]] · [[file:90.133-capture-preparation-workflow-node-editor.png][Full node-style Workflow]] · [[file:90.133-capture-preparation-workflow-swimlane.png][Full swimlane Workflow]].
+
+Upstream themes are independent visual modules; palettes, typography, drawing,
+icons and view organization compose separately. Additional domain layouts use
+project-owned declarations and pure composers. Native D2 sequence/state/data and
+other supported notation retain their official rendering path. The static package
+uses Rough.js and XYFlow's path utility; it includes no interactive whiteboard.
+
+- [[file:90.132-library-profile-scenario-architecture-project-owned.png][Project-owned architecture style/icons]] · [[file:90.133-capture-preparation-workflow-project-owned.png][Project-owned workflow style/icons]]. Kind, layout, ordered themes and glyph packs are selected in D2.
+- [[file:90.134-render-responsibility-boundaries.d2][90.134 Static renderer responsibility boundaries]] · [[file:90.134-render-responsibility-boundaries.svg][native SVG]]. See [[file:../../10-19-design/10-architecture/10.142-declarative-render-extensibility-audit.org][10.142 audit]].
diff --git a/docs/90-99-views/90-architecture-views/90.129-functional-architecture-rendering.d2 b/docs/90-99-views/90-architecture-views/90.129-functional-architecture-rendering.d2
new file mode 100644
index 0000000..183c803
--- /dev/null
+++ b/docs/90-99-views/90-architecture-views/90.129-functional-architecture-rendering.d2
@@ -0,0 +1,32 @@
+...@../../../packages/render/themes/system
+consumer: {
+ label: "Downstream repositories"
+ gateway: "Gateway / Weaver\nmodel selection and local themes"
+ case: "Engineering Library case\nOrg, SysML and decisions"
+}
+library: {
+ label: "Engineering Library / renderer package"
+ api: "Public package exports\nindex.ts / opensysml subpath"
+ core: "Functional core\ncatalog / references / pipeline"
+ runtime: "Rendering effects\nworkers / native D2 compile + render"
+ theme: "Presentation data\ndefaultTheme / custom overrides"
+ presentation: "TS visual composition\nquestion / explanations / evidence context"
+ pageTheme: "Independent page themes\ntypography / surfaces / status colors / icons"
+ model: "Optional model projection\nOpenSysML adapter and source trace"
+}
+external: {
+ label: "Official external capabilities"
+ model: "OpenSysML TS SDK\npublic model and RenderedView types"
+ engines: "Rendering libraries\nOfficial D2 compile / render"
+}
+consumer.gateway -> library.api: package dependency
+consumer.case -> library.api: same package API
+library.api -> library.core: composes functions
+library.api -> library.runtime: owns effect scopes
+library.core -> library.theme: explicit theme input
+library.runtime -> external.engines: public engine APIs
+library.model -> external.model: public SDK API
+consumer.gateway -> library.model: optional SysML projection
+
+library.api -> library.presentation: optional explanation page
+library.presentation -> library.pageTheme: explicit presentation input
diff --git a/docs/90-99-views/90-architecture-views/90.129-functional-architecture-rendering.svg b/docs/90-99-views/90-architecture-views/90.129-functional-architecture-rendering.svg
new file mode 100644
index 0000000..046ffff
--- /dev/null
+++ b/docs/90-99-views/90-architecture-views/90.129-functional-architecture-rendering.svg
@@ -0,0 +1,118 @@
+
\ No newline at end of file
diff --git a/docs/90-99-views/90-architecture-views/90.130-llm-assisted-model-view-workflow.d2 b/docs/90-99-views/90-architecture-views/90.130-llm-assisted-model-view-workflow.d2
new file mode 100644
index 0000000..5c792cc
--- /dev/null
+++ b/docs/90-99-views/90-architecture-views/90.130-llm-assisted-model-view-workflow.d2
@@ -0,0 +1,12 @@
+...@../../../packages/render/themes/flow
+confirmed: {label: "User-confirmed intent\nproject inputs and responsibility"; class: note}
+llm: {label: "LLM proposes model and views\narchitecture and workflow kept separate"; class: semantic}
+check: {label: "Check obligations and model\nASCENT queries / SysML capability"; class: policy}
+selection: {label: "Select view family and theme\nexplicit candidate capture"; class: cloud}
+render: {label: "Render candidate\nfunctional core / bounded effects"; class: runtime}
+review: {label: "Review and publish\nretain source and decision links"; class: note}
+confirmed -> llm: authorized design input
+llm -> check: candidate, not evidence
+check -> selection: supported candidate
+selection -> render: immutable input
+render -> review: images and trace
diff --git a/docs/90-99-views/90-architecture-views/90.130-llm-assisted-model-view-workflow.svg b/docs/90-99-views/90-architecture-views/90.130-llm-assisted-model-view-workflow.svg
new file mode 100644
index 0000000..3dba384
--- /dev/null
+++ b/docs/90-99-views/90-architecture-views/90.130-llm-assisted-model-view-workflow.svg
@@ -0,0 +1,106 @@
+User-confirmed intentproject inputs and responsibilityLLM proposes model and viewsarchitecture and workflow kept separateCheck obligations and modelASCENT queries / SysML capabilitySelect view family and themeexplicit candidate captureRender candidatefunctional core / bounded effectsReview and publishretain source and decision linksauthorized design inputcandidate, not evidencesupported candidateimmutable inputimages and trace
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/90-99-views/90-architecture-views/90.131-library-responsibility-explanation.d2 b/docs/90-99-views/90-architecture-views/90.131-library-responsibility-explanation.d2
new file mode 100644
index 0000000..acf6fac
--- /dev/null
+++ b/docs/90-99-views/90-architecture-views/90.131-library-responsibility-explanation.d2
@@ -0,0 +1,25 @@
+# Question-led presentation candidate derived from 90.91 and Org 10.31.
+# Short labels are a reviewed content change, not a rewrite of the original source.
+direction: right
+vars: { d2-config: { theme-id: 0; layout-engine: elk; pad: 28 } }
+classes: {
+ library: {style: {fill: "#EAF1FF"; stroke: "#245FE5"; font-color: "#173E89"; border-radius: 14; font-size: 22}}
+ profile: {style: {fill: "#F0EBFC"; stroke: "#7753D9"; font-color: "#50368A"; border-radius: 14; font-size: 22}}
+ scenario: {style: {fill: "#E7F4F0"; stroke: "#138274"; font-color: "#125F55"; border-radius: 14; font-size: 22}}
+ artifact: {style: {fill: "#FFFFFF"; stroke: "#CCD6E8"; font-color: "#364A68"; border-radius: 10; font-size: 19}}
+}
+library: {label: "Library\nPublished contracts"; class: library}
+profile: {label: "Profile\nSelected configuration"; class: profile}
+scenario: {label: "Scenario\nOne captured use"; class: scenario}
+content: {
+ label: "Project-owned content"
+ style: {fill: "#F7F9FD"; stroke: "#CCD6E8"; border-radius: 16; font-size: 20}
+ model: {label: "Architecture model"; class: artifact}
+ docs: {label: "Org documents"; class: artifact}
+ decisions: {label: "Decision records"; class: artifact}
+}
+profile -> library: "selects published contracts"
+scenario -> profile: "binds selection"
+scenario -> content.model: "captures model references"
+scenario -> content.docs: "captures document closure"
+scenario -> content.decisions: "captures decision context"
diff --git a/docs/90-99-views/90-architecture-views/90.131-library-responsibility-explanation.svg b/docs/90-99-views/90-architecture-views/90.131-library-responsibility-explanation.svg
new file mode 100644
index 0000000..1287c09
--- /dev/null
+++ b/docs/90-99-views/90-architecture-views/90.131-library-responsibility-explanation.svg
@@ -0,0 +1,113 @@
+{"question":{"ref":"10.140/questions/three-responsibilities","text":"Who publishes, who selects, and who captures?"},"basis":{"captureRef":"10.140/question-led-design-candidate","modelRefs":["90.91 / original content-contract view"],"obligationRefs":["10.31 / public contracts"],"evidenceRefs":[]},"findings":[{"icon":{"svg":"\n<svg\n class=\"lucide lucide-library-big\"\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n>\n <rect width=\"8\" height=\"18\" x=\"3\" y=\"3\" rx=\"1\" />\n <path d=\"M7 3v18\" />\n <path d=\"M20.4 18.9c.2.5-.1 1.1-.6 1.3l-1.9.7c-.5.2-1.1-.1-1.3-.6L11.1 5.1c-.2-.5.1-1.1.6-1.3l1.9-.7c.5-.2 1.1.1 1.3.6Z\" />\n</svg>\n","label":"Library"},"title":"Publish once","text":"Models · methods · policies · obligations","status":"design","refs":["10.31 / Library"]},{"icon":{"svg":"\n<svg\n class=\"lucide lucide-sliders-horizontal\"\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n>\n <path d=\"M10 5H3\" />\n <path d=\"M12 19H3\" />\n <path d=\"M14 3v4\" />\n <path d=\"M16 17v4\" />\n <path d=\"M21 12h-9\" />\n <path d=\"M21 19h-5\" />\n <path d=\"M21 5h-7\" />\n <path d=\"M8 10v4\" />\n <path d=\"M8 12H3\" />\n</svg>\n","label":"Selection"},"title":"Select explicitly","text":"Stage · viewpoints · required contracts","status":"design","refs":["10.31 / Profile"]},{"icon":{"svg":"\n<svg\n class=\"lucide lucide-scan-line\"\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n>\n <path d=\"M3 7V5a2 2 0 0 1 2-2h2\" />\n <path d=\"M17 3h2a2 2 0 0 1 2 2v2\" />\n <path d=\"M21 17v2a2 2 0 0 1-2 2h-2\" />\n <path d=\"M7 21H5a2 2 0 0 1-2-2v-2\" />\n <path d=\"M7 12h10\" />\n</svg>\n","label":"Capture"},"title":"Capture one use","text":"Architecture · Org · decisions · sources","status":"design","refs":["10.31 / Scenario"]}]}ARCHITECTURE / OWNERSHIPOne Library. Three clear responsibilities.Deterministic Engineering / DE-CASE-01 · question-led architecture explanationQuestionTHE QUESTIONWho publishes, who selects, and who captures?Publish reusable contracts. Select the right configuration. Bind real project content.1Architecture / boundaries and responsibilitiesLibraryPublished contractsProfileSelected configurationScenarioOne captured useProject-owned contentArchitecture modelOrg documentsDecision recordsselects published contractsbinds selectioncaptures model referencescaptures document closurecaptures decision context
+
+
+
+
+
+
+2Three responsibilities / no duplicated ownershipLibraryDESIGNPublish onceModels · methods · policies ·obligationsSelectionDESIGNSelect explicitlyStage · viewpoints · required contractsCaptureDESIGNCapture one useArchitecture · Org · decisions · sources3Model / obligations / evidenceArchitecture modelModel references1 linkedObligationObligation references1 linkedPendingEvidence referencesnone linked / pendingDesign candidate derived from 90.91 / 10.31. Evidence and independent review remain open.SOURCE 23c889168a078687 / DESIGN CLAIMS / ARCHITECTURE
\ No newline at end of file
diff --git a/docs/90-99-views/90-architecture-views/90.132-library-profile-scenario-architecture-comparison.html b/docs/90-99-views/90-architecture-views/90.132-library-profile-scenario-architecture-comparison.html
new file mode 100644
index 0000000..743604c
--- /dev/null
+++ b/docs/90-99-views/90-architecture-views/90.132-library-profile-scenario-architecture-comparison.html
@@ -0,0 +1 @@
+
Library / Profile / Scenario architecture
sketchbook
node-editor
dependency-matrix
project-owned
\ No newline at end of file
diff --git a/docs/90-99-views/90-architecture-views/90.132-library-profile-scenario-architecture-comparison.png b/docs/90-99-views/90-architecture-views/90.132-library-profile-scenario-architecture-comparison.png
new file mode 100644
index 0000000..32bac65
Binary files /dev/null and b/docs/90-99-views/90-architecture-views/90.132-library-profile-scenario-architecture-comparison.png differ
diff --git a/docs/90-99-views/90-architecture-views/90.132-library-profile-scenario-architecture-dependency-matrix-detail.png b/docs/90-99-views/90-architecture-views/90.132-library-profile-scenario-architecture-dependency-matrix-detail.png
new file mode 100644
index 0000000..3f2bfa3
Binary files /dev/null and b/docs/90-99-views/90-architecture-views/90.132-library-profile-scenario-architecture-dependency-matrix-detail.png differ
diff --git a/docs/90-99-views/90-architecture-views/90.132-library-profile-scenario-architecture-dependency-matrix.html b/docs/90-99-views/90-architecture-views/90.132-library-profile-scenario-architecture-dependency-matrix.html
new file mode 100644
index 0000000..53260a8
--- /dev/null
+++ b/docs/90-99-views/90-architecture-views/90.132-library-profile-scenario-architecture-dependency-matrix.html
@@ -0,0 +1,32 @@
+Library / Profile / Scenario architecture
`};
+};
diff --git a/examples/render/native.ts b/examples/render/native.ts
new file mode 100644
index 0000000..40cb20d
--- /dev/null
+++ b/examples/render/native.ts
@@ -0,0 +1,13 @@
+import {readFile, writeFile, mkdir} from 'node:fs/promises';
+import {fileURLToPath} from 'node:url';
+import path from 'node:path';
+import {compileFigureSource, renderCatalog} from '@deterministic-engineering/render';
+const root = fileURLToPath(new URL('../../', import.meta.url));
+const file = 'docs/90-99-views/90-architecture-views/90.134-render-responsibility-boundaries.d2';
+const text = await readFile(path.join(root,file),'utf8');
+const input = await compileFigureSource({[file]:text},file);
+const result = await renderCatalog({sources:input.sources,views:[input.view],presentation:{project:input.view.title}});
+if (await readFile(path.join(root,file),'utf8') !== text) throw new Error('Native view source changed');
+const directory = path.join(root,'.cache/render/library-case'); await mkdir(directory,{recursive:true});
+await writeFile(path.join(directory,'render-responsibility-boundaries.svg'),result.images[0]!.svg);
+console.log(`native ownership view: ${result.images[0]!.nodes} nodes / ${result.images[0]!.edges} edges`);
diff --git a/examples/render/package.json b/examples/render/package.json
new file mode 100644
index 0000000..09d7f66
--- /dev/null
+++ b/examples/render/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "deterministic-engineering-render-case",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "case": "bun library-case.ts",
+ "compare": "bun comparison.ts",
+ "typecheck": "tsc --noEmit",
+ "test": "bun test",
+ "native": "bun native.ts"
+ },
+ "dependencies": {
+ "@deterministic-engineering/render": "file:../../packages/render"
+ },
+ "devDependencies": {
+ "playwright": "1.64.1",
+ "typescript": "7.0.2",
+ "@types/bun": "1.4.4"
+ }
+}
diff --git a/examples/render/test/declarations.test.ts b/examples/render/test/declarations.test.ts
new file mode 100644
index 0000000..7950276
--- /dev/null
+++ b/examples/render/test/declarations.test.ts
@@ -0,0 +1,23 @@
+import {test, expect} from 'bun:test';
+import {readFile} from 'node:fs/promises';
+import {compileFigureSource, selectFigureVariant, composeFigure} from '@deterministic-engineering/render';
+import {resources} from '../library-case/resources';
+import {architecture} from '../library-case/architecture';
+import {workflow} from '../library-case/workflow';
+const root = new URL('../../../', import.meta.url);
+const declarations = ['docs/90-99-views/90-architecture-views/90.132-library-profile-scenario-architecture.d2','docs/90-99-views/90-architecture-views/90.133-capture-preparation-workflow.d2'];
+test('real Library/Profile/Scenario D2 selects downstream kinds, themes and icons without framework edits', async () => {
+ for (const file of declarations) {
+ const bytes = await readFile(new URL(file, root), 'utf8');
+ const input = await compileFigureSource({[file]:bytes},file,resources);
+ const sources = {...input.sources, ...Object.fromEntries(await Promise.all(Object.values(input.references).map(async source => [source, await readFile(new URL(source,root),'utf8')])))};
+ const variant = selectFigureVariant({...input,sources},'project-owned');
+ expect(variant.view.kind).toBe(input.view.purpose === 'architecture' ? 'responsibility-map' : 'capture-handoff');
+ expect(variant.questions).toEqual(input.questions); expect(variant.model).toEqual(input.model);
+ expect(variant.theme!.colors.accent).toBe('#246B59');
+ expect(variant.icons!.library!.label).toBe('Repository publication');
+ const figure = composeFigure(variant, input.view.purpose === 'architecture' ? architecture : workflow);
+ expect(figure.html).toContain('capture-blocked'); expect(figure.html).toContain('"evidenceRefs":[]');
+ expect(() => composeFigure({...variant, layout:'not-declared'}, input.view.purpose === 'architecture' ? architecture : workflow)).toThrow('Unsupported case');
+ }
+},30000);
diff --git a/examples/render/test/workflow.test.ts b/examples/render/test/workflow.test.ts
new file mode 100644
index 0000000..4477556
--- /dev/null
+++ b/examples/render/test/workflow.test.ts
@@ -0,0 +1,17 @@
+import {test, expect} from 'bun:test';
+import {composeFigure, type FigureComposer} from '@deterministic-engineering/render';
+import {readWorkflow} from '../library-case/workflow-declaration';
+test('flow layout validates responsibility membership and the native model path', async () => {
+
+ const input = {
+ view: {source: 'flow.d2', title: 'Flow', purpose: 'workflow' as const, kind: 'flow' as const, question: {ref: 'q', text: 'Who?'}, basis: {captureRef: 'candidate', modelRefs: [], obligationRefs: [], evidenceRefs: []}},
+ language: 'en', width: 720, sources: {'flow.d2': 'a -> b'}, references: {source: 'flow.d2'}, questions: [{ref: 'q', text: 'Who?', focusRefs: ['source']}],
+ copy: {a: 'Acquire', b: 'Bind', detail: 'Observed source'}, layout: 'swimlane',
+ presentation: {flow: {lanes: [{id: 'scenario', label: 'Scenario'}], steps: [{node: 'a', lane: 'scenario', title: 'a', detail: 'detail', icon: 'model'}, {node: 'b', lane: 'scenario', title: 'b', detail: 'detail', icon: 'model'}]}},
+ model: {subjects: [{id: 'a', type: 'rectangle'}, {id: 'b', type: 'rectangle'}], relations: [{id: 'edge', src: 'a', dst: 'b', label: 'binds'}]},
+ };
+ const composer: FigureComposer = context => {readWorkflow(context); return {markup: 'Who?', css: ''};};
+ expect(composeFigure(input, composer).purpose).toBe('workflow');
+ expect(() => composeFigure({...input, presentation: {flow: {...input.presentation.flow, lanes: [{id: 'other', label: 'Invented'}]}}}, composer)).toThrow('Undeclared flow lane');
+ expect(() => composeFigure({...input, model: {...input.model, relations: []}}, composer)).toThrow('Undeclared model relation');
+});
diff --git a/examples/render/tsconfig.json b/examples/render/tsconfig.json
new file mode 100644
index 0000000..6c65a35
--- /dev/null
+++ b/examples/render/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../packages/render/tsconfig.json",
+ "include": [
+ "*.ts",
+ "library-case/*.ts",
+ "test/*.ts"
+ ]
+}
diff --git a/packages/render/LICENSE b/packages/render/LICENSE
new file mode 100644
index 0000000..8000a6f
--- /dev/null
+++ b/packages/render/LICENSE
@@ -0,0 +1,504 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random
+ Hacker.
+
+ , 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/packages/render/README.org b/packages/render/README.org
new file mode 100644
index 0000000..4edf1bc
--- /dev/null
+++ b/packages/render/README.org
@@ -0,0 +1,146 @@
+#+TITLE: Deterministic Engineering Render
+#+LANGUAGE: en
+
+* Ownership
+=@deterministic-engineering/render= is a functional static-rendering package.
+D2/SysML and their project adapters own Questions, model relationships, explanatory
+content and source associations. TS composes selected content, applies declared
+visual styles, and exports HTML/SVG/PNG. The package version is read from
+=package.json=; the namespace contains no version.
+
+The framework does not prescribe numbered panels, a fixed card template, or a
+closed list of layouts. Node and swimlane presentations are examples. Domain
+compositions can present a dependency matrix, hierarchy, business relation,
+timeline or another explanation using the same capture and export boundary.
+Native D2 system/data/assurance/routing/flow/sequence/state notation continues
+through the existing official compiler/render API; no replacement graph language
+or layout engine is introduced.
+
+* Declaration and pure composition
+=compileFigureSource(sources, inputPath, resources?)= uses the existing owned D2
+worker. D2 parses syntax/imports; its =graph.data= carries the figure declaration.
+The installed SDK requires nested metadata in a native =|json= block scalar;
+standard =JSON.parse= reads it. No second parser or TS Questions registry exists.
+
+The declaration owns =purpose=, =kind=, =language=, Questions/focus, copy, references,
+basis links, ordered =themes=, open =layout= and project-owned =presentation= data.
+The compositor receives these and the compiled model through =FigureContext=.
+=composeFigure(input, composer)= invokes a pure function returning markup/CSS.
+Every selected Question must have a visible region; invented source associations
+and requested model relations refuse. Architecture and Workflow remain separate.
+
+=layout= is an open project selector, not a package enum of allowed diagram kinds.
+=FigureInput.presentation= stays unknown until the domain compositor validates it.
+The dependency-matrix example checks declared axes against native model node IDs.
+The Case-owned linear workflow helper validates step/lane membership and actual
+consecutive model edges; the framework does not parse or constrain its schema.
+Use the model/native D2 or a domain compositor for other organizations.
+
+* Complete visual styles
+| Independent concern | Declaration / owner |
+|---------------------+---------------------|
+| Typography and fonts | =fontFamily= and explicit =fonts= assets |
+| Marks and boundaries | Immutable semantic CSS rules in independent style modules |
+| Stroke and fill | Rough.js mode, seed, roughness and surface treatment |
+| Nodes and connectors | Node/port grammar plus XYFlow's public =getSmoothStepPath= |
+| Palette and spacing | Composable color/spacing layers |
+| Icons | Replaceable SVG assets, separate from theme and language |
+| Diagram organization | Project =layout=/=presentation= and functional composer |
+
+=./presentation-theme= exports =composePresentationTheme=,
+=extendPresentationTheme= and immutable upstream layers. For example:
+
+#+begin_src typescript
+const theme = composePresentationTheme(['graphite', 'node-editor', 'compact']);
+const custom = composePresentationTheme(['sketchbook', 'brand'], {
+ ...presentationThemeLayers,
+ brand: {colors: {accent: '#356B64'}, spacing: {gap: 16}},
+});
+#+end_src
+
+D2 selects equivalent ordered layers and output variants. =sketchbook= uses actual
+Rough.js SVG strokes, hachure fill and embedded Kalam fonts. =node-editor= changes
+node outlines, ports, connectors and canvas treatment. =blueprint=, =field-notes=
+and =graphite= provide palette/typography choices; =compact= adjusts spacing.
+Downstream can supply additional layers, icons and composers without editing a
+global registry. Font resources are declared by the selected layer, not hardcoded
+inside the exporter. Current Case content remains English, matching its sources.
+
+The package imports Rough.js and the framework-independent XYFlow system utility.
+It does not mount a React/Svelte flow editor, pan/zoom runtime or whiteboard.
+Excalidraw is a visual reference; its complete editor, React dependencies and
+scene/bundle adapters are excluded from this static package.
+
+* Export and provenance
+=exportFigure(figure, browser, {scale})= uses the host's Playwright Browser.
+It owns and closes its context, settles actual fonts/images, measures nodes,
+uses XYFlow for selected connectors, and bakes Rough.js geometry into offline HTML.
+Rendered connectors must match captured native model relations, including direction.
+It rejects modified input HTML/drawing/fonts, missing/duplicate endpoints, invalid
+images and detected clipping. The host owns artifact writes and publication.
+
+The result includes PNG, materialized HTML and a rendering receipt: input/composed
+HTML/materialized HTML/PNG fingerprints, actual drawing/routing dependencies,
+font hashes, browser identity, dimensions, scale and region bounds. Captured
+sources, Questions, model, project presentation, style and icons enter the input
+fingerprint. Pixel equality across platforms or browser releases is not claimed.
+These are rendering facts, not issued C, accepted Q, review R or authority.
+
+* Existing adapters
+=renderCatalog= retains official D2-to-SVG rendering with one or two owned workers.
+=./opensysml= consumes the actual optional OpenSysML SDK, retaining model identity
+and origins in its explicit supported projection. Unsupported SDK notation refuses.
+Saved SDK response tests do not establish live service/model validation.
+
+* Case and verification
+=examples/render= consumes the package and reads canonical D2 declarations
+=90.132= and =90.133=. It captures actual Library/Profile/Scenario, =project.sysml=
+and Org sources. Source-excerpt observations remain labeled observations.
+Current =prepare-formation-capture= still reports =capture-blocked / capture = #f=.
+
+The Case exports ten independent figures: architecture/workflow baselines,
+architecture/workflow sketchbook, architecture/workflow node-editor,
+workflow swimlane, architecture dependency matrix and two project-owned resource variants. A separate comparison script
+exports their main diagram regions for visual comparison; full figures remain
+available. No automatic aesthetic approval or model publication is implied.
+
+#+begin_src sh
+bun install --frozen-lockfile
+bunx playwright install chromium
+bun run typecheck
+bun test
+bun run test:export
+# In examples/render:
+bun install --frozen-lockfile
+bun run case
+bun run compare
+#+end_src
+
+Playwright's native runner owns browser tests, with at most two workers. Core
+imports launch no browser. SVG-only D2 consumers need no browser. Generated
+artifacts are in =.cache/render/library-case=; documented candidate snapshots live
+beside the D2 view sources. Package installation and a separate packed consumer
+verify that styles/fonts are included without the editor or workspace dependencies.
+
+* Downstream resource selection and open kinds
+=FigureResources= contains immutable =themeLayers= and =iconPacks=. D2 =icons= is
+an ordered list of selected packs. Each variant may declare a kind, layout,
+theme layers and icon packs. =selectFigureVariant(input, name)= consumes the
+already-resolved variant, preserving custom resources and the same captured
+model/Questions. Unknown/duplicate declarations refuse. No mutable global registry
+or framework edit is needed for the =workbench=/=repo= Case resources.
+
+A kind is an open identifier; standard native profiles are suggestions rather
+than the complete taxonomy. Purpose stays explicit architecture/workflow. Domain
+schemas (such as the current Case's =presentation.flow=) belong in domain modules.
+Rough.js =Options= directly configure stroke/fill; selected surfaces delegate to
+its primitives. Connector sides may be declared through data attributes and use
+XYFlow paths. HTML marker parsing uses parse5; actual visibility uses the browser.
+See [[../../docs/10-19-design/10-architecture/10.142-declarative-render-extensibility-audit.org][10.142 audit]]
+for exact findings, retained defaults, negative controls and responsibility boundaries.
+
+The native D2 theme accepts its full =CompileOptions=, including supplied font
+bytes, and leaves layout unset by default. D2 source configuration or an explicit
+caller override selects the engine. HTML drawing uses Rough.js =Options= rather
+than a second pen configuration schema. Source assertions are now declared in
+D2 =presentation.observations= and observed by the Case, not hardcoded in core.
diff --git a/packages/render/bun.lock b/packages/render/bun.lock
new file mode 100644
index 0000000..7cd6b5e
--- /dev/null
+++ b/packages/render/bun.lock
@@ -0,0 +1,260 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "@deterministic-engineering/render",
+ "dependencies": {
+ "@fontsource/kalam": "5.3.0",
+ "@terrastruct/d2": "0.1.33",
+ "@xyflow/system": "0.0.83",
+ "parse5": "8.0.1",
+ "roughjs": "4.6.6",
+ },
+ "devDependencies": {
+ "@openmbee/opensysml": "0.11.0",
+ "@types/bun": "1.4.4",
+ "fast-check": "4.10.2",
+ "playwright": "1.64.1",
+ "typescript": "7.0.2",
+ },
+ "peerDependencies": {
+ "@openmbee/opensysml": "0.11.0",
+ "playwright": "1.64.1",
+ },
+ "optionalPeers": [
+ "@openmbee/opensysml",
+ "playwright",
+ ],
+ },
+ },
+ "packages": {
+ "@bufbuild/protobuf": ["@bufbuild/protobuf@2.14.0", "", {}, "sha512-C3UGsiCwSprE2NKIIFA3hCDlpXTMCAXRZuEVp88L1GY36Y41+rYL5fryE+nOFhp4p4JPQvdV8PQ4DWgHgeTE+w=="],
+
+ "@connectrpc/connect": ["@connectrpc/connect@2.1.2", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.7.0" } }, "sha512-MXkBijtcX09R10Eb6sFeIetc6w6746eio6xtfuyVOH7oQAacT1X0GzMIQFux6Qy8cq3W/T5qX5Bei8YbFtmRGA=="],
+
+ "@connectrpc/connect-node": ["@connectrpc/connect-node@2.1.2", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.7.0", "@connectrpc/connect": "2.1.2" } }, "sha512-+i/aAOpsI8sIx1mbYp6d99zvxaUSF6t/jP9Ux9maAmjsZPgmIQ3JuIeYi0zJIP9zlCnBlJjkpPosshCgdRuThQ=="],
+
+ "@connectrpc/connect-web": ["@connectrpc/connect-web@2.1.2", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.7.0", "@connectrpc/connect": "2.1.2" } }, "sha512-1tfaK85MU+gJjwwmL31d2rzdf0XCYX99chZf63uG89SGBUd4XuZ4ZzhGo2u79TPXOE6nLIZQ2okrpyey42PYdg=="],
+
+ "@fontsource/kalam": ["@fontsource/kalam@5.3.0", "", {}, "sha512-FDkVoDfPDCSN/eO81FOYtUI67eMgpVBJmjBtAqSL0PmEhJyeFY2biC+CR+yEttksgXR9+swpFdYmQTyjwBq80Q=="],
+
+ "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
+
+ "@npmcli/agent": ["@npmcli/agent@4.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg=="],
+
+ "@npmcli/fs": ["@npmcli/fs@5.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og=="],
+
+ "@npmcli/redact": ["@npmcli/redact@4.0.0", "", {}, "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q=="],
+
+ "@openmbee/opensysml": ["@openmbee/opensysml@0.11.0", "", { "dependencies": { "@bufbuild/protobuf": "2.14.0", "@connectrpc/connect": "2.1.2", "@connectrpc/connect-node": "2.1.2", "@connectrpc/connect-web": "2.1.2" }, "optionalDependencies": { "@openmbee/opensysml-sysml-grpc-darwin-arm64": "0.11.0", "@openmbee/opensysml-sysml-grpc-darwin-x64": "0.11.0", "@openmbee/opensysml-sysml-grpc-linux-arm64": "0.11.0", "@openmbee/opensysml-sysml-grpc-linux-x64": "0.11.0", "@openmbee/opensysml-sysml-grpc-win32-x64": "0.11.0", "@sigstore/bundle": "4.0.0", "@sigstore/protobuf-specs": "0.5.1", "@sigstore/tuf": "4.0.2", "@sigstore/verify": "3.1.1" }, "peerDependencies": { "@openmbee/opensysml-wasm": "0.11.0" }, "optionalPeers": ["@openmbee/opensysml-wasm"], "bin": { "opensysml-generate": "dist/node/generate-cli.js" } }, "sha512-W9VtruoAbxV2RuKuqIVHCdsv5efgNPnCOktLK4M5nNhNrvyckR/AsenX+mmxG+Ry9r0RnNB/UKIcVS3gQHgLbg=="],
+
+ "@openmbee/opensysml-sysml-grpc-darwin-arm64": ["@openmbee/opensysml-sysml-grpc-darwin-arm64@0.11.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-82KnOccVp765a8Yza4TvTnnayVpZlL81jkUyu7dD4o12imeM+UKZE6babI8JQmnL5JdrznNxg6miUO2BoMDS8w=="],
+
+ "@openmbee/opensysml-sysml-grpc-darwin-x64": ["@openmbee/opensysml-sysml-grpc-darwin-x64@0.11.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fp4YBfzUZGDng2bIGHJbFs7edaKlUQXQlzC7d0x4UQ8Yf+v0Z9Bb2DevtfKjW9COcsUlsJcObbUGPwF2I7rdRQ=="],
+
+ "@openmbee/opensysml-sysml-grpc-linux-arm64": ["@openmbee/opensysml-sysml-grpc-linux-arm64@0.11.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-pkv9ptdZy1L6DRgh3rWia/bYdNKaMBR8CSLOPIVwBwhQTpXdfoVcHZ/ZaKZjXx0VOSFxwE/my8wQsf95ygw1fA=="],
+
+ "@openmbee/opensysml-sysml-grpc-linux-x64": ["@openmbee/opensysml-sysml-grpc-linux-x64@0.11.0", "", { "os": "linux", "cpu": "x64" }, "sha512-VTXZF52YiEZFuMBeuYS76PTwKEeuDF4wHdEjHRUbsTCMuVoo8IYDFtkf1+IysZ3PV0efeq67sGVAjLN37DJQpQ=="],
+
+ "@openmbee/opensysml-sysml-grpc-win32-x64": ["@openmbee/opensysml-sysml-grpc-win32-x64@0.11.0", "", { "os": "win32", "cpu": "x64" }, "sha512-1/3fOoozKNouzGHCEnEyDLgioOaynF2Evpn3uw21AZopxQ+pzRA9h5wijtKcP0diz7w+OFa5Zz1E3pF+sOUANA=="],
+
+ "@sigstore/bundle": ["@sigstore/bundle@4.0.0", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A=="],
+
+ "@sigstore/core": ["@sigstore/core@3.2.1", "", {}, "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g=="],
+
+ "@sigstore/protobuf-specs": ["@sigstore/protobuf-specs@0.5.1", "", {}, "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g=="],
+
+ "@sigstore/tuf": ["@sigstore/tuf@4.0.2", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.5.0", "tuf-js": "^4.1.0" } }, "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ=="],
+
+ "@sigstore/verify": ["@sigstore/verify@3.1.1", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA=="],
+
+ "@terrastruct/d2": ["@terrastruct/d2@0.1.33", "", {}, "sha512-eK5hyfGIJFolC7sUsiKvWdY9xGFctTe3d+PSijo09IYDso8psztC+A4SammizXtlwYZpnnW0AtDjfBYauceSeA=="],
+
+ "@tufjs/canonical-json": ["@tufjs/canonical-json@2.0.0", "", {}, "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA=="],
+
+ "@tufjs/models": ["@tufjs/models@4.1.0", "", { "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^10.1.1" } }, "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww=="],
+
+ "@types/bun": ["@types/bun@1.4.4", "", { "dependencies": { "bun-types": "1.4.4" } }, "sha512-9wkRDXUXK124pEnAutE6n/l18j+i9GC5IAHK8TmX/2nlySETmky0OU6yL5U8pW1AoKh++AN9auDCr647BfurjQ=="],
+
+ "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
+
+ "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="],
+
+ "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
+
+ "@types/d3-selection": ["@types/d3-selection@3.0.12", "", {}, "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw=="],
+
+ "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="],
+
+ "@types/d3-zoom": ["@types/d3-zoom@3.0.9", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA=="],
+
+ "@types/node": ["@types/node@26.6.6", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog=="],
+
+ "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
+
+ "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="],
+
+ "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="],
+
+ "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="],
+
+ "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="],
+
+ "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="],
+
+ "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="],
+
+ "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="],
+
+ "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="],
+
+ "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="],
+
+ "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="],
+
+ "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="],
+
+ "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="],
+
+ "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="],
+
+ "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="],
+
+ "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="],
+
+ "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="],
+
+ "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="],
+
+ "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="],
+
+ "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],
+
+ "@xyflow/system": ["@xyflow/system@0.0.83", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-a//eDjez3WZF47aaPTfVUDD0OINOq+WVwBZCRTqG+m0a5euX8EwlJ4jMY1Wxz0Ocj4xOeekJ70V22Pq0ll8RJw=="],
+
+ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
+ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+
+ "brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="],
+
+ "bun-types": ["bun-types@1.4.4", "", { "dependencies": { "@types/node": "*", "undici-types": "*" } }, "sha512-yjrJh5oGX7mMPKGd19YXyIjmEuj4G4zNC30bs7PM9Az7NQ3ndF3j6hbeRdvJTRQEhJ/FYQwW0a2moweEi2hMxQ=="],
+
+ "cacache": ["cacache@20.0.4", "", { "dependencies": { "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", "glob": "^13.0.0", "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^13.0.0" } }, "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA=="],
+
+ "content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
+
+ "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
+
+ "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
+
+ "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="],
+
+ "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
+
+ "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
+
+ "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="],
+
+ "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
+
+ "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="],
+
+ "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "entities": ["entities@8.1.0", "", {}, "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA=="],
+
+ "fast-check": ["fast-check@4.10.2", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-iK2f+YrcmoeGqk6fA0ea2bptcu/itMIm4NfEozq6N25+aG6h7s5HZbB/k1aV7b5w5sFLMCbbtRUsTVR+BgC3xw=="],
+
+ "fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
+
+ "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
+
+ "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
+
+ "http-cache-semantics": ["http-cache-semantics@4.3.0", "", {}, "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA=="],
+
+ "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
+
+ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+ "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="],
+
+ "ip-address": ["ip-address@10.7.5", "", {}, "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA=="],
+
+ "lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="],
+
+ "make-fetch-happen": ["make-fetch-happen@15.0.6", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw=="],
+
+ "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="],
+
+ "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
+
+ "minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="],
+
+ "minipass-fetch": ["minipass-fetch@5.0.2", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^2.0.0", "minizlib": "^3.0.1" }, "optionalDependencies": { "iconv-lite": "^0.7.2" } }, "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ=="],
+
+ "minipass-flush": ["minipass-flush@1.0.7", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA=="],
+
+ "minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="],
+
+ "minipass-sized": ["minipass-sized@2.0.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA=="],
+
+ "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="],
+
+ "p-map": ["p-map@7.1.1", "", {}, "sha512-6Lh3jQPLthYujSrlw6ccWXBGZTkoTCz27Hd92g79LH/kjAbgoEu0t442cCCNbegPSJsA0lwDX8z0bcKtkpA32g=="],
+
+ "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="],
+
+ "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
+
+ "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
+
+ "playwright": ["playwright@1.64.1", "", { "dependencies": { "playwright-core": "1.64.1" }, "bin": { "playwright": "cli.js" } }, "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA=="],
+
+ "playwright-core": ["playwright-core@1.64.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ=="],
+
+ "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
+
+ "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
+
+ "proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
+
+ "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="],
+
+ "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
+
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+ "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
+
+ "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
+
+ "socks": ["socks@2.8.10", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ=="],
+
+ "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
+
+ "ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="],
+
+ "tuf-js": ["tuf-js@4.1.0", "", { "dependencies": { "@tufjs/models": "4.1.0", "debug": "^4.4.3", "make-fetch-happen": "^15.0.1" } }, "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ=="],
+
+ "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
+
+ "undici-types": ["undici-types@8.12.3", "", {}, "sha512-EXE4ydL3S76dFb27UFjNVZHTyv/TLdATj/KUe29ptdcg6qXL48J2tZ3126E5kICj5jn781OR+XlVH1N6Mz8jbA=="],
+
+ "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
+
+ "@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
+
+ "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
+
+ "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
+ }
+}
diff --git a/packages/render/package.json b/packages/render/package.json
new file mode 100644
index 0000000..58d7175
--- /dev/null
+++ b/packages/render/package.json
@@ -0,0 +1,55 @@
+{
+ "name": "@deterministic-engineering/render",
+ "private": false,
+ "type": "module",
+ "scripts": {
+ "typecheck": "tsc --noEmit",
+ "test": "bun test",
+ "test:export": "playwright test"
+ },
+ "dependencies": {
+ "@fontsource/kalam": "5.3.0",
+ "@terrastruct/d2": "0.1.33",
+ "@xyflow/system": "0.0.83",
+ "parse5": "8.0.1",
+ "roughjs": "4.6.6"
+ },
+ "devDependencies": {
+ "@openmbee/opensysml": "0.11.0",
+ "@types/bun": "1.4.4",
+ "fast-check": "4.10.2",
+ "playwright": "1.64.1",
+ "typescript": "7.0.2"
+ },
+ "version": "1.0.0",
+ "exports": {
+ ".": "./src/index.ts",
+ "./themes/*": "./themes/*",
+ "./theme": "./src/theme.ts",
+ "./opensysml": "./src/opensysml.ts",
+ "./presentation-theme": "./src/presentation-theme.ts",
+ "./icons": "./src/icons.ts",
+ "./export": "./src/export.ts"
+ },
+ "files": [
+ "src",
+ "themes",
+ "README.org"
+ ],
+ "license": "LGPL-2.1-only",
+ "engines": {
+ "bun": ">=1.3.11"
+ },
+ "peerDependencies": {
+ "@openmbee/opensysml": "0.11.0",
+ "playwright": "1.64.1"
+ },
+ "peerDependenciesMeta": {
+ "@openmbee/opensysml": {
+ "optional": true
+ },
+ "playwright": {
+ "optional": true
+ }
+ }
+}
diff --git a/packages/render/playwright.config.ts b/packages/render/playwright.config.ts
new file mode 100644
index 0000000..493a6c1
--- /dev/null
+++ b/packages/render/playwright.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from 'playwright/test';
+export default defineConfig({
+ testDir: './test',
+ testMatch: '**/*.browser.ts',
+ workers: 2,
+ fullyParallel: false,
+ reporter: 'line',
+ outputDir: '../../.cache/render/playwright',
+});
diff --git a/packages/render/src/d2-figure.ts b/packages/render/src/d2-figure.ts
new file mode 100644
index 0000000..f0f6089
--- /dev/null
+++ b/packages/render/src/d2-figure.ts
@@ -0,0 +1,63 @@
+import {composeIconPacks, type IconPacks} from './icons';
+import { composePresentationTheme, type ThemeLayers } from './presentation-theme';
+import type { Diagram } from '@terrastruct/d2';
+import type { FigureInput, FigureQuestion } from './figure';
+import {validatePurposeKind, type ViewSelection} from './source/types';
+const object = (value: unknown, name: string): Record => {
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`Expected ${name} object`);
+ return value as Record;
+};
+const text = (value: unknown, name: string): string => {
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`Expected ${name} text`); return value;
+};
+const texts = (value: unknown, name: string): string[] => {
+ if (!Array.isArray(value)) throw new Error(`Expected ${name} array`);
+ return value.map(item => text(item, name));
+};
+/** Official D2 handles syntax/imports. Its standard plugin data owns the declaration. */
+export interface D2FigureData { readonly data: unknown; readonly diagram?: Pick; readonly inputPath: string; readonly fs: Readonly>; }
+export interface FigureResources { readonly themeLayers?: ThemeLayers; readonly iconPacks?: IconPacks; }
+export function readD2Figure(compiled: D2FigureData, captured: Readonly>, resources: FigureResources = {}): FigureInput {
+ const data = compiled.data;
+ const encoded = object(data, 'D2 graph.data').figure;
+ // A native JSON block scalar preserves nested data in the installed D2 SDK.
+ if (typeof encoded !== 'string') throw new Error('D2 vars.d2-config.data.figure must be a JSON block scalar');
+ const declaration = object(JSON.parse(encoded) as unknown, 'figure declaration');
+ const primary = text(declaration.primary, 'primary question');
+ if (!Array.isArray(declaration.questions)) throw new Error('Expected declared questions');
+ const questions: FigureQuestion[] = declaration.questions.map(value => {
+ const question = object(value, 'question');
+ return {ref: text(question.ref, 'question ref'), text: text(question.text, 'question'), focusRefs: texts(question.focusRefs, 'focus refs')};
+ });
+ const question = questions.find(item => item.ref === primary);
+ if (!question) throw new Error('Declared primary question is missing');
+ const purpose = text(declaration.purpose, 'purpose'), kind = text(declaration.kind, 'kind');
+ validatePurposeKind(purpose, kind);
+ const width = declaration.width;
+ if (typeof width !== 'number') throw new Error('Expected declared width');
+ const referenceData = object(declaration.references, 'references');
+ const references = Object.fromEntries(Object.entries(referenceData).map(([key, value]) => [key, text(value, `reference ${key}`)]));
+ const copyData = object(declaration.copy, 'copy');
+ const copy = Object.fromEntries(Object.entries(copyData).map(([key, value]) => [key, text(value, `copy ${key}`)]));
+ const basis = object(declaration.basis, 'basis');
+ const view = {source: compiled.inputPath, title: text(declaration.title, 'title'), purpose, kind, question,
+ basis: {captureRef: text(basis.captureRef, 'capture ref'), modelRefs: texts(basis.modelRefs, 'models'), obligationRefs: texts(basis.obligationRefs, 'obligations'), evidenceRefs: texts(basis.evidenceRefs, 'evidence')}} as ViewSelection;
+ const model = compiled.diagram ? {subjects: compiled.diagram.shapes.map(({id, type}) => ({id, type})), relations: compiled.diagram.connections.map(({id, src, dst, label}) => ({id, src, dst, label}))} : undefined;
+ const theme = composePresentationTheme(declaration.themes === undefined ? ['blueprint'] : texts(declaration.themes, 'theme layers'), resources.themeLayers);
+ const icons = composeIconPacks(declaration.icons === undefined ? ['engineering'] : texts(declaration.icons, 'icon packs'), resources.iconPacks);
+ const variants = declaration.variants === undefined ? [] : (() => {
+ if (!Array.isArray(declaration.variants)) throw new Error('Expected declared variants');
+ const result = declaration.variants.map(value => {
+ const variant = object(value, 'variant');
+ const variantKind = variant.kind === undefined ? undefined : text(variant.kind, 'variant kind');
+ if (variantKind !== undefined) validatePurposeKind(purpose, variantKind);
+ return {name: text(variant.name, 'variant name'), kind: variantKind, layout: variant.layout === undefined ? undefined : text(variant.layout, 'layout'),
+ theme: variant.themes === undefined ? theme : composePresentationTheme(texts(variant.themes, 'variant themes'), resources.themeLayers),
+ icons: variant.icons === undefined ? icons : composeIconPacks(texts(variant.icons, 'variant icon packs'), resources.iconPacks)};
+ });
+ if (new Set(result.map(variant => variant.name)).size !== result.length) throw new Error('Duplicate declared figure variant');
+ return result;
+ })();
+ return {view, model, theme, variants, icons, presentation: declaration.presentation, layout: declaration.layout === undefined ? undefined : text(declaration.layout, 'layout'), language: text(declaration.language, 'language'), width, questions, references, copy,
+ sources: {...captured, ...compiled.fs}};
+}
diff --git a/packages/render/src/export.ts b/packages/render/src/export.ts
new file mode 100644
index 0000000..c4f1561
--- /dev/null
+++ b/packages/render/src/export.ts
@@ -0,0 +1,162 @@
+import {getSmoothStepPath, Position} from '@xyflow/system';
+import rough from 'roughjs';
+import packageInfo from '../package.json' with {type: 'json'};
+import { readFile } from 'node:fs/promises';
+import type { Browser } from 'playwright';
+import type { ComposedFigure } from './figure';
+import { digest } from './source/catalog';
+export interface FigureExportOptions { readonly scale?: number; readonly regions?: readonly string[]; }
+export interface FigureImage {
+ readonly png: Uint8Array;
+ readonly regionImages: Readonly>;
+ /** Fully materialized, offline HTML, including embedded fonts and measured sketch strokes. */
+ readonly html: string;
+ readonly receipt: {
+ readonly inputSHA256: string; readonly composedHTMLSHA256: string; readonly htmlSHA256: string; readonly pngSHA256: string;
+ readonly drawing: string; readonly routing: string; readonly dependencies: Readonly>; readonly assetSHA256: Readonly>;
+ readonly regionSHA256: Readonly>;
+ readonly browser: string; readonly width: number; readonly height: number; readonly scale: number;
+ readonly bounds: Readonly>;
+ };
+}
+/** Host owns the official browser; this effect owns and closes just its context. */
+export async function exportFigure(figure: ComposedFigure, browser: Browser, options: FigureExportOptions = {}): Promise {
+ if (digest(figure.html) !== figure.htmlSHA256) throw new Error('Figure HTML differs from its captured fingerprint');
+ const scale = options.scale ?? 1;
+ if (!Number.isFinite(scale) || scale < 0.5 || scale > 4) throw new Error('Invalid export scale');
+ const context = await browser.newContext({viewport: {width: figure.width, height: 900}, deviceScaleFactor: scale, reducedMotion: 'reduce'});
+ try {
+ const page = await context.newPage();
+ page.setDefaultTimeout(15000);
+ await page.setContent(figure.html, {waitUntil: 'load', timeout: 15000});
+ const provenance = await page.evaluate(() => { const value: unknown = JSON.parse(document.getElementById('figure-provenance')!.textContent!); if (!value || typeof value !== 'object' || !('drawing' in value) || !('fonts' in value)) throw new Error('Invalid rendering provenance'); return {drawing: value.drawing, fonts: value.fonts}; });
+ if (JSON.stringify(provenance.drawing) !== JSON.stringify(figure.drawing)) throw new Error('Drawing differs from captured HTML');
+ if (JSON.stringify(provenance.fonts) !== JSON.stringify(figure.fonts)) throw new Error('Fonts differ from captured HTML');
+ const assetSHA256: Record = {};
+ const faces = await Promise.all(figure.fonts.map(async ({family, weight, source}) => {
+ if (!/^[a-zA-Z0-9 -]+$/.test(family) || !Number.isInteger(weight) || weight < 100 || weight > 900) throw new Error('Invalid declared font');
+ const font = await readFile(new URL(import.meta.resolve(source)));
+ assetSHA256[source] = digest(font);
+ return `@font-face{font-family:"${family}";font-style:normal;font-weight:${weight};src:url(data:font/woff2;base64,${font.toString('base64')}) format('woff2')}`;
+ }));
+ if (faces.length) await page.addStyleTag({content: faces.join('\n')});
+ // Settle actual text layout before waiting on its FontFaceSet.
+ await page.locator('[data-figure]').boundingBox();
+ await page.evaluate(async () => {
+ await document.fonts.ready;
+ for (const image of document.images) {
+ if (!image.complete || !image.naturalWidth) throw new Error('Figure contains an unloaded or invalid image');
+ await image.decode();
+ }
+ });
+ // Route declared connectors against actual font-settled node boxes, then bake to SVG.
+ const connectors = await page.evaluate(() => {
+ type Side = 'top' | 'bottom' | 'left' | 'right';
+ const result: {index: number; sourceX: number; sourceY: number; targetX: number; targetY: number; sourceSide: Side; targetSide: Side}[] = [];
+ const side = (value: string | undefined, fallback: Side): Side => {
+ if (value === undefined) return fallback;
+ if (value !== 'top' && value !== 'bottom' && value !== 'left' && value !== 'right') throw new Error('Invalid declared connector position');
+ return value;
+ };
+ const anchor = (rect: DOMRect, position: Side, distance: number) => ({
+ x: position === 'left' ? rect.left - distance : position === 'right' ? rect.right + distance : rect.left + rect.width / 2,
+ y: position === 'top' ? rect.top - distance : position === 'bottom' ? rect.bottom + distance : rect.top + rect.height / 2,
+ });
+ for (const svg of document.querySelectorAll('svg[data-connections]')) {
+ const captured: {model?: {relations: {src: string; dst: string}[]}} = JSON.parse(document.getElementById('figure-provenance')!.textContent!);
+ const canvas = svg.parentElement!, root = canvas.getBoundingClientRect();
+ const ns = 'http://www.w3.org/2000/svg', defs = document.createElementNS(ns, 'defs');
+ const marker = document.createElementNS(ns, 'marker');
+ const id = `arrow-${Array.from(document.querySelectorAll('svg[data-connections]')).indexOf(svg)}`;
+ marker.setAttribute('id', id); marker.setAttribute('viewBox', '0 0 10 10'); marker.setAttribute('refX', '9'); marker.setAttribute('refY', '5'); marker.setAttribute('markerWidth', '5'); marker.setAttribute('markerHeight', '5'); marker.setAttribute('orient', 'auto-start-reverse');
+ const arrow = document.createElementNS(ns, 'path'); arrow.setAttribute('d', 'M 1 1 L 9 5 L 1 9'); arrow.setAttribute('style', 'stroke:var(--accent);fill:none'); marker.append(arrow); defs.append(marker); svg.prepend(defs);
+ for (const path of svg.querySelectorAll('path[data-from][data-to]')) {
+ const find = (id: string | undefined) => Array.from(canvas.querySelectorAll('[data-node]')).filter(node => node.dataset.node === id);
+ const sources = find(path.dataset.from), targets = find(path.dataset.to);
+ if (sources.length !== 1 || targets.length !== 1) throw new Error('Connector needs unique rendered endpoints');
+ if (!captured.model?.relations.some(edge => edge.src === path.dataset.from && edge.dst === path.dataset.to)) throw new Error('Undeclared rendered model relation');
+ const a = sources[0]!.getBoundingClientRect(), b = targets[0]!.getBoundingClientRect();
+ const backwards = b.right < a.left;
+ path.dataset.connectionIndex = String(result.length);
+ const sourceSide = side(path.dataset.sourcePosition, backwards ? 'left' : 'right'), targetSide = side(path.dataset.targetPosition, backwards ? 'right' : 'left');
+ const start = anchor(a, sourceSide, 2), end = anchor(b, targetSide, 3);
+ result.push({index: result.length, sourceX: start.x - root.left, sourceY: start.y - root.top, targetX: end.x - root.left, targetY: end.y - root.top, sourceSide, targetSide});
+ path.setAttribute('marker-end', `url(#${id})`);
+ }
+ }
+ return result;
+ });
+ const positions = {top: Position.Top, bottom: Position.Bottom, left: Position.Left, right: Position.Right};
+ const routes = connectors.map(({index, sourceSide, targetSide, ...coordinates}) => ({index, path: getSmoothStepPath({...coordinates,
+ sourcePosition: positions[sourceSide], targetPosition: positions[targetSide], borderRadius: 7, offset: 8})[0]}));
+ await page.evaluate(routes => { for (const route of routes) document.querySelector(`[data-connection-index="${route.index}"]`)!.setAttribute('d', route.path); }, routes);
+ if (figure.drawing.mode === 'sketch') {
+ const surfaces = await page.locator('[data-surface]').evaluateAll(elements => elements.map(element => {
+ const rect = element.getBoundingClientRect(), style = getComputedStyle(element);
+ return {shape: element.getAttribute('data-surface') || 'rectangle', width: rect.width, height: rect.height, stroke: style.getPropertyValue('--accent').trim(), fill: style.getPropertyValue('--tint').trim()};
+ }));
+ const generator = rough.generator();
+ const sketches = surfaces.map((surface, index) => {
+ const options = {stroke: surface.stroke, fill: surface.fill, ...figure.drawing.options, seed: figure.drawing.options.seed + index};
+ const width = surface.width, height = surface.height;
+ const drawable = surface.shape === 'rectangle' ? generator.rectangle(2, 2, width - 4, height - 4, options)
+ : surface.shape === 'ellipse' ? generator.ellipse(width / 2, height / 2, width - 4, height - 4, options)
+ : surface.shape === 'diamond' ? generator.polygon([[width / 2, 2], [width - 2, height / 2], [width / 2, height - 2], [2, height / 2]], options)
+ : undefined;
+ if (!drawable) throw new Error(`Unsupported declared sketch surface: ${surface.shape}`);
+ const paths = generator.toPaths(drawable);
+ return {width: surface.width, height: surface.height, paths};
+ });
+ await page.locator('[data-surface]').evaluateAll((elements, sketches) => {
+ const ns = 'http://www.w3.org/2000/svg';
+ elements.forEach((element, index) => {
+ const sketch = sketches[index]!, svg = document.createElementNS(ns, 'svg');
+ svg.setAttribute('class', 'sketch-surface'); svg.setAttribute('aria-hidden', 'true');
+ svg.setAttribute('width', String(sketch.width)); svg.setAttribute('height', String(sketch.height));
+ svg.setAttribute('viewBox', `0 0 ${sketch.width} ${sketch.height}`);
+ for (const path of sketch.paths) {
+ const node = document.createElementNS(ns, 'path');
+ node.setAttribute('d', path.d); node.setAttribute('stroke', path.stroke); node.setAttribute('stroke-width', String(path.strokeWidth)); node.setAttribute('fill', path.fill || 'none');
+ svg.append(node);
+ }
+ element.prepend(svg);
+ });
+ }, sketches);
+ }
+ const report = await page.evaluate(() => {
+ const figure = document.querySelector('[data-figure]')!;
+ const root = figure.getBoundingClientRect();
+ const bounds: Record = {};
+ const issues: string[] = [];
+ const captured: {questions: {ref: string}[]} = JSON.parse(document.getElementById('figure-provenance')!.textContent!);
+ for (const question of captured.questions) {
+ const shown = Array.from(figure.querySelectorAll('[data-question]')).some(element => element.dataset.question === question.ref && element.checkVisibility({opacityProperty: true, visibilityProperty: true}) && element.getBoundingClientRect().width > 0 && element.getBoundingClientRect().height > 0);
+ if (!shown) issues.push(`Question has no visible region: ${question.ref}`);
+ }
+
+ for (const element of figure.querySelectorAll('[data-region]')) {
+ const rect = element.getBoundingClientRect();
+ const name = element.dataset.region!;
+ if (name in bounds) issues.push(`duplicate region: ${name}`);
+ bounds[name] = {x: rect.x - root.x, y: rect.y - root.y, width: rect.width, height: rect.height};
+ if (rect.left < root.left - 1 || rect.right > root.right + 1 || rect.bottom > root.bottom + 1) issues.push(`out of figure: ${name}`);
+ }
+ for (const element of figure.querySelectorAll('[data-fit]')) {
+ if (element.scrollWidth > element.clientWidth + 1 || element.scrollHeight > element.clientHeight + 1) issues.push(`clipped text: ${element.textContent?.slice(0, 60)}`);
+ }
+ if (figure.scrollWidth > figure.clientWidth + 1) issues.push('figure horizontal overflow');
+ return {bounds, issues};
+ });
+ if (report.issues.length) throw new Error(report.issues.join('; '));
+ const html = await page.content();
+ const png = await page.locator('[data-figure]').screenshot({type: 'png', animations: 'disabled'});
+ const regionImages: Record = {};
+ for (const name of options.regions ?? []) {
+ if (!/^[a-zA-Z0-9_-]+$/.test(name) || !(name in report.bounds)) throw new Error(`Unknown requested figure region: ${name}`);
+ regionImages[name] = await page.locator(`[data-region="${name}"]`).screenshot({type: 'png', animations: 'disabled'});
+ }
+ const buffer = Buffer.from(png);
+ return {png, html, regionImages, receipt: {inputSHA256: figure.inputSHA256, composedHTMLSHA256: figure.htmlSHA256, dependencies: packageInfo.dependencies, regionSHA256: Object.fromEntries(Object.entries(regionImages).map(([name, bytes]) => [name, digest(bytes)])), htmlSHA256: digest(html), drawing: figure.drawing.mode === "sketch" ? `roughjs@${packageInfo.dependencies.roughjs}` : "precise", routing: connectors.length ? `xyflow@${packageInfo.dependencies["@xyflow/system"]}` : "none", assetSHA256, pngSHA256: digest(png), browser: browser.version(),
+ width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20), scale, bounds: report.bounds}};
+ } finally { await context.close(); }
+}
diff --git a/packages/render/src/figure.ts b/packages/render/src/figure.ts
new file mode 100644
index 0000000..59f0a9d
--- /dev/null
+++ b/packages/render/src/figure.ts
@@ -0,0 +1,141 @@
+import {parseFragment, type DefaultTreeAdapterTypes} from 'parse5';
+import type { Shape, Connection } from '@terrastruct/d2';
+import packageInfo from '../package.json' with {type: 'json'};
+import { digest, relativeSource } from './source/catalog';
+import { validatePurposeKind, type QuestionRef, type ViewSelection } from './source/types';
+import { defaultIcons, type IconAsset } from './icons';
+import { defaultPresentationTheme, type PresentationTheme } from './presentation-theme';
+
+export interface FigureQuestion extends QuestionRef { readonly focusRefs: readonly string[]; }
+export interface FigureModel {
+ readonly subjects: readonly Readonly>[];
+ readonly relations: readonly Readonly>[];
+}
+export interface FigureInput {
+ /** Project-owned declaration, interpreted by its functional composition. */
+ readonly presentation?: unknown;
+ readonly layout?: string;
+ readonly view: ViewSelection;
+ readonly language: string;
+ readonly width: number;
+ readonly questions: readonly FigureQuestion[];
+ /** Captured project/model/composition sources; no new model or evidence ledger. */
+ readonly sources: Readonly>;
+ readonly references: Readonly>;
+ readonly variants?: readonly {readonly name: string; readonly theme: PresentationTheme; readonly kind?: string; readonly layout?: string; readonly icons?: Readonly>}[];
+ readonly model?: FigureModel;
+ readonly copy?: Readonly>;
+ readonly theme?: PresentationTheme;
+ readonly icons?: Readonly>;
+}
+export interface FigureContext {
+ readonly presentation?: unknown;
+ readonly model?: FigureModel;
+ readonly layout: string;
+ readonly view: ViewSelection;
+ readonly question: (ref: string) => FigureQuestion;
+ readonly reference: (ref: string) => string;
+ readonly icon: (name: string) => string;
+ readonly text: (value: string) => string;
+ readonly copy: (key: string) => string;
+ readonly relation: (from: string, to: string) => Readonly>;
+}
+export interface FigureComposition { readonly markup: string; readonly css: string; }
+export type FigureComposer = (context: FigureContext) => FigureComposition;
+export interface ComposedFigure {
+ readonly html: string;
+ readonly drawing: PresentationTheme["drawing"];
+ readonly fonts: PresentationTheme["fonts"];
+ readonly inputSHA256: string;
+ readonly htmlSHA256: string;
+ readonly width: number;
+ readonly purpose: ViewSelection['purpose'];
+ readonly sourceSHA256: Readonly>;
+}
+export const escapeHTML = (value: string): string => value.replace(/[&<>"']/g, character => ({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[character]!));
+/** Capture first, then compose. No fixed page structure or automatic graph enlargement. */
+export function composeFigure(input: FigureInput, compose: FigureComposer): ComposedFigure {
+ const snapshot = structuredClone(input);
+ if (!Number.isInteger(snapshot.width) || snapshot.width < 320 || snapshot.width > 4096) throw new Error('Invalid figure width');
+ if (!snapshot.questions.length || !snapshot.questions.some(question => question.ref === snapshot.view.question.ref)) throw new Error('Primary question is missing');
+ validatePurposeKind(snapshot.view.purpose, snapshot.view.kind);
+ if (!snapshot.view.basis.captureRef.trim()) throw new Error('Existing capture or labeled candidate reference is required');
+ const sources = Object.fromEntries(Object.entries(snapshot.sources).sort(([a], [b]) => a.localeCompare(b)));
+ if (!Object.hasOwn(sources, snapshot.view.source)) throw new Error('Composition source must be captured');
+ for (const [name, source] of Object.entries(sources)) {
+ relativeSource(name);
+ if (typeof source !== 'string') throw new Error('Invalid captured source');
+ }
+ const references = Object.fromEntries(Object.entries(snapshot.references).sort(([a], [b]) => a.localeCompare(b)));
+ for (const source of Object.values(references)) if (!Object.hasOwn(sources, source)) throw new Error(`Uncaptured reference source: ${source}`);
+ const questions = new Map(snapshot.questions.map(question => [question.ref, question]));
+ if (questions.size !== snapshot.questions.length) throw new Error('Duplicate question');
+ for (const question of questions.values()) {
+ if (!question.ref.trim() || !question.text.trim() || !question.focusRefs.length) throw new Error('Question needs wording and model/source focus');
+ for (const ref of question.focusRefs) if (!Object.hasOwn(references, ref)) throw new Error(`Unknown question focus: ${ref}`);
+ Object.freeze(question.focusRefs); Object.freeze(question);
+ }
+ const theme = snapshot.theme ?? defaultPresentationTheme;
+ const icons = Object.fromEntries(Object.entries(snapshot.icons ?? defaultIcons).sort(([a], [b]) => a.localeCompare(b)));
+ const question = (ref: string): FigureQuestion => {
+ const value = questions.get(ref); if (!value) throw new Error(`Unselected question: ${ref}`); return value;
+ };
+ const reference = (ref: string): string => {
+ if (!Object.hasOwn(references, ref)) throw new Error(`Unselected reference: ${ref}`);
+ return `data-ref="${escapeHTML(ref)}"`;
+ };
+ const icon = (name: string): string => {
+ const value = icons[name as keyof typeof icons]; if (!Object.hasOwn(icons, name) || !value) throw new Error(`Unselected icon: ${name}`);
+ return `${value.svg}`;
+ };
+ const view = Object.freeze({...snapshot.view, question: Object.freeze({...snapshot.view.question}), basis: Object.freeze({...snapshot.view.basis,
+ modelRefs: Object.freeze([...snapshot.view.basis.modelRefs]), obligationRefs: Object.freeze([...snapshot.view.basis.obligationRefs]), evidenceRefs: Object.freeze([...snapshot.view.basis.evidenceRefs])})});
+ const copy = (key: string): string => {
+ const value = snapshot.copy?.[key]; if (typeof value !== 'string') throw new Error(`Undeclared copy: ${key}`); return value;
+ };
+ const relation = (from: string, to: string) => {
+ const found = snapshot.model?.relations.find(edge => edge.src === from && edge.dst === to);
+ if (!found) throw new Error(`Undeclared model relation: ${from} -> ${to}`); return Object.freeze({...found});
+ };
+ if (!['precise', 'sketch'].includes(theme.drawing.mode)) throw new Error('Unknown drawing mode');
+ const {seed, roughness} = theme.drawing.options;
+ if (theme.drawing.mode === 'sketch' && (!Number.isInteger(seed) || seed <= 0 || (roughness !== undefined && (!Number.isFinite(roughness) || roughness < 0)))) throw new Error('Invalid sketch drawing parameters');
+ // Freeze captured model and project declarations before calling a domain compositor.
+ const freeze = (value: unknown, seen = new WeakSet