Skip to content

v0.46.0

Choose a tag to compare

@angel-ip angel-ip released this 14 Oct 13:22
· 37 commits to main since this release
a4084c2

@kanzo-tech/ui (minor)

  • Mosaic has one door: @kanzo-tech/mosaic. @kanzo-tech/ui/analytics now exports only the charts and their parts.
    • Breaking: import engine, Engine, Coordinator, Selection, JoinGraph, Relation, RelationExpr, relationKey and the other relation* helpers, numbers, queryFailure, Query, TableExpr, ExprValue, FilterExpr, MosaicClient, makeClient, every clause*, onRelation, the load* loaders, the aggregates, sql, asc and desc from @kanzo-tech/mosaic. Split the import in two:

      import { count, engine, Query, type JoinGraph } from "@kanzo-tech/mosaic";
      import { ChartBarY, ChartRoot, Dashboard, MosaicProvider } from "@kanzo-tech/ui/analytics";

      bin and the six axis marks (axisX, axisY, axisFx, axisFy, gridFx, gridFy) stay on /analytics. @kanzo-tech/ui no longer lists @uwdata/mosaic-core and @uwdata/mosaic-sql as its own peers: they are @kanzo-tech/mosaic's, so keep them installed beside it.

  • A relation's fields are read once per page, on your own TanStack Query client. useFieldStats (and with it Dashboard and AnswerCard) reads a relation's SUMMARIZE once for every part that asks for it, never retries, and reads again only for a new relation or after a failure. The reads show in your client's devtools.
    • Breaking: install @tanstack/react-query 5 and render one QueryClientProvider above a page that reads fields through @kanzo-tech/ui/analytics (Dashboard, useFieldStats). Without it those hooks throw TanStack's No QueryClient set. The charts on their own need neither. queryFieldStats is gone: use useFieldStats, or run SUMMARIZE yourself and pass the rows to fieldStats. Clear your client on sign-out.
  • Breaking: valibot is an optional peer of @kanzo-tech/ui/analytics rather than a dependency of @kanzo-tech/ui. Install it if you use /analytics; an app that never imports /analytics no longer installs it.
  • Every component takes the words it draws through one prop, translations. The Preferences parts take theirs once on PreferencesRoot, and a part's own translations win for that part. A translated panel no longer has English left in it.
    • Breaking: pass translations, not copy, to PreferencesPanel, PreferencesSections and Pref. The type is PreferencesTranslations (was PreferencesCopy). PreferencesPanel's title and hint are entries of its translations. The retired-theme notice's words are retiredTitle and retired(theme) (were retired: { title, formatDescription }).
    • Breaking: pass translations, not labels, to FindingsRoot, FindingsItem, FindingsGroup and FindingsTally. The type is FindingsTranslations (was FindingLabels).
    • Breaking: MadeWith's prefix, infix and loveLabel are entries of its translations: <MadeWith translations={{ prefix: "Hecho con", infix: "en", loveLabel: "amor" }}>.
  • Problem takes the host's words for a code through describe(code, data), which returns a ProblemDescription ({ title?, detail?, link?, page? }, the same fields as before). translations stays for the words Problem draws itself.
    • Breaking: rename copy= to describe= on Problem, and the type ProblemCopy to ProblemDescription. The function you pass is unchanged.
  • RelationPicker inflects no noun. An in-edge reads as the type it brings in and the edge: Comment · located in this place, where it read Comments located in this place. Every word it draws is an entry of translations (RelationPickerTranslations, from @kanzo-tech/ui/analytics), including the two sentence patterns out(to, label) and in(source, label, destination).
    • Breaking: plural is gone. A sentence that reads wrong is phrase(edge, direction)'s to reword, or translations.in's. A test that found a hop by its sentence reads the new one: RelationPickerHarness.add("Comment · located in this place").
  • One facet item. DataTableFacetFilter and ChartFilter take items of FacetFilterItem ({ value, label?, icon?, count? }, from @kanzo-tech/ui), as FacetFilter already did. DataTableFacetFilter keeps an item's own count and gives an item without one the column's count.
    • Breaking: pass items, not options. DataTableFacetOption and ChartFilterOption are gone; type your items as FacetFilterItem. A ChartFilter item is an object with a string value ({ value: "3" }, not a bare 3).
  • Breaking: ChartPickX, ChartPickY, ChartPickColor, ChartBrushX, ChartBrushY and ChartBrushXY are gone from /analytics. Write ChartToggleX, ChartToggleY, ChartToggleColor, ChartIntervalX, ChartIntervalY and ChartIntervalXY; they were the same components under a second name.
  • Breaking: @kanzo-tech/ui/table no longer re-exports TanStack's Table type, because Table from @kanzo-tech/ui is the markup component. Write import type { Table } from "@tanstack/react-table" where you name the instance useDataTable returns.
  • Preferences, ThemeNotice and FloatingPanel leave the package; each is now an example in the docs to copy.
    • Breaking: replace <Preferences hotkey="p" triggerClassName="…" /> with <PreferencesRoot hotkey="p"><PreferencesTrigger className="…" /><PreferencesPanel /></PreferencesRoot>. PreferencesProps is gone.
    • Breaking: ThemeNotice, ThemeNoticeProps and ThemeRetiredCopy are gone. Read useKanzoTheme().retiredTheme and toast it once, as /docs/hooks/use-kanzo-theme#the-retired-theme-as-a-toast does.
    • Breaking: FloatingPanel, FloatingPanelResizeHandle and their props are gone. Copy /docs/layout/floating-panel's example.
  • Six names no product called leave the package:
    • KanzoTheme and KanzoThemeProps. A region that wears another theme is data-theme="<theme>" and the side's class on an element of yours (/docs/themes#theming-a-region). KanzoThemeProvider stays the one themer.
    • KanzoThemeName, which was string. Write string.
    • safeParseColor. ColorPicker still takes a blank or half-typed value; for a parse of your own, wrap parseColor in a try.
    • useFindings. Read the findings where you passed them to FindingsRoot.
    • From /analytics, parseDashboard and recommend (with Recommendation and RecommendIntent). Read one relation's spec with parseDashboards({ byRelation: { [relation]: saved } }); autoDashboard is the recommender's door.
  • A question is one part: Question, with QuestionPrompt, QuestionDescription, QuestionChoices, QuestionChoice, QuestionInput, QuestionTextarea and QuestionError. It works on its own, as a guided chat draws a decision in a turn, and inside a Questionnaire, which is now only the sequence around its questions: the root, QuestionnaireProgress and the actions. On its own a question holds its answer as value, defaultValue and onValueChange({ value }), and a pick answers it in one press; inside a Questionnaire the answer is the questionnaire's, under the question's name. name is also the form field's name, so a question in a plain <form> submits its answer.
    • Breaking: inside a Questionnaire, QuestionnaireItem is Question, QuestionnaireTitle is QuestionPrompt, QuestionnaireDescription is QuestionDescription, QuestionnaireChoices is QuestionChoices, QuestionnaireChoice is QuestionChoice, QuestionnaireInput is QuestionInput, QuestionnaireTextarea is QuestionTextarea and QuestionnaireError is QuestionError, with the same props. useQuestionnaireItem is useQuestion, which also carries answer and setAnswer. The types follow: QuestionnaireItemProps, QuestionnaireChoicesProps and QuestionnaireChoiceProps are QuestionProps, QuestionChoicesProps and QuestionChoiceProps, and QuestionnaireAnswer is QuestionAnswer.
    • Breaking: useQuestionnaire() carries answers, questions, steps and translations; its blocked, indexOf, setAnswer and shortcuts are gone. Answer from inside a question with useQuestion().setAnswer.
    • Breaking: the parts' data-slots follow their names: questionnaire-item is question, questionnaire-title is question-prompt, and every other questionnaire-… slot of a question's part is question-….
  • Questionnaire and Question take their words through translations. Questionnaire's are progress(position, count), navigation (the actions row's accessible name), previous, skip, next, submit, and its questions' unanswered, which reach every question inside it; a Question's own translations win for that question. An action given children still says those.
    • Breaking: QuestionnaireProgress takes no children. Translate it with <Questionnaire translations={{ progress: (position, count) => `Pregunta ${position} de ${count}` }}>, which aria-valuetext reads too.
  • A dashboard tile carries its own question: where, conditions under the page's filter ({ field, in }, { field, between } or { field, contains }, the Condition type; a field is a path from the relation's root, crossed in when it leaves the relation), and top, a bar chart's first bars by its measure or a table's first rows. tileQuery(table, tile, context) is the one compiler of a tile, which every tile view reads through and @kanzo-tech/ai/data's answers read too; TileView draws a tile of any kind, with actions in its header. TILE_ROWS, TileContext, TileQuery, Condition, ConditionValue and TileViewProps are exported from @kanzo-tech/ui/analytics. A tile naming a field or path its relation cannot read draws what it lacks.
    • Breaking: measureExpr and bucketExpr are no longer exported: read a tile's measure and rows through tileQuery. A host that reads tiles itself reads their where and top too. A tile's fields are paths (affiliation.university.name, not University.name), so delete dashboards saved with type-prefixed fields.
  • A count counts what is measured. Across a hop it is count(DISTINCT …) of the relation's root key, so prizes by university counts a prize once per university however many joined rows lead there (Malloy's count()). A count may name a path, { op: "count", path: "affiliation" }, and counts the rows that step reaches. On a type alone it is the row count, as before.
    • Breaking: a count over a relation with hops is lower wherever a hop fanned out; a measure that names a path must be a count.
  • ChartCard ticks a count at whole numbers from zero (a count of 2 no longer reads 0.0, 0.5 … 2.0), and a bar chart's label band widens to its labels, up to 28 characters, before it cuts one. The gesture under a chart's title is a sentence, Click a bar to filter.
  • A dashboard's fields come from what the data says of them. Over a fossil corpus's join graph, useFieldStats reads each column's description and the counts fossil's writer stored, and queries nothing; it runs one SUMMARIZE only for a relation nothing counted. A field is an identifier when the data declares it (the writer's key, identity or endpoint column, an IRI, or a candidate key), and a year stored as an integer whose datatype is xsd:gYear is a time. FieldStat carries its commonest values, its annotations and its label, and a tile's title and RelationPicker's sentences read the data's labels in the page's language.
    • Breaking: a column named id, uri or iri is no longer an identifier because of its name, and a text with more than 50 values is no longer one because of its count. If a field your dashboard searched is now a dimension or a measure, declare it (in the shape, or as a candidateKeys entry of a type in the joins you hand MosaicProvider) or exclude it.
    • Breaking: useFieldStats(relation) takes the relation as ChartRoot does (a table by name, a path over the provider's joins, or SQL). fieldStats(columns, options) takes column descriptions (FieldColumn), not SUMMARIZE rows: describe a relation of your graph with relationColumns(graph, relation). SummarizeRow is gone.
  • Eyebrow: the small, muted, uppercase line that names a block from above it (Parameters, Theme). It renders a <div>, and asChild puts the look on the element it heads (<h3>, FieldLegend, FieldLabel, SliderLabel). It can replace a heading you wrote as font-medium text-muted-foreground text-xs uppercase tracking-wide.

@kanzo-tech/mosaic (minor)

  • JoinResolver is the one door to the joins. Beside the page's crossfilter it answers steps(relation) (each step's path, type and key, the root first), paths(root, hops?) (every field path from a type), relation(root, shown) (the one relation a set of paths lies on), field(on, path) and at(to, from); crossing(from, predicates, to, at?) takes the step to cross in at. A miss is a JoinMiss (join/no-step, join/no-field or join/branches). A path is Malloy's: an edge's label, ^label back along an edge, the edge's name where two of one label leave a type, then the column (affiliation.university.name, ^laureate.category.name).
    • Breaking: a relation's columns are named by their path from the root, not by their type: relationQuery writes country, knows.country and ^hasCreator.length where it wrote Person.country, Person2.country and Post.length. The root's key keeps its bare name. Rename every field you name on a relation (in a clause, a tile, a filter) to its path, and delete saved dashboards whose fields are type-prefixed.
    • Breaking: relationSteps, relationIdentities, relationRootKey, resolveRelation, RelationNeeds and Resolved are gone. A step's key is new JoinResolver(graph).steps(relation) (steps.map((s) => s.key) is what a dashboard leaves out of its fields, steps[0].key the root's), and a question's relation is resolver.relation(root, paths).
  • The one door to Mosaic. It gains the aggregates (avg, min, max, median, quantile, stddev, mode beside count and sum), verbatim, and the builders the charts write with. Everything @kanzo-tech/ui/analytics used to pass through is imported from here; see the @kanzo-tech/ui section above.
  • The types ClauseParts, AntiJoinMetadata, RelationMetadata and RelayOptions leave the entry. clauseParts(clause) and relaySelection(from, to, options) are unchanged; where you named one of these types, use ReturnType<typeof clauseParts> or Parameters<typeof relaySelection>[2].
  • A join graph describes its columns. JoinType.columns are JoinColumns: a column's name and DuckDB type, and, where its catalog states them, its role, datatype, termType, iri, nullable, annotations (JoinAnnotation) and what its writer counted (stats, a JoinColumnStats). A type and an edge may carry iri and annotations, and a type candidateKeys. relationColumns(graph, relation, language?) describes the columns a relation reads, under the names it reads them by, each with the label the data gives it. A multi-valued property is a type with a value, reached by an edge whose table is its own: the hop is one join, and the path to it names its value. labelOf(annotations, language?) reads a thing's rdfs:label in the page's language, namingColumn(type) the column whose property is a labelling property, and splitWords(name) is the last resort for a name nothing labels.
    • Breaking: a JoinType's columns are descriptions, not names: write columns: [{ name: "country", type: "VARCHAR" }] where you wrote columns: ["country"], and include the key column if you have its type. A graph built from a fossil corpus comes from @kanzo-tech/graph's readJoinGraph; build it there rather than by hand.

