Skip to content

v0.38.0

Choose a tag to compare

@angel-ip angel-ip released this 10 Oct 13:07
45d7aec

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.
    GraphTimeline used 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.

  • antiJoinOf is gone from @kanzo-tech/mosaic. The timeline was its only user and now crosses
    with semiJoinOf(key, table). Replace a call with semiJoinOf if 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's incremental window). 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 — new paceBy prop, the page's crossfilter by
    default; GraphTimeline passes its page — and takes 50 ms at least on 60 bars, longer on fewer,
    500 ms under prefers-reduced-motion. Space on the focused timeline plays and pauses; a pause and
    the end are announced.

  • While it plays, its chip in the FilterBar reads 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; TimelineHarness already 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 the FilterBar beside the new one, and the two intersected.
    bin from @kanzo-tech/ui/analytics now 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 FilterBar no
    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.

  • GraphTimeline is 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:

  • FindingsRoot takes tally (a FindingTally, or undefined for nothing checked) instead of
    findings, and labels for the rows' words; onSelect is gone. It no longer closes, or refuses to
    open, when nothing was found.
  • FindingsTrigger is FindingsBadge. Its children are required — the tally in your words, and the
    button's name — and it is a button in every state: outline when nothing was checked, success
    when nothing was found.
  • FindingsGoTo, useFinding, FindingVariant and FindingCounts are gone. A row's action is
    describe(place).action; the counts are FindingTally, keyed by severity rather than variant.
  • FindingsGroup takes title and tally and renders the rows you give it as children, instead of
    a variant and a function from a finding to a row. FindingsContent takes header and empty
    instead of title and description; pass a PopoverHeader as header.
  • In @kanzo-tech/testing, FindingsHarness is FindingsBadgeHarness (row(message),
    groupRow(message), groups()), FindingHarness is FindingRowHarness (act(label) replaces
    show()), FindingGroupRowHarness is new, and Severity is "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.

  • GraphCounts says "placed" under bound positions, where a vertex with no value in x or y
    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.
  • GraphLegend reads 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.
  • Drawn carries totals, links and placedLinks: 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 main and 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).