Skip to content

release(0.15.0): the market-demand bucket, hardening round, pipeline debt - #511

Merged
ra3orblade merged 169 commits into
mainfrom
feat/0.15.0-market-demand
Oct 20, 2026
Merged

ra3orblade merged 169 commits into
mainfrom
feat/0.15.0-market-demand

Conversation

@ra3orblade

@ra3orblade ra3orblade commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Cuts v0.15.0: the 48-rung market-demand bucket (plan row 0.15.0, harness docs/apps/market-demand-2026-10.md), the hardening round that followed four review passes, the pipeline debt from the 0.14.0 cut, and the release entry. 156 commits, one integration branch per the batching rule.

Full suite on the final head c9c63f0: 1,881 files / 19,133 tests green; bun run typecheck (packages + apps) and bun run lint (biome + every ratchet) clean.

1. Database (9.12.24–9.12.35)

Contextual inline views ({$host:true}), two-way relations (PropertyDef.inverseOf, mirrored on create/update/delete, far ends cap- and lock-checked before any write), rich-text property, collection descriptions, board swimlanes + group controls, column freedom, chart view + dashboard widget, gallery cover/field settings, bulk edit with one-step undo, change a property's type with a preview + number units, next/previous object, pin a view.

2. Notes (B11.21–B11.31)

Insert menu, editable synced blocks, block + table-cell backgrounds (tokens in all 14 themes), /task, turn block into page, side outline panel, regex find and replace (bounded), media from the vault + inline image width, tabs block, list guides + outliner keys, spell-check language picker.

3. Shell (6.14, 6.15, 7.15–7.17, 8.11, 9.22.7, 10.16, 12.18, IE-13, IE-14)

Split view (per-pane renderer identity, forged pane ids rejected through the Broker), "don't ask again" on reversible prompts only, auto-hide sidebars, close to tray, startup page, recent emoji/icons, search when empty + backlink counts, per-object version history (entities.db v13, ydoc-frame codec, retention policy, history broker service), visibility preferences, whole-vault Markdown export, scheduled backups with restore points.

4. Agent (Agent-14–Agent-16)

Provider + model settings with every AI egress through the network-broker transport; run diff and whole-run undo (agent_writes, entities.db v11); agent authorship projected read-only and unforgeable on every write path.

5. Chat, collaboration, the rest (Chats-1–4, Collab-C7, 9.14.17, 9.16.14, 9.8.16/17, 9.20.15, 9.17.24)

Message search, per-user read state, composer + time prefs, pins + typing; invite links that only pre-fill the share dialog, Viewer default, last-editor record; three-state task checkbox; Journal quick capture into a collection; Files filters + duplicates + OS file drop (@brainstorm-os/sdk/os-file-drop); Preview context panel (@brainstorm-os/sdk/backlinks); Whiteboard viewport restore.

6. Hardening (fix/0.15.0-harden-*, fix/0.15.0-pair-gate-race, fix/0.15.0-lock-at-commit, fix/0.15.0-database-friction, fix/0.15.0-history-retention)

  • Two pre-existing holes closed: the ydoc worker service is capability-gated on the broker (SH-ydoc-gate); the AI service re-checks the ledger on every call (SH-ai-gate).
  • Reserved property keys stripped at every import, merge and far-write choke point; __proto__ is never a catalog key and the catalog record has no prototype.
  • 9.12.25: setProperty/removeProperty serialised per store; the pair gate closes over the whole pair component (chains, cycles, missing partners).
  • 9.16.14: a List lock holds at the commit (saveUserList/deleteUserList re-read and refuse, embedded create-row re-reads before pinning); rename/description/icon/delete of a locked List refused with a disabled, hinted menu row.
  • 10.16 retention is linear and chunked: 250-row delete transactions, yields after every page/plan chunk/relocate; longest main-thread block 7.6 ms at 50k rows (was the whole 0.8–4.5 s pass).
  • Database friction from the hidden real-shell pass: F-507 (menu stack above every picker host, with a stylesheet invariant test), F-508 (resolvePropertyReuse: Reuse/Adopt/Distinct instead of silently dropping a pairing), F-509 (pinned column opaque), F-510 (inspector nav counter on one line), F-511 (CSV columns get catalog defs; new views seed visible columns; segment padding; shared bs-input face).
  • Screenshot-pass fixes (fix/0.15.0-shots-*, F-512..F-520): Files list rows always render a glyph on a fixed icon track (names were invisible since the shared icon track of 2026-08-08); the Notes tabs block no longer trips Lexical 0.21's broken resolveChildIndex loop on rename/add (TabsNode.getDOMSlot override, droppable on a Lexical bump); Chat's typing indicator is an in-flow row; Preview reads Modified from the entity envelope; Journal resolves a day to ONE entry (the welcome seed) and captures through its live document; a split pane destroyed from outside un-splits its container; the emoji picker searches keyword aliases; Move-to-Bin prompts say the item can be restored; the Agent run-changes panel hides host-owned keys and formats dates.
  • Size budgets re-baselined with owner acceptance: renderer 350→385 KB, main 305→365 KB (slimming pass owed).