@kanzo-tech/ai (minor)

  • Ask is offered the graph's facts. What each type and field is and holds (its kind, its role, its commonest values) is read off the join graph, so asking reads nothing before the first question, and an answer's card draws by the same facts the model was told, with no read of its own. A multi-valued property is a path Ask may write (laureate.nickname), and the chips read the data's labels.

    • Breaking: answerRelationsOf, readAnswerRelations, AnswerRelationsOfOptions, ReadAnswerRelationsOptions and AnswerField are gone, and so are types on useAskChat's and askOffers' options and AskInput, and on AskChips' over. Pass the graph alone (useAskChat({ model, engine, graph, selection })) and delete the read that built types. A field's commonest values are FieldStat.values.
  • An Ask answer is a relation and a tile. Answer is { relation, tile }, the tile holding its own where and top, and Ask's transport writes it as a data-answer part, once as asked and again under the same id once read, not as a tool call no model made. AnswerCard takes that part's data as answer and draws it with the dashboard's TileView, so Add to the dashboard adds the question whole. Chat draws data parts through a data renderer map beside tools (ChatDataRenderers), each handed { stopped }.

    • Breaking: draw answers from Chat's data renderers under answer, not from tools.answer: data={{ answer: (part, { stopped }) => <AnswerCard answer={part.data as AskOutput} graph={graph} stopped={stopped} /> }}. AnswerCard takes answer, not part; AnswerCardTranslations' rows is gone. Answer's where, show and top are tile.where, tile and tile.top. answerSchema, checkAnswer, AnswerInput, Condition and ConditionValue are gone from @kanzo-tech/ai/data; Condition is @kanzo-tech/ui/analytics'.
  • Ask's model writes every field as a path from the type it counts (university.name, ^laureate.category.name) out of an enum per type, and JoinResolver resolves them. A path the graph does not have is sent back to the model at its pointer. Two paths of one length to one field are the model's to choose, and the chip offers the other first: Ask no longer asks Which way should this be counted?. When a Task's oracle does ask, the decision is a data-ask part, drawn with @kanzo-tech/ui's Question, and the pick is sent as askEdit(turn.spec, { [decision.id]: option }). The sentence after an answer is an Intl template (Physics: 230; Chemistry: 200.) whose separators are useAskChat's translations (AskChatTranslations).

    • Breaking: a spec's fields are paths from of, not Type.column: { of: "LaureateAward", show: { …, x: "university.name" } }. A type alone's fields go by their own names (gender, not Person.gender). Delete code that numbered a reply's options or matched the reader's 1 or 2: answer a decision with askEdit(spec, decided); AskTurn's choices is gone.
  • A chat can be kept. Give useAgentChat or useAskChat an id with load(id, { signal }) and save(id, messages), and optionally keep(part, message). The chat of that id is read once before the first turn, validated with the AI SDK's validateUIMessages against the dataPartSchemas you give the hook, and saved, every message, at the end of every turn, one save at a time. load answers [] for a chat never saved. keep returns the part to store, a rewritten one, or undefined to drop it; a message left with no part is not stored, and what it keeps must validate or the save is refused.

    • chat.kept says where the chat stands: loading, ready, failed with chat.kept.error, or unsaved. Chat draws it: its skeleton while the chat is read, and the failure under the transcript through Problem, with the composer inert when the chat could not be opened. translations gains unread and unsaved.
    • useAskChat validates its answer and ask parts with its own schemas, so you pass it no dataPartSchemas; to keep no figure of the data, have keep write an answer's rows as [].
    • useKeptChat does the same for a transcript you draw from messages of your own. If you saved conversations yourself (a store, a queue of saves, a restore), delete that code and hand the hook your load and save.
  • Two parts for a guided chat:

    • Checkpoint, CheckpointIcon and CheckpointTrigger (CheckpointTriggerProps, CheckpointTranslations) mark a point in the transcript you can go back to: a thin rule with a mark, your label (Preview 2) and a Restore button. Place it in a MessageList between two Messages, since it is an <li>. It restores nothing itself: the trigger's onClick is your restore. translations={{ restore }} or children give the button your words.
    • PromptInputHeader is a row above the composer's field for what the message carries. Put pills there, such as one per connection that opens @kanzo-tech/ui's Menu with a MenuCheckboxItem per file. A press on the row outside a pill puts the caret in the field.
  • Working takes value (WorkingProps). A number from 0 to 100 draws Progress under the words at that value, null draws it indeterminate, and leaving it out draws no bar, as before. Tool's Parameters and Result headings are Eyebrow and look the same.

  • One Proposal, the Task's, for a field, a list and a chat. Assist reports each candidate as a Proposal<string>: value is the text, and target is where it lands ({ insert }, { replace } or { add }), with rationale as the chip's tooltip. Chat takes a Task's offers whole and sends a proposal's value, never its words: <Chat message={askEdit} proposals={offers} /> sends Ask's spec as it is, so a starter question is not re-asked of the model.

    • Breaking: import Proposal from @kanzo-tech/ai/task, not @kanzo-tech/ai. On an AssistEvent's proposal, text is value and range is target. Pass Chat the proposals propose or askOffers answered as they are, not mapped to { text }, with message={askEdit} for Ask's; a fixed list of questions is Proposal<string>s. askOffers returns the Task's Proposal<AskSpec>[], not { text, spec }.
  • Ask's answers are read once per page, on your own TanStack Query client. useAskChat reads an answer asked again under the same chips once, and Stop cancels the read at once.

    • Breaking: install @tanstack/react-query 5 and render one QueryClientProvider above Ask (@kanzo-tech/ai/data). Chat and Assist from the root need none.
  • Breaking: rename copy= to describe= on Chat, ModelBadge and AnswerCard, which pass it through to Problem; the type is ProblemDescription.

  • Names no product called leave the package:

    • useChat. Import it from @ai-sdk/react, which you already install as a required peer: import { useChat } from "@ai-sdk/react".
    • MessageAvatar, MessageAvatarProps and MessageActions are deleted, and MessageRole leaves the entry. Message, MessageList and MessageContent are unchanged; put an Avatar from @kanzo-tech/ui, or a row of buttons, inside a Message yourself.
    • From @kanzo-tech/ai/task, TaskState. Where you typed an onState callback, use RunOptions["onState"].
    • From @kanzo-tech/ai/data, askTransport, readAnswer, askAlternatives, link and questionOf. useAskChat is the chat, AskChips draws the alternatives, and askOffers returns each offer with its question.
  • Breaking: Question, QuestionPrompt, QuestionDescription, QuestionOptions and QuestionOption are gone from @kanzo-tech/ai. Import the question part from @kanzo-tech/ui, give Question a name, and write QuestionChoices for QuestionOptions and QuestionChoice for QuestionOption:

    import { Question, QuestionChoice, QuestionChoices, QuestionPrompt } from "@kanzo-tech/ui";
    
    <Question name={decision.id} value={chosen ?? null} onValueChange={(d) => choose(d.value)}>
      <QuestionPrompt>{decision.question}</QuestionPrompt>
      <QuestionChoices>
        {decision.options.map((o) => <QuestionChoice key={o.id} value={o.id}>{o.label}</QuestionChoice>)}
      </QuestionChoices>
    </Question>

    d.value is a QuestionAnswer: a string here, a string[] for a multiple question. A choice is drawn as a card, not as an outline button.

  • A Task no longer retries a failed model call twice before it fails: runTask and propose ask through @kanzo-tech/llm's generate, so the first failure is the one you see. Nothing to change on your side.

  • ModelBadge, Chat and the rest name a model's failure LlmError / llm/unavailable (see @kanzo-tech/llm).

