Subgraph viewers

A harness instance declares the directories it holds and the kind of graph in each. Some of those graphs have a page you can open; the sections below are the list, and counting them here would be a number that goes stale by lying — a section is added when a viewer is, and a reader wanting the total counts the sections.

  1. Why these pages exist
  2. The schema graph
    1. The relationship diagram
  3. The library — the L1 corpus
    1. Ingestion state is three states, and none of them is an error
    2. The uningested badge, and what it is computed from
    3. It is read-only
  4. The voices — what each rule cites
    1. A rule citing nothing is drawn as a finding, not as a blank
    2. A declared directory that is not there gets a row
    3. The visualiser rule did not ask for this page
  5. Where they are published — two rules, not three
    1. The subject pages
    2. docs/ is the same shape, and its subject means something specific
    3. Nothing composes a path

Why these pages exist

A directory nobody can see is one nobody checks. Measured on this instance, 2026-09-20: 19 declared subgraphs had no way to look at them, and the two most depended on were among them — schemas/, whose shapes everything else is validated against, and library/, which every knowledge-graph reference to a source resolves through.

Bean 2krx states the rule this is the first half of: every directory an instance names as a subgraph needs a visualiser, a documentation entry, and a governing skill. Three separate requirements, because they fail differently — nothing renders it, nothing says what it is for, and nothing an agent can invoke against it. This page is the second of the three for these two graphs; schema-management and the library-ingestion skills are the third.

bootstrap is exempt from the visualiser requirement and not from the others: it is the navbar footer, and what it owes instead is its own .json/.jsonld — “that is its existence”.

The schema graph

/cat-harness/schemas/ — every declaration in every declared schemas directory, with the edges between them.

  • A faceted index: search, filter by kind, filter by module.
  • A detail panel: fields with their types, optionality and doc comments; what the declaration references and what references it.
  • A per-type UML neighbourhood, drawn as a class box with its generalisations above it and its field references below, each labelled with the field it goes through.

  • A relationship diagram at the top, collapsible and closed by default: the declarations this page is scoped to, as UML boxes wired by labelled edges — what is defined here, and how it is linked?

There is still no whole-corpus class diagram. Hundreds of labelled declaration boxes render as a wall that looks like a data model and answers no question about one — the same argument the knowledge-graph viewer made and won. The diagram obeys it by staying scoped, and by declining when the scope is too big rather than drawing it anyway.

The relationship diagram

A collapsible panel at the top draws the declarations the page is scoped to as UML class boxes wired by their edges, each edge labelled with the field it goes through — so you can read not just that Role is linked to Skill, but that it goes through skills, and that it is a list.

It is scoped, and refusing is an answer. The diagram draws the page’s scope narrowed by the module filter; above 40 declarations it declines and says to pick a module. Drawing 812 labelled boxes is the hairball the knowledge-graph viewer measured at 1111 nodes.

One hop of context is drawn, faded. A strict module filter cuts the edge most worth seeing — RoleDefSchema --skills--> SkillDefinitionSchema spans two files — so a declaration just outside the filter that a drawn one touches is drawn too, dashed and dimmed. Counting a cut edge is not showing it.

Three edge kinds, and the third is drawn differently on purpose:

kinddrawnmeans
generalisationsolid, hollow triangleextends
field referencesolid, open arrowa reference in the source
id referencedashedan association declared with @ref

A field reference is something the reader found; an id reference is something an author asserted, because the target is a string id no syntactic reader can see. Drawing them alike would claim the reader saw something it did not.

Clicking a box opens that declaration’s definition, which is what makes the panel navigation rather than a poster.

The layout is static: computed once, deterministically, no simulation. Dragging and alternate arrangements are tracked separately.

The library — the L1 corpus

/cat-harness/library/ — every entry, and the uploads/ queues feeding them.

Two views over one projection, because they answer different questions:

  • Listing — every column sortable, every metadatum visible. Which is biggest? Which has no OCR? Which came from which upload?
  • Desktop — the corpus as tiles. What is in here, before you know what you are looking for.

Ingestion state is three states, and none of them is an error

OCR is the case that forces it:

shownmeans
not scannedno ocr/ directory at all — a determined answer, not a failure
N OCR pagesscanned
ocr/ present, emptya third answer again, and the one worth looking at

Rendering the first as “0” would say OCR failed on a document that was never scanned.

The uningested badge, and what it is computed from

Each queue carries a count of how many units are still waiting, per declaring instance and never merged — two instances’ queues shown as one would report a set that does not exist.

