Findings
Why a finding is always one result and a group is many but both are one item with one action, why its place is the host's and reaches the library only through describe, why an item's message wraps on a line of its own, why the header is parts, and why the entry is a badge that opens a popover — and what would reverse each rule.
What a check found is drawn in two products here: Keasy's Discover validates a graph against SHACL shapes and reports seventeen thousand results, and its recipe editor shows what fossil's compiler found in a program. Both had their own rows, their own tally and their own popover. This page is why they now share one model and one set of parts, and where the line between the library and the host falls. Findings is the reference.
One result, or a group
A Finding is always one result: a rule that failed at one place. A FindingGroup is many results
sharing a key — a rule, and usually a path — with a count, every place, and a sample to open.
The two are different types, not one type with an optional count, because a model that might be one
or many is one whose every reader has to ask which — the item below asks once, so nothing else does. groupFindings(findings, keyOf) makes groups from
findings; tallyFindings counts both, a group by its count.
The references agree on the split and differ only on where it is made:
- The W3C SHACL validation report (
sh:ValidationReport) is a flat list ofsh:ValidationResult, each one focus node, one path, one value, one source shape and constraint component, one severity. That is ourFinding, and SHACL's three severities —sh:Violation,sh:Warning,sh:Info— are ourFindingSeverity. - TopBraid EDG and shacl-play group that list by shape and then by constraint for a reader,
because a report of seventeen thousand results read one by one is not read. That is our
FindingsGroupheading, and aFindingsItemgiven a group. - Great Expectations reports a failed expectation as counts and a sample of the unexpected
values (
unexpected_count,partial_unexpected_list), never the whole list. That is the group'scountbeside itssample, and whycountis its own field: a host may hold a summary of seventeen thousand and the places of only some.
The key is the host's. Rule and path is the default reading of a SHACL report; a compiler might
group by code, a form by field. groupFindings orders worst first and keeps the host's order within
a severity, so a list sorted by line stays sorted by line.
What would reverse it: a consumer whose groups nest — shape, then path, then value — and who needs the items to know it. Then a group holds groups, and a group's item opens onto items rather than onto a sample.
Held by packages/ui/src/lib/findings.test.ts, "keeps one group per key, counting every member and
sampling the first", "orders the groups worst first, and by first appearance within a severity" and
"counts a group by its count, not by its sample".
The place is the host's
A finding's place is a type parameter the library never reads. A view takes the host's
describe(place), which returns where it is and what opening it shows: { where, detail? }. This
is VS Code's Problems panel: a diagnostic is a severity, a message, a code and a source over a
location, and the panel draws the location through the resource it belongs to, never by knowing
what is inside the file. The Language Server Protocol's Diagnostic is the same shape, with a
Range as its place.
So the library imports neither RDF nor LSP types. The RDF place — a focus node, a path, a value — and
its description live in rudof-wasm's bindings, where the report is read; the code place — a line and a
column — lives in fossil's editor. Each host writes one describe: totalCost · Project/123;
Line 12.
What would reverse it: two hosts writing the same describe for the same kind of place. Then that
reading moves into a package both depend on — still not this one, unless the place is a type this
library already owns.
One item, one action
One result and a group are one part, FindingsItem, given either: the group wears its count on the
severity badge and opens onto its sample. Each item has at most one action — Go to field, Go to
line, Show 362 — and it is the item's action, not the place's. A sampled place is read, never
acted on. VS Code's Problems panel is the reference: grouped, and one thing to do per entry, which is
to go there. The metadata-form showcase's popover arrived at the same shape on its own — severity,
where, Go to field, the message — and it was the clean one of the three call sites.
It replaced FindingRow and FindingGroupRow. They drew the same two lines from the same
Diagnostic parts and differed in what the data is, which the item can see for itself (count is
only on a group). Their action lived in two places — describe(place).action on one, a prop on the
other — and the group's sample drew a button per place, so an opened group of three was four buttons
saying Show. In Discover that sat beside a rule's own Show all 44 violations, written by the host
as a stray <li> in the group because the heading had no place for it: three ways to put nodes on the
page from one popover. Now a group's heading takes one action, an item takes one, and a place takes
none. The name is Ark's grammar, base and part, as AccordionItem is an entry of an accordion.
An action may say whether it is on. pressed makes it Ark's Toggle: Discover's Show puts
nodes on the page and pressing it again takes them off, which the host was writing by hand with
aria-pressed. It is a field of the action rather than a second kind of action, because the one thing
the item owns about it — closing the popover before it runs — is the same either way.
What line 1 reads is one result's place as describe reads it, or a group's rule label; the item's
children replace it, which is how a group says what its members share — a path, totalCost. Children,
not a where prop, because it is markup.
What would reverse it: a host whose reader needs two things to do at one place, each as common as
the other — Go to line and Quick fix, say. Then an item takes actions as children, as
DiagnosticActions does, and closing the popover becomes the part's to offer rather than the
action's to carry.
Held by packages/ui/src/composites/findings.test.tsx, "is the same item with its count on the
badge, one action for all, and a sample to read rather than to act on", "is a region named by its
title, with the tally and its one action beside the name rather than in it" and "is a toggle when it
says whether it is on, and a plain button when it does not"; packages/ui/src/index.test.ts, the
tombstones !FindingRow and !FindingGroupRow.
An item is two lines, and the message wraps
Line 1 is the severity badge, where and the action; line 2 is the message, on a line of its own,
wrapping. Nothing is held to one line but the badge and the buttons. It is how a compiler prints a
diagnostic, and it is the row metadata-form's IssueDiagnostic arrived at after the one-line header
measured a message 0 px wide in a panel a fifth of the workspace wide (DiagnosticHeader's comment
has the figures). where breaks anywhere, because an IRI has no spaces to break at.
An item is built on Diagnostic's parts and adds nothing to them but the arrangement:
DiagnosticTitle truncates, which is right for a title beside other things, and the message is given
basis-full and whitespace-normal! because a line of its own has nothing to align with.
What would reverse it: a list where scanning many items at a glance is worth more than reading
any one — a log. Then the message truncates and the item opens to read it, as DiagnosticTitle does.
Held by packages/ui/src/composites/findings.test.tsx, "puts the message on a line of its own that
wraps, and holds nothing to one line but the badge and the buttons".
The header is parts
FindingsHeader is the popover's header, and what goes in it is children: FindingsSource for
what was checked, FindingsActions for its controls, PopoverDescription for what the check
covered. Its title is not a part — it is the labels' Findings. It replaced FindingsContent's
header prop, which took markup as an attribute (the rule is on
Conventions), and each host filled it differently: Discover
with a hand-built file row, scope line and three buttons, the editor with PopoverHeader's title and
description, metadata-form with a description alone. The three now say the same four things in the
same places — Findings, flights-shapes.ttl · 4 rules, ↻ ⋯ Keep what conforms, 1,204 of 3,310
nodes checked — and differ only in the words.
It is laid out as an item is: a row that wraps, the title and the source growing on line 1, the actions at its end, the scope on a line of its own. The minimal set came from asking, part by part, whether something already draws it:
- The scope is
PopoverDescription, not a part of ours. It is Ark's description, so it describes the dialog to a screen reader as well; aFindingsScopewould have been the same element under a second name. - The title is
PopoverTitle, drawn by the header fromlabels.title, not a part a host places. Every call site said Findings or wanted to, the dialog is named by it, and a host that calls it something else says so where it says Violation: in the labels. FindingsSourceis new because nothing draws a muted monospace label that grows and wraps anywhere except the item's own where, and it is that class, shared.DiagnosticSourceisnowrapand hidden belowsm, which is right beside a message and wrong for a file name.FindingsActionsisDiagnosticActionspushed to the end of the line, under its own slot, and a group's heading uses it for its one action too. Icon buttons, and at most one text toggle: line 1 is meant to hold in a 28rem popover beside a file name, and words are what widen it.
FindingsContent is only the popover now, and FindingsList the scrolling list. Once the header
is a child it cannot be told from the list without reading children's types, so the list became its
own part — the vertical area and the switch to empty. empty stays a prop: it is the list's
fallback, drawn instead of the children and never beside them, as Show's fallback is. The rule
against markup in a prop is about regions a caller cannot reorder or wrap, and a fallback has nothing
to be ordered against.
What would reverse it: a host that needs the title to be something other than words — an icon, a
link to the rules — often enough that labels cannot say it. Then the title is a part, as the source is.
And for empty: a host that needs the empty state beside a partial list. Then it is a part.
Held by packages/ui/src/composites/findings.test.tsx, "heads the list with the title, what was
checked, its controls and what the check covered".
The list scrolls down, never across
The popover's list is a ScrollArea with orientation="vertical". Ark gives its content
min-width: fit-content, so one unbroken line set the list's width: rows ran past a 448 px popover to
507 px behind a horizontal scrollbar (measured 2026-10-02 on the findings example), and the first
FindingsContent dropped ScrollArea for a plain div to get away from it. Vertical sets the
content's floor to 0, hides the sideways overflow and draws no horizontal scrollbar. It is an option
and not the default, because a code block in a dialog relies on the sideways scroll.
What would reverse it: every ScrollArea in the library and its hosts passing vertical. Then
it is the default and both is the option.
Held by packages/ui/src/simples/scroll-area.test.tsx, "vertical drops Ark's fit-content floor and
the horizontal scrollbar", and packages/ui/src/composites/findings.test.tsx, "lists in a
vertical-only area, so a long line cannot widen the popover".
The entry is a badge that opens a popover
In Discover and in the editor the findings are behind one badge that tallies them — 362 violations · 17,364 warnings — and opens a popover the host composes: a header (the rules file, the check's controls and its scope), the grouped list, or an empty state. The tally is what a reader needs at rest; the list is what they need when they ask. A panel always open would spend a column on a list that is usually short or empty, and a page of its own would take the reader away from the graph they are fixing.
The badge is a button in every state. The first FindingsTrigger stopped being one when nothing
was found, because there was no list to open. With a header and an empty slot there always is: No
rules opens onto choosing rules, Valid onto what was checked. undefined is nothing checked and
wears outline; a tally of zero is clean and wears success; otherwise the worst severity. Its
accessible name is its text, and its text is the host's: 362 violations is a product's vocabulary and
a plural rule is a locale's.
A group's heading carries its tally, not its paths. A shape that failed on nine paths says 9 and 2 in their families, and the paths are the rows beneath it.
What would reverse it: a host where the findings are the work rather than a report on it — a data-quality console. Then the list is a page, the groups are its sections, and the badge is a summary on another page.
Held by packages/ui/src/composites/findings.test.tsx, "is a button named by the host's tally in
every state, painted by the worst" and "shows the empty slot, and not the list, when nothing was
found or nothing was checked".
How a failure leaves the library
The thrown value whole, by one path; the codes kanzo-ui names itself; and the waits it bounds. The argument across fossil, kanzo-ui and their hosts is fossil's page, not this one.
Testing a page built with Kanzo UI
Why @kanzo-tech/testing ships a harness per component in Angular CDK's shape and with its names, reads ARIA before anything else, reaches a canvas's coordinates only through a hook a test installs, and registers the one model double as a provider in the registry every model is resolved through; and why Ask's evaluation set lives beside Ask — and what would reverse each rule.