Changelog

0.11.0, 2026-10-22

Ready for heavier use: several servers, large databases, backups you can rehearse, and data that moves between workspaces with different models.

Releases are numbered by plan, not by date, so a higher number can ship before a lower one. All releases

Full changelog

Released 2026-10-22. 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.

Base 0.11 is released: each workspace runs a model of its own and moves to another release on its own (Track M, Track V), data moves between workspaces whose models differ (Track X), and the engine is hardened for production (several processes and nodes, PostgreSQL at volume, operability, backup rehearsal, touch drag), accepted by R: a full review of every change since 0.10.0 with an independent reviewer (12 findings, 10 fixed, R-F7 and R-F11 kept by decision), the full backend suite, the 56 browser runners, J36–J38 by keyboard alone on two models, the accessibility and visual gates with Settings › Operations, an upgrade from 0.10.0 and the budgets. No schema language is released; the window stays 0.5–0.10. Licensing, the contribution and security policies and the compatibility commitment moved to 0.12, the first public release.

Nine migrations since 0.10.0 (20261018_01 … 20261026_01; eight since 0.11.0-alpha.1); a database of 0.10.0 or 0.11.0-alpha.1 upgrades on start.

Changed (0.11, the first public release moved to 0.12)

  • Licensing, contribution and security policies (L1) and the compatibility commitment with the supported schema versions and their dates (L2) moved to 0.12, which becomes the first public release; the project's name is not chosen yet. 0.11.0 is released as an ordinary release.

Fixed (found in 0.11 R's review)

  • A workspace migration's type mapping naming an attribute's source was ignored when the target type also had an attribute of the source's name listed first: that attribute kept the values and the named one lost its own (R-F1). Two attributes trading values (a from b, b from a) failed the preview as an internal error (R-F2).
  • A released type moved to another application in the editor (setContainment with application) lost every record of it in the migration its activation needs, unless an API client mapped the type onto itself, which the migration page cannot: its records now go to the one application listing it (R-F3).
  • The relation matrix's and the relationship form's save did not send X-Expected-Model-Release-Id (R-F4).
  • A migration, or a change set's move, confirmed or queued before its workspace was retired still ran: it is now refused 409 WORKSPACE_RETIRED at confirmation and as it runs (R-F5). Activation holds the workspace row for key share, so a retirement during its check is seen (R-F12).
  • A workspace migration held the unique values' table lock, which every value write of every workspace waits for, from its first step to its commit (66–111 s at 100,000 records): it now takes it just before the switch (R-F6).
  • A change set's move did not hold its workspace's assignment, so an activation committing meanwhile left the change set pinned to the release the workspace had just left (R-F8).
  • A move's load committed its first step (the target's change set opened) without checking that the job was still its worker's; a worker frozen past its lease could leave a second change set (R-F9).
  • The FastAPI lower bound is 0.141, the version the engine is tested on: the read guard uses Depends(scope=...), which older versions lack (R-F10).