@kanzo-tech/llm (minor)

  • generate: a model's answer read whole ({ text, output }), from the same options as stream plus providerOptions. It asks once, so a refusal rejects with what the model threw and is never retried.
  • @kanzo-tech/llm no longer re-exports the AI SDK. It carries stream, generate, LlmError and ModelLoad, and nothing of ai's.
    • Breaking: import the SDK's names from ai, which you already install as a required peer: ToolLoopAgent, stepCountIs, tool, jsonSchema, Output, DirectChatTransport, getToolName, isToolUIPart, and the types Agent, ChatStatus, DynamicToolUIPart, InferAgentUIMessage, LanguageModel, ReasoningUIPart, Tool, ToolSet, ToolUIPart and UIMessage. For example, import { ToolLoopAgent, tool, type LanguageModel } from "ai".
  • The failure is LlmError, coded llm/unavailable, so its area is the package's name.
    • Breaking: AiError is LlmError, and ai/unavailable is llm/unavailable. Rename both where you catch the error or key your error registry on the code. data.reason is unchanged, and @kanzo-tech/ai's ModelBadge offers no retry for llm/unavailable, as it did for the old code.
  • localModel resolves every model through one provider registry, which your app sets once.
    • At your composition root, before anything asks a model: setLocalProvider(localProvider({ baseURL: "/models", worker })). The page then serves every model listed in /models/app-config.json, WebLLM's own AppConfig ({ model_list: [{ model, model_id, model_lib, … }] }), with model and model_lib resolved against /models when they are relative. Write that file wherever you download the models.
    • localModel(), loadLocalModel() and localModelLoad() without an id ask the first model listed; pass an id for another. Without a provider of yours, localModel(id) is WebLLM's prebuilt list from Hugging Face, as before.
    • A missing or empty app-config.json, or an id it does not list, fails as LlmError llm/unavailable with data.reason "unserved".
    • Breaking: localModel(id, settings) and loadLocalModel(id, settings) take no settings, and LocalModelSettings is gone; move worker to localProvider({ worker }). from: { weights, library } is gone: list the model in app-config.json instead, with whatever WebLLM's prebuilt record said of it (its overrides, among them the context window, and vram_required_MB / low_resource_required). An entry that leaves them out runs the model with WebLLM's defaults, a different context window included, and nothing fails to tell you. If your app chose the model's URL in code (an environment variable, a context provider, a branch for a test double), delete it.
  • One model double, for unit tests and end-to-end tests alike.
    • @kanzo-tech/llm/testing exports scriptedModel(script), the AI SDK's MockLanguageModelV4 answering from a script of turns: { when?, once?, reply }, a reply being { text, reasoning? }, { object }, { tool, input }, { fail, after? } or { hang: true }, with an optional pace and a failing load. A structured answer is { object }, not { text } holding JSON. Pass it where a model goes. In a unit test turns may be a function of the call.
    • playwright(page, { models }) and dom(container, { models }) from @kanzo-tech/testing register, for each model id, a provider that answers from its script in the registry above, before the page's own scripts run. localModel(id) resolves it as it resolves any provider, and localModel() resolves the first id scripted: no WebGPU, nothing downloaded, no app-config.json fetched. Each prompt is recorded on window.__KANZO_TESTING__.models.get(id).prompts. Production code carries no test branch.
    • If you injected a model of your own into the page for your end-to-end tests, register its script with playwright(page, { models }) and delete the double, the branch that served it and any guard that it never ships. @kanzo-tech/testing is an optional peer of @kanzo-tech/llm, and /testing needs it installed.

