Live Quil UML class-diagram viewer driven by EDN
Clojure
106
30 commits
updated Sep 18, 2026
A live Quil app that lays out and draws UML from an EDN IR. A policy plus a language-specific parser write the topology; this tool displays it, routes the arrows, colors CRAP, and lets you click.
The IR is topology: the namespace tree, classes, and edges. Metrics
(CC, coverage, CRAP, killed/survived) come from .metrics/ snapshots produced
by crap4clj and
clj-mutate. The viewer overlays those
files at load, keyed by namespace + function name. Agents edit the policy, not
the IR. See Policy.
Needs Clojure CLI and Java 21+.
clj -M:ir # policy → examples/uml-viewer.edn
clj -M:run
clj -M:run examples/library.edn
clj -M:run examples/uml-viewer.edn
clj -M:run --help
A tmux session uml-viewer-grok starts interactive Grok in the
examined project's directory (--yolo --trust --rules … plus a launch
prompt). On start it writes a hierarchical policy from that project's
namespaces and regenerates the IR. Type there; Esc is the real TUI interrupt.
Closing the diagram kills that tmux session (and the Terminal attach). That
instance — not every Grok in this repo — also runs clj -M:crap,
clj -M:mutate, and IR generate after later changes. Project-wide rules live
in .grok/rules/uml-viewer.md.
The examined project (and this one) must expose two aliases:
| Alias | Who | What |
|---|---|---|
:uml-viewer | anyone | Fresh window. Starts the companion. Waits for :display. |
:uml-viewer-restart | associated agent only | New JVM, same companion. Loads the EDN immediately. |
Do not pass --restart (or use :uml-viewer-restart) unless you are that
companion recycling the window after source changes. A stray --restart
skips spawning Grok and leaves a diagram with no agent. The companion
recycles the window by writing :quit-for-restart to
.uml-viewer/to-viewer.edn, waiting for the JVM to exit, then
clj -M:uml-viewer-restart. Do not SIGKILL. Closing the window still kills
Grok.
On a fresh start the canvas stays blank until the companion sends :display,
with Waiting for agent to create diagram. R reloads the current EDN
immediately and does not wait. A missing or unreadable file prints
UML viewer: file not found: … in the inspector instead of throwing.
clj -M:spec
clj -M:cov
clj -M:ir # writes examples/uml-viewer.edn
clj -M:crap # writes .metrics/crap.edn
clj -M:mutate src/uml_viewer/engine/layout.clj
This project's :crap alias uses ../clojure/crap4clj. :mutate pins
clj-mutate by git SHA. Commit
.metrics/ so a clone has numbers without re-running those tools.
Rename or move of a function is a new form: overlay does not match old names.
Layer and component mean the same thing: a namespace grouping (the first segment after the prefix, or a named proposal group).
from -> to it bundles, in any
declutter mode. Violating pairs are red.:ns). Click it to open that source
file at the top. Hover a member to highlight it; click it to open the same
file at the defn. See Source extractors.+ public and - private. defn- is not drawn on
the class box.R reloads the current EDN (the watcher also reloads on save). Overlay
re-reads .metrics/ on the next load.Esc on the class card closes it. Closing the main window exits the app.This project's diagram is generated. Do not edit examples/uml-viewer.edn.
Edit examples/uml-viewer.policy.edn, then run clj -M:ir (or press Regen).
The parser (LanguageGraph) reads source and emits facts: one class per
project namespace, :require / :use of another project ns as
:dependency, requiring-resolve of a quoted var as :dependency on that
var's namespace, defprotocol as :stereotype :interface, defrecord /
deftype of a protocol as :implements. External :requires and :imports
become foreign classes. Members are not authored — overlay fills them from
.metrics/.
The tree is the namespaces. After :prefix, every . is a nesting
level. uml-viewer.engine.layout is a child of engine.
uml-viewer.clojure-language.source-clojure is a child of clojure-language.
The policy does not assign nses to invented packages. If you want Domain /
Engine / Adapters boxes in the source tree, those segments must exist as
namespaces. To view a grouping that is not in the code, use :proposal
(see Proposed components) — do not rewrite namespaces.
To write a policy for a project:
:src and :prefix to the project's source root and ns prefix
(src and foo for foo.bar.baz).:hierarchical true (or omit :packages and :diagrams).:order — the first dotted part after
the prefix, in the order you want the boxes. Do not invent names.:foreign if they should appear as ovals.:edge-kinds {[:from :to] :association}
using the leaf ids (clojure-language.source-clojure, not
clojure-language).:levels so the generator can mark dependency-rule violations
(see Dependency rule).:proposal to name design components that are not namespaces
(see Proposed components).clj -M:ir (or Regen).If foo.bar and foo.bar.baz both exist, the bar box lists bar (the
module) and baz (the child). Double-click the component to open that
level; double-click the bar module line for its class card.
Wrong (invented partitions):
:packages [{:id :domain :nses [ir geom source]}
{:id :engine :nses [layout route]}]
Right (the ns tree):
{:title "UML viewer"
:src "src"
:prefix "uml-viewer"
:lang :clojure
:out "examples/uml-viewer.edn"
:hierarchical true
:foreign [quil]
:order [main adapters application engine source graph clojure-language domain]
:levels [[domain source graph clojure-language]
[engine]
[application]
[adapters]
[main]]
:edge-kinds {[:engine.compose :engine.layout] :association}}
| Key | Role |
|---|---|
:prefix | Strip this from each ns; remaining dots are the tree |
:hierarchical | Namespace tree (default when :packages is omitted) |
:order | Order of existing top-level ns segments, not new component names |
:levels | Groups of those segments, inner (higher-level) first. Same group = same rank |
:proposals | Named groupings of real segments; not instantiated in source. Inspector list; P returns to the ns tree |
:edge-kinds | Override parser kind for [from to] (usually :association) |
:omit-edges | Drop [from to] |
:lang | Which LanguageGraph to use (default :clojure) |
:foreign | External libs as ovals. A listed prefix collapses quil.core to quil. |
Viewer Grok loop (passed with --rules to the companion session only)
On launch: from the examined directory, write or update the hierarchical policy and regenerate the IR, then wait.
After every later source or policy change: clj -M:crap, clj -M:mutate
on the changed src/ files, then clj -M:ir. Uncovered mutants remaining are
coverage gaps; keep the snapshot and do not re-run the file or force a full
mutation because mutate exited non-zero.
clj -M:ir. Put a new
top-level segment in :order if you care about box order.:edge-kinds entry.:foreign.:packages to fake Clean Architecture components. Use
:proposals to view a grouping that is not in the code.:proposals when rewriting policy. Do not invent them on launch.
If instructed, add a named proposal (default name is a timestamp).Hand-written sample IRs (e.g. examples/library.edn) are still valid; they
are not generated.
A :dependency edge is violating when it runs from a higher-level
(inner) component to a lower-level (outer) one. That is the Clean
Architecture dependency rule: source-code dependencies point inward.
Evaluation is deterministic given :levels:
engine.layout → engine).:levels. Rank is the group's index; smaller
is inner / higher-level.from-rank < to-rank, the edge is
:violating true. Same rank is allowed. :implements and
:association are never violating. Foreign / unranked ends are not
compared.:association clears it.:order is visual box order, not rank. Nesting is not layering: you cannot
infer inner vs outer from the namespace tree alone, so :levels must group
segments that sit at the same architectural level (e.g. domain, source,
and graph). Omit :levels and nothing is marked. If :levels is omitted
and :proposals is set, rank follows the first proposal's component order.
:proposals is a list of named groupings of existing top-level segments.
Those names are not namespaces. Each item is {:id :name :layers [...]}
(:layers here are named components). The as-is diagram stays the ns tree.
The inspector lists the real diagram (the namespace tree) just above
Proposals; click it to return to the tree. Click a proposal to show it
(canvas marked PROPOSAL — not instantiated in code). New Proposal
adds an empty proposal named with a timestamp. Right-click a name to rename
or delete it. Double-click a ns box to drill the real tree.
The Declutter button cycles Declutter arrows (one arrow per component pair per direction) → Declutter elements (also hide nested names, members, and ports) → Declutter classes (also hide classes inside components) → Declutter none.
When a proposal is shown, rank follows that proposal's component order
and violating arrows are re-evaluated. The real diagram uses :levels.
Class boxes show the current view's rank (innermost 0)
at the upper left; the class card repeats Level n. Level 0 is drawn at
the bottom. Good arrows (outer → inner) point down; violating arrows
(inner → outer) point up and stay red. When arrows are collapsed, selecting a class highlights
the component arrows it belongs to. Collapsed components keep their color
and C/M dots; double-click still opens a component.
:proposals [{:id :ccp
:name "2026-09-18 10:30:00"
:layers [{:id :playfield :label "Playfield"
:nses [entities world missiles cities batteries flyers]}
{:id :hosts :label "Hosts" :nses [jvm browser]}]}]
Companion Grok must not invent :proposals on launch and must keep them when
updating :order. If instructed, add a named proposal and regenerate the IR.
The viewer draws a violating arrow red, and bold red when a selected
element highlights it. Hand-written IR may set :violating true directly.
The viewer and the companion Grok talk through .uml-viewer/ in the examined
project (gitignored). The file is the mail; tmux is only a doorbell.
| File | Direction |
|---|---|
.uml-viewer/to-viewer.edn | Grok → viewer |
.uml-viewer/to-agent.edn | viewer → Grok |
Commands are {:id n :op …} with a rising :id. Writes are tmp-then-rename.
:op | Meaning |
|---|---|
:display | Viewer loads :path (relative to the project root) |
:regen | Grok rewrites hierarchical policy, regenerates IR, then :display |
:quit-for-restart | Viewer exits the JVM without killing Grok. The associated agent then runs clj -M:uml-viewer-restart. |
Regen in the inspector queues :regen and wakes Grok with literal text, a
150ms pause, C-m, 50ms, then C-j (same timing as SwarmForge). The wake-up
does not contain the command. If Grok is busy, it finishes first, then reads
the mailbox. If tmux is missing, the button still writes the file and the
inspector says the session is not attached.
Generating the IR asks uml-viewer.graph to scan a source tree. :lang
selects the scanner (default :clojure). Register another implementation
with (graph/register! :java my-java-scanner). The scanner must satisfy
LanguageGraph:
| method | role |
|---|---|
scan | from a root directory and {:prefix …}, return {:classes :edges} |
Classes are {:id :name :ns :stereotype}. Edges are {:from :to :kind}
(:dependency or :implements). The policy layer is language-neutral.
Clojure (uml-viewer.clojure-language.graph-clojure) is the only
implementation today: it reads ns forms (including prefix lists),
requiring-resolve of a quoted var (including nested calls), defprotocol,
defrecord, and deftype. Java or C need a different parser; do not
special-case languages in policy or ir-generator. Main constructs the
implementation and passes it in.
A hierarchical policy writes one EDN document of all classes and edges
(:hierarchical true). The viewer builds each screen from the namespace tree
at the current drill level. A hand-written IR with :packages (or
:diagrams) is still a static diagram, e.g. examples/library.edn.
Metrics on the class card do not have to be authored. If .metrics/ is
present, the overlay fills CC, coverage, CRAP, killed/survived, and any
functions found in the snapshots (including privates). Authored :crap /
:coverage / :ops are the fallback when no snapshot exists.
Overlay keys snapshots by class :ns (the real source namespace). The
generator writes :ns from the scanned ns. Hand-written IR must set :ns
the same way; there is no project-specific fallback.
{:title "Lending library"
:direction :tb
:packages
[{:id :domain
:label "Domain"
:classes
[{:id :book
:name "Book"
:stereotype :class ;; optional: :interface :enumeration :abstract
:fields [{:name "isbn" :type "String"}]
:ops [{:name "find" :args ["isbn"] :returns "Book"}]}]}
{:id :app
:label "Application"
:classes
[{:id :repo
:name "CatalogRepo"
:stereotype :interface
:ops [{:name "get" :args ["isbn"] :returns "Book"}]}]}]
:edges
[{:from :sql-repo :to :repo :kind :implements}
{:from :loan :to :book :kind :association :label "borrows"}]}
Optional authored metrics, used when snapshots are missing:
:crap — a number (μ) or {:mu :max :sigma}:coverage — ratio 0–1 on a class or op:cc, :killed, :survived, :private on ops:hide-members true — compact boxPackage and class color maps CRAP (μ + σ) and mutation score each onto
1–10 using uml-viewer.domain.config cutoffs, averages them, and paints a
0–10 red–green fill. Missing CRAP or mutation data counts as red (grade 1),
not unknown. Parents take the worst CRAP and worst mutation of their
children, and a child with no data is the worst. A C and M dot in
the upper-right show the two scores. The boxes no longer print μ / max / σ.
On the class card, a Crap μ … max … σ … line sits above the table (max is
the worst function in the namespace, not a sum). Column groups are labeled
--crap-- (Crap, CC, Cov) and --mutation-- (killed, survived). The class
row shows average CRAP with a μ suffix and omits CC. Killed and survived
use the same mutation-grade colors as the M dot.
Edge :kind values:
| kind | line | head |
|---|---|---|
:inheritance | solid grey | empty triangle |
:implements | solid grey | empty triangle |
:association | solid grey | open arrow |
:dependency | solid grey | open arrow |
:aggregation | solid grey | empty diamond |
:composition | solid grey | filled diamond |
Layout follows Mermaid's three stages:
curveBasis
cubics. All arrows are solid grey. Paths that pass the target and reverse
are rejected.Clicking a member asks uml-viewer.source for the whole file and a
start line. The IR (and the class card) only supply an identity
map; a language-specific extractor turns that into
{:title :file :body :line}. The source window opens on that file and
scrolls to the member (highlighted). Clicking the module name opens the same
file at the top (:line omitted).
(source/member-source {:lang :clojure
:ns "uml-viewer.engine.layout"
:name "layout"})
:lang selects the extractor (default :clojure). Register another
implementation with (source/register! :java my-java-extractor). The
extractor must satisfy LanguageSource:
| method | role |
|---|---|
locate | path to the file that should contain the member |
extract | slice that member out of the file text |
title | window title |
Clojure (uml-viewer.clojure-language.source-clojure) is the only
implementation today: it maps :ns to src/...clj (or .cljc / .cljs)
and finds the top-level (defn name …) / (defn- name …) so the window can
jump to that line. That locate/line step is not enough for Java or C — those
need a parser or language server, and a richer identity (:class,
:signature, :file). The protocol is the seam; do not special-case
languages in the class card. Main constructs the extractor and passes it to
Core.
Quil stays in adapters.draw and adapters.sketch. The rest of the engine
does not depend on Processing.
30 commits
Clojure
100.0%
Live Quil UML class-diagram viewer driven by EDN
Clojure
106
30 commits
updated Sep 18, 2026
A live Quil app that lays out and draws UML from an EDN IR. A policy plus a language-specific parser write the topology; this tool displays it, routes the arrows, colors CRAP, and lets you click.
The IR is topology: the namespace tree, classes, and edges. Metrics
(CC, coverage, CRAP, killed/survived) come from .metrics/ snapshots produced
by crap4clj and
clj-mutate. The viewer overlays those
files at load, keyed by namespace + function name. Agents edit the policy, not
the IR. See Policy.
Needs Clojure CLI and Java 21+.
clj -M:ir # policy → examples/uml-viewer.edn
clj -M:run
clj -M:run examples/library.edn
clj -M:run examples/uml-viewer.edn
clj -M:run --help
A tmux session uml-viewer-grok starts interactive Grok in the
examined project's directory (--yolo --trust --rules … plus a launch
prompt). On start it writes a hierarchical policy from that project's
namespaces and regenerates the IR. Type there; Esc is the real TUI interrupt.
Closing the diagram kills that tmux session (and the Terminal attach). That
instance — not every Grok in this repo — also runs clj -M:crap,
clj -M:mutate, and IR generate after later changes. Project-wide rules live
in .grok/rules/uml-viewer.md.
The examined project (and this one) must expose two aliases:
| Alias | Who | What |
|---|---|---|
:uml-viewer | anyone | Fresh window. Starts the companion. Waits for :display. |
:uml-viewer-restart | associated agent only | New JVM, same companion. Loads the EDN immediately. |
Do not pass --restart (or use :uml-viewer-restart) unless you are that
companion recycling the window after source changes. A stray --restart
skips spawning Grok and leaves a diagram with no agent. The companion
recycles the window by writing :quit-for-restart to
.uml-viewer/to-viewer.edn, waiting for the JVM to exit, then
clj -M:uml-viewer-restart. Do not SIGKILL. Closing the window still kills
Grok.
On a fresh start the canvas stays blank until the companion sends :display,
with Waiting for agent to create diagram. R reloads the current EDN
immediately and does not wait. A missing or unreadable file prints
UML viewer: file not found: … in the inspector instead of throwing.
clj -M:spec
clj -M:cov
clj -M:ir # writes examples/uml-viewer.edn
clj -M:crap # writes .metrics/crap.edn
clj -M:mutate src/uml_viewer/engine/layout.clj
This project's :crap alias uses ../clojure/crap4clj. :mutate pins
clj-mutate by git SHA. Commit
.metrics/ so a clone has numbers without re-running those tools.
Rename or move of a function is a new form: overlay does not match old names.
Layer and component mean the same thing: a namespace grouping (the first segment after the prefix, or a named proposal group).
from -> to it bundles, in any
declutter mode. Violating pairs are red.:ns). Click it to open that source
file at the top. Hover a member to highlight it; click it to open the same
file at the defn. See Source extractors.+ public and - private. defn- is not drawn on
the class box.R reloads the current EDN (the watcher also reloads on save). Overlay
re-reads .metrics/ on the next load.Esc on the class card closes it. Closing the main window exits the app.This project's diagram is generated. Do not edit examples/uml-viewer.edn.
Edit examples/uml-viewer.policy.edn, then run clj -M:ir (or press Regen).
The parser (LanguageGraph) reads source and emits facts: one class per
project namespace, :require / :use of another project ns as
:dependency, requiring-resolve of a quoted var as :dependency on that
var's namespace, defprotocol as :stereotype :interface, defrecord /
deftype of a protocol as :implements. External :requires and :imports
become foreign classes. Members are not authored — overlay fills them from
.metrics/.
The tree is the namespaces. After :prefix, every . is a nesting
level. uml-viewer.engine.layout is a child of engine.
uml-viewer.clojure-language.source-clojure is a child of clojure-language.
The policy does not assign nses to invented packages. If you want Domain /
Engine / Adapters boxes in the source tree, those segments must exist as
namespaces. To view a grouping that is not in the code, use :proposal
(see Proposed components) — do not rewrite namespaces.
To write a policy for a project:
:src and :prefix to the project's source root and ns prefix
(src and foo for foo.bar.baz).:hierarchical true (or omit :packages and :diagrams).:order — the first dotted part after
the prefix, in the order you want the boxes. Do not invent names.:foreign if they should appear as ovals.:edge-kinds {[:from :to] :association}
using the leaf ids (clojure-language.source-clojure, not
clojure-language).:levels so the generator can mark dependency-rule violations
(see Dependency rule).:proposal to name design components that are not namespaces
(see Proposed components).clj -M:ir (or Regen).If foo.bar and foo.bar.baz both exist, the bar box lists bar (the
module) and baz (the child). Double-click the component to open that
level; double-click the bar module line for its class card.
Wrong (invented partitions):
:packages [{:id :domain :nses [ir geom source]}
{:id :engine :nses [layout route]}]
Right (the ns tree):
{:title "UML viewer"
:src "src"
:prefix "uml-viewer"
:lang :clojure
:out "examples/uml-viewer.edn"
:hierarchical true
:foreign [quil]
:order [main adapters application engine source graph clojure-language domain]
:levels [[domain source graph clojure-language]
[engine]
[application]
[adapters]
[main]]
:edge-kinds {[:engine.compose :engine.layout] :association}}
| Key | Role |
|---|---|
:prefix | Strip this from each ns; remaining dots are the tree |
:hierarchical | Namespace tree (default when :packages is omitted) |
:order | Order of existing top-level ns segments, not new component names |
:levels | Groups of those segments, inner (higher-level) first. Same group = same rank |
:proposals | Named groupings of real segments; not instantiated in source. Inspector list; P returns to the ns tree |
:edge-kinds | Override parser kind for [from to] (usually :association) |
:omit-edges | Drop [from to] |
:lang | Which LanguageGraph to use (default :clojure) |
:foreign | External libs as ovals. A listed prefix collapses quil.core to quil. |
Viewer Grok loop (passed with --rules to the companion session only)
On launch: from the examined directory, write or update the hierarchical policy and regenerate the IR, then wait.
After every later source or policy change: clj -M:crap, clj -M:mutate
on the changed src/ files, then clj -M:ir. Uncovered mutants remaining are
coverage gaps; keep the snapshot and do not re-run the file or force a full
mutation because mutate exited non-zero.
clj -M:ir. Put a new
top-level segment in :order if you care about box order.:edge-kinds entry.:foreign.:packages to fake Clean Architecture components. Use
:proposals to view a grouping that is not in the code.:proposals when rewriting policy. Do not invent them on launch.
If instructed, add a named proposal (default name is a timestamp).Hand-written sample IRs (e.g. examples/library.edn) are still valid; they
are not generated.
A :dependency edge is violating when it runs from a higher-level
(inner) component to a lower-level (outer) one. That is the Clean
Architecture dependency rule: source-code dependencies point inward.
Evaluation is deterministic given :levels:
engine.layout → engine).:levels. Rank is the group's index; smaller
is inner / higher-level.from-rank < to-rank, the edge is
:violating true. Same rank is allowed. :implements and
:association are never violating. Foreign / unranked ends are not
compared.:association clears it.:order is visual box order, not rank. Nesting is not layering: you cannot
infer inner vs outer from the namespace tree alone, so :levels must group
segments that sit at the same architectural level (e.g. domain, source,
and graph). Omit :levels and nothing is marked. If :levels is omitted
and :proposals is set, rank follows the first proposal's component order.
:proposals is a list of named groupings of existing top-level segments.
Those names are not namespaces. Each item is {:id :name :layers [...]}
(:layers here are named components). The as-is diagram stays the ns tree.
The inspector lists the real diagram (the namespace tree) just above
Proposals; click it to return to the tree. Click a proposal to show it
(canvas marked PROPOSAL — not instantiated in code). New Proposal
adds an empty proposal named with a timestamp. Right-click a name to rename
or delete it. Double-click a ns box to drill the real tree.
The Declutter button cycles Declutter arrows (one arrow per component pair per direction) → Declutter elements (also hide nested names, members, and ports) → Declutter classes (also hide classes inside components) → Declutter none.
When a proposal is shown, rank follows that proposal's component order
and violating arrows are re-evaluated. The real diagram uses :levels.
Class boxes show the current view's rank (innermost 0)
at the upper left; the class card repeats Level n. Level 0 is drawn at
the bottom. Good arrows (outer → inner) point down; violating arrows
(inner → outer) point up and stay red. When arrows are collapsed, selecting a class highlights
the component arrows it belongs to. Collapsed components keep their color
and C/M dots; double-click still opens a component.
:proposals [{:id :ccp
:name "2026-09-18 10:30:00"
:layers [{:id :playfield :label "Playfield"
:nses [entities world missiles cities batteries flyers]}
{:id :hosts :label "Hosts" :nses [jvm browser]}]}]
Companion Grok must not invent :proposals on launch and must keep them when
updating :order. If instructed, add a named proposal and regenerate the IR.
The viewer draws a violating arrow red, and bold red when a selected
element highlights it. Hand-written IR may set :violating true directly.
The viewer and the companion Grok talk through .uml-viewer/ in the examined
project (gitignored). The file is the mail; tmux is only a doorbell.
| File | Direction |
|---|---|
.uml-viewer/to-viewer.edn | Grok → viewer |
.uml-viewer/to-agent.edn | viewer → Grok |
Commands are {:id n :op …} with a rising :id. Writes are tmp-then-rename.
:op | Meaning |
|---|---|
:display | Viewer loads :path (relative to the project root) |
:regen | Grok rewrites hierarchical policy, regenerates IR, then :display |
:quit-for-restart | Viewer exits the JVM without killing Grok. The associated agent then runs clj -M:uml-viewer-restart. |
Regen in the inspector queues :regen and wakes Grok with literal text, a
150ms pause, C-m, 50ms, then C-j (same timing as SwarmForge). The wake-up
does not contain the command. If Grok is busy, it finishes first, then reads
the mailbox. If tmux is missing, the button still writes the file and the
inspector says the session is not attached.
Generating the IR asks uml-viewer.graph to scan a source tree. :lang
selects the scanner (default :clojure). Register another implementation
with (graph/register! :java my-java-scanner). The scanner must satisfy
LanguageGraph:
| method | role |
|---|---|
scan | from a root directory and {:prefix …}, return {:classes :edges} |
Classes are {:id :name :ns :stereotype}. Edges are {:from :to :kind}
(:dependency or :implements). The policy layer is language-neutral.
Clojure (uml-viewer.clojure-language.graph-clojure) is the only
implementation today: it reads ns forms (including prefix lists),
requiring-resolve of a quoted var (including nested calls), defprotocol,
defrecord, and deftype. Java or C need a different parser; do not
special-case languages in policy or ir-generator. Main constructs the
implementation and passes it in.
A hierarchical policy writes one EDN document of all classes and edges
(:hierarchical true). The viewer builds each screen from the namespace tree
at the current drill level. A hand-written IR with :packages (or
:diagrams) is still a static diagram, e.g. examples/library.edn.
Metrics on the class card do not have to be authored. If .metrics/ is
present, the overlay fills CC, coverage, CRAP, killed/survived, and any
functions found in the snapshots (including privates). Authored :crap /
:coverage / :ops are the fallback when no snapshot exists.
Overlay keys snapshots by class :ns (the real source namespace). The
generator writes :ns from the scanned ns. Hand-written IR must set :ns
the same way; there is no project-specific fallback.
{:title "Lending library"
:direction :tb
:packages
[{:id :domain
:label "Domain"
:classes
[{:id :book
:name "Book"
:stereotype :class ;; optional: :interface :enumeration :abstract
:fields [{:name "isbn" :type "String"}]
:ops [{:name "find" :args ["isbn"] :returns "Book"}]}]}
{:id :app
:label "Application"
:classes
[{:id :repo
:name "CatalogRepo"
:stereotype :interface
:ops [{:name "get" :args ["isbn"] :returns "Book"}]}]}]
:edges
[{:from :sql-repo :to :repo :kind :implements}
{:from :loan :to :book :kind :association :label "borrows"}]}
Optional authored metrics, used when snapshots are missing:
:crap — a number (μ) or {:mu :max :sigma}:coverage — ratio 0–1 on a class or op:cc, :killed, :survived, :private on ops:hide-members true — compact boxPackage and class color maps CRAP (μ + σ) and mutation score each onto
1–10 using uml-viewer.domain.config cutoffs, averages them, and paints a
0–10 red–green fill. Missing CRAP or mutation data counts as red (grade 1),
not unknown. Parents take the worst CRAP and worst mutation of their
children, and a child with no data is the worst. A C and M dot in
the upper-right show the two scores. The boxes no longer print μ / max / σ.
On the class card, a Crap μ … max … σ … line sits above the table (max is
the worst function in the namespace, not a sum). Column groups are labeled
--crap-- (Crap, CC, Cov) and --mutation-- (killed, survived). The class
row shows average CRAP with a μ suffix and omits CC. Killed and survived
use the same mutation-grade colors as the M dot.
Edge :kind values:
| kind | line | head |
|---|---|---|
:inheritance | solid grey | empty triangle |
:implements | solid grey | empty triangle |
:association | solid grey | open arrow |
:dependency | solid grey | open arrow |
:aggregation | solid grey | empty diamond |
:composition | solid grey | filled diamond |
Layout follows Mermaid's three stages:
curveBasis
cubics. All arrows are solid grey. Paths that pass the target and reverse
are rejected.Clicking a member asks uml-viewer.source for the whole file and a
start line. The IR (and the class card) only supply an identity
map; a language-specific extractor turns that into
{:title :file :body :line}. The source window opens on that file and
scrolls to the member (highlighted). Clicking the module name opens the same
file at the top (:line omitted).
(source/member-source {:lang :clojure
:ns "uml-viewer.engine.layout"
:name "layout"})
:lang selects the extractor (default :clojure). Register another
implementation with (source/register! :java my-java-extractor). The
extractor must satisfy LanguageSource:
| method | role |
|---|---|
locate | path to the file that should contain the member |
extract | slice that member out of the file text |
title | window title |
Clojure (uml-viewer.clojure-language.source-clojure) is the only
implementation today: it maps :ns to src/...clj (or .cljc / .cljs)
and finds the top-level (defn name …) / (defn- name …) so the window can
jump to that line. That locate/line step is not enough for Java or C — those
need a parser or language server, and a richer identity (:class,
:signature, :file). The protocol is the seam; do not special-case
languages in the class card. Main constructs the extractor and passes it to
Core.
Quil stays in adapters.draw and adapters.sketch. The rest of the engine
does not depend on Processing.
30 commits
Clojure
100.0%