--- /dev/null
+# classgraph
+
+A GHC TypeChecker plugin and browser visualizer for Haskell typeclass
+hierarchies. Drop the plugin onto a project, render the captured data as
+a self-contained interactive HTML page, and explore the inheritance DAG,
+class instances, type families, and superclass requirements with
+xdot-style highlighting.
+
+```
+ target package ───────────► per-module JSON dumps
+ (built with the plugin) (.classgraph/*.json)
+ │
+ ▼
+ classgraph-view ──────► oc.html
+ (one self-contained file)
+```
+
+## Table of contents
+
+- [What you get](#what-you-get)
+- [Quick start: the demo](#quick-start-the-demo)
+- [Using the plugin in your own project](#using-the-plugin-in-your-own-project)
+ - [Option A — `build-depends`](#option-a--build-depends-simplest-okay-for-small-projects)
+ - [Option B — `-fplugin-library`](#option-b---fplugin-library-recommended-for-real-projects)
+- [Combining dumps from multiple packages](#combining-dumps-from-multiple-packages)
+- [Viewer reference](#viewer-reference)
+- [Schema, data flow, design notes](#schema-data-flow-design-notes)
+- [Known limitations](#known-limitations)
+- [Building from source](#building-from-source)
+
+## What you get
+
+When you point the plugin at a target package and run the viewer, you get:
+
+- An interactive **classes view**: every `class` in the program as a
+ node, edges drawn for direct superclasses, with an extra dashed edge
+ whenever the superclass is mediated by a type family
+ (`class Pretty (Norm a) => Foo a`-style). Top-level classes (those no
+ other class extends) get a gold border and a `★ top` mark, and are
+ rendered in the topmost row.
+- An **instance view** per class, drilled into by double-clicking. Shows
+ every instance of that class, the constraints in each instance's
+ context, the superclass requirements (and the matching superclass
+ instances when present), and any associated `type instance F …` rows.
+ When the constraint goes through a type family, you also see the chain
+ *focused-instance → family → concrete fam-instance → satisfying
+ class-instance*.
+- A **family view** per type or data family. Shows every `type instance`
+ / `data instance` of that family. Open, closed, associated, and data
+ families are all distinguished — with `(data)` appended to the label
+ for data families.
+- Backwards navigation via the side panel: every class lists its
+ *superclasses* and its *subclasses* (i.e. the classes that extend it
+ in this program). Click any name to navigate.
+- **Search** (top right or `/`) for classes and families, with badges
+ for `external` and `family` entries.
+- **Pin** classes from the side panel to focus the classes view to just
+ the pinned classes plus their immediate superclass neighbourhood
+ (xdot-style narrowing). Click a ghost neighbour to expand by one hop.
+- **Mute** noisy ambient classes (`Show`, `Eq`, `Ord`, `NoThunks`,
+ `Typeable`…) so they vanish everywhere.
+- Per-class **instance visibility filter** and per-family **type-instance
+ filter** — checkbox lists in the side panel for hiding clutter.
+- A foldable **Help / legend** in the bottom-right of every view.
+- A **Fit** button (and `F` shortcut) to re-frame after hiding rows.
+
+## Quick start: the demo
+
+The repo ships with a tiny test target under `examples/demo` that
+exercises every extraction code path (multi-param classes, associated
+families, an open family used in a superclass context, a closed family,
+a data family, an orphan instance, equality contexts, etc.).
+
+```bash
+cabal build classgraph # build the plugin library
+cabal build demo # builds with -fplugin=Classgraph.Plugin
+cabal run classgraph-view -- \
+ --input examples/demo/.classgraph \
+ --output classgraph-demo.html
+xdg-open classgraph-demo.html # or `open` on macOS
+```
+
+That should pop a single HTML file ~700 KB containing 14 classes, 29
+instances, 4 families, and 5 family instances, ready to be explored.
+
+## Using the plugin in your own project
+
+There are two ways to attach the plugin to a target package. The right
+choice depends on whether your target has tight dependency bounds.
+
+### Option A — `build-depends` (simplest, okay for small projects)
+
+In your target's `.cabal` file:
+
+```cabal
+library
+ build-depends: classgraph
+ ghc-options: -fplugin=Classgraph.Plugin
+ "-fplugin-opt=Classgraph.Plugin:dir=.classgraph"
+```
+
+GHC loads the plugin from `classgraph` in the package set and the cabal
+solver figures everything out. Easiest to set up but **the plugin's
+transitive bounds (`aeson ^>= 2.2`, `ghc ^>= 9.14`, …) become part of
+your build plan** — which can cause a project with tight bounds (e.g.
+ouroboros-consensus, cardano-ledger) to lose a usable build plan. If
+that happens, switch to Option B.
+
+### Option B — `-fplugin-library` (recommended for real projects)
+
+GHC 9.4 introduced
+[`-fplugin-library`](https://downloads.haskell.org/ghc/9.14.1/docs/users_guide/extending_ghc.html#ghc-flag--fplugin-library-=⟨file-path⟩;⟨unit-id⟩;⟨module⟩;⟨args⟩),
+which loads a *prebuilt* plugin shared object directly. The plugin lives
+in its own dependency closure; the consumer **does not** add `classgraph`
+to `build-depends` and the cabal solver never sees it.
+
+The flag's syntax is:
+
+```
+-fplugin-library=<file-path>;<unit-id>;<module>;<args>
+```
+
+with `<args>` parsed as a Haskell list-of-strings literal (`["dir=…"]`).
+
+Concretely, in `cabal.project.local` of your target:
+
+```
+package ouroboros-consensus
+ ghc-options:
+ -fplugin-library=/abs/path/to/libHSclassgraph-0.1.0.0-inplace-ghc9.14.1.so;classgraph-0.1.0.0-inplace;Classgraph.Plugin;"[\"dir=.classgraph\"]"
+```
+
+Note: the args portion is wrapped in `"..."` and the inner quotes are
+escaped with `\"`. Cabal's `ghc-options:` parser strips bare `"` as
+token-grouping, so this exact spelling is the one that survives intact
+to GHC.
+
+**The shipped helper does this for you.** It auto-discovers the .so
+from either a local cabal-build (`./dist-newstyle/…`) or your
+cabal-install store (`~/.cabal/store/ghc-<ver>/…`), generates the
+correctly-escaped flag, and emits a ready-to-paste cabal stanza:
+
+```bash
+# From the classgraph checkout (or wherever the script lives):
+./classgraph-plugin-flag.sh --cabal --package ouroboros-consensus \
+ >> /path/to/your/project/cabal.project.local
+```
+
+It works in three scenarios:
+
+| Setup | What the helper finds |
+|---|---|
+| You cloned this repo and ran `cabal build classgraph` | `./dist-newstyle/build/.../libHSclassgraph-…-inplace-ghc<ver>.so` |
+| `cabal install --lib classgraph` (library only) | `~/.cabal/store/ghc-<ver>/classgraph-…/lib/libHSclassgraph-…-ghc<ver>.so`, looked up via the store's `package.db` |
+| `cabal install classgraph-view` (executable + transitively the lib) | Same store path as above — installing the executable also leaves the library installed in the store |
+
+Override the auto-discovery with the `CLASSGRAPH_SO` env var if you've
+got the .so in a custom location.
+
+Re-run the helper whenever you rebuild classgraph: cabal hashes the
+unit-id, so a fresh build of the plugin produces a new `.so` filename
+and a stale flag will not load.
+
+#### Finding the plugin path manually
+
+If you don't want to use the helper, you can discover the path and
+unit-id yourself with `ghc-pkg`. Pick the right db for your install
+method:
+
+```bash
+GHC=$(ghc --numeric-version)
+
+# (a) cabal install --lib / cabal install classgraph-view → cabal store
+STORE=$(ls -d ~/.cabal/store/ghc-${GHC}* | head -1)
+DB=$STORE/package.db
+
+# (b) cabal install --lib --package-env <env> classgraph → user env file
+# (no separate db, ghc-pkg --user works)
+
+LIBDIR=$(ghc-pkg --package-db=$DB --simple-output field classgraph library-dirs | tr ' ' '\n' | head -1)
+UNIT=$(ghc-pkg --package-db=$DB --simple-output field classgraph id | tr ' ' '\n' | head -1)
+
+SO="$LIBDIR/libHS$UNIT-ghc$GHC.so"
+test -f "$SO" || echo "expected $SO to exist"
+
+echo "-fplugin-library=$SO;$UNIT;Classgraph.Plugin;\"[\\\"dir=.classgraph\\\"]\""
+```
+
+For an `inplace` (local-checkout cabal-build) the path is
+`dist-newstyle/build/<arch>-<os>/ghc-<ver>/classgraph-<ver>/build/libHS<unit-id>-ghc<ver>.so`
+— same shape, no `ghc-pkg` round trip needed.
+
+#### Caveats
+
+- **The `.so` is GHC-version-specific.** A plugin built with
+ `ghc-9.14.1` only loads in `ghc-9.14.1`. Mixing minor versions can
+ silently fail or crash.
+- **Cabal hashes the unit-id.** A non-`inplace` install ends up with
+ `classgraph-0.1.0.0-1234abcd…`. The helper script always reads the
+ current id from `dist-newstyle/`. If you want a stable id for
+ scripting, add `ghc-options: -this-unit-id classgraph` to the plugin
+ library's stanza in `classgraph.cabal` and rebuild.
+- **The plugin must be built against the same RTS as the compiler.**
+ Don't link the plugin library statically with the RTS — leave it
+ dynamic so the compiler and plugin share globals. (The default cabal
+ invocation does the right thing.)
+
+## Combining dumps from multiple packages
+
+Big projects (consensus + cardano-ledger + …) live in separate cabal
+packages. Build each with the plugin attached, then merge their
+`.classgraph/` directories into a single rendered HTML by repeating
+`--input`:
+
+```bash
+cabal run classgraph-view -- \
+ --input ../cardano-ledger/eras/{shelley,alonzo,babbage,conway}/impl/.classgraph \
+ --input ../cardano-ledger/libs/{cardano-ledger-binary,cardano-ledger-core,cardano-ledger-api}/.classgraph \
+ --input ../ouroboros-consensus/.classgraph \
+ --output cardano.html
+```
+
+The merger:
+
+- Concatenates all classes / instances / families / fam-instances.
+- Deduplicates classes and families by `QualName`. First occurrence
+ wins, so a class defined in one dump and referenced as `external` from
+ another collapses to a single local node.
+- **Normalises package ids** before deduping: `pkg-1.0-inplace`,
+ `pkg-1.0-<sha>`, and `pkg-1.0-l-api-<sha>` all collapse to `pkg`. This
+ is what makes cross-dump references unify into one node — the same
+ package can otherwise appear under three different ids depending on
+ which dump observed it (locally-built, installed-via-hash,
+ internal-library).
+
+## Viewer reference
+
+| Action | Effect |
+|---|---|
+| **Click** a node | Highlight + populate the right-side details panel |
+| **Double-click** a class | Drill into its instance view |
+| **Double-click** a family | Drill into its family view |
+| **Click** a class name in the side panel | Navigate to that class (highlight + center) |
+| **Search** (`/` or top-right input) | Locate a class/family in the classes view |
+| **Pin** (side panel button) | Focus classes view to pinned classes + one-hop neighbours |
+| **Mute** (side panel button) | Hide a class everywhere |
+| **Click** a ghost (focus-mode neighbour) | Pin it and expand the focus subgraph |
+| **Back arrow** (topbar) / browser back | Return to the classes view |
+| **Fit** button or `F` | Re-frame the current view |
+| **Help** (bottom-right) | Foldable legend of every node and edge style |
+
+The instance and family views also have a per-target visibility filter
+in the side panel — checkbox per row, with `Show all` / `Hide all`
+buttons and a substring search.
+
+## Schema, data flow, design notes
+
+The plugin runs in `typeCheckResultAction` (post-typecheck, so all
+classes / instances / family equations are fully resolved). For each
+compiled module it writes one JSON file under the configured directory
+(`-fplugin-opt=Classgraph.Plugin:dir=…` or the `dir=…` arg in the
+`-fplugin-library` form, default `.classgraph`).
+
+The on-disk format is defined in `Classgraph.Schema`. The viewer is a
+single self-contained HTML file: Cytoscape.js + dagre + the program
+JSON + the styling and JS, all inlined. No HTTP server, no CDN; opens
+with `file://`.
+
+Notable extraction details:
+
+- Classes come from `tcg_tcs` filtered with `tyConClass_maybe`.
+ Superclasses come from `classSCTheta`, decomposed via
+ `classifyPredType` so constraint tuples expand into individual edges
+ and the boxed equality classes `(~)`/`(~~)` get rendered infix as
+ `lhs ~ rhs` chips on instance nodes (rather than as edges to a
+ synthetic `~` class).
+- Type families (open / closed / associated) come from `tcg_tcs`
+ filtered with `isFamilyTyCon`, plus the assoc families discovered via
+ each class's `classATs`. Closed-family branches are recovered from the
+ `CoAxiom Branched`. Data family instances are detected via
+ `fi_flavor`'s `DataFamilyInst`; the synthetic `R:…` data-constructor
+ TyCon on the RHS is hidden from rendering since it isn't usefully
+ inspectable.
+- Type-arg rendering uses `splitVisibleFunTy_maybe` for arrows and
+ filters invisible kind binders via `tyConBinders` +
+ `isInvisibleTyConBinder`. Without this, function and kind-poly
+ applications leak `FUN ManyTy (BoxedRep Lifted) …` into every label.
+- Pretty-printing uses an `SDocContext` with
+ `sdocPrintExplicitRuntimeReps = False`, `sdocLinearTypes = False`,
+ `sdocPrintExplicitKinds = False` and friends, so fallback
+ pretty-printed strings (for `OtherArg` / `LitArg`) read as
+ user-facing Haskell.
+- `iiSrc` uses the dfun's `Name` span (the `instance …` declaration)
+ rather than the class's name span (which is `UnhelpfulSpan` for
+ classes loaded from another package's interface file).
+- `fiSrc` for fam-instances uses `coAxBranchSpan` of the underlying
+ `CoAxBranch`, so each row points at its own `data instance` /
+ `type instance` declaration site.
+
+## Known limitations
+
+- **GHC 9.14 only.** The plugin uses APIs that are stable from 9.10
+ onward but the schema / viewer assume 9.14's flavour of `Class`,
+ `FamInst`, `CoAxiom`, etc. Patches for older GHCs welcome.
+- **Cross-package merge requires you to extract every package you care
+ about.** A class defined in a package you didn't build with the plugin
+ becomes an `external` stub. The shape of the graph stays correct but
+ drilling into such a class shows nothing.
+- **Family resolution chains can stop early** when the relevant fam-instance
+ isn't in your dumps. E.g. for ouroboros-consensus's
+ `HasLedgerTables (LedgerState (ShelleyBlock proto era))`, the
+ superclass `Eq (TxOut blk)` reduces to `Eq (TxOut era)` — itself a
+ family application whose era-specific `type instance TxOut … = …` rows
+ live in `cardano-ledger-shelley` etc. Extract those packages too and
+ the chain will keep going.
+- **Pretty-printing is best-effort.** Anything that survives the
+ structural converter (`Type` shapes we don't recognise) falls back to
+ GHC's `Outputable`, which is much friendlier than it used to be after
+ the `prettyCtx` overrides but isn't perfect. If you find a label that
+ reads like internal compiler noise, file an issue with a `repro.hs`.
+- **No support for data-family RHS bodies.** We record that a
+ `data instance` exists and what types it's specialised to, but not
+ the constructors / field names. Adding that is mostly a Schema +
+ Render exercise if you need it.
+
+## Building from source
+
+Requires:
+- GHC 9.14.1 (`ghcup install ghc 9.14.1 && ghcup set ghc 9.14.1`)
+- cabal-install 3.16+ (`ghcup install cabal latest`)
+
+```bash
+git clone <repo-url>
+cd classgraph
+cabal update
+cabal build all # plugin library + classgraph-view executable
+cabal run classgraph-view -- --help
+```
+
+To run the demo end-to-end:
+
+```bash
+cabal build demo
+cabal run classgraph-view -- --input examples/demo/.classgraph -o demo.html
+```
+
+To regenerate dumps for a real target (e.g. ouroboros-consensus) using
+`-fplugin-library`:
+
+```bash
+./classgraph-plugin-flag.sh --cabal --package ouroboros-consensus \
+ >> ../ouroboros-consensus/cabal.project.local
+cd ../ouroboros-consensus
+rm -rf .classgraph
+cabal build lib:ouroboros-consensus
+cd -
+cabal run classgraph-view -- --input ../ouroboros-consensus/.classgraph -o oc.html
+```
+
+## License
+
+BSD-3-Clause. See `LICENSE`.