@kanzo-tech/testing (minor)

  • ModelScript, ModelTurn and ModelReply, the model double's script types; scripted(script), the answering scriptedModel wraps; promptOf(call), which reads what a call was told; and playwright(page, { models }) / dom(container, { models }). See @kanzo-tech/llm above.
  • The harness API takes Angular CDK's names wherever CDK has the operation. Breaking, in your page objects and specs:
    • env.harness( is env.getHarness(, env.harnesses( is env.getAllHarnesses(, and env.root is env.rootElement.
    • Inside a harness, this.locate(query) and this.one(by, what, within?) are one this.locatorFor(query, what?), which returns the function that finds it: await this.locatorFor(TileHarness)(), await this.locatorFor({ role: "button", name: "Add" }, "no Add")(within). Declare it once as a field and call it where you need it.
    • Handle is TestElement, and HarnessType is ComponentHarnessConstructor.
    • On a TestElement, .attribute( is .getAttribute(, .press( is .sendKeys(, and .rect() is .getDimensions(), an ElementDimensions with top and left in place of y and x; Rect is ElementDimensions.
    • Severity is gone: it is FindingSeverity, from @kanzo-tech/ui. FindingsItemHarness.severity() answers the same three values; a test package that does not install @kanzo-tech/ui can name it as Awaited<ReturnType<FindingsItemHarness["severity"]>>.

@kanzo-tech/graph (minor)

  • readJoinGraph describes every column of a corpus as its catalog does: its DuckDB type, role, datatype, termType, iri, whether it is nullable, its annotations (its rdfs:label among them) and what fossil's writer counted in it (stats), and each type's candidateKeys, iri and annotations. An edge's label and its endpoint columns are read from fossil_tables, never parsed from the table's name or assumed to be src and dst. A multi-valued property's table is now part of the graph: a type of a row per value, reached from its source by a hop named by the property, so a dashboard, RelationPicker and Ask can reach it. The catalog is read once per corpus, for the canvas and the join graph alike.
    • Breaking: the graph reads the catalog @fossil-lang/corpus 0.3.0-alpha.32 attaches (fossil_tables with label, source_key, destination_key and candidate_keys; fossil_columns with term_type; fossil_annotations; fossil_stats). Upgrade @fossil-lang/corpus to ^0.3.0-alpha.32; a corpus attached by an older one does not read.
  • Every part takes the words it draws through translations, a partial record, English by default, and renders through Ark with its element's props, asChild among them. Each part's words have a type: GraphCanvasTranslations, GraphToolbarTranslations, GraphLegendTranslations, GraphCountsTranslations, GraphStatusTranslations, GraphSearchTranslations and GraphInspectorTranslations. A sentence written from figures, such as 5K nodes · 8K edges, is a pattern over them (nodes(count), edges(count), …). GraphLegend is a Card and takes Card's props, GraphStatus takes Badge's, and GraphSearch takes its cue's, a Button's.
    • Breaking: install @ark-ui/react beside @kanzo-tech/graph. It is now a required peer, the same one @kanzo-tech/ui already asks for.
    • Breaking: GraphSearch's placeholder is an entry of its translations: <GraphSearch translations={{ placeholder: "Find a person…" }} />.
  • Breaking: Selection and SelectionSource are VertexSelection and VertexSelectionSource (the canvas's selection, GraphState.selection and onSelect's argument), and Pick, what usePick returns, is VertexPick. A file that imported graph's Pick gets TypeScript's back, and one that aliased Mosaic's Selection to get past graph's can drop the alias.
  • ShapeGlyph (with ShapeGlyphProps), scaleOf and placementFrom leave the package. A category's glyph and colour reach a page through GraphLegend, GraphInspector and GraphSearch; the placement a preferences panel chose is useGraphPrefs().placement. Channels and Placement stay as types.

@kanzo-tech/theme (minor)

  • Breaking: Problem, what validateSection and validatePrefs return, is SectionProblem.
  • Ten names no product called leave the package: KanzoThemeName, which was string (write string); the section helpers LookDocument, sectionOf, withSection, fallbackChain and resolveSectionToken; and the density self-check, checkDensity, DENSITY_OBLIGATIONS, DensityCheck and DensityObligation. A section's payload is still yours to read and write under sections[namespace], and validateSection still checks it against its manifest.

@kanzo-tech/auth (minor)

  • The session is a query on your own TanStack Query client, and stays current. useSession reads the session again when the tab becomes visible, after a renewal, and when another tab loads, renews or signs out. A sign-out in one tab signs out the others. A session read within a minute of its expiry is renewed before it is answered. The query shows in your devtools; clear the client on sign-out.
    • Breaking: install @tanstack/react-query 5 and render one QueryClientProvider above AuthProvider. Without it AuthProvider throws TanStack's No QueryClient set. A hand-written Auth must read the session afresh on every getSession(): AuthProvider holds the only copy.
  • Every refusal that carries a code is an RFC 9457 problem. The auth routes, the API forwarder and the proxy answer application/problem+json with status, title, detail and the AuthErrorCode as code, and every code has one status everywhere (organization/denied is 403 from the routes as from the forwarder). The forwarder names the code it refused with, so you can route organization/denied or token/exchange-failed instead of coining your own code for a bare status.
    • When your API does not answer at all (connection refused, a name that does not resolve, a reset), the forwarder answers 502 with the new code api/unreachable instead of a bare 500, so you can drop a code of your own for that too. An API that answers with an error status is still passed through untouched.
    • Refusals with no code stay bodiless: a cross-site request's 403, a path under no mount, and no API configured.
    • Breaking: read code and detail where you read { error, message }. A forwarder or proxy refusal that had no body and now has a code carries one.
  • auth.session({ required: true }) sends an outage of the session store or the IdP to your problemPage with its code, as the proxy does. auth.session() without required still throws the AuthError.
    • Breaking: delete the try that redirected to the problem page around it.
  • bffAuth bounds the two requests it makes itself, /session and /refresh, and no other.
    • Breaking: do not wrap the fetch you pass bffAuth in a deadline; it is the transport for the session read too. Bound your API calls on each request instead, with a deadline that suits the call: AbortSignal.timeout(30_000) for a request and response, and a per-chunk deadline for a stream, which a total timeout would cut.
  • organizationConsoleUrl(issuer, organization) links a Members page to the organization's settings in Keycloak's admin console, from the issuer and the organization organizationOf(session, alias) returns, with no setting needed. It is undefined for an organization the realm sent without its id.
  • TenantRequest, what your organization resolver is handed ({ url, headers, cookies }), is exported from @kanzo-tech/auth/next.
  • Breaking: singleFlight and readSession are gone from @kanzo-tech/auth. Import claims and ClaimsConfig from @kanzo-tech/auth/server instead of the root. sealedCookie, cookieValue, SealedCookie, SealedCookieConfig, rewriteOrigin, organizationGroups, OrganizationGroups and OrganizationGroupsConfig are gone from @kanzo-tech/auth/server; write the admin-API lookup of a group overage with your own service account.
  • Breaking: services/compose.yaml no longer defaults the identity service's passwords. A deployment must set KC_DB_PASSWORD and KC_ADMIN_PASSWORD; without them Compose refuses to start and names the one missing. Development is unchanged: services/compose.dev.yaml reads services/auth/dev/dev.env. Running services/auth directly, pass --env-file dev/dev.env beside -f compose.yaml -f compose.dev.yaml.
  • The realm no longer carries the moved blocks for a realm applied before v0.35.0. A realm last applied before v0.35.0 must be applied once at a release between v0.35.0 and v0.45.1 first, or it re-creates the kanzo-conformance client.