7. Pipeline + release (SH-42)

  • bun pinned to 1.3.9 on both CI pipelines (latest broke the 0.14.0 Windows build with zero code changes).
  • Version bump to 0.15.0 in packages/shell/package.json + root; the in-app What's New entry (58 blocks, validated by the changelog tests).
  • BRAINSTORM_FEEDBACK_ENDPOINT still needs an org secret (owner-only; the collector deploy is deferred).
  • The release.yml workflow_dispatch dry-run is dispatched on this branch.

Gates (recorded in harness docs/_review/evaluations.jsonl, 2026-10-19)

security-review + pentest pass: 10.16, 12.18, 6.14, 9.12.24, 9.12.25, 9.12.26, 9.12.32, 9.20.15, 9.22.7, 9.8.17, Agent-14, Agent-15, Agent-16, B11.23, B11.24, B11.31, Chats-2, Collab-C7, IE-13, IE-14, SH-ai-gate, SH-ydoc-gate; security-review pass + pentest pass 9.16.14; B11.27 security-review pass; 10.16 perf pass after the retention fix.

Residue carried (on the plan row)

Shell entities.update/delete do not enforce properties.locked server-side (pre-existing advisory lock design shared by six apps; a lockRefusesWrite check there would close the one-IPC-gap TOCTOU too); a refused List delete still bins its views; split restore across restarts + Browser exclusion; Database ignores 7.15 auto-hide; editor/main-process strings untranslated; filter embeds run one full-vault scan per load; hard app-lock does not drain pending history; non-agent BeforeRestore checkpoints have no per-app cap; backups unsigned; delete does not report skipped far ends; retention's 1–2 WAL-commit spikes per hourly pass at 10–15 ms.

🤖 Generated with Claude Code

th3-br41n and others added 30 commits October 18, 2026 15:46
…(B11.21)

The gutter + now opens the host's slash catalogue as a filterable picker
through the shared search picker, anchored to the button (the gutter is
pinned while it is open), and inserts the chosen block below. The block
action menu gains Insert above / Insert below rows, and Mod+Shift+Enter
opens the same picker below the caret's block. Dismissing the picker
leaves the document untouched; without a menu host + falls back to an
empty paragraph.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…B11.22)

A transcluded body was a read-only mount of the source's Y.Doc. It now
carries an Edit here / Done toggle: in edit mode the nested editor is
editable and mounts the Notes editing plugins (slash menu, toggles,
callouts, embeds, mentions, references, link markup, inline toolbar), so
edits land in the SOURCE's doc behind an accent boundary, with the
source card as its chip. The source's denormalised title/snippet/refs
follow through the same lock-gated update funnel the open note uses.

Read-only stays the answer when the host page is read-only, the source
isn't loaded in Notes, or the source is locked (disabled toggle with a
hint; a lock landing mid-edit drops back live). Undo is the source's own
UndoManager. Document-scoped editor chords now ignore keydowns from a
nested Lexical root so a host plugin never acts on its stale selection
while typing in the nested editor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aware embedded block (9.12.24)

- sdk-types: { $host: true } PropertyRef, allowed on $contains/$notContains;
  predicateReferencesHost so a host-relative predicate with no host matches
  nothing as a whole (negation can't widen it).
- predicate-eval: PredicateContext { hostEntityId } threaded through
  evaluatePredicate + evaluateSource; $host resolves to the literal id.
  Seeded property tests: no host => never matches; host => literal-id parity.
- shell entities-repo SQL compiler: $host (and computed $contains RHS)
  compile to match-nothing, never match-all.
- block channel: optional hostEntityId in the BP Startup payload, validated
  on both ends (sanitizeHostEntityId); BlockRuntimeContext.hostEntityId();
  BpBlockMount prop; editor BlockEmbedView derives it from the render chain.
- embedded-list block resolves rows via the List's real source + member
  overrides + default view filter/sort with the host context.
- filter builder: FilterRefKind enum (prop/now/host), 'this object' RHS for
  relation properties on is/is not/contains/does not contain.
- entity-draft: $host pins pre-fill the host into a new row's relation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es, so 'relation contains this object' works on multi relations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A real block-level background for paragraphs, headings, quotes,
callouts, toggles and list items: the block action menu gains a
Background section (one row per swatch). Only the swatch NAME is stored,
on the element's own style (synced by @lexical/yjs, carried explicitly
through duplicate and the block clipboard, re-validated on paste), and
BlockBackgroundPlugin — mounted by BrainstormEditor so previews and
nested bodies paint it too — mirrors it to a data attribute that
editor-theme.css maps to a token.

