Repository navigation
v0.38.0
v0.38.0
Breaking for hosts: FindingsTrigger → FindingsBadge and the findings model (ui, testing), dataAgent({ coordinator }) → dataAgent({ engine }) (ai), antiJoinOf removed (mosaic), Organization.groups/groupsOverage required (auth), and the relation picker's hop menu renamed (testing: RelationPickerHarness). Each section says what to edit.
@kanzo-tech/ui — minor · @kanzo-tech/graph — minor · @kanzo-tech/mosaic — minor
The timeline
The timeline holds one window, reads it once, greys everything outside it, plays inside the
brushed range and draws its first year whole.
-
While a window is set, only what has the time column and falls in the window stays in colour.
GraphTimelineused to leave every type without the column whole, so a window over 30 birthdays
read 340,163 of 341,661; it now reads 30 of 341,661, as Cosmograph greys them, and edges grey
with their ends. A dashboard over a relation whose root has no such column shows no rows while
the window is set — its chip, birthday 1981 – 1985, names why, and removing it brings them back.
If you counted on the old behaviour, there is no switch: the window is a filter on time, and what
has no time is outside it. -
antiJoinOfis gone from@kanzo-tech/mosaic. The timeline was its only user and now crosses
withsemiJoinOf(key, table). Replace a call withsemiJoinOfif you want rows the table does not
hold dropped, which is what it now does for the timeline. -
Play runs inside the brushed range, accumulating. The range you brush never moves: play
publishes [its start, one bar further] until it holds the whole range, so the graph fills in over
time (Kepler'sincrementalwindow). With no range Play is disabled, described and titled Brush a
range to play. At the end it stops with the whole range kept, and the next Play starts over from
its first bar. A pointer down on the bars pauses it, and a drag makes a new range for the next Play.
Each step waits for the page to answer the last — newpaceByprop, the page's crossfilter by
default;GraphTimelinepasses its page — and takes 50 ms at least on 60 bars, longer on fewer,
500 ms underprefers-reduced-motion. Space on the focused timeline plays and pauses; a pause and
the end are announced. -
While it plays, its chip in the
FilterBarreads the window as it grows, as it reads paused,
and the chip's × stops play and lets the range go. -
The timeline is a compact band, as in Cosmograph's app. It is 44 px tall by default (was 72),
with the tick labels at the top, each right of a thin rule through the band, and the bars under
them. With no range the bars are a low-contrast fill and a range is bright. The graph docs' example
no longer puts a border on it. -
Play is a bare glyph at the timeline's left edge, with no box, muted until hovered or focused, as in Cosmograph, named
and titled Play time (Pause time while playing), so it no longer reads as the canvas toolbar's
layout play button. The bars take the rest of the width: the window's range is no longer drawn
beside them (the chip reads it), and Drag across the bars to choose a window is gone. The window
is still read by assistive technology, as before. A test that found the button by the name Play
or Pause finds it by Play time or Pause time;TimelineHarnessalready matches both, and its
API is unchanged. -
A second brush replaces the window instead of adding one.
bin()made a new transform on
every call, so each render of a chart that binned —ChartTimeline, a tile's histogram, your own
<ChartRectY x={bin("date")} />— was a new plot, its queries re-run and its brush a new
interactor: the old window stayed in theFilterBarbeside the new one, and the two intersected.
binfrom@kanzo-tech/ui/analyticsnow returns the same transform for the same field and
options. A plot that is rebuilt anyway — a resize, a theme change — keeps one clause source per
brush or pick for the chart's whole life, so whoever publishes, during a drag or while the
timeline plays, the page holds one clause and one chip for it; the new brush is drawn where the
old one was. -
Play is never inert over a window that is gone. After a rebuild the button stayed enabled and
did nothing; the window now survives the rebuild and plays. -
The chip reads birthday 7/1/1981 – 7/1/1985, not birthday birthday …: the
FilterBarno
longer prefixes a bridged part with its bridge's name when the part already reads as that name. -
The window is in colour and the rest is grey, and a drag no longer flickers. The bars above
are clipped to the window rather than re-queried for it, so dragging the brush redraws nothing,
and the brush draws as an outline rather than a grey fill over the bars it keeps. -
The first tick reads 1980, not 980. The plot leaves half a label of room at either end.
-
GraphTimelineis part of the graph's load. Its band holds its height from the first render,
and the graph reads Loading until the canvas and the timeline's bars have both drawn, then shows
them together. A column chosen or changed later waits in the band alone.
Size budgets raised as a decision, for one clause source per brush across rebuilds and the
window's clip: analytics 33.9 → 35.6 kB, one dashboard 32.3 → 33.9 kB (the last 0.8 kB of each for play inside
the range).
Relation hops
RelationPicker reads each hop as a sentence and says what it costs. The + Hop menu is now
+ Add related…, in two groups: Follow from each Place (The place it is part of) and Bring
in what points at it (Comments located in this place), each sorted by fan-out and showing it —
1 per Place, ~103 per Place, in warning colour above ten. The path is chips, and any of them can
be removed (Remove Comments located in this place), cutting the path there. Under them a grain
line, a status, says what a row now is: One row per Comment · 151,043 rows · Places without
comments are left out. The joins are still inner; the line says so. New props: phrase(edge, direction) to reword a hop the templates read wrong (a self-edge's parent/children), and
plural(type) (People).
JoinType and JoinEdge take an optional rows, and joinGraphOf / readJoinGraph fill it from
the catalog's record counts. relationHops now takes the whole JoinGraph (it reads the types'
rows) and each RelationHop carries fanOut when both counts are there.
@kanzo-tech/testing adds RelationPickerHarness: root(), pick(type), add(hop), path(),
remove(sentence) and grain(). add takes a hop's sentence, its step as relationKey writes it
(<replyOf<Comment, >isPartOf>Place), or its bare edge label where only one item has it. A suite
that clicked the menu item by its old name moves to it: getByRole("menuitem", { name: "replyOfPost → Post" }) becomes picker.add(">replyOfPost>Post") (or "replyOfPost"), and
"← isLocatedIn · Comment" becomes "<isLocatedIn<Comment". The dom environment's click now
releases a task after it presses, as a real pointer does, so a menu item selects in jsdom.
The analytics subpath grows 33.82 → 34.70 kB (limit raised to 34.8 kB); the root barrel and a single Dashboard do not move.
Relation root key
A relation's root key, by name. relationRootKey(graph, relation) in @kanzo-tech/mosaic, and
re-exported from @kanzo-tech/ui/analytics, is the column a relation publishes to the page through
— the root type's own key, the one column of a relation that is not prefixed. Where a host wrote
relationIdentities(graph, relation)[0].column, write
semiJoinOf(relationRootKey(graph, relation), table, { label }); relationIdentities is unchanged.
@kanzo-tech/graph's README describes the search as it is — it asks the corpus once per pause in
typing, under the rest of the crossfilter — and lists readJoinGraph, GraphTimeline and usePick.
@kanzo-tech/ui — minor · @kanzo-tech/testing — minor
Findings
Findings have a model, and the rows are the library's. Finding<Place> is one result —
{ severity: "violation" | "warning" | "info", message, rule: { id, label }, place, help? } — and
FindingGroup<Place> is many sharing a rule: { rule, severity, message, count, places, sample }.
groupFindings(findings, keyOf, sample?) makes groups and tallyFindings(items) counts both into a
FindingTally. The place is yours: the views take describe(place) => { where, action?, detail? }.
New: FindingRow (one result: severity, where and actions on line 1, the message wrapping on line 2,
the detail, rule and help when opened), FindingGroupRow (the count on the badge, one action for all,
the sample when opened), FindingsBadge, FindingsTally, and the types DescribePlace,
FindingAction, FindingPlace, FindingLabels, FindingRule, FindingSeverity, FindingTally.
Breaking, and what to edit:
FindingsRoottakestally(aFindingTally, orundefinedfor nothing checked) instead of
findings, andlabelsfor the rows' words;onSelectis gone. It no longer closes, or refuses to
open, when nothing was found.FindingsTriggerisFindingsBadge. Its children are required — the tally in your words, and the
button's name — and it is a button in every state:outlinewhen nothing was checked,success
when nothing was found.FindingsGoTo,useFinding,FindingVariantandFindingCountsare gone. A row's action is
describe(place).action; the counts areFindingTally, keyed by severity rather than variant.FindingsGrouptakestitleandtallyand renders the rows you give it as children, instead of
a variant and a function from a finding to a row.FindingsContenttakesheaderandempty
instead oftitleanddescription; pass aPopoverHeaderasheader.- In
@kanzo-tech/testing,FindingsHarnessisFindingsBadgeHarness(row(message),
groupRow(message),groups()),FindingHarnessisFindingRowHarness(act(label)replaces
show()),FindingGroupRowHarnessis new, andSeverityis"violation" | "warning" | "info".
ScrollArea takes orientation="vertical": the content no longer takes its width from its widest
line, and there is no horizontal scrollbar. "both" is the default and unchanged.
@kanzo-tech/graph — minor
Map counts
On a map, the counts and the legend say how much of the corpus the map places.
GraphCountssays "placed" under bound positions, where a vertex with no value inxory
is hidden rather than greyed, and gives both figures as drawn of total: 33.4K of 206.6K nodes
placed · 0 of 316.8K edges, and under the page's filter 1.2K match · 33.4K of 206.6K placed ·
40 of 316.8K edges. Hovering "placed" names the two columns. A layout's sentence is unchanged.GraphLegendreads placed of total on a map — Organisation 33,447 of 35,122 — and a
category with no position at all moves into a muted No position group with its whole count
and a hollow glyph, instead of reading 0. Under the filter a placed row keeps its n of m. When
no relation has both ends on the map, a line under the rows says so.Drawncarriestotals,linksandplacedLinks: per rank every vertex of the corpus,
every loaded relation, and the relations whose two ends have a position.- A vertex with only one of the two values is unplaced. It used to count as placed while
cosmos.gl drew nothing for it. GraphCanvasHarness.counts()finds the map's filtered sentence, which has no "nodes" in it.- The graph's size budget goes from 23.5 kB to 24 kB: measured 23.41 kB on
mainand 23.84 kB
with this change, after compacting the legend's rows into one renderer.
@kanzo-tech/ai — patch
dataAgent reads with the page's engine, and says which of the page's clauses it left out.
The agent takes engine (the page's Engine, or anything with its query) in place of
coordinator: write dataAgent({ model, engine, graph, relations, selection }). It reads outside
the coordinator's queue, so a question asked before switching tabs answers in the hidden tab, and
Stop ends the wait at once with the chat's signal.
AnswerOutput gains skipped, each a SkippedClause { clause, missing }: the page's clauses the
answer's relation could not answer, with the columns it lacks. The model is told them, and
AnswerCard draws one line under the tile — Read under the page's filter: … Not applied, as this
relation lacks the fields they filter: … — with the new under and skipped translations.
suggest() takes a count and offers at most that many, dropping a repeated text; dataSuggestions passes its count (default 4) through, so a model that writes more than it was asked for no longer puts more than four questions in the Ask strip.
@kanzo-tech/auth — minor
Each organization on a session now says which of its groups the person is in, by id.
Organization.groups holds the ids of the person's groups in that organization, read from
organization.<alias>.groups. It is for an application that grants to a group ("Research may use
this") and stores the grant by the group's id. Group paths, which Keycloak writes under the same key
when the mapper is not configured, are never read, so that case reads as no groups.
Organization.groupsOverage follows Microsoft Entra ID's overage rule. When it is true, the
person is in more groups there than the token carries, groups is empty and means nothing, and the
application reads the membership from the realm. organizationGroups(config) on
@kanzo-tech/auth/server sketches that lookup: it asks the admin API, walks the ancestors and
caches the answer. It needs a service account holding view-organizations and view-users.
The identity service (services/auth) is what writes them. It now includes a Keycloak protocol
mapper (services/auth/mappers), which the new one-shot auth-mappers service builds with Maven and
mounts into Keycloak's providers directory. No custom image is involved. An application opts in from
its declaration:
x-application:
client_id: board
groups: {} # or { inherited: false, overage: 50 }By default, a member of a subgroup also carries its ancestors' ids. Past 100 ids, an organization's
entry carries groups_overage: true and no ids.
What to change: groupsOverage and groups are required on Organization, so a test double or
fixture that builds one adds groups: [], groupsOverage: false. The first up after upgrading
downloads Maven's dependencies into the kanzo-auth-m2 volume. Later runs reuse that volume.
The size budget for @kanzo-tech/auth/server was raised deliberately, from 5.5 to 5.75 kB, to make
room for organizationGroups. Measured size went from 5.25 to 5.68 kB.
Size budgets
The budgets on main at release, each raised as a decision: graph 24.1 kB, analytics 36.5 kB (timeline and hops together, superseding the figures in the sections above), one dashboard 33.9 kB, ui root 51 kB, ai/data 7.25 kB, auth/server 5.75 kB.
Every @kanzo-tech/* is published at 0.38.0. keasy and any host should move all of them together (@kanzo-tech/* pin each other exactly).