Changelog
0.17.0, 2026-11-13
Shorter everyday paths: a workbook becomes a workspace in one go, you shape it on a canvas, and change sets publish in two clicks. You drag things into place beside the live result, and five themes let refiroo look the way you like.
Releases are numbered by plan, not by date, so a higher number can ship before a lower one. All releases
Full changelog
Released 2026-11-13. This release was not tagged in the repository. This is the changelog's own text for the release; its links to files point at those files at the tag.
0.17, the plain path, released together with 0.14, the management API reference: a workbook shaped on a canvas and made into a workspace in one step, the canvas as a full model editor, themes with their frames, a page for each workspace, Settings by task, a field that saves when it is left, an e-mail template, account security messages, the e-mail as the sign-in name, any view in a navigation group, exports that follow the view, and the complete management API reference with its error catalogue. It also holds what shipped earlier as 0.16.1-alpha.1 to alpha.5 (0.13, the documentation) and 0.16.2-alpha.1. The release review's findings are under "Fixed (the 0.14 and 0.17 release review)".
Added (0.17: upgrade and first-deploy scripts for Docker deployments)
scripts/upgrade.shupgrades a Docker Compose installation in one command: it checks the host (Docker,compose.yml, the env file, the running database, free space of twice the database's size), backs up with one time stamp (the database aspg_dump -Fcproved readable withpg_restore --list, thejob-filesvolume, the env file at mode 600, optionally the assistant volume; old backups stay), pulls the registry images, builds, stops the engine, runspython -m app.cli.database_upgrade, starts everything, waits for/healthand prints the engine version and database revision before and after with the commands that restore the backup. A failed migration leaves the engine stopped and prints those restore commands. Options--backups,--with-assistant,--skip-backup,--yes,--dry-run,--force,-f,-p,--env-file.scripts/deploy.shmakes the first deployment, on a server or a laptop (--local): it asks for the address, the HTTPS mode (automatic certificates, your own files, a load balancer, or plain local HTTP) and the first administrator, writes the env file from.env.examplewith generated secrets (mode 600, never printed, never over an existing file without--force), checks Docker, Compose 2.24+, the ports and the disk, builds, creates the database, starts everything, waits for/health, creates the administrator and prints the next steps. It refuses a host that already holds an installation. Both scripts sharescripts/lib/ops.shand take--yesand--dry-run.
Fixed (the 0.14 and 0.17 release review)
- Every error response in the API reference has an example body: each code an operation lists shows the refusal's
{"error": {"code", "message"}}envelope, instead of an empty response. - A provider's e-mail address is taken only when it can be trusted (R17-F20): an
email_verified: falseclaim is never taken as a person's sign-in name; a provider has a new setting,emailsTrusted(Settings › Security › Sign-in,POST|PATCH /sign-in-providers), and when it is off (the default for a new provider) an address is taken only withemail_verified: true, else the sign-in is refused (email_unverified) and tells the person to ask an administrator; the same rule applies when a later sign-in brings a new address; the "address is taken" message no longer names the address; two people moving to the same new address at once no longer end in a server error (R17-F26). - The e-mail template's text keeps links and names intact (R17-F21): Markdown marks are removed from the template alone, so an underscore in a link's token or
*x*in a name stay as they are. - A queued message is sent with the logo it was rendered with (R17-F22): the logo is kept by its checksum while a waiting message refers to it, the logo is no longer read for every message, and a replaced logo does not change a message already queued.
- An uploaded SVG logo is checked as XML (R17-F23): a script in any namespace, an
on*attribute, a link that is not#or an inline image, an imported or external style,foreignObject, a DTD or a stylesheet instruction is refused. - Every mail greets once (R17-F24): inside the operator's template the message's own "Hello" is dropped and the template greets (as the default does); without a template the message greets itself.
- Placeholders in the template are filled in one pass (R17-F25): a name, address or message that looks like a placeholder or a stand-in is never filled again.
- The first save of the e-mail template at the same moment no longer fails (R17-F26): the second request is answered
409 VERSION_CONFLICT. - The export reference says how a matrix cell is listed (R17-F27):
cellFieldisname, the display attribute or any other text or long text attribute the type binds, and left out the cell lists the record by its name alone. - The navigation reference says a group entry may be any view (R17-F28):
OutlineNavigationEntry.viewandentriesno longer speak of relation matrices only. - Three error lists are complete (R17-F29): the e-mail preview also answers
MAIL_TEMPLATE_TOO_LONGandREQUEST_VALIDATION_ERROR, the logo uploadREQUEST_VALIDATION_ERRORfor a missing file, and an exportREQUEST_VALIDATION_ERRORwhen it names neither a scope nor a view. - The reference test looks deeper (R17-F30): it checks the descriptions of fields nested inside a schema and validates every recorded example body against its operation's request or response schema.
- The guide coverage check is stricter (R17-F31): text in headings no longer counts, a one-word label counts only beside a context word on the page, and the inventory lists mail message kinds, search boxes and Docker build arguments; the entries now uncovered are in
guide/_coverage/notes/r17-uncovered.md. - A workspace made from a workbook cannot be made twice (R17-F14): a success raises the draft's version, so sending the same request again is
409 VERSION_CONFLICTand makes no second workspace and import (the route takes noIdempotency-Key: that key is scoped to an existing workspace, and this request creates it). - Placing new cell records is worked out in the database (R17-F15): the containers under every column record, their ancestor chains and the candidate criteria are read for the whole set of column records in recursive statements instead of one Python walk of every container per column record; the answers are the same and the statements do not grow with the number of column records.
- A workspace from a workbook refuses a draft that is not free (R17-F16):
409 DRAFT_HELD_BY_WORKSPACEwhen another workspace remembers the draft,409 AI_TASK_DRAFT_HELDwhen an assistant task names it, as opening a workspace's model never takes either. - Two workspaces made at once under one name no longer fail with a server error (R17-F17): the choice of a new workspace key is taken one request at a time, so the second gets the next free key, or
409 WORKSPACE_ALREADY_EXISTSfor an explicit key. - Dropping a reading unmaps every binding that names its added columns (R17-F18): not only a binding's
columnbut itscolumns, a relationship's ID, version and value columns, a related record's ID, version and parent column, and the key's version and name columns, as documented, instead of failing withCOLUMN_NOT_FOUNDwhen the next file is read. - A filled-down value no longer carries across a heading (R17-F19): with
headingsandfillDown, each heading row starts a new group, so an empty cell never takes a value from the group above. - A sheet tab has a menu (R17-F1) with "Make an object type of this sheet", by mouse or keyboard and on a phone, so making an object type of a sheet no longer needs a drag.
- Two quick gestures on the canvas both apply (R17-F2): steps are sent one after another, each at the version the previous answer returned; a real conflict from someone else still shows the stale notice.
- A rename on the canvas says what happened (R17-F3): no "Renamed." when the draft had moved on and nothing was saved, and the rename toast has an Undo.
- A toast's Undo takes back its own step (R17-F4), not the draft's latest one; the toolbar Undo still takes the last.
- The canvas no longer has its own "Remove object type" (R17-F5): it never removed a type loaded twice or related, yet said it was removed. The editor's "Remove type" is in the same menu, and "Don't load this list" stays in the sheet's panel.
- Renaming an object type on the canvas sends what the editor's heading sends (R17-F6): the name alone, the plural name left as it is; one shared builder, checked in the parity test.
- Reading a row as the header changes only the list the preview shows (R17-F7), not every list on the sheet.
- A relationship row's rename says "Name of the relationship …" (R17-F8), not "attribute".
- A refusal is shown even before the model outline is read (R17-F9) instead of being dropped.
- Consumer APIs names the chosen workspace's own model release (R17-F10): the release row and "Open the release" followed the default workspace even when another workspace's APIs were listed.
- A tab left open across a deployment reloads once (R17-F11) instead of showing the error screen when a page's code file no longer exists; a second failure still shows the error.
- The e-mail template asks before you leave with an unsaved draft (R17-F12), as other unsaved edits do.
- The change set band shows who and when again, and the submitter's comment (R17-F13); Details names the publication a change set started from by its number, no longer by its identifier.
- Settings paths in the project documents use the new groups (R17-F34, documents): You, Workspaces and models, People and access, Connections and This instance replace Security, Integration, Administration and Data operations in OPERATIONS, CURRENT_FUNCTIONALITY, USER_JOURNEYS, UI_ACCEPTANCE, PRODUCT and ARCHITECTURE.
- The 0.17 upgrade warning about e-mail addresses (R17-F35, OPERATIONS): the upgrade stops, changing nothing, when an active person has no e-mail address or two people share one ignoring case; the section gives the queries to find them, what to do, and says the database stays at its previous revision.
- ARCHITECTURE covers the 0.17 mechanisms (R17-F36): the e-mail template, security messages and the per-e-mail failure counter, the assistant session in a container, the webhook private-address refusal,
FORWARDED_ALLOW_IPS, the content security policy, managed sign-in, a workspace from a workbook, the canvas layout storage and the removed username. - The current-functionality list names 0.17's group entries and security messages (R17-F37): any view can be placed in a group; people are mailed when their password, second factor or passkey changes.
- The mail chapter of OPERATIONS describes the e-mail template and the security messages (R17-F38): where the template is edited, that it wraps every message and that it is in the database, so in the backup and restore.
- The 0.17 plan no longer names the sample workbook's content (R17-F40).
- The guide's navigation-group pages describe any view as an entry (R17-F32): the group page's Find a collection or view and its messages, a view entry's kind and Opens on, and Move to group… on every view.
- The guide describes the sidebar's Find a list (R17-F33) instead of saying the sidebar has no list search.
- Settings paths in the guide name today's groups (R17-F34): Workspaces and models › Import profiles and Copy and move, This instance › Jobs and AI.
- The upgrade page warns before the e-mail becomes the sign-in name (R17-F35): two checks to run on the old version, what to fix, and that a stopped upgrade leaves the database as it was.
- The mail page gives the notification e-mail location correctly (R17-F39).
- The guide names the 15 controls the stricter coverage check found undescribed (R17-F31): the workspace and Home searches with their ⌘K boxes, the canvas's find box and sheet menu, the search boxes of Settings › Model and Workspaces, Notifications, a group's members and a panel's related records, the Search or ask switch, the panel tools menu, and the build-reference arguments
REFIROO_BUILD_COMMITandREFIROO_BUILD_TAG. Route entries and search fields now carry context words, so a one-word label counts only where its place is named. - The guide's sign-in pages describe Trust the e-mail addresses … sends (R17-F20): when to turn it on, what a person sees when an address is not confirmed, and that the upgrade switched it on for existing providers.
- A stopped assistant session is collected, not left a zombie (R17-F41): when a turn ended by a stop, a timeout or a failure, the engine ended the session's process group but never collected the process, so it stayed a defunct child of the engine until the engine exited and was reported as "still running" when its handle was dropped. The turn now waits for it (and kills it if it has not gone after five seconds).
- Tests brought up to date with the changes they followed (R17-F42): the MCP clients of the agent-access tests use
streamable_http_client(mcp 1.30 deprecatesstreamablehttp_client); the relation matrix volume test patchesmatrix_read.current_pairs, where RF2 moved it; the batch statement budgets for live creates and upserts count the job response's origin read (0.13 JG); the narrowing and public-route lists of J47 name the job re-read, the deletion-impact read and the docs example route; API_JOURNEYS.md lists the e-mail template and from-workbook error codes with their journeys (J46.12 now also coversMAIL_TEMPLATE_TOO_LONG); J47.9 follows SM1 (turning on an authenticator app is told by mail); the startup tests expect migration20261203_01; the column-containers journey follows MC1 (an empty container is never the default); the import places statement test compares two sets that both need the proposal reads and counts only its own thread's statements; the starter-key check no longer meets an example key in a docstring. - The workbook pane's sheet tabs pass the accessibility check beside their menus (R17-F43): a sheet tab's actions menu button stood inside the Sheets tablist, which a tablist may not hold; the list now owns its tabs and the menus stand beside them.
- "What changed" on a workspace's Activity tab no longer skips heading levels (R17-F44): the section was an
h3straight under the page'sh1(and its facts anh4), which the accessibility check reports as a skipped level; the section is anh2there and its facts anh3, and stayh3/h4on a record's History. - Home no longer scrolls sideways at 390px (R17-F45): the page's one column grew to the widest content of its sections and the "New workspace from a workbook" button, which does not wrap, pushed the section head past the screen; the column may now shrink and the heading and its action wrap under each other.
- A workspace's Activity tab no longer skips a heading level (R17-F46): the day headings were level 3 under the page's level 1 whenever the AI summary above them is not offered (it then draws no heading); they are level 2, which reads in order with or without it.
- The new sign-in provider dialog spaces its two switches (R17-F48): each switch and its explanation stand as a field, so the trust switch's explanation no longer runs into the next switch.
- A list's columns fit the values they show (R17-F47): a table lays its columns out from the rows first drawn and keeps them still, so another page or rows scrolled to with a longer value (
long-9, thenlong-20) ended in an ellipsis while the name column had room to spare. A column that a drawn row cuts now grows to its widest value, up to the table's per-column cap, taking the room from the name column and never narrowing it below its own width; columns only grow, so they do not move back and forth while the list scrolls. - Visual baselines re-recorded after looking at every differing scenario: the 0.17 frame, Settings family, themes' current look, dialogs as popovers, views as tabs, PL5 identifiers out; masks moved to the new markup.
- Browser runners brought up to date with the changes they followed:
test:browser:authoring(UX2: Escape is pressed once the refused field has its focus back),authoring-a4a(R17-F2: waits for the queued Undo before reading the draft's version),authoring-a4c(UX2: a choice from a list saves as made, no Save button),authoring-r(ST1: Import profiles is a list in a Profiles region),pl3b(waits for the workbook pane at 390px),publications(a change set names its starting publication by number). - Browser runners brought up to date with the changes they followed (second batch):
wp12(SH1: the release header is the item header),s5(the lens switch's one-row mapping probe is not the grid's read),workspace-x5andworkspace-r(the workspace page's actions are a menu worked by arrow keys),settings(ST1: Deployment is rows of settings, so the content holds no tab stop),security-r(ST1: People and Groups are rows in regions; a person's page is headedh1; roles are chips in a list named Roles in), changes(UX1: Show more; a person's page ish1; See changes is an item of More actions),sign-in-providers,i4and the shared security surfaces (R17-F20: the harness provider sends noemail_verified, so it is added with its e-mail addresses trusted),operations(ST1: earlier rehearsals are setting rows),ai(ST1: Settings and Calls tabs, durations in seconds, Deployment rows; PL3b: the session stand-in stores its profile on the draft, so a keyed load first clears the profiles earlier loads saved),ap12(the dialog's intro holds the words the wait looked for),ap13(a session that has said nothing shows "Starting"),ao2(heading levels, R17-F44),ao3(UX1: the kinds' choices are radio groups in a fieldset; EM1: the follower is found by the e-mail address the run sets),link-mail(a person's page no longer holds the people list),ex1(NG1: a view in no navigation group is headed by its type's plural name),v1(the bundled console's token fields are named Token).
Security (the 0.14 and 0.17 release review: provider e-mail addresses)
- A provider's unverified e-mail address could be squatted (R17-F20): the e-mail is the unique sign-in name, and some providers let people set it, so an address could be taken by someone else and later receive roles granted by pasted address. See the fix above. The upgrade sets every existing provider to trusted so production sign-ins keep working; review each provider's Trust the e-mail addresses it sends and turn it off where people can set their own address. Contract change (additive):
emailsTrustedonSignInProviderCreate,SignInProviderUpdateandSignInProviderResponse; a provider created through the API without it starts as not trusted.
Changed (0.17: the view name comes first and shows when it is empty)
- In the model editor, every view's form, Add a view for each kind and Change on an existing view, now starts with Name instead of ending with it. The field has a red border while it is empty or only spaces, and the border goes as soon as you type something.
Changed (0.17: the lens row under a parent lists the mapping views that have mappings there)
- On a collection narrowed to a parent, the view tabs under the heading list only the mapping views that already have a mapped record under that parent, besides the tables, trees and relationship views and the view you are on. When no mapping view has a mapping there, or the answer cannot be read, every view is listed as before; without a parent nothing changes.
Changed (0.17 NG1: any view can stand in a navigation group)
- A navigation group places a view of any kind, not only a relation matrix: a table, a tree, a mapping grid, a
relationships table or a relation matrix.
{ "view": "<key>" }beside collection keys, in the group's one order, now accepts any view of the application (schema 0.10, additive;INVALID_REFERENCEfor a view the application does not declare,INVALID_MODELfor a view in a second group, as for collections).placeCollectionwithviewplaces, moves and takes out any view, and removing a view or giving it a new key drops or follows its member as it did for matrices. A grouped view has one sidebar entry in its group, named after the view, and its collection keeps it among its view tabs (PP-D11 to PP-D13). The sidebar and the group page show every kind as its own entry with the glyph of its kind; Move to group… on a view's row in Views and Add to group… in the group's panel offer every view;groupOperations(the canvas's group drop) takes a view through the sameplaceCollection. A document that named a table or tree in a group was refused before and is valid now.
Added (0.17 SM1: a message when a person's sign-in changes)
- A person is written to when their sign-in changes, so a change they did not make is noticed: their password was changed
(by them, with the current one) or reset (with a one-time link), an authenticator app was added or removed, a passkey was
added or removed (by them or by a security administrator). Choosing a first password sends nothing. The message says what
changed and when (UTC) and to contact an administrator if it was not them, sits in the saved e-mail template with its
plain-text part, and carries no password, code or link that signs in (D421). It is queued with the change in one
transaction, as the activation mail is, so a refused change sends nothing. A person cannot switch these messages off; with no
mail set up or no e-mail address the change is made and nothing is queued. The mail outbox lists them as kind
security(additive);identity.productnow holdsproduct_name, which was inidentity.second_factor.
Changed (0.17 CE1: shared pieces of the model editor, first part)
- An Editor | Canvas switch stands at the top of the model editor in place of the Open as canvas link, and the canvas page mounts the same one. A draft opens on the view the person used last (remembered per draft in this browser), and on the canvas from a workbook flow.
- What a load does with the records no row names (the API's
setMissing) now has a place in the interface: on a list's panel, Report them or Delete them, changed where it stands. Delete asks once and says what a load will then delete. It is not shown for a list with no key or one that loads no records of its own. - The canvas document of a draft (
GET/PATCH /api/v1/model-drafts/{id}/canvas) also keeps the folded cards and the chosen scope (model,changedorworkbook), merged by key and replaced as the positions are, outside the draft's version, history and releases (additive). - The editor's steps and refusals, operation dialogs, notices, workbook dialogs, More menu actions and the type preview are pieces another page mounts with props only; the editor uses the same ones.
- The canvas shows the model editor's own issues and history drawer (the same bar, counts, tabs, height and dry run), in place of its own foot and the "n to fix" in its strip. An issue about a relationship now selects and centres that relationship's line; it used to do nothing for a parent-and-child relationship, and the drawer no longer changes height between its tabs.
- The canvas's strip holds its tools: Find, Undo, Redo, Tidy and + moved from the top bar into the strip, after the Whole model | Changed in this draft | From the workbook switch. The switch, the tools and the summary (applications, object types, changed) each stand in a rounded box of their own quiet hue.
- "From the workbook" is offered only with a workbook: on a draft without one, the canvas's switch shows Whole model and Changed in this draft, and a scope kept on the draft falls back to the whole model.
- Add a workbook… stands beside that switch on a draft without a workbook, and the empty workbook pane is gone: the board takes its width. Once a workbook is added the workbook pane shows as for an import, and a draft that had no scope chosen stays on the whole model rather than switching to the workbook's (still empty) lists.
- Applications stand side by side when the canvas lays the board out (a draft with no places kept, or Tidy): as many application frames per row as fit, each with its share of the width, instead of the first one taking the whole row.
- A card is carried by its name too: a press on an object type's name moves the card once the pointer moves, and a click still renames it. Only the small grip and the gaps in the card's top bar carried it before, so a card seemed not to move.
- Folding a card closes up its column: the cards under it in the same application move up by the space it gave up (and down again on unfold), so its frame and lines shrink with it instead of leaving an empty area. Space left by folds made before this change goes with one press of Tidy.
- The canvas's layout editor and preview use one line in place of the strip: the type, Layout editor or Preview, the record to show, the note, and Undo, Redo, Preview (or Edit layout) and Back to the board, packed to the left. The application and workspace choices are left out there; the model editor's band keeps them. On a phone the line wraps, and the preview now stands as tall as the board (it had collapsed to a sliver).
Added (0.17 CE1: the canvas edits the whole model)
-
Names of object types and attributes on the canvas are no longer cut short where the row has room; a name that is still too long shows in full on hover.
-
The canvas does what the model editor does, on the same draft, with or without a workbook. The page uses the editor's editing hook, dialogs and notices, so every list, field, relationship line, navigation group and application frame is a target of the editor's own operation table: its menu (the ⋯ button, right click, Shift+F10) is the editor's
OperationsMenu, with the canvas's gestures (Put under…, Relate to…, filters) in a Canvas section, and its forms are the editor's. A test (parity.test.ts) fails when the table gains an operation of a kind of target the canvas has no place for. -
The whole model on the board: a switch Whole model | Changed in this draft | From the workbook (kept on the draft), a frame per application with its name on its border, Tidy inside the frames, released lists nothing changed drawn quieter, the editor's New / Changed / Released mark on each card, Find (Ctrl or Command with K; Enter centres and selects), zoom (− 100% + Fit, Ctrl or Command with the wheel; kept in the browser), a card folded to its top bar (kept on the draft). Opening a model, moving cards, Tidy, folding and zoom change neither the draft's version nor its model.
-
By hand: the editor's own Add menu in the header (one shared component,
AddMenu: Application…, Object type…, Attribute type…, Relation type…, group, with the editor's forms; an object type made there is selected and the board centred on it) and the editor's New attribute button at the foot of every card; names of object types and attributes change where they stand. The canvas uses the editor's words (object type, attribute) and "Propose again…" in More actions. An attribute row dragged onto another object type's card moves there (the editor's Move to…), and up or down within its card, or with Alt+ArrowUp / Alt+ArrowDown, changes its position (the editor's Move up / Move down; a line shows where it will land). A draft without a workbook folds the workbook pane away and offers Add a workbook…. -
Inspector (Inspector, Navigation, Views, Consumer APIs): the editor's element, column and list panels (with what a load does with the records no row names), a field's rows, the group operations, a list's tables and record page in the editor's preview and layout editing in the main area (Back to the board), the consumer API pane and panel. A click on a line opens its rule popover (how many, required, the fields it carries, which records may be chosen).
-
Issues and History in a foot drawer: an issue selects its card, field or line; a chosen step is undone, and a refused undo says what it depends on. The selection is kept in the address (
?show=canvas&element=…). -
Header: the Editor | Canvas switch, Find, Undo, Redo, Tidy, the editor's Add menu, Propose again… (in More actions), Create workspace… or Apply to this workspace… and Install and activate…, and ⋯ with Load into a change set…, Add a workbook…, Open as JSON, Delete draft…. The load into a workspace the canvas had is now the editor's own Load into a change set….
-
A layout stored before the cards grew is read apart: a card whose box overlaps another's is moved down below it, keeping its column, and the moved places are written back once to the draft's canvas (not as a model step), so no Tidy is needed.
-
What the Add menu makes that has no card is shown: an attribute type or a relation type made there is selected in the inspector (its own panel), a navigation group in the Navigation tab, each with a toast and Undo. The inspector's panel for such an element is held by the page and cleared by the next pick on the board. The Find field is wide enough for its hint.
-
Browser runner
test:browser:ce1(a model built from nothing and installed; a released model opened whole). Decisions PP-D86 to PP-D89 are numbered after the duplicate PP-D83 to PP-D85 of the same day were resolved (the PL3b ones keep their numbers). -
The model editor sets the workbook's three readings too (0.17 CE1, PP-D77): a column's panel offers Fill blanks from the cell above, Rows holding only this column are headings… and Read these columns as rows… (which asks which columns, then the two new columns' names and the object type they are added to, the list's own at first), each with its Stop entry while it is on; a sheet's list offers Read these columns as rows… beside its other actions; a column that a reading is on carries a mark in the source pane. The operations and the questions are the canvas's own, built once (
model-editor/readings.ts,ReadingAsks.tsx); each entry is one request and one undo step.
Added (0.17 PL3b: the workbook canvas)
- The workbook canvas page: sheets dropped on the board make lists; a column onto a list, a field or a list's Filters is one request and one undo step; Connect finds the column that names the other list over every row and binds it with the link (a tie is asked); several chosen columns are read as rows; the sheet's first rows with a pressable header row; fill down and heading rows from a column's menu; the reasons for refused rows with a value-map correction; Proposed on cards, fields and columns; keyboard paths and a one-column phone layout; Create workspace…, Apply, Load into an existing workspace…; Open as canvas in the model editor; PL4's Adjust opens the canvas.
- The model editor shares the canvas's draft steps, dependants dialog, workspace choice and Proposed logic.
- Fixed: the canvas read a column of numbers as text, and drew a line for a list that contains itself.
- Fixed (found by trying three workbooks): the canvas's Create workspace… now lands on the workspace's own page, where the load's progress and result are shown. A sheet dropped on a draft with no application, or on a board where a list already reads it, no longer fails or makes a second list. A column dropped on the empty board is now a new list of its values, linked back, with a source of its own, in one request; a column that already fills a field is not made a second field. Connect finds the column that names a parent when both lists read the same sheet. Headers in CamelCase read as words. A card made by a drop sits at the drop point, at least 16px in from the board's edge. The toolbar hides Create workspace…, Load into an existing workspace… and Tidy where an empty board could not finish them.
- Fixed: a session's log file handle was never closed after a turn (the AI journey J50 failed at teardown with an unclosed-file warning).
Fixed (0.17 UX1: dialogs in the middle of the window)
- Every dialog opens in the middle of the window again (PP-D69), in every theme and frame arrangement. UX1 had drawn forms as a sheet at the right edge, as tall as the form, so a short form such as a matrix cell's stood in the top right corner. Dialogs have the widths they had before; a long form scrolls inside the dialog and its buttons stay at the dialog's foot. A phone shows the dialog full width, as before.
Changed (0.17 ST1: Settings as one family)
- Every Settings page has one shape. The header carries the page's group, its glyph on a tile in the group's colour, one or two sentences and the page's one main action. Under it come groups: what the group is about on the left, its values, switches, choices and facts as rows in panels on the right; a table takes the full width. Account and Notification email give up their centred columns; Mail's form and every other form saved as a whole sits in one panel with Save in its foot.
- Every list opens its item as a page. People, Groups, Clients, Sign-in, Import profiles and Webhooks are full-width lists, as Workspaces and Model were; a row opens the item's own page, whose first line leads back to the list. The narrow list beside the item is gone.
- An opened item has a header in its colour with at most two actions; the rest are in a ⋯ menu, what cannot be undone last (a person's Deactivate and Sign out everywhere, a workspace's Retire and Delete, a webhook's Rotate key and Delete).
- Logs are on a tab. Mail (Server, Messages), AI (Settings, Calls), Agent access (Access, Calls) and Jobs
(Jobs, Limits); the tab is in the address (
?tab=). A log words a time as "today, 16:34" with the full time on hover, right-aligns numbers, gives a duration in seconds and minutes, has its states as chips and, on Mail and AI, its totals above; below it says how many are shown ("n of m" where the engine gives a total) with Show more. Jobs, Signing keys and Agent access keep their name column first, and Import profiles still reads further rows as the list is scrolled. - Colour. Each Settings group has a colour, shown on the index, beside each page's title and on its rows' tiles; a person and a client have their initials in a colour of their own; a role keeps its coloured chip in the People list and on a person's page; activity stays a line by day with a marker in its kind's colour.
- A person's, group's or client's access is grouped by place: a line for every workspace first, then one per
workspace by name, each with its roles and its consumer APIs as coloured chips and a "+" to grant more there. A
grant held through a group says so in its chip and links to the group; only the page's own grants can be removed.
The chip is the one a workspace's Access list uses (
features/security/GrantChip). - A person's, group's or client's access is set in one dialog. Opened from their page it is "‹name›'s access": what they hold is ticked, unticking removes it and ticking adds, and one Save does both; it was an add-only dialog in which everything already held was ticked, grey and locked. A role narrowed to some lists, and a person's role held through a group, are not offered there and stay removable by their chip. The chip's trash button is always shown on these pages. A removal asks no confirmation; every saved change offers Undo. From a workspace's Access the dialog still asks who and only grants.
- The index lists every page with one line saying what it is for.
- Deployment shows each setting as a row: its name with what it controls, its value, the default only where the value differs, and where the value came from.
- One person's changes (
/changes?actorId=…) is drawn from the same parts, begins with a way back, and marks each kind of change with its colour. - No route, permission or capability changed. The People list has no Groups column: the people list of the API carries no group data.
Changed (0.17 UX2: one field saves when it is left)
- A single field edited in place has no form any more (PP-D68). Clicking a value's pencil turns the value into its input where it stands: in a list's cell, in a record's details and header, in the record beside its list, in a relationship's value, on the names in Settings and in the model editor. Leaving the field saves it, as Enter does (in a text of several lines Enter is a new line and Ctrl or Cmd with Enter saves); a choice or a tick saves as it is made; Escape puts the old value back; an input left as it was saves nothing. The Cancel and Save buttons and the No value tick are gone.
- A saved value is told ‹Field› saved with Undo, also for a value set for the first time (Undo takes it out again) and for a relationship's value. An optional field left empty is the value removed, told ‹Field› removed with Undo, as the remove icon tells it; a required one left empty is refused under the field.
- A refusal (a rule of the model, a record changed elsewhere, a change set conflict) keeps the field open with the reason under it and the input kept; Cancel and Save again then stand under the reason, and leaving the field again saves only once the input has changed.
- Another field can be opened while one is open; the first saves as it is left. A list's value cell shows one toast instead of two. A one-line field keeps the height of its row.
- "Cleared" (a stored null) and "Empty text" are still shown, but a field no longer offers to set them (a JSON
field still takes
nullas typed); the API sets them as before.
Added (0.17 PL3b: the workbook canvas, engine and API)
- Three readings of a source's table (PP-D6, PP-D7, PP-D48), in the import profile, so they hold for any import, a
dry run, a profile check and a profile saved for the next file:
unpivot(a wide table read as rows: each filled cell of the listed columns becomes a row, the column's header in a new column and the cell in another),fillDown(columns repeated by blanks) andheadings(a row holding only a heading is the parent of the rows beneath it). Bindings name the added columns like any column; a name the table already has isCOLUMN_CLASH; a column the next file lacks takes its reading with it. A source without readings is read exactly as before. - Six operation kinds to set and clear them on a draft's source, each undoable and validated, with refusals by
code:
setUnpivot,clearUnpivot,setFillDown,clearFillDown,setHeadingRows,clearHeadingRows(PP-D49). POST /api/v1/model-drafts/{id}/join-candidatescompares a column of one table with the columns (or one named column) of another over every row in PostgreSQL and answers the candidates with their figures; two that match exactly as much are atiewith nobest, so a client asks (PP-D8, PP-D51).GET/PATCH /api/v1/model-drafts/{id}/canvas: where each card of the workbook canvas sits, by card key, merged by key, validated, with the draft's permission. Not part of the draft's version, its history, an undo step or a release (PP-D54; migration20261202_01).- An assistant's design must carry its profile (PP-D55): when its last check read the workbook through a profile sent with the check and the draft stores none, or another one, the task fails with a plain reason.
- The other drops of the canvas need no new route: each composes from existing operations in one request, one undo
step (PP-D52, J58.1). Journey J58 (
test_j58_workbook_canvas). The canvas itself is built separately.
Added (0.17 PL4: a workspace from a workbook)
- New workspace from a workbook (PP-D1, PP-D44 to PP-D47). A dialog on Home, in Settings › Workspaces and on a workspace's page: drop a workbook, name the workspace (prefilled from the file's name), read what refiroo found (each list with its rows, what loads and what does not and why, every row checked), then Create workspace. It lands on the new workspace's page, which shows the load's progress and what did not load. Adjust opens the model editor on the draft; Cancel removes it; More ways to start leads to today's New workspace. The action is left out for a person who lacks any step's group at the instance.
POST /api/v1/workspaces/from-workbookinstalls a workbook's draft as a release, creates the workspace on it and queues the workbook as an import job into its live data (independent: a refused row is reported, the rest load), and answers 202 with the workspace, the release and the job. A release version or an import profile key another model holds is replaced by a free one, never refused; a draft's profile now starts under a key no stored profile holds. Refused with the draft's problems:422 INVALID_MODEL,422 INVALID_IMPORT_PROFILE, and422 DRAFT_HAS_NO_WORKBOOK(new). Needs Workspace administration, Install a model (or Model release) and Bulk and import, the last two at the instance. New actionworkspaces.create-from-workbook.
Fixed (security sweep)
POST /api/v1/jobs/{jobId}/rereadis in the security oracle, and its reference text names the group that starts the job's kind, as the route checks (it said Bulk and import in the workspace). The consumer sweep expects404 CONSUMER_API_NOT_FOUNDfor a key the workspace's model does not declare, as the consumer API's text says.
Fixed (0.17 PL3b and PL4, from a workbook)
- A column whose header ends in a space, on a sheet whose header row is not row 1, can be mapped: a header named as the source-columns read lists it is matched without its surrounding whitespace, as the import reads it, and saved trimmed.
- New workspace from a workbook no longer stops with "No proposal of the draft's inference is open" when refiroo reads no list from the workbook by itself: it says so and offers Adjust, which opens the canvas. A reading that fails no longer leaves its draft behind.
- On the canvas, a sheet dropped on the board becomes one list named for the sheet with each column a field, and the first list of a draft without an application brings its application with it.
Fixed (0.17 UX1)
- AI tasks that move whole types: when the instance already holds a release with the draft's exact content (the same change made earlier for another workspace), installing returns that release and the task lands on it; it failed as "The session installed no release the moves fit".
- Layout editor, a panel's Columns… dialog: a containing type already chosen (Language on Name translations) no longer drew the panel's own columns under the "Where it sits" heading; the own columns come first, then "Where it sits".
- The guide's picture runner stops at a failed screen, because later screens may build on its state;
--keep-goingruns on. - Settings › E-mail template: the Markdown box is tall again (about thirteen lines). Settings' shared field rule had cut it to two lines.
- A publication's band no longer squeezes its sentence ("Read-only. A publication's records never change.") into a narrow column beside the Viewing bar: the sentence stands on a row of its own under the bar.
- A record opened beside its list no longer clips the remove button of a value or lets its details card scroll sideways: the row's controls stay inside a card that has no padding.
- Expand to full page replaces the record beside its list in the browser's history, so Back returns to the list.
- A page's actions wrap at phone width; Settings › Workspaces no longer scrolls sideways at 390px.
- A change set's dialog (Change model version, Submit) stays open when the window crosses the phone width.
- A record opened from the top bar's search inside a change set or a publication stays in that change set or publication.
- In a publication's view the sidebar tree reads beyond its first 50 rows: a group that started reading while the publication was loading reads again once it is there.
- At phone width a row opens the record's own page, with its relationships, and no longer a reduced view.
Changed (0.17 UX1: Instrument's type, Hearth's titles)
- Instrument reads more clearly. Its text is set in Inter (bundled, OFL-1.1;
@fontsource-variable/inter5.3.0) in place of Instrument Sans, which stays for page and record titles; codes keep JetBrains Mono. Its sizes no longer sit half a pixel under the other themes': table cells, labels and small text are 12 to 13px (were 11.5 to 12.5px), running text stays 13.5px, and nothing is smaller than 11px. Colours, frame, corners, control height and row height are unchanged, and so are the other four themes. A long list or group name in Instrument's list panel shortens with an ellipsis and no longer pushes the panel's content past its edge. - Hearth's titles are set in Fraunces (bundled, OFL-1.1;
@fontsource-variable/fraunces5.3.0) at weight 500, in place of Instrument Serif, whose single light, narrow weight looked thin beside the sans section headings; the Instrument Serif package is no longer bundled. Title sizes are unchanged from 761px up; on a phone Hearth's page and record titles are 23px (were 26px), so a long name still takes two lines. Hearth's text, colours, corners and frame are unchanged.
Added (0.17 UX1: themes with their frames, and one way to edit)
- Themes. Settings › Appearance offers Current (the look refiroo has had, the reference), Hearth, Studio,
Instrument and Website purple, each in light and dark. A theme sets colours, the title typeface (Fraunces
in Hearth, Bricolage Grotesque in Studio, both bundled, OFL-1.1), corners, type sizes and control height.
Every theme's colours are held to the same contrast minimums by
tokens.test.ts; the palettes are infrontend/tools/gen_themes.py, which writes their token blocks. - A theme arranges the frame (PP-D39). Current, Hearth and Website purple keep the top bar and sidebar. Studio has one side column (mark, applications, lists) beside a raised page. Instrument has a narrow strip (mark, applications as tiles, Help, Inbox, Settings, account), the lists as a panel, and the data context as a status line along the bottom; a page with no search has no bar. From 761px up; on a phone every theme keeps the bar and the drawer. Hearth, Studio and Instrument also have quiet scrollbars, controls that answer the pointer and surfaces that arrive with a short motion (off under "reduce motion").
- The theme base. Stylesheets no longer write a type size or a corner: 16 size tokens (
--fs-*) and four corner tokens replace about 320 hard-coded values, with shadow, motion and button-corner tokens beside them. Current's values are unchanged. - One way to edit (PP-D40), in every theme:
- a form opens as a dialog in the middle of the window (the side sheet this package first gave it is withdrawn, PP-D69); a confirmation no longer carries a "Please confirm" line;
- a value edited in place becomes its field, with no box and no repeated label; "Set null" reads "No value";
- a yes or no in a record's details is a switch that saves itself, as it was in tables;
- a saved value offers Undo in a toast (a record's name, and an attribute that had a value before);
- a confirmation that destroys nothing has a plain main button, not a red one (
ConfirmDialogtone).
- Settings and security pages follow it. A passkey's, client's, workspace's or group's name, and a group's
description, is changed where it stands (
ui/TextInPlace), and the saved change offers Undo. The "Keeps local sign-in" switch saves at once when turned on and asks only when turning it off removes the password way in. The dialogs that remain are forms: a line names the Settings group, the title reads "New ‹thing›" and the main button "Add ‹thing›" (Add group, Add role, Add person, Add client, Add webhook, Add provider, Add assistant, Add workspace, Add grant); "Save", "Rotate secret", "Rotate key", "Rotate recovery codes", "Remove grant" and "Remove second factor" replace the earlier wording. - The record area follows it. Removing a relationship, or clearing an optional value, no longer asks: it is done and a toast says "‹thing› removed" with Undo, which creates it again under the record's fresh version. A related panel with five rows or fewer folds its Search, Display and Sort behind one ⋯ menu; a larger panel keeps them. The sheets' footers are unified: Cancel is the secondary button beside one main button, progress text sits in the body, and the mapping sheet's Remove mapping moved into its form. Labels follow one word per action: Save, Add ‹type›, Delete ‹type›, Delete N records, Apply for picking several. A record's edit form says Save (was Save changes); starting a change set from a publication uses Name and Start change set, as the new change set form does; an Undo that is refused because the record changed again says so. A list's header has a pin button that brings the list to the person's shelf.
- Deleting a record is one confirmation. A new read,
GET /api/v1/objects/{objectTypeKey}/{objectId}/deletion-impact(changeSetIdfor a change set), returns what deleting a record would take with it, live or in a change set, without deleting anything: the records and relationships, their counts, the record's own relationship count, whether the delete needscascade=true, and thedeletionPreviewTokenthat confirms exactly that impact. It needs what the delete needs (Edit live, or Work in change sets) and computes the impact with the delete's own code, so the two always agree. The delete dialog reads it when it opens, shows what is lost with one danger button, and deletes under that token; if the impact changed meanwhile it shows the fresh impact once and asks again, and if it cannot be read it says so and offers Retry. - The model editor. An operation with one or two controls (42 of them: renames, keys, value types, a
pick-several list) opens as a small popover under the control that started it (
ui/AnchoredPopover), with the same fields, checks and undo step as the sheet; three or more controls keep the sheet. The buttons say Save, Add ‹thing›, Move or Apply. - A table row opens the record beside its list first (PP-D41), with the list still in place; Expand to full page goes to the record's page. On a phone the row opens the page.
- Search. ⌘K (Ctrl+K) puts the caret in the search. The top bar's search also finds the workspace's lists by name. Home has a search over every record of every workspace the person may browse, grouped by workspace.
- The workspace's page is redrawn, at the window's full width. A person's shelf holds the lists and other views (a matrix, a tree) they bring there from any application, as tiles with the number of records, in their own order. A list comes to the shelf by dragging it there from an application, by Add a list, or by the pin on the list's own page; a tile is moved by dragging it or from its menu (also on a right click). The shelf is kept in the browser. Above it is the search; under it the applications with their lists one click away, the person's change sets that wait, and what changed by day, with repeats of one change folded ("4 in a row").
- Home follows a large window up to 1680px, with more cards across, and shows what waits for the person (approvals, their own change sets) before the workspace cards, not after them.
- A file is chosen or dropped in one place: the file field is a drop area in every dialog that takes a file.
- Tables fold their columns on first load, also in a tab opened in the background. A wide column in the middle (a long identifier) no longer hides the narrow ones after it: those that fit stay.
- Fixed: a change set's band no longer runs past the page's edge (seen in Website purple): its name and state take the first row and its actions a row of their own when they do not fit.
- The guide and the API reference know Current and Website purple only and show the other themes in Current.
Changed (0.17 PL1 and PL2: change sets in two clicks, plain add and edit)
- New change set asks for a name only; the start point and the model version are under Options (the version list is offered only to Model release). The change set bar holds Review changes and Publish, with Validate now, Change model version, Rename and Discard under ⋯. Publish checks first: if the check passes, one plain confirmation names the records and relationships that go live and what happens on a conflict; if it fails, nothing is published and the page lists the problems with links to the records. A model-version mismatch is said in words instead of a disabled Publish.
- Adding a record from a page says "In ‹parent› · Change" instead of a parent picker (the picker's list no longer runs into the next field); required fields come first and are marked, the optional ones wait under More fields (n), remembered per type in the browser; missing and wrongly typed values are said under the field, never in the browser's bubble. The dialog's line above the title names the collection, the title is "New ‹type›" and the button "Add ‹type›". The relate dialog says "Add a ‹type› to ‹record›" where the relationship is known, and the words "object", "Connect an object" and "(outgoing)" are gone.
Added (0.17 MT1: the e-mail template)
- An operator can change how every mail refiroo sends looks. Under Settings › This instance › E-mail template,
the Operator role edits one template for the instance: Markdown with placeholders for the message's own text
(
{{content}}, exactly once), the product's name, the instance's address, the person's name and a logo. A PNG, JPEG or SVG logo up to 512 KB can be uploaded and is sent inside each mail. A live preview shows the result with sample values, on light and dark paper and as plain text. Send a test to me mails it to your own address, and Reset to default goes back to refiroo's own layout, which is used until a template is saved. Invitations, password resets, notices, digests and the mail test all use it, with a plain-text version. - A template with an unknown placeholder or without exactly one place for the message is refused by code
(
MAIL_TEMPLATE_UNKNOWN_PLACEHOLDER,MAIL_TEMPLATE_CONTENT_REQUIRED); raw HTML, images and unsafe links in the Markdown are not carried into the mail; an SVG logo holding script is refused (MAIL_LOGO_UNSAFE). - API:
GET,PUT,DELETE /api/v1/mail-template,PUT,DELETE /api/v1/mail-template/logo,POST /api/v1/mail-template/previewand/test, under the new actionmail-template.managein the Operations group (the Operator role holds it). Migration20261201_01adds the one-row tablemail_template. - Dependency:
markdown-it-py(already installed through another package) is now a direct, pinned one. - Not done: template changes are not written to the security audit; the mail was not opened in a real mail client (Outlook may ignore the card's width, and many clients do not draw an SVG logo).
Changed (0.17 PL5 and PL6: plain words, import file-first, grouped draft steps)
- Import from a collection opens on the file. A profile is chosen for the person when exactly one valid profile fits the file (they may still pick None or another); otherwise the fitting ones are listed; invalid and other models' profiles are not shown. The three loading switches sit under Options with the same defaults. A file "fits" by this rule: a JSON Lines file takes no profile, a CSV file takes profiles with a CSV setting or a single source, a workbook takes any valid profile.
- Your changes and Home's latest fold consecutive steps on one model draft into one expandable line with their count ("12 steps on the draft ‹name›"); expanding shows the steps as before.
- Plain words on record pages and pickers: "Record unavailable", "Cleared" instead of "Null", "Top level" instead of "Root object", "record" where "object" showed, "the workspace's live data" where a workspace key showed; a picker's lines show the record's path without the application key. The search also takes a person to places by name ("Change sets", "Settings", "Home") under "Go to".
Changed (0.17 SH1: one frame, one location line)
Frontend only; no engine or API change. The frame said where a person was three to five times and some journeys changed the data context without saying so; now it says it once.
- A page for each workspace. A card on Home opens
/workspace?workspaceKey=…in the shared frame: a search over every type's name in the workspace (the cross-type lookupGET /objects?search=; results grouped by type with where each stands, Down and Up through them, Enter opens), the applications, the person's change sets that wait there, the five latest changes, Edit model (WM1's action,useEditModel) and a ⋯ menu to Settings › Workspaces (Rename, Access, Approval, Retire) and Copy and move, each left out for a person who may not use it. The same search (WorkspaceSearch, overSearchField) is in the top bar on every page of the workspace. - Top bar. Applications that do not fit go under More, the current one always kept among the tabs. In Settings the bar shows "← Back to ‹workspace›" (the workspace the person came from, the default one when Settings was opened directly, never one just deleted) in place of the application tabs; the Settings link stays current.
- The band is one row on desktop: it no longer repeats the workspace and the application (the phone band keeps both). A change set's row is its pill, name, change count and state, with who, when, version and model under Details; the description shows once, on the review page.
- One line above a record: "← ‹the list it was opened from›" and the records above it, in place of Back to list,
the breadcrumb's application and type entries and the repeated type label. The model's
detail.breadcrumbsanddetail.headerflags still decide whether ancestors and the type show (the type once, as the eyebrow, when either type flag asks for it; the application entry is no longer drawn). A record's ID and version are under Details; Copy ID is in its ⋯ menu beside Copy link. - Settings opens on an index by task (You; Workspaces and models; People and access; Connections; This
instance, About last), and its menu uses the same groups. Every page stays reachable under the permission it had.
BreadcrumbsandTrailBarare one component (ui/Trail): the line says where the page is, with no Back button beside it, and an opened item's own name is the page title. - A page is titled by its sidebar entry (
pageTitle): a type's lenses are tabs under its name; a relation matrix placed in a navigation group is its own entry and keeps its name. - Browse records is folded in the sidebar until opened (remembered in this browser) and has no search box of its own; the top bar search finds a record across the workspace.
- A change set opened from the list lands on its changes ("See the data with these changes" leads to the lists); one just started, Continue on Home and the band's link to a person's own open change set still open the data.
- Tables that fit. In record lists and a record's relationship tables the name column is never cut and columns
that do not fit fold behind "Columns · n of m", which is the phone layout's column toggle (
mobile-secondary-column) on a wide screen; widths a person set keep winning. The model list's facts take the width of their values, the draft list's names and Home's cards get the room they need. - Columns fold by what their content needs. A folding table measures each column with its cells on one line
(
measuring-columns), keeps a column only if its content shows whole (a long free-text value ends in an ellipsis past 320px), folds from the right until the rest fits, and then fills its container. Home's cards show the roles on one line (the first two, then "and n more", the full list as the line's title and for screen readers) and the model name on one line, the tile beside the name. A record header's Details and ⋯ are centred on the title block. Settings' index links use the app's link colour. On a phone the Settings drawer leads with "← Back to ‹workspace›". The model editor no longer offers "Breadcrumbs start with the application" (the flag is still accepted and does nothing). The change set band stays on one row at desktop widths; only the name ends in an ellipsis. The record page's side column sticks below the band, not under it. - Folding runs on the first rows and fills the room. A folding table finds its container when its rows arrive (not only at mount), and the room the kept columns leave goes to the capped ones up to their content, then to the name. Home's card holds tile, name, badge and Follow on one row, with the model and the roles under it across the card; the roles end in "and n more" when they shorten.
- Fixed: a record's title clipped under the band when the side inspector opens (the inspector sticks below the band); a long version overlapping the draft count in the model list; avatar initials taking a bracket from "Name (temporary)" (letters of the name only); a change set's description shown twice on its review page.
Fixed (0.17 RF7: clean test output)
-
Backend and CI log without warnings. SQLAlchemy 2.1 deprecations are gone at the cause: no
Result.tuples()(a row unpacks as a tuple) andDISTINCT ONis written.ext(distinct_on(...))(sqlalchemy.dialects.postgresql), which needs SQLAlchemy 2.1 (lower bound raised from 2.0). Starlette's test client wantshttpx2: it is a dev dependency and the test harness uses it (the engine itself keepshttpx). The tests that build an HMAC token to show it refused use a 32-byte key. Actions run on Node 24 (checkout,setup-python,setup-nodev7, artifact actions and Docker actions on their current majors). The CI PostgreSQL runs as a step, and its log (with the refusals tests provoke on purpose) is printed only when the job failed. -
Warnings fail the test run.
filterwarnings = ["error"]in pytest's configuration, with no exceptions. That showed three real faults, fixed: the consumer events read asked for REPEATABLE READ on a connection already open, so the level was ignored and the request failed (begin_snapshot_read); test doubles that left a progress log, subprocess pipes and the fake mail server's TLS sockets unclosed. -
No button inside a button. A navigation group's label in the editor tables held its link inside the edit button; the link is now a link beside the pencil, which is the edit button (
EditInPlaceValueholdsControl), and both are reached by Tab. -
Unique keys. The Views table's "Shows" links no longer share a key when a relation matrix shows a type as rows and as columns.
-
Test output is checked. A frontend test that logs through
console.errororconsole.warnfails and prints the message, unless it says so withexpectConsoleError/expectConsoleWarn; the Vitest reporter is pinned, so a run in a coding agent's session shows the same output as CI. Two test fixtures were corrected (a shortGET /systemmock, a history out of order).
Changed (0.17 RF3: review findings, frontend)
- The reset token leaves the address bar. The application's
/reset-password?token=page replaces its address by the bare path as soon as it has read the token; the token stays in that history entry's state, so a reload before the password is set still shows the page, and is dropped once the link is spent or refused. The engine's ownGET /oauth/password?token=page answers a valid token with303to/oauth/passwordand aLax,HttpOnlycookie (oauth_reset, 60 minutes, path/oauth/password) that holds it, so neither the address bar nor the history carries it; a reload shows the page while the cookie lasts, then says the link is gone; setting the password ends the cookie. A used or unknown token is still refused at once with410. - One search box. Ten search boxes beside
ui/SearchFielduse it now (entity lists, access list, explorer, record panels, relation panels, restructuring, tree move, mapping and matrix candidate searches); the field gainedtype,autoFocusanddisabled. The record checklist, the field menu and the matrix cell filter keep their own markup (their own look). - No event is lost across a reconnect. The event stream client sends
Last-Event-IDwhen a stream givesid:lines (the engine's do not), and dispatchesreconnectedwhen a connection follows an earlier one; the inbox reads its lists again then. The job stream needs nothing: each event carries the whole job. - Pages load when opened. The application, Settings, notifications, change set review and navigation group pages are their own chunks; the sign-in and reset screens no longer carry the application. The entry script is 208.7 kB (was 2,108.8 kB for everything).
Security (0.17 RF4: review findings, build and deployment)
- Pinned versions. Claude Code in the engine image is installed at a named version (
ARG CLAUDE_CODE_VERSION, default 2.1.295) instead of the newest on the day of the build. Every base image is named by tag and digest (node:22-alpine,python:3.14-slim,nginxinc/nginx-unprivileged:1.28-alpine,postgres:17-alpine,caddy:2, and CI'spostgres:17). The backend has a lock:backend/constraints.txt(every runtime and development dependency, made withuv pip compile), which the image'spip installand CI use as constraints. How to move each is in OPERATIONS, Deploying. - The application has a content security policy, sent by
web(frontend/nginx/snippets/csp-app.inc.template): everything from its own origin, no inline script, no plug-in, no other base address, no frame. The theme script that sat inline inindex.htmlis nowpublic/theme-init.js, so scripts need no'unsafe-inline'. What the engine serves keeps its own policies (sign-in pages, guide, API reference);webadds onlyframe-ancestors 'none'to them, as before. - CI has a
permissions:block (contents: read); the image workflows already had theirs. AI_AGENT_SANDBOX=containeris set in the engine image and incompose.yml, so a container never runs the assistant session without it, whatever.envsays.- The engine believes forwarded addresses only from the compose network's private ranges (
FORWARDED_ALLOW_IPS, default10.0.0.0/8,172.16.0.0/12,192.168.0.0/16), not from every address (*). Behind another proxy, set it in.envto that proxy's range (OPERATIONS, Deploying).
Security (0.17 RF5: assistant sessions in a container)
- A session in a container cannot read another task's folder or transcript, or another running session's environment, and permissions are the barrier, not a name. Each task has a folder (its gate) under a random name in
data/assistant, which session users may enter but not list (mode 2710). The gate belongs to the engine alone (0700); for the length of a turn it is given to the group of the one session user that runs it (0710), so no other session user and no session between turns can enter it. It holdswork(the working folder and$TMPDIR),claude(the task's ownCLAUDE_CONFIG_DIR; the image sets none and the sharedassistant-claudefolder and volume are gone) and the engine's stream log of each turn (mode 0600). They are made once, before any session runs, where a session has no write right; afterwards the engine never changes a mode or writes in them by following a path a session controls: files it hands the session are made through a directory descriptor withO_NOFOLLOW, what it reads (proposal.json) is a plain file of at most 8 MiB, and the turn's log is read through the descriptor it opened. - A pool of session users, each with a group of its own. The image creates
assistant-1toassistant-N(build argumentASSISTANT_USERS, default 8), each in its own group (the engine's user is a member of every one) and in the common groupassistant, each with a home of its own. A turn takes a free one through an exclusive lock on a file indata/assistant-slots; two sessions at once are two users. With none free the turn does not start: the task fails with a note namingASSISTANT_USERS. - A reset around every turn. The sudoers rule names the pool and two commands: the Claude Code binary and
/usr/local/bin/refiroo-session-reset(root's, not writable). Run as the session user, never as root, before and after each turn, it makes that user's files in the task group-readable and writable (so the next turn, under another user, can resume the session), empties its home and its files in/tmp, and ends every process it runs. After the second review the reset ends every process of the user first, repeatedly until none is left, then empties the home and/tmp,/var/tmpand/dev/shm, then makes the files group-readable, and ends processes once more; it runs before the gate opens (a failure fails the turn and opens no gate) and, after a turn, while the gate is still open, which closes right after; the folders reach it on standard input, not on its command line. A reset that does not finish before a turn fails the turn.compose.ymlgainsinit: trueon the engine service, a reaper for the processes the reset ends. A gate left open is read again through its descriptor once its user's lock is held before it is closed. After the third review the reset also removes the user's System V shared memory, semaphores and message queues and its files in/run/lock,/dev/mqueueand/var/mail, and a turn closes every gate still open for its user's group (left by an engine killed mid-turn) before it opens its own. - Nothing secret on the command line. In a container the words go to the session's standard input and the
filesystemandnetworkparts of--settings(inert there) are dropped. The session ID stays on the command line, as stopping a left-over session finds it there. - A named list of passed variables (
PASSED_NAMESinagent.py) replaces theANTHROPIC_*,CLAUDE_*,AWS_*andNODE_*patterns, in the engine and in the sudoersenv_keep: the provider's credentials (ANTHROPIC_API_KEY,ANTHROPIC_AUTH_TOKEN,CLAUDE_CODE_OAUTH_TOKEN),ANTHROPIC_BASE_URL, the model variables,NODE_EXTRA_CA_CERTS, and Bedrock's names only whenCLAUDE_CODE_USE_BEDROCKis on. A unit test holds the Dockerfile's line equal to the list. A variable passed before by pattern (CLAUDE_CODE_USE_VERTEX,ANTHROPIC_CUSTOM_HEADERS, anAWS_*name with Bedrock off) is not passed now. - At the upgrade the first sweep moves each task's folder into the new layout, storing its random name first, closes
data/assistantto listing and closes every gate no turn holds; a folder no task names is removed only after an hour. Transcripts of tasks begun before the upgrade stay in the oldassistant-claudevolume and are not moved: such a task starts a new session, with its files, when the person next tells it something.compose.ymlno longer mounts that volume. - Known limits, in OPERATIONS: the provider's key or token still reaches the session, and nothing limits where a session connects. A relay in the engine that kept the key from the session was offered and left out by the user's choice of 2026-11-11. A third: isolation between tasks inside one container rests on Unix permissions and the reset around each turn, reviewed twice with defects found each time; a container per session is stronger and is not built.
Security (0.17 RF6: webhook addresses on private ranges)
- After review: a NAT64 address (
64:ff9b::/96) is judged by the IPv4 address in its last 32 bits (so a private or link-local one is refused), and the local-use NAT64 range64:ff9b:1::/48is refused unless the setting is on (link-local always). The refusal an author reads no longer names the address a host resolves to; the delivery log keeps it, as before RF6. A subscription edit or resume looks the host up before it locks the subscription's row. - A webhook address on a private range is refused by default. A subscription whose URL is, or whose host name resolves to, a loopback, private (RFC 1918), unique-local, shared (100.64.0.0/10), unspecified, multicast, reserved or IPv4-mapped address of any of these is refused when it is created or edited (
400 INVALID_WEBHOOK_URL, saying private addresses are refused on this instance) and again at every delivery, with every resolved address checked and the connection made to an address that was checked. A refused delivery is a failed attempt of kindaddressin the delivery log. Link-local addresses (cloud metadata) stay refused whatever the setting says. Redirects are still not followed. - New setting
WEBHOOK_PRIVATE_ADDRESSES_ALLOWED(off by default) allows the private ranges, for receivers inside the organisation's network and for development; plain HTTP tolocalhostworks only with it on. - Existing subscriptions on private addresses stop delivering until the setting is on: their batches fail with
addressand the delivery log says why. Set the setting before upgrading when receivers are inside the network. A sign-in provider's requests keep their own rule.
Changed (0.14 VD: the API documentation discrepancies resolved)
The 20 open discrepancies between docs/API.md and the code, decided by the user 2026-11-11: API.md is corrected where the code is right (11), the code is fixed where it is wrong (9). Contract changes are named as such.
docs/API.mdcorrected (no code change): the feed needs the Feed action group (feed.read);WORKSPACE_ADMIN_MUTATIONS_ENABLEDswitches the workspace operations and Workspace administration (workspaces.manage) guards who uses them;GET /system/configurationhas the setting groupai;POST /objectslists the live answers (422 INVALID_OBJECT_TYPE_FOR_APPLICATION,404 UNKNOWN_APPLICATION) and the change set's (OBJECT_TYPE_NOT_IN_APPLICATION,422 UNKNOWN_OBJECT_TYPE,422 PARENT_WRONG_SCOPE) separately; the at-least-one-of rule in a change set is left to validation by an update and an attributePOST/PATCHand checked at once by a create and an attributeDELETE; a stale version on a cascading delete is409 DELETION_PREVIEW_CHANGED; the workspace edge list takesseek;CHANGE_SET_SOURCE_INVALIDis no longer listed (it can never be answered: the request schema, withderivedFromPublicationId, refuses the same cases with422 REQUEST_VALIDATION_ERROR); a model release'sPATCH,DELETEand…/raisingare always registered and guarded bymodel.install, andPATCHonly retires or reactivates; an AI call'spurposehas all eight values;design.outline'snameis only in the assistant's outline.- Contract change: attribute
POSTandPATCHin a change set answer the record'spath, filled as the object writes do (it was empty). - Contract change:
GET /objects/{type}/{id}/parent-candidatesno longer takespublicationId. A request that sends it is refused422 UNKNOWN_PARAMETER; the model is selected byworkspaceKeyorchangeSetId. The operation's access check no longer readspublicationIdeither. - Contract change: an export job's
parametershold the request as it was sent, by alias (not, notnot_) and without what the request left out (noview,layout,peopleas null). A job stored before holdsnot_and is still read. - Contract change:
POST /agent-access/applications/{id}/blockand/restoreanswersignedInwith the people who have an open session for the assistant, as the list does (it was always 0). - Contract change:
ModelMappingOperationEntryhasremovedandmigrationas a draft's history entry does (always empty for a mapping). - Contract change:
ModelCompatibilityResponse.classificationlistscompatibleandmigration_required.invalidis produced by one answer only,compileof a draft that does not compile, soCandidateCompatibilityResponsekeeps it and its reference line says so. - Contract change:
AiTaskResponse.checkisDesignCheck.TaskCheck,CheckTable,CheckFix,CheckSummaryandRefusedGroup, which no code produced, are removed from the document and from the frontend types. POST /movesdescribesmodeandstagein the router's description;MailMessageResponse.kindandlastErrorKindnamedigestandstopped.- The reference text and its recorded examples follow each change; the OpenAPI lock is re-baselined on the tree as it stands.
Added (0.14 V9: the management API reference, webhooks and the close of Track V)
consumer subscriptionsis documented on/docs/api(11 operations): create, list, read, edit, suspend, resume, rotate the signing key, delete, the deliveries, one delivery and the resend of an event. The overview says what a subscription is and the sequence a receiver follows; how a request is signed and verified, batched and retried, and why a subscription is suspended, are in the fields that carry them. The recorded examples come from a test that starts a receiver on the same machine and stops it again: it answers503once, so the examples show a request being retried and then delivered, and a resend signed with both keys after a rotation. The signing key is a placeholder in every example.- A schema's own description can be written in the content files (a line of plain text under its
### Schemaheading), replacing the one in the code. All 254 schemas whose description named a package, a decision or a release got one, as did 33 field descriptions and two parameter descriptions; a test fails on any description in the document that carries such a number. - The reference is checked for completeness by
backend/tests/test_reference_complete.py, which replaces the gap list (reference_gaps.jsonis deleted): every operation has its section and example, no parameter or field is undescribed, every code the backend raises has a catalogue entry (the extraction now also reads a conditional status and a conditional pick of two codes), every entry is listed on an operation or says it is no response status, every guide link to the reference names a tag or operation it has, and no example holds a secret. - The catalogue gains
APPLICATION_IN_USEandAPPLICATION_OBJECT_TYPE_IN_USE(findings of a compatibility check and of a migration), ten entries that are an item's, a job's or a plan's code say so instead of carrying a status,POST /api/v1/model-drafts/{draft_id}/operationslists409 AI_TASK_DRAFT_HELD, andUNKNOWN_PARAMETERandUNKNOWN_OBJECT_TYPEsay where a subscription's filter answers them. - The diagnostic codes of a compatibility check and the item codes of a migration are in the catalogue (29 entries, each badged as no response status and saying where an integrator meets it:
diagnostics, thedetailsofMODEL_MIGRATION_REQUIRED, a migration's items, a publication's merge conflict); the completeness test reads the files that write them, so a new code of these families needs an entry. - Every other code a client can meet is in the catalogue (50 more entries): a change set's whole-graph diagnostics and a publication's conflicts, a move's reasons, a mapping's problems, an import's row errors, a draft's profile warnings, the assistant check's findings, the consumer contract and model validation findings, a subscription's suspension reasons and a backup rehearsal's failures, each badged as no response status; the completeness test reads the files that write them.
- A link straight to an error code's entry lands on it (
/docs/api/#management/description/<code>): Scalar rewrote the address and showed the Introduction. The page's own script now keeps the address, scrolls to the heading once it exists and stops when the reader takes over; checked on the instance page under a root path and on the static build, in light and dark. docs/API.mdstarts with a pointer to/docs/api; the public build of the reference was rebuilt and checked in light and dark. The OpenAPI lock is re-baselined on the tree as it stands (descriptions, summaries and responses only). The management document is 2.52 MB compact; the examples stay in separate files.
Added (0.14 V12: the management API reference, AI, mail and notifications)
ai,mailandnotificationsare documented on/docs/api(36 operations): the provider's status, test and call records, a question turned into filters, accounts of a model change and of what changed, assistant tasks from their request to the change set they load; mail settings, the test message, the outbox and a person's own notice mail with its digest; the inbox, its stream and following a workspace. Each has its title, a short section, parameter notes, error lines read from its code path and a recorded example on the reference data (the fake provider, the harness's SMTP server and a stand-in assistant session; the examples say which). Theaioverview states what a provider is sent at each send level. 86 schemas have every field described, the dry run of items among them. The gap list'sai,mailandnotificationsgroups are empty (724 to 143 items). Five differences between API.md or OPERATIONS.md and the code are indocs/TODO.md. The OpenAPI lock is re-baselined on the tree as it stands (it holds the other sessions' uncommitted changes).
Added (0.14 V11: the management API reference, security)
security,oauthandagent-accessare documented on/docs/api(79 operations): a person's own sessions, password, authenticator app and passkeys; people, their password links and provider identities; groups and members, roles, grants and who has access to a workspace; clients and their secrets; the token signing keys and the security events; sign-in providers and local sign-in; the OAuth endpoints (token, revocation, registration, keys and metadata); and agent access (the settings, agent clients, assistants and calls). Each has its title, a short section, parameter notes, error lines read from its code path and one recorded example on the guide's reference data; the three overviews say who may do what and how a client signs in. Every field of the 77 schemas they use is described, and the gap list loses the three groups (1,176 to 724 items). No real secret is in an example: client secrets, tokens, reset links, authenticator seeds, recovery codes and passwords are replaced by fixed placeholders when the examples are written (SECRET_MASKS). Passkeys and the provider sign-in are made as a browser makes them, with the harness's software authenticator and OpenID Connect provider.- The model administration operations list
404, not405, for a switched-off setting: their addresses do not exist whileMODEL_ADMIN_MUTATIONS_ENABLEDis off, so the framework answers404 HTTP_ERROR; the405text of V8 and the catalogue'sHTTP_ERRORentry were wrong for them (the model mappings' and workspaces' lines were right). The OpenAPI lock is re-baselined on the tree as it stands (descriptions, summaries, examples and responses only; it also holds other sessions' uncommitted changes).
Added (0.14 V8: the management API reference, the model)
model,model administration,model mappingsandmovesare documented on/docs/api(64 operations): model reads, releases with their diffs and activations, validations, drafts with their operations, outline, consumer API reads, dry run, inference, installation and migrations, model mappings with their operations and inference, and moves. Each has its title, a short section, parameter notes, error lines read from its code path and one recorded example on the reference data, including a second release of the Graubünden model, a third it is migrated to, a second model and a move between workspaces on the two models. The four overviews are short; the model's concepts are in the schema fields (about 1,450 gap items emptied).- Every operation kind is a schema with typed, described parameters. The 66 draft kinds and the 12 mapping kinds (
Mapping<Kind>Operation) each list their parameters with types, which are required (and when a parameter is needed only under a condition) and what they mean;POST /model-mappings/{key}/operationstakes the union of the mapping kinds, as the draft route takes its own. The schemas only describe: the kinds keep checking and keep their refusal codes. The three kinds only the server writes appear in the history reads. A unit test holds both registries and their schemas in agreement.engine/moves/mapping_kinds.pynow declares its parameter names once (PARAMETERS). The OpenAPI lock is re-baselined.
Security (0.14 V8)
POST /importswithsourceFileIdno longer reads a file the caller may not use. A caller with Bulk and import in one workspace could import another workspace's profile file, or an assistant task's workbook, by naming its ID. The import now applies the rule of the source file routes through one shared check:403 FORBIDDENfor a profile's file of a workspace where the caller has no Bulk and import (the uploader still may),404 SOURCE_FILE_NOT_FOUNDfor a task's workbook.
Added (0.14 V7: the management API reference, jobs, imports and source files)
jobs(withexports),importsandsource filesare documented on/docs/api: batches with every item kind, exports, jobs (read, list, outcomes, rows, file, events, cancel, confirm, read again), import profiles and their checks, imports, and stored files with their profiles (30 operations). Each has its title, a short section, parameter notes, error lines read from its code path and one recorded example on the guide's reference data; the import examples use the guide's fictional bus company workbook. Every field of the schemas they use is described, and the catalogue gains 29 codes (an import profile's problems, an import file's mismatches, an item's reasons, an import's notices andIMPORT_PROFILE_MODEL_MISMATCH). The gap list loses the three groups (3,222 to 2,622 items). The OpenAPI lock is re-baselined on the tree as it stands, which also holds other sessions' uncommitted changes.
Fixed (0.14 V7)
/docs/apiand/openapi.jsonwork with an optional surface off. The reference text named operations thatWORKSPACE_ADMIN_MUTATIONS_ENABLED=falseorINTEGRATION_JOBS_ENABLED=falseremove, and the document could not be built. Text for something the document lacks is now skipped when the application builds it; the tests merge it strictly into the all-capabilities document and build the document with each optional surface off.
Added (0.17 SA1: sign-in managed by an administrator)
- A person's sign-in can be managed by an administrator (
signInManaged, default false). It is read onGET /meand on every read of a person, accepted byPOST /usersand changed byPATCH /users/{id}with the person'sexpectedVersion, and shown as one checkbox on the person's form (new and edit). Migration20261129_01addsusers.sign_in_managed. - While it is on, the person's own credentials are out of their hands (
403 SIGN_IN_MANAGED, one rule inengine/identity/managed.pythat each own-credential operation calls before it checks anything else): changing the password, starting, confirming or removing a second factor, new recovery codes, and registering, renaming or removing a passkey (the engine's registration page too).GET /me/sessionslists only the session of the request, and ending another session or all of them is refused; signing out the own session works. - A forgotten-password request for a managed address answers as for any address and queues nothing. An administrator's reset link and its page, removing a second factor or a passkey, and signing a person out are not refused. An instance that requires a second factor no longer asks a managed person without one to set it up at sign-in.
- The account page hides the password, passkeys, second factor and other-session parts for such a person and says in one line that an administrator manages the sign-in. The security log words the change as "Sign-in managed by an administrator" or "Sign-in no longer managed". The OpenAPI lock is re-baselined (additive).
Added (0.14 V3: the management API reference, its conventions, catalogue and first areas)
- The reference text is merged into the OpenAPI document from
backend/app/management/reference/content/, shipped as package data:index.md(signing in, the error envelope),errors.md(the error catalogue, each code an anchor on/docs/api, its status a badge), one file per tag (a short overview, one section per operation headed by method and path with its title, parameter notes and error lines, and a## Schemassection whose field lines become the schemas' property descriptions) andexamples/;README.mdthere is the writer's guidance (one recorded example per operation). A section naming a route that does not exist fails when the document is built. Each operation lists its error responses by status, each code linked to its catalogue entry.index.mdholds the conventions;errors.mdthe 347-entry catalogue (every code the backend raises that a request can reach).workspaces,system,changesand the two health checks are written in full, with 21 recorded examples; the other areas follow, each emptying its group ofbackend/tests/reference_gaps.json, which may only shrink (test_reference_gaps.py). The examples' request and response bodies are files the document references (externalValue), served at/docs/api/examples/…and written bystatic_site; the page makes the document address absolute so Scalar loads them. A change to a management route updates its text and examples in the same change (AGENTS.md). - A
referencetest group records the examples from a run on the guide's reference model and itsdocsset:RDM_RECORD_EXAMPLES=1 python -m pytest -m referencewrites them normalised (stable placeholders for IDs, times and cursors) and short; without it the steps only assert status and code.
Added (0.14 V4: the management API reference, records)
objectsandattributesare documented on/docs/api: create, read, update and move, delete (with the cascade preview), the per-type list, the cross-type lookup, the model-aware query, parent candidates and history, and the four attribute-value operations (13). Each has its title, a short section, parameter notes, its error lines read from the code (live and change set, which word several refusals differently) and one recorded example on the guide's reference data; the 28 schemas they use have every field described. The gap list loses itsobjects(204) andattributes(37) groups. Wheredocs/API.mdand the code differ, the reference follows the code anddocs/TODO.mdlists the five differences found.- A parameter note can name a parameter with placeholders (
filter.<field>.<operator>), so the list's filter family is described.
Fixed (0.14 V4)
- The reference's example files load when the instance runs under a path prefix. Their addresses are relative to the document, so they follow its
root_path; the static build still writes its own. - The guide's links into the reference point at tags the document has:
#tag/clientsis#tag/securityand#tag/mappingsis#tag/mapping; the consumer API index, which is not in the management document, is linked without an anchor. The OpenAPI lock is re-baselined (descriptions, summaries, examples and responses only).
Added (0.14 V5: the management API reference, relations and mappings)
relations,mappingandrelation-matrixare documented on/docs/api(18 operations): add, list, read, change, remove and batch relationships, the grid change, the held read, the workspace edge list and the relationships grid; the mapping coverage, candidates, targets, replacement and AI proposals; the relation matrix and its two cell reads. Each has its title, a short section, parameter notes, its error lines read from the code (live and change set) and one recorded example on the guide's reference data; the 41 schemas they use have every field described. The gap list loses itsrelations(116),mapping(129) andrelation-matrix(73) groups. The error catalogue gainsRELATION_BATCH_REFUSED. Wheredocs/API.mdand the code differ, the reference follows the code anddocs/TODO.mdlists the one difference found (seekon the workspace edge list).- An operation with several tags is written in the file of its first tag (
content/README.md), so the edge list and the relationships grid, taggedmappingandrelations, are inmapping.md. The OpenAPI lock is re-baselined (descriptions, summaries, examples and responses only; it holds the tree as it stands).
Added (0.14 V6: the management API reference, change sets and publications)
change-setsis documented on/docs/api(23 operations): list, start, read, rename, discard and move a change set onto another release or onto the workspace's release (amigrationjob), its record and relationship changes, the check, the restructuring plan and apply, publishing, the submission, withdrawal and decision, the publication requests, the approval policy, and the publications with their withdrawal. The overview says how a change set merges onto live data, what a conflict is, how approval works and what a publication snapshot is. 80 schemas have every field described (424 fields), including the job, export, binding report, move and migration summaries the migration job returns. Each operation has one recorded example (tests/test_reference_change_sets.py: a change set with a new stop, a corrected name and a stop moved by a restructuring is reviewed, checked and published; the workspace then requires an approval by a second person, made through the API; a change is submitted, sent back, withdrawn, resubmitted and approved; the publication is withdrawn; one change set is discarded and two are moved onto a second release).- The error catalogue says what a publication's
VERSION_CONFLICTcarries (details.conflicts, their codes and the mapping-set count). - Fixed in the earlier areas' text: the pinned-model refusals (
UNSUPPORTED_PINNED_MODEL_SCHEMA,PINNED_MODEL_INVALID) were listed as 422; the engine answers 409, as the catalogue says. The OpenAPI lock is re-baselined on the tree as it stands (it holds the other sessions' uncommitted changes).
Fixed (0.17 SA1)
- A link a person asked for does not outlive the switch. Switching
signInManagedon removes the links the person asked for by e-mail address, and such a link of a managed person is refused as an expired one; an administrator's link still sets the password. - Closing the grant dialog no longer asks to discard changes when nothing was changed. The dialog compared the person, group or client it was opened for by object identity, so any refresh of the page behind it made it count as edited, and Escape or Close then asked "Discard your unsaved changes?". It now compares by ID. Found through the security browser runner, where the step failed now and then.
Fixed (0.17 RF2: review findings, engine)
- After review: using a password link ends the person's other links, and a link the person asked for stops working when an administrator manages their sign-in. Setting a password through one link deletes the others in the same transaction, so a link asked for beside an administrator's cannot set the password again. Switching
signInManagedon deletes the links the person asked for (an administrator's link stays), and a requested link of a managed person answers410 link_goneon the page and when used. - A CSV export holds no text a spreadsheet runs as a formula. A name, path or
text/longTextvalue starting with=,+,-,@, a tab or a carriage return is written with a single quote in front (the OWASP rule), in PostgreSQL where the file is written. Numbers, dates, booleans and JSON are as stored, so a negative number stays a number. XLSX already wrote every text as a text cell and is unchanged. A text that already starts with quotes and then such a character is escaped too, and the import of the table layout from CSV takes exactly one quote off the name and text columns, so an unchanged export sent back changes nothing (a real'=xis written''=xand read back'=x); a hand-made'=ximports as=x. - Placing new matrix cells in an import reads once.
PlaceReadskeeps the records of a set of types, the cell counts of a view and the containers in use, each read by one statement for the whole import instead of once per column record; the places chosen are the same. - Two first sign-ins of one new person through a provider at once make one person. The person is created in a savepoint; the second sign-in, failing on the address or the subject the first just made, finds that person and signs in. Matching by e-mail stays refused (D363, 0.12 I3), so
email_verifiedis not consulted. - A forgotten-password request no longer cancels a link an administrator made.
password_reset_links.requested(migration20261130_01) marks links an ask made; an ask replaces only those, an administrator's link still replaces every earlier one.
Fixed (0.17 RF1: review findings)
- Approving a change set no longer risks a deadlock with publishing it (RF1-F1). A decision that publishes now takes the workspace's lock before the change set's, through the one function publishing uses (
lock_workspace_for_publication), so an approval beside a direct publication or a change-set move of the same change set waits instead of ending in PostgreSQL's deadlock error. Sending a change set back keeps its lighter lock. - Reading an import preview again never touches a confirmed import (RF1-F2). The re-read locks the waiting preview, replaces it in the transaction that commits the new one, and removes the old file only after that commit. A preview confirmed meanwhile is refused with
409 JOB_NOT_AWAITING_CONFIRMATION: nothing is created and the confirmed import is not cancelled. - A failed file cleanup no longer keeps background work off (RF1-F3). When clearing the job files a stopped process left behind fails at start, the failure is logged with its cause, the job worker, exporter, assistant, listener, deriver and dispatcher start anyway, and the cleanup runs again at the next start.
Fixed (0.17 CA1: the Apply dialog names a consumer API's changes)
- A change to a consumer API shows in "What changes in the model". Renaming or re-sourcing a field of a released related block (the parent's or the object's) is a breaking change that needs a higher contract version, but the Apply and Install dialogs listed no difference for consumer APIs and said "Nothing new", which read as the edit having done nothing. The model diff now names the consumer API by its key, and the dialogs list "Consumer APIs changed:
" beside the contract notice that offers the version raise.
Added (0.17 CA1: the parent carries fields and related blocks)
- A consumer API's parent serves what the object can (PP-D29).
object.parent.fieldsis written as the object'sfieldsare: an output name to its sources (objectType,id,code,name, an attribute a parent type binds, orancestor:<type>.<field>),nullwhere the record's parent has none; andrelatedblocks written as the object's. One level: the parent has no parent. The short list form still works: a built-in name keeps its role's output name and any other source is served under its camelCase words (master-idismasterId). A parent source no parent type can read, a parent block over a relation no parent type has and an object block namedparent.…are refused at the pointer the author wrote. Read in the same statements as the object's blocks, however many records a read holds. - A related block is served always or on request (PP-D30). Every related block, the object's and the parent's, takes
served(always, the default, oronRequest). An on-request block is read and served only when the request names it ininclude(include=translations&include=parent.systemVariants, repeated or comma-separated) on get by ID, lists and XREF, and in each item and indefaultsof a batch resolve; a name that is no on-request block is400 UNKNOWN_INCLUDEwithdetails.valid. The OpenAPI document lists the names as an enum on those operations and types an on-request block as an optional property;includeis a reserved filter name. Events and deliveries leave on-request blocks out. - An On related filter may name a block of the parent (PP-D31, PP-D32):
onRelated: "parent.systemVariants"limits that block as it limits the object's, andkeepOnlyMatchingdrops the results whose parent has no match; the filter narrows the results withoutinclude, and the block is then not served.crosswalkstays on the object's own blocks. - Events follow what the parent shows. A change to an attribute or an ancestor field the parent reads, to an item of one of its related blocks, or a relationship it gains or loses makes
updatedfor each child of that parent (output: the parent's). - The contract check: adding parent fields, parent blocks,
includeand filters is compatible; serving a block only on request that was served always is a breaking change that needs a higher contract version. Every existing definition answers exactly as before. - The editor's Fields tab has a Parent section (its output name, its fields chosen from the built-in names and the attributes of the parent types, and its related records with Add related), the related tables have a Served column (Always or On request), the Operations tab lists the names
includeaccepts, and the filter dialog's block picker offers the parent's blocks; an On related filter's Matches on lists every field of the records the block reaches (as its Fields… picker does, the system a value sits under included), for the object's blocks too.
Added (0.17 EX1: Export says what it exports)
-
An export can name a view (PP-D18).
POST /exportstakesview(application, view key, the page's state, the field a link matrix's cells list) in place ofscope; the engine derives the file from the view: a table, a tree (a sheet per type), a relation matrix that creates or links, a relationships table or grid, a mapping grid. Every list view hands its state to Export through one hook in the view host; the dialog opens on This view, then the type, the application or the workspace, for editing or (a table, a type, an application, a workspace) technical, and asks where a matrix's new cells go. -
A type, an application or a workspace for editing is a sheet per type (PP-D21): fields, parent by name (by hidden ID where names clash), relationships by name, loaded from the later sheet only; the file imports back unchanged.
-
Every XLSX export has an "About this export" sheet, second (PP-D22): what was exported, what it holds, how it loads back, where new matrix records go.
-
Import profile, generically:
relatedRecord.idColumn(a related record named by an ID cell,parentoptional) andrelation.within(a column's part of a relation set). A record is written in its own application, a cell'snot loadedpair is left out whole, and an empty set on a required relationship that is already empty is unchanged, not refused.suggestedContainerIdon the matrix containers read. Additive contract change. -
The Export dialog says what "This view" holds: rows × columns (a table, a relation matrix, a relationships grid), rows (a relationships table) or records (a mapping grid, a tree), with the change set, publication or live data it is read from, counted as the page counts it (the same filters, search and change set); nothing is said of a size the page does not know. No API change.
-
A new matrix cell goes where the relationship's candidate rules allow (PP-D24, MC1 and EX1). The containers read, the cell dialog, the export and the import offer or use only the containers a record created there meets the
ancestor:<type>.<field>criteria under, found by the candidate evaluator the write rule uses. Equalities on one ancestor type that the model lets sit under the column fix one place; when the column has none, creating a cell creates that place with its values and the record in one change (the dialog sends one atomic batch and says "(will be created)"; an import adds the place as its first item). Criteria that fix no place create nothing: the place is then the container under the column that already holds cells of the view (the one holding most; none on a tie), and a container holding no cells is never preselected or used automatically, even as the only one; the dialog lists it under "Other groups (not used in this view)", and an import refuses a new cell there in plain words.GET …/relation-matrix/containersreportsderivedPlaceand each container'scellCount;suggestedContainerIdis gone. -
A matrix export asks which field a cell lists (PP-D25).
view.cellField(the name, the display attribute or a unique text attribute of the cell type) replacesnewCellContainers; the dialog's container section is gone. The file records the choice (relation.within.field) and an import reads it back; a value finding two records is refused, naming its row. -
An import shows how a matrix or relationship column is read, before anything is written (PP-D26). A file that does not record a column's field is guessed in the database from the column's existing records and says so. The binding report's column
readingand the preview section "How the file is read" give the field, the values matched, where new records go and a choice of another field;POST /jobs/{jobId}/rereadruns the preview again; the choice can be saved into the stored profile. Profile:relation.within.field,relatedRecord.under. -
A link matrix's export has a value sheet per column (PP-D27, EX1). Each column's records (those the column offers and those linked in it) are listed as full rows in an Excel table, "Values ·
": every bound field, the group and the record's ID and version, hidden. The main sheet's cells list the chosen field (any text field of the cell type, the name by default; the unique-only rule of PP-D25 is gone) and their warning-style dropdown reads the sheet's column through a defined name over the table column, so a renamed or added row shows in it and free typing is allowed. The import reads each sheet as a source ( Source.under): a row with an ID is an update under its version, a row without one a new record in the column's place (where the view creates; a view that only edits refuses it), a main-sheet value equal to a value-sheet row's field is that record, a value typed in both places is one record, a value found in neither is a new record named by it (holding it in the field when that is not the name), and a row nothing picks is still created. All of it is set-wise in the database (import_value_rows). A matrix whose cells hold the record's own fields has no value sheet. The binding report's reading counts a sheet'supdatedRowsandnewRowsand a column'sfromValueSheetandcreatedValues. Decided by recommendation: a created record's name is the value, plus the attribute when the field is not the name; link-only views get the sheet for reading and the dropdowns, with new rows only where the view hascreate; the value sheet carries a hidden version beside the ID so an edit is refused when the record changed. The About sheet of a file with such lists has a section "Editing in Numbers": Numbers turns a cell list into a pop-up menu that takes only its items, so it gives the few clicks that make the cells plain text (Format, Cell, Data Format: Automatic) or add one choice; the "New … go into" section now says where the data or a proposal puts a new record instead of the older refusal wording. -
A column with no group gets a proposed one (PP-D28, MC1 and EX1). Where neither the rules nor the data give a place,
derivedPlaceis a proposal (proposed: true): a new container under the column of the type the view's other columns' containers have, named as most of them are (a tie to the first name), only for a type that asks for no value and only when a group so named meets the binding's ancestor criteria; no model rule is added. The cell dialog says so in a sentence, lets the name be changed and creates the group and the record on one OK (createObjectInNewPlace; "Use an existing group instead" keeps the unused groups choosable); an import's preview says "will create group … under …" (newRecords.typeName,under) and loading the file creates it. The containers read givesderivedPlace.applicationKeyand each container'sapplicationKey, which the dialog creates the group and the cell in (a record stands in its parent's application; the dialog took the first application that lists the type, which a model listing a type in several applications refuses withPARENT_WRONG_SCOPE), and an import creates a new place in its column record's application.
Changed (0.17 EM1: the e-mail is the sign-in name)
- A person signs in with their e-mail address; the username is gone (PP-D14).
users.usernameand its index are removed;emailis required for an active person and unique ignoring case (409 EMAIL_TAKEN, which replacesUSERNAME_TAKEN;422 EMAIL_REQUIREDfor an active person without one, also when restoring one who has none). A deactivated person may have no e-mail, which frees the address (PP-D16). The password page, the application's JSONpasswordstep ({request, email, password}), the reset request ({email}), the failed-sign-in counter (per e-mail, case folded) and the rule that refuses a password equal to the account name (PASSWORD_REJECTED,details.reasonemail) all use the e-mail.POST /userstakes{name, email};UserResponse,MeResponseand the sign-in answers carryemailand nousername; a group's members reademail; adding members takes an ID or an e-mail;create_admintakes--email; the Users pages, the New person dialog (which also asks for the e-mail of a person restored without one) and the member pickers show and ask for the e-mail. New security events recordemail; events from before keep theusernamethey recorded. The OpenAPI lock is re-baselined (breaking, pre-1.0). - A provider's person needs an
emailclaim (PP-D15). Matching stays by provider andsub(D363), never by e-mail. A first sign-in without the claim is refused (no_email, the message names the claim and that the provider's administrator adds it); an address another person holds, active or deactivated, refuses it (email_taken,403; a security administrator resolves it, OPERATIONS). At later sign-ins the e-mail follows the token unless another person holds the new one; then the stored one stays and the sign-in succeeds. Linking an identity onto an existing person frees the deactivated person's address. - The migration
20261128_01stops on what it cannot settle (PP-D17). An active person without an e-mail whose username is an address takes it as the e-mail; if an active person still has none, or two people share an address ignoring case, the upgrade stops and names them, and nothing is invented. Check an instance for active people without an e-mail and for shared addresses before upgrading.
Changed (0.17 MC1: reuse or create in any matrix cell)
- A filled matrix cell offers Create as well as Link existing (PP-D23), each where the view's
cellActionshold it. Create showed only for an empty, inactive or history-only cell. A cell that may hold only one record keeps today's dialog: the binding'suniqueToAncestorTypeKeyis the column type read outgoing (the cell's conflict rule), or its cardinality lets the row's end hold one. A record created from a filled cell uses the same form and container choice as from an empty one, and the engine's uniqueness checks still refuse what the model forbids. Frontend only; no API change.
Fixed
- Settings › Integration › API reference and Consumer APIs follow a chosen workspace. A workspace choice at the top of both pages (shown when more than one workspace is available, kept in
?workspace=<key>) lists that workspace's consumer APIs, and the links to the reference page and the OpenAPI documents carry it, so a non-default workspace's consumer APIs open on/docs/api. Without a choice the default workspace is listed as before. The application also remembers the workspace being worked in (refiroo.workspace, shared with the documentation site and the reference page), so/docs/apiopened from the "?" help link and the guide header lists that workspace's consumer APIs; an explicit?workspace=in the address always wins. The OpenAPI document of a non-default workspace marksworkspacerequired (that key the only value), so a test request composed from it from the reference page reads that workspace instead of the default and no longer answers403 FORBIDDEN. A consumer token asking for a key the requested workspace's model does not declare now gets404 CONSUMER_API_NOT_FOUND(asdocs/CONSUMER_API.mdsays), withdetails.workspaces, the active workspaces where the client holds a grant of that API, and the message says to add?workspace=; a key the workspace declares, read without a grant, stays403. - A list's search box keeps focus while typing. In a table, tree or matrix list the box was rebuilt each time its search was applied (250 ms after the last key), so typing slowly or editing the text moved the focus out of the box. It now stays in place while its own search comes back. A search changed outside the box (back, Clear search, an answer in words) replaces the text in every list's box, which the relationships table, mapping grid and matrix did not do before. Frontend only.
- The public API reference builds from a clean checkout.
app.management.reference.static_siteimported the application before setting its throw-awaySECRETS_ENCRYPTION_KEY, and importing it builds it, so a checkout without a settings file stopped with "SECRETS_ENCRYPTION_KEY is required". It now imports the application once the key is set. Found by the 0.16.1-alpha.5 public site build, which was made with this file overlaid. - Moving a submitted change set to a new model release keeps it submitted. Confirming the move used to answer
409 CHANGE_SET_SUBMITTED. The move now completes and the request stays submitted; approvers decide it on the moved version. Only the submitter or a holder of Approve publications may move it. The request's history shows the step. - The Management API page in the API reference offers a client's credentials while signed in. R13-F35 gave the signed-in person's session token to
engineOAuth's client-credentials flow, so the page showed only that token and no Token URL, Client ID, Client Secret or scope. The session token is now a scheme of its own,signedInSession(a bearer token, preferred while signed in, renewed as before), andengineOAuthalways shows the client form withmanagementticked. The auth type list is a multi-select: tickengineOAuthand unticksignedInSessionto send with the client's token only. Signed out, onlyengineOAuthis offered. Consumer API pages are unchanged (oauth2only,consumerticked, no session token).
Fixed (wide screens)
- Application pages fill a wide window. Lists, relation matrices, mapping grids, relationship tables, trees, record pages, group pages and change-set pages stopped at 1440px with empty margins either side on a wide display; they now span the main column, and its side padding is proportional to the window, so the margin looks the same on every screen: about 1.4% of the width (20px at 1440px, was 32px; about 27px at 1920px and 36px at 2560px), at least 16px and at most 40px. Home, Changes, Notifications and Account keep their reading widths. The browser runner
lb-list-browsingchecks the list card and a relation matrix at 1920 and 2560px, and UI acceptance now names a wide layout (1920 and 2560px) beside the narrow one.
Fixed (the 0.13 release review)
Findings of 0.13 R's full review of every change since 0.16.0, numbered R13-F1 onwards.
sc07brought up to date. Its large-set step expected the starter's 25-target row to beambiguous/ "Ambiguous (25)"; since the cardinality rule ("A mapping row with several targets is "Mapped" where the relation lets a source hold many", above) it ismapped/ "Mapped (25)". The runner names the change; 9 checks pass.- The Add members dialog of a group has the dialog's margins. Why: its content sat directly on the dialog's edges (no
dialog-body), and the paste box had no field styling, so its text touched the border. It now usesdialog-bodyand the paste boxfield-input, like every other dialog; the security runner (test:browser:security) takes a screenshot of both tabs. Reported by the user during 0.13 R. - Known limit, not fixed (decided by the user 2026-11-10). In a container all assistant sessions run as the one
assistantuser, so a session could read other tasks' folders and transcripts and a concurrent session's token; the engine's secrets stay out of reach. Stated indocs/OPERATIONS.md(AI_AGENT_SANDBOX); a user per session is planned later. - R13-F1.
images.ymlreacted to every successfulCIrun whose branch was namedmain, and a fork's pull request from its ownmainis one: its commit was built, pushed and tagged:main, and the smoke step ran the fork'scompose.ymlwith the registry login present. Thepreparejob now needs the run to be a push (workflow_run.event == 'push') to this repository (head_repository.full_name == github.repository).images-cleanup.ymlhas noworkflow_runtrigger (schedule and manual only) and needed no change. - R13-F2. Every third-party action in
images.ymlandimages-cleanup.yml(actions/*,docker/*) is pinned to a full commit SHA with its tag as a trailing comment, so a moved tag cannot change what runs with the registry token. - R13-F3. With a managed identity (
identityMode), the host'sapi://AzureADTokenExchangetoken was posted asclient_assertionto whateverauthorityUrlan administrator saved. With anidentityModethe engine now accepts only Microsoft's sign-in hosts (login.microsoftonline.com,.us,login.partner.microsoftonline.cn,login.chinacloudapi.cn): any other address is422 REQUEST_VALIDATION_ERRORatauthorityUrlon save, and an address saved earlier is refused (connection) before anything is sent. - R13-F6. An assistant task's accept loaded into the task's workspace the items of an items dry run that may have been made against another workspace, and
POST /model-drafts/{id}/dry-runwithitemsneeded only Model authoring, not read access to the workspace it previewed against. A task whose check is of another workspace now fails when the proposal lands and again at the accept, loading nothing; an items dry run needs Browse in itsworkspaceKey(403 FORBIDDEN). - R13-F7. After a first accept installed the draft's release and rebased the draft on it, words and a second accept found no model change, selected no release and made the change set on the workspace's release while the items were checked against the draft's. The accept now installs the draft's release only when the model changed, and otherwise makes the change set on the draft's own release when that is not the workspace's.
- R13-F8. A Stop during the accepted load raised the cancellation but left the change set and the release the accept installed, though the task's note said the change set was discarded. The accept now discards the change set and withdraws the release it installed, then ends the load as stopped.
- R13-F11. A new items dry run of the same draft during an accept deleted the checked items, and the batch started with none. While an accepted task of the draft is loading, a new items dry run is refused (
409 AI_TASK_STATE); and if the stored items are gone when the load copies them, the task fails saying so, with no change set or release left. - R13-F9.
ClaudeCodeProviderpassed the whole context as one command-line argument, which Linux caps at 128 KiB (E2BIG), so a large context failed as "the claude command could not be run". The context now goes toclaude -pon standard input; only the fixed instructions and schema stay on the command line. - R13-F10. A Claude Code reply that was an error (or a non-zero exit) carrying
usageandtotal_cost_usdraisedProviderErrorwithout them, soai_callsrecorded no tokens or cost for a call the account was charged for;ProviderErrornow carries them andaskrecords them. The Claude Code child process also got the engine's whole environment (DATABASE_URL,SECRETS_ENCRYPTION_KEY); it now getsPATH,HOME,LANG,LC_*,TMPDIR,USER,LOGNAME,SHELLand theANTHROPIC_*andCLAUDE_*variables only. - R13-F25. The public API reference (
app.management.reference.static_site) built the management document with job creation off and every other gated setting at its default, so it lacked model drafts, jobs, imports and assistant-task writes, model mappings, source files, inference, moves, dry run, change-set migration and all consumer-subscription operations, and the guide's links to those tags went nowhere. It now builds the application with every gated setting on (INTEGRATION_JOBS_ENABLED,MODEL_ADMIN_MUTATIONS_ENABLED,WORKSPACE_ADMIN_MUTATIONS_ENABLED,CONSUMER_SUBSCRIPTIONS_ENABLED) and job files in a scratch directory it removes; a test asserts the drafts, jobs and consumer-subscriptions paths are in the document. An instance's own/docs/apiis unchanged on purpose: it describes what that instance has switched on. - R13-F26.
guide/tools/inventory.pyimported the application with default settings, somanagement-api.jsonlacked the gated operations (248 operations; now 294), andcheck.mjs coveragecounted every operation with asummaryas covered although FastAPI words one for every operation. The inventory is now written with every gated setting on, and recordswrittenSummary(a summary that differs from FastAPI's generated one). An operation counts as covered when a page links its tag in the API reference (#tag/<tag>), names itsoperationIdor its method and path, or its code carries a written summary. The count is now 18 uncovered entries (listed inguide/_coverage/inventory/uncovered.md), not 0. - R13-F27.
npm run verifyread the committed inventory without checking it, so a settings change (AI_AGENT_TIMEOUT_SECONDS) never reached it, and a setting key counted as covered when a longer key containing it stood on a page (AI_LOG_CONTENTbyAI_LOG_CONTENT_DAYS). Verify now runsnpm run check:inventory(inventory.py --check: it needs no database, writes the inventory to a scratch directory and fails when the committed JSON differs, ignoring line numbers), and keys match as whole words, paths not running on into a longer path. - R13-F28. A build for an older version (
PUBLIC_GUIDE_BASE=/docs/0.13) kept the/docs/...links and/docs/screens/*.pngimages that page bodies write, so they pointed at the latest version. The build now rewrites them to the configured base (guide/tools/rewrite-base.mjs, an Astro integration over the built pages, so raw HTML in a page is covered too), andcheck.mjs linkstakes the same base fromPUBLIC_GUIDE_BASE. Checked with a/docs/0.13build: its pages, screens and downloads carry/docs/0.13/and the link check passes. - R13-F29. The plan and the guide's process text named a
status: stub | writtenfront matter and a build refusing stubs that never existed: a page is insrc/content/docs/only when it is written, and briefs are outside the content collection. The plan's exit gate and its lines on the schema and checks now say so, the guide-home title item is marked closed,AGENTS.mdnamesnpm run verifyas the guide step,docs/testing/EXECUTION_GUIDE.mdsays/docs(not/guide) andverify, andguide/package.jsonand thecheck.mjsheader no longer say/guideor "not in verify yet". - R13-F30. The troubleshooting page told operators
ENGINE_UNREACHABLEin the browser meant DNS, TLS or proxy trouble; that code exists only in the Vite development proxy. The row now describes the production symptom: nginx's plain502 Bad Gatewayor504 Gateway Time-outwhen the engine is down or unreachable, with what to check. - R13-F31. The API reference's script policy hashed only bare
<script>elements, so an attributed inline script in a host theme's header would have been blocked; it now hashes every inline script and not a script with asrc. The documentation at/docsallowed'unsafe-inline'scripts; its inline scripts are a fixed set (ten, the same on every page), so each served HTML page now names its own by hash (guide_site._page_headers, cached by file and modification time) and'unsafe-inline'is gone fromscript-src(styles keep it). - R13-F32. The guide's sidebar skipped a listed page that did not exist and omitted a page its section did not list, silently.
check.mjs sidebar(part ofnpm run checkand verify) now fails on a listed slug without a page and on a page not listed (onlyindexand404are unlisted). - R13-F33. The 0.13 plan, the C13-AI-REST notes and the C1 notes named the internal example model's workbook, its types and an outside product. They now say "the example model" and use neutral type names, and "a CRM".
- R13-F34. The guide pages for the 18 capabilities the real coverage check found uncovered.
- R13-F35. On a consumer API in the API reference, "Try it" failed with
401 TOKEN_INVALID(reason: audience) and the person could not get a working token there. The page's sign-in configured bothengineOAuthandoauth2on every source: a consumer source offeredengineOAuthwith no scopes (Authorize then issued amanagementtoken, which consumer data routes refuse), and the signed-in person's session token (alwaysmanagement) was prefilled intooauth2in place of Client ID, Client Secret and scopes. Each source now carries its ownauthentication: the Management API sourceengineOAuth(Token URL, scopemanagementticked, the session token prefilled and renewed as before), every consumer sourceoauth2only (Token URL, scopeconsumerticked, no session token), so Authorize with a consumer client's ID and secret issues a token the contract accepts. - R13-F21. A model proposal that came with both a load and moves showed no Accept at all (the header button needed no load, the bottom block no moves), and nothing carried
journey-accept. Now the header shows "Accept the model" (withjourney-accept) whenever the proposal has moves, and "What the load does" no longer says "Accepting loads exactly this" there: it says accepting loads none of it, because the records move by the migration you confirm first. - R13-F22. The install and apply dialogs started the workspace migration without the draft's mappings when Install/Apply was pressed before
GET /model-drafts/{id}/migrationreturned or after it failed, and kept the figures of the previous workspace or version when those changed. The migration read is now kept per draft, workspace and version; Install/Apply wait until it is read, and a failed read is shown ("The migration could not be read") and starts nothing. - R13-F23. In the relationships grid's Change values dialog an unreadable typed value kept the "not a valid value" error, no preview and a disabled Apply after Clear was chosen, and after another attribute was chosen. The error now counts only under "Set to" and is reset when the attribute changes.
- R13-F24. Settings > Appearance drew four of its radio groups by hand and none linked its help text from the radio buttons. All five groups now use the one segmented choice component (
Choice, whichThemeChoicealso uses), each radio is described by its group's help text, with the same look as before. - R13-F4. The dispatcher woke a webhook only for events stored under the API's own key, but an earlier contract version's events are stored under
<key>@v<n>, so a subscription on a kept version was not woken by a change only that version derives (a delivery then waited for an unrelated change).claimnow also matches the kept version's store key. - R13-F5.
20261127_01's Microsoft 365 check comparedms_identity_mode = 'direct', which is NULL, not false, for a row with a secret or a certificate, so such a row passed without its tenant and application IDs. A new migration20261127_02replaces the check withIS NOT DISTINCT FROM 'direct'(the model's constraint too);20261126_01and20261127_01now say in their docstrings that their downgrade deletes the Microsoft 365 or managed-identity mail settings. - R13-F12. A type newly listed in several applications was given the alphabetically first one as
toApplicationKeyin the draft's migration, so records could move to an application that only borrows the type for a view. It now names the application where the type is own (atableview of it or atreeview including it); with none or several own ones it names none, and the preview refuses withAPPLICATION_TARGET_AMBIGUOUS. - R13-F13. Records refused
APPLICATION_TARGET_AMBIGUOUSwere kept out of the loss cascade but still deleted when the preview executed the migration; with children the parent key failed, the preview job errored andGET /model-drafts/{id}/migration?workspaceKey=answered 500. Such records are now never detached, emptied or deleted; the refusal is listed. - R13-F14.
setKeyrewrote the current consumer API definition but not the earlier versions it keeps, so a kept version went on naming the old key and the draft became invalid. Every kept version's slots are rewritten too, andkeyChangesnames the API when only a kept version reads the old key. - R13-F15. A relationship whose relation type was renamed by
relationTypeMappingsand that also had a value changed was advanced twice: the feed and history said version v+1, the row held v+2. It is advanced once. - R13-F16.
GET /model-drafts/{id}/migration?workspaceKey=needed only Model authoring at the instance but answered with record counts and example names of that workspace. WithworkspaceKeyit now also needs read access to records there (403 FORBIDDEN, group Browse). - R13-F17. A subscription filter on a
relation:<attribute>match field checked that a value exists (FILTER_MATCHES_NOTHING) across every workspace's relationships. It now counts only relationships of the subscription's workspace. - R13-F18. An error answered before the read began (an operation the version does not offer, a bad parameter, a bad events request) on a kept contract version lacked the
DeprecationandSunsetheaders every answer of it should carry. The error handler adds them. - R13-F19. The record a split or list conversion creates took the first moved value as its name without a limit, so a value over 500 characters failed the migration on the name column. The name is cut at 500 characters.
- R13-F20. A migration advanced relation set versions only for the relationships it removed. It now advances the sets of every anchor whose relationships it creates (a split or conversion), renames (the set it leaves and the one it joins) or removes.
- R13-F36. The 0.13 RV change added the view
system-value-mappingstomodel/rdm-starter.jsonwithout raisingrelease.version, so an instance upgraded from 0.16.0 refused to install the starter at startup ("changed without a new release version", logged; the engine started) and the view never arrived. The starter is nowrdm-starter@0.10.1. At startup the engine installs that release beside 0.10.0 and gives it only to workspaces that run no release yet; a workspace already on 0.10.0 stays there until a person with model management rights activates 0.10.1 there (Settings › Model, or the workspace's model activation), see Upgrades indocs/OPERATIONS.md. A test now holds the starter's content hash by version, so a content change without a new version fails the suite. - R13-F37. Settings › Jobs, in the Grouped view, listed a job that belongs to no group without saying Cancel requested after a person cancelled it, so a cancelled running job showed no sign the request had been taken until it ended. The single job's row now shows it, as the flat list and a group's rows do.
- R13-F38. The model pane listed a type twice, under the same key, when several views of one application borrowed it ("From other applications"; React logged duplicate keys), and listed an unused type placed in a navigation group a second time under "Listed, used by no view here". The pane now shows each type once per application, as the editor change "borrowed types shown apart" says: a borrowed type stands at its first place, and a type in a group stands in its group only (with the count of the unused list following). Vitest cases hold both.
- R13-F39. Moving a type to another place (
setContainment) left a relations panel on another type's page sorted by an ancestor type the related type no longer sits under (cannot sort by 'ancestor:…'), so the draft held an error nobody had made and the next step could be refused. The move now drops such sorts (an ancestor sort insortFieldsordefaultSortof a panel that lists one related type), and its undo puts them back. A unit test intest_layout.pyholds it. - R13-F40.
setLabelrefusedreverseName("Unknown parameter") since the kinds declared their parameters (0.13 AQ), so a relation type's reverse name could not be edited in place in the editor.reverseNameis a declared parameter ofsetLabelagain; the generated kind tests now apply it to every relation type with a reverse name. - R13-F41. "Activation needs a migration" listed the note "A step replaced the whole model" once per such step, so a draft with several of them rendered duplicate keys (a console error) and the same sentence several times. The note is now made once, and the notice keys each note by its place. A Vitest case holds it.
- R13-F42. A matrix cell's dialog could not be edited by keyboard: since 0.13 RV a cell's records are a table edited where it stands, and its edit pencils were out of the tab order (as a list's are, which has the row's Edit record for the keyboard), so only Open reached the record. The pencils of this table are tab stops (
EditInPlaceCelltakestabbable, passed byRelationMatrixCellTableto the record fields and toRelationValueCell) and show while focused; pointer users see no change. A Vitest case holds it anda4-relation-matrixnow reaches the pencil by Tab and opens it by Enter. - R13-F43. On a phone, a change set's name in the pinned band was cut to "Co…" (the waiting pill and the Withdraw button took the row since the compact band, 0.13 FX). The name now has a row of its own on a phone and the pill and button follow it.
- R13-F44. On a phone, a type's page in the model editor put the tile alone on a row above the name since the facts chip read "Released: a key change needs a migration" (0.13 MG): the title block's width was its chips side by side. It now shares the row with the tile and the chips wrap inside it.
- R13-F45. The visual gate's
publications-viewscenarios (light and dark) did not mask the publication ID and the publish time, which differ on every run, so they sat at the 0.5% threshold and failed or passed by chance. They are masked. - R13-F46. The model editor pane's "Model | Consumer APIs" tabs and its + button scrolled away with the rows, and half of them were hidden when a selection was scrolled into view (seen in the re-recorded
editor-proposed-type-light). The strip now stays at the top of the pane's scroll area with the pane's background, and rows scrolled into view stop below it (scroll-padding-top). On a phone, where the page scrolls, it is as before. - R13-F47. A borrowed type's row in the model pane cut its kind chip and status mid-word ("Codelist matrix C…", "Master value mapp"). The name now gives way with an ellipsis (its full text is in the row's tooltip); the detail ("view · relation · from home") shrinks first, then the name, and the kind chip stays whole.
- R13-F48. On a phone, the change set's facts line ("Version … · Created by … · Submitted by …") wrapped so that lines started with "·". The separator now follows the fact before it, so a wrapped line ends with one and never starts with one; desktop reads as before.
- R13-F49. The model editor's pane lost its first row's place: the "+" that stays with the sticky tab strip (R13-F46) was lifted onto the strip with a negative margin that also pulled every row 56px up, so the strip covered the first rows and the Consumer APIs tab could not be hovered or clicked. The lift is now given back below the button, so the rows keep their place under the strip, which still stays visible when the pane scrolls. Checked by
authoring-a4aand a screenshot of the scrolled pane. - R13-F50. The build reference (0.13 BR) lived in
app/build_reference.py, which two routers imported across the tiers (test_tiers_hold); it moved toapp/common/build_reference.py(it needs onlyconfig), behaviour unchanged. - R13-F51. A record list whose first rows asked for more width than the window had (RDM Systems › System values, with its relation columns) froze its columns wider than its container, so the last column's pencil and trash and the row menu were cut at the right edge with nothing showing the table scrolls. The table now takes the excess off its widest columns (their one-line cells end in an ellipsis; none goes below 120px) so it fits, and a table that still overflows shades the edge that has more beyond it.
- Visual baselines retaken for 0.13 (build acceptance, R block 5). 67 scenarios differed from baselines last recorded in 0.12 or earlier; each difference was looked at and explained by an intended change (top bar Help link and refiroo mark, the workspace actions menu, the compact change set band, model version 0.10.1, build reference on About and sign-in, Microsoft 365 on Mail, Theme on Appearance, grouped Jobs, the AI and Agent access settings pages, relationship tables, borrowed types and groups in the model editor), and the baselines were re-recorded.
- Browser runners brought up to date.
authoring-a4a: a type that contains itself and a type placed in a navigation group now stand once in the pane ("Editor: borrowed types shown apart", c120ff18), so the runner asserts one row (the group's, one level under the group row), unique row keys, and the history for a drag to the top level.a4anda5: a matrix cell opens as a table with values edited where they stand, Active as the flag itself with the refusal as a notice over the cell, and unlink as a row's own button ("matrix cell dialog as a table", 67044f34); a cell shows the first of its cell fields with a value ("matrix cells show their fields", 89042b4d), so the runners read the translated name or the code.workspace-v1: the migration page's done notice reads "was published with the move, as Publication N." (0.13 AB, 5d97640e). wm1needed no runner change (R13-F41).authoring-a4b: the drag of a hidden panel back into main now scrolls the panel's grip into view first (the page scrolls inside the window since "Wide screens", so the grip could lie above the view); the header names it. - Browser runners brought up to date (Settings and assistant).
m1-maillooked for the Settings link "Mail" by a name that also matched "Notification email" (two different pages; the runner now asks for the exact name).u3-settingsexpected a keyboard order without the top bar's Help link and the AI and Agent access entries, and pressed Shift+Tab a fixed 17 times back to Model; it now expects them and counts back from the order it asserts.ap1-aiexpected the values send level to read "up to 20 per column"; 0.13 CP made it "with no limit per column".ap12-change-model(andhelpers/reshape-stand-in.py): the engine offers no change that loses what the person did not ask to remove (2026-11-09), so the stand-in's part-of-a-type proposal now lists the split type as a removal with the person's words andmovedTo; the runner also waits for the first step's button before counting it.u4-integrationfollows the F37 fix;ao1-impact-summarypassed on three runs without a change (the axe failure seen in the release run did not recur).
Added (list browsing: Find a list, a table's inactive records and a matrix's empty columns, 0.13 LB)
-
The sidebar's Collections have a Find a list box once an application has more than ten entries. Why: with many lists the way to one was scrolling. Typing narrows the collections and placed matrices across every group and those in no group to the names that contain the words (case-insensitive, the match in bold, "N of M" announced politely); a group with a match opens with only its matches, a group without is hidden; Enter opens the first match, Escape clears. The reader's own folds are never written while a search stands, so clearing returns them.
features/objects/Explorer.tsx,model/collections.ts(narrowNavigation). -
A table view may declare
availabilityField, a yes/no attribute of its type (schema 0.10, optional). Why: a list keeps retired entries, and a reader needs them out of the way without losing them. A record whose value is false keeps its row, its name in the secondary ink with a neutral Inactive pill ("Not" when the attribute is not called Active), and the toolbar gains Hide inactive (N), which applies the ordinary filter <field> ne false(not false, so a record with no value stays and is not marked; the pill reads "Active: not No") as a pill in the address; N is one count read through the list's own count of the records whose value is false, exactly those the switch hides. The list query gainednefor yes/no attributes (filter.<field>.ne=false;docs/API.md). A field that is no boolean attribute of the type is refused at…/config/availabilityField; a renamed attribute is followed and a removed one takes the setting away, as a tree's. A model that does not use it keeps its content hash. The model editor's table settings offer Inactive records….models/schema.py,validation/application_rules.py,engine/reads/object_query.py,engine/model/candidates.py(filter_operators(listing=True)),authoring/references.py,ui/ObjectTable.tsx,features/objects/ObjectToolbar.tsx,features/model-editor/layout.ts; OpenAPI lock re-baselined (additive); journey J33.10,ExplorerFind.test,CollectionView.test, runnere2e/lb-list-browsing.mjs. -
A relation matrix can hide its empty columns (0.13 LB-D8). Why: a matrix over many languages or systems shows mostly empty columns. A toggle Hide empty columns beside Filter (off by default, kept per view in this browser with the column widths,
useTableDisplay) leaves out the columns with no related record in any row the search and parent scope leave, and the line "N empty columns hidden · Show all" says how many; columns chosen in Filter win and the toggle then says it has no effect. The engine decides it for every matching row in one statement and skips the columns in the column pages themselves (hideEmptyColumns,RelationMatrixPage.emptyColumnsHidden), so column paging stays exact; a change set and a publication read their own values.management/projections/relation_matrix.py,features/objects/RelationMatrixView.tsx,ui/TableDisplay.tsx; OpenAPI lock re-baselined (additive);test_relation_matrix,RelationMatrixView.test,e2e/lb-list-browsing.mjs.
Fixed (mapping grids and relation matrices on the shared table, 0.13 FX)
- A mapping grid's record names open the records, its columns resize and a cut-off value shows whole. The "Map to …" views and the matrices ("… by system") were drawn with tables of their own, so the source record's name was not a link, the columns could not be sized and a value cut off at the column's edge could not be read. Both are now drawn with the table every record list uses (
ui/DataTable, extended with row-header cells, per-cell attributes, a header of its own and a grid's role and keyboard handler, which a matrix needs): columns resize and keep their widths per view, and a cut-off value shows whole when pointed at or focused. In a mapping grid the source's name and each target chip open the record; a click elsewhere on the row and Map/Change still open Change mapping. The matrix keeps its cell dialog, its grid keyboard navigation and the pencil's tab stop. Each column's resize handle is a tab stop before the grid, as in every table (a page of 20 matrix columns adds 21); the a5 runner's tab budget to reach the grid went from 20 to 40. - A mapping grid's columns of a row may name the type above the row.
sourceColumnsacceptsancestor:<type>, as a matrix'srowColumnsalready do (the coverage read already returned it as the ancestor's name; the model check, shared with the matrix, already accepted it), and the model editor's mapping-grid Columns of a row offers those types under "From the parents". A system value's mapping grid can show its system. Search does not look at them.
Changed (model editor: borrowed types and drag-to-reorder)
- The model pane and a type's page tell a type an application holds from one it only borrows. Why: an application lists a type either because it keeps the type's records or because one of its relation views (a relation matrix, mapping grid or relationships table) needs it, and the editor showed both alike, and a grouped type twice. The outline (
GET /model-drafts/{id}/outline, additive) now says, per application,ownTypesandownTree(atableview of the type, or atreethat includes it),borrowed([{objectType, home, uses}], withborrowedTree) andunused; a use is{view, viewName, viewType, role, relationType, relationName}with role rows, columns, cells, from, to, or link (a type on the containment path between a matrix's column and cell types). A type'shomeis the first application, in model order, that holds it; the type entry hashome,ownInandborrowedIn. The pane shows each type once per application: a type placed in a navigation group stands only in its group (a held type as a plain row with the types it contains beneath it, a borrowed type with a dashed icon, a kind chip and "view · relation · from home"; a group row says "group · N" for its entries), the other held types in their containment tree, the borrowed types in no group under "From other applications", and the types no view uses under "Listed, used by no view here" (with "can be unlisted" where the existing unlist rule allows it). A borrowed type opens on its home application, so its Preview and layout editor show where its records are edited. The type page says "Maintained in" and adds an "Also in " line per borrowing view. - Navigation groups and group entries are dragged into order, and Starts folded is a one-click cell.
EditorTabletakes an optionalreorder(rows show the grip and a drop line; a drop sends the position the Move up and Move down items send); it is on for an application's navigation groups and a group's entries. Move up and Move down stay in the menus. An application page splits its types as the pane does: "Object types" lists the types it holds, "From other applications" the borrowed ones (From, Used by, Relation, Group), and "Listed, used by no view here" the unused ones; a borrowed row opens on its home. A borrowed row in the pane has the full text as its tooltip. A group page's entries table says each entry's Kind (List, Borrowed list or Relation matrix) and what it opens on as "view kind · view name". The Starts column became Starts folded, the one-click yes/no cell (ui/FlagCell, the box alone under its header) that saves in one step.
Changed (the Claude Code proposal provider is built in, AI_PROVIDER=claude-code)
AI_PROVIDER=claude-codeis a provider of the engine, in the image. Why: a deployed image setAI_PROVIDER=devtools.claude_code_provider:providerand refused to start (No module named 'devtools'), becausebackend/devtools/is not in the image (only the guide's reference instance mounted it). The provider moved toengine/proposals/claude_code.pyand behaves as before: one headlessclaude -pcall per request, no tools, MCP servers, project settings or saved session, in an empty temporary directory,AI_MODELas--model,AI_TIMEOUT_SECONDSas the limit. It needs noAI_API_KEY; it signs in as Claude Code does (CLAUDE_CODE_OAUTH_TOKENorANTHROPIC_API_KEYin the environment, or the host's sign-in). Theusage.jsonlfile is gone:ProviderAnswerhas an optionalcost_usd(the CLI'stotal_cost_usd), andcalls.askwrites it toai_calls.cost_usd, so the cost of a provider call shows in Calls; the other providers leave it empty.backend/devtools/and the reference instance's mount are removed; the reference.envand README useclaude-code. Production normally keepsclaudewithAI_API_KEY. Docs:.env.example,docs/OPERATIONS.md, the guide's Set up AI, Developer instance and AI pages.
Fixed (mail through Microsoft 365 arrived empty, 0.13 MS)
- A message sent through Microsoft 365 arrives with its text and HTML. Cause: the MIME handed to Graph's
sendMailhad bare LF line ends (the message's own policy); Exchange then finds no parts and delivers an empty message, while SMTP was unaffected becausesmtplibconverts the line ends.engine/mail/graph.pynow serialises with CRLF; J46.11 checks the MIME Graph receives. Seen on the first live Microsoft 365 setup (managed identity, 10 Nov 2026).
Added (build reference on About and sign-in, 0.13 BR)
- About and the sign-in screen show which commit and tag the engine was built from. Why: nobody could tell which build an instance ran, only its version.
GET /api/v1/systemhasengine.build: {commit, tag}(the first eight characters of the commit, and the release tag on it; each null when unknown), and the publicGET /oauth/sign-in/optionsanswersbuild: {version, commit, tag}, nothing more. An image gets them from build arguments (REFIROO_BUILD_COMMIT,REFIROO_BUILD_TAG, passed by.github/workflows/images.yml; the image has no.git); a checkout reads them once throughgit(app/common/build_reference.py). Settings › About, Engine has a Build row with the tag and commit linked to the repository (the source address with/tree/...removed). The sign-in screen (and the reset pages that share its frame) show one very small muted line under the card, "refiroo 0.16.0 · build v0.16.1-alpha.1 (baf823b6)", the tag and commit in the link colour, no links; hidden parts are left out when unknown. The OpenAPI baseline is re-baselined forEngineInfo.buildandBuildInfo. Guide:run/licence-and-sourcedescribes the Build row and the sign-in line.
Added (mail through Microsoft 365, 0.13 MS)
- Mail can be sent through Microsoft 365 (Microsoft Graph) beside SMTP. Why: Microsoft is retiring basic authentication for SMTP AUTH in Exchange Online, and an organisation on Microsoft 365 often cannot give refiroo a user name and password for it. Settings › Mail has a method choice at the top of the form: SMTP server (as before) or Microsoft 365, which has one Sign in with menu of four entries (client secret, certificate, managed identity in this tenant, managed identity hosted in another tenant) and takes the tenant ID, the application (client) ID, a client secret or a certificate (PEM key and certificate; sealed with
SECRETS_ENCRYPTION_KEY, never answered; the page andGET /mail-settingsshow the certificate's thumbprint and expiry,clientSecretSet), the sending mailbox (fromAddress) and, for a national cloud, the token and Graph addresses (defaultshttps://login.microsoftonline.comandhttps://graph.microsoft.com). One method is active: saving one clears the other's saved fields, and a saved credential is kept only for the same tenant, application and token address (422 MAIL_CREDENTIAL_REQUIRED); a certificate that cannot be used is422 MAIL_CERTIFICATE_INVALID. The engine takes a token with the client-credentials grant (plainhttpx, a signed RS256 assertion for a certificate, a cached token), sends the same MIME message as SMTP withPOST /users/<mailbox>/sendMail, and maps failures to the kinds it knows: a refused token, missingMail.Sendconsent or a mailbox the application may not use islogin(shared by every waiting message, with a sentence that says what to change in Entra), Microsoft out of reach, throttled or failing isconnectionand waits at leastRetry-After, a mailbox that does not exist issenderand waits to be tried again, as with SMTP. Bounces are not read: a message Graph accepted shows as sent and a bad address comes back to the sending mailbox. The MIME route ofsendMailhas nosaveToSentItems, so system mail lands in that mailbox's Sent Items. A managed identity is a third credential (MS-D5), where refiroo runs on Azure and no secret is stored:direct(the host's own identity in the organisation's tenant: the engine takes a Graph token from the Azure host's token endpoint,IDENTITY_ENDPOINTwithIDENTITY_HEADERor the VM metadata service, with no tenant token request; an optional user-assigned identity client ID; no tenant or application ID) orfederated(refiroo hosted in another tenant: a user-assigned identity's token forapi://AzureADTokenExchange, or itsUSGov/Chinaform by the token address, is theclient_assertionof the client-credentials grant at the customer's tenant).PUT /mail-settingstakesidentityModeandidentityClientId; the response hascredential,identityModeandidentityClientId; saving a managed identity clears a saved secret or certificate. Where no Azure host answers, the test and queued mail fail as a sharedloginfailure saying refiroo is not running with a managed identity. Migration20261127_01. Migration20261126_01(an existing row becomessmtp);reseal_secretscovers the new secrets. Docs:docs/API.md,docs/OPERATIONS.md(Entra setup), J46.11 in the journey catalogue, the guide's mail pages. Journey:test_j46_mail_microsoft365against a local stand-in for the token endpoint and Graph (tests/graph_server.py).
Documentation (pictures of the Microsoft 365 form, 0.13 MS)
- The mail pages show the form. Three pictures from the reference instance (
admin-mail-microsoft365-menu,-secret,-entra; recipeguide/_waves/MS.screens.json): the Sign in with menu with its four entries, the client secret entry filled with example values, and the managed identity entry with the Entra steps opened. Nothing was saved or sent.docs/API.mdno longer says that a mailbox Microsoft 365 does not know fails at once; it waits and is tried again, as the code,docs/OPERATIONS.mdand the journeys say.m1-mail.mjssection 7b runs a saved Microsoft 365 configuration (secret, certificate) in the browser on both models.
Changed (an AI task is named by what it asks, 0.13 TN)
- A task given in words is titled by a short title its own session writes, not by its request cut off. Cause: the task page and the Jobs page named it by its words cut at 80 characters (mid-word, typos included), and the draft was named the same way. Now the session's
proposal.jsoncarries an optionaltitle(a few words, for example "Move types under the person group"; promptagent/20, no second model call), the engine stores it on the task (ai_tasks.title, migration20261125_01) and the API returns it astitle(null until written). The task page heading, the Jobs page group name and the confirm bar use it; the full request stays shown uncut ("You said: …"). The assistant's draft is namedAssistant: <title>, once. Until a title arrives, and for a task that ends before its first answer, the words are cut at a word boundary, never mid-word (cutAtWordinaiTaskText.ts,cut_at_wordinengine/tasks/store.py, and the same cut in the Jobs page origin name). J50.72.
Documentation (six assistant pictures retaken, 0.13 DOC10)
- The six pictures of the assistant journeys show the current pages.
use-assistant-proposal,model-assistant-workbook-proposal,use-assistant-loaded,model-assistant-workbook-loaded,use-assistant-reloadanduse-assistant-wordswere taken before the checked-items load and showed the saved-profile load, "Accept the model" and proposal-file wording. They are retaken from one workbook task (checked, then accepted with Accept and load to change set, "What the load does", 172 records new, 322 relationships new) and one words-only task ("No change to the model", Accept), with their recipes inguide/_waves/C10.screens.json.
Documentation (the relationships table, grid and cell dialog looked at on a phone, 0.13 RV)
- Inspected once at 390 x 844 (touch) and 1280 px, light and dark; nothing needed changing. The page does not scroll sideways at either width, the grid and the matrix cell dialog's table scroll inside their own scrollers, and the filters, Display, the selection bar, the bulk change dialog, the
+ Linkcells and the group headings work. The result is recorded under RV in the 0.13 plan. test:browser:rv, a browser acceptance runner for the relationships panel, the relationships table, the grid and the matrix cell dialog: values edited and bulk changed with each write read back through the API, live and in a change set, a refused batch naming the failing item, Layout… in the model editor, keyboard, 390 x 844 touch and a dark pass, with axe.
Fixed (the guide home's tab title, the AI task fixture's keys, 0.13 FX)
- The guide home's browser tab reads "refiroo documentation" once, not "refiroo documentation | refiroo documentation". Cause: Starlight joins the page title and the site title, and the home page's
titleequals the site's. Now the home page's front matter sets its<title>tag itself (headinguide/src/content/docs/index.md); its visible heading is unchanged. test_model_independencepasses again. Cause:frontend/src/features/ai-tasks/designFixture.ts, a fixture outside the test's reach by name, held the starter model's keysdomainandcodelistas literals. The fixture now uses neutral keys (topic,category), and the tests that assert them follow (DesignView.test.tsx,DesignCheckView.test.tsx).
Fixed (the assistant's note shows a table as a table, 0.13 FX)
- A markdown table in the assistant's note ("| Result | Count |", "|---|---|") is a table, not plain text. Cause:
AssistantText(frontend/src/ui/AssistantText.tsx), the one component that shows the assistant's words in the task notes, the proposal and the design view, read paragraphs, lists, bold and code only, so a table's lines ran together as text. It now reads a table (header line, rule line, rows) and shows it with the app's ruled table (SettingsTable; at phone width each row stacks, so the page does not scroll sideways). Cells are text, never HTML. - On a phone, the assistant's task page no longer runs its text and cards past the right edge. Cause:
.task-check(frontend/src/styles/settings.css) is a grid whose one column took its widest child's width, so the notes, sections and cards were wider than the window and clipped. The column is nowminmax(0, 1fr), so each child fits and its own table scrolls inside it. Seen with the note of the task whose table was fixed above, at 390 px. - The two proposal pictures of the assistant journeys are retaken from a new checked task (
use-assistant-proposal,model-assistant-workbook-proposal; the first task was already accepted), and the alt text and captions ofuse-assistant-proposalonuse/loading-with-the-assistantandjourneys/work-with-an-assistantdescribe what it shows now (0.13 DOC10).
Fixed (a taken release version has a way forward, 0.13 FX)
- Apply, Install and Load no longer end in "already installed with different content" with nothing to do. Cause: the editor proposed the base release's next version without knowing which versions were installed (the assistant's drafts take them), and after the failed try the draft kept the taken version, so every retry hit the same
RELEASE_IDENTITY_CONFLICT. Now the draft outline carriessuggestedReleaseVersion(a free version, or the draft's own when free), the 409 carriesdetails.releaseKey,releaseVersionandfreeVersion, and each dialog (and the model workbench) offers Use, which sets the release and goes on. next_free_versioninengine/model/releases.pyis the one rule, shared with the assistant's install.
Fixed (a file already in the workspace is a checked load, 0.13 AB)
- A workbook whose every record is stored already ends
checked, and accepting it endsloadedwith nothing new. Cause: the assistant's prompt (agent/18) let the session decide "already there", send no items dry run and write no proposal, so the task failed, and an accepted items load whose change set held nothing failed with "nothing to load".agent/19says the items dry run and the proposal are always sent (the draft is the current model, each item names an existing record by ID); the accept endsloadedwith zero counts and the matched records asunchanged, no release when the model does not change, and the empty change set stays on the task to discard. A session that ends with no proposal still fails.
Fixed (the change set band stays compact, 0.13 FX)
- On a change set page the pinned band holds one compact strip, and the rest scrolls with the page. Cause: the change set bar in the band (
contextActions) reachedBelowContextBandoutsideContextBandSlotContext, whose provider wrapped only the page content, so its notices (model mismatch, sent back, warning) rendered in place inside the sticky band instead of in the slot below it; with a long submit comment the band covered about half the window.AppShellnow provides the slot to the band as well. The band keeps the name (one line, ellipsis, full name in its title), the "Waiting for approval" pill and the buttons; the version, source, model and creator line, "Submitted by", the comment (two lines with Show more / Show less where it is longer) and "Withdraw the request to edit it." sit below it with the notices. Frontend only.
Fixed (a moved collection opens in its new application, 0.13 MG)
- An old address of a collection a model migration moved to another application opens it there, instead of the "not in this workspace's model" page. Cause:
missingElementlooked only at the application named in the path, so a view, kind of record or navigation group that now lives in another application of the same model was reported as missing.relocatedPath(routing/routes.ts) now finds the one other application that holds the key and rewrites only the/apps/<key>segment, keeping the rest of the path and the whole query; the router replaces the history entry (Back does not loop), and Home's remembered "continue where you left off" address is moved the same way. A key held by none or by several applications still shows the page as before. Frontend only.
Fixed (a type's preview follows its application, 0.13 DF5)
- A type listed in several applications opens its preview, record page and table views in the application whose row was selected. Cause: the editor's selection kept only the type, so the preview took the first application that listed it (a country listed in System codes, Reference lists and Translations always showed System codes and "The draft declares no table view for Countries"). The type selection and the editor address now carry
application; tree rows and collection rows pass their own, and only the selected row is highlighted. A link naming no application, or one that does not hold the type, falls back to the first application with a detail page or table view for the type, else the first that lists it. Frontend only. - A selection that names no application opens the one that holds the workspace's records of the type, and the preview reads records for that application only. Cause of the follow-up report: a country's records all belong to Reference lists, but the preview took System codes (the first with a detail page), tried to show a Reference lists record in its page ("This object belongs to another application") and a layout edit went to a page no record uses. Resolution order: the application named, else the one that owns the workspace's records, else the first with a table view or detail page, else the first that lists the type (
applicationForType). The records query carriesapplicationKey; when the application chosen has none in the workspace, the empty record is shown with a line naming the application that holds them and a link to switch. A type listed in several applications shows the application in the band, as a choice among them.
Added (attributes of the relationship in views, 0.13 RV)
-
A matrix cell shows and changes the relationship's own values. Each cell record carries
relationAttributes; the cell dialog shows them under "Attributes of the relationship", a link or a create-in-cell sends the values entered, and an edit claims the relationship's version, with the conflict notice of the Edit values dialog. One sharedRelationAttributesserves all three. -
A matrix cell's relationship values are edited in place, as a record's fields are: each value shows with the Edit pencil, opens a small form with Cancel and Save, and saves against the relationship's version; there is no separate "Edit relationship values" step. The relationships table's cells save through the same code.
-
A matrix cell opens as a table. A row per record: its name and the view's cell fields, then the relationship's values under "Attributes of the relationship", then Open and Unlink (asked in the row); every value edits where it stands with the cells of the other tables (
RelationValueCell,EditableField), a save re-reads the cell and keeps the dialog open, and a filter box narrows the records. The separate Edit step is gone; Create and Link are as before. The dialog widens with the columns (Modalwidth) and the table scrolls inside it, name and actions fixed. -
The Relationships panel shows them as columns after the related record's, listed in Display under the same heading; no sorting by them.
-
A new view type
relationTable(schema 0.10, "Relationships table"): one binding's relationships as rows with both records' columns and the attributes as columns; filters, sorting, group headings with counts and cursor paging, live, in a change set and in a publication; cells edit where they stand, ticked rows are set in one request.GET /workspaces/{key}/relationstakesfromObjectId,toObjectId,sort,filter.<ref>.eqandseek, its rows carryrelationVersionand its pagegroupCounts. The editor's Add a view… offers it as a kind; the example model has one over the system value mappings. -
POST /relations/batchsets attribute values on many relationships as one write, all or none, refusing withRELATION_BATCH_REFUSEDand each failing item named. -
A relationships grid (0.13 RG): a relationships table with
layout: "grid"shows each record once with a column group per related record and the relationship's values under it; the viewer picks the related records and the values shown (in the address), filters, links a pair with one click and sets one value on ticked rows or every matching row, counted first (GET /workspaces/{key}/relation-grid,POST /relations/grid-change). -
A relationships grid filters with the collection's own filter bar over the filter fields of its end types' main views (every operator, search, related filters) and the relationship's values, for the rows and for the column records, shown as columns or not; rows are selected and changed as a collection's are, and a saved cell reads only its own row again.
-
The guide's reference model carries relationship values (release 1.8.1): stop to operator holds Local name, Primary contact and Inspected on, system code to municipality holds Confirmed and Note; two relationships tables over the first (rows, grid), a related-block field per value in the Stops API, and
samples/data/reference/relation-values.json.load.pymoves an existing workspace to the release.
Fixed (a refused grid change names its relationships, 0.13 FX)
- The relationships grid's Change relationship values dialog says which relationship each refusal belongs to. Found by
test:browser:rv: a value below an attribute's minimum on two pairs listed the engine's message twice with nothing to tell the pairs apart. Cause: the dialog listedrefused[].messageonly, and the engine's refused items for an existing relationship carried its ID but not its ends. The engine now addsfromObjectIdandtoObjectIdto such items (decide_relation_values, soPOST /relations/batchandPOST /relations/grid-changeboth), and the dialog writes each refusal, in the preview and after a refused Apply, as "Stop A → Operator B: message" with the names the grid shows, once per relationship and reason; the title counts those lines.
Fixed (Layout… of a relationships table, 0.13 RV)
- "Layout…" opens on the view's current layout. Cause: the draft outline (
GET /model-drafts/{id}/outline) carried a view'sconfigonly for tree and mapping-grid views; it now carries a relationships table's too, so a grid view opens on "Grid". - A relationship attribute holding JSON can be a column of a new relationships table. Cause: the Add a view dialog put every selected attribute into
sortFields(and a filterable one intofilterFields), and the model refuses a JSON field as either, so the draft became invalid. A JSON attribute is now left out of both. - A relationships grid can filter by a Yes or no relationship value. Cause: the filter built every comparison (
<,<=,>,>=) before choosing one, and SQLAlchemy refuses those on a Yes or no value, so the read answered 500; only the asked comparison is built now (reads/relation_grid.py).
Fixed (AI task progress line, 0.13 AW)
- An AI task's "Working on it" line no longer shows the session's shell command. Cause:
tasks/agent.pydefined_plaintwice, and the later one (quote comparison, which only lowercases and strips punctuation) replaced the one that puts a call in words, so the raw command reached the page. The progress helper is now_call_in_words; the line reads "Reading stored … records", "Writing …" or "Working".
Fixed (a tall Display popover looks cut off, 0.13 FX)
- The Display popover of a relationship or collection panel on a record page no longer looks cut off, and no popover opens with no height on a scrolled page. Cause: the application frame was a positioned, page-sized box that scrolls with the window, and React Aria measured a popover's room from it, so a popover placed below its button on a scrolled page got a height of zero (the Display popover then flipped above with only the room there, under a scrollbar macOS hides). The frame is no longer positioned (
.app-shell), so room is measured from the window; this coversControlPopoverandInfoHint. A popover now stays below the sticky top bar and data band (its placement boundary starts under them, and follows the window while it is open). When it cannot show all its content it scrolls the page, if that can give it the whole room below its button, else the whole room above it under the band, and places it again; only in a window too short for either does it scroll inside, with an always-visible scrollbar.
Fixed (the assistant re-checked an outdated copy of its draft, 0.13 FX)
- An AI task no longer fails over a draft its session has already corrected. Cause: database sessions keep loaded rows after a commit (
expire_on_commit=False), so on every landing round_proposal_landedread the draft the first round had loaded, not the one the session had corrected through the API since, and the compatibility check refused the same thing again until the rounds ran out. A task that proposedrootCreatable: falseon 25 list types and then set it back failed with the first round'sPARENT_REQUIRED. The draft is now read from the database on each round (populate_existing), as is the draft version a turn starts from, and a locked read of a task (load_task(..., for_update=True)) sees the committed row.
Added (attributes of the relationship in consumer API related fields, 0.13 RA)
- A related block can publish a value stored on the relationship itself. In
object.related.<name>.fields, the sourcerelation:<attribute-type-key>reads that attribute's value on the relationship each item was reached through (nullwhen it has none), in objects, lists, XREF and resolve answers and in publication andasOfreads. The attribute must be inattributeTypeKeysof a binding of the block's relation, in its direction, from an exposed type; otherwise the draft compile, install and validation refuse it at the field (INVALID_REFERENCE), as they refuse the source anywhere but a related block's fields and thematchOnthat read them. The field has the attribute's value type in the schema and the OpenAPI document; the values are read in the related items' statement, not per item. An event watching the block reports an update of the relationship's values. - They match too. An
onRelatedfilter'smatchOn(withkeepOnlyMatching,scoreOnlyMatching,crosswalk) and an XREFalsoMatchThroughrule'sfirstOfmay namerelation:<attribute>: the value is compared with the relationship's attribute, so an item matches through that relationship only. An XREF rule reads a text attribute. - The editor offers them. The field picker of a related block, and its
matchOnpickers, have a group Attributes of the relationship, listing the attributes of the relation's bindings;GET /model-drafts/{id}/outlinelistsattributeTypeKeyson each relation.
Changed (changes that need a migration are made in the editor, 0.13 MG)
- The editor applies a change to a released element that stored data holds, where it refused it. A type moved to another application or under another parent, a split, a merge, a conversion to a list type, a tighter rule, a removal and a key change are applied like any other step (undo, redo and the dry run behave as usual) and marked Needs a migration in the history, with the records, values and relationships that hold the element (
migrationon the history entries ofGET /model-drafts/{id}/operations). The menu reads "Needs a migration when installed" and Move to… says what becomes of the records.OPERATION_REQUIRES_MIGRATIONis no longer raised. A released key can change; the install dialog says that integrations naming it must change, and names the consumer APIs of the base release that name the old key. - The draft states its migration:
GET /model-drafts/{id}/migration(workspaceKey,againstReleaseId,toReleaseId) derives, each time and from the steps that stand now, which type, attribute and relation type becomes which, which application a moved type goes to, whether its records leave their parents, the split's and the conversion's new records, each key change with the consumer APIs naming the old key, and with a workspace the preview's own landing check. The install and apply dialogs state it and start the preview with its mappings (Install … and preview the migration…); the migration page opens filled in (What the draft proposes), each row marked Proposed and changeable in place. - The workspace migration carries more:
toApplicationKeyon a type mapping (a type listed in several applications moves too),relationTypeMappings,splitsandlistConversions, anddraftId. A record whose type stands at the top after a move becomes a top-level record (its parent and the containment relationship go); a split and a list conversion create records, values and relationships, shown as the new Created outcome (counts.created,?status=created); all as set statements in PostgreSQL. A tighter rule still lists the records that break it as refused, and a removal is still a loss to accept. Activation still refuses a release whose migration has not run. - A migration never loses records of a type the target still has, and the assistant offers none that loses data (user's product rule, 2026-11-09). Records of a type the target lists go to their own application, the mapping's
toApplicationKey, the one application that lists the type or the one that lists it newly; when that is not unique they are refused (APPLICATION_TARGET_AMBIGUOUS, naming the candidates) instead of lost. A workspace migration of 20 types moved between applications while a third application listed every type previewed 459 losses with no mapping; it now moves 179 records and loses none.GET /model-drafts/{id}/migrationwith aworkspaceKeystarts from the release that workspace runs, so after an install the Apply and Install dialogs still state the move. The assistant task runs the landing check on exactly the migration it would hand over, whatever kind of change caused a loss: a loss of a record, value or relationship goes back to the session as a finding, and at the handover no migration is returned and the task says per type what would be lost and that no data was changed. The one exception is what the person's words explicitly ask to remove: the session lists it in the proposal'sremovals(kind, key, the quoted words), the guard allows a loss only when it falls within those removals (records of a removed type or application, values of a removed field, relationships of a removed relation type or of removed records), and the task page and notes state the deletion in plain words with the engine's counts before the person accepts ("As you asked, the Drug list is deleted, deleting 16 records."). Agent promptagent/16. The migration page wordsAPPLICATION_TARGET_AMBIGUOUS. - Found by the before-and-after on a real model: a type listed in several applications that was moved out of one would have lost its records; the draft's migration now names the application it gained.
- A task that moves types to another application hands the migration the move. The task's migration was built from the assistant's
movesalone, so types that keep their key and change application were handed over with no mapping and the preview listed every record and relationship of them as lost. It now starts from the engine's own migration of the draft against the workspace's release (application moves withtoApplicationKey, relation type mappings, splits and list conversions) joined with the assistant's moves, one mapping per type. The landing check, the proposal's "What happens to the records" ("20 types move to Reference lists, 179 records") and the task'sresult.migration(now withrelationTypeMappings,splitsandlistConversions) use that set; the engine's note that a step replaced the whole model is shown as a task note. - The assistant's counts say what they count. A summary counts the records of the types or lists the change touches, named, and states the rest of the workspace apart; it no longer gives the workspace's total as if every record were part of the change (a move of 179 records read as "all 4657 records keep their IDs").
Added (relation matrices in navigation groups, a collection's opening view, a group panel in the editor, 0.13 NG)
- A relation matrix can stand in a navigation group as an entry of its own. A group's
collectionsmay hold{ "view": "<relation matrix view key>" }beside collection keys, in one shared order; the sidebar and the group page show the matrix named after its view, with a matrix glyph in its row type's hue, opening the matrix directly. The row type's entry stays and its view switch is unchanged; a matrix in no group stays inside its row type's entry.navigation.opensmaps a collection to the view its entry opens on, in place of the application'sdefaultViewfor that collection. Schema 0.10, optional: a model without either keeps its content hash. Validation refuses a view entry that is no relation matrix of the application, a matrix in two groups, and anopenskey or view that is no collection or view of it, each at its pointer. Presentation only: the change is compatible. - Operations.
placeCollectiontakesviewin place ofcollectionto place, move (at apositionof the group's one order) or take out a matrix; the newsetCollectionOpeningsets or clears a collection's opening view. Several entries added to a group in one request are one step and one undo. Removing a matrix view takes its entry and its opening with it, and a view given a new key keeps both;GET …/outlinelists each group'sentries,navigation.opensand each view'scollection. - The editor opens a group. Choosing a group in the model tree shows its panel: its entries with their kind, what each collection opens on (changed in place) and state, a menu to move an entry up or down, to another group or out of its group, and Add to group…, a dialog with a search that adds several collections and matrices at once and says where each stands now. The application panel's group table lists matrices among a group's entries and links the group's name to its panel; "In no group" offers Add to a group… with those collections chosen; a relation matrix's menu in the Views table gains Move to group…. The group page's search messages say "collection or relation matrix", and Remove group says that a group's relation matrices go back inside their row type's entry.
Changed (the assistant loads a file as one checked batch, 0.13 AB)
- A draft's dry run takes items.
POST /model-drafts/{id}/dry-runacceptsworkspaceKeyanditems(the batch items, each with an optionalrefnaming its cells, such asPerson!Q31:S31) in place ofsourceFileId; exactly one of the two. The worker previews every item against the draft's model in a rolled-back transaction and stores the items with the completed job.GET …/dry-runanswersitems: per type the records new, matched, changed and refused, the relationships, up to three examples with their refs, the rows read, the parts of the file by sheet and columns, the refused items with their refs, and the finding, read before the rollback, of a name that appears more than once in one type among the records the load touches, with the parents. Held to the background limits and toATOMIC_JOB_MAX_ITEMS(413 BATCH_TOO_LARGE). - Accepting runs the previewed items, with no assistant turn. The engine installs the draft as one release (the install of
POST …/installation, now one function), makes the change set on it (on the current release when the model does not change) and runs the stored items as one atomic batch; the task endsloadedwith the batch's counts, orfailednaming the refused records and their cells with nothing written, the change set discarded and the release it installed deleted again (kept, with a note, when the engine refuses to delete it). A draft edited after the check has every item checked again first. Model-only tasks and moves end as before. - The assistant works as in an ordinary session (
agent/15): it reads the workbook and the workspace's records itself, changes the draft, writes a script that turns the file into items (new records with IDs it generates, existing ones named by ID), sends one items dry run and fixes its items until nothing is refused and the findings are explained. It writes no load profile and asks only what the file and the words leave open, in the person's terms. - The task page shows What the load does in place of "Where each sheet goes" for these tasks: the figures, a table per type whose rows open to example records, the engine's findings and a folded "Where each part of the file goes"; Accept and load to change set says the engine loads exactly those records, the task bar says the same, and the loaded page says an items load only installed its release (publishing moves the workspace) and that new words are a correction checked and accepted again. Tasks recorded with a profile keep "Where each sheet goes". A load made from items saves no import profile; saved layouts load as before.
Added (related jobs grouped on the Jobs page, 0.13 JG)
- A job says where it comes from.
originon every job: the assistant task it belongs to (its own job and every job its session queued) or else the model draft it works on, with the name the task's or draft's page shows;nullfor neither.GET /jobs?grouped=trueanswers one row per origin and one per job without one, newest activity first, with job counts per state;GET /jobs?origin=aiTask:{id}(ormodelDraft:{id}) lists one group's jobs. Without themGET /jobsis as before. The OpenAPI lock is re-baselined. - Settings › Jobs groups related jobs. A group row shows its kind, name, job count, state pills and latest activity; a Grouped / All jobs switch (remembered in the browser) shows the flat list. A group opens in the same view under the shared trail bar, with a summary, state chips that filter, and Open the task or Open the draft where the person may open it; a job opened from a group keeps the trail; Esc goes up; the group and the filter are in the address (
/settings/jobs/groups/{aiTask|modelDraft}/{id}?state=failed). - Migration
20261124_01addsjobs.task_refandjobs.draft_ref(generated fromparameters, with an index on the task) so the grouping reads no JSON per job: on the dev data the grouped read fell from 300 ms to 3 ms.
Changed (the assistant loads a laid-out workbook and asks only what a person decides, 0.13 AQ)
- The session limit is a handled ending. A turn over
AI_AGENT_TIMEOUT_SECONDS(default 1800, in the settings template andOPERATIONS.md) is stopped as a person's Stop is: the session ends, the jobs it started are cancelled, a partial load is undone and the task goes back to its last point with its draft kept. The note says the session took longer than the limit; the job endscancelled, neverJOB_FAILED, and the log has one WARNING line, no traceback. - The draft operations are typed in the OpenAPI.
POST /model-drafts/{id}/operationsdescribesoperationsas a list of one schema per public kind (<Kind>Operation, discriminated bykind), generated from the kinds registry (@kind(name, params=…), the kind's docstring as description). The server's checking is unchanged; a kind added without its parameters fails the contract check. - A whole profile in one operation.
setProfiletakessourcesandvalueMapsasGET /model-drafts/{id}/profileanswers them indefinition, replaces the draft's profile in one undoable step and queues the dry run. A source'sidsent back is ignored. - A column is named by its letter too. A column binding takes
letterin place ofcolumn, so a column whose header cell is empty can be loaded; the import, a draft's dry run and the draft's column operations (mapColumn,setColumnReading,splitColumn,ignoreColumn,addValuesToList,unmapColumn,setValueMap) accept it. The draft also accepts the reader's names for a repeated header (Checklist #2). - The assistant asks only what a person decides (
agent/11): keep-as-is points (two names for one thing, no key column, a repetition kept flat) are one line of the summary, not questions; what the load cannot read is bound by letter; a tested profile is set with onesetProfile. - Answers that keep the defaults cost nothing.
POST /ai/tasks/{id}/answerswith only the assumed options records them and queues no turn; the task stayschecked. - A waiting task says it waits. An assistant task carries
ahead, the assistant jobs before its queued job; the page and the journey bar say "Waiting for the assistant: 2 tasks ahead" instead of "Reading the workbook". - Undoing a value map restores that map only.
setValueMaprecordsvalueMaps/<name>, so undoing an earlier one no longer removes the later ones. - A request that changes only the model no longer fails (
agent/12): when the words ask nothing of the stored records, the session installs one release and makes no change set, and the task endsloadedwith a migration of no type mappings that the person previews and confirms (nothing moves); an empty change set the session made is discarded. Found in task 99 (relation matrix views). - The assistant's questions are no longer cut off (
agent/13): a question reached the page as its first 500 characters, so a long reason hid the ask; the text is now kept whole, and the session is asked to put the question first, in at most three short sentences. - Answering with the assumed options no longer looks broken: the draft is already built on them, so the engine starts no new design round; the Questions card now says so and shows "Answer and design again" only once a choice differs from the assumed one.
- An assistant turn no longer fails on its own scratch files (
agent/14): the session's scratch folder is now inside its task folder, and it is told to keep every file there and to retry a refused command instead of ending the turn, a rule every later turn repeats with the paths; a refusal had ended task 95's load turns with nothing done. - A change set that adds an application can be browsed in it: inside a change set (or a publication) the app reads addresses under that change set's own model, not the workspace's active one, so switching to an application only the change set's release has no longer says it "is not in this workspace's model".
- The migration started from a change set's banner leads back: the migration page opened from the "Move … and publish" banner shows "‹ Back to the change set “…”" while it runs, waits for confirmation, fails or is cancelled, and once done, with the change set published with the move, offers "Open
in "; a plain move of the change set offers "Open the change set to publish it". The way back is in the address ( journey=change-set:…), so a reload or the browser Back keeps it. - One key is one record across the sheets of a load (task 95): two sheets keyed alike for one type (a code list and a person sheet naming the same countries) loaded each key twice, as two half-records; the rows of every sheet with one key are now one record holding all their values and relationships, in the preview and dry run too. Two sheets that fill one field of a key differently refuse both rows, naming each other with their sheet and row (
details.refs), where the later sheet's value used to win.
Changed (a draft's dry run reads its own profile, 0.13 AT)
- A draft's dry run reads the draft's own profile.
profileinPOST /model-drafts/{id}/dry-runis optional: without it the file is read with the profile the draft holds. The profileGET /model-drafts/{id}/profileanswers is accepted unchanged: its read-onlykey,unmapped,ignoredSheetsand each source'sidare ignored, where they were refused with422.
Added (findings of the depth waves, 0.13 DF)
- An earlier contract version of a consumer API is served beside the new one. A definition may keep frozen copies of itself at older contract versions (
earlierVersions, each with the day it was superseded and an optionalsunsetday). Every route and the OpenAPI document of a kept version answer from the current data through that version's definition, withDeprecation(RFC 9745) and, when a day is set,Sunset(RFC 8594) headers; the index lists one entry per version,CONTRACT_VERSION_NOT_FOUNDnames the versions served. A kept version is validated like the current one (a type or attribute the model no longer has refuses the release naming the version) and must read as it did in the active release. A webhook accepted under it keeps receiving that version's shape instead of being suspended, andPOST /consumer-subscriptionstakescontractVersionto subscribe to a kept version; the version stops when the administrator removes it, the sunset day being announced, not enforced. In the model editor, raising a contract version keeps the previous one by default; the consumer API's Settings list the earlier versions with their sunset day and Stop serving. New operation kindsraiseConsumerApiVersion,removeConsumerApiVersionandsetConsumerApiSunset.COMPATIBILITY.mdsays the notice period of a consumer API's earlier version is its administrator's. - The Webhooks dialog creates a subscription to a workspace's own events. Source chooses Consumer API or Workspace events; the latter offers the workspace and its three events (
change-set.submitted,change-set.approved,change-set.sent-back) and no filters or content choice, as the engine accepts them. - Rotate secret is offered for a deactivated client. The dialog says the new secret takes effect when the client is restored; the engine already allowed it.
- The kept earlier version is what was served. Raising a contract version freezes the definition as the draft's base release served it, not the draft's edited one (an API the base release lacks keeps nothing), so activation no longer refuses the kept version for the edits that made the raise necessary. A subscription to a version not served is refused with
details.servedContractVersions. New webhook is offered when no consumer API declares events (a workspace's own events can always be subscribed to), and a tree view needs a top type before Add a view applies.
Added (images built in CI and CI as a quick gate, 0.13 CL)
.github/workflows/images.ymlbuilds the engine and web images once forlinux/amd64andlinux/arm64on native runners, starts them withcompose.yml(.github/images/compose.smoke.yml) and checks/healthand the application, and only then writes the tags inghcr.io/refiroo/refiroo-engineand-web:v<X.Y.Z>for a stable tag (built only from a commit with a green CI run, and only if the tag equals the version inbackend/pyproject.toml),main-<12-character commit>andmainfor a successful CI run onmain, andtest-<label>for a manual run. Pre-release tags are not built..github/workflows/images-cleanup.ymldeletes untagged versions andmain-*/test-*versions older than 30 days each week; stable tags and the movingmaintag stay.- CI on GitHub is a quick gate: the linter, the backend unit tests and the frontend tests and build, on Ubuntu 24.04. The full suite runs on a developer's machine before a release (on GitHub it took a whole runner hour per push and ran out of memory). The statement counts leave out the assistant's worker thread (0.16), and the batch and move budgets gain two statements: a client token's request reads the client's kind and its assistant task (0.16 ag1, ap9). The dry-run seconds budget is a release check (
budget).
Documentation (automatic HTTPS needs port 443 alone, 0.13 CP)
- Port 443 alone is enough for automatic HTTPS (port 80 only redirects
http://tohttps://and may stay closed); the API description covers every operation.
Added (the model editor sets everything a model holds, 0.13 ED)
- A type's Settings tab sets the attribute that names its records, its effective period (a start and an end date attribute), the attributes of which a record needs at least one filled, and the relationships that place a record under a parent.
- An attribute type's Settings set a minimum and a maximum (for whole and decimal numbers), hide it from record screens and give it help text. A type's attribute table has Hidden and Help text columns for the same on that type alone.
- A relation type's Settings say whether it is oriented (deleting a record then removes the relationships it points to). The relations tables have a Carries column: the attributes a relationship holds.
- An application's Settings set its default view (its colour is drawn nowhere, so it has no control). A consumer API's Settings set its contract version, and a match through a related block can prefer types.
- On a record page, Header and breadcrumbs… chooses the title field, the line under it, whether the header names the type, and what the breadcrumbs show. A table's Sort menu and page size… and a panel's Settings… (the relationships a panel lists, their direction, the relationship column, the sort menu, parent and ancestors, a page size) complete the layout editor.
- Add a view… offers a table, a tree, a mapping grid and a relation matrix; a tree's or a mapping grid's Change… edits it afterwards.
- Three deletions have screens. An import profile and its uploaded file on the profile's page, and a mapping in the Copy and move list, each after a question in its own words; people who may not do it are not shown the button.
- Server. New operation kinds
setObjectTypeProperty,setAttributeTypeProperty,setRelationTypePropertyandsetApplicationProperty;setAttributeBindingtakesuiandsetRelationRuletakesattributeTypeKeys. The draft's outline carries these settings. An application'sstateis read by nothing and has no control.
Fixed (command-line tools without a terminal or with --help, 0.13 DOC11 clean-host rehearsal)
- Command-line tools:
database_upgrade --helpandgenerate_key --helpprint help instead of doing the work;create_adminwithout a terminal says to use--password-stdininstead of failing with a traceback.
Fixed (found by the fresh-reader check, 0.13 DOC11)
- A hierarchy view can take its first top-level record. The tree only offered Add from an existing record's menu, so an empty workspace had no way to start. The header and the empty state now offer Add for each of the view's top-level types that may stand without a parent, when the view allows creating and the person may write the type.
- A followed publication's notice opens something. It leads to the workspace's Publications page; an approval's notice still leads nowhere, as its change set is gone.
- A retired workspace says it is read-only. The context band read "Edits apply immediately in this workspace."; it now says "Retired: read only."
- A supplier profile is read and saved in the workspace of its comparison. Accept and Ignore on Settings › Import profiles saved against the default workspace's model, so a profile of another workspace's model failed with "The profile was not saved" while its detail said Valid and its header said Needs a fix. The screen now reads the profile, and
PUT /import-profiles/{key}carriesworkspaceKey, for the workspace of the profile's latest file. The Import dialog lists profiles the same way, valid or not under the release of the workspace the file loads into, so a profile of that workspace's model is no longer shown as no longer valid and can be chosen. - "Update them anyway" updates records changed since the export. An import with
onExisting: "overwrite"still held a row's exported version against the record, refusing it withVERSION_CONFLICTin the preview and the run. A file's version claims (a record's, a relationship's, a relationship list's) are now ignored under overwrite and the previous version is audited; a batch's ownexpectedVersionis still held. - Preview again after a delete takes the records' current versions. A record refused because it changed after the preview was previewed again with the version it was selected with and refused again, offering "Delete 0 records". A refusal for a changed version now names the
currentVersion(also forDELETION_PREVIEW_CHANGED), and Preview again of a bulk action sends it. - A released element's refusal says how to make the change. The model editor said the change "needs a migration (planned for 1.0)", although migrations exist; it now says to make it in the JSON view, where installing the release shows the migration to review.
- A draft conflict names a rename from its old name. "Applied since" read "Renamed Reader refactor Admin to Reader refactor Admin"; it now reads "Renamed Reader refactor to Reader refactor Admin".
Fixed (found by the fresh-reader check of the assistant journey, 0.13 DOC11)
- A person's Stop in a container shows as their stop. Ending the session needed
ps, which the engine's image lacks, and signalled the session'ssudowith a signal it cannot pass on; the Stop became an internal error and the session kept running. The session is now found by/proc, asked to end, and killed only if it does not. - A message that asks for a correlation ID shows it. A task stopped by a failed job names the ID in its warning.
- A failed assistant job reaches its task when it fails. The task no longer waits for a page to read it, and the job's result no longer says "Every item applied" when it failed with no item.
- A correction of a load that fails leaves no change set behind. The empty replacement the session made is discarded with the failed turn, so no change set is left that no task links.
- The checked relationship count is the loaded count. The check of an assistant's items counts each new record's link to its parent, as the load does (164 became 354 in the reader's workbook), and says how many of them are parent links.
- A records-only words task is named for what it is. Its trail and journey bar read "Change records" once its proposal leaves the model as it is, not "Change this model".
- "No change to the model" no longer claims a workbook fits. For a task without a workbook it says so plainly, and for a draft that changes only views or layout it says no type, attribute or relationship changes and opens the draft.
- The model summary and the task note use words. A changed type reads "Changes the attribute that names its records" instead of its key, and the assistant's closing words no longer name its own proposal file or its keys and lists (draftId, moves, removals, movedTo); the instructions ask for plain words and the engine drops the sentences that still do.
- Go on as it was works after a Stop in the first seconds. A session stopped before it wrote its transcript had nothing to resume and the next turn failed with no reason; it now starts again with the person's words, and a turn that ends without an answer says so on the task.
- A partial move is described as what accepting prepares. The assistant's instructions now say a split of some records of a type is prepared on acceptance as a release beside the old types, a change set and a final release, never as manual work for the person. The note for the removed type says its records move to the new types instead of saying they are deleted, and a partial move whose final release the session did not name says so on the task.
- A partial move's steps no longer depend on the session's last line. The engine finds the final release from the releases the task installed (the newest whose types are the change set's release without the old types beside the new ones); the session's
THEN_RELEASE:line is only a hint. - The top "Accept the model" button takes you to Accept. It moves focus to the Accept button on the page; it focused "Open in the model editor", the first button beside it.
- A words task has a neutral name until its proposal says what it changes. It reads "Change with the assistant" before a proposal, then "Change records" or "Change this model".
Fixed (findings of the documentation waves, 0.13 CP, D-*)
- Settings › AI says what the Values level sends. It read "up to N per column"; the code sends every value a feature asks for at Values, and the count only limits Samples.
- Changing a tree keeps what the form does not show. Change… on a tree rebuilt its action list from the availability field and one common child sort, dropping declared actions (Create, Rename, Move) and per-type or descending orders. It now sends only the properties that moved: the availability action is added or removed alone, and a new order keeps each type's direction.
- Oriented says what it does. The help text claimed deleting a record removes the relationships it points to; deletion cascades to the target records the record owns through required one-to-one or one-to-many relationships of an oriented type.
EVENTS_EXPIREDon resume tells the truth about the reset. The message sent the caller to re-sync from the named publication, which may be older than the feed head the reset starts after, so following it literally skipped changes. It now names the head position and tells the receiver to reload from current data taken after the reset. The error code and its details are unchanged.
Fixed (setting name on the Webhooks screen, password in a restore check refusal, 0.13 FX)
- The Webhooks screen names the setting that is read. With webhooks off it said they also need
CONSUMER_WEBHOOK_KEY_ENCRYPTION_KEY, a setting startup refuses sinceSECRETS_ENCRYPTION_KEYreplaced it; it now namesSECRETS_ENCRYPTION_KEY. - A refused
--restoredURL no longer shows its password.python -m app.cli.restore_checkprinted the value as given when it refused it. A URL that is not PostgreSQL is shown with its password hidden; a value that is not a URL at all is not repeated.
Fixed (an assistant task makes its own model draft, 0.13 AD)
- A task whose session opened the workspace's own draft no longer fails at once. The session is told how to get a draft (
agent/10): make its own withPOST /model-drafts, never open the workspace's draft or use one it did not make, and keep its own on later turns. A proposal that still names a draft the task did not create goes back to the session, as a finding does, for up to two rounds; only then the task fails, saying the session must make its own draft, no longer "try again", which could not help. A person's draft is never touched. - A load that only adds relationships is a load. After acceptance the task judged the change set by its records alone, so a session that only linked records ended
failedwith no note. The load's counts now include new, changed and removed relationships (relationsCreated,relationsUpdated,relationsRemoved) and removed records (deleted), and the task page names them. A task that fails after acceptance now always says why: no change set of its own named, another workspace, or nothing in it to load. - A failed load that left a change set behind keeps it reachable. The task page no longer says "Nothing was installed or loaded" then: it says what the assistant loaded and into which change set before it stopped, with Review the change set, the words box and Discard the task and its change set. Trying again or telling the assistant discards that change set first, so there are never two. A failed load is also re-judged when the task is read, so a task the earlier counting bug had failed lands
loadedby itself.
Fixed (jobs, client webhooks, the assistant's note, 0.13 FX)
- Jobs name the workspace by its label in the list and on the job page, not by its key.
- A client's page shows its webhooks only where the deployment manages webhook subscriptions; with them off it no longer shows "Not Found".
- Cancelling a job asks with its own words. The confirmation reads Discard the import (nothing was written) or Stop the job, no longer repeating the button that opened it.
- The assistant's closing note speaks in the person's terms. The session is told to name no files, keys, ids, endpoints or commands (
agent/9), and the engine's closingCHANGE_SET:andTHEN_RELEASE:lines are left out of the note.
Added (dark screenshots for the website, 0.13 DOC10)
guide/tools/screens.mjs --scheme darktakes a screen in dark. It sets the browser's colour scheme and the application's own Appearance setting to dark and writes<name>-dark.pngbeside the light file. Without the flag nothing changes. Eight screens the refiroo website shows are captured in dark; the Stops list's Display panel (list-display-panel) now has a screen spec in_waves/PILOT.screens.json.
Documentation (relation matrix columns, 0.13 FX)
- The guide says how to show only the columns that concern you in a relation matrix. Fill in a relation matrix has a new section, Choose which columns to show, and Relation matrices explains that the choice belongs to the page open: it is kept in the address for bookmarks and links, not saved for the view or for other people.
Added (record pages and related columns in the model editor, 0.13 RL)
- A type's page has one panel per relationship, in each direction. The page a new type gets, Declare its page, a newly allowed relationship and the page shown where an application declares none follow one rule: Details and the hierarchy at the side; in the main region the contained types, then a panel for each relationship the type holds (containment aside), titled as the relationship reads from the type, with the name, the parent type the records sit under, up to four of their attributes and, for several types, their type. A newly allowed relationship also gets a
related:column and filter on both types' tables, and removing it takes them away; pages written by hand are not rewritten. The frontend's copy is checked against the server's on the guide's example model. - Add panel… on a record page, beside New field group: related records of one of the type's relationships (marked where already on the page), contained records, or a field group, with a region and a title. A new draft operation
addPanelbuilds the panel on the server, undone byremoveViewItem. - The Columns dialogs show fields of the related records. In a table's Columns… each relationship unfolds into the fields of its records and where they sit (Has name translation › Language › Language code); a relations panel's Columns… adds where its records sit (
ancestor:<type>) and their type when it lists several. Filters still offer the relationship alone. - The model editor's preview fills a column showing a field of related records. The preview's client had sent only bare relation keys in
expand, so such a column stayed empty until the draft was applied; it now keeps every entry the active model answers. - A relations panel that comes to list several record types drops its ancestor sort, which needs one type: allowing a second type's relation no longer leaves the model invalid (0.13 RL).
Added (a theme choice: Normal or Website purple, 0.13 TH)
- Settings › Appearance has a Theme selector: Normal or Website purple. Website purple is the refiroo website's look (periwinkle, mint, navy, cream, coral) as a second theme of the interface, in light and dark. It is independent of the light and dark choice, which stays as it was and now picks the light or dark of the chosen theme, and of the type colours (A and B apply inside both). The choice is saved in this browser (
rdm:scheme:v1, JSON"normal"or"purple", default Normal) and set asdata-scheme="purple"on the page by the first-paint script, so there is no flash; the browser's address-bar colour follows.tokens.cssgains the website's colours as a primitive ramp and two role blocks (light, and dark twice, identical), each setting every role Normal's dark block sets;tokens.test.tschecks that, that the two dark blocks match and the contrast of every pair in both, with and without palette A. - The guide and the API reference an instance serves follow it too. Their header bar (
refiroo-header.css, now with a copy kept equal underbackend/app/management/brand/), Starlight's variables (guide.css) and Scalar (coloured by the same stylesheet) take the purple values, and all three follow a choice made in any of the three tabs through the storage event, as they follow the light and dark choice. The public site's guide build with a host look (PUBLIC_GUIDE_THEME) and its API page keep the site's look and ignore the choice.
Fixed (the assistant in a container, 0.13 DOC11)
- The assistant's session could not run shell commands when the engine runs in a container. With
AI_AGENT_SANDBOX=containerClaude Code's sandbox is off, and its don't-ask mode then refused every compound or multi-line command. In a container the session now starts without--restrictedand with--permission-mode bypassPermissionsand--setting-sources ""; theassistantuser stays the boundary. Outside a container nothing changes (app.engine.tasks.agent._safeguards). - Open assistant tasks survive a recreated engine container. Compose now keeps the assistant's task folders and Claude Code's session logs in the named volumes
assistantandassistant-claude; before, every upgrade (a new container) removed them and a retried task failed with "No such file or directory". Losing the volumes loses open tasks' folders and session logs, not records (compose.yml,backend/Dockerfile). - Compose sets
AI_AGENT_API_URLtohttp://127.0.0.1:8000. The session runs inside the engine's container, wherePUBLIC_URLmay not be reachable. - The session in a container can use
$REFIROO_TOKENin a command. Claude Code'sblockReadsOutsideWorkingDirectoriesrefused any compound command that expanded a variable, so most sessions ended early on the Authorization header. In a container the setting is no longer passed (theassistantuser is the boundary); on the host it stays. - "The model: what changes" reads the draft, not its steps. A task proposal whose draft the assistant made with a single whole-model step said "No change to the model". The task's design now lists the changes between the draft's model and the model it was drafted on (new applications, types, attributes, relationships, changed and removed ones), whichever steps made the draft.
- The assistant's summary and notes on a task page render as text.
**bold**,`code`and dash lists are shown as such instead of one raw block. - The web image builds again. Two frontend tests read the example model from
guide/samples/model.json, which the image's build did not copy, sodocker compose build webfailed in the type check (frontend/Dockerfile,.dockerignore). - A proposal lists a type as changed only when it changes, and says what changed. The outline of a task proposal called a type "Changed type" whenever its attribute list differed in form (default values spelled out, another order, another order number), and never said what had changed. It now compares what the attributes mean (added, removed, required, read-only, screen hints) and reads "Changed type (adds Step-free access, Remark)" or "(changes how FSO number is filled in)". "The model: what changes" gains a row when an attribute becomes required or optional, and no row when a type's description is null on one side and missing on the other.
- An instruction in words that needs no model change is a proposal, not a failure. A task without a file whose model does not change (for example "set the Abbreviation of this stop") ended "The session ended without a proposal" because the session named no draft. Such a proposal now lands with
draftIdnull anddesign.modelUnchanged: the task shows the summary, "No change to the model", the questions and Accept. Accepting installs and activates nothing; the session makes the change set on the workspace's current live release, unpublished, and the task loads as before. A workbook task still needs a draft, and a draft that is given keeps its checks (drafted by the task, held by no other). The prompt names the case and isagent/8.
Fixed (mapping coverage: several targets on a many-allowed view, 0.13 FX)
- A mapping row with several targets is "Mapped" where the relation lets a source hold many. The coverage state called every row with two or more targets "ambiguous", ignoring cardinality. The engine now reads the binding's cardinality at the source end (
one-to-manyoutgoing,many-to-oneincoming andmany-to-manyhold many): there a row with one or more targets ismapped,state=mappedlists them andstate=ambiguousreturns no rows. Where a source holds at most one target (one-to-one,many-to-oneoutgoing,one-to-manyincoming) the rule is unchanged. The grid hides the Ambiguous filter where it cannot occur and shows the target count on any row with several targets. API, application model and guide pages updated.
Changed (the public guide build, 0.13 DOC12 with website W4)
- The public site build takes its address, base path and version from settings.
PUBLIC_GUIDE_SITE_URLmakes every page name its refiroo.org address as canonical and writes the sitemap;PUBLIC_GUIDE_BASE(default/docs,/docs/0.13for the version before the latest) sets where the build is served, and the static API reference follows it;PUBLIC_GUIDE_VERSIONshows a version mark after Documentation | API reference andPUBLIC_GUIDE_OLDER=1adds "Guide for 0.13 · Latest version" to every page. In the public build the logo leads to the site's home (/) instead of the guide's, and underprefers-reduced-motion: no-preferencepages fly between the site and the guide (mascot,page, andterminalon the API reference). An instance's guide is built and behaves as before. The website'stools/engine.mjs <tag>runs the build (guide/src/styles/public-site.css,guide/src/components/Banner.astro,app.management.reference.static_site). - A host can give the public guide its own header and look (
PUBLIC_GUIDE_THEME, 0.13 DOC12 E5). The setting names an absolute directory: itsHeader.astroreplaces the guide's header, itstheme.cssloads after the guide's styles, and itsapi/folder (head.html,header.html,theme/) gives the static API reference page the same header instead of the application's, with the page's Content-Security-Policy naming the header's inline scripts by hash and the page's theme wiring following a header that togglesdata-themeitself (app.management.reference.static_site,ApiTheme). Unset, an instance's guide and the API page are unchanged. refiroo.org uses it for its own header, colours and fonts on the guide and the API reference.
Changed (documentation: automatic HTTPS needs both Compose files)
- Every page that recreates containers now warns that automatic HTTPS needs
compose.acme.ymlon each command. A highlighted caution on Install (afterCOMPOSE_FILEis set), Upgrades (before the steps, replacing the one sentence after them), Restore on a replacement host, Configure and Set up AI says to keepCOMPOSE_FILE=compose.yml:compose.acme.ymlset or add both-ffiles: a command without the overlay recreates web without Caddy in front and the site stops answering. Troubleshooting has a row for that symptom (port 80/443 already allocated or missing certificate files after an upgrade); README anddocs/OPERATIONS.mdsay the same.
Added (documentation: production deployment and AI)
-
"Deploy to production with Docker" (
/docs/run/install-with-compose/) takes an operator from an empty server to a running instance step by step: the source at a tag, a GitHub App with a short-lived token for a private repository,.envfilled by commands, automatic HTTPS with Let's Encrypt (COMPOSE_FILEset once), the first administrator, backups and upgrades to the next tag. -
The AI features are documented for every reader: asking a list in words, proposed mapping targets, loading and changing with the assistant, changing a model from words, accounts of changes, Settings › AI, agent access, assistant tasks for integrators, and setting up AI with a Claude subscription or an API key. Automatic HTTPS is presented as a feature on About and Run.
-
Every picture the guide references exists. The remaining screenshots were taken on the reference instance (the assistant chain, model moves, a stale migration, history with an assistant's preparation, mapping proposals, a failed provider test, webhooks, sign-in providers, second factors, groups, jobs and the import preview), and every caption was checked against its image. The Run pages describe the assistant's API address under Compose and its two volumes; the words-only pages describe a proposal with no model change. The two example workbooks are downloadable from the pages that use them (
/docs/downloads/). -
The fresh-reader check (DOC11), done rather than read: a fresh session given only the built guide did the quick start, every journey but the assistant one and the production checklist's outside checks, in a browser and with real requests on the reference instance; the run pages' commands were run on a clean host, including an upgrade. What it got stuck on was fixed in the pages, and the product defects it found are listed under Fixed. The assistant journey is read once the assistant work (AT) is delivered.
Added (branded mail)
- The five messages the engine sends are branded HTML as well as plain text. Activation, password reset, test message, inbox notice and digest are multipart: the plain text, unchanged, then an HTML part with the refiroo mascot and wordmark in a lavender band (a renamed product shows its name beside them, as the top bar does), a heading, a button, the raw link for a client that hides the button, and a footer saying why the person gets the message and where to change it (Settings › Notifications), with a link to refiroo.com ("Made with refiroo" before it for a renamed product). The HTML carries the same facts as the text and no more (D421, D432); every value is escaped. The images travel inside the message as
cid:parts frombackend/app/engine/mail/brand/, never loaded from the application's address. The sealed outbox text now holds the text and the HTML together (outbox.pack/unpack), so there is no migration; a message queued earlier is sent as plain text. The test message from the Mail page, sent without the outbox, carries the HTML too.
Changed (the documentation's sidebar)
- The documentation's sidebar starts folded. Only About refiroo is open by default; every other section and its groups start closed. The section and group of the page being read open by themselves, and a reader's own folding is kept as before (
guide/astro.config.mjs). - "refiroo" is marked as the product name in the documentation's text, in the wordmark's navy (light periwinkle in dark mode), by the build itself (
guide/tools/rehype-brand.mjs); code and names such asrefiroo-guide-refare left as they are.
Fixed (CI on GitHub)
- The frontend tests pass under Node 22, the version CI, the images and the README name. Browser storage is cleared after each test (
src/test/setup.ts): under Node 22 jsdom kept a saved preference, an open section or a hidden column, into the next test of a file, while Node 26 leaveslocalStorageundefined, so the leak did not show on the developer instance. The release detail test waits for the activation check's request instead of expecting it with the first text.docs/testing/EXECUTION_GUIDE.mdlists the AG3 browser module. - The backend tests run in parallel in CI,
-n autoand then theserialtests, as the execution guide runs them; a single process took about an hour. The job stops at 40 minutes. A tag push no longer runs CI a second time for the same commit, and the frontend job installs Playwright's Chromium for the gate-helper test (.github/workflows/ci.yml). - One ruff for CI and the developer instance:
ruff==0.16.5in the dev dependencies (0.17 dropped DTZ001 and refused thenoqacomments 0.16 needs); the import order and a nestedifit reported are fixed.
Changed (the starter model only)
model/holds onlyrdm-starter.json. The consumer API document tests run over the starter model, with itsrdmdefinition widened to every type where a test needs the whole model.
Fixed (a released consumer API's key fact)
- A released consumer API no longer says "Key fixed: released". Its key can be changed, as D55 decided, and Change key… stays offered; the fact now reads Released: a new key moves its address, and its tooltip says consumers of the old key get 404 once the draft is active (the contract check warns
CONSUMER_API_REMOVED). An unreleased one reads Key editable. Model elements keep Key fixed: released. The guide's Define a consumer API and Consumer APIs in the model pages say the same.
Changed (one header, 0.13 HD)
- One header for the app, the guide and the API reference. The three bars share the height, surface, rule, mark and ink underline (
frontend/public/brand/refiroo-header.css, served at/brand/); the logo leads to the app's home, Documentation | API reference come before the guide's search, and the API page drops Scalar's own dark-mode toggle. "?" in the app bar, left of the bell, opens the documentation in a new tab. One theme choice: the guide and the API page read and writerdm:theme:v1(System, Light, Dark) and, with the app, follow a change made in another open tab. A public site build setsPUBLIC_GUIDE_SITE=1and sends the logo to the site's home (first the guide's home; changed by the entry above). The public site has the API reference too:npm run build:publicinguide/(withGUIDE_OUTto build elsewhere) builds the guide for the public site and writes a static/docs/api/beside it with the engine's own page (render_page, in its static form: no sign-in script, no test-request button, the Content-Security-Policy in a<meta>element), Scalar's bundled script, the management API's document (from the code, no database) and thestopsconsumer API of the guide's reference domain (guide/samples/model.json), titled as the example. The module isapp.management.reference.static_site; an instance does not use it, since the engine serves/docs/apifrom its route before the guide's files.
Changed (image layout)
- The images keep everything under
/opt/refiroo/rdm-engine, laid out as the repository is (backend/,guide/dist,model/,data/, and in the web imagefrontend/dist,frontend/certs,frontend/nginx), and add nothing at/. An existing installation mounts the job-files volume at/opt/refiroo/rdm-engine/data/job-filesand the certificate folder at/opt/refiroo/rdm-engine/frontend/certs;compose.ymldoes both.AI_AGENT_FOLDERSdefaults to/opt/refiroo/rdm-engine/data/assistant.
Added (the guide's foundation, 0.13 DOC1)
- The guide at
/guide. A static site built with Starlight from the newguide/folder and served by the engine offline, from the version it runs; the web image's proxy forwards/guide, and/docsand Settings › About link to it. A checkout that has not built it starts as usual and/guidesays how to build it. The engine image builds the guide in its own stage and carries only the built files. - Page kinds, templates and checks. Every page is a tutorial, how-to, explanation or reference with front matter for its reader, covered items, required statements and examples; the build refuses a stub.
npm run check(front matter, stubs, template headings, banned terms, the spelling "refiroo", examples) andnpm run check:linksrun fromguide/;guide/AGENTS.mdholds the writing rules and the process for writing a page from its brief. - The
docstest group.pytest -m docsruns the guide's examples against the engine seeded with the starter model (IDs and times normalised; model examples through the engine's validation); a page prints the same file it was tested from.python -m tests.affectedselects it when a file inguide/examples/changes. - First page: Three kinds of change.
node e2e/accessibility-gate.mjs --only guideruns the accessibility checks on the guide through the running engine, light and dark, at 1440px and 390px, and fails if the page asks for anything outside the engine. - AGENTS.md: a change to user-visible behavior updates the guide pages that cover it, in the same change.
Added (the guide's page inventory and the briefs of waves A and B, 0.13 DOC3 part 2)
- Every page of the guide has an owner. 143 stub briefs in
guide/_briefs/(title, kind, reader, wave, the inventory items and required statements each owns);npm run check:coverageshows no unowned item. The generated reference pages own the rest of their kind (kind:*); 0.16's items are listed inguide/_coverage/wave-g-items.txtfor wave G. - Full briefs and tested examples for waves A and B (how refiroo works, run refiroo, administer access and security): 53 briefs, 72 new examples run by the
docsgroup, andnpm run check:briefs. Prompt filesguide/_waves/DOC4.mdandDOC5.md. - A stale coverage inventory fails the
docsgroup, andGUIDE_RECORD=1records an example's expected responses from what the engine answered; signed cursors are shown as<cursor>.
Added (the guide's reference domain and coverage inventory, 0.13 DOC3 part 1)
- The guide's running example is in
guide/samples/. The public transport stops of Graubünden from open data (opentransportdata.swiss and Wikidata, attribution inSOURCES.md) with one fictional ticketing system and one fictional workbook: the model, two dated data sets (docs, 598 records; reference, 3,528) and the scripts that fetch and load them.python3 guide/samples/load.py --set docs|reference --base <instance> --env <env file>loads either set through the public API. - A reference instance for the guide (
guide/reference/reference.sh up): the engine image on Compose projectrefiroo-guide-ref, ports 8480 and 8443, with the reference set loaded and generated secrets in a git-ignored.env. - The
docsgroup seeds the docs set instead of the starter model; the proof page's examples run on the reference domain. - The coverage inventory.
npm run inventorylists what the guide must account for (capabilities, journeys, settings, error codes, commands, roles, permissions, event types, model-language fields);npm run check:coveragecompares it with the briefs and pages, and-- --strictis the release gate.
Let us run it for you.
Backups, upgrades, TLS and monitoring are ours. Your data stays exportable any time.