The tokens are new: color.blockBackground.{gray..red}, defined in every
built-in theme (light washes / dark tints by appearance), with a contrast
ratchet holding text.primary at AA on each, and the canonical token-name
snapshot re-synced (TOKEN_NAME_VERSION 4). Table cells get a Cell
background submenu over the same tokens; hasCellBackgroundColor is on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- sdk-types: optional inverseOf on PropertyDef; validatePropertyDef accepts
  it on entityRef only.
- shell inverse-relations planner (pure): symmetric pair map (ambiguous
  pairs ignored), add/remove mirroring, single-valued far-end displacement,
  delete cleanup, one-time backfill; preserves stored value shapes.
- entities service: create/update/delete plan far-end writes from the live
  catalog, check every far end (entities.write on its type + read-only lock)
  BEFORE writing, then apply through the same Y.Doc-first write path.
- properties service: a def that newly declares inverseOf backfills first
  (as the calling app) and is saved only if the backfill was allowed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…11.24)

/task names the task in the shared search picker and, on Enter,
client-mints a task id, drops an @-mention of it at the caret and
creates the brainstorm/Task/v1 entity (rolled back if the create
fails). Because the chip IS a mention, the note -> task reference the
shared walker reads is the task's backlink, and Graph edges, clipboard
and Markdown export need nothing new; the Tasks app sees the entity via
its live-entities stack.

Every mention of a task now carries a live checkbox (the SDK Checkbox):
done state comes from the shared entity index (new getEntityProperties),
completion writes through a new EditorHost.entities slice with the same
patch the Tasks inline-task block writes, and it stays disabled for a
locked task, a read-only editor or an unwired host. Notes wires the
host under its existing entities.write:* grant (no new capability);
FullEditorPlugins offers /task only when a host is wired.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A window can now host a second object beside its active tab, each pane its
own sandboxed app renderer registered under its own app id, so the broker
keeps attributing every call to the right app even when the two panes are
different apps.

- WindowContainer: a side pane next to the strip, laid out from a pure
  state machine (window/split-state) + geometry (window/split-layout):
  ratio clamp to a 280px minimum per pane, keyboard step, swap, focus
  tracking. The window title follows the focused pane; Cmd+W in the side
  pane closes it; closing the primary's last tab promotes a same-app side
  pane, or re-homes a different-app one into its own window with its
  renderer (and identity) intact.
- Divider: the chrome view spans the window under both panes while split,
  so the 6px gap is the chrome renderer's — draggable (rAF-throttled IPC,
  sender-resolved, clamped in main) and a focusable ARIA separator, with
  no extra renderer.
- Entry points: NavigationMode.NewPanel ("new-panel", doc 37) — Alt+click
  via navModeFromEvent, and an "Open to the side" row in the shared object
  menu; registry shortcuts shell/split.{open,focus-other,swap,
  divider-left,divider-right}, handled in main against the source window
  and falling through to the app when the window isn't split.
- The intents service now passes the broker-verified sender id to the bus
  so placement modes can find the window an open came from.
- Every-renderer paths (vault-lock mask, broadcasters, windowsFor,
  running set, focus-existing) cover the side pane via panes().
- Browser (self-tabbing) windows refuse a split; addTabToContainer never
  puts another app's launch into a container's strip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ma + model settings (Agent-14)

Settings → AI can now add, edit and remove named OpenAI-compatible endpoints
(name, base URL, default model, optional key in the credential store), with an
OpenRouter preset, and edit the Ollama endpoint + model (env stays the
default). Each endpoint registers as a routable `custom-<hex>` provider and is
re-applied to the live registry on every change and vault open.

- Egress rides the network broker like every provider (SSRF floor, DNS
  pinning, proxy, audit). allowPrivate is derived from the URL: http only for
  loopback/LAN hosts; public endpoints need https and a key.
- GLM, Mistral and Solheim are now routable defaults (the picker offered them
  but the store silently dropped them).
- The broker validates pinned model ids (length cap, no control chars),
  requires an ai.provider:<id> cap for a pinned custom endpoint, and gains a
  ledger-filtered `ai.providers` listing for app model pickers.
- Budgets: custom endpoints meter at FALLBACK_CLOUD_RATE; a provider that
  reports no usage is metered by estimate, never as zero tokens.
- The OpenAI provider scrubs the key from echoed error bodies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…picker (Agent-14)

The conversation settings popover lists providers from the shell's
`ai.providers()` (so custom endpoints show by name) and adds a free-form model
field with the provider's known models as suggestions. The model persists on
the conversation, clears on a provider change, and only a sane id is ever sent.
Manifest gains ai.provider:mistral (was missing) and ai.provider:* for custom
endpoints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s-1)

