Repository navigation
v0.46.0
@kanzo-tech/ui (minor)
- Mosaic has one door:
@kanzo-tech/mosaic.@kanzo-tech/ui/analyticsnow exports only the charts and their parts.-
Breaking: import
engine,Engine,Coordinator,Selection,JoinGraph,Relation,RelationExpr,relationKeyand the otherrelation*helpers,numbers,queryFailure,Query,TableExpr,ExprValue,FilterExpr,MosaicClient,makeClient, everyclause*,onRelation, theload*loaders, the aggregates,sql,ascanddescfrom@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";
binand the six axis marks (axisX,axisY,axisFx,axisFy,gridFx,gridFy) stay on/analytics.@kanzo-tech/uino longer lists@uwdata/mosaic-coreand@uwdata/mosaic-sqlas 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 itDashboardandAnswerCard) reads a relation'sSUMMARIZEonce 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-query5 and render oneQueryClientProviderabove 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.queryFieldStatsis gone: useuseFieldStats, or runSUMMARIZEyourself and pass the rows tofieldStats. Clear your client on sign-out.
- Breaking: install
- Breaking:
valibotis an optional peer of@kanzo-tech/ui/analyticsrather than a dependency of@kanzo-tech/ui. Install it if you use/analytics; an app that never imports/analyticsno longer installs it. - Every component takes the words it draws through one prop,
translations. ThePreferencesparts take theirs once onPreferencesRoot, and a part's owntranslationswin for that part. A translated panel no longer has English left in it.- Breaking: pass
translations, notcopy, toPreferencesPanel,PreferencesSectionsandPref. The type isPreferencesTranslations(wasPreferencesCopy).PreferencesPanel'stitleandhintare entries of itstranslations. The retired-theme notice's words areretiredTitleandretired(theme)(wereretired: { title, formatDescription }). - Breaking: pass
translations, notlabels, toFindingsRoot,FindingsItem,FindingsGroupandFindingsTally. The type isFindingsTranslations(wasFindingLabels). - Breaking:
MadeWith'sprefix,infixandloveLabelare entries of itstranslations:<MadeWith translations={{ prefix: "Hecho con", infix: "en", loveLabel: "amor" }}>.
- Breaking: pass
Problemtakes the host's words for a code throughdescribe(code, data), which returns aProblemDescription({ title?, detail?, link?, page? }, the same fields as before).translationsstays for the wordsProblemdraws itself.- Breaking: rename
copy=todescribe=onProblem, and the typeProblemCopytoProblemDescription. The function you pass is unchanged.
- Breaking: rename
RelationPickerinflects 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 oftranslations(RelationPickerTranslations, from@kanzo-tech/ui/analytics), including the two sentence patternsout(to, label)andin(source, label, destination).- Breaking:
pluralis gone. A sentence that reads wrong isphrase(edge, direction)'s to reword, ortranslations.in's. A test that found a hop by its sentence reads the new one:RelationPickerHarness.add("Comment · located in this place").
- Breaking:
- One facet item.
DataTableFacetFilterandChartFiltertakeitemsofFacetFilterItem({ value, label?, icon?, count? }, from@kanzo-tech/ui), asFacetFilteralready did.DataTableFacetFilterkeeps an item's owncountand gives an item without one the column's count.- Breaking: pass
items, notoptions.DataTableFacetOptionandChartFilterOptionare gone; type your items asFacetFilterItem. AChartFilteritem is an object with a stringvalue({ value: "3" }, not a bare3).
- Breaking: pass
- Breaking:
ChartPickX,ChartPickY,ChartPickColor,ChartBrushX,ChartBrushYandChartBrushXYare gone from/analytics. WriteChartToggleX,ChartToggleY,ChartToggleColor,ChartIntervalX,ChartIntervalYandChartIntervalXY; they were the same components under a second name. - Breaking:
@kanzo-tech/ui/tableno longer re-exports TanStack'sTabletype, becauseTablefrom@kanzo-tech/uiis the markup component. Writeimport type { Table } from "@tanstack/react-table"where you name the instanceuseDataTablereturns. Preferences,ThemeNoticeandFloatingPanelleave 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>.PreferencesPropsis gone. - Breaking:
ThemeNotice,ThemeNoticePropsandThemeRetiredCopyare gone. ReaduseKanzoTheme().retiredThemeand toast it once, as/docs/hooks/use-kanzo-theme#the-retired-theme-as-a-toastdoes. - Breaking:
FloatingPanel,FloatingPanelResizeHandleand their props are gone. Copy/docs/layout/floating-panel's example.
- Breaking: replace
- Six names no product called leave the package:
KanzoThemeandKanzoThemeProps. A region that wears another theme isdata-theme="<theme>"and the side's class on an element of yours (/docs/themes#theming-a-region).KanzoThemeProviderstays the one themer.KanzoThemeName, which wasstring. Writestring.safeParseColor.ColorPickerstill takes a blank or half-typedvalue; for a parse of your own, wrapparseColorin atry.useFindings. Read the findings where you passed them toFindingsRoot.- From
/analytics,parseDashboardandrecommend(withRecommendationandRecommendIntent). Read one relation's spec withparseDashboards({ byRelation: { [relation]: saved } });autoDashboardis the recommender's door.
- A question is one part:
Question, withQuestionPrompt,QuestionDescription,QuestionChoices,QuestionChoice,QuestionInput,QuestionTextareaandQuestionError. It works on its own, as a guided chat draws a decision in a turn, and inside aQuestionnaire, which is now only the sequence around its questions: the root,QuestionnaireProgressand the actions. On its own a question holds its answer asvalue,defaultValueandonValueChange({ value }), and a pick answers it in one press; inside aQuestionnairethe answer is the questionnaire's, under the question'sname.nameis also the form field's name, so a question in a plain<form>submits its answer.- Breaking: inside a
Questionnaire,QuestionnaireItemisQuestion,QuestionnaireTitleisQuestionPrompt,QuestionnaireDescriptionisQuestionDescription,QuestionnaireChoicesisQuestionChoices,QuestionnaireChoiceisQuestionChoice,QuestionnaireInputisQuestionInput,QuestionnaireTextareaisQuestionTextareaandQuestionnaireErrorisQuestionError, with the same props.useQuestionnaireItemisuseQuestion, which also carriesanswerandsetAnswer. The types follow:QuestionnaireItemProps,QuestionnaireChoicesPropsandQuestionnaireChoicePropsareQuestionProps,QuestionChoicesPropsandQuestionChoiceProps, andQuestionnaireAnswerisQuestionAnswer. - Breaking:
useQuestionnaire()carriesanswers,questions,stepsandtranslations; itsblocked,indexOf,setAnswerandshortcutsare gone. Answer from inside a question withuseQuestion().setAnswer. - Breaking: the parts'
data-slots follow their names:questionnaire-itemisquestion,questionnaire-titleisquestion-prompt, and every otherquestionnaire-…slot of a question's part isquestion-….
- Breaking: inside a
QuestionnaireandQuestiontake their words throughtranslations.Questionnaire's areprogress(position, count),navigation(the actions row's accessible name),previous,skip,next,submit, and its questions'unanswered, which reach every question inside it; aQuestion's owntranslationswin for that question. An action given children still says those.- Breaking:
QuestionnaireProgresstakes no children. Translate it with<Questionnaire translations={{ progress: (position, count) => `Pregunta ${position} de ${count}` }}>, whicharia-valuetextreads too.
- Breaking:
- A dashboard tile carries its own question:
where, conditions under the page's filter ({ field, in },{ field, between }or{ field, contains }, theConditiontype; a field is a path from the relation's root, crossed in when it leaves the relation), andtop, 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;TileViewdraws a tile of any kind, withactionsin its header.TILE_ROWS,TileContext,TileQuery,Condition,ConditionValueandTileViewPropsare exported from@kanzo-tech/ui/analytics. A tile naming a field or path its relation cannot read draws what it lacks.- Breaking:
measureExprandbucketExprare no longer exported: read a tile's measure and rows throughtileQuery. A host that reads tiles itself reads theirwhereandtoptoo. A tile's fields are paths (affiliation.university.name, notUniversity.name), so delete dashboards saved with type-prefixed fields.
- Breaking:
- A
countcounts what is measured. Across a hop it iscount(DISTINCT …)of the relation's root key, so prizes by university counts a prize once per university however many joined rows lead there (Malloy'scount()). 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
countover a relation with hops is lower wherever a hop fanned out; a measure that names apathmust be acount.
- Breaking: a
ChartCardticks 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,
useFieldStatsreads each column's description and the counts fossil's writer stored, and queries nothing; it runs oneSUMMARIZEonly 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 isxsd:gYearis a time.FieldStatcarries its commonestvalues, itsannotationsand itslabel, and a tile's title andRelationPicker's sentences read the data's labels in the page's language.- Breaking: a column named
id,urioririis 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 acandidateKeysentry of a type in the joins you handMosaicProvider) orexcludeit. - Breaking:
useFieldStats(relation)takes the relation asChartRootdoes (a table by name, a path over the provider's joins, or SQL).fieldStats(columns, options)takes column descriptions (FieldColumn), notSUMMARIZErows: describe a relation of your graph withrelationColumns(graph, relation).SummarizeRowis gone.
- Breaking: a column named
Eyebrow: the small, muted, uppercase line that names a block from above it (Parameters, Theme). It renders a<div>, andasChildputs the look on the element it heads (<h3>,FieldLegend,FieldLabel,SliderLabel). It can replace a heading you wrote asfont-medium text-muted-foreground text-xs uppercase tracking-wide.
@kanzo-tech/mosaic (minor)
JoinResolveris the one door to the joins. Beside the page's crossfilter it answerssteps(relation)(each step'spath,typeandkey, 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)andat(to, from);crossing(from, predicates, to, at?)takes the step to cross in at. A miss is aJoinMiss(join/no-step,join/no-fieldorjoin/branches). A path is Malloy's: an edge's label,^labelback 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:
relationQuerywritescountry,knows.countryand^hasCreator.lengthwhere it wrotePerson.country,Person2.countryandPost.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,RelationNeedsandResolvedare gone. A step's key isnew JoinResolver(graph).steps(relation)(steps.map((s) => s.key)is what a dashboard leaves out of its fields,steps[0].keythe root's), and a question's relation isresolver.relation(root, paths).
- Breaking: a relation's columns are named by their path from the root, not by their type:
- The one door to Mosaic. It gains the aggregates (
avg,min,max,median,quantile,stddev,modebesidecountandsum),verbatim, and the builders the charts write with. Everything@kanzo-tech/ui/analyticsused to pass through is imported from here; see the@kanzo-tech/uisection above. - The types
ClauseParts,AntiJoinMetadata,RelationMetadataandRelayOptionsleave the entry.clauseParts(clause)andrelaySelection(from, to, options)are unchanged; where you named one of these types, useReturnType<typeof clauseParts>orParameters<typeof relaySelection>[2]. - A join graph describes its columns.
JoinType.columnsareJoinColumns: a column'snameand DuckDBtype, and, where its catalog states them, itsrole,datatype,termType,iri,nullable,annotations(JoinAnnotation) and what its writer counted (stats, aJoinColumnStats). A type and an edge may carryiriandannotations, and a typecandidateKeys.relationColumns(graph, relation, language?)describes the columns a relation reads, under the names it reads them by, each with thelabelthe data gives it. A multi-valued property is a type with avalue, reached by an edge whosetableis its own: the hop is one join, and the path to it names its value.labelOf(annotations, language?)reads a thing'srdfs:labelin the page's language,namingColumn(type)the column whose property is a labelling property, andsplitWords(name)is the last resort for a name nothing labels.- Breaking: a
JoinType'scolumnsare descriptions, not names: writecolumns: [{ name: "country", type: "VARCHAR" }]where you wrotecolumns: ["country"], and include the key column if you have its type. A graph built from a fossil corpus comes from@kanzo-tech/graph'sreadJoinGraph; build it there rather than by hand.
- Breaking: a
@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,ReadAnswerRelationsOptionsandAnswerFieldare gone, and so aretypesonuseAskChat's andaskOffers' options andAskInput, and onAskChips'over. Pass the graph alone (useAskChat({ model, engine, graph, selection })) and delete the read that builttypes. A field's commonest values areFieldStat.values.
- Breaking:
-
An Ask answer is a relation and a tile.
Answeris{ relation, tile }, the tile holding its ownwhereandtop, and Ask's transport writes it as adata-answerpart, once as asked and again under the sameidonce read, not as a tool call no model made.AnswerCardtakes that part's data asanswerand draws it with the dashboard'sTileView, so Add to the dashboard adds the question whole.Chatdraws data parts through adatarenderer map besidetools(ChatDataRenderers), each handed{ stopped }.- Breaking: draw answers from
Chat'sdatarenderers underanswer, not fromtools.answer:data={{ answer: (part, { stopped }) => <AnswerCard answer={part.data as AskOutput} graph={graph} stopped={stopped} /> }}.AnswerCardtakesanswer, notpart;AnswerCardTranslations'rowsis gone.Answer'swhere,showandtoparetile.where,tileandtile.top.answerSchema,checkAnswer,AnswerInput,ConditionandConditionValueare gone from@kanzo-tech/ai/data;Conditionis@kanzo-tech/ui/analytics'.
- Breaking: draw answers from
-
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, andJoinResolverresolves 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 adata-askpart, drawn with@kanzo-tech/ui'sQuestion, and the pick is sent asaskEdit(turn.spec, { [decision.id]: option }). The sentence after an answer is anIntltemplate (Physics: 230; Chemistry: 200.) whose separators areuseAskChat'stranslations(AskChatTranslations).- Breaking: a spec's fields are paths from
of, notType.column:{ of: "LaureateAward", show: { …, x: "university.name" } }. A type alone's fields go by their own names (gender, notPerson.gender). Delete code that numbered a reply's options or matched the reader's 1 or 2: answer a decision withaskEdit(spec, decided);AskTurn'schoicesis gone.
- Breaking: a spec's fields are paths from
-
A chat can be kept. Give
useAgentChatoruseAskChatanidwithload(id, { signal })andsave(id, messages), and optionallykeep(part, message). The chat of that id is read once before the first turn, validated with the AI SDK'svalidateUIMessagesagainst thedataPartSchemasyou give the hook, and saved, every message, at the end of every turn, one save at a time.loadanswers[]for a chat never saved.keepreturns the part to store, a rewritten one, orundefinedto drop it; a message left with no part is not stored, and what it keeps must validate or the save is refused.chat.keptsays where the chat stands:loading,ready,failedwithchat.kept.error, orunsaved.Chatdraws it: its skeleton while the chat is read, and the failure under the transcript throughProblem, with the composer inert when the chat could not be opened.translationsgainsunreadandunsaved.useAskChatvalidates itsanswerandaskparts with its own schemas, so you pass it nodataPartSchemas; to keep no figure of the data, havekeepwrite an answer'srowsas[].useKeptChatdoes 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 yourloadandsave.
-
Two parts for a guided chat:
Checkpoint,CheckpointIconandCheckpointTrigger(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 aMessageListbetween twoMessages, since it is an<li>. It restores nothing itself: the trigger'sonClickis your restore.translations={{ restore }}or children give the button your words.PromptInputHeaderis 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'sMenuwith aMenuCheckboxItemper file. A press on the row outside a pill puts the caret in the field.
-
Workingtakesvalue(WorkingProps). A number from 0 to 100 drawsProgressunder the words at that value,nulldraws it indeterminate, and leaving it out draws no bar, as before.Tool's Parameters and Result headings areEyebrowand look the same. -
One
Proposal, the Task's, for a field, a list and a chat.Assistreports each candidate as aProposal<string>:valueis the text, andtargetis where it lands ({ insert },{ replace }or{ add }), withrationaleas the chip's tooltip.Chattakes 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
Proposalfrom@kanzo-tech/ai/task, not@kanzo-tech/ai. On anAssistEvent'sproposal,textisvalueandrangeistarget. PassChatthe proposalsproposeoraskOffersanswered as they are, not mapped to{ text }, withmessage={askEdit}for Ask's; a fixed list of questions isProposal<string>s.askOffersreturns the Task'sProposal<AskSpec>[], not{ text, spec }.
- Breaking: import
-
Ask's answers are read once per page, on your own TanStack Query client.
useAskChatreads an answer asked again under the same chips once, and Stop cancels the read at once.- Breaking: install
@tanstack/react-query5 and render oneQueryClientProviderabove Ask (@kanzo-tech/ai/data).ChatandAssistfrom the root need none.
- Breaking: install
-
Breaking: rename
copy=todescribe=onChat,ModelBadgeandAnswerCard, which pass it through toProblem; the type isProblemDescription. -
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,MessageAvatarPropsandMessageActionsare deleted, andMessageRoleleaves the entry.Message,MessageListandMessageContentare unchanged; put anAvatarfrom@kanzo-tech/ui, or a row of buttons, inside aMessageyourself.- From
@kanzo-tech/ai/task,TaskState. Where you typed anonStatecallback, useRunOptions["onState"]. - From
@kanzo-tech/ai/data,askTransport,readAnswer,askAlternatives,linkandquestionOf.useAskChatis the chat,AskChipsdraws the alternatives, andaskOffersreturns each offer with its question.
-
Breaking:
Question,QuestionPrompt,QuestionDescription,QuestionOptionsandQuestionOptionare gone from@kanzo-tech/ai. Import the question part from@kanzo-tech/ui, giveQuestionaname, and writeQuestionChoicesforQuestionOptionsandQuestionChoiceforQuestionOption: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.valueis aQuestionAnswer: astringhere, astring[]for amultiplequestion. 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:
runTaskandproposeask through@kanzo-tech/llm'sgenerate, so the first failure is the one you see. Nothing to change on your side. -
ModelBadge,Chatand the rest name a model's failureLlmError/llm/unavailable(see@kanzo-tech/llm).
@kanzo-tech/llm (minor)
generate: a model's answer read whole ({ text, output }), from the same options asstreamplusproviderOptions. It asks once, so a refusal rejects with what the model threw and is never retried.@kanzo-tech/llmno longer re-exports the AI SDK. It carriesstream,generate,LlmErrorandModelLoad, and nothing ofai'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 typesAgent,ChatStatus,DynamicToolUIPart,InferAgentUIMessage,LanguageModel,ReasoningUIPart,Tool,ToolSet,ToolUIPartandUIMessage. For example,import { ToolLoopAgent, tool, type LanguageModel } from "ai".
- Breaking: import the SDK's names from
- The failure is
LlmError, codedllm/unavailable, so its area is the package's name.- Breaking:
AiErrorisLlmError, andai/unavailableisllm/unavailable. Rename both where you catch the error or key your error registry on the code.data.reasonis unchanged, and@kanzo-tech/ai'sModelBadgeoffers no retry forllm/unavailable, as it did for the old code.
- Breaking:
localModelresolves 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 ownAppConfig({ model_list: [{ model, model_id, model_lib, … }] }), withmodelandmodel_libresolved against/modelswhen they are relative. Write that file wherever you download the models. localModel(),loadLocalModel()andlocalModelLoad()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 asLlmErrorllm/unavailablewithdata.reason"unserved". - Breaking:
localModel(id, settings)andloadLocalModel(id, settings)take no settings, andLocalModelSettingsis gone; moveworkertolocalProvider({ worker }).from: { weights, library }is gone: list the model inapp-config.jsoninstead, with whatever WebLLM's prebuilt record said of it (itsoverrides, among them the context window, andvram_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.
- At your composition root, before anything asks a model:
- One model double, for unit tests and end-to-end tests alike.
@kanzo-tech/llm/testingexportsscriptedModel(script), the AI SDK'sMockLanguageModelV4answering from a script of turns:{ when?, once?, reply }, a reply being{ text, reasoning? },{ object },{ tool, input },{ fail, after? }or{ hang: true }, with an optionalpaceand a failingload. A structured answer is{ object }, not{ text }holding JSON. Pass it where a model goes. In a unit testturnsmay be a function of the call.playwright(page, { models })anddom(container, { models })from@kanzo-tech/testingregister, 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, andlocalModel()resolves the first id scripted: no WebGPU, nothing downloaded, noapp-config.jsonfetched. Each prompt is recorded onwindow.__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/testingis an optional peer of@kanzo-tech/llm, and/testingneeds it installed.
@kanzo-tech/testing (minor)
ModelScript,ModelTurnandModelReply, the model double's script types;scripted(script), the answeringscriptedModelwraps;promptOf(call), which reads what a call was told; andplaywright(page, { models })/dom(container, { models }). See@kanzo-tech/llmabove.- The harness API takes Angular CDK's names wherever CDK has the operation. Breaking, in your page objects and specs:
env.harness(isenv.getHarness(,env.harnesses(isenv.getAllHarnesses(, andenv.rootisenv.rootElement.- Inside a harness,
this.locate(query)andthis.one(by, what, within?)are onethis.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. HandleisTestElement, andHarnessTypeisComponentHarnessConstructor.- On a
TestElement,.attribute(is.getAttribute(,.press(is.sendKeys(, and.rect()is.getDimensions(), anElementDimensionswithtopandleftin place ofyandx;RectisElementDimensions. Severityis gone: it isFindingSeverity, from@kanzo-tech/ui.FindingsItemHarness.severity()answers the same three values; a test package that does not install@kanzo-tech/uican name it asAwaited<ReturnType<FindingsItemHarness["severity"]>>.
@kanzo-tech/graph (minor)
readJoinGraphdescribes every column of a corpus as its catalog does: its DuckDB type,role,datatype,termType,iri, whether it isnullable, its annotations (itsrdfs:labelamong them) and what fossil's writer counted in it (stats), and each type'scandidateKeys,iriand annotations. An edge's label and its endpoint columns are read fromfossil_tables, never parsed from the table's name or assumed to besrcanddst. 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,RelationPickerand 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/corpus0.3.0-alpha.32attaches (fossil_tableswithlabel,source_key,destination_keyandcandidate_keys;fossil_columnswithterm_type;fossil_annotations;fossil_stats). Upgrade@fossil-lang/corpusto^0.3.0-alpha.32; a corpus attached by an older one does not read.
- Breaking: the graph reads the catalog
- Every part takes the words it draws through
translations, a partial record, English by default, and renders through Ark with its element's props,asChildamong them. Each part's words have a type:GraphCanvasTranslations,GraphToolbarTranslations,GraphLegendTranslations,GraphCountsTranslations,GraphStatusTranslations,GraphSearchTranslationsandGraphInspectorTranslations. A sentence written from figures, such as 5K nodes · 8K edges, is a pattern over them (nodes(count),edges(count), …).GraphLegendis aCardand takesCard's props,GraphStatustakesBadge's, andGraphSearchtakes its cue's, aButton's.- Breaking: install
@ark-ui/reactbeside@kanzo-tech/graph. It is now a required peer, the same one@kanzo-tech/uialready asks for. - Breaking:
GraphSearch'splaceholderis an entry of itstranslations:<GraphSearch translations={{ placeholder: "Find a person…" }} />.
- Breaking: install
- Breaking:
SelectionandSelectionSourceareVertexSelectionandVertexSelectionSource(the canvas's selection,GraphState.selectionandonSelect's argument), andPick, whatusePickreturns, isVertexPick. A file that imported graph'sPickgets TypeScript's back, and one that aliased Mosaic'sSelectionto get past graph's can drop the alias. ShapeGlyph(withShapeGlyphProps),scaleOfandplacementFromleave the package. A category's glyph and colour reach a page throughGraphLegend,GraphInspectorandGraphSearch; the placement a preferences panel chose isuseGraphPrefs().placement.ChannelsandPlacementstay as types.
@kanzo-tech/theme (minor)
- Breaking:
Problem, whatvalidateSectionandvalidatePrefsreturn, isSectionProblem. - Ten names no product called leave the package:
KanzoThemeName, which wasstring(writestring); the section helpersLookDocument,sectionOf,withSection,fallbackChainandresolveSectionToken; and the density self-check,checkDensity,DENSITY_OBLIGATIONS,DensityCheckandDensityObligation. A section's payload is still yours to read and write undersections[namespace], andvalidateSectionstill checks it against its manifest.
@kanzo-tech/auth (minor)
- The session is a query on your own TanStack Query client, and stays current.
useSessionreads 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-query5 and render oneQueryClientProvideraboveAuthProvider. Without itAuthProviderthrows TanStack's No QueryClient set. A hand-writtenAuthmust read the session afresh on everygetSession():AuthProviderholds the only copy.
- Breaking: install
- Every refusal that carries a code is an RFC 9457 problem. The auth routes, the API forwarder and the proxy answer
application/problem+jsonwithstatus,title,detailand theAuthErrorCodeascode, and every code has one status everywhere (organization/deniedis 403 from the routes as from the forwarder). The forwarder names the code it refused with, so you can routeorganization/deniedortoken/exchange-failedinstead 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/unreachableinstead 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
codeanddetailwhere you read{ error, message }. A forwarder or proxy refusal that had no body and now has a code carries one.
- 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
auth.session({ required: true })sends an outage of the session store or the IdP to yourproblemPagewith its code, as the proxy does.auth.session()withoutrequiredstill throws theAuthError.- Breaking: delete the
trythat redirected to the problem page around it.
- Breaking: delete the
bffAuthbounds the two requests it makes itself,/sessionand/refresh, and no other.- Breaking: do not wrap the
fetchyou passbffAuthin 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.
- Breaking: do not wrap the
organizationConsoleUrl(issuer, organization)links a Members page to the organization's settings in Keycloak's admin console, from the issuer and the organizationorganizationOf(session, alias)returns, with no setting needed. It isundefinedfor an organization the realm sent without itsid.TenantRequest, what yourorganizationresolver is handed ({ url, headers, cookies }), is exported from@kanzo-tech/auth/next.- Breaking:
singleFlightandreadSessionare gone from@kanzo-tech/auth. ImportclaimsandClaimsConfigfrom@kanzo-tech/auth/serverinstead of the root.sealedCookie,cookieValue,SealedCookie,SealedCookieConfig,rewriteOrigin,organizationGroups,OrganizationGroupsandOrganizationGroupsConfigare gone from@kanzo-tech/auth/server; write the admin-API lookup of a group overage with your own service account. - Breaking:
services/compose.yamlno longer defaults the identity service's passwords. A deployment must setKC_DB_PASSWORDandKC_ADMIN_PASSWORD; without them Compose refuses to start and names the one missing. Development is unchanged:services/compose.dev.yamlreadsservices/auth/dev/dev.env. Runningservices/authdirectly, pass--env-file dev/dev.envbeside-f compose.yaml -f compose.dev.yaml. - The realm no longer carries the
movedblocks 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 thekanzo-conformanceclient.