The relation behind it is not a guess. Each library/<slug>/manifest.jsonld carries meta.source_file and meta.source_sha256, and the reader recomputes the hash rather than trusting it — so an entry’s source is content-verified, and a file replaced in the queue shows as differs rather than passing as the same document.

A queued unit is a loose file, or a directory whose intake.json declares a capture. An intake is one queued document however many files it declares: counting a four-file capture as four would say four documents are waiting when one is.

It is read-only

Nothing on this page edits an asset. The actions that would write — starting an ingest from the corpus view, and materialising an entry into a folio of choice — are open work, because the write path is a decision the repository owner has not yet made.

The voices — what each rule cites

/cat-harness/voices/ — every voice its instance declares, every rule, and the passage the rule was read from.

A voice is an editorial register asserted by an instance, and this subsystem’s whole claim is that it is auditable rather than asserted. check-voices.ts exists because PR #210 shipped three WHO profiles carrying ten plausible rules each with source: null — and one of those plausible rules asserted the opposite of what the WHO Editorial Style Manual says on p14. Every rule now carries the page and the quote.

Until this page, none of it was readable without opening JSON. A citation that is only machine-checked is one the reader takes on trust, which is the state the citations were added to end. So the rule row leads with the quote, and the citation is the one column that cannot be filtered away. Bean bu2q.

A rule citing nothing is drawn as a finding, not as a blank

VoiceRuleSourceSchema requires exactly one of an ingested source (libraryId + sectionId) or a node of the instance’s own knowledge graph (kgRef), so a rule with neither cannot load. The viewer renders that state anyway, in red, saying that something is loading voices without validating them — because a reader who cannot tell “no citation” from “citation not rendered” learns nothing from an empty cell.

A declared directory that is not there gets a row

agent-skills declares a voices graph and ships none. It appears in the directories table as declared, not present, rather than being omitted: those are different facts, and a consumer that scans nothing and reports a clean run is the dh4f defect.

The visualiser rule did not ask for this page

owesVisualiser returns false for a content kind and voices is content, so check:subgraph-coverage never raised a finding for it — exactly as it never raised one for the library. What this page answers is 2krx’s own sentence, “a directory nobody can see is one nobody checks”, which is a reason to build a viewer and not a row in a report.

Where they are published — two rules, not three

ruleformwhat it is
1 — a handler renders a kind’s assets/<handler>/<kind>/<optional subject>/cat-harness/library/<iris-replica>/
2 — an instance presents itself/<instance>//<iris-replica>/ mocking the IRIS website

The handler is the instance doing the rendering; the kind names what it renders; the subject is optional, and a page without one is the view over every subject.

A subject page is never published at /<subject>/<kind>/ — that is rule 2’s namespace, and a viewer there would squat on the instance’s own site.

The subject pages

pageshows
/cat-harness/library/every declared library/, and every queue
/cat-harness/library/<iris-replica>/the IRIS replica’s three entries and its queue
/cat-harness/library/agent-skills/its two entries
/cat-harness/library/<science-layer>/its one entry
/cat-harness/schemas/every declared schemas/
/cat-harness/schemas/<instance>/that instance’s modules and declarations

One projection serves all of them — a scoped page filters client-side. A second file per subject would be the same facts written N+1 times, free to disagree the moment one is regenerated. Counts are scoped too: reporting the graph-wide edge total on a page showing nine declarations would claim 512 edges among those nine.

docs/ is the same shape, and its subject means something specific

/cat-harness/docs/ is where the harness user documentation lives, so /cat-harness/docs/<iris-replica>/ is documentation about IRIS — how it is ingested, what the handler does with it — and not IRIS content relocated. The content stays in that instance’s own docs/, and part of the handler’s job is to look for those directories.

Measured 2026-09-20: only cat-harness has a docs/ directory today, and its declaration carried dependents: "skip" — which was exactly what prevented a dependent from getting one. Since 2026-09-30 (option A) the field is retired: every instance’s existing docs/ is a member of the one declared docs subgraph, and docs is a perInstance kind, so a dependent gets its own.

Nothing composes a path

The rendered-content root is resolved from the declaration, the handler from the instance’s declared name, and the page’s link back to its projection from the page’s own depth — so moving a directory moves its source, its URL and its data link together.


folio-assistant — code under the Apache License 2.0, documentation under CC BY 3.0. Generated schema and API references are rebuilt from source on every push to main.

deployed b08c961 · 2026-10-23T23:11:09Z