Header search toggle + CmdOrCtrl+F / CmdOrCtrl+Shift+F (Escape closes) open a
search strip over the transcript: This channel / All channels scope, newest-
first results with channel, author, and date+time. Picking a result switches
channel, scrolls the message into view, and flashes it.

The scan runs over the live snapshot the app already holds (no new search
capability; Message/v1 bodies aren't channel-indexed by the vault search),
bounded at 50 results and a 2-character minimum query.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Files reads as a collection: a Filter toggle in the toolbar opens a
Kind / Size / Modified / Extension row (shared SelectMenu; the Modified
axis rides the SDK's live-rolling RelativeDateRange), composing with sort,
group-by and search. A vault-wide Duplicates place in the sidebar groups
File rows by their stored content hash, showing only groups of two or
more, ordered by reclaimable bytes; filters apply before grouping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- sdk-types: PropertyKindPreset.RichText (in KIND_PRESET_ORDER + defaults,
  presetOf), RichTextValue { text, state? } + richTextPlain projection,
  CellPresentation enum + optional CellProps.presentation.
- SDK RichTextCell (RichText::Block): one-line plain projection in a grid
  with a popover editor; full in-place editor in an inspector
  (properties panel passes Expanded); persists once per editing session,
  blank clears; plain-textarea fallback when no editor is injected.
- richTextEditor seam on PropertiesProvider (the SDK can't import the
  editor); editor package ships RichTextPropertyEditor over CompactEditor,
  which gains initialState / initialText seeding.
- Inline add-property form: Text > Rich text format; Settings > Data badge,
  icon, tint; Database wires the editor seam + Expanded inspector cells and
  bridges rich values; Notes/Database label catalogs (6 locales each).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The block action menu's Turn into section gains Page. The selected
blocks are snapshotted with the block-clipboard serializer (list items
keep their list, block backgrounds ride along), the new Note is created
and the snapshot planted into its Y.Doc through the renderer resolver,
and only then are the blocks swapped for a page link — the same
PageRefNode /subpage inserts — in ONE editor update, so a single undo
restores the blocks and drops the link together (pinned at the Yjs
UndoManager level). The first block's text becomes the title.

A failed create or plant leaves the document untouched; if the blocks
were edited while the page was written the swap is skipped so the newer
text is never discarded; a selection containing the page title is
refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ty query (9.22.7)

An empty launcher query now leads with the user's saved searches and the
objects they opened most recently, above the app list. A typed query ends
with a 'save this search' row; saved searches apply on click and carry a
remove button. Every entity row shows how many objects link to it.

- LauncherMemory keeps both lists in settings.db (per device, encrypted at
  rest) under a reserved scope no app id can claim; recents cap at 12,
  saved searches at 20.
- IntentsBus reports every delivered entity open through onEntityOpened,
  which is how recents fill; a failing observer never fails the open.
- EntitiesRepository.countBacklinks is one grouped query per 64-id chunk,
  attached to search:query and search:recents results (no N+1).
- SearchIndexer.lookup resolves recent ids to titles in one query.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l unread badges (Chats-2)

Read watermarks move from the device-local storage.kv key onto an app-owned
per-user vault entity (io.brainstorm.chat/UserState/v1, keyed by the member's
sovereign pubkey) so they ride the owner's device sync. The SDK has no synced
kv (storage.kv and settings are both device-local by design). Duplicate rows
from two devices that boot before syncing merge by per-channel max; writes
target the lowest-id row and are debounced. The old kv key is the one-time
migration source and is deleted after the first synced write; a host without
a roster keeps the device-local path.

Adds Mark as read / Mark all as read (channel right-click menu, header menu,
Shift+Escape) and a CountBadge per unread channel. UserState is registered as
a system entity type so type-enumerating surfaces treat it as plumbing.

New capability: entities.write:io.brainstorm.chat/UserState/v1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(9.8.17, Files half)

New @brainstorm-os/sdk/os-file-drop subpath: isOsFileDrag + a depth-counted
OsFileDragTracker, bindOsFileDrop (DOM) / useOsFileDrop (React) drop zones,
importDroppedOsFiles (files.import bytes variant -> File/v1-ready metadata,
per-file failures reported, never aborting the batch) and
importOsFilesAsEntities (adds entities.create for surfaces without a local
tree — Database's collection views are the intended second consumer).
mimeFromName moves from Files into the module. Files' content list and
drop-upload path now ride it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(Chats-3)

The composer gains an expand/collapse control (CmdOrCtrl+Shift+E,
chat.toggle-composer) that grows the CompactEditor to a drafting height.
"Show full dates and times" in the header menu swaps Today/Yesterday day
labels and bare clock times for full dates; the preference is stored on the
per-user UserState row so it follows the user (device-local kv when the host
has no roster).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ections are picked (9.12.27)

- Database stage header: the List description is an inline edit field
  (Enter/blur commit, Escape reverts via the shortcut layer), revealed with
  an 'Add a description…' placeholder on header hover / focus; only
  entity-backed Lists are editable (vault-derived lists regenerate).
- list-crud setListDescription (trimmed, no-op when unchanged).
- SDK menus: ContextMenuItem/AnchoredMenuItem.description renders as muted,
  truncated caption text with the full text as the hover title (fits the
  fixed virtualized row height).
- SDK cross-app 'Add to collection' picker + Database's own picker show each
  Collection's description; sidebar list rows carry it as tooltip +
  aria-description.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings -> Spellcheck dictionary gains a Spelling languages picker: the
shared MultiSelectMenu over the platform's available hunspell
dictionaries, plus Use system languages to go back to the OS order.
The choice is validated main-side (an ordered, de-duplicated subset of
what Chromium reports, max 8), persisted per device at
<userData>/spellcheck-languages.json (dictionaries are a property of the
machine, not the vault), loaded at boot and re-applied live to every
spellcheck-enabled session. When nothing chosen is available the OS
order still applies; macOS (no list exposed) keeps the auto-detect note.
The custom dictionary is untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The inspector's editable view gains a Caption row (a first-class File
property, riding the grid as a host-owned extraRow with its own persister),
a backlinks section listing every live object that links to the file
(opens in a new tab, Shift for a new window) and a Move to Bin action over
the soft-delete entities.delete path. Locked files refuse all three: the
caption cell renders read-only, the Bin button is disabled with a hint, and
both persisters early-return. A binned file leaves the gallery with the
cursor on whatever slid into its slot.

The backlinks walk is extracted to @brainstorm-os/sdk/backlinks (Preview is
consumer two after Journal, which migrates onto it and folds its two
near-identical link panels into one).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings > Backup & migration gains 'Export as Markdown': pick a folder and
the vault is written as a dated folder tree, one .md per object grouped by
type, YAML front matter for properties and timestamps, attachments copied
into assets/.

- @brainstorm-os/editor serializedStateFromDoc reads a universal-body Y.Doc
  back into a serialized tree by walking the @lexical/yjs encoding directly,
  so bodies with node types the shell has no class for still export.
- Mentions, entity links and relation properties become relative links to
  the exported files; a reference to an object that is not exported
  (deleted, plumbing) becomes its plain label. No entity id reaches a file.
- Streamed one object at a time with a yield every 25; abort or failure
  removes the partial folder; a same-day re-export never overwrites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts-4)

Pins: Pin / Unpin message from a per-message menu (hover ⋯ or right-click);
the pin list is stored on the channel row (Channel/v1.pinned, bounded at 50,
decoded defensively) so every member sees the same board. A header Pin button
lists the channel's pins newest first and jumps to the chosen message; it is
disabled with a reason when no channel is open.

Typing: the shared presence layer gains an optional, closed-set
PresenceActivity (Typing) on the payload — usePresence(..., { activity }) and
peerFromState drops unknown values. Chat publishes it at the first keystroke
of a burst and clears it after 4s idle, on send, and on channel switch (two
publishes per burst); typing is never written to an entity. Other members'
typing renders above the composer (aria-live).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or emphasis, always-visible scrollbars (12.18)

Settings → Appearance → Visibility adds three opt-in preferences painted over
every theme. Higher contrast is an inline token-override layer derived from the
theme itself (tokens higherContrastOverrides — borders/dividers/muted labels/
active fill each step up, never weaker); cursor emphasis and scrollbars are
root data-attribute rules over the new color.emphasis.* tokens (every theme).
Persisted per vault in the dashboard doc, applied by ThemeProvider in the shell
and by the app preload (boot arg --brainstorm-display + app:display-changed
broadcast) so every app renderer follows. Tests: per-theme override existence,
12.16/12.17 contrast lint under overrides, 3:1 strong border, AA under the
emphasis selection wash, store/IPC/broadcast/launch/UI round-trips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
th3-br41n and others added 27 commits October 19, 2026 22:33
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ponent

Two 9.12.25 pentest holes (2026-10-19) in the `inverseOf` gate:

- Concurrent `setProperty` / `removeProperty` calls raced the gate: each
  snapshotted the catalog, awaited `canWriteType` / `backfillInverse`, then
  wrote, so two interleaved calls each passed against a catalog missing the
  other's def (an app holding only `entities.write:Book` could pair onto a
  partner and widen it to Person in the gap). Catalog mutators are now
  serialised per store on a promise chain; the catalog is read under the
  lock. Reads stay unserialised.
