Changelog
0.8.0, 2026-09-29
Two new editors: a matrix for the relations between two lists, and a tree for hierarchies, both quick on large lists.
Releases are numbered by plan, not by date, so a higher number can ship before a lower one. All releases
Full changelog
Released 2026-09-29. 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.
Added
-
Base 0.8 R1b and R2 (2026-09-29): build acceptance of B5, B5b and Milestone C in one pass. The full backend suite, Vitest, all 22 browser runners and the accessibility and visual gates pass with no fix or re-baseline, and no migration is needed, so 0.8.0 is released (D43).
-
Base 0.8 S1: a Track S volume fixture and read baseline.
tests/volume_fixture.pygenerates a model with its own keys and bulk-seeds 10,000 root records, 100,000 contained records (20,000 under one parent), a seven-level self-hierarchy, a record with 1,500 relations of one type and a parent with children of two types, then records revision 1 and derives a draft with scattered edits, tombstones and new records.tests/volume_baseline.pymeasures time, SQL statements and ORM rows per request in live, draft and history contexts; the full-volume figures and the budgets are recorded in the Base 0.8 plan. The newvolumetest group holds the budgets at two scales: live reads and writes meet them, while draft/history reads and draft writes are strict expected failures until S2. -
Base 0.8 S2: draft and history reads in SQL. Migration
20260925_01addssnapshot_attribute_values, a typed index of revision, draft-base and draft object values (D1), backfilled from existing rows and kept current by an ORM flush hook, plus ordered page indexes on revision and draft-base objects. Object pages, single records, attributes, relation pages and draft write responses read one base-plus-overlay statement instead of the whole draft or revision; at full volume a draft page takes 3–6 ms (was about 3.1 s) and a history page 2–5 ms (was 1.1–2.6 s). Live, draft and history share one object query builder, held by a parity test across all four contexts. -
Base 0.8 S3: cursor and position paging on every collection endpoint (ADR-030). Pages add
prevCursor, ananchorthat reopens the page, and withincludeTotal=trueatotalCountandstartIndex; requests addfrom=end,position=Nand, on object, relation and mapping-coverage lists,seek=<value>. Selecting a page two ways is refused with422 conflicting_page_parameters. Existing cursors keep working. -
Base 0.8 S4:
topLevel=trueon object lists pages records without a parent in every context; relation lists gainsearch(the related record's name and searchable fields) andsort(name, andcreatedAtlive) in live, draft and history. Migration20260925_02adds (owner, endpoint, relation ID) indexes on revision, draft-base and draft relations, so a draft or history relation page reads the record's own relations instead of the whole revision. -
Live object page indexes (migration
20260925_03): (workspace, type, name, sequence) and (workspace, parent, type, name, sequence). Name-ordered live pages past the first, their totals and position reads now seek instead of scanning the type: 2–13 ms at full volume (a deep position read took about 500 ms). -
Base 0.8 S5: continuous-scroll lists (ADR-030).
useCursorWindowkeeps at most five pages, prefetches one page ahead of either edge and evicts the page furthest from the viewport;ui/ScrollWindowkeeps rows still as pages arrive above them, shows quiet start and end markers and a loading marker only while the user waits, and offers a proportional scrollbar sized from the total (a click or a drag's release sends one position request), a page field, start and end buttons, Ctrl+Home/Ctrl+End and, on the collection lens, Go to a value. Arrow keys move through rows across page boundaries. Adopted by the collection lens, record panels (which gain a sort, by name by default), relation panels (which gain search and sort), the mapping lens, the mapping candidates and the object picker, now a search field over a listbox.tests/volume_seed.pyseeds the volume fixture for the newtest:browser:s5runner. -
Base 0.8 S6: the sidebar record tree reads by parent. Each root (
rootCreatable) type of the application pages its top-level records (topLevel=true) in its own bounded, scrolling group; expanding a record pages each child type separately withparentObjectId, headed by the type and its count, and collapsing releases the branch. Load more objects and the global per-type load are gone, and with them the record with an unloaded parent drawn at the top level. The tree is arole="tree"with one tab stop,aria-setsizeandaria-posinsetfrom totals and arrow keys that load across a window's edge. Search lists matching records of every type with their ancestor path.ui/ScrollWindowreads only its own rows, so windows nest, and gains agrouprole for rows owned by an enclosing widget. New runnertest:browser:s6. -
Base 0.8 S7: composed Track S acceptance. New runner
test:browser:s7walks one collection-lens journey per context (scroll, scrollbar click, seek, end, page number, copied URL) over the volume fixture and the starter model, checks each landing against the same context's API read, and opens a record deep in the tree; it passes at full volume (S7_VOLUME_SCALE=1). The accessibility and visual gates gain ascrolling-list-treescenario in light and dark, and every visual baseline was refreshed after review.volume_baseline.pyadds a whole-type name search and a deep record read. -
Base 0.8 S8: indexed search (D25). Migration
20260926_03adds SQLite FTS5 trigram indexes over live and snapshot text values, kept equal to the values by triggers on every write. A search of three or more characters looks up values only for the records the index names, and still decides with the same case-insensitive "contains" match, so results are unchanged in every context. A whole-type name search with its total at 100,000 records takes 37 ms live, 73 ms in a draft and 40 ms in history (was 0.7–0.9 s). Names stay on the (type, name) index; one- and two-character searches, and phrases in more than 10,000 values, read each record as before. -
Base 0.8 W6: delete an unused workspace to correct a mistyped key (ADR-019 amendment, D27).
DELETE /workspaces/{key}?expectedVersion=Nremoves a workspace that is not the default, was never published and has no objects or change sets, active or retired; the key can be created again. It refuses withworkspace_is_default,workspace_has_publications,workspace_not_empty(with counts) orversion_conflict, changing nothing, and is gated byWORKSPACE_ADMIN_MUTATIONS_ENABLED. Single-workspace responses adddeletableanddeleteBlockedBy(OpenAPI lock re-baselined). The Workspaces screen offers Delete… with a confirmation naming the key, or says why it cannot. -
Base 0.8 A1: the schema 0.7
relationMatrixview contract (D8, D9). Rows are records of one type, columns the records of a type that can contain the cell type at some depth, and a cell the row's related records through one binding, read from either end (direction), required or optional, of any cardinality. A view declaresrowColumns,columnLabelField,cellFields,searchFieldsand a closedcellActionsset (create,edit,link,unlink); an action that could never succeed for the model is refused. Each failure points at the failing config node, and schema 0.5 and 0.6 models refuse the view at itstype. The view groups as a lens of its row type's collection and renders a placeholder until A4. The starter (still content 0.8.0) configures it three ways: codelist and master-value translations by language, codelists by system and master values by system; the translations and systems applications now include codelists. -
Base 0.8 A2:
GET /api/v1/workspaces/{workspaceKey}/relation-matrixreads arelationMatrixview in live, change-set and publication contexts. Rows page like any collection (cursor, position, seek, totals), searching the view'ssearchFields; columns are named withcolumnIds(at most 20) or paged by name with their own cursor (D10). Each cell reportsone,several(withconflictwhen the binding's uniqueness scope is the column type),inactive,historyOnlyornone(D11), with counts, up to five records with theircellFields, versions and relation IDs, and each row its relation-set version.statewithcolumnIdsfilters rows across the whole list before paging, by one recursive walk down from the selected columns (D12); at full volume the filter is over its time budget (seconds), a temporary exception that A3b's stored cell state removes (D12). A page costs a fixed number of statements. The volume fixture gains avol-matrixapplication (30 columns, a three-level container hierarchy, 150,000 cells at full scale, an outgoing conditioned and an incoming many-to-many matrix), measured with the other reads; the generated model is now schema 0.7. Object query builders accept a view's search fields and an ID predicate that also narrows a change set's overlay parts. -
Base 0.8 A3: writes from a relation matrix cell, live or in a change set, through the generic object and relation APIs.
GET …/relation-matrix/containerslists where a new cell record can go (the column record and the records under it that can directly contain the cell type, with their path), naming the column's only container asdefaultContainerId(D13).GET …/relation-matrix/link-candidateslists the cell-type records under a column record for a row, the ineligible ones with a reason (alreadyLinked,rowCardinality,candidateCardinality,scopeTaken). Relation create and delete accept an optional relation-set claim (expectedRelationSetVersion,relationSetDirection) and refuse a stale one with409 relation_set_version_conflict; a claimed create returns the set's new version. Cell records carryrelationVersion. OpenAPI lock re-baselined; the paging inventory counts 20 collections. -
Base 0.8 A3b: stored relation matrix cell states (ADR-033). Migration
20260927_01addsmatrix_cell_statesandmatrix_state_builds; the application builds every missing owner (live, publication, change set base, change set overlay, publication history) and matrix shape at startup, on model activation and when change sets and publications are created, and recomputes the rows a write touches before it commits. The row state filter reads them by index, once per request, and derives cells from records only where a context is not built. The API contract is unchanged.matrix_states.verifyrebuilds and compares every stored state; matrix tests run it after each test. -
Base 0.8 A4: the relation matrix lens replaces the A1 placeholder. Rows scroll as every list does, the row header stays pinned with the view's row columns, and columns are the ones chosen in the Filter popover or a page of the column type, paged sideways. Each cell shows its state with an icon and a label, explained by a legend of the states the binding can produce. The Filter popover chooses the columns (searchable) and a cell state; both live in the URL. The grid is keyboard operable (arrow keys across cells and pages, Enter to open). A cell's dialog offers only the view's
cellActions: create (the column's only container preselected, otherwise a searchable picker; Create from this for a history-only cell), edit, link (candidates listed with the reason any cannot be linked) and unlink, live or in the selected change set, and read-only in a publication. A save re-reads only that cell.ScrollWindowkeeps a grid cell's column on Up and Down. New browser runnertest:browser:a4. -
Base 0.8 A5: Milestone A acceptance. Browser runner
test:browser:a5runs SC-09's translation half by keyboard (fill a missing translation live, a refused duplicate, restore one from history in a change set and publish it from the change set screen), links and unlinks a system value, and fills a cell on the generated volume model. The accessibility and visual gates gain the relation matrix, its cell dialog and Filter popover in light and dark, with four reviewed baselines. Full-volume matrix reads measure 23–82 ms in every context, within the 100 ms budget. -
Base 0.8 B1: the schema 0.7
treeview contract (D9). A tree declares itsrootObjectTypeKeys, the node types it shows (objectTypeKeys), alabelField, an optionalsecondaryField, achildSortper node type, apageSizeand a closedactionsset (createChild,rename,move,setAvailability, the last with a booleanavailabilityField). A node type unreachable from the roots through the types the tree shows, a root type that cannot stand without a parent, a field missing from a node type and an action that could never succeed are each refused at the failing config node; schema 0.5 and 0.6 models refuse the view at itstype. The view groups as a lens of its first root type's collection and renders a placeholder until B3. The starter (still content 0.8.0) adds a system hierarchy (systems, recursive system groups, system values) and a domain hierarchy (domains, codelists, master values); the generated volume model adds one over its groups, items and deep node self-hierarchy. The Domains and Systems tables gain a lens switch; four visual baselines were updated after inspection. -
Base 0.8 B2: move targets.
GET /api/v1/objects/{type}/{id}/parent-candidateslists every record of a type that can contain the record, in its application and workspace, live or in a change set, in name order with search, cursors, positions and totals. A candidate the move would refuse or not change stays listed witheligible: falseand areason(self,descendant,currentParent; D14), and every candidate carries its root-firstpath. The generated volume model lets groups nest, so a record with 20,000 records under it can move. -
Base 0.8 B3: the tree lens replaces the B1 placeholder. It shows the view's root types, and under each record one branch per child type the tree shows, ordered by the view's
childSortand paged by itspageSize, with thelabelField,secondaryFieldand, where declared, the availability field. It reuses the sidebar tree (S6): bounded scrolling branches, totals, one tab stop and arrow keys, plus type-ahead (a space typed mid-word continues it). A search result, and the record open beside the tree (also after a reload, Back or Forward), is revealed where it sits: each branch on its path opens at it withseek, and unrelated branches stay closed. Records open in the inspector beside the tree (the retained split view), full width below 1080px. The view'sactionscome from a node's menu (Shift+F10, the context menu key or the row's ⋯): add a child of each type the node can contain (the new node appears in its branch with focus), rename in place (F2), set availability (also the row's switch) and Move to…, a dialog over the B2 candidates where ineligible targets are listed disabled with their reason; a move keeps focus on the moved node and refreshes both branches. A refused write keeps the user's input and place. Writes go to the selected change set; a publication's tree is read-only. New browser runnertest:browser:b3. -
Base 0.8 B4: Milestone B acceptance. Browser runner
test:browser:b4runs SC-09's hierarchy half by keyboard on the system hierarchy (live, 1440px) and the domain hierarchy's nested master values (live, 390px): find a nested record reading only its ancestors' branches, add a child and a grandchild, be refused the move beneath the grandchild in the dialog and by the API, move the child and see its new path in the tree and breadcrumb with its containment edge changed and unrelated branches unchanged. A move staged in a change set reaches live only when the change set is published. The accessibility and visual gates gain the tree lens and its move dialog in light and dark, with four reviewed baselines. The tree lens's keyboard helpers are shared by both runners (e2e/helpers/tree.mjs). -
Base 0.8 P2a: change lists.
GET /api/v1/change-sets/{id}/object-changesand/relation-changeslist only what a change set changes, read against its source: each record or relation it touched asadded,changedorremoved(D31), withbefore(the source's row) andafter(the change set's), typed values in the object read's shape,changedFieldsand, for records, the path. A record created and deleted inside the change set is left out, as publishing skips it; one edited back to its source values stays listed with no changed fields. Relation entries name both ends and mark engine-maintained parent edges withcontainment(D32), so a move is achangedrecord withparentplus one removed and one added edge. Filters by kind, type and containment; every paging mode. The volume fixture gains a review change set touching 12,073 records and 8,273 relations at full volume, where every measured page takes 3–52 ms in 4–6 statements. -
Base 0.8 P2b: change set review screen. Review changes on the change set bar (and a link in the publish confirmation) opens
/apps/{app}/change-sets/{id}/changes: Records and Relationships tabs, each a continuous list of what the change set adds, changes and removes, with a kind badge in words, the type and model labels from the change set's model, the path or both ends, and the changed fields. A row expands to a before/after table of those fields with typed values as the record screen shows them; a moved record names its old and new parent. Added and changed records open in the change set. Filter by kind and type; parent links are hidden from Relationships unless asked for (D32). Tab, filters and scroll position live in the URL. The object change entry gainsbeforePath, where the source held a moved record, read in the same walk aspath. New runnertest:browser:p2; the accessibility and visual gates gain the review in light and dark. -
Base 0.8 B5: drag and drop in the tree lens, a second way to Move to… for mouse and pen where the view allows moves, live and in a change set. Drop onto a record, into a gap between rows (the branch the gap belongs to; at a branch end the pointer's horizontal position picks the level, shown by the drop line's indent) or onto a type heading (its parent). While dragging, the record's row is dimmed and a preview names it; an accepted target glows green, a refused one red with its reason as text (The record itself, Inside the record, Current location, Can't contain …, In another application) and a no-drop cursor. Resting on a collapsed record opens it, and the tree scrolls near its edges. A drop opens the move dialog with the target chosen, so Enter confirms the same write as Move to…; Escape or a release elsewhere writes nothing. Touch pointers, the sidebar tree and publications never drag (touch drag in 0.11, D35).
test:browser:b3gains the drags; the accessibility and visual gates gain a drag over an accepted and a refused target, light and dark. -
Base 0.8 B5b: moving a record to the top level from the tree lens, by both paths (D36). Move to… lists Top level above the candidates, outside the paged list, and confirming writes no parent. Dragging onto a top-level heading, into a gap between top-level rows or to the left end of a gap that ends branches down to the top level offers it too, and the drop opens the dialog with Top level chosen. Only a nested record of one of the tree's root types goes there; any other type is refused as Not shown at the top of this tree, and a record already there as Current location. A search's matches offer no top level.
test:browser:b3moves a nested group of the generated volume tree to the top live by Move to… and in a change set by a drag. -
Base 0.8 C1: a record's type can change in a change set (ADR-034).
services/type_changes.pychanges one record's type, optionally under a new parent, with a given attribute mapping: values are kept, mapped (same value type, ortexttolongTextandintegertodecimal), restored or given as inputs, and the rest have no place; the containment edge becomes the relation the model allows, and each child's edge the relation the new type contains it by; relationships the new type may not have are removed and the others keep their IDs; relationships offered back are restored under their former IDs, and required relationships are inputs. Uniqueness, required values and relations, uniqueness scopes, cycles and the record version are checked by the change set write services, andcheck_type_changeruns every check without keeping a write. Each change records areclassifiedaudit event with the full before state, which the history read lists withchangedFieldsandattributeTypeKeys. Publishing needed no change: it writes the new type to live and refuses a live edit of the record or a live relationship the new type cannot have. The restructuring API that exposes the service is C3. -
Base 0.8 C2: the restructuring planner (
services/restructuring.py) plans a move or a type change of one record and its subtree in a change set. It works out the possible types at the target and asks once per (type, parent type) pair where several are possible; cascades level by level, each child the new parent cannot contain becoming a type it can; suggests each attribute's source by the D40 rules, which a choice per type pair overrides; and lists per record what is kept, mapped, restored, given or without a place, the inputs still needed and the refusals. Values and relationships are offered back from the record's reclassifications in the current change set and in published ones, relationships under their former IDs. A ready plan is dry-run through the change set write services and rolled back; its digest binds an apply to the previewed plan and the change set version. More than 500 records is refused withrestructuring_too_large: D44 lowers C0's 5,000 because the dry run takes 48 s at that size and 2.8 s at 500. The routes are C3. -
Base 0.8 C3: the restructuring API.
POST /api/v1/change-sets/{id}/restructurings/previewplans an operation and writes nothing;POST /api/v1/change-sets/{id}/restructuringsapplies it when the recomputed plan matches the previewedplanDigestand the change set is still atexpectedChangeSetVersion(409 restructuring_plan_changed,409 change_set_version_conflict,422 restructuring_not_ready). Five kinds:move(with a type change where the target needs it),changeType,insertLevel(a new record between a parent and chosen children, its values given under the IDnew, its ID returned ascreatedObjectId),removeLevel(a record deleted with its relationships, its children moved to its parent, changing type where needed) andchangeTypeUnder(every record of one type under a record). The plan's records page by position in plan order with the standard page links, filtered byoutcome; cursors bind the change set version and the plan. Parent candidates takeincludeTypeChanges=truein a change set, listing the records a move with a type change could go under, each withobjectTypeKeys. -
Base 0.8 C4: restructuring in the tree lens, in a change set only. Move to… and a drag also offer places where the record's type cannot stand but another type of the tree can: the candidate and the drag say Becomes
, and choosing one opens a preview instead of moving. The node menu adds Change type…, Insert level above…, Remove this level… and Change type of all in here… , each only where the model makes it possible. The preview shows the plan by record (new type and place, each value's origin, what has no place, what is restored, inputs asked in place) and by type pair (each attribute's source changeable in place), answers type questions, lists refusals, applies the reviewed plan and links to the change set's review. Insert level above… lists the record's siblings to choose more records to group under the new level. -
Base 0.8 C4 (D45):
GET /api/v1/model/placementsserves where the context's model lets records stand, per application: the types at the top and, per parent type, the types it contains and the types a record can become under it. It is the engine's one rule (services/model/placements.py); the browser loads it with the model and no longer derives containment itself. -
Base 0.8 C4: parent candidates take a repeatable
withinObjectTypeKey, keeping the candidates and, withincludeTypeChanges, the types the record would become to a set of types; the server filters, so totals stay truthful. The tree lens passes its types, so Move to… offers what the drag offers. -
Base 0.8 C5: Milestone C acceptance.
test:browser:c5runs the restructuring journey on the starter model and the generated model against a real backend: a move that reshapes a subtree across two levels of types, answering the preview in place, inserting and removing a level, the review, publishing from the change set screen, live, the publication and both ends' history, then changing a record back in a second change set with its values or its relationship restored. The accessibility and visual gates add Move to… with a type change, the preview by record and by type, and the insert, remove and change type dialogs, light and dark. User story US-H03 and scenario SC-17.
Changed
-
Base 0.8 C4: the change set review names a record's type change on its row and as Type in its before/after table. The browser follows a record opened under a type it no longer has to its current route (D42), replacing the old route and keeping focus. In a change set, a tree lens record dragged to a place its type cannot stand, such as an item to the top level, now becomes a type that can (B5b refused it); live, such a place's reason adds that a change set can change the type. A cursor list's reload whose anchor went stale reopens at the start instead of failing.
-
Base 0.8 C1 (D42): every
/objects/{objectTypeKey}/{objectId}route answers409 object_type_changedwith the record's current type for a record found under another type in the selected context, live, in a change set or in a publication, where it answered404 object_not_found. A change-setPATCHnow checks the route's type, which it ignored. The change lists' object entries carryobjectTypeKeyonbeforeandafter, nameobjectTypefirst inchangedFieldswhen it changed, and theobjectTypeKeyfilter matches either side; a relation entry iscontainmentby the type itstoend has on either side. -
Design: Base 0.9 integration APIs. Decisions D1–D16 and CONSUMER_API C-D2–C-D16 are accepted. A code check revised D5: the feed is positioned by a per-workspace counter taken under the workspace lock until commit, so feed order is commit order on PostgreSQL too; and added D15 (a publish writes one audit entry per record it applies to live) and D16 (feeds carry keys, never the
reclassifiedbefore state).docs/INTEGRATION_API.mdgains sections B (batches with record references, upsert, relation items and relation-set replacement), F (the workspace change feed, position, retention and410recovery), Q (cross-type lookup,/workspaces/{key}/relations, the model-aware query andasOf) and J (import with preview and confirm, the file contract, the import profile with a genericrelatedRecordbinding, and export). APPLICATION_MODEL describes schema 0.8 (effectivePeriod,consumerApis). ADR-036 (a batch is a job), ADR-037 (the audit log orders the feed and consumer events) and ADR-038 (effective periods) are accepted. -
Planning: the Base 0.9 plan follows the 0.8 layout: how a package is delivered, a numbered delivery queue with dependencies and the D9 cut line, and work packages Q1–Q4, B1–B3, F1–F2, V1–V2 and J1–J4 with design references, scope, tests, budgets, test groups and exits, the C1–C16 table and R build acceptance. New test groups
jobs,feedandconsumerand runnerstest:browser:v1andtest:browser:j4are planned. The API reference track is renamed V. -
Design: Product 0.10 is model authoring and spreadsheet onboarding (ADR-035,
docs/MODEL_AUTHORING.md): one editor for the model and the import mapping, where every edit is a validated operation on a model draft, inference from an uploaded workbook proposes operations, and saved import profiles load the next version as a reviewed upsert. Production hardening moves to 0.11 and security to 0.12 across the documentation; 1.0 keeps changes to released structure, protected publishing and extensions. 0.9 D3 is revised: import runs an import profile, with XLSX. New stories US-P09 and US-P10. A-D1–A-D3 decided (one type per list, declared navigation groups, profiles stored apart from releases).docs/BASE_0_10_IMPLEMENTATION_PLAN.mdis the provisional 0.10 plan: packages A0–A8 and R, synthetic acceptance workbooks, decisions D1–D10. -
Design: Base 0.8 C0. ADR-034 accepts that a record's type can change, keeping its ID, in a change set only, within its application. The attribute mapping is derived from the model (same value type or a lossless widening), relationships are kept or removed, and descendants follow level by level. An operation is previewed, then applied against the change set version and the plan's digest. Each type change records a
reclassifiedaudit event with the full before state, and later type changes offer its values and relationships back. A route naming another type than the record's returns409 object_type_changed. One operation changes at most 5,000 records. API.md gains the restructuring API design (/change-sets/{id}/restructurings[/preview]). D37–D43 are accepted; 0.8.0 now waits for Milestone C and R2 (D43). -
Planning: Base 0.8 Milestone C, structure refactoring (C0–C5, R2 build acceptance), for delivery before 0.9. A record's type changes when it moves where only another type is allowed. The new type and the attribute mapping are derived from the model, with no declarations, and confirmed in a preview. Values without a place stay in history and are offered back later. Named refactorings (change type, insert or remove a level, change type under a record) run in a change set. D37–D43 are open.
-
Planning: Base 0.8 B5, drag and drop in the tree lens, added in the Milestone B review. Records can be dropped onto a node, into a gap between rows or onto a type heading. Drops are accepted by the model's containment rules, show a green or red glow with the reason, and are confirmed in the existing move dialog. D34 decides the targets and the confirmation; D35 defers touch drag to 0.11. B5b, an amendment to B5, adds moving a record to the top level in Move to… and by drag, for the tree's root types (D36). R1b is added as an amendment to R1: the build acceptance for B5 and B5b, required before 0.8.0 releases. One record is moved at a time.
-
Documentation: the bundled model is described as an example set.
docs/EXAMPLE_MODEL.mdexplains how engine, model and application relate, lists the views each example application declares and explains why they differ (only the example's declarations, never the application). It also shows what each gap would take. README, AGENTS, PRODUCT, APPLICATION_MODEL, UI_MODEL and the functionality catalogue state that every application gets the same feature set. Milestone A is named the Relation matrix throughout, with ADR-031 and the W1 note amended. -
Base 0.8 B3: the sidebar tree's tab stop returns to the open record once it is drawn, unless the user has since focused another item; it also gains type-ahead. A record's inspector re-reads the record when the collection beside it writes, and a narrow inspector wraps the record's actions below its title.
-
Base 0.8 B2: moving a record costs about what a rename does, however much lies under it. A live update re-checked ancestor-scoped uniqueness for every relation into the record's whole subtree on every write, rename included; it now checks nothing for a rename, the record's own relations for a value change, and after a move only the records below whose scope record is above the moved one, for bindings that declare a scope. Stored matrix cell states no longer walk down from a moved record that can hold no cell below a column, recompute a written relation's ends only in shapes of its relation type, and find a cell record's rows by its own endpoint index. Moving the 20,000-descendant record at full volume takes 6 ms live (was 773 ms) and 13 ms in a change set (was 53 ms), and a change set rename 8 ms (was 17 ms). Moving a container of about 2,500 matrix cells stays seconds in both contexts and is a known 0.8 budget exception (D29).
-
Base 0.8 A6: a migrated database now has the schema the ORM declares (D28). Ten single-column indexes declared on publication and change set tables were never created by a migration. Migration
20260928_01giveschange_set_objectsthe (owner, type, name, object) and (owner, parent, type, name, object) indexes the publication and base object tables have, so relation matrix walks through a change set seek children by parent instead of building an automatic index per statement; the other eight declarations are removed because owner-leading indexes already serve every read, and a database built by an earliercreate_allloses them on upgrade. Newtest_schema_driftfails on any difference between a migrated head and the metadata. Volume budgets and statement counts are unchanged. -
Base 0.8 A5: a relation matrix cell refused for uniqueness says so in the view's terms, naming the row, the column (or the scope) and the condition, for example "Delivery Methods already has a translation in cs where Active is Yes, and only one may count there.", instead of the API's generic "A related object already exists for this ancestor scope". The API is unchanged.
-
Base 0.8 A3: relations an object create adds through
initialRelationsnow advance both ends' relation-set versions, live and in change sets, like every other relation write (ADR-022); a matrix row'srelationSetVersiontherefore counts them.409 duplicate_relation_scopeon relation and object writes names the record already in the scope (conflictingObjectId,conflictingRelationId); the change set diagnostic addsconflictingObjectId. -
Base 0.8 W4: a global Workspaces screen beside Models, in the same list-and-detail layout, lists every workspace including retired ones and creates, renames, retires, reactivates and sets the default with their version claims; a conflict shows the current state and is never retried. It appears in the header, and as Manage workspaces in the workspace selector, only when
workspaceAdmin.manageWorkspacesis true. The selector is a read-only label while one workspace exists. Models and Workspaces show the deployment capabilities read only, naming the setting of a disabled group, whichGET /capabilitiesnow reports in each group's newsettingfield (OpenAPI lock re-baselined). The narrow navigation drawer is titled for its screen. New browser runnertest:browser:w4; the a11y and visual gates gainworkspaces-adminlight and dark, and every shell baseline was refreshed for the header entry and the single-workspace label. -
Base 0.8 W3:
WORKSPACE_ADMIN_MUTATIONS_ENABLEDgates only workspace create, rename, retire and set default (ADR-031). Change-set create, update, delete, model-release change, validation and publish, and publication withdrawal, are always registered and still refused in a retired workspace. Their OpenAPI tag ischange-sets. -
Base 0.8 W2: data drafts are change sets and workspace revisions are publications in every layer (ADR-031), with no aliases. Breaking API changes:
- Routes:
/workspaces/{key}/drafts→/workspaces/{key}/change-sets,/workspace-drafts/{id}→/change-sets/{id},/workspace-drafts/{id}/activation→/change-sets/{id}/publication,/workspaces/{key}/drafts/{id}/model-release→/change-sets/{id}/model-release,/workspaces/{key}/revisions→/workspaces/{key}/publications,/workspace-revisions/{id}→/publications/{id}, andPOST /workspaces/{key}/retirementwithexpectedActiveRevisionId→POST /publications/{id}/withdrawalwith no body (the path ID is the claim). Lists stay under their workspace; single items are flat by global ID. - Fields and parameters:
workspaceDraftId→changeSetId,workspaceRevisionId→publicationId,expectedWorkspaceDraftVersionandexpectedDraftVersion→expectedChangeSetVersion,workspaceDraftVersion→changeSetVersion,expectedActiveRevisionId→expectedActivePublicationId,derivedFromRevisionId→derivedFromPublicationId,supersedesRevisionId→supersedesPublicationId,revisionNumber→publicationNumber,activatedAt→publishedAt,retiredAt→withdrawnAt, source"revision"→"publication"and publication statusretired→withdrawn. A publication also returnsmodelReleaseKeyandmodelReleaseVersion. - Error codes:
workspace_draft_*→change_set_*(change_set_not_found,change_set_version_conflict,change_set_model_release_conflict,change_set_required,change_set_snapshot_retry,change_set_snapshot_unsupported),draft_content_incompatible→change_set_content_incompatible,workspace_revision_not_found→publication_not_foundandworkspace_active_revision_conflict→workspace_active_publication_conflict.
Migration
20260926_04rebuilds the draft and revision tables aschange_set*andpublication*with their columns, indexes and constraints, renamesworkspaces.active_revision_idandrelation_set_versions.workspace_draft_id, records snapshot value owners aspublication,baseandchange_set, and rewrites stored audit names (change-set,change-set-object,publication/published,publication-withdrawn) and payload keys. It resumes an interrupted run on SQLite and refuses another dialect with a recovery instruction. The browser says "Change set · name", "Publish change set", "Publication N" with its model release and date, and "Withdraw publication"; its URLs are/apps/{app}/change-setsand/apps/{app}/publicationswithchangeSetIdandpublicationId. The OpenAPI lock is re-baselined, thedraftstest marker ischange_sets, and the WP9 runners aretest:browser:change-setsandtest:browser:publications. - Routes:
-
Base 0.8 W1: documentation adopts the ADR-031 terms. The glossary defines workspace, live, change set, publication ("Publication N" and its per-workspace number), model draft, model release, and the verbs publish, install, activate, withdraw and retire, with a diagram of one workspace's live data, change sets and publications and a note on the old names the code keeps until W2. Milestones A and B are renamed Translation editor and Hierarchy editor in the roadmap, TODO, the 0.8 plan and the functionality catalogue. The stale "non-default workspace detail gap" notes in PRODUCT, the product guide, user journeys, user stories and the functionality catalogue are corrected: 0.6 WP9 repaired the context handoff, and the WP9 navigation runner opens detail, children, relationships and history in a non-default workspace.
-
Base 0.8 S8: a draft list's total counts its base, less the rows the draft replaces, plus its own rows, instead of the union (a total at full volume in 19–31 ms, was 45–56 ms); a first or position page that reaches the end of its list returns its total without a count.
-
Base 0.8 S4R1: relation lists sort and search the related record through the object query builder, as object lists do.
sorttakesname(now the default in every context), anysortableattribute of the related type,ancestor:<type>, and live only the related record'screatedAt/updatedAt; relation creation order is no longer a sort. Attribute and ancestor sorts need the listed relations to reach one object type (422 invalid_sortotherwise), andrelationTypeKeyrepeats to list several relation types. Search covers the related types' searchable fields instead of every searchable attribute type. Draft and history object and relation pages return each record'spath(it was empty, so ancestor columns were blank), and a relation page for a record the context does not hold answers404as it does live. Migration20260926_01rebuilds the draft and history relation endpoint indexes to cover the relation type and the other end. Relation cursors issued before the change are refused with422 invalid_cursor. -
Base 0.8 S4R2: schema 0.7. The engine accepts
schemaVersion: "0.7"beside 0.5 and 0.6 in validation, the registry, releases,model_schema_upgradeand the startup message. Relations panels may declaresortFieldsanddefaultSort(D21), validated against what the relation list accepts:name, a sortable attribute of the one related type, or an ancestor it can sit under; a 0.5 or 0.6 model declaring them is refused at the field's path, and absent fields stay out of the canonical document so earlier releases keep their hash. The starter model moves to schema 0.7, content release 0.8.0: its system panels sort by system, value code and name, its translation panels by language, translated name and name, andtranslated-namebecomes sortable. -
Base 0.8 S4R3: one sort element for table-shaped lists (
ui/TableSort), and one rule: every data column sorts. Column headers sort: a quiet sort icon on hover and focus (drawn in the header's padding, so columns never widen), an accent up or down arrow on the sorted column, activation toggling the direction,aria-sort, an accessible name stating the order and a polite announcement. Every list with sort fields shows the same icon-only Sort menu in its toolbar (radio items per field plus the direction, with a tooltip), which also reaches columns collapsed at 390px or hidden through Display and fields a view or panel declares without a column. The collection lens, record panels and relation panels use it. Removed: the lens toolbar's Sort popover and the relation panel's inline Sort select with its fixed name and creation-time options.ui/ActionMenugains radio sections, a trigger icon and a tooltip. -
Sorting no longer depends on the attribute type's
sortableflag: object and relation lists sort by any non-JSON attribute, and a relation list over several related types sorts by an attribute with records whose type lacks it last.objectTypesorts by the type's model name and, on relation lists,relationshipby the label each row shows, both then by name. The only data columns that do not sort are the computed path, JSON attributes and, outside live data, the unrecorded timestamps. Model validation follows: view and relations-panel sort fields accept any non-JSON attribute plusobjectTypeand (panels)relationship, and a relations panel may declaredefaultSortalone. The OpenAPI lock is re-baselined for the twosortdescriptions. -
Collection URLs hold one
anchorcursor for the page at the top of the list instead of acursortrail, written as the user scrolls without adding history entries. Older links withcursorparameters open at the start. Previous/Next page buttons and Load more in panels, relation lists and pickers are removed. -
Page responses carry the new navigation fields beside
itemsandnextCursor, so a client that compared a whole page body must allow them. Live relation pages build their related records in one batch instead of two statements per row. -
Draft and history
searchmatches the record name andsearchablefields, as live search does, instead of every attribute's text. Draft and history cursors issued before the upgrade are refused with422 invalid_cursor; restart from the first page. -
Base 0.8 S4R4: object lists without
sortorder by name in live, draft and history (live used creation order, drafts and history record ID), so the object picker and Explorer search results list by name and an unsorted list acceptsseekby name. Mapping coverage and candidates list by name, and coverageseektakes a source name instead of an ID. Cursors issued for the former default orders are refused with422 invalid_cursor; reload from the first page. -
Base 0.8 S4R5: every page lists
itemsfirst, then its navigation fields.totalCountandstartIndexare omitted unlessincludeTotal=trueinstead of returned asnull, and OpenAPI documents them as optional integers. Clients that tested them fornullshould test for absence. -
Base 0.8 S4R6: a draft position read resolves in its base's index and corrects for the draft's own rows, instead of skipping every earlier row through the base-plus-overlay union. Three quarters into a whole 100,000-record type it takes 60 ms (was 175 ms; about 40 ms of it is the total count), and its cost no longer grows with the position.
Fixed
-
Base 0.8 C5: change set validation, which publishing also runs, rescanned every record or relation for each record or relation in its uniqueness, parent-edge, cardinality, scoped-uniqueness and required-relation checks. Over the generated model at 2% volume (about 5,400 records) it took 125 s for an unchanged change set; it now reads indexes built once and takes about 1.4 s, with the same diagnostics in the same order.
-
Base 0.8 C5: a record whose type changed and which was then removed in the same change set was listed in
object-changeswithobjectTypeamong its changed fields although it has no after side, and the review showed it as→ Unknown type . A removed record now names only the fields its source holds, in its source type's order, and the review names it by the type its source holds. -
Base 0.8 C5: after a restructuring, the tree lens moved focus to the restructured record once its reveal read returned, even when the user had gone on to the lens search meanwhile, so the rest of what they typed went to the tree. The record now becomes the tree's tab stop without taking focus unless focus is still in the tree.
-
Base 0.8 C5: the restructuring preview's needs-input status began its second sentence in lower case, and values without a place showed booleans as
true/falseinstead of Yes/No. -
Base 0.8 R1 review: the change lists (
object-changes,relation-changes) read their page, then its paths and relation ends in further statements without holding one snapshot, so a change set write committed in between could mix two states in one page (a renamed parent in paths beside its own entry under the old name; after a concurrent publish, parent links without their end's type). Both routes now read one snapshot, as the relation matrix and parent candidates do. -
Base 0.8 R1: two browser runners no 0.8 package selected had drifted from the 0.8.0 starter model.
test:browser:sc15's incompatible candidate (a newly requiredexternal-id) was refused as a model because thecodelists-by-systemmatrix offerscreatewithout it; the candidate now also asks for the field there, so it is refused for its data again.test:browser:wp13casserted that RDM Systems offers no Codelist parent for a new master value, which the 0.8.0 model changes by adding codelists to RDM Systems; it now expects exactly the parent types the application contains, read from the model. -
Base 0.8 A review: when a relation matrix cell's edit or unlink was refused as stale and the re-read cell no longer held that record, the cell dialog went blank with no way on but Escape. It now shows the cell again with its actions and says the record is no longer in it. Starting another action from the cell clears the earlier notice.
-
Base 0.8 A review: a relation matrix kept showing cells re-read after earlier saves over a page it had reloaded, so a cell changed elsewhere since could stay stale. The reloaded rows now replace them.
-
Base 0.8 A1: a duplicate
mappingGridsourceColumns,targetDisplayColumnsorcandidateSearchFieldsentry was reported at…/config/config/<field>, a pointer to no node in the model; it is now…/config/<field>. -
Base 0.8 W6 review: on the Workspaces screen, creating a workspace after deleting one no longer leaves the "was deleted" notice above the new workspace.
-
Base 0.8 W6: a live write that resolved its workspace before the workspace was retired, or deleted, could still commit into it. It now rereads the workspace inside its transaction and returns
409 workspace_retiredor404 workspace_not_found, saving nothing. -
Base 0.8 W5: a live write validated under one model release could commit after another release was activated, storing data the new release does not allow. The write now rereads the active release inside its transaction and returns
409 model_release_changed, saving nothing; activation reserves the database writer before its compatibility check, and returns409 model_activation_retryif live writes keep it busy. Holds in one process and across instances sharing a SQLite database. -
Base 0.8 S8 review: the trigram search index is SQLite-only. Migration
20260926_03changes nothing on another dialect (it stopped a PostgreSQL upgrade), and a search there reads each record's values as before S8 instead of naming the missing index table. -
Base 0.8 S2b: a draft write no longer loads the whole draft. Object, attribute, relation and mapping writes check only the records and relations they touch: the parent and its ancestors, the relations at each end, the draft-wide holders of a unique value through the typed value index, and for a deletion the subtree it removes. Renaming one record in a 100,000-record draft takes 7.5 ms (was 4.9 s). Migration
20260926_02adds a value lookup index onsnapshot_attribute_values. A unique attribute without a value no longer conflicts with records that lack it. -
A draft or history single-record read returned an empty
path, so the record page showed no ancestors and the sidebar tree could not open the record's branch in those contexts. It now returns the ancestor path, as live reads and draft and history pages do. -
In the dark theme, a list footer's page field and Go to field kept the browser's light field colours and failed contrast; they now use the shared text-input treatment.
-
Paging a list sorted by a boolean field past its first page returned a server error; keyset cursor positions are now bound as typed values. Keyset pages also seek to the cursor through the first sort key's index instead of scanning every earlier row.
Let us run it for you.
Backups, upgrades, TLS and monitoring are ours. Your data stays exportable any time.