# Changelog weave's tracker (the Development space in the weave workspace) is the changelog of record: every Feature and Issue row carries its evidence. This file is the release-notes digest. ## v0.4.132 (2026-11-10) - On a touch screen the date and range pickers give every control a 44 px hit area: the month lays out as a 7 × 44 px grid in a 336 px popover, and the month arrows, the month and year buttons, Clear, Today and the typed date inputs each take a 44 px box. The phone touch gate now walks both pickers, so a smaller control there fails it ([[Issue#1184]]). - On a touch screen the icon picker draws its cells on a 44 px grid, eight to a row on a phone, and its search field takes a 44 px box. The cell is drawn at 46 px so that 44 px of it is still reachable under WebKit's coarser hit test on iOS. The phone touch gate now walks the icon picker, so a smaller cell there fails it ([[Issue#1189]]). - Opening a record whose HTML introduction sits above its fields no longer shows the fields and then shoves them off screen when the introduction loads. The fields and everything below the introduction wait until the frame reports its height, the last measured height of each introduction is reserved on the next open so it paints in place at once, and the editor's Introduction options decide their visibility once per session instead of growing the block a beat later. A new `html-intro-row` flicker journey opens such a record beside the table and holds the fix in `test/flicker-fixed.json` ([[Issue#1552]]). - On a touch screen the multi-select picker's **Done** button takes a 46 px box, so the full 44 px is reachable on both engines. The phone touch gate now walks the multi-select picker, so a smaller control in its footer fails it ([[Issue#1226]]). - An observer opening a record sees its title, field values and document body read-only, with no drag grips, field dialogs, "Make a field" button or next-state button. The record read now carries `readOnly: true` when the viewer's role in that space is below editor, and the record page gates every write control on it. Comments stay open to observers (Issue #1537). - A hosted weave instance now logs the layout shifts and long animation frames its users hit. `public/paint.js` watches `layout-shift` (input-led shifts skipped) and, where the browser supports it, `long-animation-frame`; a shift session over 0.1 or a frame of 200 ms or more goes to `POST /api/paint` in a throttled beacon, at most one every ten seconds. The server writes one `weave: paint {json}` line per report to its log: the score and px move, the selector of the largest moved element, the route with workspace names and ids stripped, viewport, engine family and build. No field values, names or emails reach it. The endpoint takes 8 KB at most, six reports a minute from one address and 600 a minute in all, and answers 404 on a local install (no `WEAVE_ORIGIN`). The privacy page names the report ([[Issue#1553]]). - The Terms of Service carry a Billing and Seats section between Account Terms and Cancellation and Termination. It states the free trial length weave runs (`TRIAL_DAYS`), that Editors and Architects take paid seats and Observers stay free, what happens at the end of the trial, how an Architect cancels, and the refund position Clerk Billing gives. The price points at the Billing page and checkout, so the terms print no number that can drift from the Clerk plan. The terms effective date moves to 2026-11-10 ([[Issue#1299]]). ## v0.4.131 (2026-11-10) - A create tray that stays open takes the next record while the previous write is still in flight. The pending-submit hold covers the dialogs that close on success; a create-and-stay tray hands its draft back as soon as its caller resets it, and the phone create sheet keeps its own guard: the name clears and Create goes quiet until the next name is typed ([[Issue#1445]]). - A credential field's kind, keystore and parts can be changed after it is created. `updateField` merges them through the key field's own validation, a schema document that edits them applies, and the field dialog's Holds and Kept in pickers now send what they show instead of dropping it ([[Issue#1500]]). - A create dialog now refuses its own second submit while the first write is in flight, so a double tap, a repeated Enter or two `requestSubmit()` calls make one record instead of two. The guard sits in the shared modal and tray shells, and a refused write re-enables the button so the create can be retried ([[Issue#1445]]). - A date field with no grain refuses a bare run of digits such as `12345`, which V8's `Date.parse` reads as the year 12345. The check for a four-digit year is anchored, so `12345` and `20260821` are refused while `Aug 31, 2026` is still kept as given ([[Issue#1529]]). ## v0.4.130 (2026-11-10) - On a phone, **Report a problem** now sits in a record's ⋮ menu, and the bug glyph sits in the Activity sheet's head, so a report can start from the screen that shows the problem (Issue #1023). - The **+ New** button on a record's related block now opens the create tray with an empty Name and a **Create & link** action, so no record is written before you name it (Issue #1474). ## v0.4.129 (2026-11-09) - The agent connection walkthrough opens the native workspace `/docs` setup page in a browser and reserves `/mcp` for the client connection (Issue #1528). - Architecture maps pin the shared main ancestor and reject branch-only revisions before rendering, so squash merges preserve their code citations (Issue #1507). - **Backups under the keychain store carry key names, never values** (Feature #213): when credentials live in the macOS keychain, `weave backup` writes a `keys.json` entry listing the key names and no `keystore.json`, and the summary line says so. `weave restore` reads that list and names every credential whose value this machine's key store does not hold, so each can be set again with `weave key set`. Under the file store a backup still carries `keystore.json` as before. With no `keystore.key` on disk, an encrypted or `weave://` backup now needs `WEAVE_BACKUP_PASSPHRASE` or `--passphrase-env`. `WEAVE_KEYSTORE=keychain` no longer reads as a key file path. Gate: `test/backup-keychain.test.mjs` greps the archive for a known test value and finds nothing. - Breadcrumbs and the toolbar stay above a record's banner image, and the banner sits directly below them, on records, tables and spaces (Issue #1538). - A table in a build spec takes `automations`: rules in the Workflows Script shape (`{name, trigger, actions}`, fields and states written by name, `on: false` to build one Off). `weave_build`, `POST /api/build` and `weave build` create each rule after the table's records, a dry run reports a rule that would read Setup incomplete at its path with the reason, and a re-run keeps a rule of the same name. The Work starter now ships a Done on date and one rule, built On, that stamps it when a task reaches Done (Feature #359). - CI browser jobs install Playwright's system packages from a cache of `.deb` files keyed on the runner image and the pinned Playwright version, so a slow Ubuntu mirror no longer times out the browser install step. The mirror is reached only when the cache is empty or incomplete, and a missing browser or package still fails the step (Issue #1438). - CLAUDE.md now says production follows the production branch through the promote workflow and that releases do not move it (Issue #1536). - A share that reaches a slide but not its deck now renders that slide's `deck.html` without the deck's chrome, style, format or space; a slide in several decks takes its frame from the first deck the share reaches (Issue #1453). - **A failed webhook or http-request is sent again** (Feature #311): a network error, a timeout, or an answer of 408, 429 or 5xx is retried after 1, 2, 4, 8, 16, 32 and 60 minutes, eight attempts in all. While a delivery waits the Workflows record reads Warning, and Health Reason names the attempt and the next try; a retry that lands returns it to Healthy, and after the eighth attempt it reads Failed. The steps after a webhook now wait for its answer, as they already did after an http-request. The waiting delivery is stored in the workspace, so a restart loses nothing, and the minute schedule tick sends it. A retried webhook keeps its X-Weave-Delivery id. Gate: `test/delivery-retry.test.mjs`. - A document save names the text it started from as `base` (`PUT /api/entities/:id/doc`, SHA-256 of the stored text), and the answer names the next base. A save from a text another tab or client already changed is refused with 412 and the stored record, so two tabs on one document no longer overwrite each other silently; the refused tab keeps its draft and offers Use mine. A refused document save (409, 412, 413) now stays queued and reads not saved, and Try again on the save mark sends it (Issue #1443). - **Failed Deliveries, replayable** (Feature #311): a webhook or http-request that runs out of attempts becomes a record of the new built-in `System/Failed Deliveries` table, holding the rule, the record that fired it, the request with any credential held by name only, each attempt and the last error. Setting its Status to Replay, `weave automation replay `, `POST /api/automations/deliveries//replay` or `weave_automations` with `action: replay` sends it again and runs the steps after it; the record then reads Delivered and the rule Healthy. Gate: `test/delivery-dead-letter.test.mjs`. - A test pins that a file id from a sibling workspace answers 404 and that an observer's PUT to an HTML file's storage answers 403 (Issue #1434). - `weave help` and `weave --help` build the registry commands from the command registry: each entry lists its command, positionals and flags with the first sentence of its tool description, so a new registry entry shows in help without an edit. Serve, service, backup, restore, forms and `doc export` stay hand-written beside them (Feature #179). - A date field with the full grain refuses a value with no four-digit year or a partial ISO date, such as `12`, `Aug 21` or `2026-08`, instead of storing the string as given; changing a text field to a date clears those cells (Issue #1499). - The agent-surface test names `auth.cookie` (OIDC start and callback, logout, `GET /api/auth/me`, `DELETE /api/auth/sessions/:ref`) as the one hand-written surface and states why: cookie and redirect plumbing that answers no single JSON result. A guard test keeps the list to that one row and no door keeps its own `pick` reader (Feature #179). - The import scaling check reads the fastest of three interleaved imports at each size, so one stall on a loaded CI runner no longer fails it while a superlinear import still does (Issue #1094). - **Origin tags on automation writes, and the engine halves of inbound idempotency and the echo guard** (Feature #311): every activity entry a rule writes carries `origin: {workflow}`. Two engine functions wait for the inbound webhook endpoint (Feature #227): `claimInbound(source, key)` acknowledges a delivery key it saw within 24 hours instead of running it again, kept in the workspace so a restart still knows it, and `isEcho(source, change)` answers whether an incoming change repeats values weave sent to that source in the last 10 minutes. Gate: `test/inbound-guards.test.mjs`. - Replacing an HTML introduction keeps the previous file accessible in Files without rendering its body a second time. Tables with introduction fields show loose HTML as file chips; other tables keep their existing inline previews (Issue #1522). - Single-file attachment fields can use `preview: "intro"` to show a sandboxed HTML introduction without the file bar or a repeated field heading. Source download, replacement and field settings sit in Introduction options. The existing table body order places it before fields (Issue #1514). - Tables and spaces can display covers and introductions from their System registry records. Covers remain native elements outside the HTML. Existing record layouts stay unchanged unless a table opts into introduction presentation (Feature #287). - SVG covers retain their original masks and full artwork. Their explicit image view permits styles while keeping scripts, external resources and same-origin access blocked; the raw file still downloads (Issue #1515). - Tables and spaces without an icon of their own show the default table or folder icon in the sidebar and the phone drawer, so every row lines up with the breadcrumb and with rows that set an icon (Issue #1394). - Picker toggle tests wait for queued saves to finish before checking stored dates and tags, preserving the existing persistence assertions (Issue #1516). - **Automations fire when a polled source changes** (Feature #312): a `listener-changed` trigger on a Workflows record reads a list endpoint through a System/Integrations key on the minute tick, compares the answer with the stored snapshot and runs the rule once per new or changed item, with the item as `trigger.item`. The first poll is the baseline and fires nothing. A cursor and an ETag go back to the source, a change weave itself just sent is an echo and does not fire, an unchanged source is polled less often down to the Script's `maxInterval`, the new integration column Poll Budget caps the polls an hour across rules, and a 429 waits for its Retry-After with the rule in Warning. Health Reason shows the last poll, the next poll and the items seen. The snapshot and cursor survive a restart, and the key is never stored with them. Gate: `test/automation-listener.test.mjs`. - Production promotes as soon as a main commit's `tests` and `dev-soak` runs both succeed, keeps the hourly schedule as recovery, and alerts when a promotion lags its commit by more than one batch (Issues #1535, #965). - Public documentation gains native full-logo banners, introductions above fields, and source-managed guides for first steps, linked project work, agent connection, and reading releases (Feature #360). The pilot uses existing Showcase examples and native schema-only templates. v0.4.128 gains an introduction linked to maintained guides while retaining its original release notes. Presentation sync replaces only its own attachments and descriptions, preserves independent edits, and leaves Issue, Feature, Suite and Case schemas and records unchanged. - A new record takes its public id inside the write transaction from the stored rows, so an automation that runs on create quotes the id the record keeps, even when another writer added a record first. Opening a workspace that holds duplicate public ids still renumbers the later duplicate, and now writes a `public-id-renumbered` audit line naming the table, the record and its old and new ids (Issue #1455). - The 50,000-row lookup-sort query check compares the sorted first page with the unsorted one measured in the same run, so a loaded CI runner no longer fails it while a sort that walks every row again still does (Issue #1511). - Accounts, identity links, invites and the workspace subcommands left in the CLI run from the command registry: `weave account`, `weave invite`, `weave workspace require-auth|link-preview|leave`, the `weave_accounts` actions and `/api/accounts`, `/api/accounts/:ref/identities` and `/api/invites` dispatch from one entry each with the same answers. A registry command may have no HTTP door when its route lives elsewhere (Feature #179). - Record create, read, update and delete run from the command registry on every door: `weave create|get|update|delete`, their MCP tools and `POST /api/tables//entities`, `GET|PATCH|DELETE /api/entities/` dispatch from one entry each. HTTP keeps the caller's zone, `If-Match` and its 412 with the record, `affected` on PATCH and `ok` on DELETE; MCP keeps its values check and the Users-record refusal; every create door still hands back the one-time invite (Feature #179). - Documents, their revisions and comments run from the command registry on every door: `weave doc get|set|append`, `weave doc-revisions`, `weave doc-restore`, `weave comment [delete]`, `weave_get_doc`, `weave_set_doc`, `weave_doc_revisions`, `weave_doc_restore`, `weave_add_comment`, `weave_delete_comment` and `/api/entities//doc`, `/doc/revisions[/[/restore]]`, `/comments[/]` dispatch from one entry each. Each door keeps its own answer: `GET .../doc` still answers `{field, doc}`, `weave_set_doc` still answers its length, a comment from an observer still carries the caller's name (Feature #179). - File attach, read and delete run from the command registry on every door: `weave file [attach|read|delete]`, `weave_attach_file`, `weave_files` and `/api/entities/[/fields/]/files`, `/api/files/` dispatch from one entry each, with the same answers, ETag and 304 behavior as before (Feature #179). - JSON export and import run from the command registry on every door: `weave export [--out]`, `weave import --file`, `weave_export_json`, `weave_import_json`, `GET /api/export` and `POST /api/import` dispatch from one entry each. The CLI and HTTP export still carry attachment bytes and MCP still leaves them out unless `blobs` is true; only an architect still replaces the workspace over HTTP, and the role gate reads the import as a schema write from the registry (Feature #179). - Query, trash, stats and bulk run from the command registry on every door: `weave query|trash|stats|bulk`, `weave_query`, `weave_trash`, `weave_stats`, `weave_bulk`, `POST /api/tables/
/query`, `GET /api/trash`, `GET /api/tables/
/trash`, `GET /api/tables/
/stats` and `POST /api/bulk` dispatch from one entry each. Each door keeps its own answer: the CLI query reads relations in full, MCP and HTTP as chips; `GET /api/trash` still carries views and purge; a `where` that is not JSON is still a 400 on stats; an editor is still refused a bulk write to a system table (Feature #179). - Link, unlink, workflow state and restore run from the command registry on every door: `weave link`, `weave unlink`, `weave state`, `weave restore`, their MCP tools and the `/api/entities//link|unlink|state|restore` routes dispatch from one entry each, and the HTTP answers still read the record in the caller's zone (Feature #179). - The schema and build commands run from the command registry: `weave schema`, `weave schema export`, `weave schema apply` and `weave build`, the tools `weave_schema`, `weave_apply_schema` and `weave_build`, and `GET` and `PUT /api/schema` and `POST /api/build` dispatch from one entry each with the same answers, and an editor is refused the writes by the registry's `writes: schema` (Feature #179). - Search and undo run from the command registry on every door: `weave search`, `weave undo [list]`, `weave_search`, `weave_undo`, `GET /api/search`, `GET /api/undo` and `POST /api/undo` dispatch from one entry each. Each door keeps its own answer: the CLI search still runs the record search, MCP and HTTP the universal one; `weave_undo {list}` still answers `{history}`, the CLI and HTTP the bare list. `weave undo list` is new and lists like `weave undo --list` (Feature #179). - `GET /api/share`, the share token's own read, dispatches from the command registry with the same answer and the same 404 without a token (Feature #179). - The share family runs from the command registry on every door: `weave share [list|mint|renew|revoke|update|regenerate]`, `weave_shares` and `/api/shares[/[/renew|/regenerate]]` dispatch from one entry each. HTTP still adds each grant's link, `weave_shares list` still answers `{shares}`, and only an architect still manages shares. `GET /api/share`, the token's own read, stays hand-written (Feature #179). - Space list and space members run from the command registry: `weave space [list|members]`, `GET /api/spaces` and `/api/spaces//members` (read, set, remove) dispatch from registry entries with the same answers as before (Feature #179). - Table list and table views run from the command registry: `weave table [list|view]`, `weave_table_view`, `GET /api/tables` and `/api/tables/
/views[/]` (read, write, delete) dispatch from registry entries with the same answers as before (Feature #179). - Template list and template use run from the command registry: `weave template [list|use]`, `weave_template_list`, `weave_template_use`, `GET /api/templates` and `POST /api/spaces//use` dispatch from registry entries with the same answers and refusals as before (Feature #179). - The saved view family runs from the command registry on every door: `weave view [list|get|create|delete|restore|share|unshare]`, `weave_views` and `/api/views[/[/restore|/share]]` dispatch from one entry each. `weave_views list` still answers `{views}`, the CLI and HTTP the bare list; sharing a view still takes an architect on MCP and HTTP. The CLI's unknown-subcommand hint now lists the subcommands in the MCP order (Feature #179). - The vocabulary lookup runs from the command registry: `weave vocabulary`, `weave_vocabulary` and `GET /api/vocabulary` dispatch from one entry with the same answers and the same refusal of an unknown section (Feature #179). - The workspace record, logo and rail order run from the command registry: `weave workspace get|set|logo|order|move`, the `weave_workspace` actions and `GET` and `PATCH /api/workspace`, `PUT` and `POST /api/workspace/logo`, `GET /api/workspace/logo`, `DELETE /api/workspace/logo`, `GET /api/workspaces` and `PATCH /api/workspaces/:ref` dispatch from one entry each with the same answers (Feature #179). - The editor and observer gate reads each route's `writes` from the command registry for every route the registry serves: the `/api/spaces`, table view, tables query and spaces use patterns are gone. A read-only registry command (the formula check) is now open to an observer, as its `writes: none` says (Feature #179). - **A save glyph at the right of the header** (Feature #217): while a record edit waits in the save queue the header shows a clock, then a tick for two seconds once every edit has landed, and a red alert when a write failed twice and is kept. A tap on it, or Escape, sends every waiting edit at once. It is hidden when nothing waits, names itself ("Saving 2 changes. Save now", "1 change not saved. Try again") and is a 44 px target on a phone. Gate: `test/save-glyph-browser.test.mjs`. - **Grid cells and record fields save through the save queue** (Feature #217): every edit made in a table cell or a field on the record page or dock goes into the save queue instead of its own PATCH. Four quick edits show one clock in the header and no toast, land as four PATCHes in the order they were made, then the tick. With the server unreachable the edit is retried once, the cell keeps the typed text, the error toast still says "was not saved", the glyph turns red, and a tap on it once the server answers lands the edit. The page going hidden with a field still focused, as iOS leaves it, commits that field and sends it within a second. Cmd-Z and Undo send any waiting edit before they undo, so they undo the edit just made, as before; the activity log and the "changed first" conflict toast read as before. Gate: `test/save-queue-cells-browser.test.mjs`. - **Documents save through the save queue** (Feature #217): a document edit goes into the same save queue as cell and field edits, with the same 600 ms wait, so the header glyph shows a document save as it shows any other. The small mark beside each document (·, ✓, !) is gone; a document write that fails twice keeps the draft, shows "Your draft is kept and saves on your next edit or when you leave", turns the header glyph red, and sends again on a tap, on the next edit or when the page goes hidden or away. Gates: `test/doc-save-failure-browser.test.mjs`, `test/doc-autosave-phone-browser.test.mjs`. - **http-request takes signWith, and every credentialed call carries a timestamp** (Feature #311): an `http-request` action that names a keystore credential in `signWith` is signed with the same `X-Weave-Signature`, `X-Weave-Timestamp` and `X-Weave-Delivery` headers as a webhook; one that goes through an integration carries `X-Weave-Timestamp` and `X-Weave-Delivery` beside its key. The Automations guide's new "Signed deliveries" section gives receivers the replay protection: a five-minute timestamp window and the delivery id, which every retry and replay repeats. Gate: `test/delivery-signing.test.mjs`. - **Starters for agents** (Feature #244): `weave_template_list` now answers `starters` beside the template spaces: every built-in starter's id, title and blurb, and for the four featured starters (Money, Work, People, Notes) a `grows` note on how the starter is meant to grow. `weave_template_list {starter: ""}` answers `{starter, spec}`, a spec dated this month that `weave_build` takes as is, so an agent can pick a starter before it improvises. Gate: `test/starter-core.test.mjs`. - **Templates gallery** (Feature #244): a Templates link in the sidebar, under + New space, opens `#/templates`, a gallery of every starter grouped by category. An architect picks one to add its space of tables and sample records to the workspace already in use, and lands on its first table. A starter whose space the workspace already holds shows "In this workspace" and opens that space instead of building it twice. Editors and observers see the gallery without the build. The page loads `public/starter-gallery.js` on demand. Gate: `test/starter-gallery-browser.test.mjs` (both themes, 375 px). - Between 601px and 900px the workspace rail leaves the shell, so the menu button is the only way into navigation and the peek drawer brings the rail back beside it (Issue #1457). - The drawer that carries the rail between 601px and 900px leaves it live, so its workspace marks still take a click and a Tab reaches them (Issue #1546). - The record title and view title spellcheck only while focused, so a read-mode title no longer shows a red underline on a phone (Issue #1405). - The phone touch gate walks the menu again: with every table and space drawing an icon, the space row clears the 44px minimum, so the hold for Issue #1163 and its two `test/touch-held.json` entries leave the ledger (Issue #1540). - The phone touch gate's offender ledger `test/touch-held.json` now names the platform last in its key wherever the hold in `test/lib/states.mjs` names one, so hit-area debt that only one platform's font metrics show can be held without reddening the other runner, the way `test/cutoff-baseline.json` already holds its counts per platform (Issue #1542). - A record update or create that is refused partway, such as a relation change into a private space, now leaves every field, linked record, document and activity entry as it was, and its automations and document revisions run only once every value has been written (Issue #1454). - Automations: a `webhook-inbound` trigger on a Workflows record. Saving the rule mints `POST /w//api/hooks/`; `weave automation secret` shows its secret once (`--rotate` mints a new one). A POST is admitted with an `X-Weave-Signature` over `.` and a timestamp within five minutes, the scheme weave's signed webhooks send, or with the secret as `X-Weave-Secret`. Bodies over 64 KB answer 413, a redelivered key answers 200 and runs nothing, a change weave just wrote to the sender (`from`) is dropped, and an Off rule answers 202. The body is `trigger.body`, each item of `items` is `trigger.item`, and `record` names the record set-field and add-comment write to. Every delivery is logged on the rule with its time, status, size and outcome, and the last one shows in Health Reason (Feature #227). ## v0.4.128 (2026-11-09) - **Automation pills on the relation map load with the first automation** (Issue #1041): `AUTO_ACTION`, `automationTrigger` and `drawAutomationPills` moved out of `public/app.js` into `public/automation-map.js`, fetched only when the map has an automation to draw. A table, row or home load in a workspace without automations no longer downloads them. A failed fetch shows "Automations did not load" and the map still draws its tables. Gate: `test/automation-map-lazy-browser.test.mjs`. - A picture dropped on a board card whose table has no cover field now uploads through the prepared raw upload once Add a Photo field creates the field, so the last browser upload path no longer sends the file as base64 inside JSON (Issue #1428). - Automations run from the command registry on every door: `weave automation`, `weave_create_automation`, `weave_automations` and `/api/automations` dispatch from one entry each, and `GET /api/automations/list?db=
` reads the rules as records over HTTP, the way `weave automation list` and `weave_automations {action: "list"}` already did (Feature #179). - Keys run from the command registry on every door: `weave key`, `weave_keys` and `/api/keys` dispatch from one entry per action, and `weave_keys` still refuses `reveal`. `weave key ` now lists its subcommands as list, set, reveal, share, unshare, delete. The cross-workspace probe and the route census now cover every registry route (Feature #179). - **The document editor glue loads with the editor** (Issue #1041): the slash menu, reference hints and chips, toolbar bubble, list, table and Enter keys, uploads, code block toggles and `mountDocEditor` moved out of `public/app.js` into `public/doc-editor.js` (42,283 B raw). `loadVditor` fetches it beside Vditor, so no route parses it before first paint; saves, dash rails, heading folds and teardown stay in `app.js` because a record page paints them at load. A failed fetch falls back to the source box with "The editor did not load". Gate: `test/doc-editor-lazy-browser.test.mjs`. - **Automation action `http-request`** (Feature #310): a Workflows rule calls an outside API with a method, a url whose path and query take templates, templated headers and a text or JSON body, and maps paths of the JSON answer onto fields of the firing record. `integration` names a System/Integrations record and the request goes through `integrationFetch`, so the key never enters the Script, the activity entry, the step output or the `key-used` audit entry. A url host that is not the integration's Host reads Setup incomplete, and the send checks again. With no integration the request is unauthenticated and passes the webhook address guard. The step output `{status, ok, body}` feeds later steps as `{{steps.N.body.id}}`; a non-2xx answer, a timeout or a refusal stamps Health Failed and stops the run. The automation engine gains `automationsSettled()`, which waits for runs still waiting on an answer. Gate: `test/http-request-action.test.mjs`. - **`weave key migrate --to keychain` and `--to file`** (Feature #213): moves every credential, its owner and its sharing between `keystore.json` and the macOS keychain, reads each value back from the new store, and only then deletes the old one (`keystore.json` and `keystore.key`, or every `weave:` keychain item). A value that does not read back stops the move with nothing deleted. A move weave would not follow is refused before anything moves: on a Mac the default `~/.weave/keystore.json` follows the move by itself, and any other path needs `WEAVE_KEYSTORE=keychain` or `WEAVE_KEYSTORE=file` set for the move and for every weave that reads the keys. The move is a local operator act: MCP refuses it, and HTTP refuses an account. A running server picks the store on every key read, so it follows a move made from the CLI without a restart. Gate: `test/key-migrate.test.mjs`. - **Credential values have a key store behind them** (Feature #213): `src/keystore.js` holds two stores behind the engine's key API. `file` is today's `keystore.json` and `keystore.key` pair, unchanged on disk, and stays the default on Linux, in CI and wherever a key file already exists. `keychain` keeps one macOS keychain item per key, named `weave::`, plus a `weave:keystore` item holding names, owners and sharing; it is the default on a Mac with no key file. `WEAVE_KEYSTORE=file` or `WEAVE_KEYSTORE=keychain` picks one by hand, `WEAVE_KEYCHAIN` names a keychain file, and `WEAVE_KEYSTORE` set to a path still means the file store at that path. Values reach `security` on stdin through `security -i`, never in a process argument list. Gate: `test/keystore.test.mjs`, one suite run against both stores (the keychain cases run on macOS against a throwaway keychain). - Bold words in a launcher About description stay inline with the sentence around them instead of each breaking onto its own line, on the workspace and the space About panes (Issue #1440). - **The ontology lists an automation as a record of System/Workflows** (Feature #249): `weave ontology --depth glossary` and docs/ONTOLOGY.md move Automation out of the machinery and into a new records group, with System/Workflows as its registry. Gate: `test/ontology.test.mjs`. - **One left edge for every phone field value** (Issue #1381): on the phone record page the Due date sat 7 px right of the chips beside it, because a date field puts its input inside a `.date-cell` wrapper and so missed the inset every other value kind carries. One rule now names each value kind that holds an input, so numbers, dates, plain text and chips all start at the same edge. Every normal field row is also a uniform 56 px with 4 px of block padding inside its borders, which keeps a 44 px control off the hairline, leaves the row's label a 44 px tap target, and keeps a focused input's ring clear of the divider instead of growing onto it. Gate: `test/phone-matrix-record-browser.test.mjs`. - A scheduled `promote` run now acts once the last production promotion is a batch old (`PROMOTION_EVERY_HOURS` less half an hour), counted from the newest `production` deployment. It used to skip any run GitHub delivered in an odd UTC hour, and GitHub delivers the hourly schedule late, so production could lag main by half a day (Issue #1493). - Audit and activity run from the command registry on every door: `weave audit`, `weave activity [id]`, `weave_audit`, `weave_activity`, `GET /api/audit` and `GET /api/activity[/]` dispatch from one entry each, and every door keeps its own default page (Feature #179). - CSV export and import run from the command registry on every door: `weave csv`, `weave_export_csv`, `weave_import_csv` and the `export.csv` and `import.csv` routes dispatch from one entry each. Export still prints raw CSV on the CLI and answers `text/csv` over HTTP (Feature #179). - The registry report and rebuild and the relation map run from the command registry on every door: `weave registry [report|rebuild]`, `weave map`, `weave_registry`, `weave_relation_map`, `/api/registry`, `/api/registry/rebuild` and `/api/relation-map.mmd` dispatch from one entry each. The HTTP role gate reads the rebuild as a schema write from the registry instead of a path pattern (Feature #179). - **The relation map loads where it is drawn** (Issue #1041): `relationMapView`, `mapCard`, `showMap` and `mapAutomations` moved out of `public/app.js` into `public/relation-map.js`, and `public/relmap-layout.js` is no longer a script tag on every page. The workspace home, an opened space overview, the phone space launcher and `#/map` fetch both files on first draw; a table, board or record page fetches neither. Both files are named in a `#lazy-assets` template in `public/index.html`, so `lazyScript` loads them by their versioned names and a warm home load revalidates neither. A failed fetch shows "The relation map did not load" and the rest of the page paints. Gate: `test/relation-map-lazy-browser.test.mjs`. - **One save queue for record edits** (Feature #217): `public/save-queue.js` holds the queue every editor will push `{ entityId, field, value }` into. It waits 600 ms after the last edit to a field, sends the writes one at a time in the order they were made, retries a lost connection or server error once, keeps an edit that still fails until the next flush, and reports how many edits are pending. The page going hidden or away (`visibilitychange`, `pagehide`, the event iOS Safari fires) sends every waiting edit at once. Gate: `test/save-queue.test.mjs`. - **A schema document keeps every field setting it describes** (Issues #1494, #1495, #1496, #1497, #1498): `applySchema` now carries a text field's literal setting, a toggle's on and off labels and a credential field's keystore and pair parts, so `applySchema(describeSchema(x))` rebuilds them instead of falling back to the defaults. An edit to a text literal, a toggle label, an avatar shape or a cell colour in a schema document now plans an update and lands; before, it applied as a no-op. Gate: `test/schema-mirror.test.mjs`. - Settings names each trashed line by what it holds (Space, Table in its space, Record in its table) where it used to show the registry table behind it, and its note lists tables and spaces beside records and share pages. Delete forever on a table or a space purges it, a space with every table in it, after the exact name is typed and the hold runs out (Feature #160). - **Money's Months sorts by date** (Feature #244): the Months table in the Money starter carries a month-grain Date field and its standard view sorts on it, so October sorts above November instead of below it by name. The sample month is dated to the first of the month it is built in. Gate: `test/starter-core.test.mjs`. - **Notes starter** (Feature #244): a fourth featured starter, Notes ("Notes filed by topic, newest first"), joins Money, Work and People on onboarding step 3 and the first-run empty state. It builds a Notes table (Topic, Date, sorted newest first) and a Topics table that counts its notes, with three sample notes under two topics. The note text lives in each row's Description document. Gates: `test/starter-core.test.mjs`, `test/onboarding-browser.test.mjs`, `test/first-run-browser.test.mjs`. - Trash names each line by what it holds: a trashed space reads Space, a trashed table reads Table in its space, and a record reads Record in its table, where the column used to show the registry table behind it (System/Spaces, System/Tables). Restore on a space or table line brings it back to the sidebar in one click, and a table inside a trashed space says to restore the space first. The table and space menus gain Open trash, beside the Trash row in the sidebar (Feature #160). - **Workflows Health reads as a coloured state with its reason** (Feature #249): the Health cell is the engine's own read-only chip, green Healthy, red Failed, slate No runs, and a Failed row shows its Health Reason on hover, on click and on a tap on a phone, where Health leads the row. Gate: `test/workflows-health-browser.test.mjs`. - **The Workflows table reads as the automation control panel** (Feature #249): its Standard view shows On, Name, When, Then, Health and Last Run. When names the table and the trigger in words and Then lists the actions in words, both written by the engine, so you can see what a rule does without opening its Script. Gate: `test/workflows-panel.test.mjs`. - **A Workflows row that is not set up cannot be switched on from the grid** (Feature #249): while a row is Setup incomplete and Off, its On toggle is greyed and names what is missing on hover, click or tap. A row that broke while On keeps a live toggle so you can switch it off. The engine now keeps the setup reason in Health Reason for an Off row too. Gate: `test/workflows-toggle-lock-browser.test.mjs`. ## v0.4.127 (2026-11-09) - The accessibility gate now runs every axe rule tagged `wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa` or `best-practice` instead of five named rules, walks the phone width band as well as the desktop one, carries the width band in each baseline key, and proves itself on a planted unlabelled button. The 82 violations the wider scan found are baselined against Issue #1492 (Issue #1449). - **Streamed uploads for a picture dropped on a card** (Issue #1486): streamed attachment uploads (Issue #1428) landed and were reverted the same night, because `test/board-cover-drop-browser.test.mjs` still waited for the `POST /entities//fields/Photo/files` that a board drop no longer sends. A picture dropped on a card reaches the same upload control as the field editor, so it streams through `POST /api/uploads` and `PUT /api/uploads` like every other browser upload, and the three sibling drop suites were moved to that pair while this one was missed. The case now waits for the prepare that names the cover field and for the body PUT that answers 201, and still asserts that the file lands on the row and the tile repaints with it. Gate: `test/board-cover-drop-browser.test.mjs`. - The `security scan` check no longer fails the whole repository when a GitHub release download answers 504: the scanner downloads retry across 120 seconds, and a download that still fails is annotated as a network failure rather than as a finding in the code (Issue #1490). - A sheriff flake line now names the step the shard died in, so a browser install timeout is told apart from a test that crashed its shard, and an install failure never adds a line to a per-file flake row (Issue #1353). - **Browser attachment uploads stream to disk instead of blocking the server** (Issue #1428): a 9.5 MB attachment sent as base64 inside JSON held the event loop for 181 ms while the server parsed the body, decoded it and wrote the file with a synchronous fsync, stalling a parallel health check by 126 ms. The browser now prepares a bounded upload with `POST /api/uploads` and sends the file as the raw body of `PUT /api/uploads`. The server writes that body chunk by chunk to a staging file in `uploads/` beside the workspace, fsyncs it with the async API and renames it into `files/` before the record commits. A small preview rides along in the grant. An upload whose connection ends early answers `incomplete-upload` and reads in the browser as a lost connection, so a cut upload still offers Retry. The JSON attach routes stay for MCP and API callers, avatar fields keep taking their bytes inline, and the 10 MiB limit now counts the file itself, without the base64 overhead. Gate: `test/upload-stream.test.mjs`. - A held phone touch-target state now has to miss exactly the hit areas `test/touch-held.json` records for it, so a second undersized control on a held surface fails the gate where a bare offender count passed one failure or a hundred. Each entry is a fingerprint naming the control and the axis that misses, never its pixels, so the gate reads the same on a developer's machine and on the CI runner. A planted undersized control proves it, and a held surface that comes clean still asks for its hold to be dropped (Issue #1450). ## v0.4.126 (2026-11-09) - **The link palette's + New opens the create tray** (Issue #1473): choosing + New in the link search wrote a record named from whatever you had typed, with no chance to name it properly or fill another field. It now opens the right-hand create tray on the typed name, with a Create & link button; the record is written when you press it. Gate: `test/link-search-browser.test.mjs`. - **The phone + creates in the tray** (Issue #1471): tapping + on a phone opened a bottom sheet whose table chip opened a popover, so starting a row looked nothing like starting one anywhere else. It now opens the right-hand create tray, named for the table's own row word, carrying the Name field, the table's choice chips and, when no table is in context, the table chooser inside the tray. Create still keeps the tray open on a phone for the next row, and the tray now stays above the on-screen keyboard. Gate: `test/phone-new-row-sheet-browser.test.mjs`. - **Roll up names its new record in the tray** (Issue #1472): rolling a selection up through a relation opened a small popover over the grid to name the parent. It now opens the right-hand create tray with the same Create & link button, so every create surface looks the same. Gate: `test/row-selection-browser.test.mjs`. ## v0.4.125 (2026-11-08) - A `now()` or `today()` date default reads the creator's wall clock: a floating field stamps the time in the zone the creating request names in `X-Weave-Zone` (UTC when it names none), and a fixed field stamps the time in its own zone. A row created at 21:41 UTC from New York now stores 16:41 instead of 21:41 (Issue #1480). ## v0.4.124 (2026-11-08) - A record update that names several fields checks every value before it changes any of them, so a rejected update leaves the record as it was in memory and on disk instead of keeping the fields that came before the bad one (Issue #1454). - **AVIF photos show on board cards and open in place** (Issue #1212): an AVIF upload was served as a download (`application/octet-stream`) and the board's picture rule skipped it, so a Shopping List card with an `.avif` photo drew the empty cover. The file route now recognises AVIF bytes and serves them inline as `image/avif`, and a board takes an AVIF file as a cover picture. Gates: `test/attachments-preview.test.mjs`, `test/board-layout.test.mjs`. - **Hiding a field on a board keeps the board** (Issue #1213): unticking a field in the View pane's Fields section redrew a board view as a table until the next reload, because the instant repaint drew the grid. The repaint now draws the board without the field. Gate: `test/board-layout-browser.test.mjs`. - **A board view loads its code with its rows** (Issue #1041): `renderBoard`, the card compare view and their helpers moved byte for byte out of `public/app.js` into `public/board-view.js` (20,745 B raw, 6,270 B gzip served). A board view fetches the file in parallel with its rows, and a record page fetches it before painting a related board. A cold home, grid table or row load no longer downloads it, and the `/app.js` gzip budget drops from 173,580 to 165,613. A failed fetch paints the route error with "The board did not load". Gate: `test/board-view-lazy-browser.test.mjs`. - **Finished logo and loader review sheets leave the repo** (Issue #652): the six `brand/docs/round*.html` review pages from August and September and `brand/build-loader-sheet.mjs`, which only wrote the loader sheet, are deleted. The logo and loader both settled; git history keeps every round. 203,771 bytes and 1,511 lines removed. - **Main runs eight browser shards at once** (Issue #1351): four shards had grown to 15 to 19 minutes each and were killed at the test manager's 20-minute job timeout on six of seven retried main runs in the week to 2026-11-08. The full suite now plans eight browser shards and runs all ten shards in parallel. Gate: `test/ci-test.test.mjs`. - **Timing budgets move to a trend lane** (Issues #1237, #1265, #1293, #1309): the vitals and feedback suites time pages in milliseconds on shared runners and turned main red on runner noise, so they no longer gate PRs or main. A `budgets` workflow runs them on every main push and alerts after three consecutive breaches. The budget JSON files, their raise-reason checks and the click and render budgets stay on the gate. Gate: `test/ci-test.test.mjs`. - **The standalone chip and card anatomy export is gone** (Issue #653): `docs/chip-card-anatomy.html` copied the Handbook guide "Chip and card anatomy" into a page nothing linked to, and a drift test turned every chip CSS change red until someone regenerated it. The guide stays the one copy. The chip CSS extractor the two mockup exporters share moves to `scripts/chip-css.mjs`. 35,797 bytes of HTML and 2,312 bytes of export code removed. - **One command registry for the CLI, MCP and HTTP** (Feature #179): `src/commands.js` holds one entry per operation with its tool definition, CLI command, HTTP route, what it writes and the one function that runs it; `weave`, `tools/list` and the API dispatch from it. The field writes (`add`, `update`, `rollback`, `delete`, `restore`), `relation add` and `formula check` moved first. Every door now answers those the same way: the API's `DELETE /api/tables/:id/fields/:id` returns the trashed field beside `ok: true`, and `weave field update` (and `weave_update_field` with `verbose`) add the `activity` and `lossy` the API already sent. `weave query --offset nope` is refused with exit 40 (Issue #1374). Gate: `test/command-registry.test.mjs`. - **Calendar cells share one base rule** (Feature #177): the day cell and the month or year cell in the date picker restated the same border, radius, colour, font, hover and selected rules. They now share `.date-day, .date-pick-cell` rules and differ only in height (28 px and 34 px). Nothing changes on screen. Gate: `test/date-cells-css.test.mjs`. - **The field dialog's Clock options read as the clock alone** (Issue #1477): on a date field that stores a time of day, a format with spaces in it (long, month, ordinal, quarter) leaked part of the date into the Clock control, so the options read `8, 2026 14:30` and `8, 2026 2:30 PM`. Each option is now labelled from the clock reading alone, `14:30` and `2:30 PM`, whatever the date format. Gate: `test/date-clock-labels-browser.test.mjs`. - A share link's `deck.html` renders only the slides the share reaches. A share that selects a deck but not its slides no longer shows the unselected slides' content; a space or table share that holds the slides still renders all of them (Issue #1453). - **Document decorations live in one registry** (Feature #177): the toolbar bubble, reference chips, code auto-expand, code source toggle, outline rail and heading folds each kept their own set, and three places listed the sets by hand. Scheduling, editor teardown and dock release now walk one `DOC_DECOR` list and drop each state through `dropDecor(st)`. Closing the dock now also stops the code source toggle of a document shown there, which the hand-written list had missed. Gate: `test/editor-refs.test.mjs`. - An open reference chip in the document editor takes its own room in the line and pushes the words after it along, where it used to draw over them. It stays open, with focus on its caret, when the editor redraws its chips. In prose its fields and table badge stand apart instead of running together, and a chip wider than its line keeps its badge and caret in view, so it can always be closed (Issue #1415). - A document save the server refuses keeps the draft. The section shows `!`, the toast says the draft is kept, the editor shows the draft again when you reopen the record, and the next edit, flush or page leave saves it. Saves of one document now reach the server in the order you typed them, so a slow older save can no longer overwrite newer text (Issue #1443). - No access to a workspace now offers Find your workspaces, offers Start your own workspace whenever open sign-up is on, and keeps the login held, so a person without an invite leaves the sign-in loop (Issue #1331). - Your access was removed now offers Find your workspaces, which lists the person's other workspaces (Issue #1336). - An invite opened with a login that is already a member leads with Open workspace (Issue #1337). - **One error code on every door** (Feature #179): a refused call now names the same `code` over HTTP, MCP and the CLI. MCP tool errors add `structuredContent: {error, code, rpc}` beside the `Error:` text, and the CLI prints `weave: error []: ` and exits with the code's number (44 not found, 45 conflict, 46 forbidden, 40 invalid; anything uncoded still exits 1). `src/errors.js` holds the table and AGENTS.md prints it. Gate: `test/error-contract.test.mjs`. - Opening a file you have already viewed no longer re-reads and re-hashes it: file links are cached for a year under the file id, and a revalidation answers 304 without loading the file (Issue #1430). - **Files sit directly under Fields on the record page** (Feature #357): a record's Files card used to render after every document, so on a row with a long Description an attached report sat a long scroll away. The card now follows the Fields block, the Deck or Slide preview follows the card, and the documents come after both, on desktop and phone. A row without loose files draws no Files card, as before, and a table whose body order already places the card keeps that place. Gates: `test/entity-files-order-browser.test.mjs`, `test/ui-contract.test.mjs`. - **Attachment chip links pass contrast in the light theme** (Feature #357): with the Files card in the first screen, axe measured its chip link at 3.98:1 (`#066fd1` on `#e2e6ec`). Chip links now use the darkened primary in the light theme, about 4.9:1. Gate: `test/a11y-browser.test.mjs`. - **Four browser suites wait on the state they assert instead of racing it** (Issues #1209, #1135, #1304, #1111): the phone list chips suite waits for fonts and two frames, the point at which the app re-fits chip lines, instead of a fixed 200 ms; the WebKit math showcase ignores the benign "ResizeObserver loop completed" notice, which is not a rejection; the desktop Activity pane is measured at rest at 1440 px, where the row has slack for its 340 px basis; and the forms suite waits for an edit to land before reading it back. Gate: the suites themselves. - **Small ghost buttons come from one builder** (Feature #177): twelve buttons across `public/app.js`, `public/command-palette.js` and `public/field-dialog.js` spelled out `btn btn-sm btn-ghost-secondary tiny` by hand (job retry and dismiss, the whiteboard button, remove photo, attachment remove and upload, link search, table search clear, the appears-in gear, unlink, copy link, remove automation target). They now build through `ghostBtn(attrs, ...children)`, which takes any extra class. Markup and behaviour are unchanged. Gate: `test/ghost-btn.test.mjs`. - **The Handbook pages are markdown files** (Issue #655): the 55 field, guide and formatting-sample pages that sat in template literals in `src/handbook.js` now live as `src/handbook/fields/*.md`, `src/handbook/guides/*.md` and `src/handbook/samples/*.md`, read when the module loads. Every page is byte-for-byte the text it was, so `handbookHash` is unchanged and no workspace re-syncs its Guide or Fields table. The three counts the MCP guide quotes (core tools, all tools, field types) are filled in from the code at load. `src/handbook.js` drops from 3,367 lines to 439. - **Links in a previewed HTML file open in a new tab** (Issue #1475): a plain link inside an attached HTML file navigated the preview frame itself, the target site refused to be framed, and the panel went white. The `?view` route now puts `` after the doctype, so every link opens a new tab and the panel keeps the file. Links to a spot on the same page (`#section`, `#`) and `javascript:` links still run in place, and a page that sets its own `` keeps it. Gates: `test/attachments-preview.test.mjs`, `test/html-preview-links-browser.test.mjs`. - **The unsent invite-accepted email is deleted** (Issue #656): `inviteAcceptedEmail` in `src/mail.js` was built and previewable at `GET /api/mail/preview/accepted` but no code path ever sent it. Sending it needs the inviter's address, which an invite does not keep (it records the inviter's account name), so the email, its preview and its tests go. The invite and trial emails and their previews are unchanged; `/api/mail/preview/accepted` now answers 404. - **The Invite dialog loads when opened** (Issue #1041): `inviteDialog` and `seatNote` moved byte for byte out of `public/app.js` into `public/invite-dialog.js`, fetched on the first press of Invite. A cold home, table or row load no longer downloads them (4,943 B raw). A failed fetch shows "Invite did not load". Gates: `test/settings-view-lazy-browser.test.mjs`, `test/invites-browser.test.mjs`. - The workspace MCP door page and the Connect an agent guide link Sign in, and Create an account when open sign-up is on, so a browser that lands there without an account has a way in (Issue #1338). - Left nav: drag a space to reorder the spaces, and a table to reorder it inside its space; the space travels with its tables. A table never changes its space by drag, since that changes who can see it: Move to space… in its ⋮ menu stays the one way (Feature #350, Feature #284). - **A bad API offset is refused by name** (Issue #1374): `?offset=nope` or `?offset=-1` on `GET /api/audit`, `GET /api/tables/:id/entities` and the third paged route answered a silently empty page; each now answers 400 `offset must be a whole number of rows, not 'nope'`. A missing offset still starts at the first row. Gate: `test/route-limit-number.test.mjs`. - **weave's own model is served** (Issue #654): `weave_ontology {depth: "glossary"}`, `weave ontology --depth glossary` and `GET /api/ontology?depth=glossary` answer the `ONTOLOGY` record (the entity, its levels, what each is made of and the other names for them) that only the tests read before. `weave_ontology` is now one registry entry for all three doors. Gate: `test/command-registry.test.mjs`. - **The PDF font moves out of the web root** (Issue #659): `DejaVuSans.ttf` (757,076 bytes) and its licence move from `public/vendor/fonts/` to `src/vendor/fonts/`, next to `src/pdf.js`, its only reader. No page loads it, and `/vendor/fonts/DejaVuSans.ttf` is no longer a static URL. The in-tree PDF writer stays: at 16,997 bytes it is a 75th the size of pdf-lib with fontkit (1,283,539 bytes), and every library measured still needs the same font. - A phone card's title line now reads #id, state chip, name, in that order. The field chip line starts under the state chip instead of at an inset of its own, and sits 24px below a one-line title instead of 26px, the closest it can get while the state chip keeps its 44 by 44 tap area. (Issue #1441, Issue #1387) - **Popovers share one placement rule** (Feature #177): the chip popover, the search picker and the date picker each carried a copy of the drop-below, flip-above math with different edge clamps. They now place through `placeNear(pop, anchor, { gap })`, which keeps every popover 8 px inside the viewport on all four sides. A chip popover opened from a chip near the top of a short window no longer flips above the top edge, and a picker opened from a cell scrolled past the left edge stays on screen. Gate: `test/place-near.test.mjs`. - **Popovers share one radius and shadow** (Feature #177): the chip popover, the date picker and the bug panel each restated their corner radius (8, 10 and 9 px) and drop shadow. They now read `--wv-pop-radius` and `--wv-pop-shadow` from `:root`, the chip popover's values, so the date picker and bug panel corners drop to 8 px and their shadow matches the chip popover. Gate: `test/pop-tokens.test.mjs`. - Two writers on one workspace, such as the server and the CLI, no longer give two records the same public id. The id is checked against the stored rows inside the write transaction and moves to the next free number when another writer took it first, and the public id index is now unique. Opening a workspace that already holds duplicates keeps every row: the first holder keeps its number and each later one takes the next free id (Issue #1455). - **The Quality mirror carries each case's last result and syncs over HTTP** (Issue #1244, Issue #1245): the CI reporter now records a pass or fail per test, and each shard result carries them. `src/quality-mirror.js` plans the sync as a list of writes: a case red on every attempt reads Failing, red then green on a retry reads Flaky, green reads Passing, and a case no run names takes its file's result when the file is green. `scripts/quality-sync.mjs --from --token-env --results ` applies that plan to a tracker over HTTP, removes suites whose file is gone, and fails when drift is left after the sync; `--check` reports the drift and writes nothing. Gates: `test/quality-mirror.test.mjs`, `test/quality-sync.test.mjs`. - **The Quality mirror syncs only from main's newest tested commit** (Issue #1244): `tests` runs finish out of order, so `.github/workflows/quality.yml` now skips a run whose commit main has moved past. A late run for an older commit no longer rolls the mirror back to an older test tree, and runs for commits from before `scripts/quality-sync.mjs` existed skip instead of failing. - **The tracker's Quality mirror syncs after every main tests run** (Issue #1244, Issue #1245): `.github/workflows/quality.yml` runs when the `tests` workflow finishes on a main push, red or green, checks out the commit that run tested, downloads its shard results and runs `scripts/quality-sync.mjs` against https://weave.grunion.ai/w/weave with `WEAVE_TRACKER_TOKEN`. The job fails when drift is left after the sync. It runs outside the `tests` workflow, so a tracker outage never turns main red or wakes the sheriff. - **Shift+Enter creates in the tray** (Issue #777): on a view with no inline add row, such as a board, Shift+Enter opened a centered modal titled with the generic row word. It now opens the right-hand create tray, titled with the table's own row name (New task on Tasks). Gate: `test/new-record-focus-browser.test.mjs`. - **A table with no term of its own says record, not row.** New tables read "+ New record" and "Search records", and every surface that names the thing itself says record. Row stays the word for grid geometry (row density, the Σ row, a CSV header row), and a table can still pick Row as its term. A `weave_build` table takes `records`; `rows` keeps working. This reverses the default set in Issue #618. - **Reference panels show only when the record eye turns them on** (Issue #1479): every record drew its References and Referenced by cards and opened the side column for them. The record's Show / hide fields popover now has a Reference panels section with a switch for each card, stored on the table as `referencePanels` (PATCH `/api/tables/:id`, describeSchema, export and import, duplicate). Both start off, so a record draws no reference card and keeps its side column closed until a reader turns one on. Gates: `test/backlinks.test.mjs`, `test/table-config-fields.test.mjs`, `test/references-browser.test.mjs`. - **References counts links into other workspaces** (Issue #1478): a record whose body linked rows in another workspace, such as Net `Backlog/Tasks#166` pointing at ten weave Issues, showed only its local references. `referencesFrom` now returns an external entry (workspace slug, uuid, link text, URL) for each permalink that names another workspace by `/w//` or a `.weave.` host, and the References card shows each as a plain link tagged with its workspace, counted in the header. The other workspace's rows are never read. Gates: `test/backlinks.test.mjs`, `test/references-browser.test.mjs`. - **The schema gate reads the command registry** (Feature #179): the API refuses an editor token a table or field write because its registry entry says it writes the schema, in place of five path patterns typed beside the routes. The same writes are refused as before. Gate: `test/command-registry.test.mjs`. - **Space and table writes run from the command registry** (Feature #179): create, update, delete and restore for spaces, and create, update, move, duplicate, delete and restore for tables are one entry each in `src/commands.js` for the CLI, MCP and the API. The doors now answer alike: a space or table delete answers `{ok, space|table, deleted, hard}` everywhere, and a space update answers the space with its members on every door. `weave table move` refuses a move with no destination space the way the API did. Gate: `test/command-registry.test.mjs`. - Table and Board are now the only layouts. A saved List view opens as a Table with its filters and group levels intact, and its ticked rows return to their groups. Nest by, the `nest` view key and the CLI `--nest` flag are gone, and `layout: "list"` is refused (reverses Feature #276). - **One tree gives one security scan result** (Issue #1302): the semgrep baseline now allows a fingerprint it lists however many times a run reports it. A taint rule that matched one listed line twice on one run and once on the next turned PR #564 red with no scanned file changed. A fingerprint the baseline does not list still fails the scan. Gate: `test/security/security-scan.test.mjs`. - **The Trash and Settings pages load when opened** (Issue #1041): `showTrash`, `showSettings`, the workspace delete dialog and their hold helpers moved byte for byte out of `public/app.js` into `public/settings-view.js`, fetched on the first visit to `#/trash`, `#/trash/
` or `#/settings`, or the first Delete workspace. A cold home, table or row load no longer downloads them (10,046 B raw, 3,167 B gzip, 2,740 B br). A failed fetch shows "Settings did not load" and leaves the app running. Gate: `test/settings-view-lazy-browser.test.mjs`. - **The engine reads workflow states, state categories, cell colours and system sort keys from the public cores** (Issue #658): `src/engine.js` kept its own copies of `STATE_CATEGORIES`, `DEFAULT_WORKFLOW_STATES`, `CELL_COLORS` and `SYSTEM_SORT_KEYS`, with source-matching tests holding them in step with `public/field-dialog-core.js` and `public/cell-graphics.js`. It now imports those cores and exports the very same lists, so a change lands in one file. No behaviour changes. Gate: `test/shared-constants.test.mjs`. - **A main red held only by timed-out shards convicts no commit** (Issue #1326): when the test manager kills a shard before any file reports, the sheriff files an Issue and alerts instead of bisecting on the gate conclusion and reverting whatever commit sits in the range. Gate: `test/sheriff.test.mjs`. - **A file preview leaves the document the width of the entity column** (Issue #1476): on a row whose document links another row, the entity page keeps a 320 px references column beside the document. Opening a file preview at 1440 px shrank the entity column to 484 px and left the document 122 px, three or four words a line. The references column now drops below the document whenever the entity column is 820 px or narrower, so the document keeps 466 of 484 px with the preview open. Gate: `test/side-panel-doc-width-browser.test.mjs`. - A signed-out visit to a workspace host, a row link or `/auth` on a host now opens the weave sign-in page at `/start` and returns to the exact row after sign-in (Issue #1342). - An invite link with no login opens the same page and resumes the invite after sign-in or sign-up. - `/start` on a workspace host redirects to the apex `/start` instead of answering 404 (Issue #1343). - A signed-in person arriving through `/start?next=` for a workspace their account cannot open sees a notice naming it, and the login stays held (Issue #1331). - The workspace sign-in page says Sign in, names no vendor, asks a workspace architect for an invite, and links to sign-up when open sign-up is on (Issue #1335). - `/start` accepts a `redirect_url` on the identity provider's OAuth path, so Clerk's sign-in and sign-up paths can point at the weave page (Issue #1342). - `scripts/clerk-methods.mjs` compares the methods Clerk offers at sign-up and sign-in (Issue #1330). - When `/api/start` fails, the start page shows an alert with Try again and a support link instead of a bare error line (Issue #1334). - The weave logo during setup reopens the saved draft at `/start?setup=resume`, so the workspace name and choice survive the click (Issue #1340). - The supervisor reads the free space on the releases volume before it downloads a release. Under `WEAVE_RELEASE_FREE_MB` (default 128) it fetches nothing and tries again on the next check, and `/api/health` reports `supervisor.disk` with `freeMB` and `totalMB` (Issue #880). - `railway.json` rebuilds the hosted container only when `Dockerfile`, `railway.json` or `src/supervisor.js` changes, so a push to main no longer restarts the one-volume production container (Issue #548). - **Number cells swap to their input through one helper** (Feature #177): a number cell's graphic (bar, rating, progress) and its formatted value each carried a copy of the click-to-edit, blur-to-restore swap. Both go through `swapToEdit(display, input)` now. Behaviour is unchanged. Gate: `test/swap-to-edit.test.mjs`. - **The Tabler stylesheet carries only the rules weave uses** (Issue #660): `public/vendor/tabler.min.css` drops from 536,141 to 87,157 bytes (69,037 to 17,294 gzipped) on every page load. `scripts/trim-tabler.mjs` keeps every element rule, at-rule and design token, and every selector whose classes appear in `public/`, `src/` or `bin/` source, including every class under a prefix the code builds at runtime (`btn-${tone}`). `test/tabler-trim.test.mjs` fails when the app names a Tabler class whose rule the trimmed sheet lacks. Computed styles matched the full sheet on every element of the home page, four tables, two space pages, an entity page, 25 opened menus and the bug form, in both themes. - **One shared server fixture for unit tests** (Issue #666): `withServer(weave, options, body)` in `test/lib/fixtures.mjs` serves a workspace on a free port, hands the body `{ server, port, base, fetch }`, and closes the server and its keep-alive sockets once the body settles. 50 hand-rolled start, try and close blocks across 38 unit test files now use it. No browser suite imports the fixture, so a change to it selects only the smoke set in the PR gate. Gate: `test/server-fixture.test.mjs`. - **More unit tests use the shared server fixture** (Issue #666): 44 more start, try and close blocks across 40 unit test files, including the one-line `finally { server.close(); }` shape and blocks with setup lines between the boot and the `try`, now run through `withServer` in `test/lib/fixtures.mjs`. - **Browser cases that read no paint run in one theme** (Issue #667): 48 light and dark loops across 48 browser suites asserted only geometry, counts, classes or text, which a theme cannot change. They now run once, in light: 84 duplicate dark cases gone, and the converted suites took 144 s instead of 165 s on one local run. `test/theme-loop-guard.test.mjs` fails a new theme loop whose body reads no colour, contrast or screenshot; DEVELOPMENT.md "Theme loops" states the rule. - **Slash menu source greps that a browser suite proves are gone** (Issue #664): GitHub CI has run every browser suite since the browser shards landed, so `test/ui-contract.test.mjs` drops the greps for the slash menu's group titles, glyphs, syntax column, INSERT heading, prefix score and aliases, each proven in `test/slash-commands.test.mjs`. The greps for wiring a browser cannot see cheaply (row escaping, highlight rules, the promotion threshold, cap and dedupe, and the whole dock block) stay. - **Workspaces older than v0.4.4 are refused with an upgrade path** (Issue #657): opening or importing a workspace saved before v0.4.4 (a v1 file, a file keyed by `databases`, or percent values still stored as whole numbers) now fails with a message naming v0.4.4 and telling you to open it once with weave v0.4.123 first. The one-time migrations behind those shapes (`databases` to `tables`, `entity.doc` to `entity.docs`, the percent divide-by-100 pass and the table `noun` move) are gone from the open path, which ran them on every open, settle and import. Gate: `test/upgrade-floor.test.mjs`. - **One helper hooks every overlay's window listener** (Issue #663, Feature #177): the modal, tray, page hold, full-screen viewer, document expand, onboarding, bug panel and the grid's Escape and fill-drag listeners each hand-rolled a window listener that removed itself once its overlay left the page. They now go through `whileConnected(node, type, handler)`, which returns its own `off`. Behaviour is unchanged. Gates: `test/while-connected.test.mjs`, `test/while-connected-browser.test.mjs`. - **The whiteboard draws with the document's mermaid** (Issue #662): opening a diagram as a whiteboard rendered it a second time through cytoscape and a hand-written flowchart parser, so it could only show flowcharts and drew them in another layout. It now renders the same mermaid SVG the document shows, for every diagram type mermaid draws, inside the full-screen viewer with drag to pan, wheel to zoom and double-click to fit. `public/vendor/cytoscape.min.js` (434,037 bytes) and `public/graph-parse.js` are deleted. Nodes no longer drag one by one. Gate: `test/whiteboard-label-breaks-browser.test.mjs`. - The phone workspace About pane no longer draws the whole-workspace relation map, which rendered 2,042 px wide in a 339 px card. Each space's About pane keeps its own map (Issue #1421). ## v0.4.123 (2026-11-08) - On a phone the Change photo button beside a record's name is a 44 px tap target, up from 40 px, in line with the rest of the phone UI. The photo itself keeps its 40 px size (Issue #1167). - On a phone the field editor tray keeps a 44 px tap target on every control: the option and state rows' colour dot, name box and category picker, the "+ Add" button, the tray's Close button, its text and select fields, and the Cancel and Save buttons in its footer. The colour dot still draws at 18 px inside its new target, and the touch gate now walks the tray (Issue #1187). - weave can now use a **System › Integrations** key without handing it out: the engine's `integrationFetch` sends one https request to the row's Host with the key in the Authorization header. It refuses every other host, port and scheme, a caller-set Authorization, Proxy-Authorization or Cookie header and a row that is Off or has no key, before anything is sent, and never follows a redirect. Each call stamps Last Used and writes a `key-used` audit entry with the integration, host, method, path and status; a key the host echoes back is redacted from the answer. Nothing calls it from outside the engine yet; Feature #310's http-request action will. (Feature #339) - The Credential cell of a **System › Integrations** row is write-only: it shows dots and the key's last four characters, with no Show or Copy. **Add key**, then **Replace**, opens a password box that sends the key once, tests it against the row's Host and clears itself; a toast says whether it connected. The columns weave keeps (Key Hint, State, Connected As, Health Reason, Last Checked, Last Used) read rather than edit. (Feature #339) - Every value on a phone list row's chip line now draws at the select chips' size. A plain number or a date rested as an input, which the iOS no-zoom floor (Issue #724) sets to 16px, so it read larger than the 13px chips beside it; on the phone list, where values are edited from a sheet and never in the row, they now rest as text, as a text value has since PR #646. A formula that returns text or a number draws as the slate chip a select value uses instead of as bare text. When a multiselect or relation hides some of its values, its own `+N` folds into the row's single counter, so a line never reads `+1 +2`. (Issue #1372, Issue #1373, Issue #1388) - The field dialog's Description box grows to fit its placeholder and what you type, so a phone no longer slices the placeholder's last line, and the placeholder is shorter (Issue #1397). - A field dialog note such as "lookup field, the type is fixed" keeps the same gap above the next label as every field does (Issue #1398). - **System › Integrations** is a new system table, listed in the sidebar's System group between Users and Activity, for the outside tools a workspace holds an API key for. Each row names a Provider (Fibery or Generic API key) and the one Host its key may reach. The key goes in once through `PUT /api/integrations//credential` and is tested against that host on save: a Fibery key must return the current user, whose name lands in Connected As. A green test seals it in the keystore and keeps only its last four characters as Key Hint; a red test stores nothing and says why in Health Reason. Replacing a key keeps the old one until the new one tests green, changing Host or Provider deletes it, and deleting the row deletes it. No route, MCP tool, export, share link, Activity entry or audit entry reads it back: the keystore API refuses every `integration:` name. (Feature #339) - One rule per text role across every surface and density (Issue #1306): titles balance, prose wraps evenly and folds on a whole line, and `DESIGN.md` and the Handbook guide "How text behaves" write the rule down. - A description with a paragraph gap inside its fold no longer loses the bottom of its last line above Show more, and a field note folds on its third whole line without a fade (Issue #1306). - The related subtable on a record page fits its card: a 1% column width no longer inflated it to 19,931 px (Issue #1260). - A long text value on a phone table row at Spacious ends on one whole line in its pill, where its second and third lines were sliced by the 22 px box (Issue #1420). - On a phone record page the URL field's link is a 44 px tall tap target and its edit pen a 44 px box, in line with the rest of the phone UI. The pen's icon keeps its 14 px size (Issue #1168). ## v0.4.122 (2026-11-08) - `DELETE /api/entities/:entity/files/:file` and `DELETE /api/automations/:id` now answer 404 `not-found` when they remove nothing, and 200 with the engine's own body (`deleted: true`, plus `entity` for a file) when they do. Both routes used to drop the engine result and answer 200 `{"ok":true}` either way, so a typo in a file id under a real record read as a successful delete. The CLI and the MCP tools already returned the `deleted` flag and are unchanged. (Issue #1087) - An attachment's bytes now reach the disk before its record commits. `writeFileDurable` in `src/store.js` writes a `..tmp` with `{ flush: true }` and renames it into place, and the engine's blob writer, the workspace-detach blob path and `restore` all go through it. The store runs `PRAGMA synchronous = FULL`, so until now a power loss after `attachFile`, a preview write, a logo upload, an `importJSON` or a restore could leave a row pointing at a missing or truncated `files/`. A backup also ignores a `.tmp` left in `files/` by an interrupted write instead of archiving it and counting it an orphan. (Issue #1411) - The four workflow downloads that fetch a pinned release asset, the `gitleaks`, `actionlint` and `hadolint` installs in the required `security scan` and the `mcp-publisher` install in the release job, now carry `--retry 3 --retry-all-errors --retry-delay 2 --max-time 120`. One transient 504 from GitHub's release-asset CDN used to fail the whole check and block auto-merge on every open pull request. The pinned versions and the `sha256sum -c -` verification are unchanged, so a retry that returns corrupt bytes still fails. (Issue #1419) ## v0.4.121 (2026-11-08) - A backup to `s3://` or `weave://` no longer reads the sealed archive into memory before it uploads: S3 destinations go up as a multipart upload through the existing SigV4 signer, `weave://` streams its PUT body, and the archive is sealed into the file the tar writer is already writing rather than into a second temp copy. Peak RSS for a 320 MB archive drops from 741 MB to 305 MB and stops tracking archive size. `WEAVE_BACKUP_PART_BYTES` moves the 8 MiB part size. (Issue #1410) - A relation chip's `×` unlinks behind a toast carrying **Undo**, and so does the related grid's unlink button. The phone record page leaves the `×` out of the chip's tap row, where the field's link picker takes a row out instead (Issue #1404). ## v0.4.120 (2026-11-07) - A checkbox field on a record page answers a tap anywhere in a 46 px box on a touch screen (Issue #1166). The input itself was 16 px, against the 44 px minimum that Apple's Human Interface Guidelines and Material both ask for, and a finger could miss it. The box a reader sees is still 16 px, on the same left edge as the values in the rows around it. - On a phone, the `›` control on a relation chip in a cell or a field row opens the linked row (Issue #1401). It used to expand the chip in place, which grew the field row to about 290 px of mostly empty box and scrolled the row's name out of sight behind its field values. On a desktop, and in a document on any screen, the control still shows and hides the fields. - Each icon of a rating field on a record page now sits in a 46 px slot on a touch screen, so a finger can reach it (Issue #1165). Ten icons of a rating with max 10 used to be 14 px each, against the 44 px that Apple's Human Interface Guidelines and Material both ask for. The icons wrap onto a second line in a field row too narrow for all of them, and the glyph is drawn at 20 px. - A failed upload now says why it failed and what to do next (Issue #1413). A `502` or `503` from the hosting edge, which usually means weave is restarting after a deploy, is retried on its own after about 2, 5 and 10 seconds while the tile reads **Server restarting**; only when the last try fails does the tile turn red with **Still restarting**. A `504` reads the row again and, if the file is not there, says the upload took too long (**Timed out**). A full disk now answers `507` with code `storage-full` on every write, attach routes and record saves alike, and the browser says the server is out of storage with no Retry button. Any other server error shows the request id the server logged, in a monospace face, so a report can be matched to the log. Saves and reads outside uploads use the same causes. ## v0.4.119 (2026-11-07) - Every MCP tool now carries `idempotentHint` beside its other annotations, so a client knows which calls it may safely retry. Reads, updates, sets, deletes and restores are marked idempotent; creates, appends, row imports, comments, undo, token and share mints, form submits and `weave_call` are not (Issue #1319). - The New row sheet on a phone follows the keyboard instead of hiding behind it: the sheet sits on top of the software keyboard and caps its height to the page above it, so the Name field you are typing into and the Create button both stay on screen (Issue #1386). The choice-field block takes the squeeze and scrolls, keeping the title, Name and the actions in place. The keyboard's height is published as the `--wv-keyboard` custom property from the visual viewport, so any sheet can read it. - Uploads show their progress and their failures: each file you pick or drop gets a tile in the field at once with **Uploading 40%**, then **Saving**, and the field is `aria-busy` until it lands (Issue #1361). Every file in a batch is tried on its own and one toast sums the batch up, naming the file that was not saved and why (Issue #1363). A failed file stays as a red tile with its reason and a Retry button, none for a file over the 10 MB limit, which offers **Choose another file** instead. A timeout or `504` reads the row again first, so a file the server stored before the answer was lost counts as uploaded. The document editor's upload button and the avatar picker share the same limit check and messages, the **+ file** picker takes several files (Issue #1365), error toasts carry a red accent and a new amber warning toast marks partial batches, and an error with no JSON body says the server is unavailable or returned an error instead of a bare status number (Issue #1364). The attach routes' `413` body now names its `limit` in bytes (Issue #1360). - The workspace home opens as a launcher on a phone, the same one a space opens as: a search box over every table, one tile per space with its record count, recent views and records from any space, automation runs across the workspace with any failed step, and About this workspace for the description, the system tables, saved pages and the relation map. Desktop and tablet widths keep the current home, and a workspace with no tables keeps its onboarding card (Feature #355). - `GET /api/workspace/home` answers the spaces, tables, counts, views and runs the launcher draws, with the same private-space and share-link rules as `GET /api/spaces/:id/home` (Feature #355). ## v0.4.118 (2026-11-07) - On a table wider than its pane, the add-field "+" now sits after the last column header and scrolls with the grid. It used to hold at the pane's right edge on top of whichever header sat there, so at a tablet width the last visible column lost its label and the grid read as clipped. Scroll the grid to its end to reach the "+"; the Fields menu's New field works from anywhere. (Issue #1356) - A failed upload stays in the attachments field as a tile naming the reason, with a Retry button that sends the same file again and a Dismiss button that drops it, and the field's file input empties after every attempt so the same file can be picked again (Issue #1362). - The 10 MiB upload limit is now the size of the file, not the size of the request body: a file that big lands through the browser, the REST attach routes, the CLI and MCP alike, and anything larger is refused by name with the limit in the message (`deck.pdf is 25.0 MB; files can be up to 10.0 MB`), answered as 413 rather than 400, and stopped in the browser before a byte leaves the machine (Issue #1360). - Removing a file, a logo or a purged row in one workspace no longer deletes bytes that another workspace in the same data directory still names. Every workspace shares one `files/` folder, and importing one workspace's export into another keeps the file ids, so a delete in the copy used to take the original's file with it and the original answered "File blob missing". The bytes now go only when no workspace in the data directory names the file. (Issue #1359) - A long text or Name cell now expands in place. Rest the pointer on it for 400 ms and the value spills right on its own line from the cell's top-left edge, over the neighbouring columns, and stops at the grid's right edge with a fade where it runs out of room. Clicking it opens the editor in the same spot as a box that runs to the grid edge, then wraps downward and grows as you type; Enter saves and Esc puts the value back, as before. The hover copy used to float above or below the row and the click opened a second, clipped one-line editor under it. Chips, dates and descriptions keep their pop, and touch screens get no hover copy. (Issue #1367) - The long-text editor that spills right and wraps now opens on the same roomy, ringed box as every other inline editor, 4px outside the value, and a value that fits before the grid edge stays on one line instead of wrapping early. (Issue #1367) - Collapse all in the View pane folds every group the list or board drew, the option groups with no rows included; it read a model of its own that only knew the groups holding rows, so those stayed open and the button looked dead. (Issue #1203) - On a phone, a stacked board at Spacious density shows one card per row, so a card's long text reads at full width. Compact and Comfortable keep two cards per row, and cards show their text in full at every density, as before. (Feature #354) - A Spacious table row shows three whole lines of a long Name, text value or description, set 18px apart in the same 72px row, with the third line ending in an ellipsis. Multiselect and relation chips wrap to two rows of whole chips at Spacious and keep one row at Comfortable and Compact, with a +N counting the rest; at Spacious the hidden "+ link" button no longer takes a row of its own, which had pushed relation chips past the top and bottom of the cell. A plain text value in the table now rests as text, like text with marks already did, so it wraps at Spacious and ends in an ellipsis at the other densities; a click or a typed key opens its input as before (Issue #447). (Feature #354) - On a phone, a table at Spacious now shows more than Comfortable. Its rows are 118px tall: the name always gets two whole lines, the chips wrap onto two lines of whole chips, and the +N counter sits on the last line. A view with Wrap chips on keeps its two-line name at Spacious too. Comfortable stays at 88px with a two-line name and one chip line, and every row still windows on one declared height. (Feature #354) - The cut-off gate no longer counts an ellipsis as cut-off text. A label that ends in "…" or a clamped paragraph that ends on a whole line draws every letter it shows, so the gate now fails only on letters sliced by a box edge: a line cut through its middle, a chip cut at its cell's edge, or a clamp whose box is shorter than its lines. The baseline drops its 50 ellipsis and line-clamp instances per platform. (Feature #353) - An API `limit` that is not a number is refused with a 400 naming the parameter and the value, the way `bin/weave.js` answers `--limit must be a number, not 'lots'` (Issue #1091). `?limit=lots` used to reach the store as `NaN`, and every route that pages answered 200 with an empty page: the audit trail, the undo list, a table's entities, a document's revisions and a `POST` table query all reported zero rows beside a total that counted them. A missing limit, an empty one and a numeric string page exactly as they did. (Issue #1354) - Importing a workspace into SQLite scales with its row count: 50,000 rows land in 0.9 s where they took 291 s. A full save deleted one row at a time from `entities_fts`, whose `id` column is UNINDEXED, so each delete scanned the whole index and the write path cost grew with the square of the row count. It now clears the index once and inserts every live row, and `Store.save` prepares its four row statements once per save instead of once per row (Issue #1094). - `weave audit`, `query`, `search`, `undo --list`, `doc-revisions` and `activity` refuse a `--limit` that is not a number with `--limit must be a number, not ''`, in place of SQLite's `datatype mismatch` (Issue #1091). - The GitHub Pages site unfurls with its own preview image. `docs/_config.yml` carries `docs/social-preview.png` as a front-matter default, the one place jekyll-seo-tag reads a site-wide image from, so every page serves `og:image` and `twitter:image` and the Twitter card is `summary_large_image` rather than `summary`. The repo's own social preview stays a manual upload under GitHub Settings, which no API writes (Issue #1148). - Phone test cases for Issues #1379 to #1406 run on a five-device iPhone matrix (iPhone SE, iPhone 17, iPhone 17 Pro Max, and iPhone Duo closed and open, modeled from Xcode 27.2) in Chrome and Safari engines. Each case is marked todo until its Issue lands, so it reports without turning the suite red. - `test/feedback-budget.json` holds `phone-menu|phone` at 250 ms, down from 450. The main CI gate that set the old floor read 217, 259 and 373 ms; with the focus hold off the first frame it reads 147, 124 and 125 (Issue #1061). - The phone navigation menu paints on the frame after the tap. The focus hold that makes the page behind inert and moves focus into the drawer used to run in the same task as the `nav-peek` class, and its `getClientRects()` reads forced a layout of the freshly revealed full-screen panel before the browser could paint a single frame. It now runs two frames later, so the panel appears first and keeps focus, Escape and Tab a frame afterwards (Issue #1061). - Editing a table cell or an entity field now opens a roomier box: it grows 4px on every side around the text, rounds to 8px and draws a softer 2px blue ring, in place of the hard 1.5px ring that sat a pixel from the letters. The text, the row height and the column stay where they were. A selected cell that holds chips or a picker draws the same ring. Opening a text field on an entity page no longer nudges the row 3px taller. (Issue #1366) - Text fields across the app now carry `autocomplete="off"` by default, so Safari no longer offers a Contacts card when you edit a Name cell in a grid (Issue #1375). - A table view that shows deleted rows now opens on a page and feeds the rest as you scroll, the way the standard view already did. On the weave workspace's 3,980-row Case table the open drops from 5,187,491 bytes to 244,702. Trashed rows keep their strike-through on every page, and the filter readout counts the live rows of the whole table, not of the page in hand. (Issue #1269) - `weave_query` and `POST /api/tables/
/query` answer `liveTotal` beside `total` when a read asks for `includeDeleted`: the live rows among everything the query matched. (Issue #1269) - A workflow webhook action takes `signWith`, the name of a keystore credential, and signs each delivery: `X-Weave-Signature` carries the hex HMAC-SHA256 of `.`, beside an `X-Weave-Delivery` uuid. A receiver can now tell weave from anyone else without a token hidden in the url. The credential value stays in the keystore, so the Script, the Workflows row, the export and the backup hold the name alone. A rule that names no credential posts exactly what it posted before; a rule whose credential the keystore does not hold sends nothing and reads Failed, since weave never falls back to an unsigned delivery. The verify recipe is in the Automations guide. (Issue #951) - Opening a large table now has a regression guard: a browser check measures the table-open traffic and fails if the grid ever again asks for the whole table, asks for full related rows instead of chips, or fetches the trash as a second table. The 2,062-row Case table that froze a tab for 17 to 52 seconds opens in under a second on a 3,980-row copy of production, downloading 252 KB instead of 10.2 MB and drawing 75 rows instead of 2,062. (Issue #1357) - The test runner pins `WEAVE_KEYSTORE` and `WEAVE_DATA` into a per-run temp directory, so a test that skips the override can no longer read or write the developer's own `~/.weave` (Issue #949). ## v0.4.117 (2026-11-05) - **Automations have a Handbook guide** (Issue #1247): the only prose on automations was one section of *Making a workspace your own*, so the cron schedules (Feature #312) and the `steps.N` outputs (Feature #313) that landed on 2026-10-25 reached no guide. A new **Automations** guide covers the Workflows row as the control panel, all four triggers, the schedule with its cron, zone, `where` clause and catch-up pass, all five actions, placeholders and step outputs with each action's output shape, what a webhook may reach and what marks a rule Failed, a worked example, and the CLI, REST, MCP and grid doors. The customization guide now points at it instead of carrying a second copy. Gate: `test/handbook.test.mjs` (6 cases, the trigger and action sets read out of `src/engine.js` so a new one turns the guide red). - **Board fold buttons take a thumb on a phone** (Issue #1217): the group and column head folds PR #465 added kept the 22px desktop box on a phone, so the touch gate measured 29x43.5 against its 44px minimum while the lane head, the one head the phone override named, passed. The phone rule now covers every board head, so each fold draws a real 44x44 box the board's horizontal scroller cannot clip the way it clips the `::after` slop. `board-view` and `sink-board` leave their touch hold and are walked by `test/phone-touch-targets-browser.test.mjs` again, which also gains a case per board surface asserting each fold's own box, not just its hit area. - Clicking an attachments cell in the grid opens its first viewable file in the file panel, the way a chip on the record already does; ⌘-click still opens the file in a browser tab. (Issue #1072) - ⌘K searches the workspace you are in. The `all` parameter on `GET /api/search` is gone, so the palette no longer merges rows from every workspace a viewer can open, and no row carries a workspace tag; another workspace answers by name as a jump target read from the rail. (Issue #1308) - `query` takes `shape: 'row'`: a list row of `id`, `publicId`, `name`, `fields` and the `raw` values that differ from their display value, without the `revision`, `url`, `db`, `dbId`, timestamps, actors and documents the full envelope carries. On the weave workspace's 3,980-row Case table, `{fields:['Name','Chip'],relations:'chip'}` answers 2,738,299 bytes instead of 4,380,810. The CLI takes `--shape row` and `weave_query` takes `shape`, 1,642,511 bytes off one list read. The grid, the list and the reference picker still ask for the full envelope (Issue #1270). - **SOUL.md stops refusing the marketplace weave ships through** (Issue #1145): the README scoped its plugin refusal to "weave has no third-party app store inside it" when PR #472 landed, but `SOUL.md`, which every session writing copy about weave reads first, still quoted the retired line "No marketplace, no extensions, no third-party apps" and attributed it to the README. Its permissions refusal quoted a sentence the README no longer carried either. Both now quote the README that ships. Gate: `test/docs-drift.test.mjs` checks every refusal `SOUL.md` quotes from the README against the README, and refuses a blanket no-marketplace line. ## v0.4.116 (2026-11-05) - The field-definition clear test times its hold on the page's own clock instead of fixed sleeps: `hold()` from `test/lib/hold.mjs` waits for `.hold-btn.holding` and for the sweep to finish before releasing, the new `releaseEarly()` pauses the sweep midway to prove a let-go hold changes nothing, and the engine is read with `eventually()`. Under load the old 1400 ms hold released before the 900 ms sweep ended and the 400 ms read landed before the write, which failed the gate on change 437 patch set 5 ([[Issue#475]]). - The full suite balances its four browser shards on measured runtime. The `plan` job downloads the `ci-results` artifacts of the most recent successful main run and weights each file by the `duration_ms` the reporter already records; a file the history does not name keeps its byte-size weight, and a run with no usable history plans by byte size as before. Byte size was anti-correlated with runtime for the worst offenders, since the slowest browser files are small files that loop: on the inventory of run 41959983156 the byte-weighted plan handed the shards 1,171, 1,107, 1,939 and 1,622 seconds of work, and the duration-weighted plan hands them 1,460 each ([[Issue#966]]). - The full suite's unit shards no longer download Chromium and WebKit. The engine cache and the install step in `.github/workflows/test.yml` carry `if: matrix.lane == 'browser'`, the condition `pr.yml` has had since the PR gate shipped, which gives back the 46 and 37 seconds the two unit jobs of run 39439094335 spent on engines they never opened. A new gate in `test/ci-test.test.mjs` walks every unit-lane test's imports and refuses one that reaches `test/lib/browser.mjs` or imports `playwright` at load, so the lane split stays true to what each shard needs ([[Issue#967]]). ## v0.4.115 (2026-11-04) - The browser install step of every gating workflow, the PR shards, the main shards and the dev soak, now makes three attempts inside its own budget instead of two capped at five minutes each, waits up to 300 seconds for the dpkg frontend lock, and reports whether `dpkg --configure -a` succeeded rather than discarding it with `|| true`. Three shards died on the runner's apt lock in one afternoon, each costing a full re-run of the job and reaching no test, because an attempt was killed mid-download with the step's budget unspent ([[Issue#1347]]). - The nightly compatibility matrix names `ubuntu-24.04` instead of `ubuntu-latest`, which GitHub now resolves to `ubuntu-26.04`, and a guard refuses any workflow that carries a floating runner label. PR #507 pinned every other job by name and left this matrix behind, so its WebKit install could stall the way the PR shards did ([[Issue#1258]]). - A Development/Release row that has lost its notes now stops its own release and nothing else. The export refuses only the row whose version it is writing; an older empty row is named on stderr as historical, keeps the notes `docs/development.json` already carries, and does not block the export, and a boot sync refuses only the row its manifest is for. `scripts/release-commit.mjs` writes the merged commit onto the row by appending one line rather than replacing the document, then reads the row back and refuses when the notes came back empty or without the commit, so a lost document is caught by the run that caused it instead of by the next release eight hours later ([[Issue#1329]]). ## v0.4.114 (2026-11-04) - The cookie banner says "we record" instead of naming PostHog, keeps every disclosed item, and leaves the privacy policy to name the analytics processor ([[Issue#1344]]). - A server whose workspaces all detached under an earlier version now starts instead of exiting at `Cannot detach: copied rollup Spaces.Sessions · Cost (USD) · avg is missing`. The hub registry migration required a rollup it still holds to already exist in whichever workspace owns the rolled-up table; a workspace that finished its own migration before the rollup copy shipped never received one, so every start refused and the Mac read copy on `:4400` sat four releases behind from 2026-11-01. The migration now leaves such a rollup on the hub rather than refusing, and still refuses for a workspace it is detaching in that same pass ([[Issue#1307]]). ## v0.4.113 (2026-11-03) - The vitals, feedback and render budgets are judged on the fastest of their runs rather than the median, so a loaded CI shard no longer turns a budget miss into a budget raise ([[Issue#1327]]). - Step 0 of the overnight fixer now reads a range of Issue numbers in a merged main subject, so a row landed as part of `Issues #1118, #1143-#1148` stops sitting Open ([[Issue#1313]]). - The sheriff drops from its suspect list every commit whose diff cannot reach the failing suite, says in the row when nothing in the range can explain the red, and files a repeat failure onto the open row that already names the file ([[Issue#1324]]). ## v0.4.112 (2026-11-02) - A main red whose only red test files never reached a retry verdict no longer convicts a commit. A shard killed by its own timeout reports each of its files once, so nothing says whether they stayed red; the sheriff now escalates with that file list and logs a line on each file's `flakes on main` row instead of bisecting and opening a revert PR. On 2026-11-02 that path reverted the healthy v0.4.111 release over `test/feedback-budget-browser.test.mjs` and `test/phone-new-row-sheet-browser.test.mjs`, neither of which the run ever judged (Issue #1322). ## v0.4.111 (2026-11-02) - Billing moves to one per-seat organization plan at $4.99 a seat, from a workspace's first person on: `WEAVE_BILLING_PLAN` replaces `WEAVE_BILLING_SOLO_PLAN` and `WEAVE_BILLING_TEAM_PLAN`, and `POST /api/billing/organization` (**Set up billing** on `/billing`) replaces `POST /api/billing/team`. Neither old variable was set on a deployment, so no workspace changes plan (Feature #256). - Scroll bars look the same in every pane in Chromium and Safari, whatever the macOS scroll bar setting: one 8px bar with a 4px thumb, in place of a thin overlay on a trackpad and a fat classic bar with a mouse. Bar ends stop short of rounded corners, 16px on the panes, modals and menus and 8px elsewhere. Panes keep the 8px gutter with nothing to scroll, so content does not shift when overflow appears. Firefox keeps its native thin bar and touch screens keep their overlay bars (Issue #1310). - A space's tables card shows a plain count for a table with no term of its own, and a table's own word where it has one: `331`, `1,139 bugs`. - A free trial now ends with an email: three days before the end and again on the last day, weave writes to every Architect of the workspace who has a verified address, and each mail links to **Billing**. The sweep runs hourly, marks each stage on the workspace so it sends once, and stops if a plan is bought during the trial. `GET /api/mail/preview/trial` and `GET /api/mail/preview/trial-last` show the copy (Issue #1298). - The **New user** form now shows the Workspace the account will belong to, filled with the workspace you are in and changeable before you create, and the dialog that shows the one-time token or invite link names the role and that workspace ("Architect token for net"). A saved Users row cannot change its Workspace, and the token is shown once, so the workspace was the one thing you could not check in time (Issue #1224). ## v0.4.110 (2026-11-01) - The Handbook "Agent access (MCP)" guide follows the MCP primer: read the `weave_ontology` outline, build with one `weave_build` call run with `dryRun` first, read rows with `weave_query` and `weave_search`, and reach every other tool through `weave_call`. Its tool and field-type counts come from the code, icons read `lucide:`, and fields, tables and spaces restore from the trash. The guide now lives in the Handbook source, so a release's Handbook sync replaces the stale row, and a test fails when any Handbook page or doc states a tool or field-type count the code does not have (Issue #1246). - The overnight fixer's tested core now lives in the weave repo at `agents/fixer/core.mjs`, so a cloud agent that clones only `grunion-ai/weave` can import it. It carries issue selection, batching, claim and attempt notes, enrichment and verdict parsing, plus `eligible` and `changedSinceLastAttempt`, which decide whether a tracker row is open to the fixer. Attempt notes name GitHub PRs, and lines written in the old format still parse (Feature #192). - `node agents/fixer/verify-dev.mjs ` waits for dev.weave.grunion.ai to serve a merged commit. It polls the anonymous `/api/health` every 10 seconds, accepts a full sha or a prefix of 7 or more characters, refuses a commit whose last swap failed, and exits 1 after `--timeout` seconds (default 900) with the last health body on stderr. The cloud fixer uses it in place of the Mac's `:4400` read copy, which is locked (Feature #192). - The attachment chip on a board tile, list row or table cell middle-truncates a long single file name so the extension stays visible, and shows the full name on hover (Issue #1155). - An attachment field's tiles now stay inside their row on the record page. The tile sheet sat in a column flex box whose height Chrome measured before the tiles took their aspect-ratio height, so a tile ran past the row and covered the + file button and the chips under it. The field box is now a one-column grid, which sizes each row to the tiles it holds (Issue #1191). - **Avatar faces on people surfaces** (Feature #352, relands PR #351): a compact to-many relation cell whose rows all have avatars draws a face pile, up to five faces that each link to their row, then `+N` naming the rest on hover. Created by, Modified by, the activity feed and the identity panel show the actor's System/Users avatar, photo or initials, in place of hashed initials. A one-word name splits on dashes and underscores for its initials, so `weave-fixer` reads WF while `Jean-Luc Picard` keeps JP. Gates: `test/avatar-faces-browser.test.mjs`, `test/chip-renderer.test.mjs`, `test/chip-system.test.mjs`. - The Board footer no longer shows a stray "null" before the Compare button. The picks slot handed its children straight to the native `replaceChildren`, which stringifies a non-Node argument, so the pick-hint's false branch landed as a text node reading "null" at every pick count under the cap. The slot now filters its children the way `el()` and the members body already filter theirs, and a source check fails when a `replaceChildren` call is handed a child that can be null (Issue #1234). - Reversed on a board group level now runs the columns, the lanes and the stacked sections backwards, with No value still last. The board built its group levels without the level's direction, so the toggle saved into the view and changed nothing on the page (Issue #1205). - Board groups collapse into labeled rails or compact stacked headers. Cards follow density, the board loses its extra enclosing border, and comparison checkboxes keep clear of IDs and titles (Issues #1197, #1198, #1199, #1200). - Drag placement uses settled card positions and waits for the drop animation before refreshing the board (Issue #1115). - Entity relation blocks show the linked table's default Board layout, with local folds, linked-record editing and New, Link existing and Unlink actions (Issue #1201). - A board tile sizes itself from its own content and is never stretched or shrunk to its row track, so a layout pass that undersizes the track, as desktop Safari 27 did while a board was still loading, can no longer clip the name down to its descenders. Tiles in a stacked board now take their own height instead of the tallest tile in their row (Issue #1202). - The brand kit page `brand/kit/index.html`, written by `build-kit.mjs` beside `manifest.json`, shows every kit asset on the ground it is drawn for with its code, size, PNG pixel size, uses and links to the SVG and PNG, and carries the kit README's recipes, palette and rules (Feature #342). Every kit PNG now renders at 2× or more: posters and patterns move from 1× to 2×, lockups stay 3× and the icon 4×, `manifest.json` states each asset's `scale`, and the test reads every PNG header to refuse a raster under it (Issue #1196). The kit gains the 1280 × 640 GitHub social preview `poster-github` (X8). - **Chips wrap inside board tiles and field rows** (Issues #1124, #1125, #1151, #1152, #1154): a relation, tag, select, state or field-definition chip inside a board tile field or a record field row wraps its label onto further lines instead of cutting it at the tile or row edge, an unbroken word breaks where it has to, and the relation caret stays whole beside the wrapped name. Gate: `test/chips-wrap-browser.test.mjs`. - Workspace setup gives each named workspace its own connector name and uses it consistently when adding and authenticating the connection (Issue #1276). - MCP sign-in requests profile and offline access so dynamically registered connectors can sign in and renew tokens with Clerk’s default scopes (Issue #948). - The breadcrumb keeps the workspace name whole instead of capping it at 14 characters, and a long row name gives way before any short space or table crumb loses a pixel (Issues #1127, #1158). The current crumb's own ellipsis stays by design, since the title row under it shows the full name, and the cut-off probe skips it (Issue #1159). - A formatted number that runs past its grid column now shows its compact form ($98.8B for $98,765,432,109.87) with the full amount in the cell's title, so a reader never sees a cut prefix of an amount; a number that fits keeps every digit, and widening the column brings the digits back (Issue #1160). - The cut-off baseline tightens after the board, chip and breadcrumb fixes: on linux the mention chip label row drops from 42 to 41 on desktop and leaves the phone cell, where two CI runs in a row measured none, so the gate now fails on any return of that cut-off (Issues #1152, #1161). - A new main merge no longer ends or restarts the dev soak, so one fifteen-minute window now reviews a batch of commits. Merges queue behind the running soak and the next window takes the newest commit. A soak whose commit dev moved past during the window keeps its clock and smoke schedule, posts `dev-soak` success naming the commit dev moved to, and stops applying the worker pid check and the requirement that its own watch passes. Each poll checks the release dev serves now for a rollback or a failed watch, and a failure after a move posts pending as superseded with no alert, leaving the queued soak to catch it. Gate: `test/dev-soak.test.mjs`. (Issue #964) - A dev soak that watched its commit pass and held dev for the full fifteen minutes now posts `dev-soak` success even when dev swaps to a newer main commit on the poll that completes the window, so a repository taking a merge every few minutes still produces commits the promotion workflow can pick. A soak cut short before the window elapses posts `dev-soak` pending described as superseded, where it used to post no status at all and leave promotion reading the workflow's own check-run conclusion instead. Gate: `test/dev-soak.test.mjs`. (Issue #964) - The dev soak sends its write token only to the write workspace, so a token from another workspace no longer fails the read smoke and blocks promotion (Issue #1292). - `/docs`, the README, AGENTS.md and the handbook no longer say MCP needs the Architect role. Any role connects, and `tools/list` names only the tools that role may call: an Observer reads and comments, an Editor also writes rows, an Architect also changes the schema and accounts (Issue #1121). - Done state chips take a darker green label. The old `#218358` on the chip's own `#e2f2e5` tint read 4.05:1, under the 4.5:1 floor, in the field dialog, the filter popover, the board and the table in light theme. The new `#1a6b48` reads 5.57:1, and the dark theme is unchanged (Issue #1100). - A browser install retry no longer dies on the dpkg lock a previous attempt's holder still owns; it waits up to 180 seconds for it, which fits inside the attempt's own five-minute bound (Issue #1141). - CI browser jobs now wait for the dpkg frontend lock instead of failing on it: the apt drop-in every gating workflow writes carries `DPkg::Lock::Timeout "180"`, so the runner image's own boot-time `apt-get` no longer costs a shard its install step (Issue #1230). - `GET /api/entities/:id` and the other row routes take a URL-encoded permalink in the path, so `/api/entities/https%3A%2F%2F…%2Fe%2F` names the row instead of answering not found (Issue #1140). - **The tab icon shows the whole weave mark** (Issue #1203): the favicon frames the rope at `8 8 32 32` instead of the 48-unit mark frame, so it fills 89% of the icon rather than 60%, and `favicon-16/32/48.png` and `favicon.ico` are rebuilt from it. - A click on a file chip, on the record or on the loose Files card, or on a sheet cell now opens the file in a panel to the right of the record instead of downloading it or jumping to full screen. The panel shows images, PDFs, plain text and HTML (an HTML file loads from its sandboxed `?view` route), steps through the field's files, opens the same file full-screen from its full-screen button, and closes from its ✕ or Esc. ⌘-click still opens a browser tab, and a file with no viewer stays a download link (Issue #1173). - The record page's Files card, which lists files no attachments field holds, is now a body block: it has a drag grip, a caret that folds it and remembers, and a place in the table's body order (`@files`). **Make a field** in its header adds an attachments field to the table and moves those files into it, so they take the field's preview and multiple settings (Issue #1172). - Board covers take a Fit setting: Crop (the 4:3 default), Fit, Original or Capped, a Card shape of 4:3, 1:1, 3:4 or 16:9 for Crop and Fit, and Masonry or Rows for the uneven cards of Original and Capped (Issue #1280). - The README, the GitHub Pages site and llms.txt describe weave as a self-hosted work database for people and AI agents and match what ships: Board layout, the View pane, forms, share links, templates, the phone layout, weave cloud and the published container image are described, and nothing says weave has no hosted instance, no narrower grant than a role, no forms or no marketplace (Issues #1118, #1143, #1144, #1145, #1146). The website's four-layer stack animation opens "What weave is", in light and dark. `scripts/screenshots.mjs` builds a scratch Launch workspace and shoots all seven stills from it, adding a kanban board and a phone still (Issue #1147). The Pages site carries a short title, a tagline and an image, and `docs/social-preview.png`, the GitHub social preview, reads SELF-HOSTED WORK DATABASE (Issue #1148). - A file tile with no picture (PDF, text, HTML and other files) now shows the file name as it is spelled instead of in capitals, keeps only its kind word (PDF, TEXT) in small capitals, underlines nothing on hover, and carries a 20 px remove button in place of the white box that crowded its corner (Issue #1192). - Board and list group heads now wrap a long group label onto as many lines as it needs, so a column or group always says which workflow state or option it holds. The label used to end in an ellipsis once it outgrew the column, which cut a 48-character state name by 175 px on desktop and 75 px on the phone (Issue #1157). - Group headers in the Table and List layouts show the groups above them, muted, before their own field and value ("High › Nav ›"), and the top-level header stays pinned under the column headers while its rows scroll, even far into a long group. With Σ Rollups off, each header also shows a short breakdown of the first single-select or workflow field not grouped at or above it, counting live rows only. Collapsed groups are remembered per grouping: every view of a table with the same group fields, orders and directions opens with the same groups collapsed, and a fold in one reaches the others. A different grouping, including the same fields in another order, starts expanded (Feature #348). - The View pane's Group section has a **Hide empty groups** switch for List and Board layouts. With it on, groups with no rows drop out of the view and out of `weave_query` groups; with it off, an unfiltered view still shows every option of the group field. The choice saves on the view as `hideEmpty` and travels through `weave_table_view`, the CLI, REST, view copies and `describeSchema` (Feature #345). - `weave_table_view` names `hideEmpty` in its schema; the tool's byte budget rises from 1,680 to 1,725 to carry the key (Feature #345). - **Every delete is a hold** (Feature #346, Kyle's ask of 2026-10-28, which supersedes the one-click trash of the 2026-10-10 D6 ruling): Move to trash in the table, record, space, share page and field menus, Move to trash on the selection bar, the Delete workspace dialog from Settings and the workspace menu, Revoke on a share link and on a pending invite, Delete link in the Share panel and Remove logo on the workspace rail each delete only when the press is held until the fill sweeps across. A click lets go early and deletes nothing. Menu rows carry a HOLD hint, and the selection bar tooltip reads Hold to move to trash. The Undo toast stays on every trash. In the Delete workspace dialog the typed name arms the hold, and Enter alone no longer deletes. The Views list delete and Settings Delete forever already held. Hold buttons take no text selection, callout or scroll gesture on touch. A contract test counts every DELETE call site in the app and fails until a new one is a hold. - An HTML file on a record now renders inline by default, one frame per file in a many-file field too, sized to its own content height: a long page grows the frame instead of scrolling inside it, and a short page shrinks it. The `?view` response adds one inline script after the doctype that posts the page height to the parent; weave sizes a frame only from messages whose source is that frame's own window, and the page keeps its sandbox with no access to weave's origin. The stored file bytes are unchanged (Issue #1175). - An HTML file that saves its own edits to `localStorage`, such as a deck that autosaves, now keeps them in weave. The `?view` page gets an in-memory `localStorage` and `sessionStorage` seeded from the file's saved snapshot; each `localStorage` change goes to the parent window, which accepts it only from that file's own frame and saves it, debounced, through the new `PUT /api/files/:id/storage` (`GET` reads it). A reload or another device starts from the saved snapshot. The page keeps its sandbox, so it still cannot reach the API or weave's cookies. The file bytes never change; the edits live in the file's page storage, capped at 5 MB per file, and a larger write is refused with a message (Issue #1176). - An icon name the vendored set does not carry now draws an empty icon box. The lookup went straight into `innerHTML`, so a miss wrote the word `undefined` into the span, and because an icon box is 16px wide with its content centred, that word overflowed about 22px on each side and took taps there. On a phone the Share (Beta) button's miss reached back into Copy permalink's 44px target, 23px to its left (Issue #1235). - The open-table INP budgets rise to 160 ms on desktop and 112 ms on phone, with the measurements written under `raised` in `test/vitals-budget.json`; the old 80 and 64 ms budgets sat one 8 ms quantum above the green runs and failed on slower shared runners with no code change. A cheaper dock render will try to restore them (Issue #1215). - A new relation's inverse field takes the source table's name as written, so a table named `Action Items` gives `Action Items` instead of `Action Itemss`. On the many side it uses the plural term set on the source table's Name field, cased like the table name (`Deal` with the term deal/deals gives `Deals`); with no term, or on the one side, it keeps the table name. An explicit inverse name still wins, the field dialog's placeholder previews the same name, and existing fields keep their names (Issue #1262). - Opening a workspace renames an inverse field that the old rule doubled, such as `Action Itemss` on a table related to `Action Items`, to the name the new rule gives. A rename that would clash with a field already on that table is skipped, each rename is logged in the audit trail, and a second open changes nothing. Names like `Deals` from a `Deal` table are left alone (Issue #1262). - A long key in a key chip now breaks across lines on board tiles and the record page, so the whole value is readable instead of ending in an ellipsis or being clipped at the tile edge. The grid keeps its one-line chip with the ellipsis and the hover title (Issue #1153). - Fix: `DELETE /api/keys/:name/share` with `{"account":"bob"}` now revokes that account's share; the body was never read, so the call answered 200 and left the share in place (Issue #1086). - List: the add-row name field now keeps a 44 px hit area on touch, up from 32 px (Issue #1169). - The list draws its first 200 rows per group and the Board layout its first 40 cards a column, each ending with a **Show more** button that adds the next batch in place. A 2,000-row list now paints 200 rows and 3,455 elements where it drew all 2,000 and 28,636, and the Board layout 40 cards a column and 2,247 elements where it drew 667 a column and 26,666. What Show more drew survives a redraw, a row added past the cap is drawn, and the button keeps its height in a masonry gallery and a 44 px target on a phone (Issue #1266). - Every main push now runs its `tests` gate in its own concurrency lane, keyed by the commit it tests. A shared lane holds one pending run at a time, so a newer merge cancelled the one waiting behind it: eight merges in the half hour from 18:32Z on 2026-10-25 left nine consecutive main gates cancelled and the deploy watcher two hours behind the tip (Issue #1181). - Opening a workspace MCP address such as `https://.weave.grunion.ai/mcp` in a browser shows a page instead of raw JSON (Issue #1195). It names the workspace, shows the MCP address with a copy button, gives the `claude mcp add --transport http` command and the steps for Claude.ai and ChatGPT, and links the Connect an agent guide at `/docs`. The `401` keeps its `WWW-Authenticate` challenge, and MCP clients, JSON requests and requests with no `Accept` header get the same JSON body as before. - MCP file uploads can prepare a one-use raw-byte upload grant for a row or attachments field. Local scripts upload directly without sending base64 through the model or creating another editor token. Grants expire after ten minutes and enforce workspace, destination, byte count and current account permissions (Issue #1275). - **Hiding the sidebar hides the workspace rail too** (Issue #927): on a desktop window the hide button now sits after the workspace name, and hiding takes the rail and the sidebar away together, so the page runs flush to an 8 px gutter. The show button slides in at the head of the page's crumb row and the workspace icon and name move over to make room. Moving onto the left edge still peeks the nav, now with the rail beside it; a pointer parked on the edge no longer pops the peek open when the page scrolls or redraws under it. The floating `#nav-expand` button and the collapsed rail's route tiles (Issue #909) are gone. Gate: `test/nav-toggle-browser.test.mjs`. - A long table name in the left nav now ends in an ellipsis, with the full name on hover, and the nav no longer scrolls sideways (Issue #1116). - **Opening a row paints the record at once from the row the grid holds** (Issues #1057, #1059): the dock, and the full-screen record on a phone, open in the first frame with the name, the crumb and every field value the grid already has; the document, the remaining fields, lookups, files and references stream in when the entity request lands, and a value the grid painted is left in place unless the server holds a different one. The fetched entity stays in the warm cache, so reopening the row within five seconds sends no second request, and any write clears that cache. Measured with the API held one second: `open-row|desktop` and `phone-open-row|phone` now answer in the first frame instead of after the server. Gates: `test/dock-first-paint-browser.test.mjs`, `test/feedback-budget-browser.test.mjs`. - The entity pane keeps a 360px floor beside the View, Share and Activity panes. A window too narrow for every open pane at its floor hides the table panel until a pane closes, and if that is still not enough the View or Share pane floats over the right edge, so the record title no longer stacks one letter per line and the entity toolbar stays clickable (Issue #1286). - **The parity matrix and the Airtable and Fibery pages list Board, List and Forms as shipped** (Issue #1220): `docs/PARITY.md` scores Board / kanban, List view, Forms and row avatars ✅, which moves parity from 81.5% to 87.1%, and the comparison pages map Airtable's kanban, gallery and forms and Fibery's board, list and forms onto weave's. `test/docs-drift.test.mjs` requires those rows ✅ and refuses the old "removed in Issue #75" wording in the three docs. - A row, table or space permalink no longer carries `/w/`: `url` is `/e/`, `/t/` or `/s/` on every door, and the ⧉ buttons copy the same. A workspace host such as `net.weave.grunion.ai` already names the workspace. Anywhere else, a bare link opens in whichever workspace holds it: the server answers `302` to `/w//…`, or `301` to the holder's own host when workspace hosts are on. Renaming a workspace no longer breaks links, and links already shared with `/w/` keep working (Feature #203). - On a phone a docked record's table name ellipsizes inside its crumb chip, so the #number stays whole and clear of Copy permalink. A long table name used to run under Copy permalink and the header controls (Issue #1301). - On a phone, the row page lists its fields flush on the page: the outlined, rounded panel around the Fields block is gone, the rows keep their hairline dividers, and each label starts on the same edge as the section headings below it (Issue #1285). - On a phone, every control in the board and list views now takes a 44 px tap: the board's compare checkbox and short tile titles, and the list view's fold button, row titles, and state, select and multiselect chips. The chips keep their drawn size; each gets an invisible 44 px hit area, and the list row spaces its lines so neighbouring hit areas never overlap (Issues #1128, #1129, #1130, #1131, #1132, #1133, #1134). - On an iPhone the phone menu scrolls as one column: spaces, + New space, the System group and the identity panel move together, so the space list no longer shrinks to 108 px of the 595 px menu in WebKit with the iPhone 15 profile and the second space stays in view. A browser case fails if the list scroller holds under half of the menu or any space or table link leaves view, and the menu touch hold no longer names this row (Issue #1164). - Spaces are open or private. Open stays the default, so a one-person workspace sees no change. A private space shows itself to its members and the workspace architects only: anyone else gets a 404 on its tables and rows and finds nothing of it in search, the space and table lists, the schema, the ontology, the System registry rows, views, MCP tools, CSV and markdown exports, share links and the audit log. A relation from an open space into a private one shows a locked chip named "Private", and lookups, rollups and formulas leave the private rows out. A member may carry a role for that space that overrides the workspace role, so an Observer can edit in Sales alone, and a space Editor takes a paid seat. Workspace architects and the space's own architects manage its members (`PUT` and `DELETE /api/spaces//members/`, `weave_update_space` with `access`, `member`, `role` and `removeMember`). An agent token can be scoped to one space (`createAccount` with `space`), never reaches past the account that issued it, and writes an audit entry for every change it makes. A full JSON export needs access to every space (Feature #141). - `scripts/quality-scorecard.mjs` prints one JSON row per ISO week: generalization rate of UI fixes, repeat fixes per rule class, coverage per probe over the state catalog, field types seeded, width bands covered, ceiling totals, spacing and z-index token shares, reverts on main, and the probe answers on Issue rows. The nightly flicker routine writes it to the `Development/Scorecard` table (Issue #1120). - A bug report filed from the web UI asks **Which probe would have caught this?**, for whoever fixes it to answer (Issue #1120). - `weave_query`, `POST /api/tables/
/query` and `GET /api/tables/
/entities` answer 200 rows a page when the caller names no `limit`, and the answer carries `nextOffset` while rows remain, so an agent pages instead of pulling a whole table. `limit: -1` reads the table whole, as on `weave_audit`. `weave_query` also defaults `relations` to `chip`, sending each related row's summary once; `relations: "full"` keeps the embedded summaries. On the weave workspace's 3,980-row Case table, a `weave_query` with no limit drops from 18,915,798 bytes to 483,895, and the same REST call to 1,582,427 (Issue #1267). - The workspace rail lists workspaces in the order this browser last opened them, newest first, and the workspace you are in always sits first. Each workspace page load moves its name to the front of a `wv_recent` cookie (fifty names at most, same domain, path and lifetime as `wv_rail`), and `GET /api/workspaces` answers in that order; workspaces the cookie does not name follow in their usual order. The pinned weave mark keeps its place. A workspace whose session has expired shows as a grey dashed chip titled ": session expired, click to sign in" and still opens through sign-in (Feature #349). - With a base domain set, clicking another workspace in the workspace rail opens it on its own host (`.`) instead of serving it under `/w//` on the current host. The chip goes through `GET /api/auth/open/`, which hands the current session to the other host through the single-use sign-in code, so you arrive signed in; with no session that opens it, you land on the bare host and sign in there. A page link to `/w//…` asked on another workspace's host now answers 301 to that workspace's host, so old links and bookmarks land right. Without a base domain nothing changes (Issue #1263). - **Date ranges fit their box on board tiles and the phone record page** (Issues #1126, #1162): a range input sizes to its value outside the grid, a tile gives its range the full card row, and a phone field row stacks the label over a range, so "Jan 1, 1999 – Dec 31, 2099" reads whole instead of running past the input. The grid cell keeps scrolling (Issue #897). - The README opens on a fifteen-second clip under its lead paragraph: an agent builds a workspace from a spec with `weave build`, the table fills, a row opens with its document, then an MCP `weave_query` call returns rows. `scripts/record-demo.mjs` films it from a scratch workspace on a local serve and writes `docs/demo.gif` and `docs/demo-poster.png` (Feature #293). - CI jobs run on `ubuntu-24.04` by name. GitHub moved the `ubuntu-latest` label to ubuntu-26.04 on 2026-10-28, where the runner has 7.8 GB free against the test manager's 10 GB floor and the cached Playwright 1.62.1 WebKit build does not launch, so every PR gate lost five of seven shards before its tests ran; the scheduled compatibility matrix stays on `ubuntu-latest` (Issue #1256). - **Automations fire on a schedule** (Feature #312): a Workflows row's Script takes `trigger: {"type": "schedule", "cron": "0 9 * * 1-5", "zone": "America/Los_Angeles", "where": [["State", "!=", "Done"]]}`. The five-field cron expression is read on the zone's wall clock (UTC when no zone is given), and at each due minute the rule runs its actions on every live row of its table, or on the rows the `where` filter admits, as the automation actor, stamping Health and Last Run per run. The server ticks every workspace once a minute (`src/schedule.js`); a due minute fires once, a box that slept catches up with one run going back seven days at most, a new rule starts at the first tick that sees it, and a rule switched On after its minute waits for the next one. A bad cron, zone or where clause reads Setup incomplete with the reason. REST, MCP (`weave_create_automation`) and the CLI take the trigger as they take the others; the relation map pill reads `cron 0 9 * * 1-5`. Gate: `test/automation-schedule.test.mjs`. - Added the Share beta panel with contextual read links, anonymous submission links, previews and icon actions. Links support expiration presets, custom dates, Never, and regeneration that invalidates the previous URL. Workspace UUID addresses survive renames. Submission capabilities validate before every write and retry, keep receipts private, and cannot be bypassed through the form id. Named email/domain access and automatic observer membership remain deferred (Feature #343). - The Share (Beta) button on a table toolbar draws an icon again. It asked for `share`, a name the bundled icon set does not carry, so since Issue #1235 it drew nothing. It now takes `upload` (Issue #1233). - Share is one button and one pane everywhere. The Share up-arrow now sits in the header of the workspace, every space, table, record page, docked record and share page, beside the ⋮ and in its borderless style, and opens the docked share pane for that page. The old Share… dialog and the Share… rows on the table and record menus are gone. A record's pane lists the table and space links that already reach it, a second page's Share button takes the pane over instead of only closing it, and the button carries a 44 px touch target on phones (Issue #765, Issue #1222). - A narrow docked record keeps its breadcrumb inside its box: once the middle of the trail has folded into "…" and the crumbs left still overflow even at their minimum widths, the last-but-one folds into "…" too, so the current crumb keeps its #id and part of its name. A phone header still squeezes its space crumb before folding anything (Issue #765). - On a phone the header Share button is a 44 px glass circle beside the ⋮ on the workspace home, spaces and docked records, so it answers a full-size tap on both axes instead of a 16 px wide strip (Issue #1222). - Selected-entry links now omit related and computed fields from JSON, HTML and document exports, and query responses no longer carry counts or chip metadata from outside a narrow grant. Share API requests recheck the credential after receiving a request body (Issue #1207). - The sheriff takes no action on a main `tests` run that ended cancelled, skipped, neutral or any conclusion other than failure, and logs the conclusion it ignored. A failed run whose only red jobs were cancelled is no signal either, and a `CI gate` failure carried only by cancelled shards counts as no verdict for a rerun or a bisect probe. A test file the in-run retry turned green is no longer named as failing, and a red held only by files that already have an open `flakes on main` row escalates to a person instead of reverting a pull request. (Issue #1139, Issue #1282) - The sheriff no longer reverts a pull request when its rerun is cancelled, timed out, skipped or never reaches its `CI gate` job. Only a `success` or `failure` gate decides guilt; anything else is rerun once more and then handed to the next red main run. It also re-checks that main has not gone green on its own immediately before it opens the revert, and a revert whose range took no bisect probe names that in its subject. (Issue #1139) - A sheriff revert now sets the rows it undid back to work: it reads the `Issue #` and `Feature #` references off each reverted commit's subject and moves each row from `Fixed` to `Open`, or from `Shipped` to `Building`, with a dated line naming the revert PR and the commit it took off main. A row in any other state is left alone, and a dry run writes nothing (Issue #1297). - The sheriff names only the test files that stayed red after the in-run retry, so a file the retry turned green and a file whose shard ended before its retry answered no longer reach the Issue title. The unresolved files are listed under their own heading in the evidence instead. A bisect probe that goes red on a file other than the one holding main red is no longer read as a narrowing signal: the sheriff escalates with both file lists rather than convicting the commit the probe landed on (Issue #1282). - System returns to the foot of the sidebar, under your spaces and **+ New space**, with Workflows, Users, Activity and Trash as rows that fold from the System caret. The workspace rail holds workspaces only again. The edge peek timing stays (Issue #908). - `weave serve` checks its identity provider credentials at start and every 15 minutes. It sends a made-up code to the token endpoint with the configured client id and secret: `invalid_grant` means the provider accepted the client, while `invalid_client`, `invalid_scope` or `unauthorized_client` mean sign-in is broken. `/api/health` reads the result as `auth`. Anonymous callers see only `auth.ok`, and architects also see the status, the provider's error code and the time of the check. The top-level `ok` stays true, so a rotated secret never fails the Railway health check or rolls back a release, and the promote workflow writes one alert line when it lands a commit while `auth.ok` is false (Issue #1178). - When the identity provider refuses weave's own client (`invalid_client`, `invalid_scope` or `unauthorized_client`), the sign-in callback shows "Sign-in is broken on weave's side" with no retry button and logs an ALERT line, on the workspace sign-in and on `/start` alike. Every other failed sign-in keeps "Back to sign in" and adds "Use a different account", which ends the provider session or asks it for a fresh login, so a live provider session no longer approves the same failing account again (Issue #1179). - `scripts/signup-e2e.mjs` and the `signup-e2e` workflow walk production sign-up the way a visitor does: a Clerk test user signs in through a one-time link, clicks Start with weave on grunion.ai/weave, creates a workspace on `/start`, gets the unpaid 402 and the billing page, with a screenshot per step; the workspace and the Clerk user are deleted afterwards. It runs on demand until the grunion.ai link points at the sign-up entry (Feature #334). - `GET /api/spaces/:id/home` returns a space's tables with live row counts and the automation runs that changed its rows, wherever the automation started. A run from a space the reader cannot open stays off the list. A share link gets a 403. - An `automation-ran` activity entry records `failed: {step, error}` on the row where the run stopped. - The recents list now holds saved table views and the time each item was opened. The command palette still shows the newest eight. - Opening a space on desktop or tablet lands on one continuous home page: direct table links with row counts, your recent views, your recent rows beside the automation runs that changed rows here, then a Space overview disclosure holding the description, summaries, tables and relation map. Empty sections disappear. A run opens its affected rows, and a partial run names its failed step. - The space home keys what it lists by table name and no longer says rows: **Recent** groups what you opened under each table, and a run reads `Ops → Sales · Deal 2, Contact 1`. - A space opens as a launcher on a phone: a search box scoped to the space, one tile per table with its count, recent views and records, automation runs that changed records here with any failed step, and About this space for the description, summaries and relations. Sections with nothing to show disappear, and every control is at least 44 px (Feature #351). - The phone launcher labels records by table name, matching the desktop space home: "Deal 2, Contact 1" on a run and "Deal #48" on a recent record (Feature #351). - Board: a stacked layout with two or more grouping levels now draws every level, each with its own header and count, and nests the cards in the innermost group. Only the innermost groups were drawn before, so a repeated inner label gave no way to tell which outer group it belonged to (Issue #1204). - New workspace setup now reaches its name step in a browser that refuses cookies. `/api/start` answers with a PostHog key wherever analytics is configured, so `boot()` started the consent banner before it drew the first step, and the consent code read `document.cookie` without a guard. In a sandboxed browser that read throws, and because only the `/api/start` fetch sat inside a `try`, the throw escaped and the page kept its initial "Loading weave…" status for good, with `GET /api/start` as the only request in the network log. Reading and writing the consent cookie now tolerate a browser that refuses it, and a failed analytics start can no longer stop the setup flow from rendering (Issue #1261). - The chosen workspace row sinks and darkens the moment it is tapped, in both themes, so the opening state is visible as well as named (Issue #958). - Opening a workspace link from the start page, the workspace switcher or an invite while signed out now lands on the sign-in page, and signing in goes on to the workspace. Before, the browser showed a raw "Sign in first" error with no way back. A signed-in person whose account cannot open that workspace gets a page that says so, with a link to their own workspaces (Issue #1284). - **Every automation action yields an output, and later actions read it** (Feature #313): `set-field` yields `{field, value}`, `append-doc` `{field, text}`, `add-comment` `{id, text}`, `upsert-row` `{id, publicId, name, created}` and `webhook` `{url, sent}`. A later action reads an earlier one as `{{steps.1.publicId}}` in its text, in a `set-field` string value (which now takes the same `{{…}}` templates as text actions) or as `steps.1.name` in an `upsert-row` source path, so "open the account, then write its number back on the request" is a three-action rule. A step not yet run, or a path it has not got, reads empty. The `automation-ran` activity entry carries each row's outputs under `steps`, in action order. Gate: `test/automation-steps.test.mjs`. - A guard holds the table-open query to one page with chip relations and a field projection, so a 2,000-row table opens in 234 KB instead of the 5 MB whole-table payload that froze the tab (Issue #1194). - A test that needs a scratch directory calls `tempDir(prefix)` from the new `test/lib/temp.mjs`, which removes every directory it opened when the process exits, and all 218 bare `mkdtempSync(join(tmpdir(), ...))` calls under `test/` now go through it. A suite run under the test manager was already clean, since the manager gives each job its own `TMPDIR` and removes it; a run outside the manager kept the caller's `TMPDIR` and left hundreds of `weave-*` directories behind, which starved the 10 GB disk floor the manager needs to admit a browser lane. Gate: `test/temp-dirs.test.mjs`. (Issue #1083) - **Board tile titles wrap instead of ending in an ellipsis** (Issue #1123): a long card title now wraps inside the tile on desktop and phone, and the tile grows to hold it, so the full name reads without opening the record. - A toggle's label now wraps beside its switch on board tiles and on the record page instead of ending in an ellipsis. "Published to every customer" lost 81 px on a desktop tile and 8 px on the phone record page; the table cell keeps its one-line label because the column floor is measured from it (Issue #1156). - **Move to trash answers in the first frame** (Issue #1058): the selection bar's trash button waited for every DELETE before the rows left the grid, about 0.9 s with the API held 1 s. The chosen rows, the selection bar, the loaded count and a dock open on one of them now leave in the same frame as the press, the "Moved to trash" toast with Undo shows at once and the deletes run in the background in parallel; a refused delete puts the rows back with an error toast and drops the Undo when nothing reached the trash. Undo and ⌘Z wait for a pending delete before they restore and skip the grid reload the delete would have run, so pressing Undo before the server answers goes straight to the undo and still brings every row back. The record, table, space, view and field "Move to trash" menu rows show their toast at once as well and queue Undo behind the pending delete. `trash-undo|desktop` drops from 1200 to 120 ms in `test/feedback-budget.json`. Gate: `test/trash-optimistic-browser.test.mjs`. - A text, email or key value now rests as wrapping text on a board tile and on the record page, so a long unbroken word breaks anywhere and shows in full instead of running out of its input; a click on the text opens the input, as it does for text with inline marks. The grid keeps its one-line cell with the ellipsis at rest (Issue #1161). - The View pane now sits rightmost, after the entity and context panes, with the same 16px gap every pane keeps between neighbours (Feature #340). - The full-screen file viewer no longer shows a Back button beside its close ✕. It did nothing for an HTML file, whose sandboxed frame keeps its history out of reach, and only repeated the ✕ for every other kind. Refresh, previous and next, open in a tab and ✕ stay; Esc still closes (Issue #1174). - While a file is open in the side panel or the full-screen viewer, the floating Report a problem button steps aside and the same button sits in the viewer's own bar, so it no longer covers the file's controls in the bottom corner. Its report panel opens under the bar button, and Esc closes the report panel before the viewer (Issue #1193). - Deleting a workspace from Settings opens its own **Delete workspace** dialog. The dialog names the workspace and what moves with it, and its Delete button stays off until you type the workspace's exact name. Cancel, Escape or a click outside closes it with nothing deleted, and the message after a delete still carries Undo (Issue #1171). - **Delete workspace… is in the workspace menu, and it is permanent** (Feature #347): the ⋮ beside the workspace title and a right-click on a rail chip (or a click on the current one) open the same menu, and its one row opens the **Delete workspace** dialog. The dialog says the delete cannot be undone and arms only on the exact name. The workspace leaves the rail for everyone, its address answers 404, and there is no Undo and no Trash entry; the hub keeps a tombstone only the operator can restore. The row shows only where you may delete that workspace, so the default workspace has no menu. Deleting the workspace you are in opens the next workspace on the rail; deleting your last one lands you on the weave workspace, which a sign-in whose only workspace was deleted still reads as an Observer. - **Invite people… and Link preview before sign-in left the workspace menu** (Feature #347): invite from the **Invite** button beside the workspace title, and turn the link preview on or off with its switch on the **Settings** page (or `weave workspace link-preview`). - Each workspace owns its System registry and Users table. Relations to users, forms, workflows and registry edits stay in that workspace. Hub routing no longer grants access through another workspace's architect account (Issue #979, Issue #1224). - Existing hub registries migrate with current row IDs retained, references remapped and destination data saved before source rows become inactive. Unsupported cross-workspace references and protected workspaces without local accounts stop migration before writes. - Workspace creation gives its creator a local account and follows hosting signup and billing rules. Any ordinary workspace can be deleted while another live workspace remains; routing aliases survive full backup and restore. ## v0.4.109 (2026-10-25) - A field or document write inside one create or update marks its row dirty, so a value applied after a nested save in the same call reaches disk. An automation created enabled now reads as enabled after the workspace is reopened, and still fires (Issue #1085). - The billing page draws its muted summary and back link at 5.11:1 on the canvas, up from 4.31:1, by reading the app's own light canvas-muted shade (Issue #1097). - Danger labels read 7.45:1 on the dark panel, up from 3.69:1: the dark theme now carries its own `--tblr-danger` instead of inheriting the light red (Issue #1099). - The `tests` and `dev-soak` workflows bound each `playwright install --with-deps` attempt at five minutes and retry it once, the way `pr.yml` already does, after a trickling mirror held that step for thirty minutes and cancelled two jobs of the `CI gate` on main (Issue #1107). - The list layout prints each shown field's label at full strength, so it reads 5.9:1 against the row in light instead of 3.4:1 (Issue #1096). - The Claude Code plugin manifest carries the Anthropic directory listing fields: the weave icon and the docs, support, privacy and terms links, so a directory submission reads them from `plugins/weave` (Feature #332). - The PR browser tier now selects by the DOM classes and route literals the changed blocks name, not by file name alone, so a change to a shared module such as `public/app.js` or `src/routes.js` runs the journeys that exercise it before it merges (Issue #1075). - Each `playwright install --with-deps` attempt in a PR browser job is bounded at five minutes and retried once, and apt gets a twenty-second acquire timeout with three retries, so a trickling mirror costs a retry instead of the `PR gate` (Issue #1107). - The form builder's intro and field hints read 8.7:1 on the dark panel, up from 3.56:1: `.text-muted` now takes weave's own secondary token instead of the vendored fixed grey (Issue #1098). - Every door emits one permalink per row: `url` is `/w//e/` on API reads and creates, MCP row reads and verbose writes, CLI output, chips and cards, every search hit and the bug reporter's receipt. Tables and spaces answer their `/w//t/` and `/s/` permalinks, views `/w//#/view/`. Every row parameter takes a pasted permalink, its path, `/e/` or a bare uuid, and search ranks the row a pasted link or uuid names first (Feature #203). - **View opens a pane beside the table** (Feature #340): the view picker, then Filters, Sort, Group, Fields and Display as an accordion, with Reset view and Clear all at its foot. The toolbar keeps Search, View and the table menu; the View badge counts active filters. Sort takes three levels, Group five nested levels in Table and List layouts, with subtotal rows per group (Every level, Top level, None) that stay on collapsed groups. A field sits in Sort or Group, never both; empty values sort last and group under No value; a multi-select row appears under each value and counts once above; deleted rows show struck through and never count. The server computes every subtotal: `weave_query` with a grouped view returns `groups` and `groupTotal`, `weave_stats` with `by` carries the same rollups, and the grid draws from that one answer. The phone table menu gains the same sort and group levels. Gates: `test/view-pane-combinations.test.mjs` (1,920 setups, 14 checks each, rule tests) and `test/view-pane-browser.test.mjs`. ## v0.4.108 (2026-10-23) - The accessibility gate scans 13 routes in both themes (tables, board and list views, the entity page with its editor, the map, settings, trash, activity, the forms builder, a published form, start and billing) plus six open states on a table (field dialog, command palette, filter and sort popovers, share dialog, row menu), and holds today's 17 known violations in a ratcheting `test/a11y-baseline.json`, each tied to its Issue row (#1096 to #1101) (Issue #1081) - **The activity feed answers one page by default** (Issue #940): `GET /api/activity`, `weave_activity` and `weave activity` returned every event a workspace holds when the caller named no `limit` (26,067 rows and 13.7 MB on the weave workspace). `activityFeed` now answers its newest 500 when no limit is given, the page the web UI already asks for (PR #289), so all three surfaces share one default; `limit: -1` (`--limit -1`, `?limit=-1`) still reads the whole log, and `total` keeps counting every event so `offset` pages through it. The `weave_activity` description, the CLI help and the `WEAVE_MAX_ROWS` handbook row say so. Gate: `test/activity-default-page.test.mjs` (engine, route, MCP and CLI). - **Activity filters read as chips at rest** (Issue #980): only the selected filter had a background, so Description and Fields read as headings. Every filter now carries the soft chip (`--tblr-bg-surface-secondary`) and the selected one the primary tint, in both themes, with the 44 px phone targets kept. Gate: `test/phone-activity-sheet-browser.test.mjs` (both themes, phone and desktop). - **The Activity head names its record** (Issue #979): the head read only "Activity", and on a phone the record sits hidden behind the sheet. It now reads ` › Activity` on every width; on a phone the record crumb is a 44 px button that closes the sheet and takes back its history step. Gate: `test/phone-activity-sheet-browser.test.mjs`. - **History follows an account across a rename** (Issue #699): a signed-in request set the actor to the account's name, and audit lines, activity, `createdBy`/`modifiedBy`, document revisions, comments, share links, invites and credential owners stored that string, so renaming `kyle` to `kyle.adriany` left every earlier line naming an account that no longer existed. A signed-in write now stores `account:` (and `account: via MCP` over OAuth), and every read path (the API, MCP, CLI and the web UI through them) renders the account's current name, or `removed account` once it is deleted. Lines written before this change are not rewritten and keep rendering the name they hold. Credential grants now record the account id too, and a credential owned or shared under an account's name still opens for that account. Plain writers (`web`, `local`, `mcp:`, `bug-report`, `share:
--view ` and `POST /api/tables/:table/query {view}` take a table view by id or name and answer the rows that view shows. The engine's new `viewQuery(table, view)` is the one server-side translator: each saved filter becomes `[field, 'in', names]` (a toggle's on and off names become `true` and `false`, as the grid sends them), ANDed with the caller's `where`, and the view's sort and search apply unless the caller passes its own. An unknown view answers not-found with the views the table has. Gate: `test/query-view.test.mjs` (the translation, the merge rules, and the same fixture through MCP, CLI and REST). - **Sorted table pages stop re-sorting the whole table on every request** (Issue #1093): `query` computed the sort key of every live row on every page, so the first page of a 50,000-row table sorted by a lookup took 81 ms (p50) and 94 ms (p95) over HTTP, and `GET /api/tables/:id` walked every entity in the workspace to count rows (17 ms). The engine now keeps the undressed sort keys, the sorted row order of an unfiltered query and the live row count per table on its row index, and drops all three on every write (`#mark`, `save`, `maybeRefresh`, a rolled-back write). The API shape and the result order are unchanged: `test/query-sort-cache.test.mjs` compares the cached and uncached answers byte for byte across sort directions, nulls, ties, deleted rows, lookups and a rollup, and after every kind of write. `scripts/bench-query.mjs --rows N` builds a scratch workspace and prints p50 and p95 for five requests plus RSS; `test/query-volume.test.mjs` keeps the 50,000-row lookup-sorted first page under its budget in the unit lane. - **The 50,000-row query budget is a GitHub runner figure** (Issue #1093): `test/query-volume.test.mjs` shipped with a 6 ms p95 budget measured on a laptop (3.6 to 4.5 ms warm); the unit-shard readings on GitHub runners across the next five PR runs were 6.0, 6.5, 6.6, 9.1 and 9.2 ms (the two highest on PR #408, which touched no engine code), so the gate turned red on unrelated PRs. The budget is now 15 ms, the worst observed runner reading (9.2 ms) plus 50 percent rounded up, the failure message says the number is a runner figure, and the test prints its measured p50 and p95 as a diagnostic line on every run so the next budget comes from the logs rather than a laptop. - **The reference cards fold** (Issue #993): the References and Referenced by cards in a record's side column had no control, so a long reference list could not be put away the way Activity and comments can. Each card now carries a head caret that hides its chips and keeps the `References · N` count in view. The fold is remembered per table and per card in the same `localStorage` store document sections use, so every record of that table opens the card the same way. Gate: `test/references-fold-browser.test.mjs`. - **Reference chips in the editor draw on one line again** (Issue #991): the editor's reference overlay slot carries `wv-prose`, so its chip took the read-view `white-space: normal` and broke between the State pill and the title, doubling the slot to two lines and laying it over the text after it. `public/chip.css` now keeps a chip inside `.doc-ref-slot` on one line; a title too long for the column truncates with the slot's ellipsis. Gate: `test/doc-ref-chip-one-line-browser.test.mjs`. - Workspace pages keep opening when the browser blocks access to local storage (Issue #956). - Returning to weave resumes the workspace explicitly selected in this browser, including workspaces opened through a root account. Listing workspaces preserves that choice, and revoked access still falls back to an accessible workspace (Issue #957). - **A filtered table rollup reads its child table once per query** (Issue #999): a Spaces rollup with `via` and a `where` listed and filtered the whole child table each time a row resolved it, so 1,000 rows that look up the same rollup ran 1,000 filter passes. A query now keeps one filtered child list per rollup field and every row reuses it; the list is dropped when the query returns, so the next query sees every write. A grid of 1,000 rows over a filtered 10,000-row rollup went from 45.2 s to 0.9 s. Gate: `test/rollup-shared-rows.test.mjs`. - With the thirty-minute apex sign-in gone, a visit to the weave.grunion.ai root opens the workspace named first on the `wv_rail` cookie, on its own host and last page, instead of the sign-in step. A pending invite now keeps the start list so it can be accepted (Net #124). - An apex row, table, space or share link now opens on the host of the workspace that holds it rather than always on Net (Issue #992). - A signed-in visit to the weave root now opens the workspace used last, on the page it last had open, instead of stopping on the workspace list. A deep link still wins, a gone or revoked workspace falls back to the first one the sign-in opens, and the list appears only for a sign-in with no workspaces, from the in-app Find my workspaces item, or after Use another account and Add login, which return to `/start?pick=1` so the second login's workspaces show beside the first (Issue #977). This re-lands PR #293, which the sheriff reverted in PR #314 because four browser tests still opened `/start` signed in and expected the list. - Additional workspace naming starts blank instead of reusing an occupied account address. (Issue #1014). - Setup drafts survive refresh within the same signed-in browser session; interrupted creation never automatically replays. (Issue #1009). - **The setup page art keeps the mark at one size in any window** (Issue #990): the stage beside the sign-in pane drew an 816 x 528 landscape art with `object-fit: cover`, so a narrow or portrait window (900 x 1000, an iPad held upright) scaled it about 4x and showed two strands of the mark at poster size. With `object-fit: none`, the stage draws a 3840 x 2160 art at its own scale, so the mark stays 320 px wide whatever the window and rings reach every edge of a stage up to 4K. `brand/build-radiate.mjs` takes a canvas size and writes both the half-letter marketing pair and the `public/brand/weave-radiate-{light,dark}.svg` stage pair; it also carries PR #200's half-letter asset, which this change supersedes. Gates: `test/brand-radiate.test.mjs` (4K canvas, mark scale, edge fill, ring pitch, public copies match the generator) and `test/start-stage-browser.test.mjs` (stage at 900 x 1000, 1024 x 1366, 1440 x 900 and 2560 x 1440; single pane at 800 wide). - **Share links and workspaces get generated isolation suites** (Issue #1079): `test/security/share-matrix.test.mjs` reads the mode rungs from `src/shares.js` and asserts every mode (read, comment, edit, manage) on every scope kind (entity, view, table, space, workspace) against 15 share-door probes (page GETs, a page POST, the scoped API reads, a comment, PATCH, POST and DELETE of a row), 300 cells, with the refusal text checked and every refused write leaving the rows and audit log unchanged. Expired, revoked and private grants get a row each, and the shared table page is held to rendering a many-relation cell as names joined by ", " and a one-relation cell by its name. `test/security/cross-workspace.test.mjs` derives every resource route from the dispatcher and sends each, under all five methods, with workspace A's ids from workspace B's base path with B's editor token and B's session: every probe is refused (401, 403 or 404), none carries A's data and A's audit log does not grow; A's share token and account token open nothing under B, and B's credentials open nothing under A. No leak was found. Tests only, no `src/` change. - The phone sheet grabber reads its dismissal distance from the release event, so a drag past a third of the sheet closes it even when the finger's last movement arrives with the pointerup instead of as a pointermove. Under load the final move could coalesce or land after the release, leaving the measured distance short, and the sheet sprang back. (Issue #1064) - **The sign-in guide covers the account chip and per-workspace sign-out** (Issue #718): the Door C Handbook guide described sessions only from `/auth`, and never said where to see who you are signed in as or what the sidebar menu's two sign-out rows do. A new section, Your account and signing out, covers the identity panel at the sidebar foot, **Sign out** (ends this workspace only) and **Sign out everywhere** (one `POST /api/auth/logout?everywhere=1`), signing out of a workspace opened through the hub root (the root stays signed in, Issue #716), and the 30-day idle rule applying per workspace. Gate: `test/deploy-artifacts.test.mjs` (1 new case: the guide names every menu label the app still uses and the section covers the idle rule, the hub-root mark and the single call). - **Sign out everywhere sends one request** (Issue #719): the account menu's Sign out everywhere posted `/api/auth/logout` once per workspace in the rail under `Promise.allSettled`, so a slow or refusing workspace left the sweep half done while the page still landed on the signed-out sign-in page, and the per-workspace calls never cleared the start cookie or the rail. It now posts `/api/auth/logout?everywhere=1` once (Feature #280), which ends every session the browser holds and clears the start cookie and the rail in one response. The per-workspace loop runs only when that call answers 404, for a server older than Feature #280. Gate: `test/account-chip-browser.test.mjs` (the recorder now keeps the query string; 1 rewritten case asserts exactly one `?everywhere=1` call, 1 new case asserts the 404 fallback posts each workspace's logout and still lands on the sign-in page). - **Signing back in no longer shows the setup donut** (Issue #987): a signed-out visitor at the weave root lands on `/start`, whose sign-in pane is also setup step one, so a returning person saw "Step 1 of 4 · Account" at 25% and read it as being sent back through setup. `progress()` in `public/setup-core.js` now returns nothing for the account step while the visitor is signed out, so the sign-in and create-account forms show no donut. The donut first appears on the workspace step at "Step 2 of 4", half full. Gates: `test/setup-core.test.mjs` and `test/setup-flows-browser.test.mjs`. - Explicit signup intent survives the legacy auth redirect and opens the signup pane. (Issue #911). - Empty and general setups appear before the template catalog. (Issue #1012). - The workspace picker shows loading feedback immediately and names the workspace being opened (Issue #958). - **A start page tile no longer flickers under a resting pointer** (Issue #1001): hovering an agent tile on `/start` lifted it one pixel, which moved it out from under a pointer resting on its bottom edge, dropped it back, and repeated every frame. The hover now changes the border colour only. Gate: `test/start-tile-hover.test.mjs`. - **The supervisor streams a release download to disk** (Issue #954): `unpackCommit` read the whole codeload tarball into memory (19 MB, copied twice) and `unpackRelease` gunzipped it into one 44 MB buffer, so the idle production supervisor kept about 120 MB after each swap (166 MB private memory against about 50 MB for a bare Node process). The fetch body now runs through `zlib.createGunzip()` into `streamTar` (`src/backup.js`, sharing `readTar`'s header parsing), which writes each entry to the `.part` directory as it arrives. The path-escape refusal still runs per entry, and a truncated or corrupt archive is refused. On a 36 MB fixture one unpack now adds about 7 MB RSS instead of 127 MB. Gate: `test/supervisor.test.mjs`. - **The System space page says why it offers no space controls** (Issue #849): `showSpace` passed `actions: space.system ? [] : [...]`, so the registry space drew an empty header toolbar: no Share, no Use template and no ⋮. A reader who reached that page by URL or by search found nothing to act on and no reason given, which is what the report behind this row asked about. The header now carries one line where the controls would be, `Weave owns this registry space, so it cannot be shared, used as a template or deleted.`, in secondary type. The controls stay absent, deletion is still refused in the engine, and a user space keeps Share, Use template and its ⋮ menu. Gate: `test/system-space-menu-browser.test.mjs` (4 new cases, both themes: the note is visible, readable against the page and names sharing, templates and deletion; the three controls stay gone; a user space carries no note and keeps Share). - **A view saved as Table stays Table** (Issue #740): the engine used to delete `layout` when it was set to `table`, so a view someone switched to Table read back exactly like one nobody had touched, and a phone could never tell them apart. `layout` now has three states: `list`, `table`, and unset. `weave_table_view`, `weave table view --layout`, REST and the System/Views `Layout` cell all keep `table`; `null` (or an empty `Layout` cell) clears the choice, and Reset view clears it too. Gate: `test/list-layout.test.mjs` (a Table choice reads back as `table`, survives `describeSchema`, and `null` clears it). - **A table query reads its own table, not the whole workspace** (Issue #998): the engine keeps a per-table row map beside `state.entities`, updated on create, the create rollback and hard delete, and rebuilt whenever the state object is replaced (load, refresh, import, rollback). `listEntities`, the query path, permalink and `#id` lookups, the registry row lookup and the workflow row list read it. `#queryIn` walks the table once and derives the live count, `countAll` and `trashCount` from that one pass. A 100-row query in a 50,000-row workspace went from a 109.8 ms median (336.3 ms p95) to 3.6 ms (17.2 ms p95) on the same machine. Gate: `test/engine-table-index.test.mjs` (no whole-workspace enumeration, fewer than 200 rows read, a 30 ms p95 budget, and the map following create, trash, restore, purge, undo, failed create and import). - **`npm test` fails in seconds when the Playwright browser is missing** (Issue #557): a worktree whose Playwright resolved but whose browser executable was not installed ran the whole unit lane, printed its green `# pass` block, then failed every browser file at launch and, on 2026-09-29, never exited. `scripts/test.mjs` now launches the browser once before either lane when the selection holds a browser suite; when the launch fails it prints `# BROWSER MISSING: ` and the `npx playwright install ` line, and exits 1 without starting a lane. Every run now ends with one `# RESULT green|red: unit lane ..., browser lane ...` line, so a first-lane summary can no longer be read as the verdict. Gate: `test/test-runner.test.mjs` (4 new cases, including an end-to-end `npm test` against an empty `PLAYWRIGHT_BROWSERS_PATH` that must exit 1 within 60 s). - **Undo after moving a table, space or share page to the trash restores it in place** (Issue #856): `trashRow` reopened the object on Undo whenever it had been open at delete time, so a reader who had since moved to another page was pulled back to it. The trash row now records the page the delete left the reader on. Undo reopens the object only when the reader is still on that page; otherwise it restores, leaves the current page alone and shows `Restored` with an Open action. Gate: `test/menu-trash-browser.test.mjs` (3 new cases: a table, a space and a share page trashed while open, then the reader opens another table; Undo keeps that table's hash and Open in the toast goes to the restored object). - Hosted New workspace opens the shared setup flow instead of root-only workspace administration. (Issue #1007). - **A workspace link signs in on the setup page** (Issue #994): with `WEAVE_BASE_DOMAIN` set, a signed-out visit to a path-mounted workspace link such as `/w/weave/e/1` used to go from `/w/weave/auth` straight to the identity provider, which showed Clerk's plain hosted sign-in page, and its Sign up link skipped onboarding. The workspace `/auth` now answers `302 /start?next=`; `/start` keeps a `next` only when it is a same-origin `/w//` path. The in-pane Clerk sign-in then goes on through that workspace's sign-in and lands on the linked page, a sign-up goes to the setup steps, and a visitor already signed in on `/start` who opens that workspace goes straight on. `/w//auth?signed-out=1`, workspace hosts, invite links and deployments without a base domain keep their sign-in as before. Gates: `test/setup-routes.test.mjs` and `test/workspace-signin-start-browser.test.mjs`. ## v0.4.107 (2026-10-17) - **The Activity page loads one page, not the whole log** (Issue #850): `showActivity` asked `GET /api/activity` with no `limit`, and the route's `boundedLimit` falls back to `null`, so the view received every event the workspace had ever recorded and painted a `tr` for each one. On the weave workspace that is 26,067 rows and a 13.7 MB answer, and the hop from a table to `#/activity` took 10.0, 10.1, 10.4 and 11.0 seconds against 283 to 387 ms for a 1,300-row Issue table. `ACTIVITY_PAGE` (500) now rides on the request, the same page size the record panel already asked for, and the note says `Showing the newest 500.` whenever the feed holds more. The same four hops on the same database now take 161, 318, 316 and 298 ms with 500 rows painted. `activityFeed` keeps its `limit: null` meaning for the engine, the CLI and MCP, so no verb changed. Gates: `test/activity-page-size.test.mjs` (5 cases), `test/activity-page-size-browser.test.mjs` (4 cases, both themes). - **Actor chips ask Users for the handles they draw** (Issue #850): `workspaceUsers()` read the whole Users table with `POST /tables//query {}` and cached it, which put a full table read behind the first Created By cell, record panel chip or Activity row on every page load. `actorChipEl` now names the one handle it needs, `workspaceUsers` collects the handles a paint asks for and `flushUsers` sends them as one `where: [['Name', 'in', want]]` query per microtask. Opening `#/activity` on the weave workspace sends one Users query naming four handles instead of a bare whole-table read. Grid, record panel and Activity chips resolve as before, in both themes. Gate: `test/activity-page-size-browser.test.mjs`, `test/system-fields-people.test.mjs`. ## v0.4.106 (2026-10-17) - The doc-kinds browser waits carry a draw budget and report page state on give-up, so a slow mermaid load no longer needs the gate retry (Issue #364). - **One meta tag style names a field type** (Issue #773): `.term-tag` set the monospace face and `.wv-tag` set none at all, so the same 11px secondary type word came out mono in the field editor tray and sans on every other surface. `.wv-tag` now declares the face once, `font-family: var(--tblr-font-sans-serif)` with `font-size: var(--fs-meta)` and `color: var(--tblr-secondary)`, and `.term-tag` is gone: the plural row builds a `.wv-tag` like everywhere else and keeps its 44px right-aligned column through `.term-plural-row .wv-tag`, a parent rule, so layout is the parent's business and the face is the tag's. Sans won on call sites, since the row's own fix shape kept the `.wv-tag` name and asked `term-tag` to give up only its font; it is also the face the surrounding field names and labels already use, and the type word reads as a label rather than as code. The row measured 19 `.wv-tag` uses against 9 `.term-tag` at v0.4.88; Issue #775 has since taken the computed type words off record field rows, so the live counts are 4 against 1, and the two faces were the only thing left to settle. `tools/ui-catalog` drops its now-dead `term-tag` entry. Gate: `test/ui-contract.test.mjs` asserts `.wv-tag` names the sans face at `--fs-meta` in secondary, declares no layout of its own, that no `.term-tag` rule or call site survives, and that the 44px right-aligned column comes from the parent rule. - **Collapsing the nav no longer hides Activity, Workflows, Users and Trash** (Issue #909): `#app.nav-collapsed #sidebar { display: none }` removed the sidebar subtree, and the four pinned routes lived inside it, so a collapsed nav left only workspace switching and reaching Trash meant reopening the nav first. The 52px rail now carries the same four routes as icon buttons below the workspace avatars, behind a hairline, each with a tooltip label and an `aria-label`, the current one drawn as a filled `.ws-icon.active` tile carrying `aria-current="page"`. `#rail-routes` is a static slot in `public/index.html` that `renderNav` fills from the same `registryTable('workflows')` and `registryTable('users')` reads the sidebar's System group uses, so the two never disagree about which route is current. CSS shows the slot only under `#app.nav-collapsed:not(.nav-peek)`, which keeps exactly one visible copy of each route in every state: the sidebar group when the nav is open or peeking, the rail when it is collapsed. Labels under the icons stay out; that is the width decision the row left open. Gate: `test/ws-rail-routes-browser.test.mjs` (3 cases: every route visible and one click away in the collapsed rail, a divider above the group with the clicked route filled and the only one carrying `aria-current`, and no route visible twice with the nav open). ## v0.4.105 (2026-10-17) - Marketing kit in `brand/kit/`: the weave badge (DATA · AGENTS / SYSTEMS) in 15 treatments, release and footer badges, link-preview, square, banner and story posters, and contour, rope and dot patterns grown from the mark, each as SVG and PNG. `manifest.json` names every asset's code, size, ground and use, the README carries recipes for site, email, social and print, and `test/brand-kit.test.mjs` keeps the files in step with `build-kit.mjs`. (Feature #347) - **Reversing a scroll costs nothing** (Issue #938): the row window kept one buffer of drawn rows behind the reader and two ahead, picking the deep side from whichever way the reader was going, so a reversal mid-fling spent that one buffer and reached rows the window had dropped. It now holds two buffers on both sides and reads no direction at all: `windowFor` loses its `direction` argument, `travelFor` and the `win.dir`/`win.lastTop` state go with it, and arriving at a row from above draws the same window as arriving from below. PR #269 held the same shape and was reverted because the cut-off gate counted the extra rows parked below the fold; Issue #937 taught that probe to read only what the viewport shows, so the gate no longer moves with the window. Measured against `origin/main` on the same machine, every cut-off count is identical. Gates: `test/grid-window.test.mjs` (two cases: two buffers on both sides, and a stray `direction` argument moves nothing), `test/grid-window-feed-browser.test.mjs` (1 new case: the drawn rows reach as far behind the reader as ahead, whichever way the reader came; red on the old window at rows 354-428 against 374-448). - **The new-row sheet gesture case stops needing a retry** (Issue #903): `test/phone-new-row-sheet-browser.test.mjs` measured the grabber while the sheet was still springing back from the short drag, so the next drag reached for a point the grabber had already left, and on a loaded hosted runner the long drag missed the grabber and the sheet never dismissed. Measured here in WebKit on an iPhone 15 profile: at pointerup the grabber sits 39px below its resting position with one animation still running, while the sheet's inline `style.transform` already reads empty, which is why the case's own "leaves no transform behind" check could not see the motion. `dragSheet` waits for `atRest` before it measures, the sheet's open animation is awaited before the first grabber measurement, and the spring-back is now asserted at rest through a computed identity transform. The dismissal and no-write assertions are unchanged. Ten consecutive runs under `WEAVE_CPU_THROTTLE=4` on macOS: `# pass 15 / # fail 0 / # skipped 0`, no retry. Gate: `test/phone-new-row-sheet-browser.test.mjs`. - Consent-gated in-app usage events: when `POSTHOG_KEY` is set, the app shell loads the same cookie banner as /start, adds a Cookies row to the personal menu, identifies a signed-in person by the Clerk user id (on /auth too), and after Accept records `app_opened` once a day plus the named actions `workspace_created`, `table_created`, `row_created`, `rows_duplicated`, `rows_imported`, `template_used`, `invite_sent`, `invite_accepted` and `agent_token_created`, with fixed values and counts only. The privacy policy names them. (Feature #337) - **The rail-inset gate now measures something on every machine** (Issue #636): `test/ws-rail-inset-browser.test.mjs` ended its measurement with `t.skip` whenever the page reserved no scrollbar gutter, so on this Mac it asserted nothing and a clean suite could never report `skipped 0`. The gutter is unreachable rather than merely absent under macOS overlay scrollbars: `public/style.css:88` gives `html` and `body` `overflow: hidden` while `#app` is mounted, so weave scrolls `#main` and never the document, and the skip fired on every platform. The case asserts the rail at left 0, the sidebar at 52, `scrollLeft` 0 and no horizontal page overflow across all four views with no condition on it, and a second case appends a child taller than the viewport and asserts the page still refuses to scroll, reserves no gutter and carries no `:root` margin. Both go red when `:root { margin-left: 0 }` becomes 16px, at rail 16 and sidebar 68, the numbers Issue #546 recorded before its fix. Gate: `test/ws-rail-inset-browser.test.mjs` (2 cases, `# pass 2 / # fail 0 / # skipped 0` in the browser lane on macOS). - A workflow state chip now paints the colour its state names. Eight `.k-state.cat-*` tone rules in `public/chip.css` outranked `.hue-*`, so an In progress state coloured green still showed blue in the grid, pickers, search, relation chips, cards and the field dialog. The rules are gone; `.hue-*` is the one source of chip colour, the hue-less filter chip takes its category hue, and the phone applet builds all ten hues from `chip.css` instead of a five-hue copy whose dark values had drifted. A paint contract test (every chip kind, hue and category, both themes, Chromium and WebKit) runs on every pull request, a surface test compares painted colours across eight surfaces, and a CSS lint refuses any other rule that sets `--fill`, `--text` or `--dot`. (Issue #969, Issue #771) ## v0.4.104 (2026-10-16) - Each agent on the connect step shows its own logo: Claude, ChatGPT, Gemini, Claude Code, Codex, Antigravity and Gemini CLI. (Issue #958) - Every sign-up at weave.grunion.ai starts onboarding. With `WEAVE_BASE_DOMAIN` set, the apex `/auth` goes to `/start`, and any sign-in begun on the apex returns to `/start`. A new person, including one arriving from a landing page link or from Clerk's hosted page, lands on the setup steps instead of a No access page. Workspace hosts keep their own sign-in (Feature #324, Issue #910). - The setup page, its weave tile, the sign-in card and the 404 page draw the brand rope. They each drew their own mark, and the setup page's read as three closed loops; all four now take the masked h3 mark from `src/mark.js`, inline, in both themes. (Issue #963) - Clerk's security emails (new device sign-in, account locked, two-step verification, passkey and password changes, email address change, codes and email links) use weave's email layout and lockup instead of Clerk's stock template. The templates live in `src/mail.js` beside the invite emails, and `node scripts/clerk-emails.mjs --push` sends them to the Clerk instance. (Feature #343) - **Click budget for every journey** (Feature #338): the human-simulating journeys now count the clicks each action takes and log them on every PR. `test/click-budget.json` holds the budget per journey and cell; budgets only ratchet down, and a change that adds a click fails unless it raises the budget with a written reason under `raised`. - The cut-off text gate reads only what the viewport shows, so a grid's off-screen buffer rows no longer decide the baseline counts ([[Issue#937]]). - Scrolling down a long table stops at the rows that are loaded, on a **Loading more** row that runs the weave loader, and the next page is asked for three screens before the reader gets there. A fast flick no longer lands on blank grey rows. Jumping to a row that is not loaded yet (End, a new row) loads every page up to it first (Issue #962). - Scrolling back up a long table never redraws a row you already saw. A refresh used to keep only the pages under the window, so rows above came back blank and waited on a fetch; it now re-reads every loaded row in batches of 400 and swaps them in at once (Issue #961). - The loading animation plays again on hosted workspaces. The sign-in wall had been answering the rope fragment `/brand/weave-loader-rope.html` with a redirect, so the loader sat on its still mark; brand fragments now pass the wall like the other brand assets, and a grid loader that opened before the rope landed picks it up when it does (Issue #960). - weave.grunion.ai opens on sign-in, with Create an account beside it. A first sign-in through Google or another single sign-on creates the account on the spot. Sign-in can no longer be skipped from the setup page; Skip setup stays on the later steps, and `/start?setup=skip` still goes straight to an empty workspace after sign-in. A link with a page hash, such as /#/settings, no longer sends a signed-out visitor to Clerk's hosted page (Feature #327, Issue #933). - One browser test walks onboarding end to end: sign-up, setup, the Claude connector's sign-in in a new tab, the first tools/list and tool call, then back to a setup tab that turns connected without a reload. It runs on desktop and on the iPhone profile and pins the counts in `test/onboarding-steps.json`: 3 pages, 1 sign-in, 1 consent screen and 9 clicks. The counts may only go down. (Feature #335) - Consent-gated PostHog onboarding funnel: when `POSTHOG_KEY` is set, /start and /auth show a cookie banner, and only after Accept does weave load PostHog through the same-origin `/ingest` proxy and record each setup step, the sign-up, workspace, template, agent and completion milestones, identified by the Clerk user id alone. Decline, or no key, sends nothing. The privacy policy names PostHog and the shared `grunion_consent` cookie. (Feature #336) - The AI app step on the setup page says up front that connecting opens a weave sign-in tab and to come back after Allow. Once the app connects, the setup tab retitles itself with the app's name, so it stands out among the open tabs, and fires a notification when notifications are already allowed. (Issue #936) - The "Your workspaces" page names the account you are signed in with, and each workspace is a card with its logo (or letter tile), its address, your role and an open arrow, the one you opened last on top. "Add another sign-in" reads "Use another account", and every target on a phone is 44px tall. `GET /api/start` rows carry `id`, `host`, `role`, `logo` and `seenAt`, and `GET /api/start/logo/` serves a logo to a sign-in that opens that workspace. (Issue #964) ## v0.4.103 (2026-10-16) - The Billing page says who holds which seat. Under each name the seats list prints that person's verified sign-in address, so a team workspace is readable when account names do not match the people behind them. `GET /api/billing` carries the address only for an architect, the same gate Members uses; every other member reads the seats as before (Issue #921). - The setup page's art now fills its whole panel at every window size. The form column scrolls inside the page, so the art never ends early and leaves a gray band below it, and a phone still hides it. The connect step reads shorter: each tile says in one line what it takes or which plans can connect, the steps run a dozen words or fewer, the page-wide plan note is gone, and the step's links (home, Privacy, Terms) are checked by a test. (Issue #954) - Every MCP tool now carries annotations: a title plus `readOnlyHint`, `destructiveHint` and `openWorldHint`, so clients and connector directories can tell reads from deletes. `/support` and `/docs` (agent setup, with every tool and its marks) answer without sign-in, and `/.well-known/openai-apps-challenge` serves the token in `WEAVE_OPENAI_APPS_CHALLENGE` as plain text, or a 404 when it is unset. The default `tools/list` grows from 15,389 to 17,218 bytes (Feature #331). - The left nav's scroll bar runs only beside the space list. The list scrolls inside the sidebar between the search head and the pinned foot, its bar sits at the panel edge, and the head and foot no longer ride sticky offsets, in the sidebar, the hover peek and the phone drawer alike. (Issue #957) - Safari draws the same thin scroll bar as Chrome. Tabler's 16px WebKit bar, which Safari kept on every pane while Chrome ignored it, gives way to one weave spec: `scrollbar-width: thin` for Chromium and Firefox, and an 8px WebKit bar with a 2px inset thumb at Tabler's 20% tint, 40% on hover. The nav, the main panel, the dock and every doc table get their 8px back. (Issue #956) ## v0.4.102 (2026-10-16) - The personal menu's keyboard test now expects the Settings row that Features #295 and #297 added, so main is green again. (Issue #931) - The bug panel's submit button now says **Report issue** instead of Send, and carries `⌘↵` inside the button rather than as a grey chip beside it, so the shortcut is visible where the action is. A filed report turns the same button into **Reported**. (Issue #847) - The connect page lists Claude, ChatGPT and Gemini under Chat apps, and Claude Code, Codex, Antigravity, Gemini CLI and Something else under Harnesses. Cursor and VS Code are gone, along with their install links. ChatGPT and Gemini say which plans and accounts can add a custom MCP server. (Issue #950) - The tab shows the weave mark in Safari: the server answers `/favicon.ico` with the built icon and serves every `.ico` as `image/x-icon`, and the setup, billing and not-found pages name the ICO beside the SVG icon the way the app shell does. (Issue #848) - The stacked-rhombi `layers` icon is retired everywhere (Issue #948). The Projects and tasks category and the Team projects template's Projects table show `folder-kanban`, the table Group button shows `group`, the rollup command shows `sigma`, a deck's promote button shows `copy-plus` and a workspace search hit shows `house`. The icon picker no longer offers `layers`, and a value stored as `lucide:layers` draws as `lucide:folder-kanban`. - Permanent delete moved to a new Settings page, opened from the personal menu (the ⋯ beside your name at the foot of the sidebar). Each trashed row or share page there has a Delete forever button; you type the item's name to arm the hold, then hold it until it fills. The Trash page now only restores. Workspace delete is back in the same place, behind the same typed name and hold, and moves the workspace to the trash, where Restore brings it back. Only a caller the server lets purge (an editor or architect) sees Delete forever, and only an architect on the hub root sees workspace delete: `GET /api/trash` now returns `purge`, and the `deletable` field on `GET /api/workspaces` now depends on the caller's rights. (Feature #295, Feature #297) - Settings opens from the personal menu (the ⋯ beside your name at the foot of the sidebar), between Keyboard shortcuts and Sign out. The System group keeps only Workflows, Users, Activity and Trash, as Feature #333 rules (Issue #929). - The setup picker's preview is small now (Issue #949). A pick shows one line on who the template is for, a line saying what it is like (Team projects is "Like Asana"), and a map of its tables with their icons and a line for each relation. The field lists and the table and sample-row counts are gone, and the preview stays whole above the Start button on desktop and on a 375 px phone. Search also matches the like line, so "asana" or "jira" finds its template. ## v0.4.101 (2026-10-16) - The AI app step on `/start` lists the agents most people can finish setting up, from 28 Haiku runs: Claude, Claude Code, Cursor, VS Code, Gemini CLI, and Something else. ChatGPT, the Gemini app and Perplexity are cut because custom connectors there need a paid business plan, an admin or a region most accounts lack, and the page says so. Every agent but Claude copies one tested setup prompt carrying the workspace address; Claude keeps its click steps, and Cursor and VS Code keep their Add buttons. The step heading is "Your own agent". (#330) - The billing page loads Clerk's UI bundle beside clerk-js 6 and passes it to `Clerk.load`, so the pricing table mounts instead of failing with "Clerk was not loaded with Ui components". (Issue #922) - A conditional write can now carry both halves of a close: `operation: "close"` appends evidence through `field`/`markdown` and moves the workflow field named by `stateField` in one transaction, after one revision check, so an agent closing a row no longer leaves a window a person's edit can land in and lose (Issue #902). - The dev supervisor reads main's head through the git protocol and downloads commits from codeload, so an exhausted unauthenticated GitHub REST quota no longer fails the dev swap and skips the soak. A rate-limited REST call is retried and its error names the reset time, and the dev workflow waits for dev to report the commit instead of failing on one unanswered check (Issue #930). - Both reference cases in the slash-commands suite now wait for the chip their assertion reads, through one `docHtmlWith(page, id, pattern)` helper that polls `doc.html` for that pattern and names it with the stored markdown when the wait runs out, so the GET no longer races the save and passes on a page whose inline script merely mentions the word (Issue #457). - A workspace made at `/start` opens on a free trial: for `WEAVE_TRIAL_DAYS` days (14 by default, `0` for none) every Editor and Architect creates and edits, and the AI app works over OAuth MCP and `wv_` tokens. After it, a paid seat is needed again and every row stays. `GET /api/billing` and the `/api/start` workspace rows carry `trial: { endsAt, daysLeft }`, and the sidebar links to Billing over the last 3 days. (Feature #329, Issue #918) - The setup page's agent step reports an AI app as connected only after weave answered one of its MCP calls, never a call it refused. (Issue #915) - An Architect without a seat keeps an AI app that reads: `/mcp` answers `initialize`, `tools/list` and the read tools, and write tools return a tool error with the seat-required words and the billing address. (Issue #916) - The seat-required refusal is worded for the reader's role, and the app shows it as one toast with an Open billing action instead of one toast per refused write. (Issue #917) - `GET /api/start` names the workspace waiting on a plan as `waiting` beside `canCreate`, the `plan-required` refusal carries that workspace's billing address, and `/api/start/open/?to=billing` lands on that workspace's billing page. (Issue #920) - The relation picker's browse list (a scoped search with no text) no longer offers rows of a table, or of a space, that sits in the trash; it now applies the same container check as text search. The lifecycle pack gains a second ring that runs delete, restore and purge for rows, tables, spaces and workspaces through a real `.db` close and reopen, and covers restore against a name a live table, space or workspace has taken, undo next to structural trash, search delisting under a trashed container, and CLI and server writes meeting through the per-request refresh (Feature #159). - A lookup or a rollup repoints in place. Its settings tray offers the Relation, Target field and Aggregate pickers a create offers; save, and the column keeps its name, description, width, views and position while every cell reads the new recipe. `updateField`, `PATCH /api/tables/:id/fields/:id`, `weave field update --config` and `weave_update_field` take `relationField`, `targetField` and `aggregate`, checked by the same recipe check `addField` runs, so an edit is refused exactly where a create is. A new relation keeps the old target field only when its far table has it. The tray warns when the result changes kind, the answer lists under `formulas` every formula of the table that now fails on the column, and the Activity entry names the old and the new recipe so Undo puts it back. A space rollup keeps its recipe read-only. The formula scan now counts a NaN result as an error. (Feature #241) - The AI app step on `/start` opens with weave's position: weave is AI native and adds no agent of its own, so you connect the AI app you already use. App tiles carry tinted icons from `agent-connect-core.js` data; the chosen app gets a header with Change app, the steps a numbered sequence with the copy action on the step that needs it, a status that animates while waiting and turns green with a check on connect, and the first message is the payoff. One Skip for now link. (#326) - With a Clerk publishable key, the Account step on `/start` mounts Clerk's sign-up form in the left pane, with an "Already have an account? Sign in" switch to Clerk's sign-in form. Clerk's UI bundle (`@clerk/ui@1`) now loads beside clerk-js 6 and is passed to `Clerk.load`, which fixes the fallback to the plain button on production (Issue #922). Without Clerk's scripts the provider button stays. (#327) - The setup page at `/start` keeps the radiating-rings art on the right for every step and shows progress as a donut in the left pane: 25% on Account, then 50, 75 and 100, with the workspace build carrying it from 75 toward 100. (#324) - The kind-of-work step is a gallery named "Your work": a search field that filters as you type (every word, ignoring case and accents), category chips and shelves, cards, and a details panel under the picked card listing the tables it creates, their key fields and the sample-row count. Empty and Basic stay pinned at the end and are all that shows when nothing matches; Empty takes an optional description for the AI app. Arrow keys move between cards, Enter picks and Escape clears. The primary button stays pinned at the bottom of the pane and names the pick. (#324) - A workspace that still needs a plan shows on `/start` before the next one is named, with a link to its billing page, and the last step shows the days left in a trial. (#324) - On a phone, focusing the setup gallery's search field scrolls it to the top of the screen, so the matches show above the keyboard, and the page stays put once focus leaves the field. (#324) - The eye on a relation subtable shows and hides the target's fields and filters the linked rows with the table filter's chips. The setting lives on the relation field as `config.subtable` (`fields`, `filter` as `where` triples, `sort`), so one edit narrows that subtable on every record, and a filtered subtable counts `shown / total` in its header (Feature #214). - The starter catalog grows to 40 templates in 12 categories for the setup gallery: `WeaveStarters.CATEGORIES` lists each category with a label, icon and blurb, and every template carries a `category` and two or three sentences of `details`. The 15 earlier starters keep their ids and tables. (Feature #328) - The catalog adds `basic`, one plain Items table under a new Start simple category that the gallery search falls back to, and `WeaveStarters.searchText(template)` returns the lower-case words a search should match: title, kind of work, blurb, details, category and table names. (Feature #328) - Automations take an `upsert-row` action: it creates or updates a row of a target table matched on a key field holding an external id, with a field mapping from source paths (`row.`, `event.type`) to target fields, so a repeated fire never makes a duplicate (Feature #309). ## v0.4.100 (2026-10-15) - The Handbook documents in-place account role changes, the last-architect guard and immediate permission updates for cached provider identities. (#896) - The Activity panel's revert button and row icon read "Clear value" when a field was empty before the change, instead of "Revert to null", and an empty prior value reads as a dash whether it was stored as null, an empty string or an empty list (#908). - The record Activity panel is a pane docked to the right edge of the record view: the view's full height, square edges, one left border and no shadow. The record body and its header narrow beside it and scroll while the pane stays put, on the full page and in the peek; phones keep the full-screen sheet (#906). - A table whose row holds a long relation cell opens without an 11 s stall: the Activity pane's layout rules select from the pane forward instead of through `:has()` on the view (Issue #920). - The Activity panel wraps a long field value inside its row instead of cutting it at the panel edge, keeps its filter chips on one line and puts the document chips that do not fit under a More menu. The cut-off sweep and gate walk a new Activity-panel journey on desktop and phone (#907). - The Environment reference row for `WEAVE_APPLET_PASSCODE` now describes the gate the applet runs: eight wrong answers inside ten minutes refuse every later attempt with `429`, the right passcode among them, and an unset passcode leaves no applet at `/t` instead of opening one to anyone (Issue #814). - **No em dashes in release notes** (Issue #722): CHANGELOG.md body prose carried 156 U+2014 characters that the house prose rule bans, and the GitHub Release body copies them. Each one is rewritten with the comma, colon, parentheses or hyphen its own sentence wants, read line by line. `test/changelog-fragments.test.mjs` now pins the whole file instead of the version headings, and `fragmentGuard` refuses a commit that adds or edits a `changelog.d/` fragment carrying U+2014, so the character cannot come back through the door that let it in. `changelogGuard` lets a CHANGELOG.md line be rewritten to drop the character without a version bump, and nothing else. - Billing through Clerk Billing, off until `WEAVE_CLERK_SECRET_KEY`, `WEAVE_BILLING_SOLO_PLAN` and `WEAVE_BILLING_TEAM_PLAN` are set. One Editor or Architect is covered by the solo user plan; more than one need the per-seat team plan, and weave mirrors exactly the Editors and Architects into the workspace's Clerk organization so Observers are never billed. A paid human without a seat works as an Observer and gets a 402 `seat-required` answer on a write; a failed card never cuts access. Workspaces created before `WEAVE_BILLING_SINCE` are never billed. New `/billing` page and `GET /api/billing`, `POST /api/billing/team` (Feature #256). - With `WEAVE_CLERK_PUBLISHABLE_KEY` set for the same Clerk instance as the sign-in issuer, the setup page mounts Clerk's sign-up and sign-in form in its left pane, in weave's colours, so creating an account no longer leaves the page. If Clerk's script does not load, the page falls back to the provider button (Feature #327, Issue #910). - Conditional document appends and state changes reject stale row revisions under the database write lock. Authenticated clients can read a minimal full serving-commit proof (Issue #902). - The date picker steps sideways: ‹ and › replace the ↑ and ↓ buttons for the month, year and decade steps, beside the month and year dropdowns the picker keeps. The year and decade steps gain aria-labels, and the date and range pickers name the field they edit. (#801) - An image embed with an empty source is no longer stored on a document, by any write path, so a failed paste cannot leave a broken image behind; an embed written as an example inside a code fence or inline code is left alone. An image uploaded into a document now carries empty alt text instead of the file name, which keeps a browser-synthesised name such as a page title out of the document. (#840) - `test/flicker-fixed.json` refuses a fingerprint the flicker gate never reads. The gate walks each journey with the DOM probe alone, so a `frame`, `jank` or `rerender` entry sat there unchecked; `fixedFault` now names the kind, the kinds the gate does read and the `frameFlashes` case that holds a frame finding instead (Issue #702). - Serving-commit proof recognizes supervised commit tarballs that do not include Git metadata (Issue #902). - The nav identity panel's browser suite signs in with the per-workspace session cookie, so it no longer reads as signed out after the legacy `wv_session` cookie stopped opening workspaces (Issue #717). - Renaming an account carries its credentials with it: the keystore owner and every share naming the old account name move to the new one, so the owner and the accounts a credential was granted to keep their access. (#884) - The Users table's Last Seen moves while someone stays signed in: a renewed session updates it, at most once an hour per account, so it shows recent activity instead of the last sign-in. (#877) - The plain `wv_session` cookie from before per-workspace sessions is no longer read: it opens nothing and is cleared with `Max-Age=0` on the next response. Sign in again if a browser still held only that cookie. (#717) - The `weave_apply_schema` MCP tool takes `partial`, as `PUT /api/schema` and `weave schema apply --partial` already do: a document carrying only a new space adds it and leaves the other spaces in place, so an agent no longer restates the whole workspace to add one (Issue #916). - The system tables nav test that checks a once-resolved selector against a repaint holds the gap open until the read has run, instead of racing a two-frame refill, so a slow evaluate round trip no longer fails the shard (#837). - Drag the left nav's right edge to resize it between 200px and 480px; the width is remembered, a double-click restores 264px, and the arrow keys nudge it 16px at a time. The narrow drawer and phone layouts keep their width. (#905) - On a phone every space header in the left nav is a 44px tap target again; the System head and the space links share one phone rule that outranks the 900px one (Issue #914). - The foot of the left nav is redesigned. Workflows, Users, Activity and Trash live in a built-in System space pinned under your spaces, with no counts; it folds like any space and remembers the fold. Under it, an identity panel shows your avatar, name and your role in this workspace as a coloured chip, and its menu holds Theme, Keyboard shortcuts and Sign out with the version beside it. The rail loses its ? and theme buttons; the ? key still opens the shortcuts. The rows and size line, the uptime and the version chip are gone; a stale or behind server still warns. (Feature #333) - The stored system space is named System (its key stays `workspace`), so qualified names read `System/Users`, `System/Spaces` and so on; every workspace is renamed on open, and `Workspace/...` names keep resolving. (Feature #333) - The nav show and hide buttons draw one panel-left icon on a soft borderless 28px square and sit in the same top-left spot whether the nav is open or collapsed, so the control never jumps when clicked. The titles read Hide sidebar and Show sidebar. (#906) - The table and the record open the same Fields menu. The eye button on a record used to open a second, smaller menu of switches with no reorder, while the table's opened a checkbox list with grips; one builder now draws both. Every row is a switch with a grip on its left, Show all, Hide all and New field sit on both, and dragging a row or pressing ↑ / ↓ on its grip reorders the view's columns on a table and the table's field order on a record. The Deleted and Σ rollup row switches stay on the table menu only. The record menu opens under its own button, in the dock as on the full page (Issue #910). - The Fields list keeps its switches clear of the scrollbar. The list reserves its scrollbar gutter whether or not it scrolls and pads its right edge to match the grip's inset on the left, so a switch sits 8 to 12px from the scroll thumb and no row shifts when the list starts scrolling (Issue #909). - On a phone, a workflow state or select option row in the field tray wraps: grip, icon and a name box at least 140px wide on the first line, and category, colour, preview and a 44px delete button on a second line. (#842) - A multi-select picker shows a footer with "N selected" on the left and "Done ↵" on the right, so an empty multi picker no longer looks like a single select and its save on close is visible. Done commits what is staged; picked options still leave the list as chips, with no checkboxes. (#798) - `atRest` joins `styleOf` and `settled` in `test/lib/browser.mjs`: it waits for an element whose transform is identity, whose box has not moved for a frame, and which has no running animation, so a case cannot read a sheet whose spring-back transition has not started yet. The phone picker sheet case waits on it instead of `settled` alone (Issue #901). - Every field picker opens titled with its field name, and its search hint reads from that title ("Search priority…") instead of one of four hand-typed strings. A set value keeps the hint in place of an empty box. The icon, table, space and term pickers title themselves "Icon", "Table", "Space" and "Term". (#799) - Show/hide lists in popovers use the checkbox. Rows in the Fields menu, on the table and the record, and in the Σ footer picker were switches; they now hold the same checkbox as the grid's row selector, the Filters popover and the field dialog, on the left of the label. Grips, drag and ↑ / ↓ reorder, the scrollbar gutter, and the Deleted and Σ rollup row entries stay as they were. Settings such as Link preview before sign-in and boolean field values keep their switches (Issue #915). - On a member-workspace host such as weave.weave.grunion.ai, the default workspace's rail tile now loads its own logo, links to its own home and opens its own menu record, instead of reaching the host workspace through the bare root path. The wordmark and active tile name the host workspace. (#907) - `PUT /api/schema` and `weave schema apply` take `partial`/`--partial`: a document that adds spaces no longer has to repeat every existing space, and nothing it leaves out is deleted. (#897) - The security scan's gitleaks step reads only the checked-out history, so a finding on another open branch no longer fails every pull request's required check. (#913) - The PR gate accepts the security scan test's `gitleaks not installed` skip in lanes without the gitleaks binary, as it already accepts `semgrep not installed`. (#913) - With billing on, a person with no account can create a first workspace: signing in at `/start`, or at a workspace that answers **No access** (which now offers **Start your own workspace**), lets them create one and become its Architect, working as an Observer until they hold the solo plan. Each login gets one unpaid workspace; another create answers `402` `plan-required` naming the one waiting on a plan. With billing off nothing changes (Feature #331). - `/start` is a setup page: a left pane walks a new person through Account, Workspace, Template and Agent, and the right pane shows the weave brand and weaves each table in while the workspace builds. Skip setup on any step makes an empty workspace; Set it up myself skips the guided setup straight after sign-in. Sign-in failures come back to the pane with the reason instead of a refusal page. A workspace made here counts as onboarded, so the in-app welcome no longer asks for its name again (Feature #324, Issues #569, #911). - `WEAVE_OPEN_SIGNUP=1` lets anyone who signs in create workspaces from `/start`, up to `WEAVE_SIGNUP_LIMIT` each (default 1). Without it the page tells a stranger the server is invite only, where it used to end in a 403. - The Agent step lists chat apps first (Claude, ChatGPT, Gemini, Perplexity), then Cursor, VS Code, Claude Code and any remote MCP client, each with its own steps, the workspace MCP address and a first message to paste, both with copy buttons. It turns green when that app first reaches the workspace over OAuth MCP and passes only connected or skipped (Feature #326). - `test/setup-flows-browser.test.mjs` walks every setup path in the CI gate that holds production releases, and connects a simulated chat app through protected-resource discovery, dynamic client registration, PKCE and the token exchange, the way claude.ai and ChatGPT do. - Created At and Modified At read like date fields ("Aug 28, 2026 5:14 PM" in your own zone), with seconds and the zone on hover. Created By and Modified By chips come from the Workspace/Users table: a person shows initials, an agent a glyph, the chip opens the Users row and its hover gives Name, Role and Email. An actor with no Users row, such as `web`, is a quiet chip with no link (Feature #326). - The starter catalog grows from three templates to fifteen kinds of work, each tagged with the work it tracks: hiring, team projects, product roadmap, content calendar, customer support, freelance clients, research, fundraising, events, inventory, nonprofit donors and home join personal money, personal tasks and sales. The in-app setup cards and the empty-workspace card keep the three featured templates. (#328) ## v0.4.99 (2026-10-15) - Change an account's role with `account set --role`, `weave_accounts` action `set`, or `PATCH /api/accounts/:ref`. Existing tokens and sessions keep their identity and immediately use the new role; every door, including Users rows, refuses to demote the last architect. (#896) - The last three create surfaces open in the right-hand tray: the workspace rail's **New workspace**, a table menu's **New share page…** and a space's **Use template**. The first-run setup wizard stays a centered modal (Issue #780). - The flicker gate ignores editor teardown inside a panel removed in the same update. Connected editor regressions and visible transient subtrees remain checked (Issue #899). - **The phone picker gesture test waits for the sheet to rest** (Issue #901): wait for the opening animation and short-drag spring to finish before measuring or dragging the grabber again. Check the rendered transform as well as the inline transform so an unfinished spring cannot pass as a settled sheet. - The sidebar holds its edges: the workspace name and the search button stay at the top however far the space list scrolls, and **+ New space** now sits above the system tables (Workflows, Users, Activity, Trash) in one pinned strip at the bottom (Issue #854). - The supervisor swap test reads routing order per request loop instead of pooling every loop's completions into one list, so a request that finished out of order on the draining old worker no longer reads as the proxy routing backward (Issue #821). ## v0.4.98 (2026-10-15) - **The report button sits in the corner** (Issue #846): on a desktop viewport the bug glyph was pinned 40px above the bottom edge while holding 12px from the right, so it read as floating above the corner it belongs in. It now takes the same 12px inset from both edges, the value its phone rule already used. The raised offsets that clear a real neighbour are unchanged: the open tray still pushes it to 72px, and the phone bar still pushes it to 86px. - On a phone the fields block on a row page is an inset group of 50px rows, label left and value chip right, with a chevron that opens the field's picker sheet and an empty value as a single + chip. The drag grips are gone at that width and a relation that carries chips takes its own full-width row. (Issue #741) ## v0.4.97 (2026-10-15) - Restored automatic release publication after successful same-repository main CI; existing published versions skip duplicate image and registry publication (Issue #887). - Table and field descriptions open from a raised info icon, fold after three lines, and remember each viewer's open notes. Empty table descriptions start from Add description in the table menu (Feature #302). - Temporarily restore inline descriptions while the info-icon feature receives additional phone-menu verification (Feature #302). - Preserve horizontal access to long grid URLs and date ranges without changing regular date sizing (Issue #897). - Select and state pickers list real values only. The empty dash row is gone, so Option+1 is the first option anyone wants rather than a blank one, and clearing moved to where it already worked: Backspace on an empty search, or the ✕ on the chip in the search box. A single picker holding a value now names that key in its footer, and every row's quick-pick hint is spelled "Option+1" instead of a bare glyph. (Issue #797) - Creating a field from the Fields system table opens the field tray directly, with a Table row at the top instead of a centered modal that collected one value and handed off. A column header still opens the tray with no Table row, since the table is known (Issue #779). - Creating a table happens in one right-hand tray with Name, Space and icon, opened by the Tables grid +, the empty-workspace + New table, the sidebar space + and the space page add. The two centered New table modals are gone, and a table is named before it is created rather than landing as a row called "New table" (Issue #778). - The grabber on the phone picker sheet and the new-row sheet drags: it follows the finger, dismisses past a third of the sheet or on a flick, and springs back otherwise. One shared helper draws it as a 44px target on both sheets, where before the pill was decoration that promised a swipe and did nothing (Issue #808). - The reorder grip on a view in the Views menu is a 44x44 target on a touch screen, pulled back with negative margins so the view name and the popover keep their width (Issue #804). ## v0.4.96 (2026-10-14) - Added tested historical release recovery without moving immutable tags or latest (Issue #887). - A select or multi-select cell creates a missing option in place: type a value no option matches and the picker offers "+ Create “X”" as its first row, writes the option to the field, then picks it on a select or stages it on a multi-select. An empty one now reads "No options yet. Type to add one." instead of opening to nothing. (Issue #800) - Sidebar space icons keep a consistent gap before their labels on desktop and phone (Issue #888). - One state picker everywhere: a workflow field opens the same numbered list from a table cell, a record page field row and a chip's state mark, with thin category dividers (Not started, In progress, Done, Canceled) in place of the grouped grid the chip used to draw. (Issue #796) - The root workspace's Workspace registry space (Spaces, Tables, Fields, Views, Workflows, Workspaces, Forms, Users) no longer sits in the sidebar beside user spaces. Its tables are listed on the workspace page, under Activity, and open from there; Workflows and Users keep their pinned sidebar rows. The space itself is unchanged and still cannot be deleted. (Issue #874) - Sheriff now fails its workflow when recovery needs intervention, its revert is unarmed, or tracker credentials are missing (Issue #894). - A workspace created on an instance with `WEAVE_ORIGIN` set, or whose default workspace requires sign-in, starts with `requireAuth` on, so a guessable `.` host never answers an anonymous caller. After every production promotion, `scripts/exposure.mjs` asks each workspace for its tables with no credentials and fails the run on any 200 outside `PUBLIC_WORKSPACES`. (Issue #879) - Workspace logos that cannot load show the workspace initial in the rail and page header, while keeping the logo replacement and removal controls (Issue #889). ## v0.4.95 (2026-10-14) - Buttons hover three ways, one per weight, instead of six. A quiet or icon button takes a faint neutral wash and moves its glyph to body colour, so the thirteen `.btn-ghost-secondary` buttons stop flipping to a solid slate block with white glyphs. A bordered button darkens its border to one grey, `--wv-hover-border`, in place of the two greys and the primary blue that different authors reached for. A filled primary darkens one step rather than fading to 80% alpha, so Invite, Done and Create workspace no longer wash out under the pointer. Eight hand-rolled quiet hovers collapse into one rule and the palette is two tokens, `--wv-hover-wash` and `--wv-hover-border`. Gates: `test/button-hover.test.mjs`, `test/button-hover-browser.test.mjs`. (Issue #760) - A filter value in the table filter popover is the same chip the grid cell draws: the baseline `.k` chip, borderless, 4px radius, 13px at weight 400 and 1px 8px of padding, tinted by its own hue or workflow category. The popover's second chip build (1px border, 5px radius, `6px 10px 6px 8px` padding, secondary font size) is gone, and a picked value now wears `--wv-chip-selected-ring`, one shared 1.5px primary ring, instead of swapping its border and jumping to weight 600. Gate: `test/filter-chip-baseline-browser.test.mjs`. (Issue #769) - A long filter value in the table filter popover wraps again. The baseline chip holds its label on one line, which a grid cell wants and a filter list does not: the widest option pushed the popover's own content wide enough to cut "Clear all" by 23px. Gate: `test/cutoff-gate-browser.test.mjs`. (Issue #769) - A picked option looks the same whichever popover is open. The picker drew its cursor chip with a 2px primary outline at 1px offset while a picked filter value tinted its fill, so the same option in the same table had two selected looks. Both now read `--wv-chip-selected-ring`, the 1.5px primary ring Issue #769 introduced. Gate: `test/selected-ring-browser.test.mjs`. (Issue #772) ## v0.4.94 (2026-10-14) - A row crumb no longer draws its table's icon a second time: on a full entity page the row wears its own record mark, and a hop into another table still shows that table's icon (Issue #746). - The workspace mark in the breadcrumb bar is drawn at the size of the glyphs beside it: the weave mark paints half the width of its own box, so it read smaller than every lucide crumb icon next to it (Issue #746). ## v0.4.93 (2026-10-14) - **Comments off in the UI** (Feature #323): a record, on the full page and in the peek, no longer draws the Comments card or its comment box, comments alone no longer open the side column, and the Activity panel drops its Comments chip, its comment rows and their Delete action. Stored comments, `POST`/`DELETE /api/entities/:id/comments`, the MCP comment tools, the `add-comment` automation action and `comment-added` entries in the Activity table are unchanged, so turning comments back on needs no migration. Gate: `test/comments-off-browser.test.mjs`. - **Dev smoke and soak** (Feature #323): after `Deploy dev` confirms a main commit is serving, the new `dev-soak` workflow runs a Playwright smoke against dev.weave.grunion.ai every three minutes for fifteen minutes while it reads the supervisor's watch block, and posts the commit status `dev-soak` on that SHA: success when the watch passed with no rollback, no worker restart and no 5xx, failure with a link to the run and one alert line otherwise. A failed smoke runs again at once, and only a second failure in a row fails the soak. A newer main commit cancels the older soak. Gate: `test/dev-soak.test.mjs`. - A List view row's drag grip is a 44 by 44 target on a touch screen, up from about 20 by 20, so a finger can take hold of a row to reorder it. The glyph keeps its size and sits centred in the target, the row height does not change, and a mouse pointer still gets the small grip on hover. Gate: `test/phone-list-grip-browser.test.mjs`. (Issue #805) - A wait past four seconds captions the page loader with "Still loading this page", and past ten seconds offers a "Try again" control that re-runs the route, the same hand-off the route error card makes (Issue #393). - The page loader keeps a still woven mark under `prefers-reduced-motion: reduce` instead of looping the rope, announces the wait once as a `role="status"` region reading "Loading the page", and marks `#main` busy while it covers the page (Issue #394). - Keyboard navigation inside a dialog or tray now reaches a disclosure's summary, and no longer dead-ends on the controls hidden inside a closed one. Tab through the field tray of a formula field reaches "Fields and functions", "As an agent would do it" and the tray's own Save button; Enter on a summary opens the section and the next Tab lands inside it. (Issue #816) - A grid column's resize edge is a 24px target on a touch screen, up from 6px, so a finger can take hold of it instead of opening the field menu underneath. The painted line stays a 6px hairline, drawn now by the strip's own `::after`, and a mouse pointer keeps the 6px strip it had. On a column narrower than 60px the target stops at two fifths of the header, which leaves the header the room it needs for a long press and a tap. Gate: `test/phone-col-resize-browser.test.mjs`. (Issue #807) - A row page on a touch screen draws no grip glyph on its field rows, its FIELDS block head or a document section head, and the 16px column that held the glyph is gone, so a label starts at the row edge and a value gets 26px more width. Press and hold the row itself to pick it up, as the Handbook already describes. The grips that are a block's only drag handle, on an attachments block and a related table, keep their place, and a mouse pointer still gets every grip on hover. Gate: `test/phone-row-grip-browser.test.mjs`. (Issue #806) - **Pull requests run their tests on GitHub-hosted runners before auto-merge** (Feature #319): the new `pr` workflow runs every unit file in four shards and the browser tests the diff maps to (a changed browser test, a test named for the changed module, a test whose source cites the changed file), plus a fixed smoke set, in about twenty files a shard and four shards at most. `PR gate` succeeds only when every shard ran and passed at the exact commit, and it is now a required check on main beside `Dev gate`. A newer push cancels the superseded run. Agents no longer wait on the shared local test manager to land; the full suite still runs on every main push. Gate: `test/ci-test.test.mjs`. - **Re-running a failed PR shard can turn `PR gate` green** (Issue #883): each shard's evidence artifact now carries its run attempt, and `PR gate` judges every shard by its newest attempt. Before, a re-run that passed still failed the gate, because the failed attempt's artifact overwrote the new one on download. Gate: `test/ci-test.test.mjs`. - **Production promotes itself from green main** (Feature #322): the new `promote` workflow runs hourly and acts every `PROMOTION_EVERY_HOURS` (default 2) or on dispatch. It picks the newest main commit with a `CI gate` success and a `dev-soak` success that descends from the `production` branch, fast-forwards `production` to it through the promotion deploy key, calls prod's supervisor deploy hook and waits for `/api/health` to show that commit with its ten-minute watch passed. A failed watch or a rollback fails the run, files an Issue row, alerts and opens a revert PR on main for the promoted range; `production` is never rewound. `PROMOTION_PAUSED=true` stops it. Gate: `test/promote.test.mjs`. - **Releases no longer gate production** (Feature #322): the `release` workflow's publish job leaves the `production` environment and stops waiting on prod health, since prod now follows the `production` branch that `promote` moves. Tags, the GitHub Release, the GHCR image and the MCP registry entry stay available on demand. - **A red main gets a sheriff** (Feature #320): when a main push `tests` run fails, the new `sheriff` workflow reruns its failed jobs once and logs a green rerun as a flake. A second red bisects the commits since the last green `CI gate` by dispatching `tests` on midpoint commits (the `tests` workflow takes a `ref` input for this), at most four runs, then opens a squash auto-merge revert PR naming the failing tests, the run links and the original PR, comments on that PR, files an Issue row and writes one alert line. It never reverts a revert, never acts on the same culprit twice and never pushes to main. Gate: `test/sheriff.test.mjs`. - **A refused revert PR still reaches the original PR** (Issue #884): when GitHub refuses the sheriff's revert PR, as it does while `SHERIFF_TOKEN` is unset because Actions may not open pull requests here, the sheriff comments on the original PR, files the Issue row and alerts with the pushed branch and its compare link, where it used to fail with only its failure alert. Gate: `test/sheriff.test.mjs`. - **Re-run failed jobs can turn the main CI gate green** (Issue #883): the main `tests` shards upload `ci--` and `aggregate` judges each shard by its newest attempt, as the PR gate does since #122. Before, the failed attempt's result overwrote the rerun's, so a flake that passed on rerun still read red, and the sheriff would have treated it as a real failure. Gate: `test/ci-test.test.mjs`. ## v0.4.92 (2026-10-12) - **A reference chip in the editor sits inline where its markup was** (Issue #878): an aliased `[[Issue#229|Long label]]` drew its chip over the raw markup, which kept rendering underneath, so its tail (`pped]]`) leaked past the chip into the next words, the `[[` peeked out below it, and a reference that wrapped to a second line stayed raw. While the caret is outside a reference its markup now takes no visible space and keeps exactly the chip's width, so the line reflows around the chip; a reference that wraps gets its chip too, and a chip wider than its line ends in an ellipsis. With the caret inside, the markup shows for editing. The stored markdown is unchanged. Gate: `test/doc-ref-chips-inline-browser.test.mjs`. - **A request entity id never reaches Object.prototype in the hub route switch** (Issue #881): a member route that falls back to the root for an entity it does not hold now checks both workspaces with `Object.hasOwn`, so an id of `__proto__`, `constructor` or `toString` answers the member's own 404. The semgrep result that kept the security workflow red since 25be20b is fixed in code, not baselined. - An anonymous form submission from a browser that had no client cookie yet now records its receipt under the cookie the answer mints, so a retry with a fresh idempotency key replays that receipt instead of filing a second row (Issue #811). - A write that lands while another signed-in request is still arriving now records the account that sent it, instead of whichever request reached the server last. Row authors, comment authors, bug-report reporters, template builds and credential ownership checks all read the actor captured when the request authenticated (Issue #810). - A shared table page now shows the rows the table's default view shows: its filters, its saved search and its sort order, the same configuration the table page opens with. It used to list every row in the table, so a viewer saw rows the owner had filtered out (Issue #838). - The self-updating supervisor now watches each new release for ten minutes (`WEAVE_WATCH_MS`). A serving worker that exits, three failed health checks in a row or a 5xx rate past `WEAVE_WATCH_5XX_RATE` swaps back to the kept good release, unless that release's deferred migration already wrote to the workspace, which raises an alert instead. A boot that fails falls back to the newest good release before the image, and `WEAVE_TRACK=branch:` follows a promotion branch (Feature #321). ## v0.4.91 (2026-10-12) - The record page and the peek fetch a record's references beside the record itself, so a redraw of the same record keeps its scroll and the side column never jumps. The table badge on a relation chip reads at 4.5:1 contrast in the References card (Feature #315). - The Activity panel opens inside the record as a column right of its body, on the full record page and in the peek beside a table. The record body narrows to make room; the page, the crumb row and the table no longer move or sit under the panel. On a phone it is still a full-screen sheet (Issue #871). - Activity shows in two places: the Activity button in the record's toolbar, which opens the panel inside the record, and the Activity system table at #/activity. The Activity card in the record's side column, the Activity system field (its table column and its show/hide row) and each document's History button are gone. Tables that stored the Activity switch lose it on load. The References and Referenced by cards load with the record and open the side column on their own, as do comments. `weave_query` no longer takes `Activity` in `fields`; `weave_get_entity` and `weave_activity` still carry the history (Feature #315). - **Backup to weave cloud** (Feature #316): `weave backup --dest weave:///` and `WEAVE_BACKUP_DEST=weave://…` send the sealed nightly archive to a weave cloud account, authenticated with `WEAVE_BACKUP_TOKEN`, a `wv_` token from that account. The cloud stores ciphertext only. A cloud deployment with `WEAVE_BACKUP_BUCKET` set answers `PUT /api/backups/`, `GET /api/backups` and `GET /api/backups/`, keeps each account to thirty archives and `WEAVE_BACKUP_QUOTA_BYTES` (5 GiB by default), and refuses anonymous and cross-account callers. `weave restore --from weave:///[/]` pulls the newest or the named archive. `/api/health` names the destination kind. The new Handbook page **Backup to weave cloud** covers the destination, token, quota, restore and doors. - **Migrate to weave cloud** (Feature #316): `POST /api/workspaces` with `{ name, from: "backup:", key }` creates a cloud workspace from an archive the caller's account stored, decrypting it in a scratch directory with the supplied passphrase and running the existing restore. The key is never stored or logged. A wrong key answers 400, another account's archive 404, and a second migration of the same archive 409. `weave workspace migrate --from weave:///[/]` is the CLI form. - **A GHCR image and an Official MCP Registry entry on every release** (Feature #292): the approved release pushes `ghcr.io/grunion-ai/weave:` and `:latest`, labelled `io.modelcontextprotocol.server.name=io.github.grunion-ai/weave`, then publishes `server.json` (the image over stdio, running `node bin/weave.js mcp`) with a pinned `mcp-publisher` signed in through GitHub OIDC. The release fold stamps the version into `server.json`. The README Quickstart shows `docker compose up -d` beside the node one-liner. - `GET /api/health` again answers `{ok, name}` to an anonymous caller, as Issue #520 set out: the release supervisor confirms a new worker by the version that worker reports over its private IPC channel, so a walled instance takes releases without showing its version to the public internet (Issues #520, #843 and #844). - A supervisor serving the image it shipped in reports the commit Railway built it from in `/api/health` (`supervisor.commit`), so production names its build the way dev already does (Feature #305). - **KaTeX 0.18.11** (Issue #758): the vendored math engine moves from 0.16.47, which carries advisory GHSA-238p-pmpm-9mq7 (fixed in 0.18.2), to 0.18.11. Only `katex.min.js` and `katex.min.css` change; the mhchem extension and the twenty woff2 fonts are byte-identical in the new release. `security/vendor.lock.json` is rehashed and `scripts/vendor-advisories.mjs --require-all` reports no advisory. - **The phone nav drawer holds the page** (Issue #262): below 900px, while the menu is open, everything outside it is inert, Tab and Shift+Tab stay inside it, focus goes in on open and back to the menu button on close, and the menu button carries `aria-expanded`. Every way the menu closes (Escape, its close button, the scrim, a nav link, a hash change, the window growing past 900px) frees the page. Shift+Enter, ⌘Z and `?` stand down while it is open. - **Workflows stays in every sidebar** (Issue #875): a member workspace served by the hub keeps its Workflows row beside Activity and Trash. The row finds the Workflows table through the root registry and opens it on the root, and it still shows, linking the root registry, when no Workflows table resolves. - **The tray, the full-screen viewer and the ⌘K palette hold the page** (Issue #263): each is a labelled modal dialog, Tab and Shift+Tab stay inside it, the app behind is inert while it is open, and focus goes back to the opener when it closes. Holds stack, so ⌘K over a dialog holds that dialog too and hands it back, and the tray replacing a modal keeps the page held. The palette's Tab still jumps to the next group, and it frees the page before a caller's `onPick`, `onDismiss` or `onCommit` runs. Shift+Enter (quick-create) stands down while the tray or the viewer is open. - **A phone list row never draws a chip cut off** (Issue #876): a chip whose value is wider than its slot, such as a relation to a long-named row, leaves the line and the row counts it in its +N like any other chip that does not fit. A row whose only chip leaves shows the count alone. An empty date is no chip, like every other empty field, so its placeholder never shows half drawn. - **Prefill links and the form editor choose through the picker** (Issue #860): the New row form a prefill link opens and the form editor's Audience, choice default and Yes/No default no longer draw native selects. Each opens the search picker every other selector uses, and a multiselect prefill or default picks several values in place through a new multi face on the form picker. - **Release folder retention** (Issue #873): `weave supervise` with `WEAVE_AUTO_UPDATE=1` now deletes old folders under `releases/` after every swap, every failed health check and every boot. It keeps the folder it serves and the most recently served good one for rollback, or only the newest good folder while the image serves. `releases/state.json` stays, and a folder it cannot remove is logged without blocking the swap. Production's volume had filled with three release folders, one of them a failed v0.4.89. - **A malformed percent-escape in a path answers 400 everywhere** (Issue #869): `decodePath` now throws an `invalid` WeaveError for a bad escape such as `%E0%A4%A`, so the Cloudflare worker answers 400 where it answered 500. A sparkline style outside `line`, `column` and `winloss` draws a line instead of reaching the SVG markup. The list-marker conversion and the editor's reference chips build DOM nodes instead of parsing HTML strings. - **Cool slate surfaces** (Feature #317): the canvas, panel, sunk surface and dividers move from Kami paper and navy to a neutral slate in both themes, on the app, the applet and the auth page. Text, links, primary and the rope's strands keep their colours. `brand/README.md` carries the surface table every future design reads, and `test/surface-palette.test.mjs` keeps the app on it. - **Users system table** (Feature #318, Issue #852): every account and pending invite is now a row of Workspace/Users, pinned in the sidebar under Workflows. A new row with an Email invites that person at its Role and answers with the one-time link; a new row with Kind Agent makes an agent account and answers with its token once. Editing Role or Name changes the account, deleting the row revokes the invite or deletes the account, and any table can relate to Users. Only an architect writes Users rows, and no row, query, export or MCP answer carries a token, session or invite hash. - Each workspace's Workflows row opens that workspace's own workflows at its own address, and the root lists only its own. The server scopes the Workflows query, row list and trash to the asking workspace, a member cannot read another workspace's workflow by id, and existing rows take their workspace from the table their rule runs on (Issue #877). ## v0.4.90 (2026-10-12) - Once a workspace, or the hub root it belongs to, holds an account, a caller with no token and no session is capped at observer even while the wall is off: reads, comments, form submits and bug reports still answer, and every other write answers 401. A hub with no accounts anywhere keeps today's open local use (Issue #534). - dev.weave.grunion.ai follows main: ready pull requests land after a sub-minute Dev gate, the supervisor swaps dev to each main commit with health-checked rollback, and production releases wait for an approved run (Feature #305) - **The container image is pinned by digest and runs on code it cannot change** (Issue #524): `FROM` names `node:22.23.2-slim` by its `sha256` digest, so a build gets the bytes that were reviewed rather than whatever the tag points at that day; the application tree is copied as root and the `node` user that runs it can only read it; `.dockerignore` matches `*.db`, `files/`, `node_modules` and the other state patterns at every depth instead of the repository root alone. `test/security/dockerfile-hygiene.test.mjs` guards all three. - One rail across several logins. The start page's cookie now holds every login this browser has signed in with, and `/start` lists the union of the workspaces those logins open, each row naming the login that opens it once there is more than one. "Add login" sends the person back to the provider with its account picker and adds the result; "Sign out" forgets them all. Every sign-in, at the start page or at a workspace, runs find my workspaces and sets a `wv_rail` cookie for the whole base domain naming the workspaces found, so a workspace host draws chips for workspaces it holds no session for; such a chip opens through `/api/start/open/`, which picks whichever held login has an account there. The rail's + button is now a menu: New workspace, Add login, Find my workspaces. An invite link opened in a browser that already holds a login offers "Accept as " for each one, which redeems the invite with no provider trip, or "Use another login". A new device starts with the docs workspace only and rebuilds the rail one sign-in at a time; no server-side record links logins (Feature #279). - `test/find-workspaces.test.mjs` covers the login union and labels, the rail cookie, opening with the right login, held chips on a workspace host, the auto-run on a plain sign-in, the invite chooser and sign-out; `test/find-workspaces-browser.test.mjs` drives the start page across two logins and the rail menu in both themes (Feature #279). - **AES-GCM decryption demands the full 16-byte tag** (Issue #533): the keystore and the backup archive both passed `setAuthTag` without telling `createDecipheriv` how long the tag must be, and Node then accepts a tag cut to 12, 8 or 4 bytes, each cut lowering the forgery cost. Both sites now pass `authTagLength: 16`; `test/security/gcm-tag-length.test.mjs` proves a keystore entry with a 12-byte tag is refused. - **Header crumbs stay whole beside the new toolbar buttons** (Issue #863): on a desktop the Sort button that #69 added to the table toolbar draws as an icon button (its name stays as a tooltip and in the phone tools sheet), and the dock header packs its icon buttons tighter, so the table crumbs at 1440px and the current crumb in a 550px dock are no longer cut off. - `GET /api/health` names the running version to an anonymous caller again, so the production supervisor, which waits for a candidate's health to report its own version, can swap in a new release on a walled instance. The build, sizes, workspace and backup state still need an architect (Issues #843 and #844). - Typing "- ", "* ", "+ ", "1. ", "1) ", "- [ ] " or "- [x] " at the start of an empty line in a document turns it into a list or task item on the space, not on Return. Backspace right after gives the typed marker back, and nothing converts inside code blocks or table cells (Issue #750). - The loader now leaves on the woven mark instead of waiting out its 2 s cycle: on finish it fast-forwards the rope to the next woven pose at 3x, holds 120 ms and fades over 160 ms, so a page that finished mid-cycle is visible within about half a second of its last response instead of up to 1.9 s later. The grid loader exits the same way. (Issue #392) - **The core MCP tool list fits its budget again** (Issue #857): stored positions (#36) and the forms tool pushed the default `tools/list` to 15,478 bytes, past its 15,400-byte cap. The `weave_call` one-line summaries and the `weave_workspace` logo clause are shorter; no tool loses an argument or a capability. - Every action menu now opens through one builder and wears the column header look: the ⋮ menus on tables, records, spaces, share pages, documents and decks, the nav table ⋮, the column header menu, the workspace tray menu, the account menu and the folded breadcrumb menu. Focus lands on the first row; the arrow keys, Home, End and typeahead move it; Escape and Tab close the menu and return focus to its button. Rows carry `menuitem` roles. On a phone the same menu opens as a bottom sheet with 44px rows, and the table sheet's actions open from a Table actions row. Items, labels and order are unchanged (Feature #296, Issues #762, #763). - Every object kind has one menu, and every way of opening it shows the same rows in the same order: the ⋮ button, a right-click on the page header or the sidebar row, and the workspace chip on the rail (Feature #296, Issues #761, #764). - Menus run in four sections, Settings, Organize, Share and Export, with Delete last. Export is the one labelled section: each row names a format (Markdown, Mermaid, HTML, PDF, CSV, JSON) with its extension beside it. - Rows that belong elsewhere left the menus. **Sort** shows on the desktop toolbar too, so a column's ⋮ no longer sorts. A record's Activity and Show or hide fields buttons, and its copy link, show in the dock and on a phone. Rename a table by editing its title or double-clicking its sidebar row; change a workspace logo by clicking it on the workspace page. Link preview before sign-in is a switch in the workspace menu. A deck's download, model, and new version controls are buttons on its panel. Column settings read **Field settings…**. - Every menu deletes in one click: tables, spaces, records, share pages and fields show **Move to trash**, and the **Moved to trash** toast's **Undo** puts the object back where it was. Tables and spaces no longer ask for a hold, and fields and share pages no longer delete for good. The selection bar's trash uses the same words and Undo. A space's sidebar row gains the ⋮ every other row has (Feature #296). - **Delete workspace…** has left the workspace menu and its confirm dialog is gone until Settings takes it; `DELETE /api/workspaces/:id` and the Trash page's Restore stay (Feature #297). - A share page restored from the trash takes back its old place on the workspace page instead of landing last (Issue #855). - **The vendored mermaid bundles DOMPurify 3.4.16** (Issue #559): `public/vendor/mermaid.min.js` is a fresh build of mermaid 11.17.2 whose DOMPurify is 3.4.16, past GHSA-55q2-fjhq-7xh7 (fixed in 3.4.13); every published mermaid bundle through 12.1.0 still carries 3.4.12. `security/vendor.lock.json` records the build recipe, and `test/security/mermaid-dompurify.test.mjs` fails on a bundled DOMPurify inside the advisory range. - Forms are managed inside a table through its Forms action: choose fields, labels, required values and defaults, then open, copy, edit or pause the same permalink. The editor preserves server-filled values and follows the existing Workspace/Forms registry (Feature #303). - Native form management has HTTP, CLI and MCP equivalents, with architect checks on the workspace and its registry and privileged access to hidden configuration, including accountless workspaces with sign-in required (Feature #303, Issue #815). - Form field sections are keyboard-accessible, and unrelated edits retain multiline and partial-date defaults (Issues #816, #817). - Spaces, tables and the workspaces in the rail keep a stored order. A `position` (0 is the first place) moves a space or a table on every door: `weave_update_space`, `weave_update_table` and `weave_move_table`, `PATCH /api/spaces/:space`, `PATCH /api/tables/:table` and `POST /api/tables/:table/move`, and `weave space update`, `weave table update` and `weave table move` with `--position N`. The workspaces' order lives on the hub root: `weave_workspace` order and move, `PATCH /api/workspaces/:name`, and `weave workspace order` and `weave workspace move`. A workspace saved before this keeps the order it showed, and a moved table lands last in its new space (Feature #284). - The phone new-row sheet names the row after its table when the table has no row term ("New task", "Name this task" on a Tasks table) instead of "New row"; the phone New button reads the same (Issue #792) - The phone new-row sheet opened from a table or view has no table chip, so the row always lands in that table; the chip stays only for a create with no table in context (Issue #793) - On a phone the new-row Name is a single line whose Return key reads Done and creates the row through the Create button, never while the name is empty or an IME composition is open; the desktop sheet keeps its two-line Name (Issue #794) - **The workspace logo and the row permalink button keep a 44px tap target on a phone** (Issue #864): the click-to-edit workspace logo (#69) is a 44px box on a phone or coarse pointer, and the permalink button that #69 brought into the phone dock header keeps its full 44px hit area (the crumb path no longer clips it, and it keeps a 4px step from the dock's action buttons). - On a phone, a list row's lead and chips open their editor in place, so a field changes without opening the row (Feature #300) - On a phone, Fields in the table menu opens a Phone row sheet: pick one lead field, order the chips on line two, or turn a field off, saved per view as phoneRow (Feature #299) - Prefilled create links: `/t/
/new?=&…` opens a table's new-row form with those fields set, and nothing is saved until Save. Selects match by option name and relations by the target row's Name; an unknown or computed field is left out and named in a notice, as is a value that resolves to nothing. **Copy create link** in the table menu and in the form builds one. A signed-out visit now keeps its query string through sign-in, so the link lands on the filled form after Clerk; values are capped so the link fits the sign-in hop (Feature #267). - Public form permalinks accept URL-prefilled drafts, and the form submit endpoint accepts JSON or standard form bodies. Stable field IDs, input keys and aliases preserve links through label and field-name changes. Required fields and typed input checks reject invalid or hidden values (Feature #303). - Form submissions have sender, form and server rate limits, bounded request bodies, durable 24-hour retry receipts and 60-second exact duplicate detection. Browser retries, CLI and MCP use the same receipt contract. Request-local attribution prevents another signed-in request from changing the submitter (Issues #810, #811, #812, #813). - The bug reporter keeps filing after the Issue table's Description field is renamed or its Symptom field is missing; only row forms resolve their fields strictly (Feature #303). - The table toolbar no longer has a **Stats** button, and the statistics dialog it opened is gone (Feature #308). The Σ rollup row, the space rollup tiles and the sidebar stats line stay. Agents and scripts still read every column summarised through `weave stats`, `weave_stats` and `GET /api/tables/:ref/stats`. - Share links are an architect's to mint, list, renew and revoke: a link passes the wall, so an editor no longer publishes a row, table, space or saved view by minting one, and cannot read back the token of a link an architect made. Every link now lasts thirty days unless an architect renews it on the same token (`weave share renew`, `weave_shares` renew, `POST /api/shares//renew`) or names its own expiry; links minted before expiry existed get thirty days from the upgrade (Issue #496). - The built-in Showcase no longer ships cells that read as garbage: `Days left` counts down from Due dates the seed anchors ahead of today instead of showing a negative number that grew worse as the seed aged, and `Label` stays empty on a row with no Category or Priority instead of drawing a bare ` · ` separator. Guard tests walk every seeded showcase cell, and the grid in both themes, for `#ERR`, raw markup and values that are only punctuation (Issue #580). - **A provider sign-in no longer answers 500 when the hub root holds no accounts** (Issue #859): find my workspaces (#11) read the creator's root account name without checking it exists. Sign out everywhere now also drops the start page's held logins and clears the start and rail cookies, so the start page cannot reopen a workspace without a new sign-in. - **Canonical tracker** (Feature #305): a server started with `WEAVE_TRACKER_CANONICAL=1` keeps its Development rows and skips the boot manifest sync; other servers forward bug reports to it when `WEAVE_BUG_FORWARD_URL` and `WEAVE_BUG_FORWARD_TOKEN` are set, filing locally with a note when the forward fails; `scripts/export-development.mjs --from --token-env ` exports the manifest over HTTP. ## v0.4.89 (2026-10-11) - A browser at the apex root now gets the start page whether or not it is signed in, and a permalink to the default workspace at the apex answers 301 to that workspace's own host. The apex `/api` and `/mcp` keep answering for the default workspace, so scripts and connected MCP clients need no change. The start page's type now steps 24, 18, 15 and 13 px (Feature #283). - Deleting an attachment, a workspace logo or a purged row removes its bytes from `files/` once nothing else in the workspace names them; a trashed row keeps its files for a restore (Issue #540) - Phone: a tap into a document no longer lights Undo in the writing bar. A caret move with no edit adds no undo step, so the first press of Undo always undoes a real change (Issue #755). - A chip now pins its own weight at 400, so a state chip on the bold current row of a picker no longer turns bold and reads the same as the chip in the cell beside it (Issue #770). - Run full Linux verification in six bounded test shards on the proposed merge commit. The required CI gate checks every executed test file, rejects missing browser coverage, and retains per-file timings. Keep platform compatibility checks on scheduled and manual runs. (Feature #305) - ⌘F and Ctrl-F on a focused grid cell now open the browser's own find bar instead of the table search. `/` and the toolbar magnifier still open the table search, which reaches every row of a paged table; the key sheet and the Handbook list `/` alone (Issue #822). - A computed field's glyph sits on its name only: the column header, the record field label and an opened relation chip's field label. Lookup is now marked `↳`, so the north-east arrow means only "opens" on a link chip, and every mark (`ƒ` formula, `Σ` rollup, `↳` lookup, `→` relation) is a text glyph of one footprint. Computed values drop their glyph and type word; a record row shows the value in a faint gray box lined up with editable values, and a table fills the whole computed cell with the same tint (Issue #775) - The right-hand tray now behaves like a dialog: closing it by Escape, Cancel, the close button, the backdrop or a save puts focus back on the control that opened it, Escape closes the tray and nothing behind it (an open Activity panel stays open), an error the form already shows is not toasted a second time, and on a phone the tray fills the screen, slides in from the right edge and keeps its head and action bar clear of the notch and the home indicator. New field and Edit field run on it unchanged (Issue #782). - A CSV export writes a cell that opens on `=`, `+`, `-`, `@`, a tab or a carriage return behind one quote, so a spreadsheet reads it as text, and a CSV import strips that one quote again; a number is exported as it is (Issue #531) - Gate cut-off text: a probe finds clipped text, truncated ellipsis and line-clamp boxes, and inputs whose value or placeholder runs past the box; `scripts/cutoff-sweep.mjs` walks the flicker journeys and every dialog and popover at 1440x900 and 390x844 and writes `findings.json`; the browser gate rejects any new cut-off and fails until a fixed one leaves `test/cutoff-baseline.json`, which only shrinks. (Feature #298) - One disclosure toggle: the sidebar space caret, the document and Fields block carets, the heading fold and the Schema and Members summaries all draw the same 16px lucide chevron in a 24px box (44px touch target at phone width), turned down when open, muted at rest and body colour on a faint wash under the pointer; no text-glyph carets remain, the date picker's month and year buttons included, and every button-style disclosure reports aria-expanded (Issue #766) - **Environment reference names every variable the CLI help names** (Issue #538): `WEAVE_ALLOWED_HOSTS`, `WEAVE_FRAME_ANCESTORS`, `WEAVE_INLINE_FILE_TYPES` and `WEAVE_APPLET_PASSCODE` have a row in the Handbook table, a line in the Dockerfile header and an example in compose.yaml, and `test/deploy-artifacts.test.mjs` now fails when a variable in `weave help` is missing from any of the three. - The document and entity PDF exports carry the same headers the file route gives a PDF: an RFC 6266 encoded filename with an ASCII fallback, `nosniff` and the sandbox policy (Issue #544) - **The public development export carries no home directory or personal address** (Issue #527): `scripts/export-development.mjs` rewrites `/Users//` to `~/` and removes every email address outside `@grunion.ai` before it writes `docs/development.json`; the shipped file is regenerated clean. - Field marks sit on the name's line: the relation arrow and the lookup, rollup and formula marks in column headers and entity field labels are inline 12px marks, muted and centred on the label text with the target table still in the tooltip, instead of a superscript riding 5px above the name (Issue #790) - Deleting a field or a share page now moves it to the trash. Restore brings a field back with its values, its column position, its place, width, filter and sort in every table view, and for a relation its inverse and every link. A share page comes back with its share links. The Trash page lists both, and `?hard=1`, `hard: true` or `--hard` still purges. New doors: `POST /api/tables/:table/fields/:field/restore`, `POST /api/views/:id/restore`, `GET /api/views?trash=1`, the `weave_restore_field` tool, `weave_views` action `restore`, and `weave field restore` / `weave view restore` (Issue #768). - Dialog name fields drop a class that had no style and an inline width that `.form-control` already sets. The bug note is now a standard small form field: its side padding moves from 7 to 8 px, its corner radius from 8 to 4 px and its minimum height from 40 to 32 px (Issue #787). - A formula that calls `constructor`, `__proto__` or `prototype` reads `Unknown function`, as any other unknown name does, instead of resolving the inherited object member (Issue #530) - Behind `WEAVE_TRUST_PROXY=1` the sign-in and slug rate limits key on the last `X-Forwarded-For` entry, the one the proxy appended, so a client cannot pick its own key; the Worker keys on `CF-Connecting-IP` (Issue #502) - GitHub releases follow successful main CI, preserve immutable tested tags, resume interrupted publication, and verify the hosted supervisor reaches the released version (Feature #305). - `GET /api/health` answers `{ok, name}` to an anonymous caller and the version alone to a signed-in editor or observer; the build, sizes, workspace name and backup state are an architect's to see. Health probes keep their 200 (Issue #520) - Icon-only buttons centre their glyph: icons carry no margin, buttons space an icon from its label with a 5px gap, so the selection bar's Move to trash, the view delete and grip buttons and the toolbar no longer sit 2.5px left of centre; filter chip labels trim to cap height so the letters line up with the colour dot (Issue #767) - Inline fields take their padding and type size from two tokens, `--wv-inline-pad` and `--wv-inline-fs`, at one of two densities: grid (1 px 4 px) and record field row (2 px 6 px). The record summary strip joins the grid density, so its fields move from 12 to 13 px with 4 px side padding, and a date on the record page loses its extra form padding: 5 px 8 px becomes 2 px 6 px in the field rows and 1 px 4 px in the summary (Issue #786). - An error that is not weave's own answers `Internal error (request )` with `code: internal` and the id; the message and stack go to the server log under that id instead of to the caller, on the API, the task applet and the Worker (Issue #522) - List cards keep their inset again: rows in the space page's table list, the home page's system tables and the New table row sit 14px from the card edge, and trashed workspaces render as list rows with the card title on the same inset (Issue #788) - **The workspace logo tool drops its `mime` argument** (Issue #545): a logo is typed from its bytes, so `weave_workspace` no longer advertises an argument the engine ignored. - The post-sign-in `next` is resolved as a URL against the page origin on the sign-in page and on the server, so the backslash, tab and newline spellings a browser reads as another host fall back to the mount root (Issue #501) - Phone: the reorder grip on a workflow state or select option in the field tray is a 44x44 touch target at the row's edge, so a long press no longer has to land on a 16px sliver (Issue #803). - Timestamps read like the rest of weave: the trash, the entity activity pane, comments, the Activity table and an event's When, restored-version notes and trashed workspaces print "just now", "12 min ago", "Today 3:59 PM", "Yesterday", "Sep 16" or "Sep 16, 2025" through one date-core helper, with the full timestamp on hover, instead of the browser's raw "9/16/2026, 1:06:37 AM" (Issue #789) - Release delivery reads `gh` responses of any size up to 256 MiB, so a compare against the last release that passes 1 MiB no longer fails publication with `spawnSync gh ENOBUFS`. (Issue #841) - The task applet reads a request body only when the applet is configured and the declared size is under the 10 MiB cap, answering 413 otherwise; every route `limit` (entity lists and queries, activity, audit, undo, document revisions, search) is clamped to 500, the most the UI asks for, and `WEAVE_MAX_ROWS` raises it (Issue #521) - The picker, command palette and table searches share one bare search style. The table search is no longer built as a bordered form field and then stripped. Sizes stay as they were: 13 px in the picker, 16 px in the palette and 12 px in the table search (Issue #785). - **Security policy for the product that ships** (Issue #528): SECURITY.md, the README Security and Self-hosting sections and llms.txt describe accounts and roles, `wv_` tokens, provider sign-in, the `requireAuth` switch, both binds and the request checks; the scope list adds privilege escalation, share links and stored content, and the `html` document origin is stated as a limitation under review. - The page heading and the record name are built by one title editor and share one base style. Each keeps its own hover, size and edge: the table, space and home heading keeps its outlined hover at 20 px, and the record name keeps its filled hover at 24 px (Issue #784). - UI catalog in the repo: `npm run ui-catalog` seeds a throwaway workspace from the repo's own seeds, harvests every control at desktop and phone width in light and dark, and writes `docs/ui-catalog/index.html`, one page grouped by the job each control does, with live and captured views side by side. Chips and text inputs key on their component, so a chip's tone and context are looks of one chip (Feature #301, Issue #774). - weave now keeps the verified email address from sign-in. It asks the provider for `openid email`, stores one address per sign-in identity only when the provider marks it verified, and stores no name, picture or other profile claim. This reverses Feature #252 for the email claim only. After sign-in, `/start` lists the invites waiting for that address next to the person's workspaces and accepts one in place; the one-time invite link still works. Architects see each member's address in Members, a person sees their own at `/api/auth/me`, a later sign-in fills it in for an identity linked before this release, deleting the account deletes it, and `weave export` leaves it out. The privacy policy says so (Feature #290). - `test/verified-email.test.mjs` covers the verified-only rule, normalisation, backfill, export and delete, single-use acceptance by the invited address only, the start page rows and the Members view; `test/auth-oidc.test.mjs` now expects the `openid email` scope (Feature #290). - Table search is saved with the view, beside its filter and sort. Typing in the toolbar box writes the view's `search` after the usual pause; opening the view or reloading brings back the box text and the matching rows, which the server finds through the query's `search`. Duplicate copies the search, a view started from **+ Add view** or reset to the defaults has none, and clearing the box writes an empty search. The raw Blank table still searches without writing. `weave_table_view`, `weave table view --search`, `/api/tables/:table/views/:view` and the **Workspace/Views** row's new **Search** column read and write it, and schema documents carry it (Feature #307). - Workflow webhooks post only to public addresses: the host is resolved first, loopback, link-local, private and cloud metadata ranges are refused with the reason on the rule's Health, and a redirect is a failed delivery; `WEAVE_WEBHOOK_ALLOW_PRIVATE=1` allows the ranges for a self-hosted instance (Issue #500) - A workspace without a logo shows a filled 36px tile in the rail, its colour a stable hash of the workspace id into the option hues and its initial in white, the same size and radius as an uploaded logo; the breadcrumb workspace mark wears the same tile, and hover and the active ring still read (Issue #791) ## v0.4.88 (2026-10-10) - Created By, Modified By, comment authors and version labels never print an MCP client's id: a write through the OAuth door stores ` via MCP`, and rows written before read as the person chip with "via MCP" (Issue #675). - The copy-link button beside a row's title in the header, and the copy-link on each document and deck section, show at rest instead of waiting for a hover; the section copy-link is now a focusable, labelled button (Issue #747). - A date and time column is never narrower than its widest month and hour, so "May 28, 2026 10:48 AM" shows whole in Safari instead of ending in an ellipsis (Issue #217). - The document download menu is fully opaque: the fade now sits on the three-dot button alone, which stays lit while its menu is open (Issue #430). - Forms: a `Workspace/Forms` row lets someone without write access add exactly one row to one table, as themselves. Each form names its table, the fields it shows in order with labels and defaults, the fields the server fills, an on switch and a submit floor (Observer, or Anonymous when the operator sets `WEAVE_ANONYMOUS_FORMS=1`). It opens as a page at `/f/`, `POST /api/forms//submit`, the `weave_form_submit` MCP tool and `weave form submit` (Feature #257). - The bug reporter is the first form. `POST /api/bug-report` stays, as an alias that submits through the **Report a bug** form on the weave docs workspace (Feature #257). - The weave docs workspace is built in: delete, rename and leave answer 403. Any login a workspace on the instance knows opens it as an Observer, with no invite and no account row there (Feature #257). - A column header now says where its value comes from. Hovering the mark on a lookup or rollup header reads the route in words (`↗ Name via Project → Product/Project`, `Σ sum of Estimate via Tasks → Product/Task`), and a relation header gains its own `→` mark naming the table it points at (`→ Product/Task, many` on the many side). The header line itself is unchanged (Issue #66). - The chip mockups under `docs/mockups/` are drawn from the live chip stylesheet again and fail their tests when they drift from their generators. The extractor keeps `@media` rules under their condition, so the phone picker sheet and touch-size caret no longer leak into desktop pages, and the mockup links and home badges use the markup `chip-view.js` draws (Issue #754). - One chip renderer, `public/chip-view.js`, behind every surface a row appears on: relation cells and the References panel in the browser, document pages and HTML exports through `src/markdown.js`, and the document editor's overlay. One stylesheet, `public/chip.css`, behind all of them; the second copy that lived inside `src/markdown.js` is gone (Feature #191, Feature #288). - The chip is the Ledger: the state mark, the `#id` in mono, the title, the configured chips, the home badge and the caret. No open mark and no avatar by default, per Feature #185. A relation cell keeps its ✕. - The state mark is a button: it opens a picker grouped by category (Not started, In progress, Done, Canceled) and writes the state to the row. A select or multiselect segment opens its own picker. The name and the `#id` still navigate. - The card is the Tile: the `#id` and the title, the description preview, a state row that is also the control, and the configured properties two columns across. - The editor's reference chip is sized to its own label instead of to the width of its `[[…]]` source, and a bare reference is stored as `[[Table#n|Name]]` once the editor resolves it, so the name is never cut (Issue #679). - A segment is drawn at the shared chip size; the `.wv-seg-state` rule that shrank it to `.82em` is deleted. - A table query by qualified name works: `POST /api/tables/Product%2FTask/query` now resolves like the bare name, and an unknown table key answers `not-found` instead of `No route` (Issue #439) - **Share links: one grant for a row, table, space, saved view or the whole workspace** (Feature #194). A grant carries a `wvs_` token, a scope, an access rung (read, edit, manage; comment is reserved and answers 403), a public or private visibility, a label, an optional expiry and a revoke stamp. `/s/` opens read-only pages for its scope: a row's fields and documents, a table's rows under the columns its schema names, a space's tables, a view's blocks. `Authorization: Bearer wvs_…` reaches the scoped JSON API, where edit changes rows in scope and manage also adds and trashes them; no link touches schema or the registry, and anything outside the scope answers 404. One `withinScope` check gates the pages, the reads and the writes. A private link opens only for a signed-in account. Editors and architects mint from the **Share…** dialog on a row, table, space or saved view, `weave share`, `weave_shares` or `/api/shares`; an architect revokes any link, an editor its own, and every mint and revoke is audited. The export leaves tokens out. Saved-view share tokens lift into grants on open, and `/view/` now redirects to `/s/`, so links already handed out keep working. - The interface runs on one type scale of eight steps (11, 12, 13, 14, 16, 18, 20 and 24px) held as `--fs-*` tokens, with nothing under 11px: avatar initials, the system badge, type-tile labels, column marks and mention carets all moved up to 11px, and the document editor's headings and code sit on the same steps. The UI font is now a declared system stack; the stylesheet no longer asks for Inter, which it never shipped (Issue #383). ## v0.4.87 (2026-10-09) - The blank icon slot browser case parks the pointer and reads each slot through `settled()`, so it no longer fails its first run on a stray `:hover` (Issue #753). - The chip-card-anatomy export splits a selector list on top-level commas only, so a comma inside `:is()`, `:where()` or `:not()` no longer copies that rule into the Handbook export (Issue #745). - A rollup that takes the min or max of a date, and a lookup of a date, now sort as **Oldest to newest** and **Newest to oldest** in a column's ⋮ menu, the same as a plain date column, instead of Ascending and Descending (Issue #751). - The first edit after opening a document can be undone. Vditor started its undo history empty, so Undo stayed greyed until a second edit; the editor now records the opening text as the first step (Issue #752). - **A Money template sits beside the CRM** (Feature #286): the weave docs workspace holds a Money space marked as a template, with Accounts, Months, Bills, Categories, Transactions and Income. Bills turns a weekly, quarterly or yearly charge into a Monthly cost; Months totals Spent, Earned, Net and Over budget; Categories add up their Bills per month. `test/fixtures/templates/money.json` is its exported shape, and the template exercise runs it like the CRM: every table adds, links, unlinks, removes and restores, in the template and in its Use template copy. - A pasted link now unfurls with the row's name alone as its title; the table and `#id` move to the first description line (`weave › Development › Issue #287`). The browser tab reads `Issue #287 · · ` before and after the app loads, on the row page and in the dock (Feature #285). - Each workspace can answer at its own host. With `WEAVE_BASE_DOMAIN=weave.example.com` set, `acme.weave.example.com` serves the workspace that `/w/acme/` serves, the apex keeps the default workspace, `/w//` keeps working on every host, and a slug nobody holds answers a plain 404. Sign-in stays on the apex's one callback and hands a single-use 60-second code back to the workspace host, which sets its own host-only session cookie. The MCP door and its protected-resource metadata answer per host (Feature #283). - A workspace slug is now a DNS label: an underscore folds to a hyphen and the limit is 63 characters. Create and rename refuse 16 reserved names, `weave` and `grunion` among them, with the stable codes `slug_taken`, `slug_reserved` and `slug_invalid`, and a renamed workspace keeps its old slug as a 301 alias that no other workspace can claim (Feature #283). - The New workspace dialog derives the address from the name, checks it as the person types, holds Create until the address is available, and offers four suggestion chips when it is taken or reserved. Signed-in accounts check an address at `GET /api/workspaces/slug` (Feature #283). - `/start` at the apex shows nothing about any workspace until the person signs in at the provider, then lists every workspace that identity opens, each at its own host, and lets an architect on the hub root create one. A signed-out visit to the apex root goes there (Feature #283). - `test/workspace-hosts.test.mjs`, `test/workspace-start.test.mjs` and `test/ws-new-slug-browser.test.mjs` cover host routing, the allowed-host rule, aliases, the handoff code's single use, host binding and 60-second expiry, per-host MCP metadata, each availability state, the race on create, and the dialog in both themes (Feature #283). - On a phone, a caret in a document brings up a writing bar on top of the keyboard: Undo, Redo, checklist, bulleted list, heading, insert (opens the slash menu at the caret) and hide keyboard, each a 44px button. Selecting text still swaps in the formatting row, and the floating search and New step aside while either bar is up (Feature #289, Issue #707). ## v0.4.86 (2026-10-08) - Swiping a phone list row no longer reveals state actions; swipe is removed for now at Kyle's call. Rows still open on a tap, and the row page's next-step button still moves a row to its next state (Feature #275). ## v0.4.85 (2026-10-08) - The loading mark now ships inside `index.html` and appears on first paint once the 500 ms threshold passes, so a fresh load no longer sits on a bare shell while the app fetches the animated rope (Issue #391). - A phone list row no longer leaves a blank where an empty relation sits. The cell held only its hidden "+ link" button, so it still took 47px between the chips either side of it; now a relation cell with no linked row is left out of the line, the chips that remain pack left, and the "+N" count reads from what is left (Issue #721). - `test/phone-list-rows-browser.test.mjs` seeds an empty relation alongside a filled one and asserts the empty cell is not displayed, the filled one still is unless the line overflows, and no two chips in a row sit more than the 6px flex gap apart (Issue #721). - On a touch screen the reorder grips now rest at .7 opacity instead of 0, so an entity page field row, an entity page block, a document section and a list view row each show the handle that says they can be dragged. A mouse still reveals them on hover and nothing is drawn differently on a desktop (Issue #744). - `test/phone-grips-browser.test.mjs` reads the resting opacity of all four grips in both themes, in Chromium at 390x844 and in WebKit on the iPhone 15 profile, and holds the desktop hover reveal in place (Issue #744). ## v0.4.84 (2026-10-08) - The grid's + New row is now a row. Its cell takes the density's row height and the button fills it, so the foot measures 32px Compact, 44px Comfortable and 72px Spacious instead of 34px at all three, and the add row no longer reads as a short strip under the last data row. It stays the sticky foot of the grid (Feature #196), and the phone list's add cell keeps its own height (Issue #246). - Every drag-to-reorder surface shares one treatment, with a mouse and on phones: grid column headers, the Fields popover, view tabs, group levels, workflow states, select options (new), entity page fields and blocks, and list view rows. On pickup the item lifts with a slight scale and a deeper shadow; the item itself waits, dimmed, in a dashed placeholder at the slot it will land in while its neighbours slide aside; the drop settles it into the placeholder, and Escape or a drop outside sends it home. On touch a 250 ms long press lifts it, with a haptic tick where the platform offers one. The list auto-scrolls near its edges and stops once the pointer leaves the edge band. With reduced motion the placeholder stays and the scale and slide go. Keyboard reorder is unchanged (Feature #282). - Between roughly 1040px and 620px of window the row pane's Fields block kept the desktop two-column form, and the value column shrank to as little as 40px: chips became slivers and the Amount, Vendor, Due and Note values disappeared. The block now sizes its columns from the pane rather than the window, so the 110px label arrives when the pane is under 330px and the value moves under its label when the pane is under 265px. The value column holds at 120px or more at every width from 1440px down to 375px (Issue #577). - A date value no longer demands 120px inside a column narrower than that, so the day, month and year stay readable in a narrow row pane (Issue #577). - On a phone, the Show more toggle under a clamped table or workspace description taps as a 44x44 target. It was drawn at 64x17 and kept that hit area; the drawn link is unchanged (Issue #737). - On a phone and on any coarse pointer, every control the reader can tap carries a 44x44 hit area. Crumb chips, the copy-permalink and actions buttons, the Set icon button, the view title, the workspace Schema and Members disclosures, the sidebar wordmark, the section carets, the Fields rows and their chips, inputs and date button, and the bug panel's category, send and mail controls were between 10x14 and 44x40 and now pad out to 44 without changing what is drawn, except the Fields rows and the inputs inside them, which grow to a 44px line (Issue #737). - `test/phone-touch-targets-browser.test.mjs` sweeps the workspace home, the table list, the row page, the full-screen menu, a picker sheet and the bug panel in both themes, in Chromium at 390x844 and in WebKit on the iPhone 15 profile, and names every control whose tappable span through its centre falls under 44px (Issue #737). ## v0.4.83 (2026-10-08) - The flicker sweep no longer reports a screencast frame that answers the reader's own click, keypress or scroll. The frame rule now shares the input window the DOM rules have had since Issue #631 (Issue #638). - The flicker probe measures the reader's input window in painted frames rather than 50 milliseconds, so the scroll-grid ratchet entry no longer comes back on a busy host and the flicker gate stops flaking there (Issue #712). - The flicker sweep and its gate now walk a second cell: the iPhone 15 profile in WebKit (393x659 viewport, touch, iOS Safari agent), next to desktop Chromium at 1400x900. Six phone journeys cover the bottom search bar, the full-screen menu, the table sheet, a picker sheet, tapping a list row open and going Back, and swiping a row open and closed. Phone fingerprints end in `|phone`; desktop fingerprints keep their old form (Issue #732). - The probe reports a new kind, `cut`: a painted element removed while the rest of its row is still sliding, so the gap shows for the rest of the slide. A see-through layer with nothing in it no longer counts as a transient when it is replaced (Issue #732). - `test/lib/browser.mjs` gains the shared phone profile: `launch(name, seed, { phone: true })` launches WebKit and `phonePage(browser)` opens a page as `devices['iPhone 15']` (Issue #732). - On an iPhone, a table in the menu opens on the first tap. Every hover rule that shows, hides or moves content now applies only where the pointer can hover, and on touch screens the table row menu, the add-table button and the unlink button stay visible instead of waiting for a hover (Issue #728). - On an iPhone, a tap anywhere outside an open menu, popover, picker, table sheet or the bug panel closes it. Every overlay now closes through one shared dismiss that listens for the touch itself instead of a click, which iOS never sends for a tap on empty space (Issue #726). - On a phone, Activity from a row's menu opens as a full-screen sheet above the row, with 44px close and filter controls, and the phone's Back gesture closes it. It used to open as a zero-width strip over the row (Issue #733). - On a phone, a list row keeps its chips on one line under the name at every row height. Chips that do not fit fold into a +N counter instead of wrapping onto a second line that the row clipped at Comfortable and Spacious (Issue #730). - On a phone, the list's #id column is as wide as the table's largest id, set in tabular figures, so "#149 ↗" no longer runs into the row name and every name and chip line starts on one edge 8px after it. The schema now carries each table's `maxPublicId` (Issue #729). - On a phone, the + button opens a New sheet for the current table instead of writing a blank row. The sheet is titled by the table's row term ("New bug" on Issue, "New row" on a table with no term of its own), switches tables from a chip, takes the name and the table's short choice fields, and writes nothing until Create, which adds the row and opens it. All fields adds it and opens the full row page (Issue #739). - On an iPhone, focusing a text field no longer zooms the page. At 600px and below and on touch screens every input, text area, select and editable block is at least 16px, and the app pages carry `maximum-scale=1` (Issue #724). - The phone browser tests now open their pages as an iPhone 15 in WebKit (393x659 viewport, touch, iOS Safari agent) through the shared profile in `test/lib/browser.mjs`, in place of a 390 or 375 px Chromium window. Desktop cases stay on Chromium. `touchScroll` scrolls the pane under a point the way a finger does (Issue #732). - On a phone, the row page header keeps one line: the crumb chip, the row menu and Close, each a 44px circle. Expand is gone (the row already fills the screen), and Copy link and Show or hide fields moved into the row menu (Issue #734). - Opening a row asks for the row once and for its references and document history checks at the same moment, instead of fetching the row twice and the rest after it, so a row on a phone opens one round trip sooner (Issue #735). - On a phone, scrolling the table list no longer selects rows or text under the finger. A list row opens only on a tap (down and up within 10px and 300ms); any longer or wider touch is a scroll, and list rows start no text selection (Issue #738). - On a touch phone, weave no longer offers keyboard shortcuts it cannot use: the ⌘K pill in menu search and the bug panel's ⌘↵ hint are hidden, and titles drop their shortcut clause ("Open bug beside the table", "Expand", "Send") through one helper. Desktops keep them (Issue #736). - On a phone, a swiped list row follows the finger both ways, stretches against either end, and settles open or shut on one transition. Closing keeps the actions in place until the row is home instead of flickering them away first, the tap that closes an open row does nothing else, and the revealed actions are exactly as wide as the buttons they hold, so the last one takes a tap in Safari too (Issues #725, #720, #731, #743). - On a phone, the table sheet lists the table actions (Column stats, Export CSV, New share page, Row term, Delete table) as its own rows under a Table heading, with Delete table last in red. The lone three-dot row whose menu dropped below the sheet is gone, so is the second Search box, and a Sort row picks the field and direction (Issue #723). - On a phone, the table sliders button draws its icon centred, and its sheet opens 8px below the button with the two right edges lined up (Issue #727). - On a phone, a row in a table with more than one state field shows one next-step button per field, stacked and named ("Move Status to In Progress", "Move Review to Approved"), and each moves and undoes only its own field. Swipe offers no state actions on such a table, since a swipe cannot say which field it moves; a table with one state field is unchanged (Issue #742). - Vendored files the document editor loads on its own (its icons, Lute and content themes) are now served with a one-hour cache, so opening a second row on a phone no longer refetches them. First-party files asked for without their version still revalidate every time (Issue #735). ## v0.4.82 (2026-10-08) - On a phone the document text runs the width of the screen: the pane's `@media (max-width: 600px)` padding rule had been overridden by the unconditional `#main` rule that follows it, and the document section's 18px drag-handle gutter now folds into the section head below 600px. The text column goes from 293px to 339px by permalink and from 329px to 347px in the dock, on a 375px screen. (Issue #709) - On a phone the row header no longer holds the top of the screen while you write: the sticky/loose ratio now subtracts the keyboard inset from the room it measures, and below 600px a header may take a quarter of that room rather than half. Desktop sticky headers are unchanged. (Issue #708) ## v0.4.81 (2026-10-08) - The CHANGELOG version heading reads `## v0.4.81 (2026-10-08)`. `scripts/changelog-fold.mjs` wrote the version and the date either side of a unicode em dash, which the house prose rule bans in anything published, and every heading in the file came from that one template. The 73 released headings were rewritten in the same change, so the file does not split at the version where the template changed (Issue #566). - Skip setup and Esc on the last onboarding step keep the workspace name confirmed with Continue one step earlier. Both posted an empty body, so the server fell back to the default and the name was gone; the primary button on the same step kept it. Skip and Esc on the name step itself, before Continue, still drop what was typed (Issue #615). - A rule that appends to a document now leaves a revision, so the document's history holds what the rule wrote and a reader can compare with it or restore to it. The revision is signed `workflow:`, the same name the activity entry carries. Two fires of one rule inside the revision window still coalesce into one revision, as two edits by one person do (Issue #700). ## v0.4.80 (2026-10-07) - A rollup or a lookup that draws rating icons now opens wide enough for all of them, and holds that width as its resize floor, the way a rating column has since Issue #404. A max-7 rollup drew 104px of icons in an 80px cell before. (Issue #564) - Opening another record in the dock starts it at the top. The dock reuses one scroller across records, so the previous record's offset carried over and the new record's title sat off screen above the reader; an edit or a refresh of the record on screen still holds its place (Issue #623). - A lookup of a rating over a to-many relation now draws one group of icons per related row instead of printing the numbers, and a related row with no rating keeps its slot as an unrated group. The chip, the card and the field dialog's sample draw it the same way. (Issue #377) - Checklist items in a document wrap between words. Before, Vditor's `word-break: break-all` let a line end mid-word ("sen / ds"); a URL longer than the line still wraps inside the column (Issue #715). ## v0.4.79 (2026-10-07) - Three browser suites launched their own WebKit or Chromium through `import('playwright')`, outside the shared harness. Those pages never saw `WEAVE_CPU_THROTTLE`, so the recipe DEVELOPMENT.md gives for a gate-only red could not reproduce one in `frozen-ground-browser`, `phone-shell-browser` or `editor-math-browser`. All three now ask the harness for the extra engine through `engineOf(name)`, which throttles it and closes it with the suite: the same file runs 13 s unthrottled, 16 s at throttle 4 and 36 s at throttle 20, where before the WebKit half ran at full speed whatever the setting. `test/browser-harness.test.mjs` now holds every `*-browser.test.mjs` to the harness, not only the ones its old pattern matched. - A first-paint wait that misses now says what the page had instead. `painted()` reports the hash, whether the route skeleton is up, what stands in `#main`, how many grid rows the page counts and the toast text, so a 30 s timeout on `.wv-grid tbody tr.entity-row` reads as a diagnosis rather than a bare budget. The cases on `#711` do not reproduce on this main: the cause was the route skeleton painting over a drawn page, fixed under Issue #713 and shipped in v0.4.78 (Issue #711). - Two browser cases stop losing the Verified gate to machine load. Both waited on a clock instead of the event they assert: `bulk-settle-browser` held the eight-row State change to a 600 ms wall clock, and `doc-autosave-phone-browser` gave the leave-triggered write a 400 ms request window. The bulk case now waits for the eight rows to leave the filtered view, the toast to read `Set 8`, and the grid's animations to finish. It keeps the counts it was written for: one `POST /api/bulk`, one page re-read, nothing else. In the phone cases the 600 ms autosave debounce is cancelled inside the page, so the only write that can reach the server is the one the leave handler sends, and the wait on it is a hang guard at 15 s. Both files pass at `WEAVE_CPU_THROTTLE=40`, where the old waits went red (Issue #619). - A browser case no longer loses the node it just waited for. `system-tables-nav` waited for `#main input.view-title` and then read it back through `page.$eval`, which resolves the selector a second time; `drawDatabase` opens with `main.replaceChildren()` and the view header settles on a ResizeObserver after that, so the read could land in the gap and throw. Under gate load the gap was wide enough to cost two unrelated changes a Verified vote. Twenty-eight reads across seventeen browser suites had the same shape and now go through `page.locator(selector)`, which re-resolves on each attempt. `test/browser-read-reresolve.test.mjs` fails on any new one, and `system-tables-nav` carries a case that forces the repaint and shows both halves: the single-shot read throws, the locator read waits it out (Issue #637). ## v0.4.78 (2026-10-07) - A table view can show as a **List** (View menu, **Layout: Table · List**): the same rows, filters, sorting and density, each row drawn as the table's Chip. The new **Group** button sets up to three ordered levels (select, workflow, toggle, checkbox, link or date by grain), each headed by a label or, for a link, the linked row's Chip, in option, table or A to Z order; every group folds. **Completed by** draws a checkbox field on each row and moves ticked rows to a folded **Completed · N** group; **Nest by** indents rows under a Parent link, with **Tab** / **Shift+Tab** and sideways drags. With no sort, drag sets a manual order saved in the view; a sort orders rows inside each group. Each innermost group ends in an add row that fills every group value and equals-filter and reads a typed date. The footer reads `3 of 8 to-dos · 1 completed`. All of it saves into the view and reaches agents through `weave_table_view` (`layout`, `group`, `completedBy`, `nest`, `collapsed`, `order`), the CLI and REST; the Workspace/Views row gains Layout and Group (Feature #276). - A rollup takes the number costume of its own (`format`, `decimals`, `unit`, `display` `bar`/`ring`/`heat`, `scale`, `color`), set on create or through `weave_update_field`, and a number aggregate over a checkbox or toggle counts a tick as 1: `avg` of a Done box is the share done, so a progress bar on a Trip's Chip is one rollup field (Feature #276). - A table opened with a record in the dock no longer turns back into the loading skeleton. The route skeleton fired at 150 ms even when the table had already drawn, because only the end of the whole route (which waits for the docked record) cancelled it; it painted over the rows and stayed. The skeleton now stands down the moment the page puts anything in the main panel. This was the cause of the `phone-shell-browser` WebKit timeouts in the gate (Issue #713). - Known limits of the List layout in this release: the Group button shows only in List layout (Table keeps the grouping and ignores it); the Statistics panel's Group by draws its fields from the same list but still groups one level; a list draws every row at once with no paging, and a link level reads at most 500 linked rows for its empty groups and table order. ## v0.4.77 (2026-10-07) - On a phone a glass search capsule floats at the bottom of the screen and opens the ⌘K search; on a table a blue New circle beside it adds a row. The bug button sits just above the bar, and pages and row pages leave room under their last line so nothing ends behind it (Feature #268). - On a phone each breadcrumb is a 36px chip with its icon and label, and the current one is bold; the menu button and the row page's back and forward buttons are 44px glass circles. The workspace crumb is left off on a phone, where the menu names the workspace (Feature #270). - On a phone a table is a list: each row shows its muted #id, its name over up to two lines and its values as chips underneath, at a declared 88px (68px with one-line names at Compact density) that the row window reads, so scrolling and jumping stay exact. A tap anywhere on a row opens the row instead of editing a cell. The column header and the + New row link give way to the bottom bar's New button (Feature #272). - On a phone (600px and narrower) the menu button opens a menu that covers the whole screen: the workspace name and a 44px close button on top, every space and table in 44px rows, the record count, size and version at the foot, and the workspaces, theme and help in a bar along the bottom edge. Wider screens keep the side drawer and the desktop sidebar (Feature #269). - On a phone a row in a table with a workflow field shows a wide blue "Move to " button pinned to the bottom of its row page. It moves the row to the state after its current one, refreshes the page and the table behind it, and offers Undo; a row already in its last state shows none (Feature #273). - On a phone a field's picker (workflow state, select, multi-select and icon pickers) opens as a bottom sheet titled with the field's name, with 56px options and a check on the current value. A list of eight options or fewer opens without focusing the search box, so the keyboard stays closed, and a single-choice list that short shows no search box at all (Feature #274). - On a phone, swiping a list row left in a table with a workflow field reveals buttons for the states after its current one. A tap moves the row there and the confirmation, with Undo, shows at the top of the screen. A vertical drag leaves the row as it was, and a tap elsewhere closes the buttons (Feature #275). - On a phone the table toolbar (search, view, row height, fields, filters and the table menu), which wrapped to three rows, folds into one sliders button beside the breadcrumbs. Its badge counts the active filters, and it opens a glass sheet holding the same controls as 44px rows, each opening the popover it opens on a desktop (Feature #271). ## v0.4.76 (2026-10-07) - The sidebar foot shows who is signed in: an account chip under the stats line with initials, the account name and the role in this workspace (Observer, Editor or Architect). It opens a menu with `Sign out of ` and `Sign out everywhere`, which posts `/w//api/auth/logout` for every workspace in the rail and lands on the sign-in page. A signed-out walled workspace shows `Sign in`; an open workspace with nobody signed in shows nothing. No email is shown (Feature #281). - A code block at rest no longer paints a 2px grey square above its top-left corner. The hidden source box keeps no border until the block opens for editing (Issue #704). - The document editor booting on a host already in the dock, which painted its body for 72 ms and then took it away, is on the flicker ratchet (Issue #634). - Linking a relation opens the ⌘K search scoped to the relation's target tables instead of a Name-only list: it matches any text on the row, shows linked rows checked, replaces a to-one value on pick, toggles a to-many set until `Esc` or `Return` on an empty box, and ends with `+ New `. `+ Link existing`, the relation editor in the grid and on the record, and the selection bar's `Link to…` all use it, and none preloads the target table. `GET /api/search` takes `tables=` (Issue #703). - `weave_ontology` reads the schema in one short call, and the MCP primer's first line sends an agent there before anything else. `{depth: "outline"}` (the default) gives one line per table, grouped by space: short field types, option and state names, relations as `name -> Target (inverse)` with `*` for many, and lookups, rollups and formulas as `name = short form`. On the weave docs workspace it is 5,250 characters, against 103,226 for `weave_schema`. `{concept: "Issue"}` reads one table in full with every field's description, and `{since: ""}` answers only the spaces whose schema changed. Each space now carries a schema version counted from the structural audit log, so a row write moves nothing; the outline ends with the workspace etag. The same reads are `GET /api/ontology` and `weave ontology` (Feature #277). - A signed-in browser stays signed in while it is used: each time the server extends a session (at most once a minute), the response reissues the `wv_session` cookie with the same token and a fresh 30-day lifetime. Before, the cookie was written once at sign-in and the browser dropped it on day 30 however active the person was (Issue #710). - A space is now defined as the unit of meaning in a workspace: the tables of one area, addressed as `Space/Table`, under one schema version. The ONTOLOGY export, the glossary in `docs/ONTOLOGY.md` and a new **The space** section in the Handbook's *Making a workspace your own* guide carry the same definition (Feature #278). - On a phone a checklist box in a document is a 44px tap target with an 18px box drawn inside it, so a thumb that lands 10px off still ticks the task. The text beside it still takes the caret, and the desktop box is unchanged (Issue #705). - On a phone the formatting bar stays on top of the keyboard when Safari pans the page under it. The bar now re-places itself on the visual viewport's scroll as well as its resize (Issue #706). - The iPhone undo and redo gestures (shake, three-finger swipe, the Undo key on an iPad keyboard bar) step through the document editor's own history. Before, they reached the browser's undo, which had nothing to take back (Issue #707). - Each workspace keeps its own browser session, in a cookie named `wv_session_`. Signing out of one workspace ends only that workspace's session, idle expiry of one leaves the others signed in, and **Sign out of every workspace** on `/auth` (`POST /api/auth/logout?everywhere=1`) ends them all. Removing an account ends its sessions in that workspace at once, and the next visit there answers a page saying access was removed instead of a sign-in redirect. A plain `wv_session` cookie from before this change is still read for one release and moves to the new name on first use; a session on the hub root still opens member workspaces until hub root removal lands (Feature #280). ## v0.4.75 (2026-10-07) - A click on a row's `#id` link opens it once: the grid's link carried its own opener alongside the one `#/entity/` click handler, so the dock drew twice and the document editor mounted and was destroyed inside a frame (Issue #633). - The flicker probe treats a scroll as the reader moving content, so a grid row the scroll itself recycled no longer reports as a flash; a flash that lands well after the scroll still does ([[Issue#632]]). - The flicker probe no longer counts a layout shift that the journey's own scroll caused: a real wheel scroll over the same grid reports none, and Chrome's own input exclusion never fires for a programmatic scroll ([[Issue#631]]). - Navigation holds the page you are on until the next one is ready: the route skeleton is now earned at 150 ms, the ceiling the flicker probe calls a flash, so a load that resolves faster paints no loading state at all ([[Issue#630]]). - A line break in a mermaid node or edge label now draws as a line break on the whiteboard. `A[run in a frame\n+ toggle for source]` drew in the page as two lines and on the whiteboard as one label with a literal backslash and n between the halves; `
`, `
` and `
` printed as tags. The whiteboard reads the same four forms mermaid does and wraps the label on them (Issue #471). ## v0.4.74 (2026-10-07) - A document edit by a second person is now its own activity entry, with their name, their delta and their preview. Edits within ten minutes of each other folded into one entry whatever the actor, so a second author's work was credited to the first and the entry's delta described neither edit. The revision log already split by author (Issue #680). - An automation that appends to a document now logs a `doc-appended` entry naming the document, with the length it wrote and a preview, the way a person's append does. The text landed but the activity feed never said so, so the rule's append was invisible in history and the record page could not pulse the document the rule wrote (Issue #686). - On a phone the bug button is now a 44px tap target with the same 26px face, set 3px from the screen edge like the phone applet's, and a row's page leaves 72px at its foot so the last line of a document scrolls clear of it. Desktop keeps the 26px corner button 12px in (Issue #695). - A code block's copy button now sits in its own gutter at the right of the box, so a long line scrolls under the box edge instead of running beneath the button (Issue #694). - A table that names no row term of its own now speaks `row` / `rows`, not `record` / `records`, so a fresh table's grid reads "Search rows", "+ New row" and "N rows" in the rail, matching the onboarding welcome and the first-run empty state (Issue #618). - In a document, Cmd+Shift+Z (Ctrl+Shift+Z elsewhere) now redoes what Cmd+Z undid. Vditor only knew Cmd+Y for redo, so the macOS shortcut did nothing (Issue #697). - On a phone the formatting bar for selected text is now one row of 44px buttons along the bottom of the screen, riding above the on-screen keyboard and scrolling sideways when it runs long, instead of a two-row bubble of 25x35px buttons over the line above the selection. Desktop keeps the floating bubble (Issue #693). - On a phone, where the row covers the screen, opening a row is now a history step: Back or swipe-back returns to the table, Forward reopens the row, a relation hop inside the row is its own step, and the row's Close button steps back instead of leaving a duplicate table entry. On wider screens the row docked beside the table still adds no history, as ruled in Issues #198, #226 and #276 (Issue #267). - The record page lists files attached to the row itself in a **Files** card, with the same link and `(missing)` treatment an attachments field gives, so a row in a table with no attachments field still reaches its files (Issue #463). - Hiding or showing the last column of a table no longer changes the width of the column beside it. Tabler's `card-table` gives the last cell in a row a 20px right inset where every other cell gets 4px, so the column that landed last measured a box 16px wider than its own and grew by that much. The grid now pads its last cell like the rest (Issue #690). - On a phone the slash menu's rows are now 44px tall with 15px labels, so a thumb hits the command it aims at. Desktop keeps the compact 28px rows (Issue #696). - A line command run on an empty line now leaves its placeholder selected, so the first thing you type replaces it. `/task` then "Ship it" stores `- [ ] Ship it`; it used to leave the caret after the placeholder and store `- [ ] To doShip it` (Issue #556). - Picking Table from the slash menu now puts the caret in the table's first header cell with "Column" selected, so typing names the column. It used to leave the caret on the line above, and the typing landed in a paragraph over an untouched table (Issue #692). - A workflow state can take a colour of its own. The swatch in the field dialog opens the same ten-hue picker a select option uses, the category colour stays the default, and **Reset to category colour** puts a state back on it. Chips in the grid, the tray preview, view chips, the command palette and the phone applet all read the state's colour (Issue #427). ## v0.4.73 (2026-10-06) - A data directory the process cannot write now fails with a message naming the directory, the uid and the fix, instead of reporting that the database file is not a SQLite database. The Railway deploy guide quotes the message the log really shows. (Issue #474) - Opening a document that holds math no longer raises an unhandled rejection in WebKit when a math node leaves the page while KaTeX is still loading; the node is skipped and every other formula still typesets. (Issue #470) - The preview chip in the field settings tray renames as you type, for a select option and for a workflow state, instead of holding the old name until something else redrew the list. (Issue #553) - Editing a document no longer rewrites its task items: the box reaches storage as `[ ]` or `[x]` with one space after it, so a string edit by an agent still matches and an unrelated edit leaves every task line unchanged in the revision history. (Issue #555) ## v0.4.72 (2026-10-06) - The colour swatch beside each option in the field settings tray now shows that option's colour, and a slate option is hatched the way the colour picker draws it. (Issue #552) - The rail's help button no longer shows a native tooltip on hover; it keeps its accessible name and both the click and the `?` key still open the key sheet. (Issue #431) ## v0.4.71 (2026-10-06) - **Hiding a field leaves the grid on the click** (Issue #328): every eye flip waited for its `PATCH`, then a `GET /api/schema`, then a full table query before the switch moved or the column went anywhere. On a loaded host, where each of those calls took seconds, the gesture read as broken. A hide now repaints the rows the grid already holds, one column lighter, before the write goes out: zero reads, and the switch moves on the click in both popovers. The write still queues behind `eyeWrites` in the order the flips were made (Issue #243), and the grid is read back only when the table disagrees with what was painted, which is how a hidden frozen column or a field added elsewhere still settles. A show reads once, as before: it needs the column's values. Measured on a copy of the `uno` workspace, `Agent/Sessions` at 1,791 rows: click to the column leaving the grid drops from 68-153 ms to 19-31 ms, with the PATCH, the schema read and the table query all moved off that path, and a show stays where it was at 61-80 ms. Gates: `test/fields-popover-browser.test.mjs`, `test/eye-live-rows-browser.test.mjs`, `test/ui-contract.test.mjs`. - **A schema read counts rows in one pass** (Issue #560): `describeSchema` put each table's `entityCount` together by calling `listEntities(db.id)`, which walks the whole entity map and filters on `dbId`, so a page load cost tables times rows: about 50 tables by 6,078 rows on the weave workspace. `GET /api/tables/:id` and the search's table hits paid the same. A benchmark of v0.4.52 inside a 1 vCPU container measured a schema read at 45 to 67 ms at 8.4k entities, against under 5 ms at 4,389. The engine now walks `state.entities` once per schema read, buckets live rows by `dbId` into a Map (`#liveRowCounts`), and reads each table's count from it; `universalSearch` reads the same Map for its table hits, and builds it only when a table name matched. At 50 tables by 6,000 rows `describeSchema` drops from 30.4 ms to 0.89 ms a call. The count is still derived from the rows on every read rather than held as a maintained counter, so it cannot drift when a row is trashed, restored, purged or moved between tables. Gate: `test/schema-count-scan.test.mjs`. ## v0.4.70 (2026-10-06) - **One Activity panel for the whole record, with Restore on the revision** (Issue #429): the document history was a cramped list inside the section ("3 REVISIONS Select one to view it · Esc closes", the hint clipped at the right edge) whose rows offered no revert; Restore sat behind a second click, on a preview with no diff. Now the clock-arrow in a record's header opens a panel beside it with every document's revisions, field and state changes, and comments, newest first under Today, Yesterday and a date, and a document's own clock-arrow opens the same panel filtered to that document. Rows are nav rows: what changed on one line, who and when on the next, and the row's action (Restore, Revert, Delete) one click away on hover. A selected row carries one button and, for a revision, a picker to compare it with any other version of that document; the change shows read-only where it lives, a document's diff in place of its editor (blocks, then sentences, then words, unchanged runs folded) and a field change as a card at the top. Every action toasts with Undo in the corner lane, and Undo steps back that action only, refusing if something else was written since. A restore's row reads "Description restored from
--activity `; the PATCH answer carries the entry id as `activity`. - **An export imported back into its own workspace keeps the tokens, share links and sessions** (fixes Issue #230): since Feature #222 phase 0 the export strips account token hashes, view share tokens, sessions and invites, and import needs an admin token. A reader export was rechecked on 2026-09-28 across every GET route, the table query and the MCP export, workspace, accounts and schema tools, and none of them carries a token hash, a share token or a stored credential value. The round trip was still broken. An admin who exported a workspace and imported the dump back into the same workspace wiped every stored secret, because import replaced the whole state: the next call from that admin came back 401, every account token and share link stopped working, and every browser was signed out. Import now keeps what the dump leaves out. It matches accounts and views by id and restores each token hash and share token the dump omits, and it keeps sessions and invites for the accounts the dump still names. A secret the dump does carry wins over the stored one, an account or view the dump drops takes its secrets with it, and a dump imported into another workspace matches no ids and keeps nothing. Gate: `test/auth-wall.test.mjs` (1 new case: the export carries no `wv_`, `wvv_` or `wvs_` token, `tokenHash` or `shareToken`; after an admin import every token, the share link, a session and an invite still work, twice over; a dropped account and a dropped view lose theirs; a dump's own hash replaces the stored one). - **weave tells you when a newer release is out** (Issue #253): `weave serve` asks GitHub for the latest published release of grunion-ai/weave at most once a day, with one unauthenticated request and no token, and keeps the answer in `update-check.json` beside the workspace so a restart inside the day asks nothing. `/api/health` carries `latestRelease`, `releaseCheckedAt` and `releaseBehind` beside `latestSha` and `behind`, which keep their meaning. A newer release turns the sidebar instance chip amber with `v available`, and a toast says so once per release. It works on a plain clone and on a source zip, which the commit comparison never reached. Offline, rate-limited or given a malformed answer, the check stays silent and reports nothing as newer. `WEAVE_UPDATE_CHECK=off` makes no request at all; the Handbook's Environment reference, the `Dockerfile` and `compose.yaml` list it. Kyle decided on 2026-09-28 that the running app makes this call. - **DEVELOPMENT.md names the checkout the release tagger runs from** (Issue #253): the Releasing section sent readers to `~/Documents/harness.nosync` to re-run `weave-release-tags.mjs`. The watcher has run from `~/.harness-serve` since the deploy split, so the section now points there and says the tagger checks GitHub's tags and Releases. `test/development-sync.test.mjs` holds it to that. - **A rollup's settings tray says what it computes** (Issue #387): Kyle, reading Showcase > Field Types > Peer skill, "I don't understand the config for this rollup or formula driven rating; config tray looks wrong." The tray held the name, the description and "Computed config is not editable, delete and recreate to repoint it". It now shows the recipe as three read-only rows, Relation (`Peers → Showcase/People`), Field (`Skill · rating`) and Aggregate (`avg`), in the slots the pickers take when a rollup is created, a Result line with the value on the first row that has one, drawn as the cell draws it (ported from the unlanded change 443), and a Shows as line naming where the look comes from: "Drawn as the rating on People › Skill: 5 stars.", a bar, ring or heat from a number field, a count in plain figures, or the values of the field it reads. A link opens that field's settings, where the look is changed. A lookup gets Relation, Field and Shows as; a space rollup shows Table, Field and Aggregate. The dead-end sentence is now "The recipe is set when the rollup is created and cannot be changed here." Repointing a rollup in place is deferred to Feature #241 (Kyle, 2026-09-28). Gate: `test/rollup-tray-browser.test.mjs` (new, 5 cases: the rollup's rows, result, display and link in light and dark; the lookup's rows and display in light and dark; a count, a bar rollup and a plain number rollup, with a save through the tray). - **A slash command takes the line it is typed on** (Issue #455): Text, Heading 1 to 6, Bulleted list, Numbered list, Task list and Quote used to insert a placeholder block and leave the line's words behind as body text, so `## Plan /task` ended as the heading plus a `- [ ] To do` under it. They now convert the line: the old block marker comes off, the new one goes on, and the words, their inline formatting and a nested item's indent stay. `Buy milk /task` gives `- [ ] Buy milk`, and `/text` on a heading gives a plain paragraph. The caret goes to the end of the converted line, the conversion saves through the usual debounce, and undo returns to what was typed. An empty line still gets the prefix and its placeholder. Code block, Mermaid, Table, Divider, Line break, Image, Raw HTML, the references and the format commands still insert. ## v0.4.51 (2026-09-27) - **A hairline grid header, and a "+" that trails the last field** (Feature #240, Kyle 2026-09-27): the header was a grey band with rounded top corners, and the add-field "+" sat in the one column with no width of its own, which took whatever the fields left of the card, so on a small table the grey ran past the last field to the card's edge around a lonely "+". The header now paints the card surface (opaque, so rows never show through it while it is stuck) over one hairline a step stronger than the row rule, and its labels are sentence case, 12px, weight 500, in the secondary ink. A hovered header cell takes the secondary surface, the one sign left that it resizes, reorders and opens a menu. The grid is as wide as its columns, so the "+" sits right after the last field and the slack past it is bare card; no field is stretched to fill it (Feature #233, rule 2). On a grid wider than its card the "+" cell is `position: sticky; right: 0` on the surface, with a 16px fade on its left and a chip border, both only while the wrap overflows (`.wv-overflow-x`, set beside `.wv-fit`). A stuck header casts a faint shadow on the rows under it and none at rest: the grid's one capture-phase scroll listener marks the wrap `.wv-head-stuck` once a frame while the header sits below its table's top, for the page and for a scrolling wrap alike; with the Σ row shown the shadow falls from it. The "+ New" bar drops its grey hover fill. Tests: `test/grid-hairline-header-browser.test.mjs` (new, 14 cases in light and dark: surface and square corners, label type, the stronger rule, the trailing "+" and bare slack, the sticky "+" at both scroll ends with its fade, the stuck shadow on page and wrap scroll, the lone hover tint and the fill-free "+ New"), `test/grid-layout-browser.test.mjs` and `test/ui-contract.test.mjs` (the "+" no longer takes the slack), `test/eye-live-rows-browser.test.mjs` (the ⋮ case scrolls Stage clear of the sticky "+" before it clicks). - **One checkbox, and a Fields popover that drags and fits** (fixes Issues #441, #384, #445, #446): the grid's row selector, the Fields popover, the Filters popover and the field dialog all draw weave's one checkbox, `input.form-check-input`, so each computes the same 16px box, 4px radius, border and checked fill in both themes; the hand-drawn `.field-visible-check` and its `role="switch"` are gone, and each Fields row, Deleted rows, the Σ rollup row and each filter option is a real, labelled checkbox that Space flips. A field dialog checkbox sits on its label's first line everywhere, not over it, and the Literal hint sets its syntax as code. A Fields row reads `grab` at rest over its name and its grip, never an I-beam; a press that moves drags it (the Views list shares the same drag), the cursor reads `grabbing` wherever the pointer goes until the drop, Escape or a cancelled pointer, no text selects, and one square-ended line marks the gap. The Fields popover grows to the room below its button before its list scrolls, its header stays outside the scroll box, and a scrolled list snaps so no row sits half under the top. Tests: `test/checkbox-parity-browser.test.mjs` and `test/fields-popover-browser.test.mjs` (new), `test/table-toolbar-browser.test.mjs`, `test/eye-live-rows-browser.test.mjs`, `test/popover-toggle-browser.test.mjs`, `test/footer-rollups-browser.test.mjs`, `test/table-patch-browser.test.mjs`, `test/ui-contract.test.mjs`. - **Select filters and an "X of N" count in the Filters popover** (fixes Issues #319, #448): the Filters popover offers single-select and multi-select fields beside workflow and toggle fields, each a group of weave's checkboxes with the option's dot, labelled **Single select** or **Multi select**. Options in one field combine with OR, fields with AND, and a multi-select row matches when it has any chosen option; the choice saves into the current view like any filter, and a new row made on a select-filtered grid starts inside the filter (Issue #341). A view's `filters` now take option names for those fields on every surface (engine, `weave_table_view`, CLI, REST, the Workspace/Views row's Filter text), validated like states. The footer reads "X of N" in the table's plural row term (`7 of 413 bugs`, plural even at 1): X counts the rows left after the filters and the search, N every undeleted row. It is one node, updated in place off the grid's own read and announced with `aria-live="polite"`, dimmed while a change is on its way, with Clear all on its right; the groups scroll under it and the popover grows to the room below its button. The query takes `countAll: true` (`weave_query`, `weave query --count-all`, `POST /api/tables/:t/query`) and answers `all`, the undeleted rows before any where or search. The header's crumb path now ellipsizes before the toolbar drops a row, so a filter badge never moves the grid. Tests: `test/select-filters.test.mjs` and `test/filter-select-browser.test.mjs` (new; both themes, Chromium and WebKit). - **One system default for Reset view and New view; deleted rows and the Σ row saved per view** (fixes Issue #442): `systemDefault(db)` is the one answer to "a fresh view": every regular field in schema order with the table's default system columns, no filter, sort, search, widths or frozen columns, no deleted rows, no Σ rollup row, Comfortable. Reset view writes it into the view on screen and clears the search; + Add view creates the new view from it, so the two cannot drift. Deleted rows and the Σ row are now the view's, beside filters, fields, sort and density: two view keys, `deleted` (true shows the trashed rows in place) and `rollups` (true draws the Σ row, false hides it, null follows the table's older `hideRollups`, so no grid changes on the upgrade), on `weave_table_view`, `weave table view --deleted on|off --rollups on|off|table`, `/api/tables/:t/views/:v`, the schema document and the Workspace/Views row's new **Show Deleted** and **Rollup Row** columns. The Fields popover's Rows boxes save into the current view, and a switch to another view shows that view's own. Tests: `test/view-rows-state.test.mjs` and `test/view-reset-browser.test.mjs` (new, both themes, Chromium and WebKit), `test/footer-rollups-browser.test.mjs`, `test/sort-values-browser.test.mjs`, `test/ui-contract.test.mjs`. - **The Views list: handles, row buttons, inline naming, and Standard** (fixes Issue #443): the View dropdown has no right-click, context menu or double-click rename any more. Every view row shows its drag handle; a drag by it, with a mouse or a finger, or Alt+Up / Alt+Down on a focused row reorders the list, with the square-ended insertion line the Fields popover uses. Rename, Duplicate and a hold-to-delete are row buttons that show on hover and on keyboard focus, and Rename edits the name in place. **+ Add view** no longer opens a modal: it appends a row inside the open dropdown with a focused, selected name, the next free `View 2`, `View 3`, …; Enter or a click away creates the view and opens it with the dropdown still open, and Escape drops the row. Duplicate uses the same row under its source, offered as ` 2`. The first view's row reads **Opens first**, and the caption moves with whichever view is dragged to the top. The first view is named **Standard**: a new table's is born with the name, and a view still called `Default` is renamed once when its workspace opens (its Workspace/Views row too), unless the table already has a Standard; a view named Default afterwards keeps its name. The View button, the Handbook, AGENTS.md and the `weave_table_view` description say Standard. Tests: `test/view-standard-name.test.mjs` (new), `test/table-views-browser.test.mjs` (rewritten for the list), and the view suites that addressed the first view by name. ## v0.4.50 (2026-09-27) - **An architecture map that fails the build when it drifts** (Feature #238): `docs/architecture/weave.architecture.html` is an interactive map of the runtime (both host adapters, the request core, the engine, both stores, the renderers, sign-in, backup, the CLI and MCP surfaces) rendered by [archify](https://github.com/tt-a1i/archify) from `weave.architecture.json`. Each of its twenty source citations links to the pinned commit on GitHub. `node scripts/architecture.mjs` re-pins and re-renders it, and each release re-pins it with the version bump. Tests: `test/architecture-map.test.mjs` (new: cited files exist, every `src/` and `bin/` module is on the map, the HTML matches the JSON and its revision, the pin is in the branch history). - **An option or state with no icon wears the blank slot a table and a space wear** (Issue #419): Kyle, 2026-09-26, "update blank icons for options to be the same as table and space icons". In the field dialog every select option row and workflow state row with no icon drew a grey bordered box holding an em dash, and an option given an icon printed its stored name (`lucide:star`) as text in that box. Both rows now use `iconButton()`, the control the table and space headers use: a ghost ring on a borderless button when unset, the drawn icon when set, the same picker behind it. The `.opt-icon` style is gone. Tests: `test/blank-icon-slot-browser.test.mjs` (new: option and state slots match the table slot's element, glyph and box in both themes; a picked icon draws rather than printing its name); `test/chip-system.test.mjs` now looks for `iconButton(` in the option editor. - **A new row's choice is empty unless its field names a default, and the Default is a picker of the field's own options** (Issues #421, #422): Kyle, 2026-09-26, "select fields that don't have a blank or default value should not create a value: null (empty) unless otherwise specified" and "Default config should be a drop down selection of available options". The engine marked the first state `default` on every workflow field that marked none (in `addField`, in the normaliser every states edit runs through, and on the seeded four), and a type conversion filled an empty cell with that state, so every new row carried a status. Now a state is the default only when the author marks it (at most one), an empty cell converts to empty, and a state can be set back to empty from the API, the CLI or the cell's picker (a blank row, as a select has). In the field dialog, select and multi-select lose the free-text Default box and workflow gains a Default: a picker over the field's own options or states with *No default* first, which follows a rename as it is typed and clears when its option is removed. The engine reads the default after the options in the same save, so a default that names a just-renamed option is accepted, and drops a select default whose option was removed. Existing workspaces: every workflow field made before this change stored `default: true` on its first state, so it keeps starting rows there; nothing is rewritten, and picking *No default* in the dialog clears it. Tests: `test/choice-defaults.test.mjs` (new, engine and REST), `test/choice-default-dialog.test.mjs` (new, dialog core), `test/choice-default-browser.test.mjs` (new, tray and grid); `engine`, `workflow-defaults`, `workflow-defaults-browser`, `table-move-duplicate`, `field-dialog-core`, `ui-contract` and `picker-browser` updated where they pinned the implied default or a state picker with no clear row. - **An empty grid cell at rest is blank** (Issue #420): Kyle, 2026-09-26, "make empty cells blank to cut noise". Every empty cell drew a hint, so a table with many empty cells read as a wall of repeated text: "Add description…" in a dashed pill, the date box's "today, 15 sep, 9/15/26…" placeholder (clipped mid-glyph), a grey dash chip for an unset select, state, lookup or rollup, a dashed "+" for an empty multi-select, file or document chip. Each of those now carries `is-empty` (or is already `.k-add` / `.k-empty`), and one rule in style.css paints them at opacity 0, and every placeholder transparent, in a grid cell that holds neither the focus nor a focused control. Focus the cell and the hint is back; the invisible chip is still the click target that opens its picker. This supersedes the 2026-08-24 chip ruling "empty is dashed, never dimmed" for grid cells at rest. The entity page, footers, stat tiles and activity logs keep their marks: a dash there means no value was computed. Tests: `test/empty-cells-browser.test.mjs` (new: a row with every field empty shows no text, placeholder or dashed outline at rest in both themes; the focused cell shows its hint and the cell it left goes blank; a click on an empty select cell opens the picker); `test/grid-chrome.test.mjs` now pins the empty description as blank at rest and dashed on focus. - **The icon-motion browser case no longer loses a 200 ms run to a loaded gate** (Issue #401): the first moving icon in the nav is a disclosure chevron whose run lasts 200 ms, and the case polled from the test side for the play classes after `hover()`. At load 80-135 the first poll landed after the run had ended, so "a hover plays the icon" failed on an unchanged main and the retry did not rescue it. A `MutationObserver` in the page now records each run as it starts, and the case reads that record: the hover plays exactly the hovered icon exactly once, the press plays the second icon, and the fixed wait before the press became a wait for the hover run to end. No product change. Tests: `test/icon-vocabulary-browser.test.mjs` (the motion case stalls on purpose for one run length after the hover, which fails the old polling read and passes the record). - **The Name column opens at 260px** (Issues #261, #414): Kyle's decision of 2026-09-26. Feature #233 opened Name at the mockup's 220px, and a thirty-character name such as "Quickstart for a new workspace" was cut off there. The dropped commit 41398c3 measured 240px leaving that name one pixel of room in Chromium's fallback face and chose 260px. The number moved inside Feature #233's width model (`NAME_WIDTH` in `public/column-resize.js`) rather than as the CSS `min-width` 41398c3 used, so the rest of that model holds: a stored view width or a resize still wins, every other type keeps its default, and the "+" column still takes the slack. The Handbook's width guide says 260. Three browser fixtures sat within 40px of their card and moved with it: `test/scroll-browser.test.mjs` opens its docked grid at 1900px instead of 1800 and the Σ-row case in `test/footer-rollups-browser.test.mjs` opens at 1480 instead of 1400, so each grid still fits its card and the page scrolls; the select fill in `test/grid-range-browser.test.mjs` scrolls Kind into the wrap's view before it grabs the fill handle. Gates: `test/column-layout.test.mjs` (Name and a computed Name default to 260), `test/grid-layout-browser.test.mjs` (Name opens at 260 beside the other types' defaults; in light and dark, a thirty-character name reads whole at the default, the "+" column fills the card, and a stored 180px wins). - **The README stops selling the board and list views the app removed** (Issue #385): the Screenshots grid drops the board shot (and `docs/screenshots/board.png`) for a three-up of documents, relation map and search; the Views row names the table, entity pages (docked or full page) and the relation map; `docs/README.md`, `llms.txt` and the Airtable comparison no longer list a board or list view. `scripts/screenshots.mjs` no longer captures a board. Tests: `test/readme-screenshots.test.mjs` requires every README screenshot to exist, every file in `docs/screenshots/` to be shown, the capture script to match the README, and no screenshot, Views row, screenshot index or Airtable mapping to name a board or list view. - **Row density: Compact, Comfortable and Spacious, held to a declared height and saved with the view** (Feature #239, fixes Issue #440): every row is exactly 32, 44 or 72px, whatever its cells hold. The height is the `--wv-row-h` token and never a measurement: each value sits in one clip box (`--wv-cell-h`) from the grid's cell builder, the row rule is an inset shadow, and the row window reads the token instead of the measured cache from Issue #324. Only the height moves; text size and column widths are the same at all three. Spacious is new and shows two lines of a text value that carries marks (a plain text box stays one line), two lines of a description and a compact card; Compact and Comfortable show the card as a one-line chip. A multi-select or relation cell shows the whole chips that fit (two rows at Spacious) and a +N for the rest, and its hover pop shows them all. Density is a view setting beside filter, sort and fields: a pick autosaves into the live view, a new view starts at Comfortable, `weave_table_view`, `weave table view --density`, `/api/tables/:t/views/:v`, the schema document and the Workspace/Views row's Density column all read and write it, and the old per-browser `weave-grid-density:
` key is read once into a view that has none and removed. In development a painted row that misses its token logs the field type that grew it. Tests: `test/view-density.test.mjs` (new), `test/grid-density-browser.test.mjs` (row pitch in both themes, only height moves, whole chips, saved per view, the key handover, a Spacious flip at row 400 of 600), `test/grid-chrome.test.mjs` (caps are now exact), `test/ledger-grid.test.mjs`. - **A table change redraws the grid, not the page** (Issue #444): search, filters, a view switch, Reset view, field show/hide and order, and Add field used to rebuild the breadcrumb, title, description and toolbar with the rows. The description went blank until `/markdown` answered, so the grid jumped up and back, and a view switch painted the route skeleton. The chrome is now drawn once per table and the grid body is swapped under it in one task. The toolbar, an open popover and the search box keep their nodes, focus and caret. Measured with `/markdown` held 120 ms: 7 to 16 blank frames and 82 header nodes replaced per action before, 0 and 0 after. - **The grid shows that it is loading** (Issue #433): a filter chip, a keystroke in the search, a field flip or Reset view dims the grid body and sets `aria-busy` at once, never the toolbar. Past 500 ms the weave rope sits over the grid and finishes its cycle. A chip clicked while a slow re-read runs is kept. Tests: `test/table-patch-browser.test.mjs` (new). - Table toolbar: visible table search, current-view and density dropdowns, a Fields popover with visibility and drag or keyboard ordering, and a workflow/toggle Filters popover. View controls include Add view, Reset view, and Clear filters, search, and sorting; changes save automatically. Add field opens the existing field tray, and the rightmost table-actions menu stays unchanged (Feature #237). ## v0.4.45 (2026-09-26) - **Automation fire order is stored** (Issue #285): the rules on one trigger fired in whatever order the store read them back. An automation carried no ordinal, the store read the table with a bare `SELECT id, json FROM automations`, and the engine fired `Object.values(state.automations)` in that order, which was rowid order: an accident of insertion that a VACUUM, a hand-edited `.db` or a JSON import could change, and that no surface showed. (A bare `SELECT id` on the same table walks the primary-key index and comes back in uuid order, a third order.) Every automation now carries `seq`, a workspace-wide monotonic counter on `meta.automationSeq` minted at create and never reused after a delete, the same shape as activity's `seq` (Issue #282). It lives in the rule's json, so the SQL schema is unchanged and an existing `.db` opens with no DDL: the engine's open pass numbers every unnumbered rule in rowid order, the order it fired in until now, so no workspace changes behaviour on upgrade, and never moves a number already handed out. The store and the Cloudflare store load automations `ORDER BY` seq (unnumbered last, by rowid); the engine fires and lists by seq, so an import that keeps a dump's key order or a rule a pre-seq CLI appends still fires in order (the straggler last, numbered on the next open). `seq` rides `GET`/`POST /api/automations`, `weave_automations` over MCP, `weave automation list|describe`, and `weave export`/`import`, which keep it and the counter as written; the relation map's pills stack in that order. Gates: `test/automation-seq.test.mjs` (a workspace-wide seq that never repeats; fire order by seq on a `.db` whose rowids return the rules reversed, which fired CBA before this and ABC now; a pre-seq `.db` numbered in rowid order and written through, with no renumber on a second open; the pre-seq straggler; export/import of a dump with its automations listed backwards; route, MCP and CLI parity) and `test/automation-seq-browser.test.mjs` (the relation map stacks a table's rules in fire order). - **Overlapping eye flips both reach the table** (Issue #243): flip two switches in the eye while the first flip's PATCH is still out, and both switches move but the table keeps only one of them. Reproduced on `:4400` from `["Chip","Card"]`: two clicks in one tick on Status and Suite left `["Chip","Card","Suite"]`, with Status lost. Every switch PATCHes a whole setting (the hidden set, the system set, the Σ flag). Issue #240 made each handler read the table at click time, but two clicks that both land before either PATCH returns still read the same pre-flip set, and the later write drops the earlier flip. Idle, a round trip is ~20 ms, so this needs load: Issue #216 measured the same tail at 8.7 s. The fix splits a flip into a write and a paint. Writes (PATCH, then the schema reload) go through one queue in click order, and each reads the table only when its turn comes, so the second flip builds on the first. The paint (grid redraw, dock, the popover's rows) stays off the queue, so a PATCH never waits on a render, and only the last flip of a burst paints. One queue covers the hidden fields, the system columns and the Σ row switch. Client only: the engine and its PATCH verb are unchanged. A paint can now land well after its click, so it checks what is on screen first: "last" is judged per table, so a flip in the dock's eye for another table no longer cancels the grid's repaint; only an eye popover on the same table learns the new rows, and it learns its own row set, so a column's ⋮ menu opened since keeps its rows; and the eye's page is redrawn only while it is still the page on screen, so a reader who moved to another table does not get the old one drawn over it. With the first PATCH held open, the second flip went out beside it before the fix (2 PATCHes on the wire) and waits behind it after (1), and the pair now draws the grid once. Gates: `test/eye-live-rows-browser.test.mjs` "two flips that overlap both reach the table", "a late repaint leaves a column's ⋮ menu alone", "a flip on one table still repaints when another table's eye flips behind it" and "a late repaint does not draw its table over the page the reader moved to"; `test/ui-contract.test.mjs` "the eyeball: hidden fields, system columns and deleted rows from one popover; Manage fields is gone" pins the queue and the paint outside it. - **A second click on the eye closes its popover** (Issue #320): Kyle, 2026-09-18, "visibility toggle dialog doesn't close on icon click". `showPopover()` hangs its outside-click listener in the capture phase and counted the trigger itself as outside, so a second click on the eye closed the popover and that same click's own handler opened a fresh one: the dialog never closed. The field menu's ⋮, a Σ row cell and a colour swatch wore the same loop. A click on the trigger now closes its popover and stops there, so every trigger toggles. The eye passes an `owns` option, because a flip redraws the surface and the eye that opened the popover is a detached node by the next click: any eye in the same region (`#main` or `#dock`) counts as its own, while the other surface's eye still opens its own popover in one click. A popover already closed by Escape or a pick only unhooks its listener and lets the click through, so the eye opens on the first click after an Escape. Gates: `test/popover-toggle-browser.test.mjs` (second click closes and stays closed, third opens; Escape and an outside click still close and the eye reopens after either; the eye redrawn by a flip still closes; each in light and dark; the docked entity's eye switches in one click and closes its own; a column's ⋮ toggles and another column's ⋮ switches in one click). - **Headers select no text** (Issue #417). Dragging a header, resizing a column or double-clicking its edge no longer leaves header labels highlighted, in Safari as well as Chrome, and a gesture clears any selection you had. Cell values stay selectable. - **A keyboard shortcut sheet on `?` and the rail's `?` chip** (Issue #268, and Kyle's report "command enter the sent work but there is not clue that this is possible in the ui"): `public/grid-keymap.js` carries a full spreadsheet keymap and nothing in the UI listed it; the only printed hint was the ⌘K footer, and the problem report's ⌘Return send had no mark at all. The keymap now exports `WeaveGridKeymap.bindings`, one row per key with the words the sheet prints and the presses that reach it, and answers `?` on a resting cell with a new `help` verb (inside an open cell `?` is still typed). `openKeySheet()` renders those rows, plus the handful of keys app.js owns (⌘K, ⌘⇧E, `/` or ⌘F, ⌘C/⌘V, ⌘Return in the report), through the existing `modal()`: `modal()` gained a show-only form (`onSubmit` null: one Done button, focus on it) and now returns focus to the element that opened it when the dialog still holds focus at close. `?` anywhere outside an input, a contenteditable or the document editor opens the same sheet, as does a new `#rail-help` chip above the theme toggle. The report panel prints `⌘↵` beside Send, and Send's tooltip names the key. The Handbook's keyboard guide gains the `?` row and a paragraph on the sheet; the bug report guide names ⌘Return. Gates: `test/grid-keymap.test.mjs` (at rest, ? opens the key sheet; the key sheet lists every key the keymap answers, in both modes; every row on the key sheet is a key the keymap still answers) and `test/key-sheet-browser.test.mjs` (? on a resting cell opens the sheet with every grid verb and Esc puts focus back; ? in an open cell is typed; the rail chip opens it and gets focus back; ? with nothing focused opens it and ? in the report note is typed; the report shows ⌘↵ beside Send), each browser case in light and dark. - **The number Sample draws the column's own figures** (Issue #388): Kyle, reading Showcase > Field Types > Progress, "I don't understand the config for this rollup or formula driven rating; config tray looks wrong." The column holds 72, 100 and 15; its settings tray drew three bars labelled 0.25, 0.6 and 1, fixed placeholders that matched no row and no format, so a scale of "column max" could not be judged from the tray at all. The Sample now draws this column's own smallest, middle and largest figures, dressed in the costume the form holds and measured against the scale it holds: 15, 72 and 100 against a column max of 100, and the same three figures against a fixed 200 when the scale moves. Equal figures collapse, so a one-row column draws one bar rather than three of the same. A column holding no numbers yet keeps the quarter, three fifths and whole of the scale, in the costume's own range (0..1 under percent, 0..100 otherwise), under the note "Example figures. This column holds no numbers yet." The figures come from `GET /api/tables/:ref/stats`, the same five-number summary the rollup row reads, fetched once per dialog and only when a graphic display asks for it; a field being created has no column and samples examples. The Sample stays empty until the column answers, so a figure the field never holds is never painted, not even for a frame. The number costume moved out of `src/engine.js` into `public/number-core.js`, the classic-script-plus-ESM pattern `cell-graphics.js` uses, so the tray dresses a figure through the very function the cell dresses its value with. Gates: `test/number-core.test.mjs` (new, 8 cases: the Issue's own column, a fixed scale, collapsing figures, float noise, the two example ranges, what 100% is, the costume, the parse gate), `test/number-format.test.mjs` (1 new case holding the engine and the tray to one costume across six configs and four values), `test/number-display-browser.test.mjs` (4 new cases in both themes: the column's figures and bar widths with no example note, the figures holding still as the scale goes fixed, and an empty column labelled as examples). - **The entity header and each document's section head hold while you scroll** (Issue #411): Kyle, 2026-09-26, "entity header should be frozen at the top on scroll, so should each document header when scrolling in that document". The full page already pinned its header (Issue #321); the dock, the pose a table row opens into, set its header back in the flow, so the title, breadcrumbs and actions left with the first screen. `#dock .dock-entity .view-header` is sticky now, and `publishViewHeaderHeight()` writes the dock header's height to `--wv-view-h` on `#dock` beside the page's own on the root. `.doc-section-head` is sticky at `--wv-view-h` on an opaque band, inside its own section, so it leaves when the document ends and the next head takes its place; the dash rail pins under the header instead of at a fixed 88px. Test: `test/sticky-entity-heads-browser.test.mjs` (page and dock: both heads keep their top edge across two scroll positions, a head leaves with its section, a folded section goes by, both themes). - **System columns move like fields** (Issue #418, Kyle: "system fields should be reorderable as well"). Created At, Modified At, Created By and Modified By, once shown from the eye, drag to any place with the same insertion line, freeze and unfreeze across the seam beside #, step with Alt+Shift+Left and Right, and resize from their edge. Each view keeps their order, width and visibility in its one field list, so agents move them with `weave_table_view` by name. Tables that showed system columns before keep them, at the end of every view. Only # stays locked first. - **Selection-bar trash carries Undo, and ⌘Z steps it back** (Issue #259): ticking rows and pressing the bar's trash moved them at once, the toast read "Moved to trash 1 bug" with nothing to press, focus fell to ``, and ⌘Z was bound to nothing although `POST /api/undo` worked; the 2026-09-12 audit trashed Issue #65 this way by accident. The engine keeps one undo entry per row, so a two-row trash leaves two, and the workspace's stack is shared with every other writer. The trash toast now carries **Undo** (the same `toast` action the row menu's delete uses), which steps back only the run of this gesture's own entries still on top of the stack and restores by id any row that run cannot reach: a registry row, whose trash is structure, or a row buried under a later write. Stepping back two blindly would have undone the later write and only one row. ⌘Z (Ctrl+Z) outside a text box, an open cell, a document editor and the popovers and dialogs the dock defers Escape to steps back the whole last gesture when it is still on top, else one entry, and redraws the grid in place through the same re-read a bulk write takes. The engine has no redo, so ⇧⌘Z stays unbound. After the trash the cursor lands on the row after the trashed ones (or the one before), and after Undo on the first row restored. The Handbook's selection-bar table and undo paragraph say so. Gates: `test/trash-undo-browser.test.mjs` (new, 5 cases: the toast's Undo brings back both rows and focus sits on the next row, in light and dark; Undo after a write from elsewhere restores both rows and leaves that write standing; one ⌘Z on a resting cell brings back the whole two-row trash; ⌘Z and Ctrl+Z inside an open cell editor send no request to `/api/undo`). - **A density flip on a page-scrolled grid lands on the reader's row** (Issue #413): on a page-scrolled grid of uneven rows, Comfortable to Compact put row 224 at the top of the view where row 223 had been (600 rows, 2187x1359, standing at 10,800px, both themes). The flip from Issue #342 put the row back under the header once, and the re-window after that correction read it as travel: the buffer turned to the other side of the window, and the spacer that replaced those rows did not weigh what they did, so the row moved again (Issue #317). `wrap.wvSetDensity` now re-anchors the direction of travel before each corrective re-window and reads the row again, up to three passes, until it holds. The header's edge also keeps the fraction of the row it cut before the flip. The offset math is ported from the dropped `wvKeepPlace` in e6c63dd. Gates: `test/grid-density-browser.test.mjs` (2 new cases: a page-scrolled grid of uneven rows keeps the row heading the view across Compact and back to Comfortable, in light and dark; both failed on the parent at 223 to 224). - **Toasts get a lane, and the version chip moves to the sidebar** (Issue #380): design pass finding F3 found fixed corner chrome covering content. The version chip sat on the grid's loaded count, the bug button sat on Save changes, and a toast landed on the open bug panel at 390px. Kyle approved the mockup on 2026-09-26. The chip sits under the stats line in the sidebar foot, and the foot's ground now reaches the sidebar's bottom edge. `toast()` keeps its signature for every caller and gains one behaviour: a lane centred at the bottom between the content panel and a 300px corner reserve at 1000px and wider, the top of the screen below that; an icon and a close button on every toast; text that wraps and never truncates; 4 s, 8 s with an action, 10 s for an error, or 60 ms a character when longer, capped at 20 s; a clock that pauses on hover, focus and a hidden tab; Esc; a 48px swipe on touch; three at most; a repeat count; polite and alert live regions. With a tray open the bug button rises above Save changes. Test: `test/toast-lane-browser.test.mjs` (1440x900 and 390x844, both themes). ## v0.4.43 (2026-09-26) - **Rating: any max, five unless named, and a default you click** (Feature #234): Kyle, 2026-09-25: "for ratings default to 5 but allow the user to put any quantity; also the default rating selection should not be a text box but an interactive rating preview using the selected icon." A rating's max is now any whole number from 1 to 100; it was 1 to 10. A rating made with no max is out of five on every door (the engine, `POST /api/tables/:id/fields`, `weave_add_field` and `weave field add`), and one test now holds all four to it. The guard of 100 stays on purpose: every icon is a button in every visible grid cell, so a max in the thousands would put tens of thousands of nodes in one screen of rows, and a row of more than 100 icons is wider than any screen. The reason sits beside `RATING_MAX` in `src/engine.js`, and the dialog mirrors the number. In the field dialog the max is a whole-number box prefilled with 5, with 3, 5 and 7 as shortcut chips beside it; a number past the guard is marked invalid and never saved. The default is no longer a text box. It is a row of `max` icons in the chosen icon, drawn by the same `ratingEl` the grid cell uses: click the nth icon to set the default to n, and click it again to clear it. With the row focused, the arrow keys move it by one, a digit sets it (a digit past the max sets the max, 0 clears), End sets the max, and Home, Backspace or Delete clear it. A screen reader hears "Default: 3 of 5". The row redraws when the icon or the max changes, and a default above a lowered max comes down with it. The Showcase gains Brightness, a 12-point sun rating with a default of 6, through the existing `SHOWCASE_ADDITIONS` sync. Gates: `test/rating-field.test.mjs` (3 new cases: any max up to the guard with a value of 17 stored, out of five on all four doors, a default sent with a new max and clamped by a lowered one), `test/field-dialog-core.test.mjs` (4 new cases: the guard mirrored from the engine and the typed box, click and label, the keys, the clamp in the state and the definition), `test/rating-browser.test.mjs` (2 new cases, one per theme: a 12-icon preview, click to set and to clear, the keys with focus held, a clamp on a lowered max, an icon change, then a row made with + New starting at the default and drawn in the grid), `test/seed.test.mjs` (1 new case: Brightness and a new row at its default). - **A rating default sent with a new max kept the old max** (Issue #396): `updateField` read the `default` lane before the rating's `max` lane, and the field dialog sends both in one save, so raising a 5-point rating to 12 with a default of 11 stored a default of 5. The rating lane now runs first, and lowering the max clamps a standing default in the config as well as on every row. Gate: `test/rating-field.test.mjs` ("a default rides with the max"). ## v0.4.42 (2026-09-26) - **Columns keep their widths** (Feature #233, Kyle's mockup review of 2026-09-25). Every field type opens at a default width: Name 220px, text, links and multi-selects 180, long text 280, selects 124, relations 136, dates 112, numbers 88, currency 104, checkboxes 56. A header label never clips: a longer label widens its own column, and no drag, keyboard nudge or fit goes under it. A date column also opens wide enough for its format's longest date. Showing, hiding, adding or removing a field leaves every other width alone; the space at the right of the grid takes up the difference. A hidden field comes back at its own width and in its own place. - **Resize** moves only the column you drag. The columns to its right slide over, and a readout shows the field, its width and the change. Double-click the edge to fit the longest value, up to the type's maximum. Alt+Left and Alt+Right on a focused header resize by 8px. - **Reorder** by dragging a header: a ghost follows the pointer, one line marks where the column lands, and the moved columns slide into place. Near the edges of a wide grid it scrolls. Alt+Shift+Left and Alt+Shift+Right move a focused header one place. - **Freeze** a field by dropping it across the seam beside #, and unfreeze it by dropping it back. The line reads "Freeze here" or "Unfreeze" when a drop changes the frozen zone. Frozen fields stay put while the grid scrolls sideways, up to 60% of its visible width. - Widths, order and the frozen count belong to the view. Agents set them with `weave_table_view` (`widths`, `frozen`), the CLI with `--widths` and `--frozen`, and the Workspace/Views row carries Frozen and Widths. - Fixed: a dragged column snapped back to its old place on the next sort, and rows scrolled into view later showed the old cell order (Issue #399). ## v0.4.40 (2026-09-25) - **Drag a view tab to choose the default** (Feature #229, Kyle's rulings of 2026-09-25): "Instead of a star, allow drag to reorder, the first (leftmost) view is the default, then we don't need the 3 dots", and "blank can be removed". The leftmost tab opens with the table, and the order of the strip is the only thing that says so. A tab moves by drag with a mouse (press and move), with a finger (hold still for a moment, then slide, so a quick swipe still scrolls the strip), or with Alt+Left and Alt+Right on a focused tab. Double-click renames a tab in place: Enter or clicking away saves, Escape cancels. Right-click, Shift+F10 or the ContextMenu key (or a finger held and let go) opens Rename…, Duplicate view… and a hold-to-delete Delete view. Duplicate view… replaces the old Save as view item. The + button makes a view with every field in schema order, no filter and no sort, which is how you get back to the raw table. An old `…/view/blank` link still opens the raw table, read-only, with no tab lit, and its note now points at + and the tab menu. A table keeps at least one view: deleting the last one is refused with a message, on every door. Agents: position is the single source of the default. `weave_table_view` reads carry no `default` flag, `position: 0` makes a view the default, `default: true` still works as the older spelling of the same move, the strip list no longer ends with a Blank entry, and `Table/blank` still reads the raw table. `applySchema` writes views before it deletes and then places the order, so a document can replace every view and the order round-trips. The tool's schema went from 910 to 888 bytes. Kyle did not say where rename, duplicate and delete should go once the ⋯ menu left; the session chose double-click and the tab's context menu. Gates: `test/table-views.test.mjs` (26 cases: the position alias, the last-view refusal through the verb, MCP, CLI, REST and the Views row, the order round-trip and a replace-every-view document) and `test/table-views-browser.test.mjs` (13 cases: no star, dots or Blank tab in the DOM, a mouse drag and a touch drag that persist and change what the bare route opens, a quick swipe that does not reorder, double-click rename with Escape, the tab menu, hold-to-delete and the last-view refusal, keyboard reorder and Shift+F10, + from a filtered view, and the old blank link), green in Chromium and WebKit. ## v0.4.39 (2026-09-25) - **The Showcase shows bar, ring, heat, ratings and sparklines** (Issue #371): the three displays shipped on 2026-09-23 (Features #230, #231, #232) never reached the live Showcase. `seedFieldShowcase` returned early once the space existed, so only a brand-new docs workspace got even the one `Fit` rating the seed had. The new columns are now a data list, `SHOWCASE_ADDITIONS` in `src/weaver-seed.js`, that the fresh seed and an existing workspace both go through. Boot runs `syncShowcase` on the docs workspace once per build, keyed on the list's hash, the way the Handbook sync works (Issue #255). A missing field is added after its anchor column, in the schema and in every view that shows the anchor; a value is written only into an empty cell of a row the seed names; nothing is renamed or removed. Field Types gains Progress (bar, column scale), Score (bar, fixed scale 10), Completion (percent ring, fixed scale 1), Load (heat), Fit (5 stars), Effort (3 bolts), Love (7 hearts), a Peer skill rollup (avg of a rating, drawn as icons), four lookups over Peers, and four sparklines ordered with `sortby` by the date each peer joined: Age trend (line), Delta columns (column), Wins and losses (win/loss), and Skill trend (line, null when a row has no peers). People gains Skill (rating), Joined (date) and Delta (a number with a negative). Gates: `test/seed.test.mjs` now reads `NUMBER_DISPLAYS` and `SPARKLINE_STYLES` from the engine, so a display added later without a Showcase column fails the build, and asserts ratings at max 3, 5 and 7 with two vendored icons, a 0 and an unrated cell, the rating rollup, a loss and a win in the win/loss series, and the null list; `test/showcase-sync.test.mjs` (new, 5 cases) covers an older Showcase catching up beside its anchors, a person's same-name column and filled cells left alone, one pass per build, a workspace without a Showcase left without one, and the serve boot. ## v0.4.38 (2026-09-25) - **The view header holds at the top of the page** (Issue #321): Kyle, reading a table in Safari, "some times table title and description stays visible ... sometimes not, should always stay visible with breadcrumbs and upper right toolbar". The "sometimes" was which box scrolls. A grid wider than its card takes a vertical scroller of its own (Issue #233), so the page never moves and the header only looked pinned; a grid that fits clips, the page is the scroller, and the header left with it: measured on the Issue table at Kyle's 2187x1359, where a 2500px scroll put the header at -2468. `#main > .view-header` is sticky now, so the breadcrumb, title, description and toolbar read the same in both modes. The 16px under it is padding rather than margin, because rows read straight through a transparent band, and z-index 6 clears every sticky layer the grid has (Issue #252 tops out at 5) while tying with the cell expansion and the field menu, which are later in the document and still open over it. The grid's own field headers and the Σ row start where the header ends: `viewHeader` measures it into `--wv-view-h` on the root, and a `.wv-fit` wrap (the one that clips, so the page is its scroller) adds it to their `top`; every other wrap scrolls itself and keeps its own top edge. The same reading is `scroll-padding-top`, so a focused control, a hash anchor or find-in-page stops below the header, and it feeds the grid's window arithmetic, which read the page's top edge as 0 and put a commit mid-table a header's height out. A header that would take half the window does not pin at all: a 200px window, or a description the reader expanded to thirty lines, would hold nothing but header, and the Show less control at its foot would sit off screen with no way to scroll to it. The docked pane keeps its header in the flow, as before. Gates: `test/sticky-view-header-browser.test.mjs` (new, 8 cases: the page scrolling under a held header with crumb, title, description and toolbar on screen; the field headers and Σ row flush under it; a wrap-scrolling grid unchanged; an opaque band in both themes; the dock's header static and never taking the page header's reading; the too-tall header standing down and pinning again when folded; a window shrunk to 200px and given back), plus `test/sticky-add-row-browser.test.mjs`, `test/footer-rollups-browser.test.mjs`, `test/grid-window-browser.test.mjs` and `test/grid-density-browser.test.mjs` moved to read the body's top edge off a stuck field header rather than the ``'s layout box. ## v0.4.37 (2026-09-24) - **A density flip keeps the reader's place** (Issue #342): flipping Comfortable to Compact part way down a long grid put the reader back at row 0. Two things sent them there. The row window is placed from a body-relative offset divided by the row height, so the same offset at a shorter row height is a different row: on a 600-row table at scroll 10,800 the flip drew from row 332 instead of row 209. And the segmented control rebuilt its buttons on every pick, so the button the reader had just clicked left the document, focus fell to ``, and the browser scrolled to the top of the page to show it. The grid now owns the flip (`wrap.wvSetDensity`): it reads the row at the top edge off the DOM before the flip, applies the new density, re-measures, and lands that row back under the sticky header inside the one gesture, so nothing is painted in between. Read off the DOM rather than off the row height because weave's rows are not all the same height (Issue #324) and an estimated anchor was twenty rows out. The control keeps its buttons and moves the `on` mark, which holds focus where the reader put it for every segmented control in the app, the field dialog's included. Gates: `test/grid-density-browser.test.mjs` (new, 3 cases: a wide grid scrolling in its own wrap holds the row across Compact and back to Comfortable, a page-scrolling grid holds it too, and the clicked button keeps focus and the mark). - **The bulk-write browser cases wait for the paint** (Issue #349): `Set a field… on a number` failed its first run and passed on the retry in three Gerrit gate logs. `runBulk()` clears the selection the moment `POST /api/bulk` answers and repaints the written rows only after the re-read behind it lands, so a case that reads the cells between those two moments loses the race under a loaded gate. A new `holdRepaint()` helper stalls that re-read by 600ms, which turns the gap from a coin flip into a certainty, and the six affected cases in `test/row-selection-browser.test.mjs` now wait for the value they assert instead of reading it off the click. The product is unchanged; the reads were early. Gates: `test/row-selection-browser.test.mjs` (6 cases rewritten, then 5 consecutive raw runs, 3 runs under CPU pressure and 3 alongside the full suite, all 24/24). ## v0.4.36 (2026-09-23) - **Formula lists and the sparkline** (Feature #232): Kyle, 2026-09-23, asked for in-cell trends across related rows, driven by formulas. A formula may now return a list or null; a list comes in through a lookup over a to-many relation, in relation order. The new `sortby(values, keys)` orders it by a parallel list, ascending and stable, blank keys last, so `sortby([Deal amounts], [Deal close dates])` is the amounts in date order. A test proves the precondition first: two lookups over one relation keep their blank slots in position (a row with no amount holds a null in the amounts beside its date), and a trashed row leaves both lists together. That held already; nothing needed fixing. The formula's display costume gains `sparkline` with a `style` of `line` (the default), `column` or `winloss`; a number field refuses it, since a stored number is one value. The grid cell, the chip and the card draw the newest 60 points (a line with a dot on the newest point and gaps at blanks, columns standing on zero with losses in red, or win/loss bars about a midline); the hover lists every value and says when it cut; a screen reader hears "5 values, last 70, low -20, high 70". Sort and filter on a sparkline column read the last number in the series; the API returns the whole list. The formula dialog offers Display (text or sparkline), Style and a sample series once the check names the result a list. `sortby` joins the builder's number chips and the vocabulary (`sparklineStyles`). Also fixed (Issue #358): a rating or a meter on a chip or a card lost its drawing in `view-core.js`, which kept only a segment's label and text; a rating now draws its icons there. Gates: `test/formula-arrays.test.mjs` (new, 6 cases: lookup slot alignment, sortby ordering and errors, list and null results with the API returning numbers, the sparkline costume and its validation, last-value sort and filter, the chip's series), `test/sparkline-browser.test.mjs` (new, 6 cases: the three styles in both themes, the 60-point cap and its hover, the chip, the dialog in both themes), `test/rating-browser.test.mjs` (1 new case: the chip draws the rating's icons), `test/cell-graphics.test.mjs` (4 new cases), `test/field-dialog-core.test.mjs` (2 new cases), `test/vocabulary.test.mjs` (the style list held to the engine and the dialog). ## v0.4.35 (2026-09-23) - **Table views** (Feature #229): Kyle, 2026-09-23, asked for several named views per table, as Attio and Airtable show them, and ruled on the shape the same day. A strip of tabs now sits under the table title. Each view keeps its visible columns in order, its state filter and its sort. The starred view is the default: it sits first and opens with the table. **Blank** closes the strip: the raw table, every field in schema order, no filter, no sort, computed and never stored. A change to the filter, the sort, a column drag or the eye saves into the view on screen, with no Save button. **+** makes a view from Blank; **⋮ → Save as view…** copies the view on screen under a new name; **Make default**, **Rename view…** and **Delete view** sit in the same menu. Blank is read-only and says so. A view's link is `#/table/
/view/`. The agent surface came first and is one verb on every door: `tableView` in the engine, `weave_table_view` over MCP, `weave table view` on the CLI, and `GET`/`PATCH`/`DELETE /api/tables/:table/views/:view`. Tables and views are addressed by name (`Issue/Open bugs`, `Issue/blank`). One ordered `fields` list carries visibility and order together; `show`, `hide` and `move: {field, before|after}` edit one field at a time, so a wide table is never resent; `default` and `position` place the view in the strip; `from` copies a view; writes return the compact view, never the table. Filters and sort go through `updateTable`'s own validators, now shared. The MCP surface grew by one tool and 849 bytes (54 tools and 26,263 bytes before, 55 and 27,112 after); `weave_update_table` dropped `hiddenFields` from its schema, and the engine still accepts it. Every view is a row in the new **Workspace/Views** registry table (`Fields`, `Filter`, `Sort`, `Default`, `Position`, related to its Tables row), and a row edit runs the same verb. `describeSchema` emits each table's `views` and `applySchema` round-trips them; an omitted view needs `allowDestructive`. Migration: on first open, each table's filter, sort and hidden set become a starred view named **Default**, in the column order it had, so the grid reads as it did. The Tables row's Hidden Fields, Filter and Sort now mirror the default view, and `updateTable`'s `hiddenFields`, `filters` and `sort` write it. `fieldOrder` stays the schema order (the entity page and Blank). Naming: Feature #17's cross-table pages keep `createView`, `weave_views` and `/api/views` in code; their table menu item is now **New share page…**, so "Save as view" means one thing. Not in this change: per-person views and per-view density (density stays a reading preference in the header). Gates: `test/table-views.test.mjs` (new, 24 cases: compact reads, Blank, one-call definition, relative edits, refusals that write nothing, one validator and one message for filters and sort, partial updates, positioning, rename and delete, the legacy verbs, field add, rename and drop, migration, the schema document, duplicate and export, the registry row, MCP, the tool's size, REST, CLI, the capped writer token), `test/table-views-browser.test.mjs` (new, 8 cases: strip placement, autosave into the view on screen with a permalink reload, Blank read-only, Save as view, + and Make default, the eye per view, Issue #341, both themes), and 15 existing suites moved to read the default view. - **+ New on a filtered view starts the row inside the filter** (Issue #341): a row made on a filtered grid was born in the workflow's default state, outside the filter, so the grid never showed it. Views make filtered grids common, so this lands with them. The new row now takes the filter's first state when the filter does not already hold the default (a toggle takes its on label when the filter holds only that). Gate: `test/table-views-browser.test.mjs` (+ New on a view filtered to Done makes a Done row the grid draws). ## v0.4.34 (2026-09-23) - **Rating field type** (Feature #231): Kyle, 2026-09-23, asked for a score you set with one click instead of a number field and a scale to remember. `rating` is a new value type: a whole number from 0 to the field's `max`, drawn as `max` icons with the first ones filled. The config is `{ max, icon }`: `max` is 1 to 10 (5 unless named; the field dialog offers 3, 5 and 7 and takes any other), and `icon` is one icon from the inventory, a star unless named, picked with the same icon picker a space or a state uses. Click the nth icon to set n; click the filled current one to clear to 0. On a resting grid cell a digit sets the rating (a digit past the max sets the max) and Backspace or Delete clears it; no text box ever opens. A screen reader hears "3 of 5". A write is rounded and held to 0..max, numeric text included, and anything else is refused by name; lowering `max` holds every row to the new ceiling on read. Formulas, sort, filter, stats, CSV, bulk Set, paste and the API all read a number. `number` converts to `rating` by rounding and clamping, and back to the number it held. A lookup of a rating, and a rollup whose answer stays on the scale (`avg`, `min`, `max`, `median`), draw the same icons read-only, rounded to a whole icon, while the API keeps the unrounded figure; a `sum` or a spread prints as a number. The chip and the card draw the icons too. Added everywhere the types are listed: the vocabulary, the dialog's type grid, the MCP tool descriptions, the Handbook (a rating page, the definable types, the keyboard table), ONTOLOGY, README, the showcase seed and the uno applet. Gates: `test/rating-field.test.mjs` (new, 7 cases: registration and defaults, max and icon validation, rounding and clamping, the default, formulas, sort, filter, stats and CSV, number to rating and back, lookup, rollup and chip), `test/rating-browser.test.mjs` (new, 7 cases: icons and "n of max" in both themes, click to set and to clear, digit and Backspace on the resting cell, a rounded read-only rollup, the dialog's presets, icon picker and sample in both themes), `test/grid-keymap.test.mjs`, `test/cell-graphics.test.mjs` and `test/field-dialog-core.test.mjs` (new cases for the keys, the icon count and label, and the form round-trip). ## v0.4.32 (2026-09-23) - **Number display: bar, ring and heat** (Feature #230): Kyle, 2026-09-23, asked for in-cell graphics on number fields, like Excel's, so a progress or score column reads at a glance. The number costume gains `display` (`text` by default, or `bar`, `ring`, `heat`) and `scale` (`column` by default, or a fixed number above 0). A bar and a ring fill to the value's share of the scale; heat tints the cell by it. The value's text rides beside the graphic and is what a screen reader reads (`aria-label="60%"`); the SVG is aria-hidden. The column scale is the column's max, computed through `src/stats.js`, so it equals a Space-level `via` max rollup over the same column. Every read now carries `scales` (field name to its 100% mark) for its graphic columns, so a paged grid, a chip and a card draw against the whole column rather than the rows they hold; the engine caches each max and drops the cache on every write and reload. The API still returns the raw number in `raw` and the dressed text in `fields`. A numeric formula takes the same keys, and a rollup over a graphic column wears that column's display the way it already wore its format (a count wears none). Grid cells, chip segments and card fields draw the graphic; a graphic cell is never marked `clipped`; a click hands over the raw number, as a dressed number already did. The field dialog gains a Display picker, a Scale control (column max or fixed) and three sample rows that redraw as you choose. Going back to text drops a stored fixed scale: `updateField` now clears any costume key the canonical form no longer carries. Stars stay out of this costume, since a rating is its own type (Feature #231). Not in this change: doc mentions rendered server-side keep the text only. Gates: `test/number-display.test.mjs` (new, 9 cases: keys and validation, raw versus dressed, column and fixed scales, the scale following writes and the trash, formula, rollup, chip meter, parity with the `via` max rollup), `test/cell-graphics.test.mjs` (new, 8 cases: share clamping, bar, ring and heat geometry, hover text, the engine's list, the page loading the module), `test/number-display-browser.test.mjs` (new, 7 cases: the three displays against fixed and column scales in both themes, a narrow graphic cell never clipped, click-to-edit and redraw, rollup and chip meters, the dialog's picker and sample rows in both themes), `test/field-dialog-core.test.mjs` (4 new cases), `test/vocabulary.test.mjs` (the display list held to the engine and the dialog). - **The Name column opens at 260px where the card has room** (Issue #261, the width half): at 1440x900 the 2026-09-12 audit read "Looks bro", "Quickstar" and "CLI refere" in the Issue and Guide tables, because Name had no default width and auto table layout gave it the min-content of its `` while the prose, Status and Severity columns took the slack. An Issue-shaped fixture (Name, two selects, three prose columns) reproduced it at 147px with a thirty-character name clipped. The Name field's header and cells now carry a `name-col` class, chosen by role through `nameFieldOf`, so a renamed or reordered Name keeps it, and `.wv-grid :is(th, td).name-col` sets a bare `width: 260px`, the same 260px that caps every unsized cell. Auto layout treats a bare width as the column's preferred width: it fills it from spare room ahead of the other columns and squeezes it when there is none, so the floor never pushes a grid past its card. A first cut used `min-width: 260px`, which did: with a record docked at 1280x700 the grid grew to 554px in a 414px card, the wrap flipped from page scroll to its own scroller (`wv-grid-scroll`), and `test/scroll-browser.test.mjs` lost the page's scroll. `min(260px, 30cqi)` on a container wrap still overflowed a grid whose other columns sat at their min-content, and a percentage width is ignored inside `min()` on a table cell while a bare `30%` shrank Name in sparse tables. The cost of the bare width is a ceiling: a table with only a column or two beside Name now stops Name at 260px (the documented cap) where auto layout used to hand it 450px to 513px of slack. A stored width is inline `width`/`min-width`/`max-width` on the same cells and wins outright, narrower than 260px included; double-click fit and the windowed grid are unchanged. The customization guide says so. The single-click-enters-edit half stays open: it would reverse Feature #134. Gates: `test/name-column-width-browser.test.mjs` ("an unsized Name column opens at the floor, and a thirty-character name reads whole", "a stored width narrower than the floor is honoured", "the floor never pushes a crowded grid past its card" and "with a record docked beside the table, the floor leaves the page scrolling", light and dark; the last two first prove their fixture fits with the floor taken off), `test/handbook.test.mjs` ("the customization guide covers what a reader can change" pins the new sentence). - **The applet's description sheet keeps what was typed into it** (Issue #247): Kyle, 2026-09-08: "mobile descriptions not captured on save, need autosave". The phone applet's description sheet wrote only from its Save button, so a tap on the dimmed scrim above it (the way every other sheet there is dismissed) closed it and dropped the text, and so did switching apps or closing the tab. Save itself closed the sheet before its request, so a write that failed lost the text as well. Every way out of the sheet now runs through one close hook that writes the text when it changed: the scrim, the Save button, Escape (new, for an iPad keyboard), and another sheet taking its place. A pause of 800 ms in typing writes it with the sheet still open (the desktop editor waits 600 ms; a thumb types slower and autocorrect stalls mid-word), and `visibilitychange` and `pagehide` write it with `keepalive` so the request outlives the page. Writes are chained so an older text never lands after a newer one, and the one write that is not (the leaving write goes out at once) is numbered, so an older answer that comes back last is ignored rather than read as the newest. The sheet's heading shows **Saving…**, **Saved** or **Not saved**; a failed write leaves the text on the task as a draft that the next open starts from, and the next close retries it. The same generated page serves node and the Worker, and the server routes are unchanged. Gates: `test/applet-autosave-browser.test.mjs` (7 cases at 390x844 with touch: scrim tap then reopen, Escape, a pause with no close in light and dark, the page going away, a failed Save kept and retried, an older write answering after the newer one) and `test/applet.test.mjs` ("the description sheet writes on every way out, and on a pause"). - **A file cell takes a dropped file** (Issue #85): Kyle, 2026-08-26: "empty file cells look wrong and hover drop hotbox doesn't exist or work". Dragging a file from the desktop over an attachments cell did nothing, and letting go handed the file to the browser: no cell or record surface listened for `dragover` or `drop`, and `dataTransfer.files` appeared nowhere in `public/`. The only way in was **+ file** on the record page. A new `fileDropZone()` lights the grid cell (the whole `` and its ``. - **The tab title names the place, and the table title is an `

`** (Issue #267, part): `document.title` read "Weave" on every page, so tabs, history entries and bookmarks all looked alike. A new `weaveBreadcrumbs.docTitle` builds ` · `: a table or space page reads its name, a docked row or the entity page reads the row, the workspace page reads the workspace name alone, and "Weave" is left only where no workspace has loaded. Closing the dock gives the tab back to the table, and a rename retitles it. The editable title input now sits inside an `

` that resets the element's own margin and type, so the header measures the same in both themes (19px/700, identical box). Not in this change: Back walking docked rows. The dock pushes no history entry by the rulings on Issues #198, #226 and #276, and changing that needs a routing decision. Gates: `test/breadcrumbs.test.mjs` (+1), `test/doc-title-browser.test.mjs` (new, 3 cases). - **A relation write lands in both rows' histories** (Issue #286): `#setRelationValue` updated both sides of a paired relation but logged `relation-updated` only on the row the caller touched, so a project whose Tasks list gained or lost a row showed nothing in its activity: the field had moved under it with no entry to say so, and the steal case (a single-value inverse moving to a new holder) left the losing row silent too. Each far row now gets its own entry in the same write, naming its own field and the row on the other end, tagged `inverse: true` so a reader can tell the write a row made from the write it received; Fibery shows the link on both feeds the same way. Every door that moves a relation is covered, because they all go through this one path: `link`, `unlink`, `updateEntity`, `createEntity`, a hard delete's unlink, and an undo stepping any of them back. A one-way target-set relation is unchanged: the target has no inverse field to name, so nothing is logged there. Gate: `test/relation-activity.test.mjs` (new, 8 cases). ## v0.4.21 (2026-09-15) - **A type change into a formula checks the expression** (Issue #288): `addField` and `updateField` both ran `checkExpression` before storing a formula, but `#migrateFieldType` checked only that an expression was present, so `updateField(t, 'X', { type: 'formula', config: { expression: 'if(upper(' } })` landed and every row read `#ERR: Unexpected end of formula`. The migration branch now runs the same parse and known-field check, with the field's own name off the list, beside the cycle refusal Issue #283 put there; the refusal carries the message `addField` gives and leaves the type alone. `applySchema` used to turn a computed Name into a formula before creating the fields it reads, which the check would now refuse, so both its create-table and existing-table paths apply that type change after the table's other fields. Gates: `test/formula-cycle.test.mjs` (+1: parse error, unknown field, self-reference, then a valid migration), `test/name-field.test.mjs` (+1: a computed Name that reads later fields through the declarative door, fresh and onto an existing table). - **Automation changes land in the audit log** (Issue #284): `createAutomation`, `updateAutomation` and `deleteAutomation` wrote no audit row, so `weave audit` could not say who added, paused or removed a rule. They now write `automation-created` (table, name, trigger type), `automation-updated` (table, name, the patched keys, the way `field-updated` does) and `automation-deleted` (table, name) through the same audit helper the schema verbs use, with the caller as actor. A delete of an id that does not exist changes nothing and writes nothing; automations on system tables stay out of the log like their fields do. The REST, CLI and MCP surfaces all call these verbs, so every door is covered. No schema change: `audit_log.detail` is free-form JSON. Gate: `test/accounts-audit.test.mjs` (+1). - **Opening a table no longer reads its trash** (Issue #270): `showDatabase` fetched `GET /api/tables/:id/trash` beside every table query, and for the Case table that list was 1,577 KB, built from a second whole-workspace scan plus a `readEntity` per trashed row, all to print "(N)" beside the eyeball's "Deleted rows" switch. The query now takes `trashCount: true` and answers the table's trash count as a number (the whole table's trash, not the filtered page's), so the open costs one `POST …/query`. The trash list is fetched only when "Deleted rows" puts the trashed rows in the grid. The option reaches `weave_query` on MCP as well as `POST /api/tables/:ref/query`; a query that does not ask keeps its `{ total, items }` shape. Gates: `test/soft-delete.test.mjs` (+2: the count on request, and the REST and MCP opt-in), `test/trash-on-demand-browser.test.mjs` (new: no trash fetch on open, the eye still reads `(1)`, one trash fetch when the deleted rows are shown), `test/ui-contract.test.mjs` (the eyeball contract now names the count on the query). ## v0.4.20 (2026-09-14) - **The slash-menu wheel case stops racing its own page** (Issue #275): the Issue #137 case in `test/slash-commands.test.mjs`, "a wheel over the slash menu never scrolls the page out from under it", lost three of Gerrit 334's gate runs on a change that never went near the editor, and took the Verified vote down with it. The flake lived in the keystroke that opens the menu. Vditor hides its hint on any scroll, with a listener on the window and another on the editor's own element; pressing Enter in the case's eighty-paragraph fixture parks the page at the end of the document, and the next keystroke animates it back to the caret over about a second. That keystroke is the `/`: the menu opened, the animation's first scroll event shut it again, and the wait for something visible spent its thirty seconds against twenty-one rows sitting at `display: none`: the signature the gate logged, three times. An 8× CDP CPU throttle standing in for a loaded gate machine reproduced it on four runs in five. The case now lets the page come to rest, puts the caret in the middle of the viewport, and waits for the scrolling to stop before it types, so the `/` has no scrolling left to cause: ten runs in ten under the same throttle, and the file green ten times running unthrottled. Two sleeps became conditions alongside it: the menu is measured once the clamp that caps it has landed, and the page is read after the wheel when scrolling stops rather than 400 ms later, and a new assertion refuses to let a wheel that missed the menu pass for one that hit it. The wheel contract itself is word for word what it was, and no product code changed. - **A range sorts by its start, then by its end** (Issue #287): Issue #279 undressed the two types whose painted form does not order the way the value does, and left `daterange` dressed: its raw value is an object, and ordering it needed a product call nobody had made. So a `Window` column sorted on `"Oct 1 – Oct 3, 2026"` against `"Sep 9 – Sep 12, 2026"` and put October first, because `O` beats `S`. The rule is the one a calendar keeps: a range sorts by its start, and two spans that open on the same day are broken by their ends, so the shorter one stacks first. `weaveDateGrain.rangeKey` builds that key: the stored ends joined in order, which compare as text within a grain the way a single date already does, and both comparators read it: the engine's `#pathValue` under `undressed` (so `weave query --sort`, `POST /api/tables/:ref/query` and `weave_query` all move together) and `public/app.js` for the grids that hold every row. An open end is refused at the door and only `importJSON` can write one, so `rangeKey` sorts it after a closed range that opens on the same day: an open range extends furthest. Handbook: the `daterange` guide's Gotchas. Gates: `test/sort-values.test.mjs` (+3: start-then-end with a tie on the start, a range of clock times, the refusal plus the imported open end), `test/sort-values-browser.test.mjs` (+2: the server's sort and the grid's own, on the same table). ## v0.4.19 (2026-09-13) - **The caret keys stay with the caret** (Issue #260): click into a long name, press End, type, and the edit landed in the MIDDLE of the value. A CX audit did exactly that and left Issue #85 named "L zzs broken: …". `End` inside an open cell was the browser's, and Chromium reads a bare End in a single-line field as *scroll to the end of the document*: the windowed grid (Issue #271) scrolled a thousand rows, recycled the row the editor sat in and dropped focus on ``, while the caret never moved. The grid places the caret itself now: `Home` and `End` in an open cell resolve to a `caret` verb and the keystroke is consumed, so nothing scrolls, and only for a single-line ``: a textarea's `End` belongs to the line it is on, and a select, number or date control has no caret to place, so each of those keeps the browser's key. `⇧End`, `⌥←`/`⌥→` and `⌘←`/`⌘→` are real editing commands, were never broken, and stay the browser's; at rest `Home`/`End` are still the first and the last ROW of the table. Handbook: the **In an open cell** table in the grid guide. Gates: `test/grid-keymap.test.mjs` (the open-cell verb, the three modified forms, and the resting rows), `test/grid-window-browser.test.mjs` (End in an open cell of a 1,200-row table: the caret lands at the end of the value, the window holds its scroll, and the next character appends). - **A loop says so** (Issue #283): a formula that reached its own field the long way round (`Total` reading `Bonus` while `Bonus` read `Total`) hit the depth-8 recursion guard, which returned `null`. The cell then read like a legitimately empty one, and one level up the arithmetic turned that null into a number nobody could explain (a two-row rollup loop computed `6`). Two halves now. **At save**, an expression that closes a cycle through the table's other formulas is refused with the path: `Formula 'Bonus' would close a reference cycle: Bonus → Total → Bonus`, on `addField`, on `updateField`, on a text→formula type change (which validated nothing), and in `checkFormula`, so the dialog says it before the save does; the walk only follows formula→formula edges, every other type being a leaf, and a field that is not in the table yet is matched by the name a live formula still calls it. **At read**, a computed field that re-enters itself for the same row answers with `#CYCLE:` and the path instead of null: fields alone when the loop stays on one row, `Alpha › Double → Alpha › PeerSum → Beta › Double → Beta › PeerSum → Alpha › Double` when it travels between rows, because the rows are what closed it, and the marker travels rather than being consumed: a formula reading a looping field reports the loop, a rollup reports it rather than summing it, a lookup carries it out, and no display costume is painted over it. The loop that no save can see (one that closes through a rollup or a lookup, which needs two rows pointing at each other) is exactly what this half catches. The stack unwinds in a `finally`, so an erroring formula never leaves the next read looking cyclic. Gates: `test/formula-cycle.test.mjs` (11: the four save doors, the REST 400, the rollup loop through two rows, the row outside the loop, the unwind, and `references()` over both arms of `if`). - **A sort orders the value, not the costume it wears** (Issue #279): uno's `Agent/Sessions`: 720 rows, sorted Created descending, Created a `date` field wearing the `long` format, put "Sep 9, 2026 9:51 AM" above every row created on Sep 12, so an hourly writer's rows read as missing. Both comparators had been handed the painted string: the engine's `#pathValue` returns display values, and `query`'s sort compared those, while `public/app.js` compared `item.fields[…]` for the grids that hold every row. `"Sep 9"` beats `"Sep 12"` as text, `"15,829,984"` loses to `"900"`, and `"$17.25"` beats `"$9.50"`. Sorting now undresses the two types whose painted form does not order the way the value does (a number stays a number, a date stays its stored instant) and leaves every other type on its display form, because for an option, a state or a joined relation the name *is* the value. It is the rule a formula already read by (`#resolve`) and the one the Handbook's date guide already promised ("comparison, sorting and date math all run on the stored value"). Filters are untouched: `contains "Sep"` still reads the costume. Both halves changed together, so a paged grid (server-side sort, Issue #271) and a whole one land in the same order. Gates: `test/sort-values.test.mjs` (5: date, number through separators and currency, select by option name, empty cells last, the filter guard), `test/sort-values-browser.test.mjs` (2: the same table read through the server's sort and through the grid's own). ## v0.4.18 (2026-09-12) - **Every document keeps its history** (Feature #225, Kyle 2026-09-12: "all weave documents should have a built-in version history", so decklet can drop its in-file snapshots): `setDoc`/`appendDoc` overwrote `entity.docs` and the prior text survived only as an undo before-image (popped on undo) and a length delta in the feed; nothing could bring a past document back. A new `doc_revisions` table beside the entity blob (mirrored in the Durable Object store) now takes a snapshot of the new text on every document write: `setDoc`, `appendDoc`, an `updateEntity` naming a document, a create born with one. Writes by one actor to one field inside ten minutes fold into the newest snapshot (the activity feed's own window, Issue #32), so a typing session is one revision and the 600 ms autosave never builds a keystroke log; identical text records nothing; 200 per document, oldest trimmed. A document that predates the log gets its prior text as the baseline on its first write. `listDocRevisions` is metadata only, newest first (`seq`, `at`, `actor`, `len`); `getDocRevision` carries the text; `restoreDocRevision` is an ordinary `setDoc` recorded fresh, so a restore never overwrites the session it steps back from, and undo records fresh too. A purge drops the row's revisions; the trash keeps them; an export carries the current text only. Doors: `GET /api/entities/:ref/doc/revisions?field=`, `GET …/revisions/:seq`, `POST …/revisions/:seq/restore`; `weave doc-revisions` / `weave doc-restore`; `weave_doc_revisions` / `weave_doc_restore`. In the chrome, a `lucide:history` control beside the source toggle shows once a document has more than one revision and opens a panel inside the section: newest first with relative time, actor and size delta; a row renders that revision read-only in place of the editor under a **Viewing revision** bar; **Restore** writes it back and re-renders, **Back** or Escape returns. Both themes. Handbook: the `document` page's **History** section. Gates: `test/doc-revisions.test.mjs` (22: the store contract on memory, sqlite and the DO shim; coalescing, baseline, cap, restore, undo, purge; the three doors), `test/doc-history-browser.test.mjs` (3, both themes). - **Paste into a relation cell links by name** (Feature #224): Feature #220's plan refused a `relation` column, so once Feature #221 let ⌘V reach the cell, a paste into `Owner` or `Peers` wrote nothing and the toast named the column. A paste now links: the grid fetches the related table's rows first (the same `POST /api/tables/:id/query` the record picker runs) and the pure plan matches each label to a row, a copied weave block by id, otherwise the exact name (trimmed, case-insensitive), then `#12`. Matched ids ride the existing `bulk set` (the engine's `updateEntity` already replaces a relation's whole set from an id list, and a single-cardinality field takes one), so a paste is one call per distinct value set with one Undo, and copy is untouched. A label that names no row is counted and named in the toast (`1 unmatched (Nobody)`) beside the unreadable count; a cell whose labels all miss is dropped rather than emptied. Without the rows to hand (a fetch that failed), the column refuses by name as before. Handbook: the "What the types force" list under "Ranges, fill and paste". Gates: `test/grid-range.test.mjs` (+8 pure: `resolveRelation`, the relation plan, the toast), `test/grid-range-browser.test.mjs` (+3: copy → paste → chips → Undo; a TSV label with one stranger; a single relation from `#id, name`). - **Passkeys: people sign in without a token** (Feature #222 part 2, door B; Feature #208; Feature #141 §3): with `requireAuth` on, a browser had no way in that did not involve pasting a `wv_` secret. `GET /auth` is now a sign-in page served through the wall (one button, a discoverable passkey, no username step), and `/api/auth/*` carries the two WebAuthn ceremonies, verified with `node:crypto` and a 60-line CBOR decoder, no dependency: `none` attestation, ES256 or RS256, clientDataJSON type/challenge/origin, `rpIdHash`, user presence, the signature over `authData || sha256(clientDataJSON)`, and a counter that must advance once either side counts. A verified sign-in mints a 30-day sliding session in an `HttpOnly; SameSite=Lax` cookie (`Secure` under an https `WEAVE_ORIGIN`), stored as its sha256 beside the account; the wall accepts it as it accepts a Bearer token, and a Bearer token wins when both are present. A dead cookie sends a page to `/auth?next=…` and an API call to a JSON 401. The first passkey starts from the CLI: `weave account invite ` prints a one-time URL good for 15 minutes; `account sessions`, `account revoke-session --all`, `account remove-credential` are the lost-device verbs, on the CLI and on `weave_accounts`. `/auth` doubles as the account page: passkeys, sessions, sign out, sign out others, add this device. `WEAVE_ORIGIN` is required when auth is on off loopback (`serve` refuses to start without it); `WEAVE_TRUST_PROXY=1` lets the in-memory rate limits (10 options and 5 failed verifies a minute per address) read `X-Forwarded-For` behind Railway or Fly. Sessions and invites never leave through an export. New Handbook guide **Door B: passkeys**. Gates: `test/webauthn.test.mjs` (fixtures built in-test from a software authenticator, every negative case), `test/auth-session.test.mjs` (engine + routes: invite → register → wall → logout → wall; expiry, revocation, rate limits, Bearer precedence), `test/auth-passkey-browser.test.mjs` (Chromium's virtual authenticator through the real page). - **`weave backup` and `weave restore`, with a nightly that needs one env var** (Feature #222 phase 3, Feature #209, Issue #250): the only backups were a laptop script into iCloud and the JSON export the Handbook warns drops attachments. `weave backup` now takes the whole data directory (every workspace `.db` through `VACUUM INTO` from a throwaway connection, so a server mid-write neither blocks it nor lands in it; `files/`; `keystore.json`) into one POSIX tar with a `manifest.json` last: entries, a sha256 per entry, entity counts per workspace, the weave version, and the orphan report. `keystore.key` never goes in; its material seals the archive instead (AES-256-GCM under a scrypt key), or `WEAVE_BACKUP_PASSPHRASE` / `WEAVE_KEYSTORE_PASSPHRASE` when set, and with none of the three the archive is a plain tar and says so. A reference to a blob that is gone from `files/` (Issue #250 found fifty-six) is counted and listed, never fatal. `--dest s3://bucket/prefix` (or `WEAVE_BACKUP_DEST`) uploads with a SigV4 signer written on `fetch` and `node:crypto` (R2 via `WEAVE_BACKUP_ENDPOINT`, B2, S3) and prunes past the newest thirty archives; the signer reproduces the three worked examples in the S3 reference. `weave restore --data ` verifies every sha256 against the manifest, refuses a database a server holds open (a `-wal`/`-shm` sidecar or a `BEGIN IMMEDIATE` that cannot take the lock) and an existing target without `--force`, then unpacks; `weave restore ` stays the entity verb. `weave serve` with `WEAVE_BACKUP_DEST` set runs the backup daily at 04:00 UTC on an in-process timer chain (Railway cron services cannot share the volume), writes `backup-completed` / `backup-failed` to the audit log, and `/api/health` carries `backup: {lastAt, lastStatus, nextAt, dest}`. Handbook: **Backup and restore**: the three variables, R2/B2/S3 examples, retention, the rehearsal, `keystore.key`, Litestream as the upgrade. Gate G4: `test/backup.test.mjs` (23 cases: round trip with two workspaces, an attachment byte-for-byte, an orphan and the keystore; live-writer safety; the ustar writer against `tar -tf`; the signer vectors; upload, list and prune against an in-test bucket; the scheduler on a fake clock; health). - **The dock follows the click; the page stays** (Issue #276, Kyle 2026-09-12): a relation chip pointing at a row in another table used to tear the table down, push `#/table/` and dock the row beside the *other* table, so the crumb restarted there and the reader lost the grid they were reading. `openEntity` now docks the row beside the table under the reader whatever table the row lives in, pushes no history, and only travels when there is no table page at all (home, a space, activity, ⌘K from there). A hop from inside the dock extends the dock's chain across tables (each frame carries its own table, the state carries the anchor) and the dock's crumb is that chain run through `entityCrumbs`, so the table crumb appears where it changes (`Deals › Acme › Contacts › #4`) and a new back arrow (or Esc) walks it one frame back; a frame already on the chain cuts back to it. `?e=` on the current table hash survives a reload for a foreign row, which renders from its own table. The expand arrow still lands on `#/entity/`. Gates: `test/entity-dock-browser.test.mjs` (4 new cases), `test/entity-dock-default-browser.test.mjs` (expectations moved to the page-stays rule, plus the no-table-under-you case), `test/breadcrumbs.test.mjs` (`dockCrumbs`). Handbook: the grid guide names the rule. - **The auth wall covers every route, and an export keeps its secrets** (Feature #222 phase 0, Issue #230, Feature #194 problem 2): `requireAuth` and the role caps applied to `/api/*` only, so with auth on every entity page, `doc.html`, `entity.pdf`, `deck.html` and the app shell itself were readable by anyone who reached the port. With `requireAuth` on, every route now needs a Bearer token; a browser gets a small page saying the workspace requires authentication, an API call keeps the JSON 401. The doors that stay open: `/api/health`, a view's share link, the passcode applet under `/t`, and the static CSS/JS/font/image assets a sign-in page will need. A reader token may read any page and write at none. `GET /api/export` (and `weave export`, and the MCP tool) no longer carry account `tokenHash` values or view share tokens: an imported account keeps its name and role but verifies no token until re-created, an imported view arrives unshared until shared again. `POST /api/import` is a schema write and needs an admin token. Gate: `test/auth-wall.test.mjs` (six cases; the route list is derived from `src/routes.js` so a new route cannot hole the wall unnoticed). - **Grid cell clipboard follows the selection** (Feature #221, Kyle's "B+", 2026-09-12): a click opens a cell exactly as before, and `⌘C`/`⌘V` follow the selection: text selected in the open control is the browser's own copy and paste; with no selection (a bare caret, a picker's popover, a checkbox, a resting cell) they take the **cell**, typed, through Feature #220's `bulk set` with one Undo. A clicked text-like cell opens with its whole value selected, and a second click places the caret. Chips, url links, the `#id` link, checkbox flips and pickers are untouched. Pure rule `WeaveGridKeymap.clipboardTarget` (`test/grid-keymap.test.mjs`); gestures in `test/grid-range-browser.test.mjs`; the Handbook's grid guide names both rules. ## v0.4.17 (2026-09-11) - **The Handbook on a running install follows the build** (Issue #255): `applyHandbook` wrote the `Handbook/Guide` and `Handbook/Fields` pages only from `seedWeaver`, and `serve` seeds only when no `weave.db` exists, so an existing docs workspace kept the pages of whichever build first created it. A landed Handbook edit passed its gate and never reached the reader: on 2026-09-10 the live **Making a workspace your own** guide was missing the whole "Ranges, fill and paste" section. `serve` now stamps a hash of the generated pages on the docs workspace's meta at boot, beside the Development sync, and re-applies them when the hash moves: one pass per build, not one per boot, fail-open and logged. The apply stays name-matched and additive: a guide someone wrote themselves is never touched, and nothing is deleted. `weave handbook sync --data ` applies on demand and `weave handbook check` exits 1 on drift, modelled on `quality`. The first boot on `:4400` brought two stale field pages, `workflow` and `rollup`, up to date. Gate: `test/handbook-sync.test.mjs` (10 cases, including a real `serve` boot against a stale `weave.db`). ## v0.4.16 (2026-09-10) - **The # column freezes to the grid's left edge** (Issue #252): a wide table scrolls sideways inside its wrap, and the whole header used to go with it: scroll right to read a far column and the row lost its identity, with no id to say which record the values belonged to and the `#12 ↗` permalink off screen. The `#` and the selection checkbox beside it are anchored to the wrap's left edge now, and they travel as a pair: the checkbox pinned at the edge, the `#` clearing exactly the width the checkbox column renders at, which the suite reads off the live layout rather than trusting the arithmetic. A frozen cell floats over content that is still moving, so it paints: a token ground in both themes, a chosen row's 9% accent mixed against that surface instead of against `transparent`, and a hairline down its right edge that is reserved at rest and only takes its colour once the wrap is actually scrolled: the frozen column reads as a seam, not as a panel laid over the grid. Five sticky layers now have numbers instead of sharing two, because a tie is broken by document order and that put a body cell over the Σ rollup row it scrolls beneath: frozen body cells, the Σ row, the Σ row's own frozen corner, the header, the header's corner. The other half of the Issue (that no drag can move the `#` or land a field ahead of it) was already locked when Issue #139 landed and needed no change. Gates: `test/frozen-id-column-browser.test.mjs` (11 cases, both themes, including the Σ row, the sticky `+ New` row and a grid with no checkbox column) and the standing `test/table-reorder-browser.test.mjs`. - **A state field starts with states in it** (Issue #251): adding a **State** column opened the tray on an empty list and then refused the save ("Workflow field needs at least one state"), so a status column cost you a status vocabulary before it could exist, and `weave field add T Status workflow` threw unless `--config` carried a `states` array. The engine already knows four state categories, and those four are the lifecycle almost every column wants: a workflow whose config omits `states` now arrives as **Not started · In progress · Done · Canceled**, one per category, `Not started` the default. The seed sits at the point of blankness rather than in a relaxed validation: `normalizeSelfContainedConfig` fills an absent `states`, the tray's `blankState('workflow')` hands back the same four, and `"states": []` is still refused, because a list someone emptied on purpose is not a lifecycle. An option-less select converting to a workflow lands on the four as well, where it used to hit that refusal. The four are ordinary states once they are there: rename, reorder, recolour, add and delete them in the field config tray. Handbook: the `workflow` page's Config section names the defaults and the empty-array refusal. Gates: `test/workflow-defaults.test.mjs` (8 cases: engine, CLI, REST and MCP parity, dialog round-trip, and the dialog/engine/Handbook parity), `test/workflow-defaults-browser.test.mjs` (4 cases, both themes). - **Grid ranges: shift-select cells, drag-fill, and copy/paste values** (Feature #220). Kyle: "I need a better way to select, drag to duplicate, copy or paste values, like single or multi select dropdowns, across cells." #134 made the cell rest as a value and #132 gave a run of ROWS one write; both stop at the row, so copying one Status onto the next four meant reopening a dialog and re-picking. A rectangle of cells is now a **range**: `⇧` and the arrows grow one, or the pointer drags one, and `Esc` lets it go: rows first when both are up, so Space and `⇧↑↓` still mean exactly what they meant. The blue square on the range's bottom-right corner is a **fill handle**: drag it down a column or across a row and the range's values are written over everything it covers, one axis at a time. `⌘C` puts TSV on the clipboard for a spreadsheet and the same block typed for another weave tab, which is what makes a select paste as an option rather than as a word; `⌘V` tiles the block to fit whatever it lands on, so one copied cell fills twenty. Select, multi-select and workflow paste by **option identity**: the id carries over within a table, the label goes into a different one, and a label that names no option there is refused rather than invented. A multi-select is a replacement, never a merge. A formula, rollup, lookup, view, relation or document column takes no paste: the cell refuses and the toast names the column, following `bulk`'s precedent of saying what did not land. Text from a spreadsheet is read in each column's own terms (`false` is false, not `Boolean('false')`), and a cell that will not read is dropped and counted while its neighbours land. The write is the existing `bulk set`, one call per distinct set of values (so a fill down twenty rows is one write), and the toast carries an **Undo** that steps the whole thing back. `bulk set` gained `changed` beside `done` for that: a row that already held the value pushes no undo entry, and counting the rows it merely visited would step past this write into somebody else's. Not built: `⇧`-click, which stays the browser's "open a window" on every navigating surface (`023b777`); the range comes from `⇧`-arrows and the pointer drag instead. Handbook: "Ranges, fill and paste" under the grid guide. Tests: `test/grid-range.test.mjs` (36 pure), `test/grid-range-browser.test.mjs` (13 browser), plus 6 keymap cases and 2 engine cases. ## v0.4.15 (2026-09-09) - **A test that fails once gets asked again** (Issues #44 and #239): `npm test` runs through `scripts/test.mjs`, which re-runs the files that failed (only those) and lets the second answer stand. Sixty-eight of the 192 suites drive a real Chromium, and under the full run a wait for a menu to paint can lose to a thirty-second budget: a different browser case failed on roughly every other full run while the same file alone was green eight times out of eight, and each one cost a Verified −1 on a change that was fine plus a re-gate to prove it. A regression is not a coin toss, so a broken file fails both times and the vote stands; the retry can only rescue a failure that did not reproduce. Nothing green pays for it: there is no second run on a passing suite. The flake is not absorbed either: every retried file is named on stdout under `# RETRYING` and `# RETRIED`, inside the `tail` that `weave-review.sh` quotes into its Verified +1, so a suite that keeps needing the second chance says so in the review that passed it. Capping browser concurrency was the other candidate and the measurements refused it: the 68 browser suites take 110 s at `--test-concurrency=4` against 66–70 s at 10, 16 and 24, and were green at every one of those over eight runs. `npm test` is now the one command in the README, CONTRIBUTING, AGENTS.md, the PR template and the GitHub matrix. Gates: `test/test-runner.test.mjs`, `test/extras.test.mjs`. ## v0.4.14 (2026-09-09) - **The field dialog can write a space rollup** (Issue #222): a rollup over a whole table (the Σ under a grid column) is a field on the `Workspace/Spaces` row of the space that holds the table, and `via` names the table instead of a relation to cross. Three surfaces wrote one: the Σ footer picker with a switch, `weave field add --config`, and `weave_add_field`. The dialog offered only the registry's own relations, so `+ field → rollup` on the Spaces grid could produce a count of `Tables` or `Workflows` and nothing else. Its rollup section now asks **Rolls up**: *Through a relation* or *Over a table*, on the Spaces registry, the one place the engine accepts `via`, and *Over a table* picks the table, the aggregate and the column. Every other grid's rollup section is untouched, because `via` is refused there. Reopening a space rollup also folds back onto its table and keeps its `where`, where before it claimed an empty relation and dropped the filter. Writing a filter stays API-only. Gates: `test/field-dialog-core.test.mjs`, `test/space-rollup-dialog-browser.test.mjs`. ## v0.4.13 (2026-09-08) - **The registry lives at the weave root** (Feature #219): the system registry (Spaces, Tables, Fields, Workflows as rows) exists once, in the Workspace space of the default workspace, instead of being minted inside every workspace file. The root grows a `Workspaces` table (one row per workspace the hub serves, the level-1 row `docs/ONTOLOGY.md` said was missing) and every registry table gains a system `Workspace` relation, so uno's tables and test's sit side by side under one column. A member workspace joins the root on adoption: its sidebar shows only its own spaces, its structure is projected as root rows its own structural verbs keep true, and a row edit at the root routes to the engine that owns it. A legacy per-workspace `Workspace` space is tombstoned (rows kept) and its space rollups move to the root Spaces table, where `via` now names a table in any workspace: the Σ row and the space tiles keep working in members. Registry rows deep-link into their workspace; a member's `/w//api` answers for its registry rows. The Worker's single-workspace hub is its own root, unchanged. Handbook: **Registry at the root**. Gates: `test/root-registry.test.mjs`, `test/root-registry-hub.test.mjs`, `test/root-registry-routes.test.mjs`, `test/root-registry-browser.test.mjs`. - **A grid has no Σ row until someone asks for one** (Issue #249): Kyle filed "hide summation row by default" from a uno table. The Σ row shipped in Issue #233 as on-unless-hidden, and the flag behind it stored only the hiding: `hideRollups: true` meant off and the absence meant on. Flipping the default on that shape would have hidden the rows Kyle had switched on himself, because a table he opted into looked exactly like one he had never opened. So the flag now says both things: `false` is a table that asked for the row, `true` is one switched back off, and the absence means nobody has asked, which reads as off. Nothing stored needed rewriting: `true` was off before and is off now, and every absence was on before and is the new default. The eye's **Σ rollup row** switch is the way in, and it writes both directions; the **Hide Rollups** checkbox on the Tables row reads checked for a table nobody opted in, which is what it now shows. Gates: `test/table-config-fields.test.mjs`, `test/footer-rollups-browser.test.mjs`, `test/ui-contract.test.mjs`. - **A JSON export carries its attachment bytes, and a lost blob says so** (Issue #121): a file-backed workspace keeps attachment blobs in a sibling `files/` directory, outside `state`, and `exportJSON()` was a deep clone of `state`, so a dump named every file and carried none of them. `weave export --out dump.json` then `weave import --file dump.json` into another data directory left every attachment as metadata pointing at nothing: the row still offered a link, the link answered raw 404 JSON, and the reporter could only ask "file missing?". The export now walks each entity's file ledger and the workspace logo, reads those blobs and carries them as base64; `importJSON` (and opening a `.json` dump as a workspace, which is an import by another name) writes them back into `files/` and drops `fileBlobs` from state, so no base64 rides a save into `weave_meta`. A blob already gone is simply absent: an export must not fail on damage it did not do. `weave export` and `GET /api/export` always carry the bytes; MCP's `weave_export_json` is a reader rather than a backup and omits them unless asked (`blobs: true`). The other half is the loss itself: `readEntity` marks a file whose blob is gone with `missing: true`, the dressed cell reads `c-json-editor.html (missing)`, and the entity page and the grid cell drop the anchor and draw the name dimmed in a dashed chip. No new colour, and the × stays, so a dead pointer can be cleared. Gates: `test/file-blobs.test.mjs`, `test/attachment-missing-browser.test.mjs`. ## v0.4.12 (2026-09-08) - **A fresh install applies its whole manifest** (Issue #245): the shipped `docs/development.json` carries Feature rows whose `Milestone` the seeded select stops short of (`v0.4`, `v0.5`), and a select validates on write, so `syncDevelopment` threw on the first such row. The throw landed mid-pass: Issues applied, Features applied up to that row, the release block never reached, and the stamp, written last, never set, so every boot repeated the same half-pass behind `bin/weave.js`'s bare `catch { /* no manifest in this build */ }`, a comment naming the one thing that was not happening. `widenSelects` now appends the values the manifest carries to a select or multiselect before any row is applied, append-only, so existing option ids and hues survive along with the rows holding them. Each row's writes are guarded on top of that: a row the workspace still cannot take costs that row rather than the pass or the release block behind it, and is named in the returned `skipped`; only a whole pass writes `meta.developmentSync`, so a skipped row comes back on the next boot. The boot catch prints what it swallowed. A freshly seeded workspace fed the shipped manifest went from a throw, no stamp and 0 Release rows to 430 rows created, 12 updated and 11 releases with their contents. Gate: `test/development-sync.test.mjs`. ## v0.4.11 (2026-09-08) - **A corrected manifest can take back what a wrong one claimed** (Issue #244): fixing v0.4.5's `Fixes` in the canonical workspace and re-exporting `docs/development.json` repaired the canonical record and every instance that had yet to sync. The one instance that synced between the mistake and the correction kept the false claim, and would have kept it forever: `syncDevelopment` applied the manifest additively, so `w.link(id, 'Fixes', …)` added the nine watcher rows and nothing ever removed them, however few targets the next manifest named. A release row is manifest-owned in a way an Issue row is not: no instance links its own rows into an upstream release, so its `Fixes` and `Ships` are reconciled to exactly what the manifest names, added and unlinked both. Issue and Feature rows stay additive, which is what protects a locally filed row through an update. Reproduced on a fresh workspace fed the 2026-09-05 manifest and then the corrected one: the old code held 13 fixes with 9 watcher rows among them, this one lands on the 4 the manifest names. Gate: `test/development-sync.test.mjs`. ## v0.4.10 (2026-09-08) - **A route that fails says so, instead of holding the skeleton** (Issue #118): opening uno's Project table left the page on its loading skeleton and never took it off. The grid's `POST /tables//query` answered 500, `showDatabase`'s await rejected, and nothing above it caught: `paintSkeleton` had already replaced `#main`, so the page sat holding the skeleton of a table that was never going to arrive. The hole belonged to every route rather than to the grid: `showEntity` was the only one that caught at all, and it caught by falling home, which says nothing about what broke. `paintRouteError` now names the failure in the server's own words and carries the retry where the skeleton used to wait, and both entry points end in it: `renderRouteSafely` wraps the hashchange render (through `Promise.resolve().then`, since `renderRoute` may return a promise, a value or nothing, and may throw before it returns) and boot's first render gets the same catch. The card is Tabler tokens only, so it reads in both themes. The 500 itself no longer reproduces on main: its cause was a rollup or lookup whose target field had been deleted, which made `#resolve` throw on every read that reached the row, and `ae1f150` fixed that (Issue #206); uno's Project table still carries the scar in its description, a "Total Estimate" rollup over a Task estimate that no longer exists. Verified on a copy of the live uno workspace: the pre-fix build painted 83 skeleton elements and no error state, this one paints the error card and none. Gates: `test/route-error-browser.test.mjs`, `test/page-loader.test.mjs`. - **v0.4.5 no longer claims nine fixes it never made** (Issue #229): the main watcher files a `gerrit/main @ did not reach GitHub` row every time its mirror push is refused, and a closing sweep on 2026-09-05 linked nine of those rows `Fixed in → v0.4.5`. That release merged the same morning and touched none of them: the mirror failures ended when `gh`'s active account went back to `grunion-ai`. The nine are unlinked from the release's `Fixes` and stay closed; v0.4.5 carries Issues #134, #83, #133 and #232 with Features #175, #190 and #195, as it always did. The manifest is gated on it now: no release in `docs/development.json` may name a watcher incident row among its fixes. Gate: `test/development-sync.test.mjs`. - **One box scrolls on the table page** (Issue #136): a grid wider than its card scrolls in a box of its own (Issue #233), and that box claimed half the screen whatever stood above it. On a short window the chrome, the half-screen box and the gutter under it added up past the viewport, so the page scrolled around a box that already scrolled: the two nested bars Kyle filed, where the inner one runs out and the outer one takes over. The box is cut to the room left below its own top now, and where that room falls under 120px the page takes the whole scroll back: one box moves either way. The cut also follows the box while the chrome above it is still arriving: a description that lands after the grid moves the box down, and the first measurement is then a box too tall for the room. Measured on the Issue table at 1470×500, the page overflowed the window by 74px and now overflows by none. Gate: `test/grid-scroll-region-browser.test.mjs`. - **One Suite row per test file, enforced** (Issue #95): the Quality mirror is generated from `test/`, and the Suite table says "One row per test file", but two files had grown a second row each (`applet` alongside `Applet`, and two `Structure trash` rows), and no amount of syncing removed them. `syncQualityMirror` keyed the workspace's existing rows into a `Map` by File, so a second row with the same File overwrote the first and the loser was then invisible to both passes: never renamed, never reconciled, never pruned. The rows are grouped by File now instead of overwritten, the oldest row wins (keeping the id anything already links to), and every twin is pruned by the same pass that clears a suite whose file left the tree. `weave quality check` therefore reports the drift it used to skip: it was returning `removedSuites: 0` against a live mirror carrying 182 rows for 180 files. Gate: `test/quality-mirror.test.mjs`. - **The eye's rows are taught, not swapped** (Issue #240): flick two switches quickly and the second one did nothing: no movement, no column change, and focus left on `` so Escape stopped closing the popover. A flip's tail is asynchronous (PATCH, schema reload, redraw, then the rows), and under load nothing bounds it, so it could land between a mousedown and its mouseup; `replaceChildren` then detached the row the mousedown had focused, and a down and an up on different nodes fire no `click` at all. `relearnRows` now copies the new switch state onto the rows that are already there, keeping the node, its focus and `showPopover`'s arrow walk, which captures its row list once, at open, and rebuilds only when the row set itself changed, where Issue #223's focus restore still applies. Each row handler reads the table at click time, since a taught row keeps the handler it was built with. The Σ footer picker shares the tail and the fix. Gates: `test/eye-live-rows-browser.test.mjs`, `test/ui-contract.test.mjs`. ## v0.4.9 (2026-09-07) - **Autocomplete in the formula itself** (Feature #207): in the script box, `[` offers this table's readable fields and two letters offer the functions that start with them, in a popover positioned at the caret (a mirror div wearing the textarea's metrics, no editor dependency), drawn from the same two lists the chips use and ranked by prefix. ↑ ↓ move, Return picks and lands a call's caret between the parens, Escape closes the list without closing the tray. `formulaSuggest()` / `formulaApply()` in `public/field-dialog-core.js` are the pure half. Gates: `test/field-dialog-core.test.mjs`, `test/formula-builder-browser.test.mjs`. - **A formula preview you can steer** (Feature #206, Issue #237): the check's verdict names the result `type` (number, text, boolean, list, null, error), and `scan: true` (`--scan` on the CLI, `{scan: true}` on `weave_check_formula` and `POST /api/tables/:id/formula-check`) evaluates over up to 200 rows for `{rows, capped, nulls, errors, sampleByOutcome}`, the figures an agent asserts against before saving. The dialog wears the type as a badge, steps the preview through the rows with `‹ row n of N ›`, reports `⚠ n rows → null · m → #ERR`, and shows the **Result format** section only for a numeric result. The agent-surface gate now reads `checkFormula`'s signature so an option missing from any door fails the build. Found on the way: `dateadd()` on an empty date returned the epoch shifted, not null (Issue #237). Gates: `test/formula-check.test.mjs`, `test/agent-surface.test.mjs`, `test/formula-builder-browser.test.mjs`, `test/formula.test.mjs`. - **The formula dialog prints its own API** (Feature #205): a collapsed **As an agent would do it** footer under the script shows the CLI lines for what is being built (`weave formula check`, `weave field add`/`update` with the expression, `weave get`) and the MCP sequence `weave_check_formula → weave_add_field → weave_get_entity`, following the name box and the expression as they are typed. `agentRecipe()` in `public/field-dialog-core.js` is the string builder (shell-quoted, placeholders for what is not typed yet). Gates: `test/field-dialog-core.test.mjs`, `test/formula-builder-browser.test.mjs`. - **Formula chips that teach** (Feature #204): the builder's function chips sit in the four groups the grammar has (logic, text, number, date), and hovering, focusing or tapping a chip shows a card with the signature, one sentence of doc and an example, in the DOM rather than a title tooltip. Document and attachments fields are listed greyed with the reason instead of vanishing. Every `FORMULA_FUNCTIONS` entry carries `group`, `doc` and `example`, and `weave_vocabulary` / `weave vocabulary formulaFunctions` / `GET /api/vocabulary` serve the catalog verbatim. Gates: `test/field-dialog-core.test.mjs`, `test/vocabulary.test.mjs`, `test/formula-builder-browser.test.mjs`. - **A text field can be literal** (Issue #86): `{ "literal": true }` on a text field's config (the dialog's **Literal** box, or `--config` from the CLI/MCP) paints the characters as typed instead of dressing inline markdown, so a column of syntax, a regex or a glob reads as written. Gates: `test/text-literal.test.mjs`, `test/text-literal-browser.test.mjs`. - **A date box is as wide as its date** (Issue #159): the date input was a fixed 120/200px box, and an ordinal costume ("Wednesday 30th September 2026") was cut off inside it. The box now measures its text in its own font and grows to it; the stylesheet width is the floor. Gate: `test/date-box-browser.test.mjs`. - **The clipped-cell expansion opens for a resting pointer** (Issue #67): crossing a row of clipped cells flashed each one in turn; a cell now expands after the pointer has rested on it for 180ms, and a pointer that leaves first opens nothing. The design questions the Issue left open are answered in its note. Gate in `test/grid-chrome.test.mjs`. - **The grid-tab-focus suite waits for the cursor, not for two animation frames** (Issue #199): the first assertion failed about once in six runs, reading `{ eid: null, field: null, tag: 'BODY' }` where the tabbed cell belongs. It settled and then read focus in a second round trip, and the redraw lands between the two: replacing the `

` drops focus on `` until `restoreGridFocus` puts the cursor back a frame later. A per-frame trace of the gesture shows the dip plainly: `TD/Tail` → `BODY` → `TD/Tail`, 16ms apart on an idle machine, and unbounded on a loaded one. The wait now reports the cell it settled on from inside the same evaluate, so no frame separates settling from reading, and the "mark gone plus two animation frames" read is gone with it. Every wait in the suite also carried a 5s cap while the suite itself has taken 52s under the full gate (`review-logs/227-1`) and 27s (`282-1`); the caps are gone, and a case that holds the redraw's query open for 5.5s keeps them gone. Tab out of a resting cell still reads focus without waiting, because it writes nothing and nothing is in flight. Gate: `test/grid-tab-focus-browser.test.mjs`. - **The appears-as browser suite waits for the eye's round trip, not for a sleep** (Issue #216): `test/appears-hidden-browser.test.mjs` timed out on two of four full-gate runs while passing every time on its own. A flip is a PATCH, then the page's redraw, then the popover teaching its own rows the new state, and the helper waited only for the popover to close plus 400ms, a margin the tail clears by 20ms when the machine is idle and misses under a loaded gate. A rebuild landing inside the next flip's click orphans the mousedown, so no `click` fires and focus falls to ``, where Escape can no longer reach the popover: it stays open and the wait burns its full 30s. The helper now waits for the switch itself to read the new state, which is the last step of that round trip, and the sleep is gone. `test/entity-eye-sync-browser.test.mjs` already waited on that signal but capped it at 5s, and the same round trip took 8.7s on a loaded gate; the cap is gone too. - **A writer token cannot un-trash a table** (Issue #149): `POST /api/tables/:id/restore` joins move and duplicate in the schema-write gate. Gate in `test/table-move-duplicate.test.mjs`. - **The outline opens in place** (Issues #131, #144): clicking the dash rail used to float the headings in a fixed panel at the viewport's middle, so the outline "snapped" away from the dashes just clicked. The panel now widens the minimap where it is (same sticky anchor, same left edge), and the inline x-pin that fixed positioning needed is gone. Browser gate: `test/doc-rail-browser.test.mjs`. - **A sniffed diagram draws, a sniffed model edits as code** (Issues #188, #189): an undeclared document field holding mermaid source or a JSON model went to the markdown editor, which drew the source as paragraphs. `docViewMode` now returns `diagram` for mermaid (drawn by the one `renderMermaidIn` every fenced block uses, with the app's `` source toggle) and `code` for JSON (the monospace code box). The mermaid sniff wants a diagram header (`graph LR`, `flowchart TD`, `pie title …`, a kind on its own line), so "graph theory, a primer" stays markdown. Browser gate: `test/doc-kinds-browser.test.mjs`. - **The slash menu is a list to scroll, not a sheet** (Issue #137): the inline clamp let the menu grow to the whole viewport; it is now capped at 400px with tighter rows, and a wheel over the menu stops at the menu's edge instead of chaining to the page: the window scroll that Vditor answers by closing the menu ("it disappears when trying to scroll"). Browser gate in `test/slash-commands.test.mjs`. ## v0.4.8 (2026-09-07) - **A server older than its own checkout says so** (Issue #114): static assets are read from disk per request and `src/*` is loaded once at boot, so a checkout that moves under a running process serves new client code against an old engine: on 2026-08-28 that made + New and Shift+Enter fail silently for three minutes, with nothing on screen naming the mismatch. `/api/health` now carries `diskSha` beside `sha` and the verdict `stale`, read for real (throttled to 5s) rather than inferred: the existing `behind` cannot stand in for it, since that compares the boot commit to the *remote*, arrives lazily or not at all, and its remedy is a pull rather than a restart. The instance chip in the bottom-right wears the danger tint with both commits, and a toast on load names the running commit, the one that served the page, and the restart. `weave service status` reports the same verdict: its old check compared version strings, and most commits do not bump the version. - **Import carries every column across** (Issue #110): a table's columns are read from `fieldOrder` everywhere they are listed: the grid, `describeSchema()` and so `GET /api/schema`, the CSV header, the phone applet, while the fields themselves live in `db.fields`. An order that had forgotten a field left that field present and invisible, and `importJSON` copied the order across verbatim, so a plain `number` column arrived on the far side of `GET /api/export` → `POST /api/import` with the same table and entity counts and no column. Opening a workspace and importing one now reconcile the two: an id naming no field goes, a field the order forgot comes back on the end, and the repair is written once rather than re-derived on every open. - **One scroll moves one box** (Issue #69): `Element.scrollIntoView()` is defined to scroll every scrollable ancestor of its target, so jumping to a heading in a docked document dragged the page behind the dock along with it: the reset Kyle reported against the outline rail. The four programmatic scrolls (the outline rail, the editor's slash menu, the search palette, a new grid row taking the caret) now go through `scrollTargetIntoView()`, which resolves the one box that may move and leaves every other scroller where the reader left it. The rail's jump became a glide, and lands the heading on the same reading line its tracker measures from; a reader who asked for less motion, and a hidden tab that would never run the animation frames, still get the instant landing. `keepScroll()` holds every box the reader had scrolled instead of the grid's alone. The arithmetic (which box, and where its scrollTop lands) is pure, in `public/editor-lib.js`. ## v0.4.7 (2026-09-06) - **A name that runs past its column expands on hover again** (Issue #157): the marker that opens the hover expansion was measured on the `
`) and the record's attachments box while a drag carrying files is over it: an inset 2 px ring in `--tblr-primary` over an 8% primary tint, so nothing moves (the test measures the cell's box before and during the drag, and it is identical). The drop uploads every file, in order, through the field's files route, then re-reads the row once, and the cell names the new files. **+ file** and the drop now call the one `upload()` in `editorFor`; the button's own FileReader copy is gone. A drag whose types do not include `Files` (text, a column header, a row or block being moved) is left alone: no ring, and its events keep their default. A depth count keeps the ring on while the pointer crosses from the cell onto the chip inside it. The empty-cell half had already been fixed: on the day of the report the empty file chip wore Tabler's global `.empty` and stretched its row, which Issue #93 renamed to `is-empty` that afternoon. No gate held a file column to it until now: an empty file cell's row measures 47 px, the same as a full one, in both themes. Handbook: the `attachments` field page says how to drop files. Gates: `test/attachment-drop-browser.test.mjs` (new, 10 cases: in both themes a synthesized file drag over an empty grid cell is accepted, lights it with no change to the cell's box, and the drop answers 201, names `brief.txt` in the cell and leaves the bytes on the server; the record's attachments box takes a dropped file; an empty file cell's row is as tall as a full one and its chip is dashed and never `.empty`; dragleave off the chip clears the ring while crossing onto it does not; a text-only drag is not taken and draws no ring; three dropped files all land; **+ file** still uploads). - **A column's sort reads by type, and the system columns sort** (Issue #254, with Issues #273 and #318): every ⋮ offered "Sort ascending" and "Sort descending", which meant A to Z on a name, smallest first on a number and oldest first on a date, and a select or a workflow sorted its option and state names alphabetically. The system columns had no sort at all: `Created At` drew a header with no ⋮ (Kyle clicked it six times on 2026-09-12), the `#` column had none either, and `PATCH /api/tables/:id { sort: [{ field: 'Created At' }] }` was refused because `updateTable` checked the name with `getField()`, which knows only the table's own fields. The stored direction stays `asc` / `desc` (Kyle, 2026-09-09). `fieldDialogCore.sortLabels` words the two rows from the field: Oldest to newest for a date or range, Smallest to largest for a number or a numeric rollup, A to Z for text, url, email and key, Option order for a select and State order for a workflow, and plain Ascending / Descending for a multiselect, relation, checkbox, toggle and the rest. The engine's `#pathValue` sorts a select and a workflow by where the value sits in the definition (`#definitionRank`, unknown values after every listed one, empty cells still last) and reads `Created At`, `Modified At`, `Created By`, `Modified By` and `Public Id` off the entity after the table's own fields, so a field of the same name still wins; `updateTable` accepts those five names (`SYSTEM_SORT_KEYS`) and still refuses Activity. The grid mounts a sort-only ⋮ on the `#` header and on each system header but Activity, and its own sort for a whole table mirrors both rules. On a table sorted by `Modified At` or `Modified By`, every cell commit takes the full re-read rather than the in-place patch (Issue #257's `gridMoves`), because every edit rewrites the stamp and the edited row belongs at the top. Multiselect and relation columns keep sorting by their painted names, joined. Gates: `test/sort-labels.test.mjs` (4, every engine type through `sortLabels` and the system names pinned to the engine's), `test/sort-values.test.mjs` (+5: option order, state order, multiselect and relation unchanged, the five system sorts stored and served, Activity refused and a same-named field winning), `test/sort-values-browser.test.mjs` (+4: Created At from its ⋮ in light and dark, the grid's own sort of a system column and of #, an edit deep in a Modified At sort lifting its row to the top), and `test/ui-contract.test.mjs`, `test/field-menu-browser.test.mjs`, `test/row-selection-browser.test.mjs`, `test/cell-commit-patch-browser.test.mjs` moved to the new wording. - **The release order names the tag and the GitHub Release** (Issue #253): DEVELOPMENT.md's Releasing section and CLAUDE.md rule 5 listed the Release row, the bump, the export and the CHANGELOG digest, and stopped at landing through Gerrit. Neither said that a bump reaches users only as a `v` tag and a GitHub Release, so tagging stopped at v0.4.4 on 2026-09-02 and an install on 0.4.4 read as current while main was at 0.4.15. Publishing moved to the harness main watcher (`scripts/weave-release-tags.mjs`, after the GitHub mirror), which tags the commit that introduced each untagged version and publishes a Release titled `weave v` from its `## v` CHANGELOG section. The docs now carry that as step 5: what the watcher does on its own, that the digest heading must be `## v` or the version is skipped and filed as an Issue, and the check that stays with whoever landed the bump (`gh release list -R grunion-ai/weave` names it as Latest the same day, with `--dry-run` on the tag script to see what is owed). A running instance still cannot tell its user a newer release exists; that half is left open. Gates: `test/development-sync.test.mjs` "the written release order ends with the tag and the GitHub Release". - **The table beside the dock keeps 320px at every window and every pin** (Issue #326): Kyle, 2026-09-20, at 1470x794 in Safari with a Sessions row docked: "left panel needs minimum width to avoid responsive break". The left panel is the table pane (`#main`). A dragged divider stored its width and `applyDockWidth` put it back as a fixed `width` with `flex: none`, clamped only at drag time against the window of that moment, and `#main` carries `min-width: 0` so a wide grid never pushes it. A dock pinned at 900px on a big screen therefore came back at 900px on a smaller one and left the table 230px at 1470, and 56px at 1100 with 188px of sideways page scroll. The pin is now a shrinkable flex basis (`flex: 0 1 `) set by one function, `pinDock`, which the drag, the stored-width apply and the double-click reset all call, and a docked `#main` has a 320px floor in the stylesheet while the dock's own floor drops to 0. The browser does the clamp on every layout, so a window resize or opening the nav needs no listener, the dock is the panel that yields, and nothing spills into a sideways page scroll. The stored pin is never rewritten by a small window: only the drag's release writes it, storing the width it shows floored at 360px (the smallest pin an open keeps), so an over-drag stores what the table left, a drag on a window too narrow for a 360px dock still leaves a 360px pin, and widening the window gives the pinned width back. Measured in Chromium with a 900px pin: 1470 now reads table 320 / dock 810, 1100 reads 320 / 440, 1600 keeps 360 / 900; the even split at 900 reads 320 / 240 where it read 200 / 360 before. Gates: `test/entity-dock-resize-browser.test.mjs` (+8, light and dark: a 900px pin at 1470 and 1100 leaves the table 320px with no page overflow and the pin untouched; a window narrowed to 1000 and widened to 1600 gives the 900px dock back with the pin unchanged; an over-drag stops at the table's floor and stores the width shown; a drag at 1000, where the dock is squeezed to 340px, stores a 360px pin that a reopen at 1600 honours). - **A centred dialog holds the keyboard and the page behind it** (Issue #263, with Issue #158): the 2026-09-12 audit counted ten hand-rolled dialogs and found none that held focus. In `modal()`, Tab from the last button walked out into the workspace rail and the grid behind the backdrop, a control behind it could still take focus, and nothing told a screen reader a dialog had opened. A new `holdPage(back, box)` in `public/app.js` makes the box `role="dialog"` with `aria-modal="true"`, labelled by its `

`, marks every other body child `inert` (Chromium 102 and Safari 15.5 on) except the toasts and the problem reporter, which float above dialogs on purpose, and keeps Tab and Shift+Tab cycling inside the box; a Tab from the body lands on the first control. Popovers a dialog opens are appended after the hold, so its pickers stay live. Callers often close a dialog by removing its backdrop (Restore in the workspace trash, a definition's Clear, `tray()` opening over it), so the page comes back on the removal itself and focus that fell to the body returns to the opener; a modal replacing a modal keeps the page held. `modal()` and the Column stats panel (the second `#modal-back`) both go through it. The New workspace dialog (Issue #158) was already `modal()`; its input drops the inline `width:100%` and wears the modal's own input styling. The other dialogs the audit counted (the tray, the full-screen viewer, ⌘K) still need the same hold, and the popovers are not dialogs. Gates: `test/modal-trap-browser.test.mjs` (New workspace is a labelled modal dialog with a style-free input, Tab from the last control wraps to the first and Shift+Tab from the first to the last, the page is inert while open and Escape frees it and returns focus, a removed backdrop frees the page while a replacing modal keeps it held; light and dark) and the Column stats case in `test/footer-rollups-browser.test.mjs`. - **A second click on a calendar button or a picker's trigger closes it** (follow-up to Issue #320): the agent that fixed #320 found two more copies of the loop it took out of `showPopover()`. `datePopover()` and `searchPicker()` each hang their own outside-click listener in the capture phase and counted the trigger as outside, so a second click on the date's or the range's calendar button, a select or workflow chip, a multiselect box, a picker-style select face or the icon button closed the dialog and that same click's own handler opened a fresh one: it never closed. A click on the anchor now closes its dialog and stops there, so each trigger toggles; a multi picker commits what was staged on that click, as on any other way out. A dialog already closed by Escape or a pick only unhooks its listener and lets the click through, so the trigger opens on the first click after either, and another trigger still switches in one click. The value box (`valuePop()`), the dots menu, the document outline, the bug panel and the context menu were checked and do not loop: the value box is a later step anchored to a button that opens a different first step, and the others either run in the bubble phase with a toggling trigger or already count the trigger as inside. Gates: `test/picker-toggle-browser.test.mjs` (new, 16 cases: a second click closes and stays closed and a third opens, for the date button, the range button, a select chip and a multiselect box; Escape and an outside click still close and the trigger reopens after either, for the date button and a select chip; each in light and dark; the date's button switches from the range's dialog and the select chip from the multiselect's picker in one click; a pick closes the calendar and the next click opens it; the closing click on a multiselect box commits the staged tag). - **The table page's header stays on screen** (Issue #321): Kyle, 2026-09-18: "some times table title and description stays visible onsite Coll sometimes not, should always stay visible with breadcrumbs and upper right toolbar". Which he got depended on the grid's width. A grid wider than its card scrolls inside its own wrap (Issue #233), so everything above the wrap held still; a grid that fits scrolls the page, and `.view-header` was not sticky, so crumbs, title, description and toolbar all went off the top and the column header stuck to the viewport's edge. Measured on a 600-row fitting grid at 1470x900 scrolled to 8,000px: the block's top sat at -7,968px. The table page's header block is now pinned (`.view-header.wv-pin`): it reaches over the panel's padding with an opaque surface, rests 12px up, and sits above every sticky layer of the grid. `pinViewHeader` measures the block's bottom edge into `--wv-pin-h`, and a page-scrolled grid sticks its column header there, with the Σ row one header row below and the frozen # column on the same line; a wrap that scrolls itself keeps its sticky cells at the wrap's top. The row window reads the view's top from the column header's sticky offset, so a row brought into view from above (`wvScrollToRow`, the keyboard's moves) lands under the column header, not under the pinned block, and the anchor for the direction of travel moves with that edge, so a description arriving late is not read as a row of travel (which prefetched a second page at open). `wvKeepPlace` already read the line off the sticky cells and needed no change. An opened description, or its editor, is capped at a fifth of the viewport (never under five lines) and scrolls inside the block: a 60-paragraph description opened to a 1,324px block before, and the pinned block now stays under 45% of a 900px viewport. A block taller than half the window lets go (`.wv-pin-loose`) and scrolls with the page as before, re-checked on resize: pinned in a 200px window, it covered the rows so a row's checkbox could not be clicked, which is what `test/row-selection-browser.test.mjs` caught. After the change, at the same 8,000px, the block rests at -12px and the column header at its bottom edge, in both themes, and with the dock open. Gates: `test/sticky-view-header-browser.test.mjs` (new, 8 cases: a page-scrolled grid far down keeps crumbs, title, description and toolbar on screen and on top at one resting position, the column header, Σ row and frozen # head directly under the block, a body row directly under them, and a row scrolled to from above landing right under the column header; an opened long description capped with the rest scrolling inside it; in a 200px window the block scrolls away with the page and a row's checkbox takes a real click; a wrap-scrolled grid unchanged, the page still and the column header at the wrap's top; each in light and dark); `test/grid-density-place-browser.test.mjs` (the page-scrolled flip is now a real click on the pinned toolbar); `test/sticky-add-row-browser.test.mjs` and `test/footer-rollups-browser.test.mjs` (the column header holds under the pinned block, not at the top edge); `test/grid-window-browser.test.mjs` (the cell-commit case picks its on-screen row below the sticky cells); `test/view-desc-clamp-browser.test.mjs` (Show more opens past five lines, and on the table page the rest scrolls); `test/grid-bottom-bounce-browser.test.mjs` (comment only). - **A code block's markdown sits behind a button** (Issue #96): Kyle, 2026-08-26, on a Description holding a mermaid diagram: "move show raw formatting behind a code icon in the upper righthand corner of the code panel and use this to hide and show". His trace is six clicks on the diagram. Vditor's IR mode expands a fenced block into its markdown the moment the caret enters it: the opening and closing fences (drawn as the node's `::before` and `::after`), the language line, and for a diagram the source in place of the drawing, since weave hides the rendered copy while a block is expanded. Every click on the diagram turned the picture back into text. Now the block the caret is in keeps looking like itself: a code block shows its code with no fences or language, and a diagram keeps its drawing. A new `attachCodeRawToggle()` floats one button (`lucide:code-xml`, the icon the document head's Edit source toggle already wears) over the upper right of that block's panel, beside the IR surface and never inside it, where Lute would serialise it. The button toggles `.wv-code-raw` on the editor host, and style.css shows the fences, the language and a diagram's source only under that class. `teardownDocEditors` stops each toggle (button removed, host class dropped, a queued pass disarmed) and clears the registry with the others. The state lives on the host, not the block, because Vditor rebuilds a block's DOM as it is typed in. It resets when the caret leaves the block, so every block opens with its markdown hidden. A keystroke into a diagram whose source is hidden opens the source first, so nobody edits text they cannot see. Typing in a code block with the fences hidden writes the markdown as before, and with them shown the language line edits the fence. Handbook: the Document formatting guide's Code and Diagrams sections name the button. Gates: `test/code-raw-toggle-browser.test.mjs` (new, 8 cases: in both themes a click into a ```` ```js ```` block leaves both fences and the language undisplayed, puts the button within 16 px of the panel's right edge and 12 px of its top with the icon at 3:1 or better on the slab, and one click shows the raw lines while a second hides them; typing ` // hi` with the fences hidden saves ```` ```js\nconst x = 1; // hi\n``` ````; with the source shown, typing in the language line saves ```` ```jsx ````; the raw view closes when the caret leaves; clicking a mermaid diagram keeps the drawing and the button swaps in its source and back; a keystroke into the hidden diagram source opens it and lands in the fence; no button survives a route change), `test/editor-refs.test.mjs` (`teardownDocEditors` clears `docCodeRaw` with the other decoration registries), `test/handbook.test.mjs` (the formatting guide names the **** button). - **A table page asks for the columns it draws, and each related row's chip once** (Issue #272): `POST /api/tables/Case/query` answered every field of every row, each row's comments, activity and files, and for a relation the far row's whole summary once per row that pointed at it, so a 200-row Case page carried the same forty suites two hundred times, each with its chip's segments (a suite's segments hold its inverse relation's names). The query takes two opt-ins. `fields: [...]` keeps the entity shape and cuts `fields`, `raw` and `docs` to the named fields; the stamps always ride, `Activity` brings the history its system column counts, and comments, activity and files are left out. `relations: 'chip'` answers a relation value as `{ id, publicId, name }` and sends each far row's summary once, in `chips`, keyed by id. The table page names its visible columns, its sort fields and its shown system columns, asks for chip-level relations, and grafts each summary back onto its references, so a relation cell draws the same chip, caret and segments included. A hidden field is not fetched: the eye's flip re-reads the page with it named. Neither option is on by default, so every other caller (the entity page, related grids, registry pages, `select`) keeps its shape. Both reach `POST /api/tables/:ref/query`, `weave_query` on MCP and `weave query --fields A,B --relations chip`. Measured on a seeded Case table (2,060 rows, a dozen fields, 40 suites with long descriptions): one grid page of 200 went from 1,654,711 bytes in 46 ms to 841,151 bytes in 30 ms, and the whole table from 17,082,120 bytes in 502 ms to 6,987,258 bytes in 214 ms. Gates: `test/query-fields.test.mjs` (new, 7: the cut shape, a named document and Activity, refusals, the chip references and the once-per-row summaries, the unchanged default, the grid page, and REST, MCP and CLI), `test/agent-surface.test.mjs` (every query option reaches every door), `test/grid-select-browser.test.mjs` (new: the page's query names its columns and asks for chips, the relation chip keeps its caret and segments in light and dark, the Activity column still counts, and a field shown from the eye is fetched and paints). ## v0.4.31 (2026-09-23) - **Table search in the toolbar** (Feature #228): Kyle, 2026-09-23, asked for a search box on the table page that searches only the current table and hides every row that does not match. A magnifier now sits first in the table's toolbar, left of the eye. Click it, or press `/` or `⌘F` on a resting cell, and it opens into a box that narrows the grid as you type (150 ms pause, the palette's). It runs the ⌘K matcher, scoped to the table: a name fragment, a `#143` id, or the words in a text field, a document or a comment. `Weave#search` gained a `table` scope and the table query a `search` parameter, both over one private matcher, so the palette and the box cannot drift apart; the matcher now also reads text, url and email values (a `key` stays out), which the palette gains too. The search runs on the server, so a paged table narrows across every page, in the table's own sort, inside its saved filter, and the foot reads `3 records found`. It is transient view state: in memory, one table at a time, never written to the Tables row, so nobody else's view moves. `Esc` clears and folds the box; `Return` on a single match opens that row in the dock; a miss reads "No records match" with **Clear search**; `+ New` clears the search first, since an empty new row matches nothing (the search's half of Issue #341). The route, `weave_query` (MCP) and `weave query --search` (CLI) all take the parameter. Not in this change: highlighting the matched text inside the cells. Gates: `test/table-search.test.mjs` (new, 7 cases: the scope, text-field matches in both entry points, composition with where, sort and paging totals, `#id` and bare id, empty and missed searches, the trash, route and MCP parity), `test/table-search-browser.test.mjs` (new, 8 cases on a 503-row paged table in Chromium and WebKit: typing narrows every page with no PATCH; a text-field value and a `#id` match; `Return` docks a single match; a miss and its clear; `Esc`; `/` and `⌘F` from a resting cell open the box without opening the cell; the saved filter composes and survives; `+ New` during a search shows the new row). ## v0.4.30 (2026-09-23) - **A clipped cell's expansion opens above it, never over the value** (Issue #346, superseding Kyle's #327): hovering a clipped cell for 180 ms drew a copy of its value in `.cell-pop-layer`, pinned so the copied text sat exactly on the live text (the 2026-08-26 over-the-value rule from Issue #93). The copy is `pointer-events: none`, so a click still reached the cell, but the inline editor, the caret and every typed character then sat under a static copy of the old value. Kyle, 2026-09-23: "it blocks click and edit, should show above the row/cell instead". `showCellPop()` now keeps the copy's content on the cell's content left edge and sets its bottom edge 4 px (`CELL_POP_GAP`) above the cell's top. When the room above inside the wrap's visible area, or the viewport, is shorter than the pop, it flips to 4 px below the row, so the first rows of a table still get an expansion instead of one clipped off the top. A `mousedown` (capture) or `focusin` anywhere in the grid takes the expansion down, and a cell whose own control holds focus opens none, so editing never runs under a copy. Kept from earlier fixes: min-width at the cell's width and the 380 px measure, the cell's typography on the copy (Issues #67, #93), the input-to-text swap that lets a long name wrap (Issue #157), the 180 ms rest delay, no column movement, and Esc through `DOCK_ESC_OWNERS`. Gates: `test/cell-pop-above-browser.test.mjs` (new, 5 cases: a clipped Name six rows down opens above its cell within 8 px, on the value's left edge, between cell width and 380 px, in both themes; the first row's pop opens below the row and inside the grid; a real mousedown takes the pop down and a pointer resting on the cell being edited opens none; focus moving into another cell hides it), `test/grid-chrome.test.mjs` (the over-the-value case rewritten: left edge kept, the copy never overlaps its cell). ## v0.4.29 (2026-09-23) - **The last row of a table holds still, and + New lands the caret there** (Issue #324): Kyle filed two complaints about one gesture, clicking "+ New record" at the foot of the Issue grid: "clicking new breaks scroll", and the new row should become an entry with the cursor in its Name field. One mechanism sat under both. A grid draws the rows in view and stands two spacers in for the rest, sized at ONE measured row height, and that height was re-read from whichever row happened to head the window, on every pass. weave's rows are not all the same height (the live Issue grid measures 46.5, 47 and 48), and the spacer above a window 250 rows down stands in for 250 of them, so a pixel of row-to-row variance became hundreds of pixels of page height. At the last row, where the scroll is already at its maximum, the box clamped to the new height, the clamp resized the wrap, and the `ResizeObserver` watching the wrap measured again: a three-position cycle roughly thirty times a second with no hand on the wheel. The report's own replay log carries the other half of that signature, 60 `ResizeObserver loop completed with undelivered notifications` errors in the 1.9 seconds before Kyle hit Report. The + New button under it never held still long enough to be clicked, which is why the row it makes never took the caret. The height is now measured once per table, density and width and then kept (`win.rowHAt` names what was measured), so a density flip or a resize measures again and a scroll never does. Measured on a copy of the live `weave.db` at the reporter's 2187x1359 viewport, in both themes, standing at the last row of the Issue table: self-scrolls over two idle seconds 60 to 0, positions in the cycle 3 to 1, and a + New click that Playwright abandoned after six seconds of an unstable button now lands with the caret in the new row's Name cell. Not in this change: a row born outside the table's active filter is nowhere in the grid's order, so + New on a filtered table still makes a row the reader cannot see, which is Issue #341 and Kyle's call to settle; and a density flip sending the reader back to the top of the table, which is Issue #342. Gates: `test/grid-bottom-bounce-browser.test.mjs` (3 new cases: the last row of a page-scrolled 600-row grid with uneven rows fires no scroll of its own over two idle seconds; + New there lands the caret in the Name cell and leaves the page still; a density flip re-measures, so the spacer stands in at the compact row's own height). - **The bottom of a table holds still** (Issue #317): Kyle hit the end of the Issue grid in Safari and the box bounced, once per wheel notch, for as long as he kept pushing. A grid's rows are not all the same height (the live Issue grid measures 46.5, 47, 48 and 50px), the spacers that stand in for the rows not drawn are sized at ONE measured height, and the window paints a double buffer behind the direction of travel. At the last row the leading edge is pinned, so a direction flip moves only the trailing edge: a buffer of rows swapped with a spacer that did not weigh the same, the content height moved 189px, the box clamped its own scroll to the new bottom, and that 27px clamp read as backwards travel, which flipped the direction again. The direction now comes from `WeaveGridWindow.travelFor`, which holds the direction and its anchor until the reader has travelled a whole row, so a correction the box makes for itself is not a gesture and a slow scroll still accumulates to a flip. Measured on a copy of the live `weave.db` in both themes, twelve notches at the last row: backwards jumps 12 → 0, content heights 11,377/11,566px → one, scroll positions 10,809/10,782 → one, resting on the last row. Not in this change: sizing the spacers from the real heights of the rows they stand in for, which is a row-height index and its own decision. Gates: `test/grid-bottom-bounce-browser.test.mjs` (new, 1 case: pushing past the last row of an 800-row grid with uneven rows travels backwards zero times and rests on the last row), `test/grid-window.test.mjs` (1 new case: a sub-row correction holds the direction and its anchor, a row of travel either way flips it, an unmeasured row height falls back to the density's own). ## v0.4.28 (2026-09-22) - **The icon grid keeps one width while the readout names what the pointer is on** (Issue #336): the picker popover was shrink-to-fit, and its widest content is the name readout Issue #142 put in the search bar. Hovering a long label (`tick · done · complete`) widened the box from 292px to 345px, the `auto-fill` grid reflowed into the new width, the cell slid 12px out from under the pointer, the leave event reset the readout to the shorter resting name, the box snapped back, and the pointer was over the cell again: a flutter that shook the grid for as long as the cursor rested there. `.picker-pop:has(.picker-cells)` now declares `width: 292px` instead of `min-width: 292px`, which also gives the `max-width: 50%` on `.picker-name` a definite box to measure against, so a long name ellipsises inside the row instead of pushing the row wider (a percentage max-width against an indefinite containing block never constrained intrinsic sizing, which is why the existing cap did not hold). The row-term picker carries no readout and opts back out with `width: auto`, so it still sizes to its widest row. Verified on a copy of the live data in both themes: width 292px at rest and 292px under the longest label in the vocabulary, cell movement 0px, against 292px and 345px with a 12px shift before. Gate: `test/icon-vocabulary-browser.test.mjs` (1 new case: popover width and the hovered cell position are identical at rest and while the longest label is read out, and the readout stays inside the box). ## v0.4.27 (2026-09-21) - **A warm load fetches none of the shell's assets, and no boot script blocks the parser** (Issue #313): every static went out `Cache-Control: no-cache`, so a warm load of `/` on :4400 revalidated 31 static assets over the wire (24 answered 304), behind 24 classic scripts that each blocked parsing in turn. weave has no build step, so the Node adapter (`src/server.js`) now versions the shell when it serves it: `createAssetVersions` rewrites every local `src`, `href` and module `import` in `index.html` that names a file to `?v=` (cached per file until its mtime or size moves), and an asset asked for at its current version is sent `public, max-age=31536000, immutable`. An unversioned or stale-versioned ask keeps `no-cache`. The shell stays `no-cache`, and its ETag is now a hash of the bytes served, with no `Last-Modified`, because an asset can change under an unchanged `index.html` and a 304 on the file's mtime would keep the old URLs. All 24 classic boot scripts carry `defer`; deferred scripts and the inline lean-qr module run in document order once the DOM is parsed, so `window.leanQR` now exists before `app.js` runs instead of after. Not in this change: concatenating the `*-core.js` modules. The Cloudflare Worker (Issue #231) is unchanged and stays correct: its Assets binding serves `public/index.html` as written, with plain URLs and the platform's own revalidation, so `public/index.html` must keep plain URLs (a test pins it). Measured on a scratch server over a copy of the live data, warm load of `/`: static assets on the wire 31 → 0, bytes 9,902 → 0, parser-blocking scripts 24 → 0, page errors 0 in both themes, document editor boots as before. Gates: `test/asset-cache.test.mjs` (new, 5 cases: every served shell asset carries its content hash, immutable only at the current hash, the shell's ETag follows its bytes and ignores If-Modified-Since, versions follow a content change and skip missing files and page links, every classic script deferred with plain URLs on disk), `test/asset-cache-browser.test.mjs` (new, 2 cases: the deferred boot renders with every boot global in place and no page error; a warm load sends only the document to the server), `test/bug-report.test.mjs`, `test/chip-system.test.mjs`, `test/ui-contract.test.mjs` (script-tag pins follow `defer`). ## v0.4.26 (2026-09-20) - **Activity entries carry a commit counter, and the feed orders by it** (Issue #282): an activity entry recorded only a wall-clock `ts`, so two writes inside one millisecond had no defined order, and a clock that stepped backwards reordered history. The feed's id tie-break was wrong in both directions: across entities it ranked same-millisecond events by entity uuid, which is arbitrary, and within one entity it compared the index as a string, so `:9` read as newer than `:10`. The live weave workspace holds 6,681 entries that share a timestamp with another, and the Documents record's feed rendered 12, 11, 9, 10, 8. Every entry now carries `seq`, a monotonic counter over the workspace kept on `meta.activitySeq` and minted at the single stamping site: `seq` is the order, `ts` stays the display, and the feed, the route, the CLI and MCP all sort on it descending. `deleteEntity` and `restoreEntity` pushed straight onto `e.activity` and bypassed that site; both route through it now, which also gives them an actor they never had and the `ACTIVITY_CAP` trim they skipped. A folded document session keeps the seq it started with, since it is one event. `activitySeq` is excluded from the schema fingerprint for the same reason `publicIdCounter` is: a row write must not cost every open tab a schema refetch (Issue #274). Every open numbers whatever it finds unnumbered and never moves a number already handed out, because a CLI and a server can share one `.db`, and a write from one of them landed unnumbered inside the hour this shipped. Verified on a copy of the live `weave.db`: the numbering pass covered 10,531 entries and exactly two rows moved, the `:9` and `:10` pair the defect predicts. Gates: `test/activity-seq.test.mjs` (new, 9 cases: the counter and its uniqueness, the feed's order against adversarial uuids, `:10` over `:9`, a clock stepping backwards, deleted and restored stamping, the fold keeping its seq, the numbering pass and its idempotence, the schema fingerprint, and route, CLI and MCP parity across a `.db` reopen), `test/activity-seq-browser.test.mjs` (new, 1 case: the Activity table reads newest first in both themes). ## v0.4.25 (2026-09-18) - **Statics and JSON go out gzipped, statics carry an ETag, and a route reads the schema once** (Issue #258): nothing the Node server sent was compressed, whatever the browser said it could inflate, so a cold load of `/` moved 2,187,417 bytes over 46 resources. The Node adapter (`src/server.js`) now gzips text-shaped answers of 1 KB or more (`text/*`, JSON, JavaScript, XML, SVG) when `Accept-Encoding` allows gzip (`acceptsGzip` follows RFC 9110: gzip's own q wins over `*`, q=0 refuses), with `Vary: Accept-Encoding`; images and tiny answers are left alone, a static's gzip is cached by path and validator, and the Worker adapter is untouched because Cloudflare compresses at its edge. Measured against a copy of the live data: `app.js` 533,704 → 170,981 bytes, `/api/schema` 98,125 → 19,234, `tabler.min.css` 536,141 → 68,867, `style.css` 164,576 → 42,096; a cold load of `/` 2,187,417 → 505,208 bytes. Statics also carry a weak `ETag` (size and mtime; weak so one validator covers both encodings) and answer `If-None-Match` with 304, ahead of the existing `If-Modified-Since` (Feature #148). In the client, the home page's relation map, every space page and `#/map` fetched `/api/schema` again although the tab already held it and Issue #274 keeps that copy current; they now read `state.schema` and fetch only `/automations`. `loadSchema` starts its own schema, the root registry (`readRegistry`, member workspaces) and `/workspace` together instead of one after another, and the home page asks for `/workspace` and `/workspaces` together. Schema reads: a cold load of `/` 2 → 1, a space page 1 → 0, `#/map` 1 → 0; a member workspace's cold load 3 → 2 (its own and the root registry's). Not in this change: content-hashed asset URLs with immutable caching, and deferring or concatenating the 23 boot scripts, which change the asset pipeline and load order (split into their own Issue). Gates: `test/compression.test.mjs` (new, 7 cases: app.js gzipped and byte-identical once inflated, identity without the header and on q=0, the schema gzipped with its route headers intact, PNG and tiny answers left alone, ETag and If-None-Match 304 in both encodings, the Accept-Encoding parser, the gzip cache keyed by path), `test/schema-fetch-browser.test.mjs` (new, 3 cases: one schema read on boot with the map drawn, none on a space page or `#/map`, a member's three boot reads in flight together), `test/fetch-failures-browser.test.mjs` (the registry case follows the read into `readRegistry`). ## v0.4.24 (2026-09-17) - **A capped activity log says it is capped** (Issue #281): an entity keeps its newest 500 activity entries, and older ones were dropped with nothing to say so, so a busy entity's history read as complete. The workspace audit log (Feature #14, `audit_log`) never held entity activity: `#audit` records structural actions only (tables, views, keys, accounts, schema applies), so it keeps none of the dropped entries. Each trim now adds the number it dropped to `activityDropped` on the entity (stored in the entity's JSON row, no migration). The entity read carries it through the route, CLI (`weave get`) and MCP (`weave_get_entity`); the activity feed adds `dropped` for its scope (`GET /api/activity`, `weave activity`, `weave_activity`). The entity's Activity pane shows one line, "N older entries not kept", and the Activity table note repeats it. The cap stays 500 (`ACTIVITY_CAP`). Entities trimmed before this change have no recoverable count and read 0: an entity holding exactly 500 entries may never have lost one, so a length-based marker would guess. Not in this change: keeping every entry and paging at read time, which changes stored size and needs a decision. Gates: `test/activity-cap.test.mjs` (new, 4 cases: the count and its accumulation, the feed's scoped sum, legacy entities read 0, route/CLI/MCP parity), `test/activity-cap-browser.test.mjs` (new, 3 cases: the pane line in both themes and absent under the cap, the table note). - **A burst of filter-chip clicks is one query; a window drag reschedules the outline once per frame** (Issue #269): each filter-chip click PATCHed the table, re-read the schema and re-queried the grid on its own, and read the selection the strip was drawn with, so a second quick click overwrote the first and three clicks cost three round-trips. The strip now keeps its own copy of the selection, a click paints its chip at once, and one PATCH and one re-query run 250 ms after the clicks pause; flushes chain, so a click during a re-read queues one more instead of racing it. A reader who leaves the table inside the pause keeps the saved filter and is not pulled back to the grid. Not in this change: filtering already-loaded rows client-side. The grid reads its rows through the engine's where-language by contract (Feature #38), and a narrowed grid has no copy of the rows a widened filter needs, so that half needs a decision. The window `resize` listener rescheduled every document rail on every event; it now does one pass per animation frame. Gates: `test/filter-strip-browser.test.mjs` (new, 2 cases: four clicks in one task paint at once, land as one PATCH and one query, keep every chip, and show the rows `Weave#query` returns for the selection; a click just before leaving the table is saved without pulling the reader back to the grid), `test/doc-rail-browser.test.mjs` (+1: thirty resize events schedule no rail work inside the burst and one per rail after the frame). - **A failed request says so** (Issue #265): the ⌘K palette ran its search inside a `setTimeout` with no `try`, so a failed search was an unhandled rejection and the palette kept the previous query's results, and with no cancellation a slow earlier answer could land after a newer one and overwrite it. The palette now paints a "Couldn't search: " row under its input and aborts the previous keystroke's request (`api()` takes an optional `{ signal }`). The workspace registry no longer empties the rail silently: a failed root-schema fetch or a non-2xx answer toasts, while a 401/403 still reads as "no registry to show". The inline `#` search toasts once per run of failures instead of never, and a record or view that fails to open toasts before falling back to the workspace home. Every other bare `catch` in `public/app.js` now carries a one-line comment naming why silence is right (local parsing, storage, decoration fetches). Gate: `test/fetch-failures-browser.test.mjs` (new, 7 cases: a source rule over every bare catch, the registry, # search, showEntity/showView and `api()` signal checks, and two browser cases, one for the failed search row and one for the stale response dropped). - **An open tab notices a schema change made anywhere else** (Issue #274): `state.schema` was fetched once at boot and refreshed only on the tab's own structural edits or on window refocus (Feature #33), so a Severity field recoloured by the CLI, an agent over MCP, an automation or a second tab kept rendering `hue-slate` through every hash navigation until someone hit reload. Rows refetch on every navigation, so the grid drew fresh values against field definitions the tab had held for hours: a renamed field kept its old header, a new option rendered as an uncoloured chip. The structure now carries a version. `Weave#schemaVersion()` fingerprints the structural rows (meta, spaces, tables, automations) with `publicIdCounter` dropped, since a row insert is not a schema change; it is derived rather than stored, so the CLI and the server compute the same string with nothing to bump or coordinate, and it is memoised behind every save and reload at about 0.4ms on the largest workspace here. Every API response stamps it as `X-Weave-Schema-Version`, and `GET /api/workspace` carries it in the body for callers that read no headers. In the client, a read stamped with a version the tab has not loaded (a GET, or the row query every navigation makes) refetches the schema and redraws, through the same `syncSchema` the focus listener now calls. A write does not: the tab caused it, and the path that made it already refreshes what it needs, so a column resize still commits without a repaint (Issue #160). Four navigations with an unchanged schema cost zero extra round-trips. Route renders are chained while this lands, one at a time in the order they were asked for: the render that discovers the drift is the one making the query, so without an order it can finish last and win the page back for the schema it was drawing from. Gates: `test/schema-version.test.mjs` (new, 5 cases: the fingerprint holds across row writes and moves on a field config, a table rename, a new column and a new space; the header and the workspace body), `test/schema-stale-browser.test.mjs` (new, 3 cases: the repro as filed, a column added elsewhere, and the no-extra-round-trip guard). - **The workspace page titles the tab once, not twice** (Issue #298): `document.title` on a workspace home read `Weave Workspace · Weave Workspace`, because `showHome` sets the page name to the workspace's own name and `docTitle` then joins it to the workspace name again. Its contract said the opposite since Issue #267 (`public/breadcrumbs.js`: "The workspace page is the workspace name alone"), and `test/doc-title-browser.test.mjs` only ever saw the right title because it polls for five seconds and caught the moment between `syncDocTitle(null)` and the header's repaint. `docTitle` now collapses the pair when the page name is the workspace name, so the assertion lands on a resting state. Found while chaining route renders for Issue #274. Gate: `test/breadcrumbs.test.mjs` (+1). ## v0.4.23 (2026-09-17) - **Search keeps the hierarchy and finds saved views** (Issue #280): `universalSearch` sorted workspace, space, table and row hits together and then sliced to `limit`, and a row can score up to 20 against 8–9 for a container, so `GET /w/weave/api/search?q=weave&all=1&limit=200` returned 200 rows and no workspace, and `q=issue&limit=40` never returned the Issue table. `limit` now bounds entity rows only: a shared `Weave.capRows` keeps every matching workspace, space, table and view, caps rows, and orders in three tiers: an exact id hit first (Issue #113), then the containers, then text-matched rows. The scoped route, the cross-workspace merge (`all=1`) and `weave_search` on MCP all go through it. Saved views are searched by name (kind `view`, url `#/view/`), and a ⌘K pick on one opens the view. Gates: `test/search-hierarchy.test.mjs` (new, 4 cases: engine, id-hit ranking, scoped and cross-workspace routes), `test/search-view-browser.test.mjs` (new: the palette labels and opens a view). ## v0.4.22 (2026-09-16) - **A cell commit patches its row in place** (Issue #257): committing a cell fired `PATCH /api/entities/:id`, then `GET` of the same row, then `POST …/query` for the whole table, and `drawDatabase` rebuilt the grid with `main.replaceChildren()`, so every commit on the Case table cost the full open again, and even a 30-row table flashed and lost its scroll position. `PATCH /api/entities/:ref` now answers the fresh row plus `affected`: the ids the write marked dirty (a new `Weave#touching` collects them beside `#dirty`, which every save clears) together with the rows the edited one links to, since a lookup or rollup there shows its value. The client's `wvPatchRows` keeps each `

`s and redraws only the cells whose value moved. The cell is the focus stop (Feature #134), so a cell that is not repainted cannot lose focus, and scroll, selection, range and any open editor all survive. The follow-up `GET` is gone. The client still does a full re-read for a registry row, an edit to the sorted or filtered column, a bulk write or undo, and any response without `affected`. The response body is still `readEntity`, so older clients, the CLI and MCP see no change. On live `:4400`, one Name commit with Tab on Development/Issue (277 rows) fires one `PATCH` and nothing else, in 334 ms, and rows drawn, `scrollTop` and focus all hold. Gates: `test/cell-commit-patch.test.mjs` (new, the server contract), `test/cell-commit-patch-browser.test.mjs` (new, the DOM half); nine cases in `grid-keymap-browser`, `grid-tab-focus-browser` and `toggle-browser` now wait on the write coming back rather than on a replaced `
`, and a text cell's `` is `width: 100%`: the value overflows inside the control, the cell never reports overflow, and so no Name on any grid was ever marked. Clipping is now read from the control as well as the cell, so every text-backed cell participates. The expansion itself was the second half: it cloned the ``, and a cloned control falls back to its own intrinsic width, so hovering opened a box that cut the value off in the same place. The copy is now the value as text, carrying the control's own font, weight and metrics, and it wraps inside the popover the way a description's lines do. - **The icon picker names what you are pointing at** (Issue #142): the grid said everything in shapes and kept every name in a native tooltip: slow for a mouse, silent for a keyboard, and no help at all to someone reopening the picker to check which icon is set. The search bar now reads out the name of the cell under the pointer or the focus ring, and rests on the icon already set when neither is on the grid. The grid is unchanged: no name beside any cell, same icons, same groups. ## v0.4.6 (2026-09-05) - **The grid from the keyboard** (Feature #134, Issue #84): cells rest as values and open on purpose. At rest, ←→↑↓ move, Tab and ⇧Tab move and wrap row to row without ever leaving the grid, Return opens the cell (value selected, picker up, checkbox flipped), any character opens it and types over the value, Space toggles the row, ⇧↑/⇧↓ extend the run, ⌘A takes the table, ⇧Return makes the next row open on its name, ⌘Return docks the record. Open, Return commits down, Tab commits across, ↑↓ commit and move, Esc puts the value back. Every field `` is the focus stop, so Tab now lands on select, multi-select, checkbox and date cells the browser's own order skipped, and the last cell of the last row is the end of the grid rather than the start of the browser chrome. The keymap is one pure core, `public/grid-keymap.js`; the Handbook grid section carries both tables. - **The puck's commands, slice 3** (Feature #132, Issue #161): the selection bar gains **Set a field…** (pick a field, then a value: state chips, options, a checkbox, or a typed box, written across the selection), **Link to…** (pick a relation, search the far table, pick one row), and under ⋯ **Move to table…** (each row re-created in the chosen table by field name and type, the original trashed, unmatched fields, files and comments named as skipped), **Roll up into a new …** (one new parent in the relation's table, every chosen row linked to it) and **Copy links**. The bar is fixed to the bottom of the viewport, so it stays in reach on a grid taller than the screen. - **One bulk verb, three doors** (Feature #132): `POST /api/bulk`, `weave bulk ` and the `weave_bulk` MCP tool are the same engine call, `bulk(ids, op, params)`. Every row is its own attempt: the reply names `done` and `failed` with the reason, each row keeps its own undo step and activity entry, and the verbs underneath are the single-row ones, so nothing can drift from them. A bulk command that half works and reports success is how a row goes missing quietly; this one reports per row. - **Column resize tracks the pointer, stops at the label, never opens the field dialog** (Issues #98, #100, #160): a drag that only set `style.width` painted nothing under a stored min/max-width, so the column jumped on release; every move now paints header and cells through the same `paintColumnWidth` the commit uses, with one rounding, so the painted width is the stored width. The floor was the engine's 60px, so a header could hide its own label. Now the floor is the label's measured width plus the header padding, from the pure core in `public/column-resize.js`. Safari resolved the click of a captured drag to the header under the pointer and opened the field tray; the grip now marks the header for the gesture and the header click is inert until the next press. - **A range is picked in ONE dialog** (Issue #197): a daterange field was two date controls side by side, and the tray's default was a bare text box the engine could only refuse. `rangeControl()` is one box reading the whole span in the field's costume and one calendar; the first pick starts a span, the second closes it (swapped if it came first), the days between are lit, typed Start/End inputs live inside it, and the grain reaches it, so year·month picks two months and a clock range two times. The same control mounts in the grid cell, on the entity page and in the tray's Default. - **Every opener docks** (Issue #198): only the ledger's #id link and the document chip went through `dockEntity`; a relation chip, a card, a mention, a ⌘K hit, a trash row and a fresh row all set `location.hash` and drew the expanded page. `openEntity()` now travels to the entity's table when you are elsewhere and docks beside it; one capture-phase listener routes every plain click on an entity link through it. `#/entity/` stays the expanded page for a new tab, a permalink and the outward arrows. - **A new row takes the caret** (Issue #195): + New and Shift+Enter aimed at `td:nth-child(2) input`, which has been the #id link since the selection column landed, so the focus call was a silent no-op. `focusNewRow` scrolls the row into view and opens its Name cell. The foot button calls the grid's own `onAdd`: the space page and the home page create "New table" / "New space" in place, and a relation grid's + New creates, links and focuses. - **Workspaces from the rail** (Feature #156, Issues #190, #202, #204, #191): right-click any rail chip for **Update logo…**, **Remove logo** while one exists, and **Delete workspace…** where the hub allows it. Delete confirms by typing the workspace's name; the current workspace redirects home, a sibling stays put with an Undo toast. Trashed workspaces restore from a 26px glyph in the bottom-right cluster beside the version tag, present only while the trash has rows. `GET /api/workspaces` rows carry `deletable`, so the UI hides the affordance where the server would refuse it. The + New workspace glyph is centred. - **Chips: one size, a caret that faces the text, a label that truncates** (Issues #194, #193, #201): the chip size lives in two tokens, `--wv-chip-font` (13px) and `--wv-chip-line` (22px, a 24px hit target), read by every chip surface. The retract caret is a half turn, so expand and retract read as one control folding in and out. A pointer chip's name sits in a `.k-label` span. Inside a constrained column it ellipsizes; the home badge and ↗ stay at the right end, and the full name rides in the title. - **The embedded relation grid keeps the target table's hidden set** (Issue #200): a Release's Fixes grid drew Chip and Card and every cell read `[object Object]`. `relatedGrid` filters through `visibleCols(target)`, and the generic cell fallback refuses objects: a `wv-unrendered` marker names the type instead of a box holding `String(obj)`. - **The v0.3.0 schema page is gone** (Issue #196): `addFieldDialog` fell back to `openSchemaEditor()` whenever the add did not start on a table route, which every registry grid is. It now hands you back to wherever you were; the schema editor, the relation dialog only it reached and the `.schema-table` rule are deleted. - **An icon moves only on its own hover or press** (Issue #192): the load wave and the picker-scroll play are gone; a document-level pointerdown joins the mouseover listener, one run per trigger through the same `playIcon` gate, reduced-motion stills it. - **Every seeded space and table wears a `lucide:` icon** (Issue #203): Release rocket, Issue bug, Feature star; `ensureReleaseTable` carries the rocket, so workspaces seeded before the table existed pick it up on the next sync. - **Handbook and mockups** (Features #180, #181, #182): a "Chip and card anatomy" guide draws the real chip and card markup with a dashed outline and a numbered badge on each of 13 elements, and a legend of what each does, how you use it and where its hitbox is; `docs/chip-card-anatomy.html` is the same guide as one self-contained page. `docs/mockups/card-view-options.html` holds fifteen card options (A–O) with a comparison matrix, every field value drawn as the cell's own chip. The polymorphic-relations guide points each use case at its Showcase table. Chip anatomy round 2 (P–T) and the relation-map junction mockups (A–D) are on the same shelf. ## v0.4.5 (2026-09-05) - **The ⋮ panel stays on the page** (Issue #133): the document section's downloads menu opened past the right edge: a 178px panel at `right: 1383` in a 1280px viewport, so the reader saw a sliver of "…ownloa" and could reach none of it. The side was chosen where each menu was written (`align: 'right'`, passed by three callers out of seven), which no constant can get right on both edges. `dotsMenu` now measures on open: `menuSide()` weighs the anchor, the panel width and the bounds it has to live inside, and `menuBounds()` takes those bounds from the nearest ancestor that clips its overflow: the side peek, not the window. The caller's `align` survives as the preference and wins whenever it fits. Same panel now opens at `right: 1238`. - **Tab carries on from the row you are on** (Issue #83): editing a cell and tabbing out fires the PATCH, and the grid redraw that follows it landed a beat after the browser had already moved focus along the row, tearing out the cell just reached and dropping focus on ``, so the next Tab restarted at the top of the page. Every grid redraw now reads the live focus on the way in and puts it back on the same cell, caret where it was. - **⌘-click opens a tab, everywhere** (Issue #134): one predicate, `nativeClick`, answers ⌘/Ctrl, Shift and the middle button, and one capture-phase listener acts on it for the surfaces that navigate without being a link: a grid row, the relation panel's rows, an activity row, a ⌘K hit, a node on the relation map. Each declares its destination as `data-href` and nothing else; real anchors are left to the browser, and form controls keep their own modifiers, so shift-click still extends a text selection and the row checkbox still range-selects. - **Release notes are rows** (2026-09-04): `Development/Release` holds one row per release: version, Date, Commit, the Issues it `Fixes`, the Features it `Ships`, and the notes as its Description. The notes are mandatory: the export refuses a row without them, sync refuses a manifest without them, and the suite refuses a package version with no row. The four releases to date are backfilled from this file; the manifest now ships them, so every instance's docs workspace grows the table on its next boot. - **Chip and Card: every table says how its rows appear elsewhere** (Kyle, 2026-09-04): two `view` fields minted on every table, hidden from the grid by default, never deletable, held by role (`chipFieldId` / `cardFieldId`) so a rename costs nothing. The config is the table's: the #id link, the workflow state, a description preview at `none`/`small`/`medium`/`large`, and which other fields ride along (or the first few non-empty ones in column order). The chip is what a relation cell, a doc mention and a reference card draw: one builder, segments behind the doc-mention caret; the card is the tile a board or gallery will draw. The entity page shows both under **Appears as**, each with a gear into the field dialog, whose pane previews the view live under the candidate config (`GET /api/entities/:ref/view?shape=&config=`). `readEntity` ships both under their field names (one line in `fields`, the object in `raw`); every relation summary carries `chip`. CSV export leaves them out; a partial `fieldOrder` may too. A hidden column now stays hidden through a rename. - **Formula authoring gets a check step**: `check()` validates an expression statically (syntax, function names, field references), and the engine refuses to save one that fails, including a formula that references its own field. `weave formula check`, `POST /api/tables/:id/formula-check` and the `weave_check_formula` MCP tool run the same check and preview the computed value on a real row, so the loop for a human and an agent is identical: check until `ok: true`, save, read a cell back. The script editor shows the verdict live while typing (red parse error, or `= value` from row 1), function chips land the caret inside the parens, and the field being edited no longer offers itself as a chip. - **Icons move** : the icon set is Lucide (595 names) carrying movingicons.dev motion: an icon plays once when the page loads, once when the picker scrolls it into view, once per hover, never on a loop. The value form is `lucide:`; every `iconly:` stored before 2026-09-02 resolves to its Lucide twin through `public/icon-registry.js`, so nothing migrates. Fourteen state marks (✓ ✕ ★ ! ? ▶ ⏸ ⊘ ⚑ ◎ ⛓ ⌁ → +) draw as their Lucide twins; the six progress rings stay hand-drawn, re-inked to the same stroke. Rebuild the set with `scripts/build-lucide-moving.mjs`. ## v0.4.4 (2026-08-31) - **Development sync** (#152): every build ships `docs/development.json` (the canonical Issue + Feature lists exported at release by `scripts/export-development.mjs`), and `weave serve` applies it to the local docs workspace on boot. Updating weave now updates the known/resolved issue list and the roadmap; locally filed rows are never touched (match is by name). `test/development-sync.test.mjs` pins the manifest to the package version. - **Workspace delete** (Issue #122): `DELETE /api/workspaces/:ref` + a hold-to-confirm action on the workspace page. The `.db` (with WAL/SHM) moves to `/trash/`, recoverable, invisible to the hub's scan. The default and weave docs workspaces refuse. - **A fresh workspace explains itself** (Issue #123): hub-created workspaces open with a description naming the first steps and what the Workspace registry space is. - **The registry registers itself** (Issue #126): the system Workspace space and its four tables are rows in Spaces/Tables like everything else: the registry now means its own description. Deleting or repurposing those rows is refused; columns cannot be added to system tables through the Fields registry. - **Percent means percent** (Issue #127): number fields with the percent costume follow the spreadsheet convention: stored fraction, displayed ×100 (0.325 → "32.5%"), input ÷100. Existing percent values migrate ÷100 once on first open, so every cell reads exactly as it did. - **Formula chips insert parseable tokens** (Issue #128): a field chip inserts `[Due Date]` when the name needs bracketing (spaces, punctuation, keywords, function-name collisions) and the bare name only when it is a safe identifier. - **Shift+Enter is save-and-create-another from inside a row** (Issue #125): the cell commits, the new row's Name cell takes focus. - **Empty-schema banner gone** (Issue #124): "This table has no fields beyond its name." read as breakage, not help. ## v0.4.3 (2026-08-28) - **Target-set (polymorphic) relations** (#150): one relation field may target SEVERAL tables (`targetDbs`), including the registry's `Workspace/Spaces` and `Workspace/Tables`, so a row can point at a space or a table as easily as at another row (a bug scoped to a whole space, a dependency on an entire workstream, an agenda mixing rows with structure). One-way by design (no inverse spray); lookups/rollups keep the single-target contract; filters traverse per-row; chips carry their home table; the picker searches every member; the relation map draws one edge per member. Rollback-safe: singleton relations are bit-for-bit unchanged, and removing multi-target fields restores the pre-feature shape exactly. ## v0.4.1 (2026-08-21) - **Skeleton loading** (#49, inspired by 0xGF/boneyard): navigation paints a skeleton of the real destination, natively, the library being framework-bound. - **Share QR codes** (#50, inspired by p2r3/ha.mr): sharing a view shows a scannable code beside the copied link (lean-qr 2.7.3 vendored; `.mjs` MIME fix included). - **Simultaneous boots survive** (Issue #39): `busy_timeout` now precedes the WAL switch. ## v0.4.0 (2026-08-21) ### The agent-native meta-model - **The workspace's structure, as rows** (#12): a `Workspace` system space holds `Spaces`, `Tables` and `Fields` registries: real entities synced both ways by the engine's own verbs. Create a row, get a space/table/column; rename the row, the real thing renames; custom fields (a workspace logo, an owner) ride structure for free. - **Every field is a row** (#52): the Fields registry is the schema surface, carrying each column's shape as a `field`-type Definition (#85: entity page as the control surface, `materializeField`). - **The schema as a document** (#13): `weave schema export/apply`: edit the JSON, the workspace grows to match; deletions need `--allow-destructive`, type changes never apply. ### Identity, audit, and the road to hosting - **Every change names its actor** (#65): createdBy/modifiedBy + per-table system columns (Created/Modified At/By, Activity #95); server X-Weave-Actor, CLI WEAVE_ACTOR/OS user, MCP client name. - **Accounts, roles, audit** (#14): admin/writer/reader tokens (sha256 at rest, handed out once), requireAuth for the hosted future (#84, v0.5), a durable audit log of structural change. - **The key field type** (#64): secrets live in a chmod-600 keystore, never in workspace data; set over HTTP, never readable back. ### Views - **Filters** (#38, closes the oldest issue #2): workflow-state chips driving the engine's where-language server-side. - **Side peek** (#39, #48): rows and activity entries open beside the page, not instead of it. - **Saved views + share links** (#17): named blocks resolved live; sharing mints a revocable capability URL that outlives the auth wall. - **The workspace page** (#51): its own shape as a read-only mermaid map; **record nouns** (#40). ### Typed values - **Number costumes** (#97): format/unit/decimals/separator, display-only, formulas keep raw numbers. - **The date system** (#44): iso/us/eu/long + time costumes, date math in formulas (dateadd, datediff, year/month/day, now), and a type-or-pick cell that reads "next friday". ### The editor, completed - **Syntax highlighting** (#35): the slash menu seeds a language; rendered pages load vendored highlight.js on demand. - **Live `[[…]]` chips in the editor** (#86): an overlay decoration over Vditor's IR: Lute owns the text, weave never mutates it; literal under the caret. - **Outline dash rail** (#87) and **collapsible headings** (#88): a minimap with a place tracker; folds are view state (content byte-identical), persisted per document. - **The row editor** (#89): one shared Vditor instance mounts into the focused row (2ms warm), and the icon-sprite leak `destroy()` misses is fixed. - **Math** (#90): KaTeX 0.16.47 vendored (with mhchem, load-bearing); the heavier diagram engines stay out, documented. ### Operations - **Service management** (#54): `weave service install|status` (launchd), health with startedAt/uptime, instance status in the nav. - Hygiene: mermaid 11.17.0 (XSS advisories, #8), MCP version honesty (#19), qualified relation refs (#21), board title ellipsis (#20), activity coalescing (#32), webhook test deflaked (#27). Slipped to v0.5 (recorded on their tracker rows): #84 hosting, #16 attachments field type, #46, #47, #49, #50, PDF non-WinAnsi glyphs (Issue #4).