- `pairTargetTypes` collected the key's partners but not the partners'
  partners, so a third def naming one half of an existing pair was gated
  without the other half's types. The walk now closes transitively over
  `inverseOf` in both directions, bounded by a visited set.

Adds `PropertiesMethod` for the handler's method discriminators.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A menu is opened FROM the picker hosts (the Links-to / Two-way-with
selects inside the property form), yet .fm-menu was pinned at 130 and
the dimmer at 50 while the picker-host backdrop sits at 9000: the menu
painted under the form and every click landed on the backdrop, which
cancelled it. The stack vars are now literal top-of-stack values
(dimmer 9100, menu 9101), .fm-dimmer / .fm-menu take their z from the
vars so the ordering holds whichever sheet loads last, and
menus.test.tsx fails if any SDK layer is ever declared above the dimmer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…airing instead of dropping them

findReusablePropertyDef matched on name + type alone, so naming a new
two-way relation after an existing property reused that def and threw
the Links-to target and the paired property away behind a 'reused'
toast. resolvePropertyReuse now decides per draft: Reuse when the
existing def already expresses the same settings, Adopt (write the
target / pairing onto the existing def, re-pointing a new partner at
its key) when it lacks them, and Distinct (mint the draft as its own
def) when the existing def is already paired elsewhere. Both the column
and the collection constructors follow the decision and the toast names
which happened, in all six packs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…and selection