Fixed (found in 0.11 R's browser runners)

  • At 390px a notice with actions (a change set on another release offering to move it or its workspace) kept its actions beside its text: the text was a word wide and the page scrolled sideways by up to 339 px. A notice's actions now go below its text when they do not fit, and a long action wraps its label (wp13c, sc15).
  • A list drawn by row number as tall as its rows, such as the type change dialog's records, could stay empty: with no row drawn it had no height, and with no height the virtual range drew no row. It now draws the rows it holds whenever that range comes out empty (c5).
  • After applying a restructuring, focus was meant to move to Done a frame later; on a page busy redrawing what the apply changed, Done was not there yet and focus was lost. It moves once Done is drawn (c5).

Changed (0.11 R)

  • A migration preview holds the live rows it converts while it runs; a write to one of them waits for it, which ARCHITECTURE and API.md now say (R-F7, decided by the user). A preview over private copies is in TODO.

Added (0.11 U1, touch drag in the record tree)

  • On a touch screen a record in a tree view or the sidebar is dragged by holding it still for half a second (D210): moving first scrolls, a short tap opens the record. Once lifted, the finger drags instead of scrolling, the list scrolls at its top and bottom edge (56 px for a finger, D211), and the preview sits above the finger, inside the screen (D212). Let go where it was lifted, nothing opens; in the sidebar the node menu (Move to…) opens, also on iPhones, which had no way to open it (D213). Never in a publication or a read-only tree (D214).
  • test:browser:touch-u1: Chromium with Pixel 7 emulation and real touch input, WebKit with iPhone 15 emulation (D215).

Fixed (found in 0.11 U1)

  • The drag's edge scroll at desktop widths started its top band under the sticky top bar, so a drag upwards could not scroll the page; the band now starts below the bar (scroll-padding-top).
  • OperationsPage.tsx failed lint (preserve-manual-memoization).

Changed (0.11 Q3, attribute sorts over very large parents)

  • A change set's list sorted by an attribute reads in value order, as live and publication lists do: the base's records it keeps and its own, each through its value index, merged by their first rows (pagination.merged, D205). At full volume the 20,000-child parent's first page takes 14.5 ms (167.9 before).
  • A total under an attribute sort counts the records without reading their values, in every context (live 94 → 11 ms at full volume).
  • Records sorted by an attribute that share a value come by record ID live too, as in change sets and publications (D206). A live cursor or anchor of such a list issued before this release is refused once as INVALID_CURSOR, and the browser restarts the list.
  • Migration 20261026_01: ix_snapshot_attribute_values_change_set_key_boolean, so a change set's sort by a boolean reads in value order (D207).
  • Not changed: a position deep under an attribute sort still grows with the records listed (124 ms in a change set at row 15,000 of 20,000, over the budget; stored sort keys in TODO); ancestor sorts, measured within the budget (D208).

Fixed (found in 0.11 Q3)

  • test_build07_wp11 expected the 12-character correlation ID O1 replaced with the request's X-Request-Id.

Added (0.11 O2, backup rehearsal and the Operations page)

  • python -m app.cli.restore_check --restored <URL> [--backup <name>] checks a backup restored into a separate database: it prepares the copy as startup would, loads each workspace's release on its own, counts each workspace's records, relationships, open change sets and publications beside the live records, and records the result, passed or failed with its code, in the instance (table backup_rehearsals, migration 20261025_01; J12.16, J12.17, D198–D200). A copy that is the instance's own database is refused before anything changes.
  • GET /api/v1/system/backup-rehearsals, paged newest first, and GET /api/v1/system/summary, the instance figures of /metrics as JSON (D201).
  • Settings › Administration › Operations: the instance's figures, each workspace served or not and by which process, every process with its threads, connections and requests, the last backup rehearsal with each workspace of the copy, the earlier ones, how to rehearse and the probe addresses; refreshed every 15 seconds while visible (U7, D202, D204).
  • scripts/postgres.sh rehearse [<dump>] restores a development dump into a scratch database, checks it and drops it; scripts/postgres.sh backup leaves out the processes' rows (D203).
  • OPERATIONS § Backup and restore: taking a backup, rehearsing it, recovering, and what a restore is; the rehearsal on the development database recorded.

Changed (0.11 O2)

  • A startup refusal carries a code (DATABASE_COLLATION, DATABASE_EXTENSIONS, DATABASE_REVISION_UNKNOWN, DATABASE_MIGRATION_FAILED, DATABASE_UNREACHABLE), which a failed rehearsal records; the messages are unchanged.

Added (0.11 O1, metrics, logs, health and readiness)

  • GET /health/live, the liveness probe: 200 whenever the process answers, never asking the database, so a database outage restarts no process. /health stays the readiness probe (D191).
  • Each process reports itself through the database every PROCESS_HEARTBEAT_SECONDS (15; table engine_processes, migration 20261024_01). GET /api/v1/system/readiness names each active workspace served or unavailable, with the live processes that cannot serve its release and why (unsupportedSchema, invalid, notLoaded); GET /api/v1/system/processes lists every process live or silent, ready or not, with its threads, pool and counters (J12.13, D192).
  • GET /metrics in the Prometheus text format, the whole instance in one scrape: requests by route template and status, durations, connections in use and missing by reason, failing threads, readiness, each labelled by process; jobs, webhook subscriptions, processes and unavailable workspaces counted when scraped (J12.14, D193, D194).
  • LOG_FORMAT (text, or json lines) and LOG_LEVEL; one access line per request from the engine instead of uvicorn's; every request carries X-Request-Id, the client's when well formed, which INTERNAL_ERROR's correlationId now is (J12.15, D195, D196).

Fixed (0.11 O1)

  • Processes starting together could collide building the relation matrix states (a unique violation logged at ERROR; the API served and derived the cells from records). The build now runs under the startup lock, after the bootstrap install. The JSON and text log formats showed it: the previous log printed the line without its level.
  • The engine's INFO lines (startup builds, recoveries) were not written under uvicorn: nothing configured the engine's loggers, so only warnings and errors reached the console.

Changed (0.11 Q2, value indexes)

  • A change set's unique checks and key references on a number, decimal, date or instant seek the value instead of reading every value of the attribute in the change set and its base: four partial indexes on snapshot_attribute_values (migration 20261023_01). Live checks already sought their value. The indexes are the same for every model; what follows a workspace's model is which values are looked up (D186).
  • A move's match by a unique attribute seeks the target workspace's unique values by digest; it read every value of the attribute in every workspace (J38.5).

Fixed (0.11 Q2)

  • The mapping inference counted only unique text values as values the two workspaces share, and proposed the match on the first attribute unique on both sides. Unique values of every type count now, and the match goes to the unique attribute whose values the records share (J38.1).

Added (0.11 N3, several processes)

  • DATABASE_IDLE_TRANSACTION_TIMEOUT (300 s, 0 off), reported by GET /system: the database ends a transaction a stalled process (frozen, starved of CPU, cut off) left idle that long, so the writes waiting for its locks go on. A frozen process held its workspace's writes until it resumed (J24.3). The startup lock's connection does not carry it.
  • 503 DATABASE_UNAVAILABLE takes details.reason transactionEnded when the database's notice reaches the resumed process.
  • The concurrency and uniqueness invariants are tested across two server processes on one database: a unique value, a record's version, a parent cycle, a cardinality limit, feed positions, and a background job's item on one node against a live write on the other.
  • tests/test_boundaries.py finds SQL text that writes record rows outside the write unit; the three modules that do are listed with the lock they hold. No database constraint was needed.

Fixed (0.11 N3)

  • A job step whose database connection was lost or ended failed the job with an internal error and a traceback. It is rolled back and left to its lease, and the worker reports the outage in one line.

Added (0.11 Q1, database connections)

  • Each process's database connections can be set: DATABASE_POOL_SIZE (5), DATABASE_POOL_MAX_OVERFLOW (10) and DATABASE_POOL_TIMEOUT (30 s), reported by GET /system; OPERATIONS gives the arithmetic per process and per instance. A start whose processes could need more connections than the server allows logs a warning naming both figures (J12.11).
  • A request that gets no database connection answers 503 DATABASE_UNAVAILABLE with Retry-After and details.reason poolExhausted, tooManyConnections or unreachable, logged in one line naming the cause and the settings. It answered 500 INTERNAL_ERROR with no cause in the log (J12.8, J12.11). The job worker, the event deriver and the webhook dispatcher report an exhausted pool or a full server in one plain line, as an unreachable database, not as a failure with a traceback.

Fixed (0.11 Q1)

  • A process with a pool of one connection could not start: the startup lock held one pooled connection while the bootstrap install waited for another. The lock's connection is outside the pool.
  • Five NOT IN subqueries, which PostgreSQL scans once per row once they outgrow work_mem, are NOT EXISTS: the people export's walk, the mapping list's exclusive targets, the move binding's related records, a query's negated relationship criterion and the relation matrix's state filter in a change set. Not measured at full volume.
  • The move binding's and the people export's working tables are analyzed once filled; PostgreSQL never analyzes a temporary table by itself, so their joins were planned on guesses. Not measured at full volume.

Fixed (the model editor's moves)

  • A type could not be moved to another application: Move to… and a drop on an application only set its parent, so the type stayed where it was, and a parent in the other application left the draft invalid. setContainment takes application (and fromApplication for a type in several): the type and every type under it move there with their views and detail pages, their containment and relations kept, in one step. A drop on another application, or its top level chosen in Move to…, sends it (J33.15). Nothing that can stand in the other application is left out: a list moves with the type it lists even when a column reaches another application's type, mapping grids and matrices that need a moving type follow it, a page the other application has already takes the panels it lacks, and what stays keeps its columns. First delivered without these, it dropped a list whose language column reached the translations, columns of a staying panel, and mapping grids; a sweep of every type to every application over three real models found them. What can stand in neither application (a matrix whose types no application lists together, a staying page's children panel of a type that moved) goes, and is named: a history entry's new removed lists it, and the editor says it after the step and in History. History names the application a type moved to.
  • A tree view's top type given a parent left the draft invalid ("cannot exist without a parent"); a type moved to the top dropped out of the tree it was a node of. The tree now grows up to the new parent, or takes the type as a top of its own, and drops what it can no longer show or offer.
  • Merging a type into its parent made the survivor contain itself, and left consumer APIs naming the merged-away type. The containment between the two is dropped, and the consumer APIs name the survivor.
  • Add XREF rule… listed a field several exposed types share under the first of them alone, so the others (a model's means of transport) seemed to have no field to match on. A rule weighs a field on every exposed type alike, so the list now shows each field once, under "Attributes of the exposed types", as the field sources do.
  • A list inside a dialog (the relationship dialog's record list) carried the wheel at its first or last row on to the page behind the dialog, so the table there moved. A list carries it no further than its dialog, and the dialog holds the browser's own carry too.
  • The model pane did not scroll while a drag neared its top or bottom, so rows out of view could not be reached; it scrolls as the record tree does, through one shared helper.
  • The t1 designer runner followed 0.11: a model of its own installs into a new workspace, read there by the consumer tier; a wait for an undone attribute no longer trips on two matching cells.
  • The Install and activate dialog's buttons fell below its frame when the differences ran long; they stay at its foot as Map to…'s do.

Changed (0.11 N2, the job worker on several nodes)

  • A running job keeps its lease by a heartbeat, renewed every third of JOB_LEASE_SECONDS whatever its step. Before, a step longer than the lease (a move's binding, which renewed nothing; a chunk; one long statement of a migration) was taken over by another process and rolled back, and with two processes they could take it from each other indefinitely. A job now moves to another worker only when its process stops renewing (stopped, crashed, frozen, cut off from the database), no longer when its worker waits on a lock (J24.3 revised).
  • A job queued through a process whose worker is busy starts at once on an idle process, which hears it through LISTEN; before, it waited for that process's next poll, up to 60 s.

Fixed (0.11 N2)

  • Two dry runs, inferences, mapping inferences or profile checks of one subject sent at the same moment both ran, neither stopping the other, and an older inference finishing last replaced the newer one's proposals. They are now decided one after the other; the later one stands.
  • A profile, inference, dry run or check asked to stop after its last check still completed and wrote its result; it now ends cancelled, writing nothing.
  • Two files uploaded to one import profile and profiled on two processes could leave the profile's check on the older file, when that file finished last. A file replaced meanwhile is no longer checked.
  • A cancel sent while a move settled its rows, or deleted in its source, waited for that step; it now answers at once, and a cancel during the source's deletion stops it there.
  • An export taken over by another process after its first stalled could be served corrupted or removed: both wrote one partial file, and the first, finding its lease lost, removed the job's files, the new owner's included. Each worker now writes and removes only a partial file of its own lease, and renames it into place under the job's lock.

Added (0.11 N1, several processes)

  • Model administration works with several processes on one database: Uvicorn workers (WEB_CONCURRENCY above 1) or instances on several nodes. Startup no longer refuses MODEL_ADMIN_MUTATIONS_ENABLED with more than one worker. A release activated or a workspace migrated through any process is served by every other from its next request, with no restart.
  • Processes starting together on one database check, migrate and install the bootstrap model one at a time under a database lock; before, the second could fail its migration and stay in maintenance mode. Release installations take a database lock in place of the in-process one, so two processes installing the same model at once answer 201 and 200 with one release instead of a 500.
  • A read whose workspace was migrated, or whose change set was moved, to another release while it ran answers 409 MODEL_RELEASE_CHANGED instead of one release's model over the other's data, in one process or several; the shell's notice offers Reload. The in-process guard it replaces covered only the moment a request resolved its release.

Fixed (0.11 N1)

  • A write whose commit check waited for an activation of its workspace could commit under the release it no longer ran, and an activation waiting for another one answered 503 ("runs no installed release") instead of 409 ACTIVATION_CONFLICT. Both locked the workspace's assignment joined to its release; after the wait, PostgreSQL rechecked the join against the release read before it, and the switched assignment dropped out. They now lock the assignment alone.
  • The Settings navigation test still listed the pages before "Copy and move" (X4).

Fixed (0.11 X6, list reads)

  • A table with relation columns opened in seconds: a column showing a value of each related record's ancestor built every related record whole (all its values, its path) and kept one value, 250 ms of a 294 ms read for 200 rows, and the table read 200 rows at once. An expansion that names its fields (every relation column does) now reads only those fields, with the ancestor's value found by a walk up the parent chain, in the one statement it already ran; the answer is the same in live data, a change set and a publication. A 20-row page with relation columns now loads 3 to 4 database rows per listed row instead of 6.5 to 8.5.
  • Identical reads made while the first is still in flight share it: the development build's double mount no longer sends every read twice, and a page load reads capabilities, the workspace and the rail's lists once each. Nothing is cached.

Changed (0.11 X6, long lists)

  • Every long list (collections, record panels and relationships, the model editor's tables, job, migration, move and profile reports, the relation matrix, the mapping grid, change set review, the restructuring and tree-move dialogs, the matrix cell dialog and column filter, the record lookup) is drawn by row number: the scroll space spans the whole list, sized from its total, and only the rows on screen and a screen either side are in the page. A row whose page has not arrived shows as a loading row at its place, and pages arriving or rows forgotten move no other row. The browser's own scrollbar is the list's; the hand-drawn one is gone. Every read is the reader's page (20 by default); a scrollbar drag, a fling or the page field reads once where the list comes to rest. An edit in place keeps the list where it was. The record tree keeps its groups as they are.
  • A list reads on in the background, a page at a time, until it holds about as much as it keeps (600 rows): a list of a few hundred records is held whole a moment after it opens, so scrolling never meets a row not read (reported 2026-10-19: grey rows while scrolling a few hundred records).
  • Scrolling a table with relation columns no longer stutters. A relation cell works out how many chips fit from their text and its column's width instead of drawing every chip, measuring it and drawing again; the edit and remove buttons are drawn in the row the pointer is over or that holds focus, the other rows keeping their room (on a touch screen every row still has them); a row already drawn is not drawn again while the list scrolls; the visible rows are found from the scroll position, not read from the page. In the production build the reported view scrolls at a steady 60 frames a second.
  • Fixed: a row could move under the reader while scrolling (row heights rounded to whole pixels; the first drawn row losing half its border), and Ctrl+End followed at once by Ctrl+Home landed at the end.
  • Known limits: the browser's find in page finds only the drawn rows (the list's search finds every record); a screen reader browsing a table without moving focus meets a row's edit buttons only in the row that has focus.

Fixed (tables)

  • Lists drawn by the shared table (collections, record panels, the model editor's preview and the rest) scroll without jumping: the columns keep the widths the first rows laid out instead of moving as rows come and go; a page read while scrolling is drawn in the background, row by row; writing the list's place to the address no longer draws the page again. (The 200-row reads and the browser's scroll anchoring of this change were replaced in 0.11 X6 by lists drawn by row number.)
  • The wheel is no longer held in a table: it first brings the whole table, with its controls, into view, and at the table's first or last row the page scrolls on. The table's height leaves room for the top bar, the data band and its controls. Carrying one gesture's momentum on from the table to the page is in place but not yet smooth (reported 2026-10-19).
  • The model editor's issues and history start closed; Show issues opens them without keeping them open next time.

Added (hierarchy browser)

  • Expanding a record or type heading in a hierarchy (the rail and the tree lens) opens its child when it holds only one that can expand: a record's only heading, a heading's only record. Only that one level opens, so the reader sets the depth; with several children all stay collapsed. It happens at every expansion, also after the child was collapsed by hand, and the tree's first display opens a sole top-level heading. A reveal (the open record's path) or a drag does not.

Fixed (navigation rail)

  • The rail no longer scrolls by itself. A record group finishing its load (its first page, the active record's branches, a reload after a write) scrolled the rail to that group, so the rail jumped as lists arrived; now only keyboard movement in the tree scrolls it, and only the rail, never the page. The tree lens loads the same way, and shows a record it reveals on request.
  • The rail keeps its place for one application in one workspace, across pages, records and views, and into and out of a change set or publication, which mount the frame afresh. Another application or workspace opens the rail at its top, instead of at the offset the previous one had.
  • A record chosen in the rail leaves focus in the rail, as a collection or group already did; the record page no longer takes it.
  • Rows dropped or added above the view (a long group paging on, Show earlier records) no longer shift it in a browser without scroll anchoring; scrolling past the rail's end no longer scrolls the page.

Fixed (0.11 X5)

  • Publishing a change set into a workspace whose records are newer than the database's statistics (a moved workspace, an import into an empty one) took minutes: freezing the publication's relationships joined both ends to the records and PostgreSQL paired every record with every other, and freezing its records recomputed the workspace's values for each record. A publish of 11,000 moved records took 9 minutes and now takes 19 s; 110,000 records publish in 3.4 minutes. The same copy opens a change set over current live data, so a small move just after a large publication no longer waits two minutes (the 13-second wait seen in X3).
  • A move's dry run built each record's item by reading every relationship once per record: 63 s of an 11,000-record dry run, now under 6 s.
  • A move, an import or a publication of more than 65,535 records failed with a database error: the unique-value check after the write, the write unit's removals and the publication's merge listed every record as a parameter of its own. Each is now one array parameter or a join.
  • Moving a whole subtree kept its top record in the source, "with dependants", when those dependants were the records the same move deleted: the source's deletions now run in passes, so a parent is deleted once its moved children are.
  • A move's deletions refused before the copy is published name the copy's change set and workspace instead of its ID.
  • The copy and move editor answers faster at everyday sizes: a step's figures in 0.3 s at 200 records and 1.4 s at 2,000, the editor open in 1.1 s and 2.0 s. A dry run of records just written read the selection again for every record (4–7 s at 2,000 records, now under 1 s on the server); a repeated dry run in a larger workspace compared its own rows with each other (40 s, now 1.7 s); the relationships whose records do not move were looked up one by one; and the page redrew every mapping row for new figures.
  • The mapping grid loads again: since the tables began reading 200 rows at a time it asked the coverage route for more than its 100 and showed "Could not load this content".
  • The copy and move editor's default buttons say which field they change ("Set a default for Steward…"), so they are not two buttons both named "Change…".

Added (0.11 X5)

  • test:browser:workspace-x5, the move checkpoint: records copied and moved between workspaces of partly compatible models through a proposed and hand-completed mapping, the report, the reviewed change sets published, a second copy updating the same records, records moved back to their originals, and the editor timed at 200 and 2,000 records.
  • The copy and move surfaces are in the accessibility and visual gates.
  • volume_baseline.py --only-move: a whole volume workspace moved into another model, each step timed once, with every statement over half a second named.

Added (0.11 X4)

  • The move editor in the browser. Settings › Data operations › Copy and move lists the mappings and the recent moves; New copy or move… chooses two workspaces and a saved mapping or a new one, proposed by the engine and applied before the editor opens. A workspace's page in Settings › Workspaces has Copy or move data…, the same dialog with that workspace as the source.
  • The editor (/settings/moves/{mapping}?from=&to=): both workspaces and models, what moves (every mapped type, the whole workspace, chosen types with filters, or one record and its subtree), Copy or Move, the dry run's figures with their change since the last step, the source's types, the mapping by type with every row a step marked Proposed while it is as proposed, undo and redo, and why a row was proposed with its evidence and alternatives. Each step runs the dry run of every item again and cancels the one before; a mapping with problems lists them, linked to their rows.
  • What cannot move: every item by reason, linked to the mapping row that changes it; Start the move… opens the change set (and, for Move, the source's deletions) under a name given, and a mapping or release changed since the dry run asks to run it again. At 390px the editor is tabs.
  • ui/ProposalNote and ui/OutcomeCards, shared with the workbook editor and the migration page; the model editor's undo and redo rules read a mapping's history too.

Changed (0.11 X4)

  • The move editor says what is happening in one status line under its title: the dry run in progress and why Start waits, the dry run ready, the copy or move loading with its progress, and done with the change set to review. A copy is called a copy throughout; the settings page is Copy and move.
  • The change set bar shows "Change set validation passed" beside its buttons instead of in a notice below, so the buttons stay where they are.
  • The workspace selector lists active workspaces only; a retired workspace is opened from Settings › Workspaces.

Fixed (0.11 X4)

  • A move's relationship row names its two records (fromName, toName on GET /api/v1/jobs/{jobId}/rows), and the report shows each as "‹from› → ‹relationship› → ‹to›"; rows of one record looked alike, naming it by ID. A refusal by a relationship end's criteria reads "Record not allowed at a relationship's end", saying what it means.
  • The jobs list names move and mapping-proposal jobs, which it showed without a kind.

Added (0.11 X3)

  • The move between workspaces whose models differ. A move's dry run (POST /api/v1/moves) now waits for confirmation, and POST /api/v1/jobs/{jobId}/confirm loads what it previewed into a new change set of the target workspace, opened for review over current live data; with stage: execute the move loads at once. Nothing reaches the target's feed or consumers until that change set is published.
  • Where each record a move created came from (its source workspace and record) is kept: a second move updates the records found again, and a record moved back updates its original (updateOriginal, foundBy: origin) or, when a move deleted the original, is created again under the original's ID. A created record's row names its origin, and a record's history names it on every entry a move caused (cause.origin).
  • mode: move: the copy, plus a change set of the source deleting each selected record that loaded with its own relationships; a record changed in the source since the dry run (SOURCE_CHANGED) or one that a record staying depends on (OBJECT_HAS_DEPENDANTS) is kept, each row saying what became of it in the source. The summary names both change sets, the source's feed position at the load and the deleted and kept counts.
  • 409 MOVE_COPY_NOT_PUBLISHED: a move's deletion change set publishes only after its copy; 422 MOVE_NOTHING_TO_MOVE: a confirmation with nothing to create or update. A confirmation is also refused, leaving the dry run waiting, when the mapping changed (MOVE_MAPPING_CHANGED), either workspace runs another release (MODEL_RELEASE_CHANGED) or one is retired.
  • Migration 20261022_01: the table record_origins and a change set's waits_for_change_set_id.

Changed (0.11 X3)

  • A move's dry run ends in awaiting_confirmation instead of completed, and expires unconfirmed after the job retention, as an import's preview. Its rows can be filtered by the outcome updateOriginal.

Fixed (0.11 S1)

  • The job worker runs one chunk at a time whichever thread drives it: its claims accept its own leases, so a second thread of the same worker (a test calling run_until_idle while the service's thread ran) could run one chunk twice and fail the job.
  • Three tests left behind by per-workspace activation (0.11 M1) assert it again: J35 and the J27 export activate into the workspace they use, and the unique-value rows are checked per workspace.

Changed (0.11 S1)

  • Backend test runs: in a parallel run each journey test goes to a worker on its own, while any other test module stays on one worker, so a large journey module spreads over the workers (J34 with J35, 1 min 10 s; J34 alone took 5 min 3 s). The journey model file is written whole before any worker reads it.
  • The full backend suite runs with six workers (pytest -n 6) under a ceiling of 11 minutes, revised from 5 (D126): 10 min 15 s for 5,950 cases with the machine usable, where ten workers took 8 min 49 s at a load near 145. A package's checks have a ceiling of 3 minutes, revised from 2 (D127). The development and test database folders are ignored by git also as links to .nosync folders.

Added (0.11 X2)

  • The dry run of a move between workspaces whose models differ (POST /api/v1/moves), a background job of kind move: what each selected record and the relationships it holds become in the target workspace through a model mapping at the version the client read, read from the source's live data or one of its publications. The selection is the whole workspace, types with an export's criteria, a subtree, records by ID, or several of these together. Every row is listed at GET /jobs/{jobId}/rows (filtered by outcome, reason and sourceType): create under the record's counterpart ID, update of a record found by its counterpart or by the mapping's match, refused with the target's own refusal, or notMoved with the mapping's reason and the step that changes it; with what a moving record leaves behind (attributes not mapped, values that do not convert, read-only attributes, effective periods the target type lacks) and possible duplicates. The job's move summary counts rows by outcome, type, relation type and reason. Nothing is written to either workspace.
  • 422 MOVE_MAPPING_HAS_PROBLEMS: a move whose mapping names what the two workspaces' releases lack or do not allow is refused with the problems.
  • Migration 20261021_01: the pgcrypto extension, the function rdm_counterpart (a counterpart ID derived in PostgreSQL, the same as the Python one) and the job kind move.

Changed (0.11 X2)

  • RDM Engine requires the pgcrypto extension beside pg_trgm and btree_gin; startup creates it or refuses with the instruction to install it, as for the other two.

Added (0.11 X1)

  • Model mappings (/api/v1/model-mappings): what each type, attribute, relationship and parent of one model becomes in another, with value maps, defaults and how existing target records are matched, saved between two workspaces' releases. A mapping changes only through operations (mapType, mapAttribute, mapRelation, setParent, setMatch, setValueMap, setMissing, setDefault, setReleases and their removals), with history, undo and redo; every read reports its problems (what a release lacks or no longer allows) and coverage (what it leaves out), against its releases or the releases two named workspaces run.
  • The mapping's inference, a background job of kind mappingInference: proposals per type and per relationship from keys, names, attributes, parents and the records the two workspaces share, each with its evidence and alternatives, applied as steps marked as proposed. Two workspaces on one model are proposed as identity.
  • Migration 20261020_01: the tables model_mappings, model_mapping_operations and model_mapping_proposals, the job kind mappingInference and an index of a mapping's jobs.

Changed (0.11 X1)

  • A model draft and a model mapping share one history of steps, undo and redo; a draft's answers are unchanged.

Added (0.11 V4)

  • The schema-language window, 0.5–0.10, and the conversion rules between its versions written in one place (APPLICATION_MODEL, "Schema versions and the window"): reading, writing, running side by side, moving data between releases of different versions (by declarations only, never by version) and what happens outside the window. The window is declared once (SCHEMA_VERSIONS), held by a unit test.

Changed (0.11 V4)

  • Installing a draft of a schema outside the window answers 422 UNSUPPORTED_SCHEMA_VERSION with draftId, schemaVersion, supportedSchemaVersions and operation: "install", as activation does, instead of 422 INVALID_MODEL.

Fixed (0.11 V4)

  • RDM_MODEL_PATH naming a model of a schema outside the window stopped startup with a traceback; it now refuses with a message naming the setting, the file, its schema and the supported ones.
  • A workspace on a release of a schema newer than the window was logged with the schema 0.4 upgrade command, which cannot help; the log now says a later RDM Engine wrote the database and is the one to run.

Added (0.11 V3)

  • A change set left on a release its workspace no longer runs moves onto the workspace's release even when its content does not fit it: POST /change-sets/{id}/model-migrations (expectedChangeSetVersion, optional targetReleaseId, typeMappings), registered where background jobs run, starts a migration job (parameters.moves: "changeSet"). Its preview converts the change set's own content with a migration's rules, lays it over live data as it is now and lists every conversion, type change, loss and refusal, changing nothing; a record or relationship the change set changed that live data changed since it started is refused (CHANGE_SET_CONFLICT, MAPPING_SET_CHANGED), as is every rule the moved change set breaks. Confirmed (the change set's version, the workspace's assignment and feed position), the change set's rows, its base (now a capture of live data) and its release change in one transaction; live data, the workspace's release and its feed never do. Code MIGRATION_TARGET_NOT_WORKSPACE_RELEASE.
  • The change set bar's notice offers Move this change set to ‹release›… beside V2's offer; the migration page names the change set, says live data stays as it is, confirms with Move change set and opens the change set when done.
  • snapshot_values.reindex_change_set: a change set's typed value index rebuilt from its rows in set statements.

Added (0.11 V2, API)

  • A change set on a release its workspace no longer runs (typically one started from an older publication) is published by migrating the workspace to the change set's release: POST /workspaces/{key}/model/migrations takes changeSetId and expectedChangeSetVersion, and targetReleaseId is then optional. The preview also lists what would stop the publication as refused items (CHANGE_SET_CONFLICT and the graph's rules); the confirmation binds the change set's version and the active publication; the migration, the switch and the publication happen in one transaction, or nothing does. The summary names changeSetId, changeSetVersion, activePublicationId and publicationId. Code MIGRATION_TARGET_NOT_CHANGE_SET_RELEASE.

Added (0.11 V2, browser)

  • The change set bar of a change set on another release offers Move ‹workspace› to ‹release› and publish…; the migration page then says it publishes the change set, lists what would stop the publication, confirms with Migrate and publish and names the new publication.

Added (0.11 V1, API)

  • POST /workspaces/{key}/model/migrations: a workspace's data migrated to another installed release, of its model or another, as a migration job. Its preview changes nothing and lists, at GET /jobs/{id}/rows, every value converted, record reclassified, loss and refusal; typeMappings keeps a renamed type's records under their IDs. POST /jobs/{id}/confirm binds the reviewed assignment version and feed position and needs acceptLoss for a loss; the data and the workspace's release then switch in one transaction, each change audited with its feed position. Codes MIGRATION_REVIEW_STALE, MIGRATION_HAS_REFUSALS, MIGRATION_LOSS_NOT_ACCEPTED, INVALID_MIGRATION_MAPPING, MIGRATION_TARGET_IS_CURRENT, MIGRATION_FAILED.
  • A job's migration summary; POST /jobs/{id}/confirm takes an optional body.
  • Migration 20261019_01: the migration job kind, rdm_convert_value and an index of a migration's items.

Added (0.11 V1, browser)

  • Settings › Model's activation dialog offers Preview the migration… when the data does not fit; the migration's page (/settings/model/migrations/{jobId}) lists what the release no longer has with a choice for each missing type and field, the items by outcome, and confirms once the loss is accepted; a stale review asks to preview again. test:browser:workspace-v1.
  • A migration is refused with 409 CONSUMER_CONTRACT_VERSION_REQUIRED when its target changes a consumer API's contract without a new version, as activation is.
  • test_migration_budget (budget): a workspace of 100,000 records migrated once.

Changed (0.11 V1)

  • The compatibility analysis can list the records behind each rule a record breaks, and its scoped uniqueness count climbs from the rule's targets to their scope only (over 9 minutes at 100,000 records before, 5.8 s now); activation shares it.
  • A migration's long steps renew the job worker's lease as they run.
  • ADR-034 amended (D71): a migration's type mappings change record types in live data.

Let us run it for you.

Backups, upgrades, TLS and monitoring are ours. Your data stays exportable any time.