The pinned cell mirrored the row's hover / selected fill with the bare
--hover and --accent-soft tints, both translucent (an accent over
transparent, rgba(…, 0.06–0.16) in most themes), so a sticky cell let
the cells scrolling beneath it show through — the selection appeared
to slide over the Name column. Each mirrored face now layers its tint
over the opaque page colour, and grid-view.test.ts fails if a pinned
rule ever paints a tint bare again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…itle

The header is a flex row whose title had no flex-grow and no min-width,
so a long title's basis pushed the whole row into shrink and the
"3 of 4" counter folded onto three lines. The title is now the single
flexible, truncating item (flex: 1 1 auto; min-width: 0); the nav slot
and the counter are flex-shrink: 0 with white-space: nowrap.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…log record has no prototype

A def keyed __proto__ landed on the snapshot's prototype instead of in it:
invisible to Object.values and the dashboard, yet persisted in the Yjs doc.
No capability bypass, but a catalog an app can hide things in is wrong.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…te (9.16.14)

Pentest 2026-10-19 (low): every membership writer checked `isListLocked`
up front, then awaited (entity creates, the 400 ms persist debounce) and
committed through `saveUserList` regardless — a List locked by another
surface in that window still took the members write.

At the commit:
- `saveUserList` / `deleteUserList` re-read the row and refuse a locked one
  (`SaveListOutcome` / `DeleteListOutcome.RefusedLocked`, carrying the
  persisted List). `reconcileUserLists` then drops the stale local List for
  the persisted row (re-adding a refused delete on its fallback Grid),
  re-renders, and flashes `status.listLocked`, so the UI stops showing a
  change that never landed. The kv migration honours the outcome too.
- `createEmbeddedRow` re-reads the List through the BP graph after the
  create; locked (or unreadable — fail closed) means no members write, the
  row stays an unpinned orphan, and the error names it.
- Board drop and "+ New" re-check the live lock (`liveListLocked`) right
  before `schedulePersist`, after their awaits.

The known gap — rename, description, icon and delete of a locked List were
ungated:
- The pure CRUD returns a locked List untouched; the app flashes the
  refusal, the stage title / description are not editable while locked, and
  the sidebar Rename / Delete rows and the header ⋯ Remove render disabled
  with a new `list.menu.lockedHint` (all 6 packs).

Tests: persistence refusal + outcomes, locked CRUD no-ops, embedded create
race + fail-closed re-read.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the grid's fields, segment padding, shared input face

(a) csvToEntityImport wrote typed values under header keys but never
registered a def, so an imported column had no Change type and no
number unit — both read the catalog. propertyDefsForCsvImport mints
one def per property column (keyed by the header the values live
under, typed as inferred, Date at day granularity) and the import
persists them before the list is created; keys the catalog already
holds are left alone.
(b) the plain-number formatter already appends def.unit; a 'people'
unit rendered as a bare '3' only because the imported column had no
def to carry it — (a) is the fix, pinned by a test through formatScalar.
(c) createView started every view with columns: [] — a grid shows just
Name, a gallery shows no fields at all. seedColumnsForNewView copies
the visible columns of the view the user is on (else the list's first
view) onto the new one.
(d) .db-popover__segment had 2px of padding all round; now the row's
space-1 / space-2 like the calendar segments.
(e) the property form's name and paired-name inputs hand-rolled their
own face (one unstyled entirely); both are .bs-input now and the
private face is gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts:
#	apps/database/src/logic/list-crud.test.ts
The hourly retention pass (10.16) ran as one synchronous block: a vault-wide
scan, the whole plan, one delete transaction over every dropped row, and an
O(affected × rows) keep-set rescan per rewritten entity. At the byte cap on a
50k-row / 5k-entity vault that was a single 0.8–4.5 s block (3.7–18 s at
100k / 10k); the event loop never turned once.

- `pruneLocked` groups rows by entity ONCE on the way in; each rewritten
  entity's keep set is read off its group (linear, no rescans).
- The scan is paged by rowid (`RETENTION_SCAN_PAGE`), deletes go in
  `RETENTION_DELETE_CHUNK`-row transactions walked in scan order (2.5× cheaper
  per chunk than plan order), and the pass breathes (`yieldToEventLoop`)
  between pages, plan chunks, delete chunks and per-entity rewrites. A pass
  whose history is disposed mid-way stops instead of touching a closed vault.
- `planRetention` is now driven off a generator that pauses every
  `PLAN_CHUNK` candidates, including inside a chunked merge sort, so
  `planRetentionYielding` never holds the thread for a whole vault; the sync
  `planRetention` drives the same steps and is byte-identical in result.

Measured (Bun, in-process, 50k/5k and 100k/10k at the cap): wall 821 → 463 ms
and 3666 → 1290 ms; longest synchronous block = whole pass → 7.6 / 12.4 ms;
every scan page, plan step and per-entity rewrite under 5 ms; the residual
tail is SQLite commit/checkpoint I/O inside 1–2 of the delete chunks.

Tests: a 20k-row / 2k-entity golden (sha256 of the drop set captured from the
original single-pass planner) for both drivers, the yield count, and a
counting-repo test that every delete transaction holds ≤ 250 rows, one
transaction per rewritten entity, and the pass pauses after every step.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… through removeTestDir

Both new suites removed their temp vault with a raw rm; on the Windows
runner that is EBUSY while SQLite still holds the handle, so all 34 tests
failed on the Verify leg. Every sibling suite already goes through the
Windows-tolerant helper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
After importing files, List and Icons view showed each File row as
"H.. pdf Today" — the name a sliver, the Kind cell in the name's lane.
Bookmark and Event rows showed the same.

Cause: `.content-row__glyph:empty { display: none }` removed the glyph host
from the row grid for any entity without its own icon, so the NAME became
the first grid item and sized to the icon track while the Kind cell took the
`1fr` lane. The `auto` track masked it (the name took its max-content width,
misaligned but visible) until 2445b8b pinned the track to a fixed px per
folder; 9.8.16 (567d22a) did not touch it.

Fix the class, not the instance: one shared `RowGlyph` replaces `TileMedia`
and the two list-mode branches, and is never empty — image thumb, own icon,
folder glyph, extension chip (tiles) / file-kind glyph (lists), opener-app
squircle, generic entity glyph. The track is fixed in the stylesheet (20px,
never `auto`) and `listIconTrack` no longer collapses to 0; the `:empty`
collapse is gone.

Tests: `row-glyph.test.tsx` renders every row kind in all four view modes
and asserts a non-empty glyph host; `styles-row-grid.test.ts` asserts the
first grid track is fixed-width and no `:empty` collapse exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The context panel showed "Modified —" for a freshly imported file while
Files showed "Today" for the same row (9.20.15, release shot
preview-context-panel.png).

Cause: `entityToPreviewFile` read only `properties.updatedAt`, a property a
`File/v1` row from Files never carries — its recency lives on the entity
envelope, which is what Files' Modified column and Preview's own sidebar
ordering (`previewFilesFromEntities`) already read. The Files intent push
(`app.tsx`) was already correct; only Preview's self-resolve path lagged.

Fix: resolve `entity.updatedAt` first, then the property, the same way
Files does.

Tests: +3 on `entityToPreviewFile` — envelope only, envelope over a stale
property copy, property when the envelope has none.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Searching "coffee" in the shared emoji picker returned "No matches" (release
shot fail-icon-recents.png).

Cause: the search index was name + slug from `unicode-emoji-json`, which
carries only the official CLDR names — ☕ is "hot beverage", ✅ "check mark
button", ❤️ "red heart". None of the common names people type are in it.
The 8.11 Recent row (5b0e40a) is not involved: search replaces the sections
with the search results and never touched the matcher.

Fix at the data level: `emoji-keywords.ts` — a curated slug → aliases map
(GitHub/Slack-style shortcodes plus plain words for the common emoji).
`searchEmojis` matches aliases alongside name and slug; the `:shortcode:`
resolver and typeahead accept an alias too (`:coffee:` → ☕), with a real slug
always winning over an alias.

Tests: `emoji-keywords.test.ts` — coffee, heart, check, rocket and fire each
return a hit that includes the expected emoji; every alias key is a real
dataset slug (fails on a stale one); alias resolves as a shortcode; a real
slug still resolves first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… document

Today showed two entries and the quick capture landed in the wrong one. The
editor autofocuses on open, which fires the placeholder create before the first
vault snapshot arrives, so `ensureEntry` saw an empty day and minted
`journal-<key>` beside the seeded welcome entry titled with the same date. The
list then resolved the day by snapshot order (last row wins), so which entry
the editor, the capture and the word count targeted was a race.

- `logic/day-entry.ts`: one deterministic rule (earliest created, then id) for
  the list index, and `ensureDayEntry`, which asks the vault for the day's rows
  before minting — a day with a row anywhere in the vault reuses it (the
  welcome entry when it is today's). A seeded id now also opens on its day.
- `logic/capture-into-day.ts`: the capture resolves the day first, then
  appends through the live editor mounted on THAT entry; the editor bridge
  records the entry it is mounted on and refuses a sibling document, so the
  `body` snippet and word count are mirrored onto the row the list shows.

Tests: day-entry (pick / rows / ensure / open-by-id), capture-into-day,
index determinism, bridge note-id guard. apps/journal 244/244.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…utside

A side pane only ever left the split through the container's own paths
(Cmd+W in the pane, closeSidePane, dispose). A renderer closed from outside —
DevTools, another webContents.close(), the harness closing a page — stayed
registered as the side pane, so the split stayed open and the primary pane sat
at pane width (717 px on a 1440 window) for the rest of the window's life,
including after a park/unpark. Every pane now listens for its webContents
`destroyed` and leaves the container the same way a user close would; the
container's own teardown detaches the listener before it closes the renderer.

Pane geometry never reached the remembered window size: WindowIndex reads the
BaseWindow's bounds and launch-setup opens every window at the fixed default.
Tests pin both invariants: un-splitting hands the full body back to the
surviving pane, a divider drag or step never calls setBounds on the OS
window, the tracker keeps the window's bounds across a pane layout, and a
destroyed side pane / strip tab leaves the window.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t be undone

The Move-to-Bin prompt read "This can't be undone here" directly above the
"Don't ask again" checkbox — a checkbox the shared confirm memory offers only
because moving to the Bin IS reversible. The body now says the item goes to
the Bin and can be restored from there, in all six packs (en, de, fr, it, es,
pt). Code's file-delete prompt carried the same sentence and takes the same
copy. The irreversible kinds keep their wording.

A per-locale test on each app's shipped text pins the contract: the
Move-to-Bin body never claims irreversibility and always offers a restore.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…'s lineage walk

Renaming a tab (double-click, F2, the options menu) and adding a tab threw
`RangeError: Invalid array length` and reset the editor, so the rename input
never appeared. Chrome reports a focused `<input>` or a clicked strip control as
a document selection at its parent + child index, a DOM point inside the tabs
block but outside the panes slot. Lexical 0.21's `ElementDOMSlot.resolveChildIndex`
walks the lineage of such a point with `indexPath`, whose loops never advance
(`node = child.parentNode`, `sibling = node.previousSibling`), so it pushes until
V8 refuses the array; the `$beginUpdate` catch then rebuilds the DOM and the strip
loses its state.

`TabsNode.getDOMSlot` now resolves any point outside the panes slot into the
pane being shown (its `hidden` attribute is right there), so `$selectionPane`
never switches tabs under a rename and Lexical's own descent lands the caret in
the visible pane. Covered by two jsdom tests that place the selection on the
rename input and on the add button under a lowered `Array.prototype.push`
ceiling, which reproduces the exact RangeError before the fix.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…pt and the composer

"Maya Chen is typing…" was absolutely positioned above the composer, so once
the transcript filled the column it painted across the last message. The row
is now in flow with a reserved 20px height, so the label never overlaps content
and the list does not jump when it appears or clears.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r people

The agent's "Changes by the agent" panel listed `values: {}`,
`createdAt: 1792445200119` and `updatedAt: …` as if the agent had chosen them.
`AGENT_CHANGE_HIDDEN_KEYS` (the reserved provenance keys plus createdAt,
updatedAt, values) now drops those rows in the shell's `buildRunChanges` and,
defensively, in `<RunChanges>`; an empty object or array previews as nothing,
so a row that says nothing is omitted on both sides. `humanFieldChanges` /
`formatChangePreview` format an epoch stamp on a date-shaped key (`dueAt`,
`publishedDate`) through the shared date formatters, honouring the host's
`FormatContext`, which the Agent app passes from `useFormatContext`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ra3orblade
ra3orblade merged commit d6e3a9a into main Oct 20, 2026
3 checks passed
@ra3orblade
ra3orblade deleted the feat/0.15.0-market-demand branch October 20, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants