diff --git a/docs/public/icons/code-branch.svg b/docs/public/icons/code-branch.svg
deleted file mode 100644
index e2476b1d..00000000
--- a/docs/public/icons/code-branch.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/docs/public/icons/git-branch.svg b/docs/public/icons/git-branch.svg
new file mode 100644
index 00000000..2a8e4ef1
--- /dev/null
+++ b/docs/public/icons/git-branch.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/public/icons/lock.svg b/docs/public/icons/lock.svg
index 854d8372..954d58ac 100644
--- a/docs/public/icons/lock.svg
+++ b/docs/public/icons/lock.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/docs/public/image/build-and-change.gif b/docs/public/image/build-and-change.gif
new file mode 100644
index 00000000..e81950c5
Binary files /dev/null and b/docs/public/image/build-and-change.gif differ
diff --git a/docs/public/image/install.gif b/docs/public/image/install.gif
new file mode 100644
index 00000000..0b622e09
Binary files /dev/null and b/docs/public/image/install.gif differ
diff --git a/docs/public/image/og.png b/docs/public/image/og.png
new file mode 100644
index 00000000..7715c434
Binary files /dev/null and b/docs/public/image/og.png differ
diff --git a/docs/public/image/tui.gif b/docs/public/image/tui.gif
new file mode 100644
index 00000000..370afa75
Binary files /dev/null and b/docs/public/image/tui.gif differ
diff --git a/docs/public/image/tui/change-history.png b/docs/public/image/tui/change-history.png
new file mode 100644
index 00000000..1091c609
Binary files /dev/null and b/docs/public/image/tui/change-history.png differ
diff --git a/docs/public/image/tui/changes-list.png b/docs/public/image/tui/changes-list.png
new file mode 100644
index 00000000..cd0d5630
Binary files /dev/null and b/docs/public/image/tui/changes-list.png differ
diff --git a/docs/public/image/tui/config-add.png b/docs/public/image/tui/config-add.png
new file mode 100644
index 00000000..9e6cfeb0
Binary files /dev/null and b/docs/public/image/tui/config-add.png differ
diff --git a/docs/public/image/tui/config-list.png b/docs/public/image/tui/config-list.png
new file mode 100644
index 00000000..b6762c69
Binary files /dev/null and b/docs/public/image/tui/config-list.png differ
diff --git a/docs/public/image/tui/database-menu.png b/docs/public/image/tui/database-menu.png
new file mode 100644
index 00000000..06282e20
Binary files /dev/null and b/docs/public/image/tui/database-menu.png differ
diff --git a/docs/public/image/tui/explore-overview.png b/docs/public/image/tui/explore-overview.png
new file mode 100644
index 00000000..04ec7053
Binary files /dev/null and b/docs/public/image/tui/explore-overview.png differ
diff --git a/docs/public/image/tui/explore-table-detail.png b/docs/public/image/tui/explore-table-detail.png
new file mode 100644
index 00000000..4b304b0f
Binary files /dev/null and b/docs/public/image/tui/explore-table-detail.png differ
diff --git a/docs/public/image/tui/explore-tables.png b/docs/public/image/tui/explore-tables.png
new file mode 100644
index 00000000..a6b58b4e
Binary files /dev/null and b/docs/public/image/tui/explore-tables.png differ
diff --git a/docs/public/image/tui/home.png b/docs/public/image/tui/home.png
new file mode 100644
index 00000000..f80db4c5
Binary files /dev/null and b/docs/public/image/tui/home.png differ
diff --git a/docs/public/image/tui/identity.png b/docs/public/image/tui/identity.png
new file mode 100644
index 00000000..df35e95c
Binary files /dev/null and b/docs/public/image/tui/identity.png differ
diff --git a/docs/public/image/tui/lock.png b/docs/public/image/tui/lock.png
new file mode 100644
index 00000000..37666fd2
Binary files /dev/null and b/docs/public/image/tui/lock.png differ
diff --git a/docs/public/image/tui/log-viewer.png b/docs/public/image/tui/log-viewer.png
new file mode 100644
index 00000000..c9ea2f4b
Binary files /dev/null and b/docs/public/image/tui/log-viewer.png differ
diff --git a/docs/public/image/tui/more-menu.png b/docs/public/image/tui/more-menu.png
new file mode 100644
index 00000000..0a356844
Binary files /dev/null and b/docs/public/image/tui/more-menu.png differ
diff --git a/docs/public/image/tui/run-menu.png b/docs/public/image/tui/run-menu.png
new file mode 100644
index 00000000..fdb4a790
Binary files /dev/null and b/docs/public/image/tui/run-menu.png differ
diff --git a/docs/public/image/tui/secrets.png b/docs/public/image/tui/secrets.png
new file mode 100644
index 00000000..7a164860
Binary files /dev/null and b/docs/public/image/tui/secrets.png differ
diff --git a/docs/public/image/tui/settings.png b/docs/public/image/tui/settings.png
new file mode 100644
index 00000000..87cd5282
Binary files /dev/null and b/docs/public/image/tui/settings.png differ
diff --git a/docs/public/image/tui/sql-terminal.png b/docs/public/image/tui/sql-terminal.png
new file mode 100644
index 00000000..f55b9092
Binary files /dev/null and b/docs/public/image/tui/sql-terminal.png differ
diff --git a/docs/public/image/tui/vault.png b/docs/public/image/tui/vault.png
new file mode 100644
index 00000000..43276307
Binary files /dev/null and b/docs/public/image/tui/vault.png differ
diff --git a/docs/tapes/01-install.tape b/docs/tapes/01-install.tape
new file mode 100644
index 00000000..cb7090b7
--- /dev/null
+++ b/docs/tapes/01-install.tape
@@ -0,0 +1,65 @@
+# Install flow: get the binary, create an identity, bootstrap a project.
+#
+# `noorm init` is a real @clack/prompts wizard — the Type/Enter pairs below are
+# answering its "display name" and "email" prompts, not printing decoration.
+# Because no identity exists yet in the sandbox, init asks for one; if you
+# reorder this after `identity init` the wizard skips both prompts and the
+# keystrokes would leak into the shell.
+
+Source theme.tape
+
+Output ../public/image/install.gif
+
+Set Width 1600 # ~150 cols
+Set Height 460 # ~18 lines
+
+# Off-camera: build the sandbox and enter it. HOME is redirected so the
+# recording never reads or writes the real ~/.noorm.
+Hide
+# Strip coding-agent env vars so the recording shows a plain terminal and
+# not whoever's shell happened to render it. See env-scrub.sh.
+Type "source ./env-scrub.sh" Enter
+Type "./sandbox.sh fresh" Enter
+Wait+Screen@180s /sandbox ready/
+Type "export HOME=/tmp/noorm-demo/home" Enter
+Type "cd /tmp/noorm-demo/project" Enter
+Type "export PATH=$HOME/.local/bin:$PATH" Enter
+Type "export PS1='$ '" Enter
+Type "clear" Enter
+Show
+
+Sleep 1s
+
+Type "curl -fsSL https://noorm.dev/install.sh | sh" Sleep 500ms Enter
+
+# The installer prints "Installing ..." immediately, then spends ~15s pulling a
+# 67 MB binary from GitHub before printing "Installed ...". That transfer was
+# roughly half the finished GIF, all of it a motionless screen.
+#
+# So: catch the "Installing" line on camera, stop capturing across the
+# transfer, and resume once the binary has landed. Nothing is faked — the
+# output is the real installer's, and the frames that get dropped are the ones
+# where nothing changes. Hide only suspends capture; Wait still works inside it.
+Wait+Screen@60s /Installing noorm/
+Sleep 800ms
+Hide
+Wait+Screen@300s /Installed noorm/
+Show
+Sleep 3s
+
+Type "noorm identity init --name 'Ada Lovelace' --email 'ada@example.com'" Sleep 500ms Enter
+Wait+Screen@60s /Fingerprint/
+Sleep 4s
+
+Type "noorm init" Sleep 500ms Enter
+Sleep 6s
+
+Type "clear" Enter
+Sleep 500ms
+Type "noorm info" Sleep 500ms Enter
+Sleep 2s
+
+# Hold the final screen. PlaybackSpeed 2 halves playback, so this 12s of
+# tape reads as ~6s in the GIF — long enough to take in the last frame
+# before the loop restarts.
+Sleep 12s
diff --git a/docs/tapes/02-build-and-change.tape b/docs/tapes/02-build-and-change.tape
new file mode 100644
index 00000000..3856d131
--- /dev/null
+++ b/docs/tapes/02-build-and-change.tape
@@ -0,0 +1,56 @@
+# Run and change flows against a live Postgres.
+#
+# This is the tape that carries noorm's actual argument: `run build` applies
+# the SQL files as the current schema, `change ff` walks an existing database
+# forward, and `change history` shows what ran. The sandbox starts already
+# bootstrapped so the recording opens on the interesting part.
+
+Source theme.tape
+
+Output ../public/image/build-and-change.gif
+
+Set Width 1600 # ~150 cols
+Set Height 300 # ~10 lines; output never exceeds 8
+
+Hide
+# Strip coding-agent env vars so the recording shows a plain terminal and
+# not whoever's shell happened to render it. See env-scrub.sh.
+Type "source ./env-scrub.sh" Enter
+Type "./sandbox.sh bootstrapped" Enter
+Wait+Screen@180s /sandbox ready/
+Type "export HOME=/tmp/noorm-demo/home" Enter
+Type "cd /tmp/noorm-demo/project" Enter
+Type "export PATH=/tmp/noorm-demo/bin:$PATH" Enter
+Type "export PS1='$ '" Enter
+Type "clear" Enter
+Show
+
+Sleep 1s
+
+# The SQL files define the schema. A fresh database gets there in one command.
+Type "noorm run build" Sleep 500ms Enter
+Wait+Screen@300s /Build completed/
+Sleep 4s
+
+Type "clear" Enter
+Sleep 500ms
+
+# Two changes ship with the demo schema, neither applied yet.
+Type "noorm change list" Sleep 500ms Enter
+Wait+Screen@60s /pending change/
+Sleep 4s
+
+# Fast-forward walks the existing database to current.
+Type "noorm change ff --yes" Sleep 500ms Enter
+Wait+Screen@300s /Fast-forward/
+Sleep 6s
+
+Type "clear" Enter
+Sleep 500ms
+Type "noorm change history" Sleep 500ms Enter
+Sleep 2s
+
+# Hold the final screen. PlaybackSpeed 2 halves playback, so this 12s of
+# tape reads as ~6s in the GIF — long enough to take in the last frame
+# before the loop restarts.
+Sleep 12s
diff --git a/docs/tapes/03-tui.tape b/docs/tapes/03-tui.tape
new file mode 100644
index 00000000..fcf2f090
--- /dev/null
+++ b/docs/tapes/03-tui.tape
@@ -0,0 +1,193 @@
+# The TUI (`noorm ui`) — a full walkthrough on a project with nothing set up:
+# create a config, create the database, build the schema, fast-forward the
+# changes, read the history, then browse the result in the explorer.
+#
+# Three things this tape has to respect that the CLI tapes do not:
+#
+# 1. Size is the composition, not a crop. Ink lays out against the terminal
+# it is handed and does not reflow afterwards. The add-config form is the
+# tallest screen (10 fields rendered at once, not as steps) and sets the
+# height for the whole recording.
+# 2. Keys are not text. Single-key hotkeys need a Sleep after each one; Ink
+# repaints on its own schedule and a key sent mid-repaint is dropped.
+# 3. In the form, Enter on a text field SUBMITS THE WHOLE FORM — it does not
+# advance. Only Tab and the arrow keys move between fields. Enter is
+# correct on a select (it confirms the option and advances) and on the
+# final checkbox (it submits). Getting this wrong silently creates a
+# half-filled config instead of failing loudly.
+#
+# Screen keys, from src/tui:
+# home [r] Run [c] Config [g] Change [d] DB [q] Quit
+# config [a] Add
+# run [b] Build
+# change [f] FF [h] History
+# db [c] Create [x] Explore
+
+Source theme.tape
+
+Output ../public/image/tui.gif
+
+Set Width 1200 # ~111 cols
+Set Height 1160 # ~49 lines, sized to the add-config form
+Set FontSize 15
+Set TypingSpeed 90ms
+
+Hide
+# Strip coding-agent env vars so the recording shows a plain terminal and
+# not whoever's shell happened to render it. See env-scrub.sh.
+Type "source ./env-scrub.sh" Enter
+# `project` leaves a real project with an identity but no config and no
+# database — everything below is created on camera.
+Type "./sandbox.sh project" Enter
+Wait+Screen@180s /sandbox ready/
+Type "export HOME=/tmp/noorm-demo/home" Enter
+Type "cd /tmp/noorm-demo/project" Enter
+Type "export PATH=/tmp/noorm-demo/bin:$PATH" Enter
+Type "export PS1='$ '" Enter
+Type "clear" Enter
+Show
+
+Sleep 1s
+Type "noorm ui" Sleep 500ms Enter
+Sleep 4s
+
+# ── Config ────────────────────────────────────────────────────────────────
+Type "c"
+Sleep 2s
+Type "a"
+Sleep 2500ms
+
+# Config Name — Tab, never Enter.
+Type "dev"
+Sleep 800ms
+Tab
+Sleep 1s
+
+# Database Type — PostgreSQL is already highlighted; Enter confirms + advances.
+Enter
+Sleep 1200ms
+
+# Host — "localhost" is prefilled, so Tab straight past it.
+Tab
+Sleep 800ms
+
+# Port — the 5432 shown is a placeholder, not a value; the test container is
+# on 15432 and leaving this blank yields a config that cannot connect.
+Type "15432"
+Sleep 800ms
+Tab
+Sleep 600ms
+
+Type "noorm_demo"
+Sleep 800ms
+Tab
+Sleep 600ms
+
+Type "noorm_test"
+Sleep 800ms
+Tab
+Sleep 600ms
+
+Type "noorm_test"
+Sleep 800ms
+Tab
+Sleep 1s
+
+# User Role — Admin at the terminal.
+Enter
+Sleep 1200ms
+
+# Agent Role — already Viewer: DEFAULT_ACCESS is { user: 'admin', agent:
+# 'viewer' }, so an agent gets read-only against this config out of the box.
+# That access split is the point, so just confirm it.
+#
+# Do not "helpfully" arrow to Viewer here. This tape used to press Up twice to
+# get there from Admin, which was correct until the default changed; the same
+# two presses now wrap round to Admin and hand an agent full access while the
+# recording still claims read-only. Selects follow the default, not a position.
+Enter
+Sleep 1200ms
+
+# Test Database — Space toggles, Enter submits the form.
+Space
+Sleep 1s
+Enter
+Sleep 4s
+
+Escape
+Sleep 2s
+
+# Navigation is two levels deep everywhere below: Home -> list -> action. One
+# Escape backs out of the action to the list, a second returns to Home. Getting
+# this count wrong does not error, it just lands on the list screen and the next
+# hotkey means something else there — a single missing Escape sends "r" to the
+# DB list, where it opens Data Transfer instead of Run.
+
+# ── Create the database ───────────────────────────────────────────────────
+Type "d"
+Sleep 2500ms
+Type "c"
+Sleep 2500ms
+Type "y"
+Sleep 5s
+Escape
+Sleep 1500ms
+Escape
+Sleep 2s
+
+# ── Build the schema ──────────────────────────────────────────────────────
+# Build and fast-forward both gate on a confirm ("Run 4 SQL files on dev?").
+# Skipping the "y" does not error — Escape just cancels, and the recording
+# looks like it worked while the database stays empty.
+Type "r"
+Sleep 2s
+Type "b"
+Sleep 2500ms
+Type "y"
+Sleep 6s
+Escape
+Sleep 1500ms
+Escape
+Sleep 2s
+
+# ── Apply the changes ─────────────────────────────────────────────────────
+Type "g"
+Sleep 3s
+Type "f"
+Sleep 2500ms
+Type "y"
+Sleep 6s
+Escape
+Sleep 2s
+
+# History of what ran — [h] from the change list, so only one Escape back.
+Type "h"
+Sleep 5s
+Escape
+Sleep 1500ms
+Escape
+Sleep 2s
+
+# ── Explore the result ────────────────────────────────────────────────────
+Type "d"
+Sleep 2500ms
+Type "x"
+Sleep 5s
+
+# [1] Tables, then down to `task` — the payoff table. Its detail shows both the
+# inherited compound key (user_id + created_at + task_index, no surrogate id)
+# and the `priority` column the fast-forward just added, which is the whole
+# walkthrough in one screen.
+Type "1"
+Sleep 3s
+Down
+Sleep 700ms
+Down
+Sleep 1200ms
+Enter
+Sleep 3s
+
+# Hold the final screen. PlaybackSpeed 2 halves playback, so this 12s of
+# tape reads as ~6s in the GIF — long enough to take in the last frame
+# before the loop restarts.
+Sleep 12s
diff --git a/docs/tapes/04-screenshots.tape b/docs/tapes/04-screenshots.tape
new file mode 100644
index 00000000..7f5e4bca
--- /dev/null
+++ b/docs/tapes/04-screenshots.tape
@@ -0,0 +1,181 @@
+# Stills for the Terminal UI page (docs/tui.md).
+#
+# This tape produces no GIF worth keeping — the Output below is a throwaway.
+# The deliverables are the Screenshot PNGs, which replace the hand-drawn ASCII
+# mockups that used to sit in tui.md and had already drifted from the real
+# screens (the [c] Create label was wrong in both for a while).
+#
+# Run `./shots.sh` rather than calling vhs directly: it renders this tape and
+# then crops each PNG down to its own content, since one tape has one canvas
+# but the screens vary from ~14 to ~49 lines tall.
+#
+# Screen keys, from src/tui:
+# home [r] Run [c] Config [g] Change [d] DB [+] More [q] Quit
+# config [a] Add
+# change [f] FF [h] History
+# db [c] Create [x] Explore
+# global Shift+L log viewer, Shift+Q SQL terminal, ? help
+
+Source theme.tape
+
+Output ../../tmp/screenshots-throwaway.gif
+
+# Tall enough for the add-config form, the tallest screen in the app. Every
+# other shot gets cropped back down by shots.sh.
+Set Width 1200
+Set Height 1160
+Set FontSize 15
+Set TypingSpeed 90ms
+
+Hide
+Type "source ./env-scrub.sh" Enter
+Type "./sandbox.sh built" Enter
+Wait+Screen@180s /sandbox ready/
+Type "export HOME=/tmp/noorm-demo/home" Enter
+Type "cd /tmp/noorm-demo/project" Enter
+Type "export PATH=/tmp/noorm-demo/bin:$PATH" Enter
+# Apply one of the two changes so the Changes screen shows a real mix of
+# applied and pending rather than an all-pending list.
+Type "noorm change next --yes" Enter
+Sleep 4s
+Type "export PS1='$ '" Enter
+Type "clear" Enter
+Show
+
+Type "noorm ui" Enter
+Sleep 5s
+Screenshot shots/home.png
+Sleep 2s
+
+# ── Config ────────────────────────────────────────────────────────────────
+Type "c"
+Sleep 3s
+Screenshot shots/config-list.png
+Sleep 2s
+
+Type "a"
+Sleep 3s
+Screenshot shots/config-add.png
+Sleep 2s
+Escape
+Sleep 2s
+Escape
+Sleep 2s
+
+# ── Changes ───────────────────────────────────────────────────────────────
+Type "g"
+Sleep 3s
+Screenshot shots/changes-list.png
+Sleep 2s
+
+Type "h"
+Sleep 4s
+Screenshot shots/change-history.png
+Sleep 2s
+Escape
+Sleep 2s
+Escape
+Sleep 2s
+
+# ── Run ───────────────────────────────────────────────────────────────────
+Type "r"
+Sleep 3s
+Screenshot shots/run-menu.png
+Sleep 2s
+Escape
+Sleep 2s
+
+# ── Database ──────────────────────────────────────────────────────────────
+Type "d"
+Sleep 3s
+Screenshot shots/database-menu.png
+Sleep 2s
+
+Type "x"
+Sleep 4s
+Screenshot shots/explore-overview.png
+Sleep 2s
+
+Type "1"
+Sleep 3s
+Screenshot shots/explore-tables.png
+Sleep 2s
+
+Down
+Sleep 600ms
+Down
+Sleep 1s
+Enter
+Sleep 4s
+Screenshot shots/explore-table-detail.png
+Sleep 2s
+Escape
+Sleep 1500ms
+Escape
+Sleep 1500ms
+Escape
+Sleep 2s
+
+# ── Global overlays ───────────────────────────────────────────────────────
+# Shift+Q opens the SQL terminal from anywhere; VHS has no Shift+ modifier, so
+# an uppercase letter is how you send it (Ink reads the case as shift).
+Type "Q"
+Sleep 4s
+Screenshot shots/sql-terminal.png
+Sleep 2s
+Escape
+Sleep 2s
+
+Type "L"
+Sleep 3s
+Screenshot shots/log-viewer.png
+Sleep 2s
+Type "L"
+Sleep 2s
+Escape
+Sleep 2s
+
+# ── Secondary screens ─────────────────────────────────────────────────────
+# All reachable from Home directly, and also grouped under [+] More.
+Type "+"
+Sleep 3s
+Screenshot shots/more-menu.png
+Sleep 2s
+Escape
+Sleep 2s
+
+Type "l"
+Sleep 3s
+Screenshot shots/lock.png
+Sleep 2s
+Escape
+Sleep 2s
+
+Type "s"
+Sleep 3s
+Screenshot shots/settings.png
+Sleep 2s
+Escape
+Sleep 2s
+
+Type "i"
+Sleep 3s
+Screenshot shots/identity.png
+Sleep 2s
+Escape
+Sleep 2s
+
+Type "v"
+Sleep 3s
+Screenshot shots/vault.png
+Sleep 2s
+Escape
+Sleep 2s
+
+# Secrets are per-config, so they hang off the config list rather than Home.
+Type "c"
+Sleep 2500ms
+Type "k"
+Sleep 3s
+Screenshot shots/secrets.png
+Sleep 2s
diff --git a/docs/tapes/README.md b/docs/tapes/README.md
new file mode 100644
index 00000000..4ad0d460
--- /dev/null
+++ b/docs/tapes/README.md
@@ -0,0 +1,185 @@
+# Terminal recordings
+
+The GIFs in `docs/public/image/` are generated from the `.tape` files here with
+[VHS](https://github.com/charmbracelet/vhs). A tape is a script, not a capture,
+so re-recording after a CLI change is one command instead of a fresh take.
+
+| Tape | Output | Shows |
+|------|--------|-------|
+| `01-install.tape` | `install.gif` | `curl \| sh` install, `identity init`, `noorm init`, `noorm info` |
+| `02-build-and-change.tape` | `build-and-change.gif` | `run build`, `change list`, `change ff`, `change history` |
+| `03-tui.tape` | `tui.gif` | `noorm ui` — create a config, create the database, build, fast-forward changes, history, explorer |
+| `04-screenshots.tape` | `../public/image/tui/*.png` | Stills of every TUI screen, for `tui.md`. Run via `./shots.sh` |
+
+
+## Prerequisites
+
+```bash
+brew install vhs
+brew install --cask font-geist-mono # see "Font" below
+
+bun run build # tapes run dist/cli/index.js
+docker compose -f docker-compose.test.yml up -d --wait postgres
+```
+
+
+## Recording
+
+```bash
+cd docs/tapes
+vhs 01-install.tape
+vhs 02-build-and-change.tape
+vhs 03-tui.tape
+./shots.sh # 04-screenshots.tape + per-image cropping
+```
+
+Each tape builds its own sandbox first, so they can run in any order and none
+of them depends on a previous one.
+
+`shots.sh` is a wrapper, not an alternative: `04-screenshots.tape` produces
+stills at one canvas size (tall enough for the add-config form, the tallest
+screen in the app), and the script crops each one back down to its own content
+and re-pads it. Running the tape directly leaves every short screen sitting on
+a slab of empty terminal.
+
+
+## The sandbox
+
+`sandbox.sh` creates `/tmp/noorm-demo` and every tape calls it off-camera:
+
+```
+/tmp/noorm-demo/
+├── home/ → HOME during the recording; identity lands in home/.noorm
+├── bin/noorm → shim onto dist/cli/index.js
+└── project/ → schema copied from demo-project/
+```
+
+Three things this buys:
+
+- **Your `~/.noorm` is never touched.** `HOME` is redirected, so
+ `noorm identity init` writes to the sandbox. The identity in the GIFs is a
+ throwaway keypair generated at record time.
+- **Recordings reflect your working tree.** `bin/noorm` runs the local build,
+ not whatever version happens to be installed globally. `01-install.tape` is
+ the deliberate exception — it runs the real published installer, which
+ defaults to `$HOME/.local/bin` and therefore also stays in the sandbox.
+- **The path is short.** noorm's diagnostic log prints absolute file paths, and
+ a long prefix wraps every line.
+
+Modes: `fresh` (nothing set up), `project` (identity and a project, but no
+config and no database — the TUI walkthrough creates both on camera),
+`bootstrapped` (identity + config), `built` (also applies the schema).
+
+`sandbox.sh` drops the `noorm_demo` database on the test Postgres container
+each run, so recordings never inherit objects from a previous take — that
+failure mode shows up as `cannot drop columns from view` mid-build. Every mode
+except `project` recreates it immediately; `project` leaves it absent so the
+TUI walkthrough can create it on camera.
+
+
+## The demo schema
+
+`demo-project/` is a 4-file schema: `app_user`, `project`, `task`, and an
+`open_task` view, plus two changes that add a `priority` column and update the
+view. It models noorm's own argument — `project` and `task` use inherited
+compound keys rather than a surrogate ID per table.
+
+The SQL files describe the schema **as it exists today**, including `priority`.
+The changes exist for databases built before that column landed, and are
+written idempotently (`ADD COLUMN IF NOT EXISTS`) so they are safe on both.
+
+To record against the full example instead:
+
+```bash
+NOORM_DEMO_SCHEMA=todo-db ./sandbox.sh bootstrapped
+```
+
+Be aware this does not currently produce a usable recording. `run build` logs
+one line per file carrying the absolute path twice (~250 characters), so 60
+files is thousands of wrapped lines; VHS renders every frame and the build does
+not finish inside a 5-minute wait. The 4-file schema exists for that reason.
+
+
+## Font
+
+Geist Mono is the brand face and is **not** bundled. Without it VHS silently
+falls back to a much wider default, and the `Set Width` values here — which are
+sized in pixels, not columns — produce a terminal too narrow for the output.
+Symptom is every line wrapping mid-path.
+
+Sizing was measured with `tput cols; tput lines`: at `FontSize 16` one Geist
+Mono cell is about 10.2 x 22.4 px, so
+
+```
+Width = columns * 10.2 + 64 (64 = Padding * 2)
+Height = lines * 22.4 + 64
+```
+
+Re-measure if you change the font or size — do not scale the numbers by eye.
+
+
+## Notes on VHS
+
+Things that cost time to discover, kept here so they only cost it once:
+
+- **`Type` cannot contain escaped quotes.** `Type "f() { x \"$@\"; }"` is a
+ parse error, which is why the `noorm` shim is a file in `sandbox.sh` rather
+ than a shell function defined in the tape.
+- **A leading underscore in a filename breaks `Source`.** `Source _theme.tape`
+ fails to parse; `theme.tape` is fine.
+- **The default prompt is `>`, not `$`.** Waits written against `/\$ $/` never
+ match. Every tape sets `PS1` explicitly in its hidden block.
+- **`Wait` matches the current line; `Wait+Screen` matches the whole screen.**
+ Anything that has scrolled needs `Wait+Screen`.
+- **Order matters in the hidden block.** Sourcing a file from this directory
+ has to happen before the tape `cd`s into the sandbox, or it silently fails
+ and `clear` wipes the error.
+- Prefer waiting on real output (`/Build completed/`) over waiting on a prompt.
+- **`Set PlaybackSpeed 2` shortens the GIF, not the render.** VHS still waits
+ out every `Sleep`, so a tape that takes eight minutes to record still does.
+ Keep the waits generous and speed up the output instead of trimming Sleeps —
+ a shorter wait is what drops a step on a slower machine.
+- **Dwells are written for 2x, not 1x.** A `Sleep 2s` holding a block of output
+ is only 1s on screen once PlaybackSpeed halves it — not long enough to read
+ before the next command or a `clear` wipes it. Output dwells here are 4-6s of
+ tape (2-3s visible). Double the tape value, not the visible one.
+- **`Hide` is the tool for unavoidable dead air.** `01-install.tape` pulls a
+ 67 MB binary from GitHub, ~15s of motionless screen that was half the GIF.
+ The tape catches the "Installing" line on camera, hides across the transfer,
+ and shows again on "Installed". Nothing is faked — the output is the real
+ installer's, and only frames where nothing changes get dropped. `Wait` still
+ works while hidden, which is what makes this possible.
+- **`Screenshot` needs a `Sleep` after it.** The write is flushed
+ asynchronously, so without a pause the next keypress can change the screen
+ before the PNG lands and you get the *following* screen under the current
+ screen's filename. This is silent — the file exists and looks plausible.
+- **`Screenshot` rejects absolute paths.** Use a filename relative to wherever
+ you invoke `vhs`. It is the fastest way to iterate on a TUI sequence, since
+ you get the exact frames you care about without coalescing the whole GIF.
+
+Driving the TUI adds three more, all of which fail *silently* — the recording
+looks plausible while the database stays empty, so verify against the database
+rather than against the GIF:
+
+- **Enter on a text field submits the whole form.** It does not advance. Only
+ Tab and the arrows move between fields. Enter is correct on a select (confirm
+ + advance) and on the final checkbox (submit).
+- **Run Build and Change FF each gate on a confirm** ("Run 4 SQL files on
+ dev?"). Miss the `y` and Escape simply cancels the operation.
+- **Navigation is two levels deep**: Home → list → action. One Escape returns
+ to the list, a second to Home. One missing Escape sends the next hotkey to
+ the wrong screen — `r` on the DB list opens Data Transfer, not Run.
+
+
+## Known rough edges
+
+Both are in the CLI, not the tapes:
+
+- `run build` at the default log level prints one `file:after` line per file
+ with the absolute path repeated in a `filepath=` field. It is too verbose to
+ record on a real schema.
+- There is no summary-only log level. `NOORM_LOG_LEVEL=warn` suppresses the
+ per-file lines *and* the `Build completed` summary, so a successful build
+ prints nothing at all. `settings.logging.level` in `.noorm/settings.yml` does
+ not gate the diagnostic stream either — only `NOORM_LOG_LEVEL` has any
+ effect, and only those two settings exist in practice.
diff --git a/docs/tapes/demo-project/changes/2026-02-01-add-task-priority/change/001_add_priority.sql b/docs/tapes/demo-project/changes/2026-02-01-add-task-priority/change/001_add_priority.sql
new file mode 100644
index 00000000..96212c0f
--- /dev/null
+++ b/docs/tapes/demo-project/changes/2026-02-01-add-task-priority/change/001_add_priority.sql
@@ -0,0 +1,5 @@
+ALTER TABLE task ADD COLUMN IF NOT EXISTS priority INT NOT NULL DEFAULT 3;
+
+ALTER TABLE task DROP CONSTRAINT IF EXISTS task_priority_range;
+ALTER TABLE task ADD CONSTRAINT task_priority_range
+ CHECK (priority BETWEEN 1 AND 5);
diff --git a/docs/tapes/demo-project/changes/2026-02-01-add-task-priority/changelog.md b/docs/tapes/demo-project/changes/2026-02-01-add-task-priority/changelog.md
new file mode 100644
index 00000000..fb9f0c19
--- /dev/null
+++ b/docs/tapes/demo-project/changes/2026-02-01-add-task-priority/changelog.md
@@ -0,0 +1,5 @@
+# Add task priority
+
+Tasks gained a 1-5 priority with a check constraint. The SQL file in
+`sql/01_tables/003_task.sql` describes the column as it exists today; this
+change is how databases built before it catch up.
diff --git a/docs/tapes/demo-project/changes/2026-02-01-add-task-priority/revert/001_add_priority.sql b/docs/tapes/demo-project/changes/2026-02-01-add-task-priority/revert/001_add_priority.sql
new file mode 100644
index 00000000..d5fa4bac
--- /dev/null
+++ b/docs/tapes/demo-project/changes/2026-02-01-add-task-priority/revert/001_add_priority.sql
@@ -0,0 +1,2 @@
+ALTER TABLE task DROP CONSTRAINT IF EXISTS task_priority_range;
+ALTER TABLE task DROP COLUMN IF EXISTS priority;
diff --git a/docs/tapes/demo-project/changes/2026-02-08-priority-in-view/change/001_update_view.sql b/docs/tapes/demo-project/changes/2026-02-08-priority-in-view/change/001_update_view.sql
new file mode 100644
index 00000000..12a4273c
--- /dev/null
+++ b/docs/tapes/demo-project/changes/2026-02-08-priority-in-view/change/001_update_view.sql
@@ -0,0 +1,6 @@
+CREATE OR REPLACE VIEW open_task AS
+SELECT t.user_id, t.created_at, t.task_index, t.title, t.priority,
+ p.name AS project_name
+FROM task t
+JOIN project p ON p.user_id = t.user_id AND p.created_at = t.created_at
+WHERE t.done = false;
diff --git a/docs/tapes/demo-project/changes/2026-02-08-priority-in-view/revert/001_update_view.sql b/docs/tapes/demo-project/changes/2026-02-08-priority-in-view/revert/001_update_view.sql
new file mode 100644
index 00000000..d26015e8
--- /dev/null
+++ b/docs/tapes/demo-project/changes/2026-02-08-priority-in-view/revert/001_update_view.sql
@@ -0,0 +1 @@
+DROP VIEW IF EXISTS open_task;
diff --git a/docs/tapes/demo-project/sql/01_tables/001_user.sql b/docs/tapes/demo-project/sql/01_tables/001_user.sql
new file mode 100644
index 00000000..338cd4be
--- /dev/null
+++ b/docs/tapes/demo-project/sql/01_tables/001_user.sql
@@ -0,0 +1,5 @@
+CREATE TABLE IF NOT EXISTS app_user (
+ user_id SERIAL PRIMARY KEY,
+ email TEXT NOT NULL UNIQUE,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
diff --git a/docs/tapes/demo-project/sql/01_tables/002_project.sql b/docs/tapes/demo-project/sql/01_tables/002_project.sql
new file mode 100644
index 00000000..40c6d10c
--- /dev/null
+++ b/docs/tapes/demo-project/sql/01_tables/002_project.sql
@@ -0,0 +1,9 @@
+CREATE TABLE IF NOT EXISTS project (
+ user_id INT NOT NULL REFERENCES app_user(user_id),
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ name TEXT NOT NULL,
+
+ -- Inherited key: a project is identified by its owner plus its creation
+ -- instant. No surrogate project_id, so every child row carries the owner.
+ PRIMARY KEY (user_id, created_at)
+);
diff --git a/docs/tapes/demo-project/sql/01_tables/003_task.sql b/docs/tapes/demo-project/sql/01_tables/003_task.sql
new file mode 100644
index 00000000..5c92da4d
--- /dev/null
+++ b/docs/tapes/demo-project/sql/01_tables/003_task.sql
@@ -0,0 +1,12 @@
+CREATE TABLE IF NOT EXISTS task (
+ user_id INT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL,
+ task_index INT NOT NULL,
+ title TEXT NOT NULL,
+ done BOOLEAN NOT NULL DEFAULT false,
+ priority INT NOT NULL DEFAULT 3,
+
+ PRIMARY KEY (user_id, created_at, task_index),
+ FOREIGN KEY (user_id, created_at) REFERENCES project(user_id, created_at),
+ CONSTRAINT task_priority_range CHECK (priority BETWEEN 1 AND 5)
+);
diff --git a/docs/tapes/demo-project/sql/02_views/001_open_tasks.sql b/docs/tapes/demo-project/sql/02_views/001_open_tasks.sql
new file mode 100644
index 00000000..12a4273c
--- /dev/null
+++ b/docs/tapes/demo-project/sql/02_views/001_open_tasks.sql
@@ -0,0 +1,6 @@
+CREATE OR REPLACE VIEW open_task AS
+SELECT t.user_id, t.created_at, t.task_index, t.title, t.priority,
+ p.name AS project_name
+FROM task t
+JOIN project p ON p.user_id = t.user_id AND p.created_at = t.created_at
+WHERE t.done = false;
diff --git a/docs/tapes/env-scrub.sh b/docs/tapes/env-scrub.sh
new file mode 100644
index 00000000..f51a2ad0
--- /dev/null
+++ b/docs/tapes/env-scrub.sh
@@ -0,0 +1,20 @@
+# shellcheck shell=bash
+# Sourced (not executed) by every tape before recording starts.
+#
+# `noorm info` reports a detected coding agent, e.g. "Agent: Claude Code
+# (CLAUDECODE, CLAUDE_CODE_ENTRYPOINT)". That reflects whoever rendered the
+# GIF, not anything about noorm, so it must not end up in a published asset.
+#
+# Matched by pattern rather than an explicit list: agent detection lives in the
+# released binary, which moves independently of this repo, so a hardcoded list
+# would silently rot the next time a vendor adds a variable.
+
+# -E (ERE) is required: BSD/macOS sed does not support \| alternation in BRE,
+# so the basic-regex form matches nothing here and silently scrubs nothing.
+for _noorm_tape_var in $(
+ env | sed -nE 's/^(CLAUDE[A-Z_0-9]*|AI_AGENT|CURSOR[A-Z_0-9]*|AIDER[A-Z_0-9]*|COPILOT[A-Z_0-9]*|WARP[A-Z_0-9]*|TERM_PROGRAM[A-Z_0-9]*)=.*/\1/p'
+); do
+ unset "$_noorm_tape_var"
+done
+
+unset _noorm_tape_var
diff --git a/docs/tapes/sandbox.sh b/docs/tapes/sandbox.sh
new file mode 100755
index 00000000..912249d9
--- /dev/null
+++ b/docs/tapes/sandbox.sh
@@ -0,0 +1,153 @@
+#!/usr/bin/env bash
+#
+# Builds the isolated demo project the .tape files record against.
+#
+# Everything lands under /tmp/noorm-demo — a deliberately short path, because
+# noorm's diagnostic log prints absolute file paths and a long prefix wraps
+# every line in the recording.
+#
+# HOME is redirected into the sandbox so `noorm identity init` writes to
+# $DEMO_ROOT/home/.noorm and never touches the real ~/.noorm.
+#
+# Usage:
+# ./sandbox.sh # fresh project, no identity, no config
+# ./sandbox.sh bootstrapped # identity + config + schema already applied
+#
+set -euo pipefail
+
+DEMO_ROOT="${NOORM_DEMO_ROOT:-/tmp/noorm-demo}"
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+MODE="${1:-fresh}"
+
+PG_CONTAINER="noorm-test-postgres"
+PG_DB="noorm_demo"
+PG_PORT=15432
+PG_USER=noorm_test
+PG_PASS=noorm_test
+
+# The demo runs the built CLI, not a globally installed noorm, so a recording
+# always reflects this working tree.
+NOORM_BIN="$REPO_ROOT/dist/cli/index.js"
+
+if [ ! -f "$NOORM_BIN" ]; then
+
+ echo "error: $NOORM_BIN missing — run 'bun run build' first" >&2
+ exit 1
+fi
+
+if ! docker ps --format '{{.Names}}' | grep -qx "$PG_CONTAINER"; then
+
+ echo "error: $PG_CONTAINER not running — start it with:" >&2
+ echo " docker compose -f docker-compose.test.yml up -d --wait postgres" >&2
+ exit 1
+fi
+
+# Guard the destructive step: only ever remove a path we just derived from
+# DEMO_ROOT, and only if it looks like our sandbox.
+case "$DEMO_ROOT" in
+ /tmp/*|"$TMPDIR"*) ;;
+ *) echo "error: refusing to wipe '$DEMO_ROOT' — must live under /tmp" >&2; exit 1 ;;
+esac
+
+rm -rf "$DEMO_ROOT"
+mkdir -p "$DEMO_ROOT/home" "$DEMO_ROOT/project" "$DEMO_ROOT/bin"
+
+# A `noorm` on PATH backed by this working tree, so the run/change/TUI tapes
+# record the code you have checked out rather than the last published release.
+# VHS's Type command cannot contain escaped quotes, so the shim has to be a
+# real file — a shell function defined inside the tape will not parse.
+cat > "$DEMO_ROOT/bin/noorm" <
&2
+ exit 1
+ ;;
+esac
+
+# Always drop, so a recording never inherits objects from a previous take —
+# that failure mode surfaces mid-build as "cannot drop columns from view".
+#
+# `project` mode stops there and leaves the database absent, because the TUI
+# walkthrough creates it on camera via the db screen. Every other mode needs it
+# to exist up front.
+docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d postgres \
+ -c "DROP DATABASE IF EXISTS $PG_DB;" >/dev/null
+
+if [ "$MODE" != "project" ]; then
+
+ docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d postgres \
+ -c "CREATE DATABASE $PG_DB;" >/dev/null
+fi
+
+# `project` stops after identity + `noorm init`: a real project with no config
+# yet, which is where the TUI walkthrough starts (it creates the config itself).
+if [ "$MODE" = "project" ]; then
+
+ export HOME="$DEMO_ROOT/home"
+ cd "$DEMO_ROOT/project"
+
+ node "$NOORM_BIN" identity init \
+ --name "Ada Lovelace" --email "ada@example.com" >/dev/null
+
+ node "$NOORM_BIN" init --yes >/dev/null
+fi
+
+if [ "$MODE" = "bootstrapped" ] || [ "$MODE" = "built" ]; then
+
+ export HOME="$DEMO_ROOT/home"
+ cd "$DEMO_ROOT/project"
+
+ node "$NOORM_BIN" identity init \
+ --name "Ada Lovelace" --email "ada@example.com" >/dev/null
+
+ # `noorm config add` is TUI-only, so a scripted bootstrap goes through
+ # `ci init`, which reads the same connection details from NOORM_* env.
+ NOORM_IDENTITY_PRIVATE_KEY="$(cat "$HOME/.noorm/identity.key")" \
+ NOORM_IDENTITY_NAME="Ada Lovelace" \
+ NOORM_IDENTITY_EMAIL="ada@example.com" \
+ NOORM_CONNECTION_DIALECT=postgres \
+ NOORM_CONNECTION_HOST=localhost \
+ NOORM_CONNECTION_PORT="$PG_PORT" \
+ NOORM_CONNECTION_DATABASE="$PG_DB" \
+ NOORM_CONNECTION_USER="$PG_USER" \
+ NOORM_CONNECTION_PASSWORD="$PG_PASS" \
+ node "$NOORM_BIN" ci init --name dev --force >/dev/null
+fi
+
+# `built` also applies the schema, so the TUI opens on a real database instead
+# of reporting "empty database" on its home screen. Changes stay pending — that
+# is the state worth showing.
+if [ "$MODE" = "built" ]; then
+
+ node "$NOORM_BIN" run build >/dev/null 2>&1
+fi
+
+echo "sandbox ready at $DEMO_ROOT ($MODE)"
diff --git a/docs/tapes/shots.sh b/docs/tapes/shots.sh
new file mode 100755
index 00000000..fcb0a673
--- /dev/null
+++ b/docs/tapes/shots.sh
@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+#
+# Renders 04-screenshots.tape and crops each still down to its own content.
+#
+# One tape has one canvas, but the TUI screens run from ~14 to ~49 lines. The
+# canvas is sized for the tallest (the add-config form), so every other shot
+# comes out with a slab of empty terminal below it. This trims that back off
+# and re-adds even padding, so each image is sized to what it actually shows.
+#
+# Output lands in ../public/image/tui/.
+#
+set -euo pipefail
+
+cd "$(dirname "${BASH_SOURCE[0]}")"
+
+BG="#161A20" # warm ink, the dark-mode terminal background
+PAD=28
+SRC="shots"
+DEST="../public/image/tui"
+
+command -v vhs >/dev/null || { echo "error: vhs not installed (brew install vhs)" >&2; exit 1; }
+command -v magick >/dev/null || { echo "error: imagemagick not installed" >&2; exit 1; }
+
+rm -rf "$SRC"
+mkdir -p "$SRC" "$DEST" ../../tmp
+
+vhs 04-screenshots.tape
+
+shopt -s nullglob
+shots=("$SRC"/*.png)
+
+if [ ${#shots[@]} -eq 0 ]; then
+
+ echo "error: tape produced no screenshots" >&2
+ exit 1
+fi
+
+for f in "${shots[@]}"; do
+
+ name="$(basename "$f")"
+
+ # -trim removes the uniform background on all sides, including the padding
+ # VHS drew, so the padding is re-added afterwards at a known width. +repage
+ # resets the virtual canvas -trim leaves behind, without which the offset
+ # comes back on the next operation.
+ magick "$f" \
+ -bordercolor "$BG" -border 1 \
+ -fuzz 2% -trim +repage \
+ -bordercolor "$BG" -border "$PAD" \
+ "$DEST/$name"
+
+ printf '%-28s %s\n' "$name" "$(magick identify -format '%wx%h' "$DEST/$name")"
+done
+
+rm -rf "$SRC" ../../tmp/screenshots-throwaway.gif
+
+echo
+echo "wrote ${#shots[@]} stills to $DEST"
diff --git a/docs/tapes/theme.tape b/docs/tapes/theme.tape
new file mode 100644
index 00000000..2e6886e0
--- /dev/null
+++ b/docs/tapes/theme.tape
@@ -0,0 +1,40 @@
+# Shared look for every noorm recording. Sourced, not run on its own.
+#
+# Palette is the brand's warm-ink dark mode from
+# .claude/skills/noorm-design/colors_and_type.css — ink #161A20 background,
+# cream #F2ECE0 text, ember #E05742 cursor. ANSI slots map onto the seed
+# family (bronze for magenta, mossy green for green) so any color the CLI
+# emits lands somewhere on-brand instead of on a stock terminal red.
+
+Require docker
+Require node
+
+Set Shell bash
+
+# Geist Mono is the brand face but is NOT bundled — install it or VHS silently
+# falls back to a much wider default and every line wraps. See README.md.
+# brew install --cask font-geist-mono
+Set FontFamily "Geist Mono"
+Set FontSize 16
+Set Padding 32
+Set BorderRadius 10
+Set TypingSpeed 45ms
+Set Framerate 24
+
+# 2x playback. The Sleep values in each tape are chosen for the CLI's real
+# latency — long enough that a slow build or a confirm prompt never gets cut
+# off — which makes honest recordings feel sluggish. Halving them instead would
+# start losing steps on a slower machine, so keep the waits generous and speed
+# up the finished GIF. Also halves the frame count, so the files get smaller.
+#
+# This does NOT shorten render wall-clock: VHS still waits out every Sleep.
+Set PlaybackSpeed 2
+
+# Sizing math, measured with `tput cols; tput lines` at FontSize 16:
+# usable width = Width - 64, and one Geist Mono cell is ~10.2 x 22.4 px. So
+# Width = cols * 10.2 + 64
+# Height = lines * 22.4 + 64
+# noorm's diagnostic log prints absolute paths, so the CLI tapes need a wide
+# terminal (~150 cols) or the output wraps mid-path and reads as noise.
+
+Set Theme { "background": "#161A20", "foreground": "#F2ECE0", "cursor": "#E05742", "selection": "#2B3340", "black": "#212931", "red": "#E05742", "green": "#6E8F4E", "yellow": "#C2873C", "blue": "#4B7398", "magenta": "#916336", "cyan": "#D2A47A", "white": "#F2ECE0", "brightBlack": "#7E8590", "brightRed": "#ED7561", "brightGreen": "#8FAE6E", "brightYellow": "#D2A47A", "brightBlue": "#6E93B8", "brightMagenta": "#B47A45", "brightWhite": "#FFFFFF" }
diff --git a/docs/tui.md b/docs/tui.md
index 5995f8bc..19dcb02f 100644
--- a/docs/tui.md
+++ b/docs/tui.md
@@ -7,6 +7,8 @@ Launch the interactive terminal interface with:
noorm ui
```
+
+
Everything in noorm is accessible through keyboard shortcuts. No mouse needed.
The TUI is a dedicated subcommand — every other `noorm` command runs as a non-interactive CLI. Running `noorm` on its own prints the command list (citty's `--help`) instead of opening the wizard, so the entry into the TUI is always explicit. See the [CLI Reference](/headless) for the headless surface.
@@ -14,31 +16,7 @@ The TUI is a dedicated subcommand — every other `noorm` command runs as a non-
## Home Screen
-```
-noorm - Database Schema & Change Manager
-
-Active Config: dev | Configs: 2
-
-┌─ Status ────────────────────────────┐ ┌─ Quick Actions ──────────────┐
-│ │ │ │
-│ Connection: ● Connected │ │ [1] Run Build │
-│ Pending: 0 pending │ │ [2] Apply Changes (ff) │
-│ Lock: FREE │ │ [3] View Lock Status │
-│ │ │ │
-│ Stage Configs: │ │ │
-│ ✓ dev │ │ │
-│ prod [user:operator mcp:viewer] │ │ │
-│ │ │ │
-└─────────────────────────────────────┘ └──────────────────────────────┘
-
-┌─ Recent Activity ────────────────────────────────────────────┐
-│ │
-│ [OK] [BUILD] build:2024-01-15T10:30:00Z in 2 hours (0.3s) │
-│ │
-└──────────────────────────────────────────────────────────────┘
-
-[c]onfig chan[g]e [r]un [d]b [l]ock [s]ettings [k]eys [i]dentity [q]uit
-```
+
## Navigation Map
@@ -74,40 +52,60 @@ Active Config: dev | Configs: 2
| Key | Screen | Description |
|-----|--------|-------------|
+| `r` | Run | Execute schema files |
| `c` | Config | Manage database connections |
| `g` | Changes | View and apply changes |
-| `r` | Run | Execute schema files |
| `d` | Database | Explore schema, run queries |
-| `l` | Lock | View/manage database locks |
+| `+` | More | Settings, vault, identity, lock |
| `s` | Settings | Project configuration |
-| `k` | Secrets | Manage encrypted secrets |
+| `v` | Vault | Team-shared encrypted secrets |
| `i` | Identity | View/edit your identity |
+| `l` | Lock | View/manage database locks |
+| `u` | Update | Check for a newer noorm |
| `q` | — | Quit noorm |
+`s`, `v`, `i`, and `l` work from Home directly as well as from `[+] More`.
+Three number keys run the quick actions listed on the home screen:
+
+| Key | Action |
+|-----|--------|
+| `1` | Run build |
+| `2` | Apply changes (fast-forward) |
+| `3` | View lock status |
+
+Per-config **secrets** are not on this list — they hang off a config rather
+than the project, so you reach them with `k` from the config list.
+
### Common Actions (in sub-screens)
| Key | Action | Available In |
|-----|--------|--------------|
-| `a` | Add new | Config, Changes, Secrets, Settings |
-| `e` | Edit | Config, Secrets, Settings |
+| `a` | Add new | Config, Changes, Secrets |
+| `e` | Edit | Config, Changes, Secrets |
| `d` | Delete | Config, Changes, Secrets |
| `c` | Copy | Config |
-| `v` | Validate | Config (test connection) |
-| `x` | Export | Config, Identity |
-| `i` | Import | Config |
+| `k` | Secrets | Config (secrets for the highlighted config) |
+| `+` | More | Config (export, import, validate) |
| `Enter` | Use/Activate | Config (set as active) |
+Export, import, and validate live behind `[+] More` on the config list rather
+than on the list itself, which keeps the destructive and the routine actions
+apart.
+
### List Navigation
| Key | Action |
|-----|--------|
-| `↑` / `k` | Move up |
-| `↓` / `j` | Move down |
+| `↑` | Move up |
+| `↓` | Move down |
| `Enter` | Select |
| `Escape` | Go back |
-| `1`-`9` | Quick select item by number |
+| `1`-`9` | Quick select by number, on lists that show numbers |
+
+Numbered selection is enabled per list — if a list renders numbers down its
+left edge (Settings and the schema explorer do), the digits work there.
### Global Shortcuts
@@ -115,6 +113,8 @@ Active Config: dev | Configs: 2
| Key | Action |
|-----|--------|
| `Shift+L` | Toggle log viewer overlay |
+| `Shift+Q` | Open the SQL terminal |
+| `?` | Show help |
| `Escape` | Go back / Cancel |
| `Ctrl+C` | Quit |
@@ -124,21 +124,7 @@ Active Config: dev | Configs: 2
### Config List
-```
-Home > Configurations
-
-┌─ Configurations ────────────────────────────────────────────┐
-│ │
-│ > ○ dev postgres │
-│ ● test postgres (active) [test] │
-│ ○ prod postgres [user:operator mcp:off] │
-│ │
-└─────────────────────────────────────────────────────────────┘
-
-[a] Add [e] Edit [d] Delete [c] Copy [x] Export [i] Import [v] Validate [Enter] Use
-
-[Esc] Back
-```
+
- `●` indicates active config
- `○` indicates inactive config
@@ -147,26 +133,16 @@ Home > Configurations
- `[test]` tag shows test configs
- Press `Enter` on a config to activate it
+Press `a` to add one. Adding a config is the one operation that is interactive only — `noorm config add` on the CLI directs you here:
-### Changes List
+
-```
-Home > Changes
+The two role fields set access per **channel** — who is *driving*. `User Role` covers a human on the CLI, TUI, or SDK; `Agent Role` covers an AI agent, over MCP and the CLI alike. They are independent, so a config can be wide open at your terminal and read-only — or invisible — to an agent. New configs default to `admin` for you and `viewer` for the agent. See [Configs](/guide/environments/configs#access-roles) for what each role permits.
-┌─ Changes ───────────────────────────────────────────────────┐
-│ │
-│ Total: 3 Applied: 2 Pending: 1 │
-│ │
-│ > ✓ 2024-01-15-init-schema │
-│ ✓ 2024-01-20-add-user-roles │
-│ ○ 2024-02-01-add-notifications │
-│ │
-└─────────────────────────────────────────────────────────────┘
-[a]dd [e]dit [d]elete [r]un re[v]ert [n]ext [f]f re[w]ind [h]istory
+### Changes List
-[Esc] Back
-```
+
- `✓` = Applied
- `○` = Pending
@@ -178,166 +154,39 @@ When no changes exist:
No changes found. Press [a] to create one.
```
+Press `h` for the execution history — what ran, when, and who ran it:
+
+
+
### Run Menu
-```
-Home > Run SQL
-
-┌─ Run SQL Files ─────────────────────────────────────────────┐
-│ │
-│ Config: dev (local) │
-│ Schema Path: sql │
-│ │
-│ Effective Build Paths: │
-│ Include: tables, views │
-│ │
-└─────────────────────────────────────────────────────────────┘
-
-┌─ Available Actions ─────────────────────────────────────────┐
-│ │
-│ [b] Build - Execute full schema build │
-│ [e] Exec - Pick files to execute │
-│ [f] File - Execute a single file │
-│ [d] Dir - Execute all files in a directory │
-│ │
-└─────────────────────────────────────────────────────────────┘
-
-[b] Build [e] Exec [f] File [d] Dir [Esc] Back
-```
+
### Database Menu
-```
-Home > Databases
-
-┌─ Database Operations ───────────────────────────────────────┐
-│ │
-│ Config: dev │
-│ Connection: CONNECTED │
-│ Tracking Tables: Initialized │
-│ Tracked Executions: 12 │
-│ │
-└─────────────────────────────────────────────────────────────┘
-
-┌─ Available Actions ─────────────────────────────────────────┐
-│ │
-│ [c] Create - Build database from SQL files │
-│ [d] Destroy - Drop all managed objects │
-│ [x] Explore - Browse database schema │
-│ [w] Wipe - Truncate table data (keep schema) │
-│ [t] Teardown - Drop user objects (keep noorm) │
-│ │
-└─────────────────────────────────────────────────────────────┘
-
-Warning: These operations modify the database directly.
-
-[c] Create [d] Destroy [x] Explore [w] Wipe [t] Teardown [Esc] Back
-```
+
### Schema Explorer
-```
-Home > Databases > Explore Database
-
-┌─ DB Explore ────────────────────────────────────────────────┐
-│ │
-│ Config: dev (postgres) │
-│ Database: myapp_dev │
-│ Total Objects: 47 │
-│ │
-│ [1] Tables 3 │
-│ [2] Views 3 │
-│ [3] Procedures 0 │
-│ [4] Functions 5 │
-│ [5] Types 0 │
-│ [6] Indexes 13 │
-│ [7] Foreign Keys 2 │
-│ │
-└─────────────────────────────────────────────────────────────┘
-
-[1-7] Navigate [Esc] Back
-```
+
Press a number to drill into a category:
-```
-Databases > Explore Database > Tables
-
-┌─ Tables (3) ────────────────────────────────────────────────┐
-│ │
-│ / Filter tables... │
-│ │
-│ 1 > public.todo_items 13 columns │
-│ 2 public.todo_lists 9 columns │
-│ 3 public.users 9 columns │
-│ │
-│ [/] Search │
-│ │
-└─────────────────────────────────────────────────────────────┘
-
-[Enter] View detail [Esc] Back
-```
+
Select a table to see its full schema:
-```
-public.todo_items
-
-Columns (13)
-* id uuid NOT NULL DEFAULT gen_random_uuid()
- embedding USER-DEFINED NULL
- created_at timestamp with time zone NOT NULL DEFAULT now()
- updated_at timestamp with time zone NOT NULL DEFAULT now()
- deleted_at timestamp with time zone NULL
- list_id uuid NOT NULL
- title character varying NOT NULL
- description text NULL
- is_completed boolean NOT NULL DEFAULT false
- priority smallint NOT NULL DEFAULT 0
- due_date timestamp with time zone NULL
- completed_at timestamp with time zone NULL
- position integer NOT NULL DEFAULT 0
-
-Indexes (5)
- idx_todo_items_due_date (due_date)
- idx_todo_items_embedding (embedding vector_cosine_ops)
- idx_todo_items_list_id (list_id)
- idx_todo_items_position (list_id, "position")
- todo_items_pkey (id) UNIQUE
-
-Foreign Keys (1)
- todo_items_list_id_fkey
- (list_id) → todo_lists(id)
-
-[Esc] Back
-```
+
### SQL Terminal
-```
-┌─ SQL Terminal ──────────────────────────────────────────────┐
-│ │
-│ noorm> SELECT * FROM users LIMIT 3; │
-│ │
-│ ┌────┬─────────┬─────────────────┬─────────────────────┐ │
-│ │ id │ name │ email │ created_at │ │
-│ ├────┼─────────┼─────────────────┼─────────────────────┤ │
-│ │ 1 │ Alice │ alice@email.com │ 2024-01-15 09:30:00 │ │
-│ │ 2 │ Bob │ bob@email.com │ 2024-01-16 14:22:00 │ │
-│ │ 3 │ Charlie │ charlie@co.com │ 2024-01-17 11:45:00 │ │
-│ └────┴─────────┴─────────────────┴─────────────────────┘ │
-│ │
-│ 3 rows (12ms) │
-│ │
-│ noorm> _ │
-│ │
-│ [h] history [c] clear │
-└─────────────────────────────────────────────────────────────┘
-```
+Press `Shift+Q` anywhere to open the SQL terminal against the active config:
+
+
- Tab completion for table/column names
- Query history with up/down arrows
@@ -348,19 +197,69 @@ Foreign Keys (1)
Press `Shift+L` anywhere to toggle the log overlay:
-```
-┌─ Logs ──────────────────────────────────────────────────────┐
-│ │
-│ 09:30:01 INFO Connected to dev (sqlite) │
-│ 09:30:02 INFO Building schema... │
-│ 09:30:02 DEBUG Checking sql/01_tables/001_users.sql │
-│ 09:30:02 INFO ✓ sql/01_tables/001_users.sql (changed) │
-│ 09:30:03 DEBUG Checking sql/01_tables/002_posts.sql │
-│ 09:30:03 INFO • sql/01_tables/002_posts.sql (unchanged)│
-│ │
-│ [/] search [p] pause [Shift+L] close │
-└─────────────────────────────────────────────────────────────┘
-```
+
+
+The overlay sits on top of whatever screen you were on, so you can watch events
+while an operation runs. `[/]` searches, `[Space]` pauses the live tail, and
+`[Enter]` opens a single entry in full.
+
+
+### More Options
+
+Press `+` from home for the screens that aren't part of the day-to-day loop:
+
+
+
+Each of these also has a direct key from home — `+` just groups them.
+
+
+### Settings
+
+
+
+Edits here write to `.noorm/settings.yml`. Press `i` to create that file if the
+project doesn't have one yet. See [Configs](/guide/environments/configs) and
+[SQL File Organization](/guide/sql-files/organization) for what each group
+controls.
+
+
+### Identity
+
+
+
+Your identity signs your name to every change execution, which is what makes
+[change history](/guide/changes/history) attributable across a team. It lives
+in `~/.noorm/`, not in the project, so it follows you between repositories.
+
+
+### Vault
+
+
+
+The vault holds team-shared encrypted secrets in the database itself, so
+teammates get them by connecting rather than by copying a `.env` around. It
+starts uninitialized — press `i` to create it. See [Vault](/guide/environments/vault).
+
+
+### Secrets
+
+Per-config secrets are reached with `k` from the config list, not from home —
+they belong to a config rather than to the project:
+
+
+
+These are values a config needs at connection or render time. See
+[Secrets](/guide/environments/secrets) for how they resolve against stages.
+
+
+### Lock
+
+
+
+noorm takes a lock around operations that write to the database, so two people
+running a build against the same environment don't interleave. `[s]` shows
+status, `[a]` acquires, `[r]` releases, and `[f]` force-breaks a stale lock.
+See [Locking](/dev/lock).
## Tips
diff --git a/docs/wiki/cli.md b/docs/wiki/cli.md
index 427757e3..05d1d3ac 100644
--- a/docs/wiki/cli.md
+++ b/docs/wiki/cli.md
@@ -1,76 +1,88 @@
---
type: Domain
+description: Citty CLI entry point, command-group orchestration, help system, headless output contract, and binary packaging for `noorm`
---
# cli
## What it does
-Citty-based CLI with 17 top-level command groups. Each command group maps to a subdirectory under [`src/cli/`](../../src/cli). Commands emit events via the observer and delegate to core modules. Headless mode (`--yes`, `--json`) suppresses interactive prompts and formats output as JSON.
+[`src/cli/index.ts`](../../src/cli/index.ts) is the citty entry point: it registers 19 top-level `subCommands` (13 command groups that each carry their own subcommands — `change`, `ci`, `config`, `db`, `dev`, `identity`, `lock`, `mcp`, `run`, `secret`, `settings`, `sql`, `vault` — plus 6 standalone leaf commands — `info`, `init`, `ui`, `update`, `version`, `complete`), each lazily imported via a thunk, except `complete` which is a directly-assigned zero-cost stub to avoid forcing every other thunk to resolve during tab-completion registration.
-Published as `@noormdev/cli` from [`packages/cli/`](../../packages/cli).
+Every subcommand except `noorm ui`, `noorm sql repl`, `noorm settings edit`, `noorm settings secret`, and `noorm init` is non-interactive by default; commands accept `--json`/`--yes`/`-y`/`--config`/`-c`/`--force`/`-f`/`--dry-run` from the shared `sharedArgs` in [`src/cli/_utils.ts`](../../src/cli/_utils.ts) and print a JSON envelope or plain text.
+
+Published to npm as `@noormdev/cli` from [`packages/cli/`](../../packages/cli); the npm package is a thin `noorm.js` shim that execs a separately-downloaded, `bun build --compile`-produced platform binary, verified against a checksum before use.
## Artifacts
-- [`packages/cli/package.json`](../../packages/cli/package.json) — published package `@noormdev/cli`, version `1.0.0-alpha.35`; entry `noorm.js`
-- [`packages/cli/noorm.js`](../../packages/cli/noorm.js) — thin wrapper that runs the compiled binary
-- [`packages/cli/scripts/postinstall.js`](../../packages/cli/scripts/postinstall.js) — postinstall script for binary extraction
-- [`packages/cli/CHANGELOG.md`](../../packages/cli/CHANGELOG.md) — CLI release history
-- [`skills/noorm/SKILL.md`](../../skills/noorm/SKILL.md) — Claude Code skill for noorm CLI usage
-- [`skills/noorm/references/cli.md`](../../skills/noorm/references/cli.md) — comprehensive CLI command reference (1011L)
-- [`skills/noorm/references/config.md`](../../skills/noorm/references/config.md) — config management reference
-- [`skills/noorm/references/sdk.md`](../../skills/noorm/references/sdk.md) — SDK reference for skill use
-- [`skills/noorm/references/templates.md`](../../skills/noorm/references/templates.md) — template reference for skill use
+- [`packages/cli/package.json`](../../packages/cli/package.json) — publishes `@noormdev/cli`, currently version `1.0.0`; declares `bin.noorm -> ./noorm.js`, `postinstall` script, `engines.node >=22.13`
+- [`packages/cli/noorm.js`](../../packages/cli/noorm.js) — ESM shim; resolves `bin/noorm` (or `bin/noorm.exe` on `win32`) next to itself and `execFileSync`s it with `stdio: 'inherit'`, forwarding the child's exit code
+- [`packages/cli/scripts/postinstall.js`](../../packages/cli/scripts/postinstall.js) — downloads the platform/arch-matched binary from `https://github.com/noormdev/noorm/releases/download/@noormdev/cli@/noorm--`, verifies its SHA-256 against the release's `checksums.txt`, and only then `chmod`s and renames it into `bin/`; skips entirely when it detects the noorm monorepo source checkout (root [`package.json`](../../package.json) name `@noormdev/main` two directories up)
+- [`packages/cli/bin/`](../../packages/cli/bin) — gitignored (`.gitignore:35`); holds the platform binaries either downloaded by postinstall or built locally by `bun run build:binary`
+- [`packages/cli/README.md`](../../packages/cli/README.md), [`packages/cli/CHANGELOG.md`](../../packages/cli/CHANGELOG.md), [`packages/cli/LICENSE`](../../packages/cli/LICENSE) — published package metadata
+- [`skills/noorm/SKILL.md`](../../skills/noorm/SKILL.md) — Claude Code skill front matter and routing table (SDK vs CLI vs config vs templates reference)
+- [`skills/noorm/references/cli.md`](../../skills/noorm/references/cli.md) — CLI command reference consumed by the skill (activation, global flags, env vars, per-command usage, CI/CD examples, exit codes)
+- [`skills/noorm/references/sdk.md`](../../skills/noorm/references/sdk.md), [`skills/noorm/references/config.md`](../../skills/noorm/references/config.md), [`skills/noorm/references/templates.md`](../../skills/noorm/references/templates.md) — the skill's other three reference files
+- [`skills/noorm/evals/evals.json`](../../skills/noorm/evals/evals.json) — skill benchmark eval definitions
+- [`skills/noorm/workspace/iteration-1/`](../../skills/noorm/workspace/iteration-1) — recorded with/without-skill benchmark outputs (`cicd-pipeline`, `sdk-application-code`, `sdk-test-bootstrap`) used to grade the skill
## CLI code
-- [`src/cli/index.ts`](../../src/cli/index.ts) — citty entry point; registers all subcommands, help interceptor, `--cwd` global flag
-- [`src/cli/_utils.ts`](../../src/cli/_utils.ts) — shared CLI utilities: headless detection, output formatting, flag parsing
-- [`src/cli/change/`](../../src/cli/change) — `change add|edit|ff|history|list|next|revert|rewind|rm|run` (13 files)
-- [`src/cli/ci/`](../../src/cli/ci) — `ci init|secrets|identity/*` — CI automation commands
-- [`src/cli/config/`](../../src/cli/config) — `config add|cp|edit|export|import|list|rm|use|validate` (10 files)
-- [`src/cli/db/`](../../src/cli/db) — `db create|drop|explore*|reset|teardown|transfer|truncate` (16 files)
-- [`src/cli/dev/`](../../src/cli/dev) — `dev test-helpers|test-workers` — internal diagnostics
-- [`src/cli/identity/`](../../src/cli/identity) — `identity edit|export|init|list`
-- [`src/cli/lock/`](../../src/cli/lock) — `lock acquire|force|release|status`
-- [`src/cli/mcp/`](../../src/cli/mcp) — `mcp init|serve`
-- [`src/cli/run/`](../../src/cli/run) — `run build|dir|exec|file|files|inspect|preview` (8 files)
-- [`src/cli/secret/`](../../src/cli/secret) — `secret list|rm|set`
-- [`src/cli/settings/`](../../src/cli/settings) — `settings build|edit|init|secret` (5 files)
-- [`src/cli/sql/`](../../src/cli/sql) — `sql clear|history|query|repl`
-- [`src/cli/vault/`](../../src/cli/vault) — `vault cp|init|list|propagate|rm|set`
-- [`src/cli/init.ts`](../../src/cli/init.ts) — `noorm init` — project initialization wizard
-- [`src/cli/info.ts`](../../src/cli/info.ts) — `noorm info` — display project + env info
-- [`src/cli/ui.ts`](../../src/cli/ui.ts) — `noorm ui` — launch TUI
-- [`src/cli/update.ts`](../../src/cli/update.ts) — `noorm update` — self-update
-- [`src/cli/version.ts`](../../src/cli/version.ts) — `noorm version` — print version info
+- [`src/cli/index.ts`](../../src/cli/index.ts) — citty root command; `resolveCommand` walks argv to find the target command for the `--help` interceptor; `extractGlobalCwd` strips `-c`/`--cwd ` (and rejects any other flag) before the subcommand name, the only root-level flag; `rewriteBareSqlArgv` inserts a synthetic `query` token when `noorm sql ""` is invoked bare, matched against a `SQL_VERBS` allowlist regex; `printHelpWithExamples` renders citty's usage plus an [`EXAMPLES`](../../EXAMPLES) block; `entry()` wires cwd resolution, env-based identity override installation (`loadIdentityFromEnv`/`setKeyOverride`/`setIdentityOverride`), the `complete` fast path, the `--help`/`-h` interceptor, and `runMain`
+- [`src/cli/_utils.ts`](../../src/cli/_utils.ts) — `sharedArgs` (`config`/`json`/`force`/`dryRun`/`yes`), `isYesMode`/`isInsecureMode` (flag-or-env truthy parsing, `NOORM_YES`/`NOORM_INSECURE`), `createCliLogger` (builds the `Logger` every CLI command runs under, reading `settings.logging.*`), `withContext`/`withVaultContext` (context create → connect → `ensureSchemaVersion` → run → disconnect → logger stop, returning `[result, null] | [null, Error]`), `outputResult`/`outputError` (stdout-only result output; `toJsonEnvelope` derives the envelope's `success` boolean from a payload's `status` field when `success` isn't already present), `handleVaultResult`
+- [`src/cli/_exit.ts`](../../src/cli/_exit.ts) — `EXIT` map (`SUCCESS: 0`, `FAILURE: 1`, `USAGE: 2`, `PARTIAL: 3`), `exitCodeForStatus` (unknown status collapses to `FAILURE`, never `SUCCESS`), `isSuccessStatus`
+- [`src/cli/info.ts`](../../src/cli/info.ts) — `noorm info`; gathers CLI/schema/state/settings versions, active config + connection details, identity + DB registration timestamps, detected agent harness (`detectAgentHarness`), and DB object counts into one report, tolerating partial failures at each step
+- [`src/cli/init.ts`](../../src/cli/init.ts) — `noorm init`; TTY-only unless `--yes` and a full identity already exist at `~/.noorm/identity.{key,pub,json}`; `--here` inits in the original cwd instead of a discovered parent [`.noorm`](../../.noorm); prompts for identity name/email via `@clack/prompts` when no identity exists, then calls `performProjectInit`
+- [`src/cli/ui.ts`](../../src/cli/ui.ts) — `noorm ui`; the only subcommand that renders the Ink/React TUI; `ink`/`react`/`../tui/app.js` are imported inside `run()` so no other command pays their load cost; installs a no-op `Writable` as the logger's console/diagnostics streams to keep Logger output from corrupting Ink's rendering
+- [`src/cli/update.ts`](../../src/cli/update.ts) — `noorm update`; checks GitHub for a newer release, shows a live download-progress line on a TTY (carriage-return in place) or periodic text otherwise, supports `--insecure`/`NOORM_INSECURE` to downgrade an unreachable-checksum failure to a warning (never a confirmed mismatch)
+- [`src/cli/version.ts`](../../src/cli/version.ts) — `noorm version`; reports Node/platform/arch, identity key paths and existence, and project/config detection — no DB connection required
+- [`src/cli/dev/index.ts`](../../src/cli/dev/index.ts) — `noorm dev`; internal diagnostics subcommand group (`test-helpers`, `test-workers`), not part of the 12 domain-owning command groups
+- [`src/cli/change/`](../../src/cli/change), [`src/cli/ci/`](../../src/cli/ci), [`src/cli/config/`](../../src/cli/config), [`src/cli/db/`](../../src/cli/db), [`src/cli/identity/`](../../src/cli/identity), [`src/cli/lock/`](../../src/cli/lock), [`src/cli/mcp/`](../../src/cli/mcp), [`src/cli/run/`](../../src/cli/run), [`src/cli/secret/`](../../src/cli/secret), [`src/cli/settings/`](../../src/cli/settings), [`src/cli/sql/`](../../src/cli/sql), [`src/cli/vault/`](../../src/cli/vault) — the 12 command groups registered by `index.ts` and covered in depth by `core-change`, `core-db`, `core-state`, `core-identity`, `mcp-rpc`, and `core-runner` respectively (see Coupling)
+
+### Tests (top-level, framework-scoped)
+
+- [`tests/cli/citty-args.ts`](../../tests/cli/citty-args.ts) — `assertArgsDef`; shared type-guard helper for driving citty's real `parseArgs` against a command's resolved `args` in tests
+- [`tests/cli/citty-help.test.ts`](../../tests/cli/citty-help.test.ts) — subprocess-driven; asserts the `--help` interceptor prints citty's `USAGE`/`COMMANDS` plus the [`EXAMPLES`](../../EXAMPLES) block, and that the breadcrumb (`USAGE noorm change add`, `USAGE noorm db explore tables`) is never doubled at any nesting depth
+- [`tests/cli/global-flags.test.ts`](../../tests/cli/global-flags.test.ts) — asserts `-c`/`--cwd` is the only flag honored before the subcommand; `--json`/`--yes`/`--dry-run`/an unrecognized flag placed before the subcommand all exit non-zero naming the flag and the fix, rather than being silently dropped
+- [`tests/cli/exit-codes.test.ts`](../../tests/cli/exit-codes.test.ts) and [`tests/cli/json-envelope.test.ts`](../../tests/cli/json-envelope.test.ts) — subprocess tests against the compiled CLI (`dist/cli/index.js` via `run/_setup.js` helpers) pinning `EXIT`/`toJsonEnvelope`: `run dir` with mixed file outcomes exits `PARTIAL` (3), all-fail exits `FAILURE` (1), a missing/empty target exits `USAGE` (2) rather than a false `success:true`; every sampled `--json` command across 24 invocations returns an object (never a bare array) whose `success` boolean agrees with the process exit code
+- [`tests/cli/lazy-startup.test.ts`](../../tests/cli/lazy-startup.test.ts) — walks the TypeScript AST of top-level static imports/exports from [`src/cli/index.ts`](../../src/cli/index.ts); asserts the headless entry point never statically reaches `ink`, `react`, or any file under [`src/tui/`](../../src/tui), and that [`src/cli/ui.ts`](../../src/cli/ui.ts)/[`src/cli/sql/repl.ts`](../../src/cli/sql/repl.ts) don't statically import `ink`/`react` either
+- [`tests/cli/postinstall-source-checkout.test.ts`](../../tests/cli/postinstall-source-checkout.test.ts) — spawns a copy of the real `postinstall.js` against fabricated directory trees; confirms it skips the download (exit 0, no `bin/noorm`) only when the root package two levels up is named `@noormdev/main`, and still downloads for both a real npm consumer and an unrelated monorepo with the same directory shape
+- [`tests/cli/yes-flag.test.ts`](../../tests/cli/yes-flag.test.ts) / [`tests/cli/insecure-flag.test.ts`](../../tests/cli/insecure-flag.test.ts) — unit-test `isYesMode`/`isInsecureMode` truthy/falsy parsing directly, plus subprocess coverage of the four TTY-gated commands (`sql repl`, `settings edit`, `settings secret`, `init`) refusing `--yes`/`NOORM_YES` on a non-TTY with a redirect hint, distinct from the plain TTY-refusal message
+- [`tests/cli/cli-logger-settings.test.ts`](../../tests/cli/cli-logger-settings.test.ts) — exercises `createCliLogger` against real `settings.yml` files: `logging.enabled: false` suppresses the log file but not console/`--json` output, `logging.file`/`level`/`maxSize`/`maxFiles` are honored; isolated into its own CI group (see project [`CLAUDE.md`](../../CLAUDE.md)) because it uses the real `getSettingsManager`/`resetSettingsManager` and is the victim, not the source, of a permanent mock — [`tests/cli/screens/init/init-screen.test.tsx`](../../tests/cli/screens/init/init-screen.test.tsx) and [`tests/cli/screens/init/init-flow.test.tsx`](../../tests/cli/screens/init/init-flow.test.tsx) replace the `SettingsManager` class via `mock.module`, which no later `mock.module` call actually undoes in Bun's process-global mock registry, so load order decides whether this file sees the real class or a leaked mock
+- [`tests/cli/env-bootstrap.test.ts`](../../tests/cli/env-bootstrap.test.ts) — confirms `NOORM_IDENTITY_PRIVATE_KEY`/`_NAME`/`_EMAIL` let `noorm info` report identity with no `~/.noorm/` files present, and that unset env vars fall through to disk
+- [`tests/cli/init.test.ts`](../../tests/cli/init.test.ts) — subprocess coverage of `noorm init`'s TTY gate, `--force` re-init guard, `--here` vs. parent-[`.noorm`](../../.noorm) walk-up, and global `-c ` (including rejecting a non-directory path)
+- [`tests/cli/agent-channel-escalation.test.ts`](../../tests/cli/agent-channel-escalation.test.ts), [`tests/cli/change-edit.test.ts`](../../tests/cli/change-edit.test.ts), [`tests/cli/change-prompts.test.ts`](../../tests/cli/change-prompts.test.ts), [`tests/cli/settings-edit.test.ts`](../../tests/cli/settings-edit.test.ts), [`tests/cli/settings-secret.test.ts`](../../tests/cli/settings-secret.test.ts), [`tests/cli/sql-repl.test.ts`](../../tests/cli/sql-repl.test.ts) — subprocess tests of individual TTY-gated or channel-sensitive commands; deeper command semantics belong to `core-change`/`core-state`/`core-identity`, but the TTY-refusal and channel-resolution mechanics they exercise are CLI-framework behavior
+- [`tests/cli/app.test.tsx`](../../tests/cli/app.test.tsx), [`tests/cli/app-context.test.tsx`](../../tests/cli/app-context.test.tsx), [`tests/cli/router.test.tsx`](../../tests/cli/router.test.tsx), [`tests/cli/screens.test.tsx`](../../tests/cli/screens.test.tsx), [`tests/cli/keyboard.test.tsx`](../../tests/cli/keyboard.test.tsx), [`tests/cli/focus.test.tsx`](../../tests/cli/focus.test.tsx), [`tests/cli/types.test.ts`](../../tests/cli/types.test.ts), [`tests/cli/debug-pid.test.tsx`](../../tests/cli/debug-pid.test.tsx) — test `src/tui/*` (App, router, screens registry, keyboard hooks, focus stack, route-parsing utilities); there is no `tests/tui/` directory, so these TUI-domain tests live under [`tests/cli/`](../../tests/cli) top level rather than a subdirectory
+- [`tests/cli/VaultScreen.test.tsx`](../../tests/cli/VaultScreen.test.tsx) — deliberately located at [`tests/cli/`](../../tests/cli) rather than `tests/cli/screens/vault/` (stated in the file's own header comment): CI runs [`tests/cli`](../../tests/cli) as one Bun process, [`tests/cli/hooks/useVaultSecretKeys.test.tsx`](../../tests/cli/hooks/useVaultSecretKeys.test.tsx) mocks `core/vault` and Bun's `mock.module` never actually restores the real module, so load order between the two files decides which one runs against the real vault — placement here is load-bearing, not incidental
+- [`tests/cli/config-validation.test.ts`](../../tests/cli/config-validation.test.ts), [`tests/cli/settings-validation.test.ts`](../../tests/cli/settings-validation.test.ts) — unit test [`src/tui/utils/config-validation.ts`](../../src/tui/utils/config-validation.ts)/`settings-validation.ts` directly (TUI-domain source, top-level test file location)
## Docs
-- [`docs/cli/`](../cli) — 9 user-facing CLI reference pages
-- [`docs/dev/headless.md`](../dev/headless.md) — headless mode internals
-- [`docs/guide/automation/non-interactive.md`](../guide/automation/non-interactive.md) — non-interactive usage
-- [`docs/guide/automation/ci.md`](../guide/automation/ci.md) — CI usage
-- [`docs/guide/automation/mcp.md`](../guide/automation/mcp.md) — MCP usage
-- [`docs/headless.md`](../headless.md) — public headless reference (1592L)
+- [`docs/cli/flags.md`](../cli/flags.md) — global (`-c`/`--cwd`) vs. per-subcommand flag rules, the `--config`/`-c` overload
+- [`docs/cli/help.md`](../cli/help.md) — `--help`/`-h` discovery, why there's no `noorm help `
+- [`docs/cli/init.md`](../cli/init.md), [`docs/cli/run.md`](../cli/run.md), [`docs/cli/secret.md`](../cli/secret.md), [`docs/cli/identity.md`](../cli/identity.md), [`docs/cli/sql.md`](../cli/sql.md), [`docs/cli/sql-repl.md`](../cli/sql-repl.md), [`docs/cli/settings-edit.md`](../cli/settings-edit.md), [`docs/cli/settings-secret.md`](../cli/settings-secret.md) — per-command reference pages (8 files total alongside `flags.md`/`help.md`)
+- [`docs/headless.md`](../headless.md) — public headless-mode reference
+- [`docs/dev/headless.md`](../dev/headless.md) — internal headless/CI/MCP usage notes
## Coupling
-- Every CLI command imports from [`src/core/`](../../src/core) — any core API change may require CLI command updates.
-- [`src/cli/ui.ts`](../../src/cli/ui.ts) launches the TUI ([`src/tui/app.tsx`](../../src/tui/app.tsx)) — TUI startup is a CLI concern.
-- [`src/cli/mcp/serve.ts`](../../src/cli/mcp/serve.ts) starts the MCP server from [`src/mcp/server.ts`](../../src/mcp/server.ts) — MCP domain depends on CLI entry.
-- Headless mode output shape is consumed by CI pipelines and SDK integration tests.
-- [`src/cli/db/drop.ts`](../../src/cli/db/drop.ts) and [`src/cli/sql/query.ts`](../../src/cli/sql/query.ts) call `checkConfigPolicy`/`executeRawSql` from [`src/core/policy/`](../../src/core/policy)/[`src/core/sql-terminal/executor.ts`](../../src/core/sql-terminal/executor.ts) — CLI destructive and raw-SQL commands gate through the same policy checks as MCP and TUI.
-- [`src/cli/config/import.ts`](../../src/cli/config/import.ts) validates imported JSON via `parseConfig` ([`src/core/config/schema.ts`](../../src/core/config/schema.ts)) instead of a hand-rolled shape check — validation errors surface as `ConfigValidationError`.
-- `settings.paths.sql` and `settings.paths.changes` from `settings.yml` are the correct path sources (not per-config `paths` fields) — several run-related screens use `settings?.paths?.changes ?? 'changes'` pattern.
+- [`src/cli/index.ts`](../../src/cli/index.ts) lazily imports [`src/cli/change/index.ts`](../../src/cli/change/index.ts), [`src/cli/db/index.ts`](../../src/cli/db/index.ts), [`src/cli/identity/index.ts`](../../src/cli/identity/index.ts), [`src/cli/mcp/index.ts`](../../src/cli/mcp/index.ts), [`src/cli/run/index.ts`](../../src/cli/run/index.ts), [`src/cli/config/index.ts`](../../src/cli/config/index.ts), [`src/cli/settings/index.ts`](../../src/cli/settings/index.ts) — these command groups are documented in depth by the **core-change**, **core-db**, **core-identity**, **mcp-rpc**, **core-runner**, and **core-state** domains respectively (`config`/`settings`/`ci` fold into **core-state**, `identity`/`secret`/`vault`/`sql`/`lock` into **core-identity**); a change to any of those core modules can require a matching CLI command update here.
+- [`src/cli/ui.ts`](../../src/cli/ui.ts) dynamically imports `../tui/app.js` — TUI startup is invoked from the CLI, but the TUI's own framework code and tests live in the **tui** domain (including most of its test coverage, physically inside [`tests/cli/`](../../tests/cli) — see Tests above).
+- [`src/cli/mcp/serve.ts`](../../src/cli/mcp/serve.ts) (in [`src/cli/mcp/`](../../src/cli/mcp), covered by **mcp-rpc**) calls `startServer()` from [`src/mcp/index.ts`](../../src/mcp/index.ts), which in turn calls `createMcpServer` from [`src/mcp/server.ts`](../../src/mcp/server.ts) — the MCP domain's entry point is a CLI command.
+- [`src/cli/_utils.ts`](../../src/cli/_utils.ts)'s `withContext`/`withVaultContext` call `createContext` (**sdk** domain, [`src/sdk/index.ts`](../../src/sdk/index.ts)), `loadPrivateKey`/`loadIdentityMetadata`/`registerIdentity` (**core-identity**), `ensureSchemaVersion` (**core-state**, [`src/core/version/`](../../src/core/version)), and `resolveChannel` (**core-policy**) — every headless command's context lifecycle depends on all four domains.
+- `postinstall.js` mirrors `isInsecureMode`'s truthy-string parsing from [`src/cli/_utils.ts`](../../src/cli/_utils.ts) by hand (comment: "Mirrors the TS `isInsecureMode`... so the escape hatch behaves identically") because the script runs before the workspace is built and cannot import TypeScript sources.
+- [`packages/cli`](../../packages/cli)/[`packages/sdk`](../../packages/sdk) are a Changesets **fixed** version group ([`.changeset/config.json`](../../.changeset/config.json): `"fixed": [["@noormdev/cli", "@noormdev/sdk"]]`) — they always bump together on release.
+- [`.github/workflows/release-binary.yml`](../../.github/workflows/release-binary.yml) reads [`packages/cli/package.json`](../../packages/cli/package.json)'s `version` to build the GitHub Release tag (`@noormdev/cli@`) that both [`scripts/build-binary.mjs`](../../scripts/build-binary.mjs) (infra domain) and [`packages/cli/scripts/postinstall.js`](../../packages/cli/scripts/postinstall.js) (this domain) target — the two must resolve to the same tag.
## Conventions worth knowing
-- Commands attach `examples: string[]` to their `defineCommand` result; the help interceptor in [`src/cli/index.ts`](../../src/cli/index.ts) appends them after citty's auto-generated usage.
-- `--cwd ` global flag (like `git -C`) must precede the subcommand.
-- `--yes` / `-y` flag suppresses all confirmation prompts (headless mode).
-- `--json` flag formats output as machine-readable JSON.
-- Build produces a standalone binary via `bun build --compile` — worker paths must use `resolveWorker()`.
-- `config list` ([`src/cli/config/list.ts`](../../src/cli/config/list.ts)) prints an access tag (`user: agent:`) instead of a `protected` flag, shown only when `guarded(config)` is true.
-- `db drop` ([`src/cli/db/drop.ts`](../../src/cli/db/drop.ts)) no longer requires `--yes` unconditionally — it's only required when the resolved `db:destroy` policy check returns `requiresConfirmation`.
-- Workspace package `@noormdev/cli` publishes the pre-built binary; `postinstall.js` extracts it.
+- Commands opt into an [`EXAMPLES`](../../EXAMPLES) block by assigning `(command as typeof command & { examples: string[] }).examples = [...]` after `defineCommand`; the interceptor in `index.ts` reads that top-level [`examples`](../../examples) property, not a citty-native field.
+- `-c`/`--cwd ` must precede the subcommand (`git -C` semantics); every other flag, including `-c` meaning `--config` on many subcommands, only works after the subcommand — an unrecognized flag before the subcommand is a hard error, not a silent drop.
+- `noorm sql "SELECT ..."` (a bare SQL string with no explicit `query` subcommand) is rewritten to `noorm sql query "SELECT ..."` by `rewriteBareSqlArgv`, gated on a `SQL_VERBS` regex so `sql history`/`sql clear`/`sql repl` are untouched.
+- Exit codes are a 4-value contract shared by ~235 call sites: `0` success, `1` total failure, `2` usage error (nothing attempted), `3` partial (mixed state, unsafe to blindly retry) — `exitCodeForStatus` treats an unrecognized status string as `FAILURE`, never `SUCCESS`.
+- Every `--json` payload is a JSON object with a top-level boolean `success` that always agrees with the process exit code; list results live under a named key (`configs`, `changes`, `tables`, …), never as a bare top-level array — enforced by `toJsonEnvelope` in `_utils.ts` and pinned by [`tests/cli/json-envelope.test.ts`](../../tests/cli/json-envelope.test.ts).
+- TTY-gated commands (`noorm init`, `noorm sql repl`, `noorm settings edit`, `noorm settings secret`) refuse to run on a non-TTY; passing `--yes`/`NOORM_YES` on a non-TTY still refuses but with a different, redirect-hinting message than the bare TTY refusal.
+- `--yes`/`-y` (`isYesMode`) and `--insecure` (`isInsecureMode`) both parse `NOORM_YES`/`NOORM_INSECURE` the same way: any non-empty value except `0` or a case-insensitive `false` is truthy; the CLI flag always wins over a `0`/`false` env value.
+- The compiled/lazy-loaded CLI never statically imports `ink`, `react`, or anything under [`src/tui/`](../../src/tui) except through `noorm ui`'s dynamic `import()` — verified by an AST walk in [`tests/cli/lazy-startup.test.ts`](../../tests/cli/lazy-startup.test.ts), not a runtime check.
+- `noorm` binaries are built via `bun build --compile`; worker thread paths inside the binary must resolve through `resolveWorker()` (see project [`CLAUDE.md`](../../CLAUDE.md) / **worker-bridge** domain), not hardcoded strings, because `bun build --compile` strips [`src/`](../../src) and rewrites `.ts` to `.js` in the embedded binary.
+- A confirmed checksum mismatch on `noorm update` or npm postinstall is always a hard failure; `NOORM_INSECURE`/`--insecure` can only downgrade an *unreachable* `checksums.txt` (or a missing entry) into a warning, never override a confirmed bad hash.
diff --git a/docs/wiki/core-change.md b/docs/wiki/core-change.md
index 480bc591..ceb187a2 100644
--- a/docs/wiki/core-change.md
+++ b/docs/wiki/core-change.md
@@ -1,25 +1,31 @@
---
type: Domain
+description: Versioned database changes — scaffold, parse, execute, revert, and track history
---
# core-change
## What it does
-Manages versioned database changes: scaffold (create/add/remove/reorder files), parse (discover + validate), execute (forward/revert with tracking), and history (execution records per change and per file).
+Manages versioned database changes: scaffold (create/add/remove/rename/reorder change files on disk), parse (discover + validate change folders), execute (forward/revert with checksum-based skip detection), and history (per-change and per-file execution records).
-Change directories hold a `manifest.json` and SQL files. Each change has a description-based name, forward files, and optional revert files. Execution state is stored in the `__noorm_change__` and `__noorm_executions__` noorm tables.
+A change directory holds a `change/` folder, an optional `revert/` folder, an optional `changelog.md`, and SQL or `.txt` manifest files. Execution state is stored in the `__noorm_change__` and `__noorm_executions__` tables ([`src/core/shared/tables.ts`](../../src/core/shared/tables.ts)).
## CLI code
-- [`src/core/change/scaffold.ts`](../../src/core/change/scaffold.ts) — create/add/remove/rename/reorder change files on disk
-- [`src/core/change/parser.ts`](../../src/core/change/parser.ts) — `parseChange`, `discoverChanges`, `resolveManifest`, `validateChange`, `parseSequence`, `parseDescription`
-- [`src/core/change/executor.ts`](../../src/core/change/executor.ts) — `executeChange`, `revertChange`; applies SQL via the runner, records results. Each gates via `assertPolicy` (`core/policy`) against `ChangeContext.access`/`channel` before running (`change:run`/`change:revert` permissions)
-- [`src/core/change/history.ts`](../../src/core/change/history.ts) — `ChangeHistory`; queries `__noorm_change__` and `__noorm_executions__` for per-change and per-file history; selects the change row's `id` and surfaces it as `appliedHistoryId` on `ChangeStatus`
-- [`src/core/change/tracker.ts`](../../src/core/change/tracker.ts) — `ChangeTracker`; `canRevert` logic, orphaned-change detection
-- [`src/core/change/manager.ts`](../../src/core/change/manager.ts) — `ChangeManager`; high-level facade: `list`, `run`, `revert`, `ff` (fast-forward), `rewind` (revert back to a target change, ordering applied changes by `appliedAt` descending then `appliedHistoryId` descending)
-- [`src/core/change/validation.ts`](../../src/core/change/validation.ts) — `validateChangeContent`; structural content checks
-- [`src/core/change/types.ts`](../../src/core/change/types.ts) — all change types, error classes (`ChangeValidationError`, `ChangeNotFoundError`, etc.); `ChangeStatus`/`ChangeListItem` carry an optional `appliedHistoryId: number | null`
+- [`src/cli/change/index.ts`](../../src/cli/change/index.ts) — registers the `change` command group: `add|edit|ff|list|next|rm|run|revert|history|rewind|history-detail`
+- [`src/cli/change/_prompt.ts`](../../src/cli/change/_prompt.ts) — shared interactive change-name pickers (`selectChangeFromFs`, `selectChangeFromStatus`, `requireTty`) used across the offline (add/edit/rm) and DB-aware (run/revert/rewind/history-detail) commands
+- [`src/cli/change/add.ts`](../../src/cli/change/add.ts) — offline; scaffolds a new change via `createChange`
+- [`src/cli/change/edit.ts`](../../src/cli/change/edit.ts) — offline; spawns `$EDITOR`/`$VISUAL`/`code` against the change folder
+- [`src/cli/change/rm.ts`](../../src/cli/change/rm.ts) — offline; gates on `change:rm` via `checkConfigPolicy` (not `assertPolicy`), then calls `deleteChange`
+- [`src/cli/change/run.ts`](../../src/cli/change/run.ts) — applies one named change
+- [`src/cli/change/next.ts`](../../src/cli/change/next.ts) — applies the next N pending changes
+- [`src/cli/change/ff.ts`](../../src/cli/change/ff.ts) — fast-forward: applies all pending changes; warns rather than fails when the changes directory is missing
+- [`src/cli/change/revert.ts`](../../src/cli/change/revert.ts) — reverts one applied change
+- [`src/cli/change/rewind.ts`](../../src/cli/change/rewind.ts) — reverts applied changes back to (and including) a named change
+- [`src/cli/change/list.ts`](../../src/cli/change/list.ts) — lists all changes with status; an orphaned change appends `, orphaned` inside the same parenthetical (e.g. `myname (success, orphaned)`)
+- [`src/cli/change/history.ts`](../../src/cli/change/history.ts) — combined change/revert execution history
+- [`src/cli/change/history-detail.ts`](../../src/cli/change/history-detail.ts) — per-file history for one change's operations
## Docs
@@ -27,22 +33,32 @@ Change directories hold a `manifest.json` and SQL files. Each change has a descr
- [`docs/guide/changes/overview.md`](../guide/changes/overview.md) — user-facing: what changes are
- [`docs/guide/changes/forward-revert.md`](../guide/changes/forward-revert.md) — forward and revert semantics
- [`docs/guide/changes/history.md`](../guide/changes/history.md) — history querying
-- [`docs/cli/run.md`](../cli/run.md) — run command docs (also covers change run)
+- [`docs/cli/run.md`](../cli/run.md) — run command docs for `noorm run` (build/file/dir/files/exec); a separate command family from `noorm change`
## Coupling
-- Calls `runner` (`runFile`) to execute SQL inside a change — changes in runner's `RunOptions` or file-execution semantics propagate here.
+- Calls `runner`'s checksum utilities (`computeChecksum`, `computeCombinedChecksum` from [`src/core/runner/checksum.ts`](../../src/core/runner/checksum.ts)) — checksum algorithm changes propagate here.
+- `ChangeTracker` ([`src/core/change/tracker.ts`](../../src/core/change/tracker.ts)) extends `Tracker` from [`src/core/runner/tracker.ts`](../../src/core/runner/tracker.ts) — base tracker changes affect revert/stale logic.
- Reads config via [`src/core/config/`](../../src/core/config) to resolve the active database connection — config schema changes affect `ChangeContext` construction.
-- Emits events via [`src/core/observer.ts`](../../src/core/observer.ts) (`change:*` events) — the TUI subscribes via `useChangeProgress` hook.
+- Emits `change:*` events (`change:start`, `change:file`, `change:complete`, `change:skip`, `change:created`, `file:dry-run`) via [`src/core/observer.ts`](../../src/core/observer.ts) — the TUI's `useChangeProgress` hook ([`src/tui/hooks/useChangeProgress.ts`](../../src/tui/hooks/useChangeProgress.ts)) subscribes, consumed by `ChangeNextScreen`, `ChangeFFScreen`, `ChangeRevertScreen`, `ChangeRewindScreen`, `ChangeRunScreen` under [`src/tui/screens/change/`](../../src/tui/screens/change).
- Writes to `__noorm_change__` and `__noorm_executions__` tables defined in [`src/core/shared/tables.ts`](../../src/core/shared/tables.ts) — table renames propagate to executor and history queries.
-- CLI commands in [`src/cli/change/`](../../src/cli/change) call manager + scaffold functions — CLI argument shape changes here require CLI command updates.
-- `executeChange`/`revertChange` call `assertPolicy` from [`src/core/policy/`](../../src/core/policy) before executing — `ChangeContext` carries `access`/`channel` for the gate; policy-matrix changes affect which roles can run/revert changes.
+- `executeChange`/`revertChange` call `assertPolicy` from [`src/core/policy/`](../../src/core/policy) before executing, gated on `change:run`/`change:revert`; [`src/cli/change/rm.ts`](../../src/cli/change/rm.ts) gates `change:rm` separately via `checkConfigPolicy` — `ChangeContext` carries `access`/`channel` for the gate; policy-matrix changes in [`src/core/policy/matrix.ts`](../../src/core/policy/matrix.ts) affect which roles can run/revert/rm changes.
+- `ChangeTracker.markAllAsStale` is called from [`src/core/teardown/operations.ts`](../../src/core/teardown/operations.ts) (core-db domain) after a teardown, to mark applied changes as needing re-application.
+- [`src/sdk/namespaces/changes.ts`](../../src/sdk/namespaces/changes.ts) wraps `ChangeManager` and the scaffold functions for the programmatic SDK — SDK's `Changes` namespace API shape changes with `ChangeManager`'s public methods.
+- [`src/rpc/commands/changes.ts`](../../src/rpc/commands/changes.ts) exposes change operations (e.g. `change_history`) as MCP/RPC commands, delegating to `ctx.noorm.changes` — same SDK surface as above.
+- CLI commands in [`src/cli/change/`](../../src/cli/change) call `ChangeManager` + scaffold functions directly — `ChangeManager`/scaffold signature changes require CLI command updates.
## Conventions worth knowing
-- Change directory names follow the pattern `YYYY-MM-DD-`.
-- `manifest.json` lists files in execution order; reorder functions rewrite it.
-- `parseSequence` extracts a numeric prefix from filename for ordering.
-- `DEFAULT_CHANGE_OPTIONS` and `DEFAULT_BATCH_OPTIONS` define timeout and retry defaults.
-- Error classes extend `Error` with a `code` field; callers check `code` to distinguish failure modes.
-- `ChangeManager.rewind()` sorts applied changes by `appliedAt` descending, tiebreaking on `appliedHistoryId` (the history row's autoincrement id) descending when two changes share the same second-precision `appliedAt` — e.g. entries applied within the same `change ff` batch.
+- Change directory names follow `YYYY-MM-DD-` (`DATE_PREFIX_REGEX` in `parser.ts`); a name without a date prefix is parsed with `date: null` and the whole name as `description`.
+- Change files are ordered by 3-digit sequence prefix: `NNN_description.{sql,sql.tmpl,txt}` (`SEQUENCE_REGEX`); `.txt` files are manifests referencing other SQL files, resolved in the manifest's own line order (not re-sorted).
+- `createChange` always scaffolds one stub file into `change/` and one into `revert/` (`CHANGE_STUB_TEMPLATE` / `REVERT_STUB_TEMPLATE` in `scaffold.ts`) — an empty `change/`+`revert/` pair fails `parseChange`'s validation, so the stub exists purely so the change is runnable immediately.
+- `executor.ts`'s pre-execution content gate (`hasExecutableSql`) checks for any non-blank, non-`--`-comment line — it does not call `validateChangeContent` from `validation.ts`. `validation.ts`'s `SQL_TEMPLATE` constant (`'-- TODO: Add SQL statements here\n'`) is a stale exact-match check no longer used at the executor seam; it is still imported and called only by the TUI's `ChangeFFScreen.tsx` and `ChangeRunScreen.tsx` for pre-flight UI checks.
+- `DEFAULT_CHANGE_OPTIONS` and `DEFAULT_BATCH_OPTIONS` (`types.ts`) define `force`/`dryRun`/`preview`/`output`/`abortOnError` defaults; `executor.ts` and `manager.ts` each keep their own local copy of the same defaults (`DEFAULT_OPTIONS`, `DEFAULT_BATCH`).
+- Error classes (`ChangeValidationError`, `ChangeNotFoundError`, `ChangeAlreadyAppliedError`, `ChangeNotAppliedError`, `ChangeOrphanedError`, `ManifestReferenceError`) extend `Error` with a `name` and structured fields; callers distinguish failure modes by class, not a `code` field.
+- Only Postgres wraps a change's file execution in a DB transaction (`TRANSACTIONAL_DIALECTS` in `executor.ts`): MySQL's DDL implicitly commits, MSSQL's GO-batch execution hasn't been verified to compose with a wrapping transaction, and SQLite is excluded so per-file partial success (used by unit tests) keeps working. On a failed Postgres change, neither the DDL nor its history rows persist — the caller still sees the failure via the returned `ChangeResult`, unwrapped from a thrown `ChangeRollback` sentinel.
+- `history.ts`'s `hydrateDate` normalizes `executed_at` to UTC: Postgres and MySQL drivers (`pg`, `mysql2`) parse the naive `timestamp`/`datetime2` column in the host's local zone, so their `Date` values are reinterpreted field-by-field as UTC; SQLite returns text and is parsed by appending `Z`. MSSQL (`tedious`) is deliberately left unmodified — not measured, left as-is to avoid a correction in the wrong direction.
+- `ChangeStatus`/`ChangeListItem` carry `appliedHistoryId?: number | null` — the `__noorm_change__` row's autoincrement id, used as the true apply-order tiebreaker (over second-precision `appliedAt`) in `ChangeManager.rewind()`.
+- `ChangeHistory.needsRunFile` excludes `pending` and `skipped` execution rows from its lookback, and bounds the lookback at the most recent opposite-direction operation's id — a prior success only licenses a per-file skip while no revert/re-apply has happened since.
+- `RESET_MARKER = '__reset__'` is a reserved change name: `ChangeHistory.recordReset` writes a `db teardown` audit row under this name so it appears in `getHistory`/`getUnifiedHistory`, but `getAllStatuses` explicitly filters it out so it never appears in `change list`.
+- `isPendingChange` (types.ts) is the single shared predicate for "needs a forward run" (`pending`, `reverted`, or `stale` status, and not orphaned) — used by `ChangeManager.next`/`ff` and the CLI's interactive pickers; the file's own doc comment warns this predicate must be updated everywhere at once when a new status is added.
diff --git a/docs/wiki/core-db.md b/docs/wiki/core-db.md
index 397761a5..9834162b 100644
--- a/docs/wiki/core-db.md
+++ b/docs/wiki/core-db.md
@@ -1,59 +1,79 @@
---
type: Domain
+description: Database lifecycle, connection factory, schema exploration, teardown, and cross-database transfer across postgres/mysql/mssql/sqlite
---
# core-db
## What it does
-Database lifecycle operations: create/drop databases, schema exploration (tables, views, functions, indexes, FKs, stored procedures, types), data transfer between databases, and schema teardown (truncate data, drop all objects). All operations are dialect-aware (PostgreSQL, MySQL, MSSQL, SQLite).
+- Owns everything that touches a live database connection: creating/dropping databases ([`src/core/db/`](../../src/core/db)), opening connections per dialect ([`src/core/connection/`](../../src/core/connection)), reading schema metadata ([`src/core/explore/`](../../src/core/explore)), wiping data or dropping objects ([`src/core/teardown/`](../../src/core/teardown)), and moving rows between two databases ([`src/core/transfer/`](../../src/core/transfer)).
+- Each of the four operational modules (`db`, `explore`, `teardown`, `transfer`) follows the same dialect-dispatch shape: a `dialects/index.ts` maps `Dialect` (`'postgres' | 'mysql' | 'sqlite' | 'mssql'`) to a per-dialect implementation of a shared `DialectXOperations`/`TeardownDialectOperations`/`TransferDialectOperations` interface.
+- [`src/core/transfer/`](../../src/core/transfer) only supports `postgres`, `mysql`, `mssql` (`TRANSFER_SUPPORTED_DIALECTS` in [`src/core/transfer/dialects/index.ts`](../../src/core/transfer/dialects/index.ts)) — sqlite has no dialect module there.
+- [`src/cli/db/`](../../src/cli/db) exposes these operations as `noorm db ` via Citty subcommands.
+
+## Artifacts
+
+- [`src/core/db/operations.ts`](../../src/core/db/operations.ts) — `checkDbStatus`, `createDb`, `destroyDb`; SQLite's `checkDbStatus` pre-probes existence before `testConnection` because opening a connection to a missing SQLite file auto-creates it.
+- [`src/core/db/policy.ts`](../../src/core/db/policy.ts) — `assertDbPolicy`, the shared destructive-lifecycle gate used by `core/db` and `core/teardown` (both reached directly by the TUI and indirectly by the CLI via the SDK).
+- [`src/core/db/dual.ts`](../../src/core/db/dual.ts) — `withDualConnection`, generic two-connection lifecycle (connect both, run fn, always cleanup both) used by `transfer` and vault-copy.
+- [`src/core/db/dialects/postgres.ts`](../../src/core/db/dialects/postgres.ts), `mysql.ts`, `mssql.ts`, `sqlite.ts` — per-dialect `databaseExists`/`createDatabase`/`dropDatabase`/`getSystemDatabase`.
+- [`src/core/connection/factory.ts`](../../src/core/connection/factory.ts) — `createConnection` (retry/backoff via `@logosdx/utils` `retry`, `shouldRetry` skips auth/config failures), `testConnection` (`testServerOnly` swaps to the dialect's system database: `postgres`, `master`, none for mysql/sqlite).
+- [`src/core/connection/manager.ts`](../../src/core/connection/manager.ts) — `ConnectionManager` singleton (`getConnectionManager`); tracks cached (by config name) and ephemeral connections plus `WorkerBridge` instances, closes everything on the `app:shutdown` observer event.
+- [`src/core/connection/defaults.ts`](../../src/core/connection/defaults.ts) — `DEFAULT_PORTS` per dialect and the shared `PortSchema` (1-65535) used by `core/config` and `core/settings`.
+- [`src/core/connection/dialects/mssql.ts`](../../src/core/connection/dialects/mssql.ts) — `resolveTlsServerName`/`buildTediousOptions`; connecting to MSSQL by IP address needs a synthetic SNI ServerName (`UNVERIFIED_TLS_SERVER_NAME`) because RFC 6066 forbids an IP literal as SNI, and `verifyDatabaseExists` probes `sys.databases` on `master` before opening the real pool to avoid a cryptic tedious/tarn ECONNRESET hang.
+- [`src/core/connection/dialects/mssql-limit-plugin.ts`](../../src/core/connection/dialects/mssql-limit-plugin.ts) — `MssqlLimitPlugin`, a Kysely `OperationNodeTransformer` that rewrites `LimitNode` → `TopNode` because Kysely 0.28.x's `MssqlQueryCompiler` doesn't override `visitLimit()`.
+- [`src/core/connection/dialects/sqlite.ts`](../../src/core/connection/dialects/sqlite.ts) / `sqlite-bun.ts` — `better-sqlite3` vs `bun:sqlite` adapters; `factory.ts` picks the Bun one when `globalThis.Bun` is defined.
+- [`src/core/connection/dialects/bun-sqlite.d.ts`](../../src/core/connection/dialects/bun-sqlite.d.ts) — hand-written minimal `bun:sqlite` type declarations, to avoid depending on full `bun-types`.
+- [`src/core/explore/operations.ts`](../../src/core/explore/operations.ts) — `fetchOverview`, `fetchList`, `fetchDetail`; overview counts are derived from the same listing calls the detail views use (not separate `COUNT(*)` queries) so the two can't disagree; `__noorm_*` tables are filtered out of `tables`/`indexes`/`foreignKeys`/`triggers` unless `includeNoormTables` is set.
+- [`src/core/explore/dialects/postgres.ts`](../../src/core/explore/dialects/postgres.ts), `mysql.ts`, `mssql.ts`, `sqlite.ts` — system-catalog queries per dialect (`information_schema`/`pg_catalog` for postgres, `INFORMATION_SCHEMA` for mysql, `sys.*` for mssql); postgres and mssql each define an `EXCLUDED_SCHEMAS` negative filter, mysql instead scopes every query to `TABLE_SCHEMA = ` via `resolveSchema()` (a positive single-database match, since MySQL has no schema level below the database), sqlite has neither (`assertSchemaSupported` in `operations.ts` rejects a `schema` option on sqlite).
+- [`src/core/teardown/operations.ts`](../../src/core/teardown/operations.ts) — `truncateData` (disable FK → truncate → re-enable FK, three separate statement groups so FK re-enable still runs even if truncate fails), `teardownSchema` (drop order: FK constraints → CHECK constraints (mssql only) → procedures → functions → views → tables → types, because MSSQL schema-bound objects hold dependency locks on their tables), `previewTeardown` (dry-run wrapper).
+- [`src/core/teardown/dialects/postgres.ts`](../../src/core/teardown/dialects/postgres.ts), `mysql.ts`, `mssql.ts`, `sqlite.ts` — per-dialect DDL generation (`truncateTable`, `dropTable`, `dropView`, `dropFunction`, `dropProcedure`, `dropType`, `dropForeignKey`); only MSSQL implements `dropCheckConstraints` (a CHECK constraint referencing a scalar UDF blocks dropping that function while the table exists — MSSQL error 3729).
+- [`src/core/transfer/planner.ts`](../../src/core/transfer/planner.ts) — `planTransfer`; queries source table metadata + FK relations per dialect, topologically sorts tables into dependency order, probes destination schema for missing tables, and (for cross-dialect transfers) builds per-table `columnTypes` via `buildDtSchema` from `core/dt`; `queryMysqlTables` detects the identity column via `INFORMATION_SCHEMA.COLUMNS.EXTRA LIKE '%auto_increment%'`.
+- [`src/core/transfer/executor.ts`](../../src/core/transfer/executor.ts) — `executeTransfer`; three per-table strategies picked in `executeTransfer`: `transferTableSameServer` (direct SQL, same dialect + same server + `onConflict: 'fail'` + not cross-dialect), `transferTableCrossDialect` (routes through `DtStreamer`/`createKeysetPager`/`queryDatabaseVersion` from `core/dt`), `transferTableCrossServer` (batched INSERT).
+- [`src/core/transfer/same-server.ts`](../../src/core/transfer/same-server.ts) — `isSameServer`; PostgreSQL is *never* same-server (no `dblink`/`postgres_fdw`, so a same-database same-server statement would degenerate to `INSERT INTO t SELECT ... FROM t`); MySQL/MSSQL can query cross-database on one server; SQLite is never same-server.
+- [`src/core/transfer/dialects/postgres.ts`](../../src/core/transfer/dialects/postgres.ts), `mysql.ts`, `mssql.ts` — per-dialect FK toggle, identity-insert toggle, sequence reset, conflict-aware INSERT, and direct-transfer SQL builders (no sqlite module — transfer excludes sqlite).
+- [`src/core/transfer/events.ts`](../../src/core/transfer/events.ts) — `TransferEvents` observer contract (`transfer:planning`, `transfer:plan:ready`, `transfer:starting`, `transfer:table:before/progress/after`, `transfer:complete`).
## CLI code
-- [`src/core/db/index.ts`](../../src/core/db/index.ts) — `checkDbStatus`, `createDb`, `destroyDb`, `getDialectOperations`
-- [`src/core/db/dual.ts`](../../src/core/db/dual.ts) — `withDualConnection`; opens source + destination connections for transfer
-- [`src/core/db/dialects/`](../../src/core/db/dialects) — dialect-specific create/drop implementations
-- [`src/core/explore/operations.ts`](../../src/core/explore/operations.ts) — `queryTables`, `queryViews`, `queryFunctions`, `queryIndexes`, `queryForeignKeys`, `queryProcedures`, `queryTypes`
-- [`src/core/explore/dialects/`](../../src/core/explore/dialects) — per-dialect SQL for introspection queries
-- [`src/core/explore/types.ts`](../../src/core/explore/types.ts) — `TableInfo`, `ColumnInfo`, `ViewInfo`, `IndexInfo`, `ForeignKeyInfo`, etc.
-- [`src/core/teardown/operations.ts`](../../src/core/teardown/operations.ts) — `truncateData`, `teardownSchema`, `previewTeardown`
-- [`src/core/teardown/dialects/`](../../src/core/teardown/dialects) — dialect-specific truncate/drop implementations
-- [`src/core/teardown/types.ts`](../../src/core/teardown/types.ts) — `TruncateOptions`, `TeardownOptions`, `TeardownResult`
-- [`src/core/transfer/index.ts`](../../src/core/transfer/index.ts) — `transferData`; gates via `assertPolicy` (`core/policy`, `db:reset` permission) against the destination config before opening any connection, then orchestrates plan + execute
-- [`src/core/transfer/executor.ts`](../../src/core/transfer/executor.ts) — `executeTransfer`; batch row copy with FK ordering
-- [`src/core/transfer/planner.ts`](../../src/core/transfer/planner.ts) — `planTransfer`; dependency-sorted transfer plan
-- [`src/core/transfer/same-server.ts`](../../src/core/transfer/same-server.ts) — `sameServerTransfer`; direct SQL shortcut when source + dest are on same server
-- [`src/core/transfer/dialects/`](../../src/core/transfer/dialects) — per-dialect identity-column and conflict-resolution strategies
-- [`src/core/transfer/types.ts`](../../src/core/transfer/types.ts) — `TransferOptions`, `TransferResult`, `TransferPlan`
-- [`src/core/connection/factory.ts`](../../src/core/connection/factory.ts) — `createConnection`, `testConnection`; Kysely instance factory
-- [`src/core/connection/manager.ts`](../../src/core/connection/manager.ts) — `ConnectionManager`; singleton connection lifecycle
-- [`src/core/connection/dialects/`](../../src/core/connection/dialects) — dialect drivers (pg, mysql2, tedious, better-sqlite3)
+- [`src/cli/db/index.ts`](../../src/cli/db/index.ts) — registers the `db` command group: `create`, `drop`, `explore`, `reset`, `teardown`, `transfer`, `truncate`.
+- [`src/cli/db/create.ts`](../../src/cli/db/create.ts) — `noorm db create`; gates via `checkConfigPolicy(..., 'db:create')` before any status probe (SQLite's probe would otherwise auto-create the file for a denied role).
+- [`src/cli/db/drop.ts`](../../src/cli/db/drop.ts) — `noorm db drop`; warns to stderr when `NOORM_CONNECTION_*` env overrides retarget the config's stored database away from what will actually be dropped, gated on `db:destroy`.
+- [`src/cli/db/reset.ts`](../../src/cli/db/reset.ts) — `noorm db reset`; thin wrapper requiring `--yes`, delegates to `ctx.noorm.db.reset()` (SDK teardown + build).
+- [`src/cli/db/teardown.ts`](../../src/cli/db/teardown.ts) — `noorm db teardown`; `--dry-run`, `--preserve-schemas`; exits 1 (after already dropping objects) if `postScript` was configured but failed to execute.
+- [`src/cli/db/truncate.ts`](../../src/cli/db/truncate.ts) — `noorm db truncate`; `--dry-run`, `--preserve`, `--only`.
+- [`src/cli/db/transfer.ts`](../../src/cli/db/transfer.ts) — `noorm db transfer`; three mutually-exclusive modes (`--to `, `--export `, `--import `); `.dtzx` export/import prompts for a masked passphrase on an interactive TTY, requires `--passphrase` non-interactively; `fk`/`identity` flags are declared under their positive names so citty's built-in `--no-fk`/`--no-identity` negation works (a `noFk`/`noIdentity` declaration would silently no-op).
+- [`src/cli/db/explore.ts`](../../src/cli/db/explore.ts) — `noorm db explore`; bare invocation prints overview counts, subcommands drill into each category.
+- [`src/cli/db/explore-tables.ts`](../../src/cli/db/explore-tables.ts), `explore-views.ts`, `explore-procedures.ts`, `explore-functions.ts`, `explore-types.ts`, `explore-indexes.ts`, `explore-fks.ts`, `explore-triggers.ts` — one Citty subcommand per `ExploreCategory`; `views`/`procedures`/`functions`/`types`/`triggers` accept a positional `name` directly for detail view, `tables` instead reaches detail view only via a nested `detail` subcommand (`noorm db explore tables detail `, in `explore-tables-detail.ts`), `indexes`/`fks` are list-only.
+- [`src/cli/db/explore-tables-detail.ts`](../../src/cli/db/explore-tables-detail.ts) — `noorm db explore tables detail `, registered as a subcommand of `explore-tables.ts`.
## Docs
-- [`docs/dev/explore.md`](../dev/explore.md) — explore internals
-- [`docs/dev/teardown.md`](../dev/teardown.md) — teardown internals
-- [`docs/dev/transfer.md`](../dev/transfer.md) — transfer internals
-- [`docs/guide/database/create.md`](../guide/database/create.md) — create database guide
-- [`docs/guide/database/teardown.md`](../guide/database/teardown.md) — teardown guide
-- [`docs/guide/database/transfer.md`](../guide/database/transfer.md) — transfer guide
-- [`docs/guide/database/explore.md`](../guide/database/explore.md) — explore guide
-- [`docs/guide/database/terminal.md`](../guide/database/terminal.md) — SQL terminal guide
+- [`docs/guide/database/create.md`](../guide/database/create.md) — `noorm db create` walkthrough.
+- [`docs/guide/database/explore.md`](../guide/database/explore.md) — `noorm db explore` walkthrough.
+- [`docs/guide/database/teardown.md`](../guide/database/teardown.md) — teardown/truncate walkthrough.
+- [`docs/guide/database/transfer.md`](../guide/database/transfer.md) — transfer walkthrough.
+- [`docs/dev/transfer.md`](../dev/transfer.md) — transfer module design notes (FK ordering, identity preservation, same-server optimization, conflict resolution).
## Coupling
-- Transfer calls `withDualConnection` from [`src/core/db/dual.ts`](../../src/core/db/dual.ts) — dual-connection semantics shared with other DB ops.
-- Teardown must skip `__noorm_*` tables (defined in [`src/core/shared/tables.ts`](../../src/core/shared/tables.ts)) — `isNoormTable` guard in `teardown/operations.ts`.
-- Connection manager ([`src/core/connection/manager.ts`](../../src/core/connection/manager.ts)) is used by runner, change executor, SQL terminal, vault ops — reset-manager pattern coordinates with lifecycle domain.
-- CLI commands in [`src/cli/db/`](../../src/cli/db) surface all these ops — explore query shapes flow through to CLI output formatters.
-- [`src/cli/db/drop.ts`](../../src/cli/db/drop.ts) calls `checkConfigPolicy` from [`src/core/policy/`](../../src/core/policy) (`db:destroy` permission) — `--yes` now satisfies the matrix's confirmation requirement rather than gating on a `protected` boolean.
-- `transferData` ([`src/core/transfer/index.ts`](../../src/core/transfer/index.ts)) calls `assertPolicy` from [`src/core/policy/`](../../src/core/policy) — transfer and drop both route through the same policy domain as runner/change/sql-terminal.
-- DT module ([`src/core/dt/`](../../src/core/dt)) reads rows from transfer context — transfer and DT share the row-fetch pattern.
+- **core-policy**: every destructive entry point (`assertDbPolicy` in [`src/core/db/policy.ts`](../../src/core/db/policy.ts), `assertPolicy`/`checkConfigPolicy` calls in [`src/core/transfer/index.ts`](../../src/core/transfer/index.ts) and `src/cli/db/*.ts`) resolves against `Permission` values (`db:create`, `db:reset`, `db:destroy`, `db:truncate`, `db:teardown`, `transfer:plan`) and the role matrix defined in [`src/core/policy/matrix.ts`](../../src/core/policy/matrix.ts) and [`src/core/policy/types.ts`](../../src/core/policy/types.ts). Adding a new destructive db operation means adding its permission there first.
+- **core-state**: [`src/core/db/operations.ts`](../../src/core/db/operations.ts) and [`src/core/db/dual.ts`](../../src/core/db/dual.ts) call `bootstrapSchema`/`tablesExist`/`ensureSchemaVersion` from [`src/core/version/`](../../src/core/version); [`src/core/connection/manager.ts`](../../src/core/connection/manager.ts) subscribes to the `app:shutdown` event from [`src/core/observer.ts`](../../src/core/observer.ts); connection config types come from [`src/core/config/types.ts`](../../src/core/config/types.ts).
+- **core-change**: [`src/core/teardown/operations.ts`](../../src/core/teardown/operations.ts) imports `ChangeHistory`/`ChangeTracker` from [`src/core/change/`](../../src/core/change) to mark changes stale and record a reset event when `teardownSchema` is called with `configName`/`executedBy`.
+- **sdk** ([`src/core/dt/`](../../src/core/dt)): [`src/core/transfer/planner.ts`](../../src/core/transfer/planner.ts) and `executor.ts` depend on `buildDtSchema`, `DtStreamer`, `createKeysetPager`, `queryDatabaseVersion` from [`src/core/dt/`](../../src/core/dt) for cross-dialect type conversion and streaming — a change to the DT column-type model can break cross-dialect transfer.
+- **worker-bridge**: [`src/core/connection/manager.ts`](../../src/core/connection/manager.ts) tracks `WorkerBridge` instances (type from [`src/core/worker-bridge/types.ts`](../../src/core/worker-bridge/types.ts)) so they shut down alongside regular connections; [`src/workers/connection.ts`](../../src/workers/connection.ts) (the persistent DB worker) imports `core/connection` to own the actual Kysely instance off the main thread.
+- **sdk namespaces** ([`src/sdk/namespaces/db.ts`](../../src/sdk/namespaces/db.ts), `dt.ts`, `transfer.ts`) wrap `core/explore`, `core/teardown`, `core/transfer`, `core/dt` directly; `core/db` is reached only transitively, via `core/transfer/index.ts`'s use of `core/db/dual.ts`'s `withDualConnection` — `db.ts` itself has no `core/db` import. [`src/cli/db/create.ts`](../../src/cli/db/create.ts) and `drop.ts` call `checkDbStatus`/`createDb`/`destroyDb` from `core/db` directly, bypassing the SDK/`withContext` layer entirely. The SDK is otherwise the primary consumer surface for the CLI's `withContext`-based commands (`ctx.noorm.db.*`, `ctx.noorm.transfer.*`).
+- **tui**: [`src/tui/hooks/useConnection.ts`](../../src/tui/hooks/useConnection.ts), `useVaultConnection.ts`, and [`src/tui/utils/connection.ts`](../../src/tui/utils/connection.ts), `run-context.ts`, `config-validation.ts`, `change-loader.ts` import `core/connection` directly for the TUI's own connect/validate flows.
+- **mcp-rpc**: [`src/rpc/commands/explore.ts`](../../src/rpc/commands/explore.ts) calls into `core/explore` directly.
## Conventions worth knowing
-- `testConnection(config, { testServerOnly: true })` connects to the dialect's system database without requiring the target DB — used in setup wizards.
-- All dialects tested in integration: [`tests/integration/explore/`](../../tests/integration/explore), [`tests/integration/teardown/`](../../tests/integration/teardown), [`tests/integration/transfer/`](../../tests/integration/transfer).
-- Transfer supports PostgreSQL, MySQL, MSSQL only (not SQLite) — `TRANSFER_SUPPORTED_DIALECTS` in [`src/core/transfer/dialects/index.ts`](../../src/core/transfer/dialects/index.ts).
-- Same-server transfer skips batch loop and uses direct `INSERT … SELECT` SQL.
-- Teardown skips noorm internal tables by name; `previewTeardown` returns a dry-run list without executing.
+- Every dialect-dispatch module (`db`, `explore`, `teardown`, `transfer`) follows the same shape: a `types.ts` interface (`DialectDbOperations`, `DialectExploreOperations`, `TeardownDialectOperations`, `TransferDialectOperations`), one implementation file per dialect under `dialects/`, and a `dialects/index.ts` with a `Record` (or `Partial>` for transfer) lookup plus a `getXOperations(dialect)` accessor.
+- `__noorm_*` table names are the universal marker for noorm-internal tracking tables; `isNoormTable()` is defined separately in [`src/core/teardown/operations.ts`](../../src/core/teardown/operations.ts) (exported for tests) and [`src/core/explore/operations.ts`](../../src/core/explore/operations.ts) (private), both checking the same prefix.
+- Dialect-specific default schemas are centralized in [`src/core/teardown/operations.ts`](../../src/core/teardown/operations.ts)'s `DEFAULT_SCHEMAS` (`postgres: 'public'`, `mssql: 'dbo'`) for display-name qualification; MySQL and SQLite have no entry because MySQL's "schema" is the database itself and SQLite has none.
+- `CreateDbOptions.precheckedStatus` and `TruncateOptions`/`TeardownOptions`/`DestroyDbOptions.policy` are both optional-but-load-bearing: callers that already ran an equivalent gate (the SDK) omit `policy`; every caller with no gate of its own must supply it.
+- `attempt`/`attemptSync` from `@logosdx/utils` wrap operations only where the result is inspected/translated (per [`.claude/rules/typescript.md`](../../.claude/rules/typescript.md)); errors that would just propagate are left unwrapped.
+- Tests under [`tests/core/explore/dialects/`](../../tests/core/explore/dialects) use a shared recording harness ([`tests/core/explore/recording-db.ts`](../../tests/core/explore/recording-db.ts), `createRecordingDb`) that builds a real Kysely instance with the dialect's actual adapter/compiler/introspector but a driver that records compiled SQL and replays canned rows per regex-matched rule — this exists because an earlier stub returning `SELECT 1` made wrong `WHERE` predicates structurally undetectable.
+- [`tests/integration/`](../../tests/integration) (12 subdirectories: `change`, `cli`, `connection`, `error-diagnostics`, `explore`, `impersonate`, `runner`, `sdk`, `sql-terminal`, `teardown`, `transfer`, `version`) requires live database services and is organized per-dialect within most subdirectories (`postgres.test.ts`, `mysql.test.ts`, `mssql.test.ts`, `sqlite.test.ts` where applicable); several subdirectories (`change`, `runner`, `sdk`, `sql-terminal`, `version`) exercise other domains' code but need a live connection to do so. [`tests/integration/cli/setup.ts`](../../tests/integration/cli/setup.ts) is the shared CLI-integration harness (`noorm()`/`noormJson()` run the built CLI binary via `zx`, `setupTestProject()`/`cleanupTestProject()` manage a per-test SQLite project directory).
+- `skipIfNoContainer(dialect)` / `TEST_CONNECTIONS` / `makeTestConfig` / `createTestConnection` / `deployTestSchema` / `seedTestData` (from [`tests/utils/db.ts`](../../tests/utils/db.ts), outside this domain's paths) are the shared fixtures every integration test in this domain uses to skip gracefully when postgres/mysql/mssql containers aren't reachable.
diff --git a/docs/wiki/core-identity.md b/docs/wiki/core-identity.md
index 30928e3e..af7d2e37 100644
--- a/docs/wiki/core-identity.md
+++ b/docs/wiki/core-identity.md
@@ -1,64 +1,107 @@
---
type: Domain
+description: Cryptographic identity, team vault secrets, structured logging, and ad-hoc SQL terminal execution.
---
# core-identity
## What it does
-Two-tier identity system: (1) audit identity — name/email for execution provenance tracking; (2) cryptographic identity — Ed25519-like keypair for config sharing and state encryption. Also owns the vault (per-database encrypted secret store), the logger (structured log output with redaction), and the SQL terminal history.
+Manages X25519 cryptographic identity (keypair generation, key storage, CI env bootstrap, audit-identity resolution) used to authenticate users and to encrypt shared secrets. Stores team-shared vault secrets in the database, encrypted with a vault key individually sealed to each user's public key. Captures observer events into a structured, redacted, rotating log file. Executes ad-hoc SQL through Kysely behind an access-policy gate and persists gzip-compressed query history per config.
## CLI code
-- [`src/core/identity/crypto.ts`](../../src/core/identity/crypto.ts) — keypair generation, `encryptForRecipient`, `decryptWithPrivateKey`, `deriveStateKey`, `encryptState`, `decryptState`
-- [`src/core/identity/factory.ts`](../../src/core/identity/factory.ts) — `loadExistingIdentity`; load keypair from disk
-- [`src/core/identity/resolver.ts`](../../src/core/identity/resolver.ts) — `resolveIdentity`, `formatIdentity`, `identityToString`; audit identity resolution with caching
-- [`src/core/identity/storage.ts`](../../src/core/identity/storage.ts) — `saveKeyPair`, `loadPrivateKey`, `loadPublicKey`; disk persistence at `~/.noorm/`
-- [`src/core/identity/sync.ts`](../../src/core/identity/sync.ts) — `registerIdentity`; syncs identity record to `__noorm_identities__` table
-- [`src/core/identity/env.ts`](../../src/core/identity/env.ts) — `loadIdentityFromEnv`; CI override via `NOORM_IDENTITY_*` env vars
-- [`src/core/identity/hash.ts`](../../src/core/identity/hash.ts) — identity hash derivation
-- [`src/core/identity/types.ts`](../../src/core/identity/types.ts) — `Identity`, `CryptoIdentity`, `KnownUser`, `IdentityOptions`
-- [`src/core/vault/storage.ts`](../../src/core/vault/storage.ts) — vault CRUD (`initVault`, `getSecret`, `setSecret`, `removeSecret`, `listSecrets`)
-- [`src/core/vault/key.ts`](../../src/core/vault/key.ts) — `generateVaultKey`, `encryptVaultKey`, `decryptVaultKey`, `encryptSecret`, `decryptSecret`
-- [`src/core/vault/copy.ts`](../../src/core/vault/copy.ts) — `copyVaultKey`; share vault access with another identity
-- [`src/core/vault/propagate.ts`](../../src/core/vault/propagate.ts) — `propagateVault`; push vault data across configs
-- [`src/core/vault/resolve.ts`](../../src/core/vault/resolve.ts) — `resolveVaultSecret`; read a secret at runtime for template context injection
-- [`src/core/vault/events.ts`](../../src/core/vault/events.ts) — vault observer event types
-- [`src/core/logger/logger.ts`](../../src/core/logger/logger.ts) — `Logger`; structured logging with levels, rotation, redaction
-- [`src/core/logger/redact.ts`](../../src/core/logger/redact.ts) — pattern-based redaction of sensitive values
-- [`src/core/logger/formatter.ts`](../../src/core/logger/formatter.ts) — log line formatting
-- [`src/core/logger/rotation.ts`](../../src/core/logger/rotation.ts) — log file rotation
-- [`src/core/logger/queue.ts`](../../src/core/logger/queue.ts) — async write queue to prevent I/O blocking
-- [`src/core/logger/classifier.ts`](../../src/core/logger/classifier.ts) — log level classification
-- [`src/core/sql-terminal/executor.ts`](../../src/core/sql-terminal/executor.ts) — `executeRawSql`; classifies the query (`classifyStatements` from `core/policy`) and gates it via `assertPolicy` against a `SqlPolicyGate` (access/channel/dialect) before delegating to `executeRawSqlUnchecked`, the ungated execution path reserved for tests
-- [`src/core/sql-terminal/history.ts`](../../src/core/sql-terminal/history.ts) — `SqlHistoryManager`; persistent SQL REPL history
+- [`src/core/identity/types.ts`](../../src/core/identity/types.ts) — `Identity` (audit identity: name/email/source), `CryptoIdentity` (keypair + identityHash), `KnownUser`, `SharedConfigPayload`
+- [`src/core/identity/crypto.ts`](../../src/core/identity/crypto.ts) — `generateKeyPair`, `derivePublicKeyFromPrivate`, `encryptForRecipient`/`decryptWithPrivateKey` (ephemeral X25519 ECDH + HKDF + AES-256-GCM for config sharing), `deriveStateKey`/`encryptState`/`decryptState` (state-file encryption keyed directly off the identity private key)
+- [`src/core/identity/hash.ts`](../../src/core/identity/hash.ts) — `computeIdentityHash` (SHA-256 of `email\0name\0machine\0os`), `isValidIdentityHash`, `truncateHash`
+- [`src/core/identity/storage.ts`](../../src/core/identity/storage.ts) — reads/writes `~/.noorm/identity.key`, `identity.pub`, `identity.json`; `isValidKeyHex` (88/96 hex-char length check), `validateKeyPermissions` (0600 enforcement, always `true` on win32), `backupKeyPair`; in-memory `setKeyOverride`/`setIdentityOverride` for CI
+- [`src/core/identity/factory.ts`](../../src/core/identity/factory.ts) — `detectIdentityDefaults` (git config / OS user), `createCryptoIdentity`, `regenerateKeyPair`, `createIdentityForExistingKeys`, `loadExistingIdentity`
+- [`src/core/identity/env.ts`](../../src/core/identity/env.ts) — `loadIdentityFromEnv`; builds a `CryptoIdentity` from `NOORM_IDENTITY_PRIVATE_KEY`/`NOORM_IDENTITY_NAME`/`NOORM_IDENTITY_EMAIL` without touching disk
+- [`src/core/identity/resolver.ts`](../../src/core/identity/resolver.ts) — `resolveIdentity` (audit identity priority chain: config override → crypto identity → `NOORM_IDENTITY` env → git → system user), `formatIdentity`/`identityToString`
+- [`src/core/identity/provenance.ts`](../../src/core/identity/provenance.ts) — `withAgentProvenance`; appends `(via )` to `executed_by`, truncated to fit the 255-char DB column
+- [`src/core/identity/sync.ts`](../../src/core/identity/sync.ts) — `registerIdentity`, `fetchKnownUsers`, `syncIdentity`, `syncIdentityWithConfig`; upserts the current identity into the `identities` table and pulls known users
+- [`src/core/identity/index.ts`](../../src/core/identity/index.ts) — barrel export plus `resolveIdentity`'s process-level cache (`cachedIdentity`), `getIdentityForConfig`, `getIdentityWithCrypto`, `waitForIdentityToLoad`
+- [`src/core/vault/types.ts`](../../src/core/vault/types.ts) — `EncryptedVaultKey`, `VaultSecret`, `VaultStatus`, `VaultCopyResult`, `VaultPropagationResult`, `PendingVaultUser`
+- [`src/core/vault/key.ts`](../../src/core/vault/key.ts) — `generateVaultKey`, `encryptVaultKey`/`decryptVaultKey` (ephemeral X25519 ECDH sealed per-recipient), `encryptSecret`/`decryptSecret` (AES-256-GCM with the vault key)
+- [`src/core/vault/storage.ts`](../../src/core/vault/storage.ts) — `initializeVault` (idempotent), `getVaultKey`, `setVaultSecret`/`getVaultSecret`/`getAllVaultSecrets`/`listVaultSecretKeys`/`deleteVaultSecret`/`vaultSecretExists`, `getVaultStatus`; only `initializeVault`/`getVaultKey`/`setVaultSecret`/`listVaultSecretKeys`/`deleteVaultSecret` have `*Checked` policy-gated twins — `getVaultSecret`/`getAllVaultSecrets` are gated indirectly (useless without the key `getVaultKeyChecked` gates, per its doc comment) and are called ungated in production (e.g. [`src/cli/vault/list.ts`](../../src/cli/vault/list.ts)'s `getAllVaultSecrets`); `vaultSecretExists` takes no vault key at all (just checks row existence by key name) and relies instead on a caller-level policy check ([`src/cli/vault/rm.ts`](../../src/cli/vault/rm.ts) gates the surrounding command via `checkVaultPolicy`); `getVaultStatus` has no twin and needs none
+- [`src/core/vault/policy.ts`](../../src/core/vault/policy.ts) — `VaultPolicyGate`, `checkVaultPolicy`/`assertVaultPolicy`; wraps `core/policy`'s `checkConfigPolicy`/`assertPolicy` for vault permissions
+- [`src/core/vault/propagate.ts`](../../src/core/vault/propagate.ts) — `getUsersWithoutVaultAccess`, `propagateVaultKey`/`propagateVaultKeyTo` (+ `*Checked` twins); per-user failures land in `result.failed` rather than being dropped
+- [`src/core/vault/resolve.ts`](../../src/core/vault/resolve.ts) — `resolveVaultKey`, `resolveSecret`/`resolveSecrets` (priority: config-specific local → global local → vault), `buildSecretsContext` (merges vault → global → config-specific for template rendering)
+- [`src/core/vault/copy.ts`](../../src/core/vault/copy.ts) — `copyVaultSecrets`; cross-config secret copy over `withDualConnection`, supports `dryRun` and `force`
+- [`src/core/vault/events.ts`](../../src/core/vault/events.ts) — `VaultEvents`, merged into `NoormEvents`
+- [`src/core/vault/index.ts`](../../src/core/vault/index.ts) — barrel export for the vault module
+- [`src/core/logger/types.ts`](../../src/core/logger/types.ts) — `LogLevel`, `EntryLevel`, `LogEntry`, `LoggerConfig`, `DEFAULT_LOGGER_CONFIG` (default file [`.noorm/state/noorm.log`](../../.noorm/state/noorm.log))
+- [`src/core/logger/classifier.ts`](../../src/core/logger/classifier.ts) — `classifyEvent` (regex-pattern event-name → level classification), `shouldLog`
+- [`src/core/logger/formatter.ts`](../../src/core/logger/formatter.ts) — `generateMessage` (per-event message templates), `formatEntry`, `serializeEntry`, `sanitizeData`
+- [`src/core/logger/color.ts`](../../src/core/logger/color.ts) — `formatColorLine`, `STATUS_ICONS`, `formatDuration`; uses [`src/core/theme.ts`](../../src/core/theme.ts)
+- [`src/core/logger/timestamp.ts`](../../src/core/logger/timestamp.ts) — `formatLogTimestamp`/`formatLogTimestampIso`; hand-rolled `Date` formatting to avoid a dayjs dependency on the per-line hot path
+- [`src/core/logger/redact.ts`](../../src/core/logger/redact.ts) — `filterData`, `maskValue`, `addMaskedFields`, `redactCredentialsInText` (strips credentials embedded in URIs), `listenForSecrets` (subscribes to `secret:set`/`global-secret:set`)
+- [`src/core/logger/rotation.ts`](../../src/core/logger/rotation.ts) — `checkAndRotate`, `parseSize`, `rotateFile`, `cleanupRotatedFiles`
+- [`src/core/logger/queue.ts`](../../src/core/logger/queue.ts) — `WriteQueue`; ordered, non-blocking file writes
+- [`src/core/logger/logger.ts`](../../src/core/logger/logger.ts) — `Logger` class; subscribes via `observer.queue(/./)`, writes console (JSON/color/plain) and file (always JSON) output, owns the rotation interval and `app:shutdown` cleanup
+- [`src/core/logger/init.ts`](../../src/core/logger/init.ts) — `enableAutoLoggerInit`/`disableAutoLoggerInit`/`getInitializedLogger`; defers `Logger` construction until `settings:loaded` fires, forces file logging off (`file: ''`) under `isCi()`
+- [`src/core/logger/reader.ts`](../../src/core/logger/reader.ts) — `readLogFile`; parses JSON-Lines log file newest-first, skipping malformed lines
+- [`src/core/logger/index.ts`](../../src/core/logger/index.ts) — barrel export for the logger module
+- [`src/core/sql-terminal/types.ts`](../../src/core/sql-terminal/types.ts) — `SqlExecutionResult`, `SqlHistoryEntry`, `SqlHistoryFile(Serialized)`, `ClearResult`
+- [`src/core/sql-terminal/executor.ts`](../../src/core/sql-terminal/executor.ts) — `executeRawSqlUnchecked` (ungated Kysely `sql.raw()` execution), `executeRawSql` (classifies the statement via `classifyStatements` and gates it via `assertPolicy` before delegating), `SqlPolicyGate`
+- [`src/core/sql-terminal/history.ts`](../../src/core/sql-terminal/history.ts) — `SqlHistoryManager`; per-config history at `.noorm/state/history/.json` plus gzipped per-query results under `.noorm/state/history//`
+- [`src/core/sql-terminal/index.ts`](../../src/core/sql-terminal/index.ts) — barrel export; deliberately omits `executeRawSqlUnchecked` so the ungated primitive is never one autocomplete away from a production call site
+- [`src/cli/identity/index.ts`](../../src/cli/identity/index.ts) — `noorm identity` command group: `init`, `edit`, `export`, `list`
+- [`src/cli/identity/init.ts`](../../src/cli/identity/init.ts) — creates a new identity; `--force --yes` backs up and replaces existing keys and warns that existing `state.enc` is not re-encrypted under the new key
+- [`src/cli/identity/edit.ts`](../../src/cli/identity/edit.ts) — updates name/email via `createIdentityForExistingKeys` (recomputes `identityHash`, warns when it changes)
+- [`src/cli/identity/export.ts`](../../src/cli/identity/export.ts) — prints the public key for sharing
+- [`src/cli/identity/list.ts`](../../src/cli/identity/list.ts) — lists known users synced from connected databases (reads local state, not the vault)
+- [`src/cli/secret/index.ts`](../../src/cli/secret/index.ts) — `noorm secret` command group: `list`, `rm`, `set` (config-scoped local secrets, stored in `state.enc`)
+- [`src/cli/secret/_policy.ts`](../../src/cli/secret/_policy.ts) — `resolveSecretPolicy`; resolves the target config name and gates it via `checkConfigPolicy`, since `StateManager` itself takes no config object
+- [`src/cli/secret/list.ts`](../../src/cli/secret/list.ts), [`src/cli/secret/rm.ts`](../../src/cli/secret/rm.ts), [`src/cli/secret/set.ts`](../../src/cli/secret/set.ts) — list/remove/set a config-scoped secret; `rm`/`set` require `secret:write` and honor `NOORM_YES` via `isYesMode`
+- [`src/cli/vault/index.ts`](../../src/cli/vault/index.ts) — `noorm vault` command group: `cp`, `init`, `list`, `propagate`, `rm`, `set`
+- [`src/cli/vault/_secret-value.ts`](../../src/cli/vault/_secret-value.ts) — `readSecretValue`; shared `--stdin`-or-positional secret input for `vault set` and `secret set`, strips one trailing newline
+- [`src/cli/vault/init.ts`](../../src/cli/vault/init.ts), [`src/cli/vault/list.ts`](../../src/cli/vault/list.ts), [`src/cli/vault/set.ts`](../../src/cli/vault/set.ts), [`src/cli/vault/rm.ts`](../../src/cli/vault/rm.ts) — initialize/list/set/remove vault secrets through `withVaultContext` plus the `*Checked` core entrypoints
+- [`src/cli/vault/propagate.ts`](../../src/cli/vault/propagate.ts) — grants vault access to pending identities; shows the pending list before requiring `--yes`, treats any per-user failure as `EXIT.PARTIAL`
+- [`src/cli/vault/cp.ts`](../../src/cli/vault/cp.ts) — copies vault secrets between two configs via `copyVaultSecrets`, loading identity/private key directly rather than through `withVaultContext` because the core function manages its own dual connection
+- [`src/cli/sql/index.ts`](../../src/cli/sql/index.ts) — `noorm sql` command group: `query`, `history`, `clear`, `repl`
+- [`src/cli/sql/_config.ts`](../../src/cli/sql/_config.ts) — `resolveHistoryConfigName`; resolves the config name for history-only commands, decrypting state only when no explicit name or `NOORM_CONFIG` is set
+- [`src/cli/sql/query.ts`](../../src/cli/sql/query.ts) — executes one SQL statement (or `--file`) via `executeRawSql`; does not record history
+- [`src/cli/sql/history.ts`](../../src/cli/sql/history.ts) — shows persisted history recorded by the interactive terminal only
+- [`src/cli/sql/clear.ts`](../../src/cli/sql/clear.ts) — clears history, optionally `--older-than `
+- [`src/cli/sql/repl.ts`](../../src/cli/sql/repl.ts) — launches the Ink TUI directly at the SQL Terminal screen; requires a TTY, rejects `--yes`/`NOORM_YES`
## Docs
-- [`docs/dev/identity.md`](../dev/identity.md) — cryptographic identity internals
+- [`docs/cli/identity.md`](../cli/identity.md) — `noorm identity` command reference
+- [`docs/cli/secret.md`](../cli/secret.md) — `noorm secret` command reference
+- [`docs/cli/sql.md`](../cli/sql.md) — `noorm sql` command reference
+- [`docs/cli/sql-repl.md`](../cli/sql-repl.md) — `noorm sql repl` command reference
+- [`docs/dev/identity.md`](../dev/identity.md) — identity system internals
- [`docs/dev/vault.md`](../dev/vault.md) — vault internals
-- [`docs/dev/secrets.md`](../dev/secrets.md) — secret management
+- [`docs/dev/secrets.md`](../dev/secrets.md) — local secrets internals
- [`docs/dev/logger.md`](../dev/logger.md) — logger internals
- [`docs/dev/sql-terminal.md`](../dev/sql-terminal.md) — SQL terminal internals
- [`docs/guide/environments/vault.md`](../guide/environments/vault.md) — user guide: vault
-- [`docs/guide/environments/secrets.md`](../guide/environments/secrets.md) — user guide: secrets
-- [`docs/cli/identity.md`](../cli/identity.md) — identity CLI reference
-- [`docs/dev/headless.md`](../dev/headless.md) — headless/CI identity override docs
+- [`docs/guide/environments/secrets.md`](../guide/environments/secrets.md) — user guide: local secrets
+- [`docs/guide/database/terminal.md`](../guide/database/terminal.md) — user guide: SQL terminal
## Coupling
-- Identity keypair is used by [`src/core/state/manager.ts`](../../src/core/state/manager.ts) for state encryption/decryption — identity must initialize before StateManager loads.
-- Vault uses the identity hash for per-user encryption key derivation — identity + vault are tightly coupled.
-- Logger uses [`src/core/observer.ts`](../../src/core/observer.ts) events to capture log lines from all modules.
-- SQL terminal history writes to `~/.noorm/sql-history/` — path convention separate from project `.noorm/`.
-- CI environment loads identity from env vars (`NOORM_IDENTITY_NAME`, `NOORM_IDENTITY_EMAIL`, `NOORM_IDENTITY_KEY`) via `loadIdentityFromEnv` — CLI init reads from keychain by default.
-- `__noorm_identities__` table (defined in [`src/core/shared/tables.ts`](../../src/core/shared/tables.ts)) stores registered identities — `sync.ts` writes to it.
-- `executeRawSql` imports `assertPolicy`/`classifyStatements` from [`src/core/policy/`](../../src/core/policy) — the read/write/ddl classification and the destructive-function denylist live in the policy domain, not here.
+- core-policy: [`src/core/vault/policy.ts`](../../src/core/vault/policy.ts) and [`src/core/sql-terminal/executor.ts`](../../src/core/sql-terminal/executor.ts) gate every operation through `assertPolicy`/`checkConfigPolicy`/`classifyStatements` from [`src/core/policy/`](../../src/core/policy); [`src/core/identity/provenance.ts`](../../src/core/identity/provenance.ts) reads `AgentHarness` from [`src/core/policy/harness.ts`](../../src/core/policy/harness.ts). Changes to `Permission`, `ConfigAccess`, or the SQL classifier ripple into all four submodules here.
+- core-state: [`src/core/vault/resolve.ts`](../../src/core/vault/resolve.ts) takes a `StateManager` for local-secret resolution; [`src/core/logger/init.ts`](../../src/core/logger/init.ts) waits on the `settings:loaded` event and reads the `Settings` type from [`src/core/settings/`](../../src/core/settings); [`src/core/identity/sync.ts`](../../src/core/identity/sync.ts) calls `tablesExist`/`ensureSchemaVersion` from [`src/core/version/`](../../src/core/version); [`src/cli/sql/_config.ts`](../../src/cli/sql/_config.ts) and [`src/cli/secret/_policy.ts`](../../src/cli/secret/_policy.ts) resolve the active config through `initState`/`getStateManager`. `StateManager`'s own encryption key is derived from the identity private key (`deriveStateKey`), so `core-state` cannot decrypt state until this domain has an identity available.
+- core-db: [`src/core/identity/sync.ts`](../../src/core/identity/sync.ts) and [`src/core/vault/copy.ts`](../../src/core/vault/copy.ts) open connections via `createConnection`/`withDualConnection` from [`src/core/connection/`](../../src/core/connection) and [`src/core/db/dual.ts`](../../src/core/db/dual.ts); vault and identity storage share the `NoormDatabase`/`noormDb`/`getNoormTables` helpers in [`src/core/shared/tables.ts`](../../src/core/shared/tables.ts).
+- sdk: [`src/sdk/namespaces/vault.ts`](../../src/sdk/namespaces/vault.ts), [`src/sdk/namespaces/db.ts`](../../src/sdk/namespaces/db.ts), [`src/sdk/namespaces/lock.ts`](../../src/sdk/namespaces/lock.ts), [`src/sdk/state.ts`](../../src/sdk/state.ts), [`src/sdk/context.ts`](../../src/sdk/context.ts), and [`src/sdk/noorm-ops.ts`](../../src/sdk/noorm-ops.ts) import `core/identity` and `core/vault` directly and re-export a subset as the public `@noormdev/sdk` surface — renaming or reshaping any `*Checked` export here is a breaking SDK change.
+- mcp-rpc: [`src/rpc/commands/query.ts`](../../src/rpc/commands/query.ts) calls `executeRawSql` directly, sharing the same `SqlPolicyGate` contract used by `noorm sql query` and the TUI SQL terminal.
+- tui: [`src/tui/screens/identity/`](../../src/tui/screens/identity), [`src/tui/screens/vault/`](../../src/tui/screens/vault), [`src/tui/screens/db/SqlTerminalScreen.tsx`](../../src/tui/screens/db/SqlTerminalScreen.tsx) (and sibling Sql*Screen files), and [`src/tui/components/overlays/LogViewerOverlay.tsx`](../../src/tui/components/overlays/LogViewerOverlay.tsx) import these core modules directly and re-implement the CLI's policy-gate pattern in Ink screens.
+- cli (shared plumbing): `withContext`/`withVaultContext`, `outputResult`/`outputError`, `sharedArgs`, `isYesMode`, `resolveChannel` used throughout [`src/cli/identity/`](../../src/cli/identity), [`src/cli/secret/`](../../src/cli/secret), [`src/cli/vault/`](../../src/cli/vault), [`src/cli/sql/`](../../src/cli/sql) live in [`src/cli/_utils.ts`](../../src/cli/_utils.ts); exit codes come from [`src/cli/_exit.ts`](../../src/cli/_exit.ts) — both owned by the `cli` domain, not this one.
## Conventions worth knowing
-- Cryptographic identity stored at `~/.noorm/identity.key` (private) and `~/.noorm/identity.pub` (public).
-- Vault secrets encrypted per-database with a vault key; vault key encrypted per-user with their public key.
-- Logger redaction patterns are configurable; `redact.ts` uses regex matching against log line text.
-- `loadIdentityFromEnv` checks `NOORM_IDENTITY_*` vars — used by `noorm ci identity` command for CI injection.
-- Audit identity resolution caches result for duration of command execution.
+- Policy-gate pattern: raw vault primitives (`getVaultKey`, `setVaultSecret`, `deleteVaultSecret`, `listVaultSecretKeys`, `propagateVaultKey`, `propagateVaultKeyTo`, `initializeVault`) are ungated; every production caller must use the `*Checked` wrapper instead. `executeRawSqlUnchecked` is the SQL-terminal equivalent and is deliberately excluded from [`src/core/sql-terminal/index.ts`](../../src/core/sql-terminal/index.ts)'s barrel export.
+- Two distinct identity concepts: `Identity` (audit identity — name/email/source, used for `executed_by` tracking, resolution priority config → crypto → `NOORM_IDENTITY` env → git → system user, process-cached in [`src/core/identity/index.ts`](../../src/core/identity/index.ts) unless overrides are passed) versus `CryptoIdentity` (keypair + `identityHash`, used for vault/config-sharing encryption).
+- Ephemeral-key encryption pattern is repeated, not shared: config sharing ([`src/core/identity/crypto.ts`](../../src/core/identity/crypto.ts)) and vault key sealing ([`src/core/vault/key.ts`](../../src/core/vault/key.ts)) each implement their own `deriveSharedSecret`/`deriveEncryptionKey` — generate an ephemeral X25519 keypair, ECDH with the recipient's public key, HKDF-SHA256 to a 32-byte key, AES-256-GCM — with a distinct HKDF `info` string per use (`'noorm-config-share'`, `'noorm-state-encryption'`, `'noorm-vault-key'`).
+- Key material validation: `isValidKeyHex` requires exactly 88 (SPKI public) or 96 (PKCS8 private) hex characters. A key that fails validation is a hard error everywhere it's read (`loadPrivateKey`, `setKeyOverride`, `deriveStateKey`) — never silently truncated, because `Buffer.from(str, 'hex')` truncates at the first invalid pair and a truncated key still HKDFs to a deterministic, publicly-computable output.
+- Key file permissions: `~/.noorm/identity.key` is written 0600, `identity.pub` 0644; `validateKeyPermissions` checks `mode & 0o077 === 0` (rejects any group/other bit) and always returns `true` on `win32`, since Windows `stat` doesn't reliably report POSIX modes.
+- CI identity bootstrap: `NOORM_IDENTITY_PRIVATE_KEY`/`NOORM_IDENTITY_NAME`/`NOORM_IDENTITY_EMAIL` are read once by `loadIdentityFromEnv`; the CLI entrypoint installs the result via `setKeyOverride`/`setIdentityOverride`, in-memory overrides that make `loadPrivateKey`/`loadIdentityMetadata` skip disk reads for the rest of the process. `computeIdentityHash` deliberately omits `os.hostname()` for the env-loaded case so every CI runner sharing the same private key resolves to the same identity.
+- Secret key names: `SECRET_KEY_PATTERN` in [`src/core/vault/storage.ts`](../../src/core/vault/storage.ts) (`/^[A-Za-z][A-Za-z0-9_]*$/`) is deliberately identical to `StateManager.setSecret`'s validation — both feed the same `$.secrets` template namespace.
+- Observer event typing is inconsistent across the four submodules: `identity:*`, `sql-terminal:*`, and `logger:*` events are declared inline in `NoormEvents` ([`src/core/observer.ts`](../../src/core/observer.ts)), while `vault:*` gets its own `VaultEvents` interface ([`src/core/vault/events.ts`](../../src/core/vault/events.ts)) merged into `NoormEvents` — vault is the only one of the four with a dedicated events file.
+- Log event classification ([`src/core/logger/classifier.ts`](../../src/core/logger/classifier.ts)) is regex-pattern-based on event-name prefix/suffix, not a registry — a new `xyz:` event namespace defaults to `debug` level unless added to `INFO_PATTERNS`/`WARN_PATTERNS`/`ERROR_PATTERNS`/`DEBUG_PATTERNS`.
+- SQL history and results are gzip-compressed (`SqlHistoryManager`, [`src/core/sql-terminal/history.ts`](../../src/core/sql-terminal/history.ts)) and written 0600 (files) / 0700 (dirs) — the same permission discipline as `state.enc`. `sql query` (headless/CI) never writes history; only `sql repl` and the TUI SQL terminal do. Vault has no local-disk history file — vault secrets live in DB rows only.
+- `sql history`/`sql clear` resolve their target config via `resolveHistoryConfigName` (explicit flag → `NOORM_CONFIG` → active config), decrypting state only when neither of the first two is set — these are the only two SQL-terminal commands that don't require a database connection.
+- Test coverage includes dedicated edge-case files beyond the mirrored happy-path tests: [`tests/core/identity/key-file-corruption.test.ts`](../../tests/core/identity/key-file-corruption.test.ts) and [`tests/core/identity/storage-key-permission-guard.test.ts`](../../tests/core/identity/storage-key-permission-guard.test.ts) target malformed-key rejection and the 0600 permission guard; [`tests/core/vault/idempotent-init.test.ts`](../../tests/core/vault/idempotent-init.test.ts) and [`tests/core/vault/policy-gate.test.ts`](../../tests/core/vault/policy-gate.test.ts) cover `initializeVault`'s idempotency and the `*Checked` gate; [`tests/core/logger/redact-coverage.test.ts`](../../tests/core/logger/redact-coverage.test.ts) and [`tests/core/logger/rotation-reopen.test.ts`](../../tests/core/logger/rotation-reopen.test.ts) target redaction field coverage and the re-open-after-rotation behavior in `logger.ts`.
diff --git a/docs/wiki/core-policy.md b/docs/wiki/core-policy.md
index 99308ebf..73833796 100644
--- a/docs/wiki/core-policy.md
+++ b/docs/wiki/core-policy.md
@@ -1,5 +1,6 @@
---
type: Domain
+description: Access-control policy — role×permission matrix, SQL statement classifier, and the legacy protected→access migration, imported by every caller channel that enforces a config-scoped action.
---
# core-policy
@@ -10,38 +11,45 @@ Single access-control layer for every config-scoped action across every caller c
Also owns raw-SQL statement classification (`read`/`write`/`ddl`, with a destructive-function denylist) used to gate ad-hoc SQL, and the one-version `protected` boolean → `access` migration path (`resolveLegacyAccess`).
-## CLI code
+## Artifacts
- [`src/core/policy/types.ts`](../../src/core/policy/types.ts) — `Role` (`viewer`/`operator`/`admin`), `Channel` (`user`/`agent`), `ConfigAccess` (`{ user: Role; agent: Role | false }`), `Permission`, `PolicyTarget`, `PolicyCell` (`allow`/`confirm`/`deny`), `PolicyCheck`
-- [`src/core/policy/matrix.ts`](../../src/core/policy/matrix.ts) — `MATRIX`; the hard-coded `Permission × Role → PolicyCell` table (not user-extensible), mirroring [`docs/spec/config-access-roles.md`](../spec/config-access-roles.md)
-- [`src/core/policy/check.ts`](../../src/core/policy/check.ts) — `checkPolicy`, `checkConfigPolicy`, `assertPolicy`, `guarded`, `confirmationPhraseFor`; the enforcement entrypoints every caller reaches for
-- [`src/core/policy/classify.ts`](../../src/core/policy/classify.ts) — `classifyStatements`; SQL-parser-cst-based statement classifier with a keyword-based fallback, a CTE-DML upgrade rule, and `DESTRUCTIVE_FUNCTIONS` denylist (e.g. `pg_terminate_backend`, `lo_import`, `setval`)
+- [`src/core/policy/matrix.ts`](../../src/core/policy/matrix.ts) — `MATRIX`, the hard-coded `Permission × Role → PolicyCell` table (not user-extensible), mirroring [`docs/spec/config-access-roles.md`](../spec/config-access-roles.md)
+- [`src/core/policy/check.ts`](../../src/core/policy/check.ts) — `checkPolicy`, `checkConfigPolicy`, `assertPolicy`, `isVisibleToChannel`, `guarded`, `formatAccessTag`, `confirmationPhraseFor`; the enforcement and display entrypoints every caller reaches for
+- [`src/core/policy/channel.ts`](../../src/core/policy/channel.ts) — `resolveChannel`; resolves `NOORM_CHANNEL` env override, then harness provenance (`isAgentSession`), then defaults to `user`
+- [`src/core/policy/harness.ts`](../../src/core/policy/harness.ts) — `AGENT_HARNESSES`, `detectAgentHarness`, `isAgentSession`; env-marker allowlist for Claude Code, Codex, Cursor, Gemini CLI, and a generic `AI_AGENT`/`NOORM_AGENT` self-declaration
+- [`src/core/policy/classify.ts`](../../src/core/policy/classify.ts) — `classifyStatements`; sql-parser-cst-based statement classifier with a keyword-based fallback (used whenever the CST parser throws, which is the routine path for `mssql` since it has no grammar of its own here), a CTE-DML upgrade rule, and `DESTRUCTIVE_FUNCTIONS` denylist (e.g. `pg_terminate_backend`, `lo_import`, `setval`, `pg_read_file`)
- [`src/core/policy/legacy-access.ts`](../../src/core/policy/legacy-access.ts) — `resolveLegacyAccess`, `DEFAULT_ACCESS` (`{ user: 'admin', agent: 'viewer' }`), `GUARDED_ACCESS` (`{ user: 'operator', agent: 'viewer' }`)
- [`src/core/policy/index.ts`](../../src/core/policy/index.ts) — barrel export for all of the above
## Docs
-- [`docs/spec/config-access-roles.md`](../spec/config-access-roles.md) — implementation contract: data model, matrix, migration
+- [`docs/spec/config-access-roles.md`](../spec/config-access-roles.md) — implementation contract: data model, permission matrix, SQL classification, enforcement, migration
- [`docs/design/config-access-roles.md`](../design/config-access-roles.md) — design rationale for the role model
-- [`docs/dev/config.md`](../dev/config.md), [`docs/dev/config-sharing.md`](../dev/config-sharing.md) — updated to describe `access` in place of `protected`
+- [`docs/dev/config.md`](../dev/config.md), [`docs/dev/config-sharing.md`](../dev/config-sharing.md) — describe `access` in place of the removed `protected` boolean
- [`docs/guide/environments/configs.md`](../guide/environments/configs.md), [`docs/guide/environments/stages.md`](../guide/environments/stages.md) — user-facing access-role guidance
- [`skills/noorm/references/config.md`](../../skills/noorm/references/config.md) — skill reference for config access roles
## Coupling
-- [`src/core/config/schema.ts`](../../src/core/config/schema.ts)/`types.ts` and [`src/core/state/manager.ts`](../../src/core/state/manager.ts) import `resolveLegacyAccess`/`ConfigAccess` — see the `core-state` domain for where `access` is resolved, defaulted, and backfilled.
-- [`src/core/change/executor.ts`](../../src/core/change/executor.ts), [`src/core/runner/runner.ts`](../../src/core/runner/runner.ts), [`src/core/transfer/index.ts`](../../src/core/transfer/index.ts), and [`src/core/sql-terminal/executor.ts`](../../src/core/sql-terminal/executor.ts) all call `assertPolicy` at their core seam — see `core-change`, `core-runner`, `core-db`, and `core-identity` respectively.
-- [`src/mcp/server.ts`](../../src/mcp/server.ts) and [`src/rpc/types.ts`](../../src/rpc/types.ts) gate every non-`'open'` `RpcCommand` via `checkConfigPolicy` — see `mcp-rpc`.
-- [`src/sdk/guards.ts`](../../src/sdk/guards.ts) and [`src/sdk/index.ts`](../../src/sdk/index.ts) wrap `checkConfigPolicy` for the SDK's `channel`-aware guards — see `sdk`.
-- [`src/tui/components/dialogs/SmartConfirm.tsx`](../../src/tui/components/dialogs/SmartConfirm.tsx)/`ProtectedConfirm.tsx` and every destructive-action TUI screen call `checkConfigPolicy` directly to build confirm-dialog props — see `tui`.
-- [`src/core/settings/rules.ts`](../../src/core/settings/rules.ts)'s `protected` rule-match key checks `guarded(config)`, not a config field — see `core-state`.
+- [`src/core/change/executor.ts`](../../src/core/change/executor.ts) calls `assertPolicy` (`change:run`/`change:ff`/`change:revert`) — changes to `MATRIX` or `PolicyCheck` shape affect `core-change`.
+- [`src/core/runner/runner.ts`](../../src/core/runner/runner.ts) calls `assertPolicy` (`run:build`/`run:file`/`run:dir`) — affects `core-runner`.
+- [`src/core/db/policy.ts`](../../src/core/db/policy.ts) (`assertDbPolicy`), [`src/core/transfer/index.ts`](../../src/core/transfer/index.ts) (`assertPolicy`, `db:reset`/`transfer:plan`) — affects `core-db`.
+- [`src/core/vault/policy.ts`](../../src/core/vault/policy.ts) (`checkVaultPolicy`/`assertVaultPolicy`) and [`src/core/sql-terminal/executor.ts`](../../src/core/sql-terminal/executor.ts) (`assertPolicy` + `classifyStatements`) — affects `core-identity`.
+- [`src/core/config/schema.ts`](../../src/core/config/schema.ts), [`src/core/config/types.ts`](../../src/core/config/types.ts), [`src/core/config/resolver.ts`](../../src/core/config/resolver.ts), [`src/core/state/access.ts`](../../src/core/state/access.ts), and [`src/core/settings/rules.ts`](../../src/core/settings/rules.ts) import `ConfigAccess`/`resolveLegacyAccess`/`guarded` only — data resolution, defaulting, and rule-matching, not enforcement. Affects `core-state`; a shape change to `ConfigAccess` propagates to all of them.
+- [`src/mcp/server.ts`](../../src/mcp/server.ts) calls `checkConfigPolicy` at dispatch for every non-`'open'` `RpcCommand`; [`src/rpc/types.ts`](../../src/rpc/types.ts) types `RpcCommand.permission` as `Permission | 'open'` — affects `mcp-rpc`.
+- [`src/sdk/guards.ts`](../../src/sdk/guards.ts) (`checkConfigPolicy`, throws `ProtectedConfigError`) and [`src/sdk/index.ts`](../../src/sdk/index.ts)/[`src/sdk/types.ts`](../../src/sdk/types.ts) (re-export `Channel`/`ConfigAccess`/`Role`) — affects `sdk`.
+- [`src/cli/_utils.ts`](../../src/cli/_utils.ts), [`src/cli/change/rm.ts`](../../src/cli/change/rm.ts), `src/cli/config/*.ts`, [`src/cli/db/create.ts`](../../src/cli/db/create.ts)/`drop.ts`, [`src/cli/run/inspect.ts`](../../src/cli/run/inspect.ts)/`preview.ts`, [`src/cli/secret/_policy.ts`](../../src/cli/secret/_policy.ts), [`src/cli/sql/query.ts`](../../src/cli/sql/query.ts), `src/cli/vault/*.ts` call `resolveChannel`/`checkConfigPolicy`/`assertPolicy` directly — affects `cli`.
+- [`src/tui/components/dialogs/SmartConfirm.tsx`](../../src/tui/components/dialogs/SmartConfirm.tsx) and every destructive-action screen (`ChangeFFScreen`, `ChangeRunScreen`, `ChangeRevertScreen`, `ChangeRemoveScreen`, `ChangeRewindScreen`, `ChangeNextScreen`, `ConfigRemoveScreen`, `DbCreateScreen`, `DbDestroyScreen`, `DbTeardownScreen`, `DbTransferScreen`, `DbTruncateScreen`, `LockForceScreen`, `RunBuildScreen`, `VaultScreen`) call `checkConfigPolicy`/`confirmationPhraseFor` directly to build confirm-dialog props; [`src/tui/app-context.tsx`](../../src/tui/app-context.tsx) and [`src/tui/screens/config/ConfigAddScreen.tsx`](../../src/tui/screens/config/ConfigAddScreen.tsx) derive default `access` values from `DEFAULT_ACCESS`/`GUARDED_ACCESS` only; [`src/tui/utils/config-validation.ts`](../../src/tui/utils/config-validation.ts) reads `guarded` — affects `tui`.
## Conventions worth knowing
-- `checkPolicy`'s `confirm` cell resolves differently per channel: `user` prompts for `yes-` (`confirmationPhraseFor`, skippable via `NOORM_YES`), `agent` collapses `confirm` to `deny` — an agent confirming its own destructive action is theater.
-- `agent: false` (invisible config) is never a role and is not expected to reach `checkPolicy` — visibility is enforced upstream (`SessionManager.connect`, `list_configs`).
-- `checkConfigPolicy` fails closed: a config with no `access` at all is denied on every channel.
-- `classifyStatements` fails closed to `ddl` for anything it can't positively classify as `read` or `write` — an unrecognized statement could do anything.
+- `checkPolicy`'s `confirm` cell resolves differently per channel: `user` prompts for `yes-` (`confirmationPhraseFor`, skippable via `NOORM_YES`), `agent` collapses `confirm` to `deny` — an agent confirming its own destructive action is theater, and on the CLI it would need only `--yes` to walk through it.
+- `agent: false` (invisible config) is never a role and is not expected to reach `checkPolicy` — visibility is enforced upstream via `isVisibleToChannel`.
+- `checkConfigPolicy`/`assertPolicy` fail closed: a config with no `access` at all is denied on every channel.
+- `resolveChannel` precedence: `NOORM_CHANNEL` env override (exact `user`/`agent` only) > harness provenance (`isAgentSession`, an allowlist of env markers the harnesses set for themselves, deliberately excluding `TERM_PROGRAM`/`CI`/TTY state) > `user` default. The MCP server never calls `resolveChannel` — it constructs its session with `agent` literally.
+- `classifyStatements` fails closed to `ddl` for anything it can't positively classify as `read` or `write` — an unrecognized statement, or one the CST parser can't parse and the keyword fallback can't identify, could do anything.
- `DESTRUCTIVE_FUNCTIONS` is a denylist, not an allowlist, by design: `SELECT f()` is statically undecidable, so only known-dangerous builtins upgrade a `SELECT` to `write`.
-- `guarded(target)` (`target.access.user !== 'admin'`) is display-only — used by TUI styling, `config list`, and settings rule matching — never an enforcement input.
-- Tests: [`tests/core/policy/check.test.ts`](../../tests/core/policy/check.test.ts) (270L) covers `checkPolicy`/`checkConfigPolicy`/`assertPolicy`/`guarded`/`confirmationPhraseFor`; [`tests/core/policy/classify.test.ts`](../../tests/core/policy/classify.test.ts) (446L) covers read/write/ddl classification, multi-statement, CTE handling, CTE-DML, and the destructive-function denylist.
+- `guarded(target)` (`target.access.user !== 'admin'`) and `formatAccessTag` are display-only — used by TUI styling, `config list`, and settings rule matching — never an enforcement input.
+- `DEFAULT_ACCESS` (`{ user: 'admin', agent: 'viewer' }`) is what every config gets when its author never set `access` and never set legacy `protected: true`; a legacy `protected: true` maps to `GUARDED_ACCESS` (`{ user: 'operator', agent: 'viewer' }`).
+- Tests: [`tests/core/policy/check.test.ts`](../../tests/core/policy/check.test.ts) drives `checkPolicy` against an `EXPECTED_MATRIX` authored independently of [`src/core/policy/matrix.ts`](../../src/core/policy/matrix.ts) (from [`docs/spec/config-access-roles.md`](../spec/config-access-roles.md)) — covers 15 of the 27 defined permissions across role×channel; the matrix has grown past this independent test copy for `db:truncate`, `db:teardown`, `config:write`, `vault:read`/`write`/`propagate`, `secret:read`/`write`, `transfer:plan`, `lock:force`, and `debug:read`/`write`; [`tests/core/policy/classify.test.ts`](../../tests/core/policy/classify.test.ts) and `classify-corpus.test.ts` cover CST/fallback-path assertions and an adversarial SQL corpus; [`tests/core/policy/agent-escalation.test.ts`](../../tests/core/policy/agent-escalation.test.ts) asserts the agent/CLI-shell-out escalation is closed for every permission on a stock config; [`tests/core/policy/default-access.test.ts`](../../tests/core/policy/default-access.test.ts) drives `parseConfig`/`migrateState` end to end rather than comparing constants; [`tests/core/policy/channel.test.ts`](../../tests/core/policy/channel.test.ts) and `visibility.test.ts` cover `resolveChannel` precedence and `isVisibleToChannel` fail-closed handling respectively.
diff --git a/docs/wiki/core-runner.md b/docs/wiki/core-runner.md
index ebf83239..5e393323 100644
--- a/docs/wiki/core-runner.md
+++ b/docs/wiki/core-runner.md
@@ -1,50 +1,70 @@
---
type: Domain
+description: SQL file execution with checksum dedup and Eta templating
---
# core-runner
## What it does
-Executes SQL files against a database connection with checksum-based deduplication. Processes `.sql` and `.sql.tmpl` files. Template files are rendered via Eta before execution. Results are tracked in `__noorm_executions__`. Preview mode renders and returns SQL without executing.
+- Executes `.sql` and `.sql.tmpl` files against a Kysely connection ([`src/core/runner/runner.ts`](../../src/core/runner/runner.ts)), tracking each run in `__noorm_change__`/`__noorm_executions__` via `Tracker` ([`src/core/runner/tracker.ts`](../../src/core/runner/tracker.ts)) so unchanged files are skipped on the next run.
+- Renders `.sql.tmpl` files through an Eta-based engine ([`src/core/template/engine.ts`](../../src/core/template/engine.ts)) with auto-loaded data side-cars, inherited `$helpers` files, and built-in helpers (`quote`, `escape`, `include`, `json`, `now`, `uuid`).
+- Exposes five execution modes — `runBuild`, `runFile`, `runDir`, `runFiles`, `preview` — plus `checkFilesStatus` for pre-execution status categorization ([`src/core/runner/runner.ts`](../../src/core/runner/runner.ts)).
-The template engine ([`src/core/template/`](../../src/core/template)) is co-owned by the runner: runner calls `processFile`/`isTemplate` to render `.sql.tmpl` files before execution.
+## Artifacts
+
+- [`src/core/runner/runner.ts`](../../src/core/runner/runner.ts) — `runBuild`/`runFile`/`runDir`/`runFiles`/`preview`/`checkFilesStatus`/`discoverFiles`/`executeFiles`; the first six form the policy-gated entrypoint set every SDK/TUI/CLI caller funnels through — `discoverFiles` and `executeFiles` are not policy-gated.
+- [`src/core/runner/tracker.ts`](../../src/core/runner/tracker.ts) — `Tracker` class: `needsRun`, `needsRunByName`, `createOperation`, `recordExecution`, `createFileRecords`, `updateFileExecution`, `finalizeOperation`, `skipRemainingFiles`, `priorSuccessfulExecutions`.
+- [`src/core/runner/checksum.ts`](../../src/core/runner/checksum.ts) — `computeChecksum`, `computeChecksumFromContent`, `computeCombinedChecksum` (SHA-256).
+- [`src/core/runner/mssql-batches.ts`](../../src/core/runner/mssql-batches.ts) — `splitMssqlBatches` (splits on line-only `GO`), `executeSqlBody` (dialect dispatch: mssql splits on `GO`, sqlite splits on statement boundaries, postgres/mysql execute the body whole).
+- [`src/core/runner/sqlite-statements.ts`](../../src/core/runner/sqlite-statements.ts) — `splitSqliteStatements`, a boundary scanner (not a SQL parser) that tracks string/identifier quoting, comments, and `BEGIN`/`CASE`…`END` trigger bodies to find real statement boundaries.
+- [`src/core/runner/types.ts`](../../src/core/runner/types.ts) — `RunOptions`, `RunContext`, `FileResult`, `BatchResult`, `NeedsRunResult`, `FileInput`, `ExecuteFilesOptions`, `FilesStatusResult`, and `DEFAULT_RUN_OPTIONS`.
+- [`src/core/runner/index.ts`](../../src/core/runner/index.ts) — public export surface for the domain.
+- [`src/core/template/engine.ts`](../../src/core/template/engine.ts) — `processFile`, `processFiles`, `renderTemplate`, `isTemplate`; owns the configured `Eta` instance (custom `{% %}` tags, `$` varName, `autoEscape: false`) and the `-- {% %}` directive-line stripping convention.
+- [`src/core/template/context.ts`](../../src/core/template/context.ts) — `buildContext` assembles the `$` template context (helpers, auto-loaded data files, config, secrets, `env`, built-ins); `MissingSecretError` and the `$.secrets` proxy that throws on an unresolved key instead of stringifying `undefined`.
+- [`src/core/template/helpers.ts`](../../src/core/template/helpers.ts) — `findHelperFiles`/`loadHelpers` walk from a template's directory up to `projectRoot`, merging `$helpers.{ts,js,mjs}` files root-to-leaf (child overrides parent).
+- [`src/core/template/loaders/`](../../src/core/template/loaders) — per-extension data loaders: `json5.ts`, `yaml.ts`, `csv.ts` (lazy-imported), `js.ts` (dynamic import, `Bun.build()` bundling path for compiled binaries), `sql.ts`, `dt.ts` (`.dt`/`.dtz`, not `.dtzx`). `loaders/index.ts` registers extensions and marks `.js`/`.mjs`/`.ts` as `isExecutableExtension`.
+- [`src/core/template/utils.ts`](../../src/core/template/utils.ts) — `toContextKey` (filename → camelCase), `sqlEscape`, `sqlQuote` (throws `UndefinedSqlValueError` on `undefined`), `isWithinRoot` (segment-aware path containment), `generateUuid`, `isoNow`.
+- [`src/core/template/types.ts`](../../src/core/template/types.ts) — `TemplateContext`, `BuiltInHelpers`, `RenderOptions`, `ProcessResult`, `Loader`/`LoaderRegistry`, `DATA_EXTENSIONS`, `TEMPLATE_EXTENSION` (`.tmpl`), `HELPER_FILENAME` (`$helpers`), `HELPER_EXTENSIONS`.
## CLI code
-- [`src/core/runner/runner.ts`](../../src/core/runner/runner.ts) — `runBuild`, `runFile`, `runDir`, `preview`, `discoverFiles`; core execution loop. Each exported entrypoint gates via `assertPolicy` (`core/policy`) against `RunContext.access`/`channel` (`run:build`/`run:file`/`run:dir` permissions)
-- [`src/core/runner/tracker.ts`](../../src/core/runner/tracker.ts) — `Tracker`; records execution results, computes `needsRun`, queries `__noorm_executions__`
-- [`src/core/runner/checksum.ts`](../../src/core/runner/checksum.ts) — `computeChecksum`, `computeChecksumFromContent`, `computeCombinedChecksum`; SHA-based change detection
-- [`src/core/runner/mssql-batches.ts`](../../src/core/runner/mssql-batches.ts) — `executeSqlBody`; splits MSSQL `GO`-delimited batches before execution
-- [`src/core/runner/types.ts`](../../src/core/runner/types.ts) — `RunOptions`, `RunContext`, `FileResult`, `BatchResult`, `SkipReason`, etc.
-- [`src/core/template/engine.ts`](../../src/core/template/engine.ts) — Eta-based `renderTemplate`; called by runner for `.sql.tmpl` files
-- [`src/core/template/context.ts`](../../src/core/template/context.ts) — `buildContext`; assembles variables injected into templates
-- [`src/core/template/helpers.ts`](../../src/core/template/helpers.ts) — built-in SQL helpers (`sqlEscape`, `sqlQuote`, `generateUuid`, `isoNow`)
-- [`src/core/template/loaders/`](../../src/core/template/loaders) — data loaders for JSON5, YAML, CSV, JS, SQL side-car files
-- [`src/core/template/types.ts`](../../src/core/template/types.ts) — `TemplateContext`, `Loader`, `LoaderRegistry`
+- [`src/cli/run/index.ts`](../../src/cli/run/index.ts) — registers the `run` command group with subcommands `build`, `dir`, `exec`, `file`, `files`, `inspect`, `preview`.
+- [`src/cli/run/build.ts`](../../src/cli/run/build.ts) — `run build`; runs `ctx.noorm.run.build`, reports `unmatchedInclude`/`unmatchedExclude` warnings and dry-run tmp/ output.
+- [`src/cli/run/dir.ts`](../../src/cli/run/dir.ts) — `run dir `; validates the directory exists, reports `EXIT.USAGE` (not success) when zero SQL files are found.
+- [`src/cli/run/exec.ts`](../../src/cli/run/exec.ts) — `run exec `; accepts a directory (delegates to `discoverFiles`) or a glob pattern (expanded via `Bun.Glob` when available, else Node's `fs/promises.glob`).
+- [`src/cli/run/file.ts`](../../src/cli/run/file.ts) — `run file `; executes a single file via `ctx.noorm.run.file`.
+- [`src/cli/run/files.ts`](../../src/cli/run/files.ts) — `run files --paths `; comma-separated file list via `ctx.noorm.run.files`.
+- [`src/cli/run/inspect.ts`](../../src/cli/run/inspect.ts) — `run inspect `; builds the template `$` context without rendering, categorizes entries into data files/helpers/builtins, reports helper load errors and secret counts.
+- [`src/cli/run/preview.ts`](../../src/cli/run/preview.ts) — `run preview `; renders a `.sql.tmpl` and writes raw SQL to stdout (or `--json`), without executing.
+- [`src/cli/run/_render-secrets.ts`](../../src/cli/run/_render-secrets.ts) — `resolveRenderSecrets` shared by `preview`/`inspect`: probes the vault tier with retry disabled so an offline render degrades to local-only secrets (`vaultProbeFailed`) instead of hanging.
## Docs
-- [`docs/dev/runner.md`](../dev/runner.md) — runner internals reference
-- [`docs/dev/template.md`](../dev/template.md) — template engine internals
-- [`docs/guide/sql-files/execution.md`](../guide/sql-files/execution.md) — user guide: file execution
-- [`docs/guide/sql-files/templates.md`](../guide/sql-files/templates.md) — user guide: template syntax
-- [`docs/guide/sql-files/organization.md`](../guide/sql-files/organization.md) — user guide: file layout conventions
+- [`docs/dev/runner.md`](../dev/runner.md) — runner design notes.
+- [`docs/dev/template.md`](../dev/template.md) — template engine design notes.
+- [`docs/cli/run.md`](../cli/run.md) — `noorm run` subcommand reference.
+- [`docs/guide/sql-files/execution.md`](../guide/sql-files/execution.md) — how execution/change-detection works for end users.
+- [`docs/guide/sql-files/organization.md`](../guide/sql-files/organization.md) — file/directory ordering conventions.
+- [`docs/guide/sql-files/templates.md`](../guide/sql-files/templates.md) — `.sql.tmpl` authoring guide.
## Coupling
-- Runner calls [`src/core/template/`](../../src/core/template) (`processFile`, `isTemplate`) — template API changes affect runner's file loop.
-- Runner writes to `__noorm_executions__` table defined in [`src/core/shared/tables.ts`](../../src/core/shared/tables.ts).
-- Change executor ([`src/core/change/executor.ts`](../../src/core/change/executor.ts)) calls `runFile` — runner `RunOptions` changes propagate to change execution.
-- MSSQL batch splitting (`mssql-batches.ts`) is only invoked for MSSQL dialect; dialect info flows in via `RunContext`.
-- CLI commands in [`src/cli/run/`](../../src/cli/run) call `runBuild`, `runFile`, `runDir`, `preview` — CLI surface reflects `RunOptions` defaults.
-- `runBuild`/`runFile`/`runDir` call `assertPolicy` from [`src/core/policy/`](../../src/core/policy) before executing — `RunContext` carries `access`/`channel` for the gate.
+- **core-change**: `ChangeTracker` ([`src/core/change/tracker.ts`](../../src/core/change/tracker.ts)) extends `Tracker`, giving it constructor-compatible checksum tracking — but core-change does not call the core runner's `executeFiles` or depend on its `ExecuteFilesOptions` contract. [`src/core/change/executor.ts`](../../src/core/change/executor.ts) defines its own private, same-named `executeFiles` function with an unrelated signature (`ChangeContext`/`Change`/`ChangeFile[]`/`direction`/`checksum`/`force`/`history`/`startTime`), and the need-to-run check for change execution is a separate `needsRun` implementation on `ChangeHistory` ([`src/core/change/history.ts`](../../src/core/change/history.ts)), not the inherited `Tracker.needsRun`.
+- **core-policy**: every exported entrypoint (`runBuild`/`runFile`/`runDir`/`runFiles`/`preview`/`checkFilesStatus`) gates through `assertPolicy` from [`src/core/policy/index.ts`](../../src/core/policy/index.ts) against the `run:build`/`run:file`/`run:dir` permissions (matrix: viewer deny, operator confirm, admin allow). Adding a new run entrypoint or changing the permission matrix touches both domains.
+- **sdk**: [`src/sdk/namespaces/run.ts`](../../src/sdk/namespaces/run.ts) (`RunNamespace`) wraps 6 of the 8 exported runner functions (`discoverFiles`, `preview`, `runFile`, `runFiles`, `runDir`, `runBuild`) and builds `RunContext` (secrets, dialect, identity, access) for every call; `checkFilesStatus` has no `RunNamespace` wrapper and is called directly by the TUI (`RunDirScreen.tsx`, `RunFileScreen.tsx`), bypassing the SDK layer. [`src/sdk/namespaces/templates.ts`](../../src/sdk/namespaces/templates.ts) wraps the template engine for `ctx.noorm.templates`.
+- **tui**: `src/tui/screens/run/*.tsx` (`RunBuildScreen`, `RunDirScreen`, `RunExecScreen`, `RunFileScreen`, `RunInspectScreen`) and [`src/tui/utils/run-context.ts`](../../src/tui/utils/run-context.ts) consume the same core runner/template functions as the CLI and SDK.
+- **core-state**: emits `build:start`/`build:complete`, `run:file`/`run:dir`/`run:files`, `file:before`/`file:after`/`file:skip`/`file:dry-run`, `template:render`/`template:load`/`template:helpers`, and `error` events, typed in the shared observer at [`src/core/observer.ts`](../../src/core/observer.ts) (a core-state artifact).
+- **core-identity**: `formatIdentity` ([`src/core/identity/resolver.ts`](../../src/core/identity/resolver.ts)) stamps `executedBy` on every tracked operation.
+- **sdk**: [`src/core/template/loaders/dt.ts`](../../src/core/template/loaders/dt.ts) reads `.dt`/`.dtz` files via `DtReader` from [`src/core/dt/reader.ts`](../../src/core/dt/reader.ts) (the DT binary format lives in the sdk domain).
## Conventions worth knowing
-- `DEFAULT_RUN_OPTIONS` defines timeout, skip-unchanged, and preview-mode defaults.
-- `.sql.tmpl` extension triggers template rendering; `.sql` files are executed verbatim.
-- `$helpers.ts` file in the SQL directory is loaded as helper functions for templates.
-- MSSQL batches split on `GO` token (case-insensitive) per `mssql-batches.ts`.
-- Checksum is SHA-256 of file content; combined checksum used for directory-level change detection.
-- `SkipReason` enum: `unchanged`, `preview`, `dry-run`.
+- Checksums are computed from *rendered* content for `.sql.tmpl` files, not raw file bytes — `executeSingleFileWithUpdate` recomputes the checksum after rendering and overwrites the pending row's raw-file checksum, because comparing raw bytes made every template re-execute on every build ([`tests/core/runner/template-dedup.test.ts`](../../tests/core/runner/template-dedup.test.ts)).
+- `executeFiles` inserts a `pending` execution row for every file in a batch upfront (before any file runs), so `Tracker.needsRun` must exclude the running operation's own id (`excludeOperationId`) or every file reads as "new" forever.
+- Dry-run output writes rendered SQL — including every resolved secret in plaintext — to `/tmp/`, mirroring the source path and stripping `.tmpl`; files and any created directories are written owner-only (`mode: 0o600`/`0o700`), and `tmp/` is not gitignored by `noorm init`.
+- `run preview`/`run inspect` reuse the `run:file` permission cell rather than a dedicated permission — both resolve every secret tier into plaintext and can execute `$helpers`/side-car scripts even though nothing is written to the database.
+- Data-file auto-loading in `buildContext` skips `.js`/`.mjs`/`.ts` side-cars unless the template source textually references the resulting context key (`$.key` or `$['key']`) — otherwise `preview`/`inspect`/`--dry-run` would execute arbitrary code with no way for the user to know.
+- `include()` and the `$helpers` directory walk both enforce project-root containment via `isWithinRoot` (segment-aware, not a bare `startsWith`), so a sibling directory like `-evil` cannot be traversed into.
+- `$.secrets` is a `Proxy` that throws `MissingSecretError` on an unresolved key instead of resolving to `undefined` — `sqlQuote(undefined)` also throws `UndefinedSqlValueError` rather than stringifying to the literal text `undefined`.
+- MSSQL batch splitting (`splitMssqlBatches`) and SQLite statement splitting (`splitSqliteStatements`) are the only two dialects requiring file-content splitting before execution; postgres and mysql receive the full file body via `sql.raw(...)`.
diff --git a/docs/wiki/core-state.md b/docs/wiki/core-state.md
index 265c9fe1..df426498 100644
--- a/docs/wiki/core-state.md
+++ b/docs/wiki/core-state.md
@@ -1,70 +1,62 @@
---
type: Domain
+description: Encrypted project state, settings.yml, config resolution, app lifecycle, and schema/state/settings version migration
---
# core-state
## What it does
-Manages encrypted application state (configs, secrets, active config pointer), project settings (`settings.yml`), config resolution, and version migration across three layers (schema, state, settings). Also owns lifecycle (shutdown orchestration) and the project-discovery bootstrap.
+Persists project data across three files with different trust levels: [`.noorm/state/state.enc`](../../.noorm/state/state.enc) (encrypted configs, secrets, known users), `.noorm/settings.yml` (version-controlled build/stage/rule config), and process-lifetime app startup/shutdown coordination. [`src/core/version/`](../../src/core/version) runs three independent migration tracks (schema, state, settings), each keyed on its own integer version number in `CURRENT_VERSIONS` ([`src/core/version/types.ts`](../../src/core/version/types.ts)), separate from the CLI's semver package version. [`src/core/observer.ts`](../../src/core/observer.ts) is the central `ObserverEngine` instance (`@logosdx/observer`) that every domain in the repo emits events through, and whose `NoormEvents` interface aggregates event types from `settings`, `lifecycle`, `update`, `vault`, `transfer`, `dt`, `logger`, and `teardown`.
-Configs are stored encrypted in `.noorm/state/state.enc` using AES-256-GCM. Settings live in `.noorm/settings.yml` (plaintext YAML). Version migration runs at startup across all three layers.
+## Artifacts
-Each config carries `access: ConfigAccess` (per-channel role pair, replacing the removed `protected: boolean`), resolved via `resolveLegacyAccess` from [`src/core/policy/`](../../src/core/policy). [`src/core/config/schema.ts`](../../src/core/config/schema.ts) maps a legacy `protected` boolean input to `access` at parse time; `StateManager.load()` backfills `access` on any config that reaches the current schema version without it.
-
-## CLI code
-
-- [`src/core/state/manager.ts`](../../src/core/state/manager.ts) — `StateManager`; encrypt/decrypt state, CRUD for configs and secrets. `load()` runs the schemaVersion-keyed migration (`migrateState`/`needsStateMigration` from `core/version/state/`, e.g. v2's `protected`→`access` mapping) ahead of the package-semver migration in `state/migrations.ts`, then backfills `access` on any config still missing it
-- [`src/core/state/encryption/`](../../src/core/state/encryption) — AES-256-GCM encrypt/decrypt primitives
-- [`src/core/state/migrations.ts`](../../src/core/state/migrations.ts) — `migrateState`, `needsMigration`; package-semver-keyed, distinct from the schemaVersion-keyed migrations in `core/version/state/`
-- [`src/core/settings/manager.ts`](../../src/core/settings/manager.ts) — `SettingsManager`; loads/saves `settings.yml`, validates against schema, stage merging
-- [`src/core/settings/schema.ts`](../../src/core/settings/schema.ts) — Zod schema for settings file
-- [`src/core/settings/rules.ts`](../../src/core/settings/rules.ts) — `ruleMatches`, `evaluateRule`, `evaluateRules`; config-based conditional overrides. The rule's `protected` match key checks `guarded(config)` (`core/policy`), not a config field
-- [`src/core/settings/defaults.ts`](../../src/core/settings/defaults.ts) — `DEFAULT_SETTINGS`
-- [`src/core/settings/events.ts`](../../src/core/settings/events.ts) — settings-related observer event types
-- [`src/core/config/index.ts`](../../src/core/config/index.ts) — `makeNestedConfig`; builds config object from env at module scope (known contamination source — see CLAUDE.md)
-- [`src/core/config/resolver.ts`](../../src/core/config/resolver.ts) — `resolveConfig`, `SettingsProvider`; picks active config from state + settings. `applyStageCeiling` clamps a resolved config's `access` down to `{ user: 'operator', agent: 'viewer' }` when the linked stage sets `protected: true` — replaces the old hard-violation check in `checkConfigCompleteness`
-- [`src/core/config/schema.ts`](../../src/core/config/schema.ts) — config schema validation; `withResolvedAccess` maps a legacy `protected: boolean` input to `access: ConfigAccess` via `resolveLegacyAccess` (`core/policy`)
-- [`src/core/lifecycle/manager.ts`](../../src/core/lifecycle/manager.ts) — `LifecycleManager`; shutdown phase orchestration, signal handlers
-- [`src/core/lifecycle/handlers.ts`](../../src/core/lifecycle/handlers.ts) — signal/exception handler registration
-- [`src/core/lifecycle/types.ts`](../../src/core/lifecycle/types.ts) — `ShutdownPhase`, `AppMode`, lifecycle state types
-- [`src/core/version/index.ts`](../../src/core/version/index.ts) — `VersionManager`, `checkSchemaVersion`, `migrateSchema`, `ensureSchemaVersion`, `bootstrapSchema`
-- [`src/core/version/schema/`](../../src/core/version/schema), [`src/core/version/state/`](../../src/core/version/state), [`src/core/version/settings/`](../../src/core/version/settings) — per-layer migrations (`version/state/migrations/v2.ts` maps the removed `protected` boolean to `access`)
-- [`src/core/project.ts`](../../src/core/project.ts) — `findProjectRoot`, `initProjectContext`, `isNoormProject`, `getGlobalNoormPath`
-- [`src/core/project-init.ts`](../../src/core/project-init.ts) — `initProjectContext` bootstrap: loads state, settings, lifecycle, runs version migrations
-- [`src/core/environment.ts`](../../src/core/environment.ts) — env variable detection and normalization
-- [`src/core/observer.ts`](../../src/core/observer.ts) — singleton `observer` (ObserverEngine from `@logosdx/observer`); event bus for all modules
+- [`.noorm/state/state.enc`](../../.noorm/state/state.enc) — AES-256-GCM encrypted JSON ([`src/core/state/manager.ts`](../../src/core/state/manager.ts)), mode `0o600`; holds `configs`, `secrets`, `globalSecrets`, `knownUsers`, `activeConfig`, `version`, `schemaVersion`.
+- `.noorm/state/state.enc.bak` — previous-generation backup written by `backupExisting` ([`src/core/state/persistence.ts`](../../src/core/state/persistence.ts)) before every overwrite.
+- `.noorm/state/state.enc.lock` — advisory `O_EXCL` lock file ([`src/core/state/persistence.ts`](../../src/core/state/persistence.ts)), 5s acquire timeout, 30s staleness threshold.
+- `.noorm/settings.yml` — YAML, version controlled, parsed/written by [`src/core/settings/manager.ts`](../../src/core/settings/manager.ts) via the `yaml` package.
+- [`.noorm/`](../../.noorm), [`.noorm/state/`](../../.noorm/state), `.noorm/.gitignore`, `sql/`, `changes/`, and the root [`.gitignore`](../../.gitignore) `# noorm` block — created by `src/core/project-init.ts:performProjectInit`.
## Docs
-- [`docs/dev/config.md`](../dev/config.md) — config internals
-- [`docs/dev/config-sharing.md`](../dev/config-sharing.md) — multi-user config sharing
-- [`docs/dev/settings.md`](../dev/settings.md) — settings file reference
-- [`docs/dev/state.md`](../dev/state.md) — state file internals
-- [`docs/dev/version.md`](../dev/version.md) — version migration internals
-- [`docs/dev/project-discovery.md`](../dev/project-discovery.md) — project root detection
-- [`docs/dev/logger.md`](../dev/logger.md) — logger internals (uses observer)
-- [`docs/guide/environments/configs.md`](../guide/environments/configs.md) — user guide: configs
-- [`docs/guide/environments/stages.md`](../guide/environments/stages.md) — user guide: stages
-- [`docs/guide/environments/secrets.md`](../guide/environments/secrets.md) — user guide: secrets
+- [`docs/dev/config.md`](../dev/config.md) — config internals.
+- [`docs/dev/settings.md`](../dev/settings.md) — settings file reference.
+- [`docs/dev/state.md`](../dev/state.md) — state file internals.
+- [`docs/dev/version.md`](../dev/version.md) — version migration internals.
+- [`docs/dev/project-discovery.md`](../dev/project-discovery.md) — project root detection.
+- [`docs/guide/environments/configs.md`](../guide/environments/configs.md) — user guide: configs.
+- [`docs/guide/environments/stages.md`](../guide/environments/stages.md) — user guide: stages.
+- [`docs/guide/environments/secrets.md`](../guide/environments/secrets.md) — user guide: config-scoped secrets stored in `state.enc`.
## Coupling
-- `src/core/config/index.ts:34` calls `makeNestedConfig(process.env, …)` at module scope — snapshots env at first import. Same bug was fixed for `SettingsManager` in commit `ec9ccc2`. Not yet migrated to call-time.
-- Observer ([`src/core/observer.ts`](../../src/core/observer.ts)) is imported by virtually every core module — it is the event bus; all `observer.emit()` calls couple to TUI hooks.
-- VersionManager runs migrations at startup via `project-init.ts` — schema + state + settings must all be at CURRENT_VERSIONS before app proceeds.
-- StateManager uses identity key from [`src/core/identity/storage.ts`](../../src/core/identity/storage.ts) for encryption — identity domain must initialize before state loads.
-- LifecycleManager coordinates connection teardown — `ConnectionManager.reset()` is called in lifecycle shutdown handlers.
-- RPC session layer ([`src/rpc/session.ts`](../../src/rpc/session.ts)) reads state via StateManager for active config lookup.
-- [`src/core/config/schema.ts`](../../src/core/config/schema.ts), [`src/core/config/resolver.ts`](../../src/core/config/resolver.ts), [`src/core/state/manager.ts`](../../src/core/state/manager.ts), and [`src/core/settings/rules.ts`](../../src/core/settings/rules.ts) all import `ConfigAccess`/`resolveLegacyAccess`/`guarded` from [`src/core/policy/`](../../src/core/policy) — the `core-policy` domain owns the role matrix and channel checks; shape changes to `ConfigAccess` propagate to all four.
+- Imports `resolveLegacyAccess`, `ConfigAccess`, `Role` from **core-policy** ([`src/core/state/access.ts`](../../src/core/state/access.ts), [`src/core/config/schema.ts`](../../src/core/config/schema.ts), [`src/core/config/resolver.ts`](../../src/core/config/resolver.ts)) to resolve and clamp per-config access roles — data resolution, not enforcement.
+- Imports `guarded` from **core-policy** (`src/core/settings/rules.ts:isConfigGuarded`) so rule matching on `match.protected` reflects actual access state rather than a stored flag.
+- [`src/core/state/manager.ts`](../../src/core/state/manager.ts) imports `KnownUser` ([`src/core/identity/types.ts`](../../src/core/identity/types.ts)) and `loadPrivateKey` ([`src/core/identity/storage.ts`](../../src/core/identity/storage.ts)) from **core-identity**; [`src/core/state/encryption/crypto.ts`](../../src/core/state/encryption/crypto.ts) imports `deriveStateKey` ([`src/core/identity/crypto.ts`](../../src/core/identity/crypto.ts)) and `isValidKeyHex` ([`src/core/identity/storage.ts`](../../src/core/identity/storage.ts)) — state encryption is keyed off the user's identity private key.
+- [`src/core/config/types.ts`](../../src/core/config/types.ts) imports `ConnectionConfig`/`Dialect` from [`src/core/connection/types.ts`](../../src/core/connection/types.ts); [`src/core/config/validate.ts`](../../src/core/config/validate.ts) imports `testConnection` from [`src/core/connection/factory.ts`](../../src/core/connection/factory.ts); [`src/core/config/schema.ts`](../../src/core/config/schema.ts) and [`src/core/settings/schema.ts`](../../src/core/settings/schema.ts) both import `PortSchema` from [`src/core/connection/defaults.ts`](../../src/core/connection/defaults.ts) — all **core-db**.
+- [`src/core/config/types.ts`](../../src/core/config/types.ts) imports `LogLevel` from [`src/core/logger/types.ts`](../../src/core/logger/types.ts) (**core-identity**).
+- [`src/core/lifecycle/manager.ts`](../../src/core/lifecycle/manager.ts) registers a default shutdown resource that calls `getConnectionManager().closeAll()` from [`src/core/connection/manager.ts`](../../src/core/connection/manager.ts) (**core-db**).
+- [`src/core/version/schema/index.ts`](../../src/core/version/schema/index.ts) calls `waitForIdentityToLoad` from [`src/core/identity/index.ts`](../../src/core/identity/index.ts) (**core-identity**) after bootstrapping or migrating tracking tables.
+- [`src/core/observer.ts`](../../src/core/observer.ts)'s `NoormEvents` interface extends `SettingsEvents` (this domain) and imports event-payload types from [`src/core/update/`](../../src/core/update), [`src/core/vault/events.ts`](../../src/core/vault/events.ts), [`src/core/transfer/events.ts`](../../src/core/transfer/events.ts), [`src/core/dt/events.ts`](../../src/core/dt/events.ts), [`src/core/logger/types.ts`](../../src/core/logger/types.ts), [`src/core/teardown/types.ts`](../../src/core/teardown/types.ts) — every domain that emits events depends on this file, and this file's type surface depends on those domains' event shapes.
## Conventions worth knowing
-- State file path: `.noorm/state/state.enc` (configurable via `StateManagerOptions`).
-- Settings file path: `.noorm/settings.yml`; `SETTINGS_FILE_PATH` constant exported from [`src/core/settings/index.ts`](../../src/core/settings/index.ts).
-- `CURRENT_VERSIONS` in [`src/core/version/index.ts`](../../src/core/version/index.ts) is the version triple that must match after migration.
-- `observer` is a module-scope singleton; `resetConnectionManager`/`resetSettingsManager`/`resetStateManager` are test-only reset points.
-- Stages in settings allow per-environment config overrides; `evaluateRules` applies them at runtime.
-- `initProjectContext` is the canonical startup sequence called by CLI entry and SDK `createContext`.
-- `Config.access` defaults to `{ user: 'admin', agent: 'viewer' }` (`DEFAULT_ACCESS`) when absent; a legacy `protected: true` maps to `{ user: 'operator', agent: 'viewer' }` (`GUARDED_ACCESS`) — both constants live in [`src/core/policy/legacy-access.ts`](../../src/core/policy/legacy-access.ts).
-- `src/core/config/protection.ts` (hard-block rules for protected configs) was deleted — access enforcement now runs entirely through `core/policy`.
+- `StateManager` ([`src/core/state/manager.ts`](../../src/core/state/manager.ts)) keeps three snapshots: `#state` (working copy), `#baseline` (state as loaded, cloned), and `#diskFingerprint` (SHA-256 of the last-read/written raw file contents). Every `#persist()` re-reads the file, compares its fingerprint to `#diskFingerprint`, and if changed, three-way merges via `mergeState` ([`src/core/state/merge.ts`](../../src/core/state/merge.ts)) using `#baseline` to distinguish "we never touched this key" from "we deleted this key".
+- `mergeState`'s per-field rules: `schemaVersion` takes `Math.max(ours, theirs)` (never steps backward), `activeConfig` keeps `ours` only if it changed from baseline, `secrets` is merged two levels deep (`mergeSecrets`) so a concurrent `secret set` on a sibling key isn't dropped, and any top-level field not in `KNOWN_FIELDS` (written by a newer build this one doesn't model) is carried through under the same equals-baseline rule.
+- `StateManager.load()` runs two independent migration systems in a fixed order: `migrateSchemaVersion` ([`src/core/version/state/index.ts`](../../src/core/version/state/index.ts), keyed on the `schemaVersion` int field) runs first on the raw untyped record, then the package-semver `migrateState` ([`src/core/state/migrations.ts`](../../src/core/state/migrations.ts), keyed on the `version` string field) runs second. Reversing the order would drop `schemaVersion` and anything else the semver migration doesn't know about, since it only knows `State`'s seven top-level fields.
+- After both migrations, `StateManager.load()` calls `repairConfigAccess` ([`src/core/state/access.ts`](../../src/core/state/access.ts)) on every config's `access` field unconditionally — the single point that backfills a missing/malformed `access`, so no downstream consumer (`setConfig`, `listConfigs`, `guarded`) needs its own fallback. It is fail-closed: an unrecognized shape can only make a config *more* restrictive (falls back to `MOST_RESTRICTIVE_ROLE = 'viewer'`), never less.
+- `src/core/state/migrations.ts:migrateState` deliberately drops the legacy `identity` field (moved to `~/.noorm/`) rather than carrying it through, so a private key once stored in `state.enc` doesn't get re-persisted forever. Everything else unknown is spread through rather than rebuilt from an allowlist.
+- `isValidSecretKey`/`InvalidSecretKeyError` ([`src/core/state/manager.ts`](../../src/core/state/manager.ts)) are the single declaration of the secret-key identifier regex (`/^[A-Za-z][A-Za-z0-9_]*$/`); `setSecret` enforces it, and TUI live-typing validators call the same predicate instead of hand-copying the pattern.
+- `writeFileAtomicSync` ([`src/core/state/persistence.ts`](../../src/core/state/persistence.ts)) stages to a sibling `...tmp` file, `fsyncSync`s it, then `renameSync`s over the target, and fsyncs the containing directory afterward (best-effort). `acquireWriteLock` uses `open(..., 'wx')` (`O_EXCL`) rather than `flock` because `flock` silently no-ops on some network filesystems.
+- `EncryptedPayload.kdf` is optional; absent means `hkdf-sha256` (the only derivation ever shipped) so old payloads keep decrypting if a future build changes the derivation. `IV_LENGTH` is 16 bytes, not the NIST-recommended 12 — a deliberate deviation noted in [`src/core/state/encryption/crypto.ts`](../../src/core/state/encryption/crypto.ts).
+- `SettingsManager` ([`src/core/settings/manager.ts`](../../src/core/settings/manager.ts)) keeps `#document` (exactly what's on/will be written to disk) separate from `#settings` (`#document` merged with a `NOORM_*` env-var overlay via `allSettingsEnv`/`makeNestedConfig`). Stage/rule mutators (`setStage`, `addRule`, etc.) read and write `#document`, never `#settings` — merging the env overlay into the persisted document would commit ambient shell values (vault tokens, DB passwords) into version-controlled `settings.yml`.
+- `allSettingsEnv()` ([`src/core/settings/manager.ts`](../../src/core/settings/manager.ts)) calls `makeNestedConfig` fresh per-call; `getEnvConfig()`'s `makeNestedConfig` call is at module scope, but `memoizeOpts: false` keeps its returned `allConfigs()` accessor reading `process.env` live on every invocation — both avoid the same env-snapshot staleness by different means, sidestepping a Bun test-runner edge case where a module-scope closure over `process.env` misses later mutations.
+- `resolveConfig` ([`src/core/config/resolver.ts`](../../src/core/config/resolver.ts)) merges five layers in ascending priority — `DEFAULTS` → stage defaults → stored config → env vars → CLI flags — via `merge`/`clone` from `@logosdx/utils`, then runs `parseConfig` and `applyStageCeiling`. `DEFAULTS` deliberately omits `access` so a merged-in stored/env/flag `access` (or legacy `protected`) is what `parseConfig`'s `resolveLegacyAccess` fallback actually sees.
+- `applyStageCeiling` ([`src/core/config/resolver.ts`](../../src/core/config/resolver.ts)) only clamps access *down*: a stage with `defaults.protected: true` caps resolved access at `{ user: 'operator', agent: 'viewer' }` (`PROTECTED_STAGE_CEILING`) via `roleRank`/`clampToCeiling`, but never loosens a config that was already stricter than the ceiling.
+- `ConfigSchema` ([`src/core/config/schema.ts`](../../src/core/config/schema.ts)) is a `.transform(withResolvedAccess)` — parsing a config always resolves `access` from either the new `access` field or the legacy `protected` boolean, and never echoes `protected` back into the output; `access` is the only stored source of truth after parse.
+- Database names are validated against `DANGEROUS_DB_NAME_CHARS` ([`src/core/config/schema.ts`](../../src/core/config/schema.ts)) — quotes, backticks, brackets, semicolons, and control characters — because `database` is interpolated into raw DDL (`CREATE`/`DROP DATABASE`) as a quoted dialect-specific identifier. SQLite is exempt since its `database` is a file path.
+- `LifecycleManager` ([`src/core/lifecycle/manager.ts`](../../src/core/lifecycle/manager.ts)) runs shutdown in a fixed phase order (`stopping` → `completing` → `releasing` → `flushing` → `exiting`), each phase running its registered `LifecycleResource`s sorted by `priority` under a per-phase timeout via `runWithTimeout`; a second `SIGINT`/`SIGTERM`/`SIGHUP` during `shutting_down` forces `process.exit(128 + signalCode)` rather than re-entering shutdown.
+- [`src/core/version/`](../../src/core/version) has three parallel, near-identical migration engines (schema in `version/schema/`, state in `version/state/`, settings in `version/settings/`), each with its own `MIGRATIONS` array, `getXVersion`, `checkXVersion`, and `migrateX` following the same `up(data)`/`down(data)` migration-interface shape from [`src/core/version/types.ts`](../../src/core/version/types.ts).
+- [`src/core/version/state/migrations/v3.ts`](../../src/core/version/state/migrations/v3.ts) renames `access.mcp` → `access.agent` (the channel key), reusing `repairConfigAccess` so an unrecognized value downgrades to `viewer` rather than being dropped — the same fail-closed rule v2 established.
+- `findProjectRoot` ([`src/core/project.ts`](../../src/core/project.ts)) walks up from cwd looking for a [`.noorm`](../../.noorm) directory, stopping at (and not treating as a project) the user's home directory — `~/.noorm/` is global identity storage, not a project. `getOriginalCwd`/`setOriginalCwd` capture the pre-chdir cwd exactly once (first call wins) so commands like `init --here` can still reach it after `initProjectContext` has already `chdir`'d.
+- `performProjectInit` ([`src/core/project-init.ts`](../../src/core/project-init.ts)) only creates/updates identity when `identityInfo` is non-null; when null, it assumes a global identity already exists in `~/.noorm/`. It keys the [`.gitignore`](../../.gitignore) append-check on the literal entry string [`.noorm/state/`](../../.noorm/state) rather than the `# noorm` header comment, because earlier versions wrote the header with nothing under it.
diff --git a/docs/wiki/index.md b/docs/wiki/index.md
index cc328616..f194ffb3 100644
--- a/docs/wiki/index.md
+++ b/docs/wiki/index.md
@@ -1,24 +1,25 @@
---
-reflects_rev: cf0d4c3b4b5d4ce5c54e12436ce3cfbefdb59191
+reflects_rev: 3acf100dd323521cae7cf853e89e6b4c079a42b2
type: Index
+description: Bun workspace monorepo — noorm, a database schema/change manager with Ink/React TUI, Citty CLI, and Kysely SQL layer
---
repo
-33101f0bf56f53ee3568f149bd7f99afd795ead2
+bf245fd116209d22ea6385945f0d255d7dc8af92
1
# Project signals
## Framework & runtime
-- **Language:** TypeScript (80% LOC, 887 files), Bun runtime (>=1.2), Node >=22.13
-- **SQL layer:** Kysely query builder + executor; dialect-aware across PostgreSQL, MySQL, MSSQL, SQLite
-- **TUI:** Ink 6 + React 19 ([`src/tui/`](../../src/tui)); Citty for CLI arg parsing ([`src/cli/`](../../src/cli))
+- **Language:** TypeScript (82% LOC, 1031 files), Bun runtime (>=1.2), Node >=22.13
+- **SQL layer:** Kysely 0.28 query builder + executor; dialect-aware across PostgreSQL, MySQL, MSSQL, SQLite
+- **TUI:** Ink 6.8 + React 19.2 ([`src/tui/`](../../src/tui)); Citty 0.2 for CLI arg parsing ([`src/cli/`](../../src/cli))
- **Event bus:** `@logosdx/observer` (`ObserverEngine`); module-scope singleton in [`src/core/observer.ts`](../../src/core/observer.ts)
-- **Templating:** Eta 4 for `.sql.tmpl` files; data loaders for JSON5/YAML/CSV/JS side-cars
+- **Templating:** Eta 4.5 for `.sql.tmpl` files; data loaders for JSON5/YAML/CSV/JS side-cars
- **Error handling:** `@logosdx/utils` `attempt`/`attemptSync` tuples — no try-catch in source
-- **Encryption:** AES-256-GCM for state ([`src/core/state/encryption/`](../../src/core/state/encryption)), Ed25519-like keypairs for identity
-- **MCP:** `@modelcontextprotocol/sdk` wrapping RPC registry over stdio
+- **Encryption:** AES-256-GCM for state ([`src/core/state/encryption/`](../../src/core/state/encryption)), X25519 ECDH keypairs for identity/vault
+- **MCP:** `@modelcontextprotocol/sdk` 1.29 wrapping RPC registry over stdio
## Build / test / lint
@@ -29,63 +30,71 @@ type: Index
| Build binary | `bun run build:binary` | [`scripts/build-binary.mjs`](../../scripts/build-binary.mjs) (bun compile) |
| Dev watch | `bun run dev` | [`package.json`](../../package.json) |
| Test (all, serial) | `bun run test` | [`package.json`](../../package.json) |
-| Test CI group 1 | `bun test --serial $(find tests/utils tests/core tests/sdk -name '*.test.ts' \| grep -v tests/core/transfer \| sort \| tr '\n' ' ')` | `.github/workflows/ci.yml:127` |
-| Test CI group 2 | `bun test --serial tests/core/transfer` | `.github/workflows/ci.yml:132` |
-| Test CI group 3 | `bun test --serial tests/cli` | `.github/workflows/ci.yml:137` |
-| Test CI group 4 | `bun test --serial tests/integration` | `.github/workflows/ci.yml:142` |
+| Test CI group 1 (core, non-transfer) | `bun test --serial $(find tests/utils tests/core tests/sdk -name '*.test.ts' \| grep -v tests/core/transfer \| sort \| tr '\n' ' ')` | `.github/workflows/ci.yml:127` |
+| Test CI group 2 (transfer, isolated) | `bun test --serial tests/core/transfer` | `.github/workflows/ci.yml:132` |
+| Test CI group 3 (CLI, non-logger-settings) | `bun test --serial $(find tests/cli \( -name '*.test.ts' -o -name '*.test.tsx' \) ! -name 'cli-logger-settings.test.ts' \| sort \| tr '\n' ' ')` | `.github/workflows/ci.yml:138` |
+| Test CI group 4 (CLI logger settings, isolated) | `bun test --serial tests/cli/cli-logger-settings.test.ts` | `.github/workflows/ci.yml:153` |
+| Test CI group 5 (integration) | `bun test --serial tests/integration` | `.github/workflows/ci.yml:158` |
| Lint | `bun run lint` | ESLint, [`eslint.config.js`](../../eslint.config.js) |
| Typecheck | `bun run typecheck` | [`tsconfig.json`](../../tsconfig.json) |
-CI gate: lint → typecheck → build → 4 test groups → 3 example jobs. Integration tests require live DB services (docker-compose or CI service containers).
+CI gate: lint → typecheck → build → 5 test groups → 3 example jobs. Integration tests require live DB services (docker-compose or CI service containers). [`tests/core/transfer`](../../tests/core/transfer) and [`tests/cli/cli-logger-settings.test.ts`](../../tests/cli/cli-logger-settings.test.ts) are isolated into their own serial groups because Bun's `mock.module` registry is process-global and never actually restores — an `afterAll` cleanup that re-registers the real module is a no-op, so any file that mocks a module poisons every file loaded after it for the life of the process. Two init-screen tests replace the `SettingsManager` class; `getSettingsManager` then constructs a mock instance, so `createCliLogger` reads `settings: {}` instead of `settings.yml`. Which file wins depends on load order (root files before subdirectories on macOS, the reverse on Linux), so a single-process run passes locally and fails only on CI.
## Language breakdown
| Language | LOC | Files | % |
|----------|-----|-------|---|
-| TypeScript | 204655 | 887 | 80% |
-| Markdown | 43261 | 198 | 17% |
-| YAML | 1114 | 16 | 1% |
-| JavaScript | 1005 | 22 | 1% |
-| HTML | 955 | 26 | 2% |
-| CSS | 913 | 3 | <1% |
-| Shell | 726 | 4 | <1% |
+| TypeScript | 240775 | 1031 | 82% |
+| Markdown | 44873 | 136 | 15% |
+| JavaScript | 1261 | 22 | 1% |
+| YAML | 1158 | 16 | 1% |
+| HTML | 1090 | 27 | 1% |
+| CSS | 1015 | 3 | 1% |
+| Shell | 930 | 7 | 1% |
+| JSON | 471 | 22 | 1% |
+| Vue | 203 | 3 | 1% |
+| TOML | 10 | 2 | 1% |
## DevOps & CI
-- **CI:** GitHub Actions (`ubuntu-24.04`), Bun 1.3.11 pinned; 4 test groups + 3 example jobs per push to master/main
+- **CI:** GitHub Actions (`ubuntu-24.04`), Bun 1.3.11 pinned; 5 test groups + 3 example jobs per push to master/main
- **DB services (CI):** Postgres 17 on 15432, MySQL 8.0 on 13306, MSSQL 2022 on 11433
-- **DB services (local):** [`docker-compose.test.yml`](../../docker-compose.test.yml) at repo root (same ports)
-- **Publish:** Changesets-driven (`changeset publish`) via [`.github/workflows/publish.yml`](../../.github/workflows/publish.yml); packages: `@noormdev/cli` and `@noormdev/sdk`
+- **DB services (local):** [`docker-compose.test.yml`](../../docker-compose.test.yml) at repo root (same ports); postgres/mysql services are `tmpfs`-backed, mssql is not
+- **Publish:** Changesets-driven (`changeset publish`) via [`.github/workflows/publish.yml`](../../.github/workflows/publish.yml); fixed-version group: `@noormdev/cli` and `@noormdev/sdk`
- **Binary release:** `bun build --compile` → GitHub Releases via [`.github/workflows/release-binary.yml`](../../.github/workflows/release-binary.yml)
-- **Docs:** VitePress, deployed via [`.github/workflows/docs.yml`](../../.github/workflows/docs.yml)
+- **Docs:** VitePress, deployed via [`.github/workflows/docs.yml`](../../.github/workflows/docs.yml); site now includes [`docs/tapes/`](../tapes) (VHS-recorded terminal demos, replacing static screenshots)
## Domains
| Domain | Repo paths | One-liner | Detail |
|--------|------------|-----------|--------|
-| core-change | [`src/core/change/`](../../src/core/change), [`src/cli/change/`](../../src/cli/change), [`tests/core/change/`](../../tests/core/change) | Versioned DB changes: scaffold, parse, execute, history | [`docs/wiki/core-change.md`](core-change.md) |
+| core-change | [`src/core/change/`](../../src/core/change), [`src/cli/change/`](../../src/cli/change), [`tests/core/change/`](../../tests/core/change) | Versioned DB changes: scaffold, parse, execute, revert, history (timestamps hydrated as UTC on pg/mysql) | [`docs/wiki/core-change.md`](core-change.md) |
| core-runner | [`src/core/runner/`](../../src/core/runner), [`src/core/template/`](../../src/core/template), [`src/cli/run/`](../../src/cli/run), [`tests/core/runner/`](../../tests/core/runner), [`tests/core/template/`](../../tests/core/template) | SQL file execution with checksum dedup and Eta templating | [`docs/wiki/core-runner.md`](core-runner.md) |
| core-db | [`src/core/db/`](../../src/core/db), [`src/core/connection/`](../../src/core/connection), [`src/core/explore/`](../../src/core/explore), [`src/core/teardown/`](../../src/core/teardown), [`src/core/transfer/`](../../src/core/transfer), [`src/cli/db/`](../../src/cli/db), [`tests/core/connection/`](../../tests/core/connection), [`tests/core/explore/`](../../tests/core/explore), [`tests/core/teardown/`](../../tests/core/teardown), [`tests/core/transfer/`](../../tests/core/transfer), [`tests/integration/`](../../tests/integration) | DB lifecycle: create/drop, explore schema, teardown, cross-DB transfer | [`docs/wiki/core-db.md`](core-db.md) |
| core-state | [`src/core/state/`](../../src/core/state), [`src/core/settings/`](../../src/core/settings), [`src/core/config/`](../../src/core/config), [`src/core/lifecycle/`](../../src/core/lifecycle), [`src/core/version/`](../../src/core/version), [`src/core/project.ts`](../../src/core/project.ts), [`src/core/project-init.ts`](../../src/core/project-init.ts), [`src/core/environment.ts`](../../src/core/environment.ts), [`src/core/observer.ts`](../../src/core/observer.ts), [`tests/core/state/`](../../tests/core/state), [`tests/core/settings/`](../../tests/core/settings), [`tests/core/config/`](../../tests/core/config), [`tests/core/lifecycle/`](../../tests/core/lifecycle), [`tests/core/version/`](../../tests/core/version) | Encrypted state, settings.yml, config resolution, lifecycle, version migration | [`docs/wiki/core-state.md`](core-state.md) |
| core-identity | [`src/core/identity/`](../../src/core/identity), [`src/core/vault/`](../../src/core/vault), [`src/core/logger/`](../../src/core/logger), [`src/core/sql-terminal/`](../../src/core/sql-terminal), [`src/cli/identity/`](../../src/cli/identity), [`src/cli/secret/`](../../src/cli/secret), [`src/cli/vault/`](../../src/cli/vault), [`src/cli/sql/`](../../src/cli/sql), [`tests/core/identity/`](../../tests/core/identity), [`tests/core/vault/`](../../tests/core/vault), [`tests/core/logger/`](../../tests/core/logger), [`tests/core/sql-terminal/`](../../tests/core/sql-terminal) | Identity keypairs, vault secrets, structured logger, SQL terminal history | [`docs/wiki/core-identity.md`](core-identity.md) |
| core-policy | [`src/core/policy/`](../../src/core/policy), [`tests/core/policy/`](../../tests/core/policy) | Access-control policy: role×permission matrix, SQL statement classifier, legacy `protected`→`access` migration | [`docs/wiki/core-policy.md`](core-policy.md) |
| sdk | [`src/sdk/`](../../src/sdk), [`src/core/dt/`](../../src/core/dt), [`packages/sdk/`](../../packages/sdk), [`tests/sdk/`](../../tests/sdk), [`tests/integration/sdk/`](../../tests/integration/sdk) | Programmatic API (`createContext`) + DT binary serialization format | [`docs/wiki/sdk.md`](sdk.md) |
-| cli | [`src/cli/`](../../src/cli), [`packages/cli/`](../../packages/cli), [`skills/noorm/`](../../skills/noorm), [`tests/cli/`](../../tests/cli) | Citty CLI with 17 command groups, headless mode, binary distribution | [`docs/wiki/cli.md`](cli.md) |
-| tui | [`src/tui/`](../../src/tui), [`.claude/rules/tui-development.md`](../../.claude/rules/tui-development.md), [`tests/cli/components/`](../../tests/cli/components), [`tests/cli/hooks/`](../../tests/cli/hooks), [`tests/cli/screens/`](../../tests/cli/screens) | Ink/React TUI with focus manager, keyboard routing, per-domain screens | [`docs/wiki/tui.md`](tui.md) |
+| cli | [`src/cli/`](../../src/cli), [`packages/cli/`](../../packages/cli), [`skills/noorm/`](../../skills/noorm), [`tests/cli/`](../../tests/cli) | Citty CLI with 12 domain-owning command groups + 6 leaf commands, headless mode, binary distribution | [`docs/wiki/cli.md`](cli.md) |
+| tui | [`src/tui/`](../../src/tui), [`.claude/rules/tui-development.md`](../../.claude/rules/tui-development.md), [`tests/cli/components/`](../../tests/cli/components), [`tests/cli/hooks/`](../../tests/cli/hooks), [`tests/cli/screens/`](../../tests/cli/screens) | Ink/React TUI with focus manager, keyboard routing, ~94 registered screens | [`docs/wiki/tui.md`](tui.md) |
| mcp-rpc | [`src/mcp/`](../../src/mcp), [`src/rpc/`](../../src/rpc), [`src/cli/mcp/`](../../src/cli/mcp), [`tests/core/mcp/`](../../tests/core/mcp), [`tests/core/rpc/`](../../tests/core/rpc) | MCP server over stdio wrapping flat RPC command registry, permission-gated dispatch | [`docs/wiki/mcp-rpc.md`](mcp-rpc.md) |
| worker-bridge | [`src/core/worker-bridge/`](../../src/core/worker-bridge), [`src/workers/`](../../src/workers), [`tests/core/worker-bridge/`](../../tests/core/worker-bridge), [`tests/workers/`](../../tests/workers) | Hub-and-spoke worker threads for DT serialization and DB connection worker | [`docs/wiki/worker-bridge.md`](worker-bridge.md) |
-| infra | [`.github/`](../../.github), [`scripts/`](../../scripts), [`examples/`](../../examples), [`docs/`](..), `tsup.*.config.ts`, [`docker-compose.test.yml`](../../docker-compose.test.yml), [`bunfig.toml`](../../bunfig.toml) | CI, build pipeline, binary release, example projects, VitePress docs | [`docs/wiki/infra.md`](infra.md) |
+| infra | [`.github/`](../../.github), [`scripts/`](../../scripts), [`examples/`](../../examples), [`docs/`](..), `tsup.*.config.ts`, [`docker-compose.test.yml`](../../docker-compose.test.yml), [`bunfig.toml`](../../bunfig.toml) | CI, build pipeline, binary release, example projects, VitePress docs (incl. [`docs/tapes/`](../tapes) VHS demo recordings) | [`docs/wiki/infra.md`](infra.md) |
## Cross-cutting
**Test layout:** Tests mirror [`src/`](../../src) under [`tests/`](../../tests). [`tests/utils/`](../../tests/utils) holds shared DB helpers. [`tests/fixtures/`](../../tests/fixtures) has SQL fixtures per dialect. [`tests/integration/`](../../tests/integration) requires live databases. [`tests/global-setup.ts`](../../tests/global-setup.ts) / [`tests/global-teardown.ts`](../../tests/global-teardown.ts) coordinate integration DB bootstrap.
-**Known contamination:** `src/core/config/index.ts:34` calls `makeNestedConfig(process.env, …)` at module scope — snaps env at first import. This causes test cross-contamination when running the full suite in one process. Workaround: run test groups in separate `bun test --serial` invocations (same as CI).
+**Test isolation (updated 2026-08):** the previously-documented contamination source — `src/core/config/index.ts:34` calling `makeNestedConfig(process.env, …)` at module scope — does not reproduce; the call passes `memoizeOpts: false`, so lookups re-read `process.env` live rather than snapshotting at import (see [`docs/wiki/core-state.md`](core-state.md)'s Conventions section for the mechanism). The real isolation driver, confirmed in root [`CLAUDE.md`](../../CLAUDE.md), is that Bun's `mock.module` registry is process-global and never restores — see the Build/test/lint section above for the CI-group rationale.
**Convention pointers:** [`.claude/rules/typescript.md`](../../.claude/rules/typescript.md) (4-block function structure, `attempt` over try-catch), [`.claude/rules/tui-development.md`](../../.claude/rules/tui-development.md) (focus system, Ink layout), [`.claude/rules/testing.md`](../../.claude/rules/testing.md) (test naming, coverage), [`.claude/rules/documentation.md`](../../.claude/rules/documentation.md) (three-pillar structure).
-**Domain partitioning basis:** Domains are functional vertical slices. `core-state` groups the startup/persistence concerns (state, settings, config, lifecycle, version) because they all initialize together in `project-init.ts`. `core-identity` groups crypto identity, vault, logger, and SQL terminal because they share the "user-facing sensitive data" concern. `core-db` groups connection, explore, transfer, and teardown because they all operate against a live database connection. `core-change` and `core-runner` are separate because changes are versioned operations while runner handles idempotent file execution — they share `runFile` but have distinct lifecycles. `core-policy` is a new cross-cutting domain ([`src/core/policy/`](../../src/core/policy)): domains that enforce a config-scoped action via `assertPolicy`/`checkConfigPolicy` (`core-change`, `core-runner`, `core-db`, `core-identity`, `sdk`, `cli`, `tui`, `mcp-rpc`) import from it directly, and `core-state` imports it too but only for `resolveLegacyAccess`/`guarded` — data resolution and display styling, not enforcement. The role×permission matrix and SQL classifier live nowhere else, so `core-policy` gets its own vertical slice rather than being folded into `core-state`.
+**Domain partitioning basis:** Domains are functional vertical slices. `core-state` groups the startup/persistence concerns (state, settings, config, lifecycle, version) because they all initialize together in `project-init.ts`. `core-identity` groups crypto identity, vault, logger, and SQL terminal because they share the "user-facing sensitive data" concern. `core-db` groups connection, explore, transfer, and teardown because they all operate against a live database connection. `core-change` and `core-runner` are separate because changes are versioned operations while runner handles idempotent file execution — they do not share an execution path (core-change implements its own `executeFiles`/`needsRun`, distinct from the core runner's same-named functions). `core-policy` is a cross-cutting domain ([`src/core/policy/`](../../src/core/policy)): domains that enforce a config-scoped action via `assertPolicy`/`checkConfigPolicy` (`core-change`, `core-runner`, `core-db`, `core-identity`, `sdk`, `cli`, `tui`, `mcp-rpc`) import from it directly, and `core-state` imports it too but only for `resolveLegacyAccess`/`guarded` — data resolution and display styling, not enforcement. `infra` absorbs the entire [`docs/`](..) tree (including the new [`docs/tapes/`](../tapes) VHS demo-recording sources and [`docs/guide/relational-design.md`](../guide/relational-design.md)) rather than splitting docs out per-domain, since the docs site is built/deployed as one VitePress unit.
-**Access-control policy (2026-07, config-access-roles feature):** `Config.protected: boolean` was replaced by `Config.access: ConfigAccess` (per-channel `user`/`agent` roles), enforced through the new `core-policy` domain. `src/core/config/protection.ts` and `src/rpc/protection.ts` were both deleted — their rule-checking is absorbed into `core/policy`. The runner/change/transfer/sql-terminal executors gate at their core seam via `assertPolicy`, so SDK/CLI/TUI/MCP callers all inherit one enforcement path. `StateManager.load()` now also runs the schemaVersion-keyed migration (`core/version/state/`, v2 maps `protected`→`access`) ahead of the pre-existing package-semver migration — previously only the semver path ran here.
+**Access-control policy (2026-07, config-access-roles feature):** `Config.protected: boolean` was replaced by `Config.access: ConfigAccess` (per-channel `user`/`agent` roles), enforced through the `core-policy` domain. `src/core/config/protection.ts` and `src/rpc/protection.ts` were both deleted — their rule-checking is absorbed into `core/policy`. The runner/change/transfer/sql-terminal executors gate at their core seam via `assertPolicy`, so SDK/CLI/TUI/MCP callers all inherit one enforcement path. `StateManager.load()` runs the schemaVersion-keyed migration (`core/version/state/`, v2 maps `protected`→`access`, v3 maps `access.mcp`→`access.agent`) ahead of the pre-existing package-semver migration.
-**Deterministic substrate:** `.claude/project/deterministic-signals.md` (generated 2026-06-01T02:30:22Z, atomic 3.0.0)
+**Recent change (2026-08, local):** [`src/core/change/history.ts`](../../src/core/change/history.ts) now hydrates `executed_at` as UTC on pg/mysql — `pg`/`mysql2` were parsing noorm's naive UTC text through the host's local timezone, so a change applied moments ago could render as "in 4 hours" on a UTC-negative host. mssql is deliberately left alone (tedious was not measured). See [`docs/wiki/core-change.md`](core-change.md)'s Conventions section.
+
+**Unresolved review findings (informational):** `core-change.md` and `core-identity.md` each required a targeted post-review correction beyond the standard reviewer loop (a dead-code/out-of-domain doc claim in core-change's Docs section, and a gzip/permission-model misattribution to vault in core-identity's Conventions section) — both were fixed directly rather than re-dispatching a 4th sub-agent iteration; content is now accurate as of this refresh.
+
+**Deterministic substrate:** [`docs/wiki/scan.md`](scan.md) (regenerated this refresh via `atomic signals scan`)
diff --git a/docs/wiki/infra.md b/docs/wiki/infra.md
index 067e2351..3c4d987a 100644
--- a/docs/wiki/infra.md
+++ b/docs/wiki/infra.md
@@ -1,57 +1,69 @@
---
type: Domain
+description: Build pipeline, CI, npm/binary release, example fixtures, and the VitePress docs site.
---
# infra
## What it does
-Build pipeline, CI, binary release, package publishing, and reference examples. The monorepo root orchestrates two publishable packages (`@noormdev/cli`, `@noormdev/sdk`) and three examples. CI runs four isolated test groups. Binary release produces a standalone `noorm` executable via `bun build --compile`.
+Builds and publishes the two workspace packages (`@noormdev/cli`, `@noormdev/sdk`) via tsup and Changesets, produces standalone binaries via `bun build --compile`, runs GitHub Actions CI across three dialects plus example-project smoke tests, and builds/deploys the VitePress docs site at noorm.dev. Also holds the three example projects and the VHS tape sources that record the docs site's terminal GIFs/screenshots.
## Artifacts
-- [`examples/llm-memory-db-pg/`](../../examples/llm-memory-db-pg) — PostgreSQL LLM memory DB example with SDK, CLI, and MCP coverage
-- [`examples/llm-memory-db-mssql/`](../../examples/llm-memory-db-mssql) — MSSQL equivalent with TVP patterns
-- [`examples/todo-db/`](../../examples/todo-db) — reference CI target: soft-deletes, JSONB, TVFs, transactional SPs; used as CI stress test
+- [`examples/todo-db/`](../../examples/todo-db) — Postgres reference project (soft-deletes, JSONB, TVFs, transactional SPs); depends on `@noormdev/sdk`, `kysely`, `pg`; driven end-to-end by the `example-todo-db` CI job
+- [`examples/llm-memory-db-mssql/`](../../examples/llm-memory-db-mssql) — MSSQL example exercising table-valued parameters and schema-bound validator UDFs; depends on `@noormdev/sdk`, `kysely`, `tedious`, `tarn`, `zod`; driven end-to-end by the `example-llm-memory-db-mssql` CI job
+- [`examples/llm-memory-db-pg/`](../../examples/llm-memory-db-pg) — Postgres LLM-memory-DB example; depends on `@noormdev/sdk`, `kysely`, `pg`, `zod`; has no corresponding CI job in [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml)
+- [`docs/tapes/demo-project/`](../tapes/demo-project) — throwaway 4-file schema (`app_user`, `project`, `task`, an `open_task` view, two changes) used only to record the docs site's GIFs/screenshots, not by CI or by the other examples
## CLI code
-- [`scripts/build.mjs`](../../scripts/build.mjs) — builds both `@noormdev/cli` and `@noormdev/sdk` packages via tsup
-- [`scripts/build-binary.mjs`](../../scripts/build-binary.mjs) — `bun build --compile` to produce standalone binary
-- [`scripts/Dockerfile`](../../scripts/Dockerfile) — Docker image for binary builds
-- [`scripts/ralph-wiggum.sh`](../../scripts/ralph-wiggum.sh) — release automation helper
-- [`tsup.cli.config.ts`](../../tsup.cli.config.ts) — tsup config for CLI package build
-- [`tsup.sdk.config.ts`](../../tsup.sdk.config.ts) — tsup config for SDK package build
-- [`tsconfig.json`](../../tsconfig.json) — root TypeScript config
-- [`tsconfig.sdk-types.json`](../../tsconfig.sdk-types.json) — SDK type extraction config
-- [`tsconfig.test.json`](../../tsconfig.test.json) — test TypeScript config
-- [`bunfig.toml`](../../bunfig.toml) — Bun runtime config
-- [`docker-compose.test.yml`](../../docker-compose.test.yml) — local dev databases: PostgreSQL (15432), MySQL (13306), MSSQL (11433)
+- [`scripts/build.mjs`](../../scripts/build.mjs) — zx script; runs tsup against [`tsup.cli.config.ts`](../../tsup.cli.config.ts) then [`tsup.sdk.config.ts`](../../tsup.sdk.config.ts), prepends a `#!/usr/bin/env node` shebang to the CLI bundle, then generates `packages/sdk/dist/index.d.ts` via `dts-bundle-generator`
+- [`scripts/build-binary.mjs`](../../scripts/build-binary.mjs) — `bun build --compile` for 5 targets (darwin-arm64/x64, linux-x64/arm64, windows-x64) into `packages/cli/bin/noorm-`; each build embeds [`src/cli/index.ts`](../../src/cli/index.ts) plus both worker entry points and injects `__CLI_VERSION__`
+- [`scripts/check-flag-placement.sh`](../../scripts/check-flag-placement.sh) — the `lint:docs` script; greps [`README.md`](../../README.md), [`docs`](..), [`skills`](../../skills), [`examples`](../../examples) for the broken "flag before the subcommand" form, exempting a fixed list of files that intentionally show it as a documented contrast
+- [`scripts/Dockerfile`](../../scripts/Dockerfile) / [`scripts/ralph-wiggum.sh`](../../scripts/ralph-wiggum.sh) — sandboxed Docker image (Node 24 + Claude Code) and a loop-until-`DONE ` runner script; not referenced by any CI, build, or release workflow
+- [`tsup.cli.config.ts`](../../tsup.cli.config.ts) — bundles [`src/cli/index.ts`](../../src/cli/index.ts) to `packages/cli/dist`, ESM, `node22` target, `noExternal: [/.*/]` except `better-sqlite3`, `bun:sqlite`, `pg-native`, `react-devtools-core`
+- [`tsup.sdk.config.ts`](../../tsup.sdk.config.ts) — bundles [`src/sdk/index.ts`](../../src/sdk/index.ts) to `packages/sdk/dist`, sourcemaps on, treeshake on, externalizes `kysely` and the DB drivers (`pg`, `mysql2`, `tedious`, `tarn`, `better-sqlite3`, `bun:sqlite`) as peer deps, aliases `ansis` to a stub (SDK doesn't need terminal colors)
+- [`docker-compose.test.yml`](../../docker-compose.test.yml) — local test DB services on non-default ports: Postgres 17 (15432), MySQL 8.0 (13306), MSSQL 2022 (11433); postgres and mysql are `tmpfs`-backed, mssql is not
+- [`bunfig.toml`](../../bunfig.toml) — `bun test` config: `preload = ["./tests/preload.ts"]`, 30s timeout, `root = "./tests"`, `concurrency = 1`
## Docs
-- [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) — CI: lint → typecheck → build → 4 test groups → 3 example jobs (445L)
-- [`.github/workflows/publish.yml`](../../.github/workflows/publish.yml) — changesets-driven publish to npm
-- [`.github/workflows/release-binary.yml`](../../.github/workflows/release-binary.yml) — binary release to GitHub Releases
-- [`.github/workflows/docs.yml`](../../.github/workflows/docs.yml) — VitePress docs deployment
-- [`docs/getting-started/installation.md`](../getting-started/installation.md) — install instructions
-- [`docs/.vitepress/config.mts`](../.vitepress/config.mts) — VitePress site config (192L)
+- [`docs/index.md`](../index.md) — VitePress home page: hero, feature grid (links to `/guide/relational-design`, `/reference/sdk`, `/guide/database/transfer`, `/guide/automation/mcp`, `/headless`), quick-start snippet
+- [`docs/.vitepress/config.mts`](../.vitepress/config.mts) — site config; `srcExclude: ['wiki/**', 'spec/**', 'design/**', 'superpowers/**', 'tmp/**', 'tapes/**']` because those directories' prose (e.g. [`docs/wiki`](.)'s `` blocks) breaks VitePress's Vue-SFC markdown compiler; defines nav/sidebar, OG/Twitter meta, and a Google Analytics tag
+- [`docs/.vitepress/theme/`](../.vitepress/theme) — theme extending VitePress `DefaultTheme` with `HeroEyebrow.vue`, `HeroTerminal.vue`, `HeroStats.vue`, and `brand.css`
+- [`docs/dev/`](../dev) — ~24 contributor-facing architecture pages, one per core module/feature, indexed from [`docs/dev/index.md`](../dev/index.md); served under a separate `/dev/` sidebar
+- `docs/guide/{sql-files,environments,changes,database,automation}/` — user-guide pages, several per subtopic
+- [`docs/guide/relational-design.md`](../guide/relational-design.md) — guide page arguing for inherited compound keys and basetype-subtype tables over ORM-style surrogate IDs and polymorphic associations, using a `user → todo → todo_item` compound-key example; linked from the homepage feature grid and the main sidebar's Features group
+- [`docs/cli/`](../cli) — 10 CLI reference pages (`flags`, `help`, `identity`, `init`, `run`, `secret`, `settings-edit`, `settings-secret`, `sql-repl`, `sql`)
+- [`docs/getting-started/`](../getting-started) — `installation`, `first-build`, `concepts`, `building-your-sdk`
+- [`docs/reference/sdk.md`](../reference/sdk.md) — SDK API reference (`createContext`, `ctx.kysely`, `ctx.noorm` namespace)
+- [`docs/spec/`](../spec), [`docs/design/`](../design) — checkpoint specs and design docs for specific features (e.g. `config-access-roles.md`, `v1-49-54-cli-field-defects.md`); excluded from the built site
+- [`docs/tapes/`](../tapes) — VHS tape sources (`01-install.tape`, `02-build-and-change.tape`, `03-tui.tape`, `04-screenshots.tape`, `theme.tape`) plus shell helpers `env-scrub.sh`, `sandbox.sh`, `shots.sh` and a [`README.md`](../../README.md); renders the GIFs/PNGs under [`docs/public/image/`](../public/image); excluded from the built site via `srcExclude`
+- [`docs/public/install.sh`](../public/install.sh) — the installer served at `https://noorm.dev/install.sh`
+- [`docs/wiki/`](.) — this wiki; excluded from the built VitePress site for the reason noted in `config.mts`
## Coupling
-- Binary build (`build-binary.mjs`) must list all worker entry points explicitly — worker-bridge domain path conventions must be stable.
-- CI test split (4 groups) is a workaround for `mock.module` cross-contamination + runner image regression — see CLAUDE.md for the known contamination source.
-- Examples use `@noormdev/sdk` and CLI — they serve as integration smoke tests in CI.
-- Changeset config ([`.changeset/config.json`](../../.changeset/config.json)) references `@noormdev/cli` and `@noormdev/sdk` — only these two are publishable.
-- [`packages/cli/package.json`](../../packages/cli/package.json) and [`packages/sdk/package.json`](../../packages/sdk/package.json) carry the published versions and peer deps.
+- CI ([`.github/workflows/ci.yml`](../../.github/workflows/ci.yml)) triggers only on `src/**`, `tests/**`, `packages/**`, `examples/**`, [`bun.lockb`](../../bun.lockb), `tsconfig*.json`, and the workflow file itself — a [`docs/`](..)-only change never runs `ci.yml`
+- [`.github/workflows/docs.yml`](../../.github/workflows/docs.yml) triggers only on `docs/**` (push to `master` only, no `pull_request`); its own "Check `--json` doc placement" step still runs `bun run lint:docs` ([`scripts/check-flag-placement.sh`](../../scripts/check-flag-placement.sh)), so a docs-only change can fail that check even though `ci.yml` never runs
+- `docs.yml` deploys `docs/.vitepress/dist` to the `gh-pages` branch and writes a `noorm.dev` [`CNAME`](../../CNAME) file into it at deploy time (separate from the root-level [`CNAME`](../../CNAME) file, which is `noorm.dev` as well)
+- [`.github/workflows/publish.yml`](../../.github/workflows/publish.yml) triggers on `.changeset/**` and `packages/*/package.json`; a successful `@noormdev/cli` publish additionally triggers its `build-binaries` job, which runs the same `bun run build:binary` as [`.github/workflows/release-binary.yml`](../../.github/workflows/release-binary.yml) (that workflow is otherwise manual-only, `workflow_dispatch`)
+- [`scripts/build-binary.mjs`](../../scripts/build-binary.mjs) must list every worker entry point explicitly ([`src/workers/connection.ts`](../../src/workers/connection.ts), [`src/workers/compute.ts`](../../src/workers/compute.ts)) — adding a worker in the worker-bridge domain requires updating this list too
+- [`examples/todo-db`](../../examples/todo-db) and [`examples/llm-memory-db-mssql`](../../examples/llm-memory-db-mssql) depend on `@noormdev/sdk` and the CLI bundle built by `bun run build:packages` — they act as CI integration/smoke tests for the sdk and cli domains, not just as documentation fixtures
+- [`.changeset/config.json`](../../.changeset/config.json) (triggers `publish.yml`) fixes `@noormdev/cli` and `@noormdev/sdk` in one version group — [`packages/cli/package.json`](../../packages/cli/package.json) and [`packages/sdk/package.json`](../../packages/sdk/package.json) always bump together
+- root [`package.json`](../../package.json)'s `workspaces` field (`packages/*`, `examples/*`) is what makes the three [`examples/`](../../examples) directories resolve `@noormdev/sdk` as a workspace link rather than a published version
+- [`docs/tapes/`](../tapes) tapes run against `dist/cli/index.js` (the `bun run build`/tsc output, not the tsup bundle) and a live Postgres from [`docker-compose.test.yml`](../../docker-compose.test.yml) — recording requires both the core build and the docker-compose services to be current
## Conventions worth knowing
-- CI services: Postgres 17 on port 15432, MySQL 8.0 on port 13306, MSSQL 2022 on port 11433.
-- CI runs on `ubuntu-24.04`.
-- Test split: (1) utils+core(no transfer)+sdk, (2) core/transfer, (3) cli, (4) integration. All use `--serial`.
-- Integration tests need live DB services — not runnable locally without `docker-compose up`.
-- Examples run as separate CI jobs (`example-todo-db`, `example-llm-memory-db-pg`, `example-llm-memory-db-mssql`).
-- `NOORM_TEST_PREBUILT=1` tells example test harness to skip local bootstrap and use CLI-generated DB state.
-- Bun pinned to `1.3.11` in CI (see [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml)); local dev uses `>=1.2`.
-- `@noormdev/main` (root [`package.json`](../../package.json)) is private and not published.
+- Bun is pinned to `1.3.11` identically across `ci.yml`, `publish.yml`, and `release-binary.yml`'s `oven-sh/setup-bun` steps
+- `ci.yml`'s `build` job runs `bun run test` as 5 separate `bun test --serial` invocations, not one unified run: (1) [`tests/utils`](../../tests/utils)+[`tests/core`](../../tests/core) (excl. [`tests/core/transfer`](../../tests/core/transfer))+[`tests/sdk`](../../tests/sdk), (2) [`tests/core/transfer`](../../tests/core/transfer) alone, (3) [`tests/cli`](../../tests/cli) (excl. `cli-logger-settings.test.ts`), (4) [`tests/cli/cli-logger-settings.test.ts`](../../tests/cli/cli-logger-settings.test.ts) alone, (5) [`tests/integration`](../../tests/integration). Groups (1)/(2) split per a comment citing a GitHub Actions runner-image regression (`ubuntu24/20260406.80`) causing PG connection-state corruption when run together; groups (3)/(4) split because Bun's `mock.module` registry is process-global and never restores
+- CI jobs besides `build`: `example-todo-db`, `example-llm-memory-db-mssql`, and `cli-e2e` (sqlite-only fixture asserting headless exit codes: 0 for a clean build, 3 for a partial build with one parse-error file — exit code 2 is reserved for usage errors)
+- `example-*` CI jobs mint an ephemeral identity via `noorm ci identity new --json`, mask the private key with `::add-mask::`, then bootstrap state via `noorm ci init --force --json` — no committed identity or `state.enc` is involved
+- `NOORM_TEST_PREBUILT=1` tells an example's SDK test harness to skip its own local bootstrap and connect directly against the database state the preceding CLI steps already produced
+- the `example-llm-memory-db-mssql` job runs `noorm run build` instead of `noorm db reset`, because schema-bound validator UDFs in that project lock the tables they reference and block teardown (documented in [`examples/llm-memory-db-mssql/mssql-problems.md`](../../examples/llm-memory-db-mssql/mssql-problems.md))
+- [`docs/.vitepress/config.mts`](../.vitepress/config.mts) sets `markdown.image.lazyLoading: true` because the recorded GIFs (`tui.gif` alone is ~1.6 MB) are the heaviest assets on the site and none sit above the fold
+- [`docs/tapes/theme.tape`](../tapes/theme.tape) defines the shared VHS look (Geist Mono font, brand palette, `PlaybackSpeed 2`) and is `Source`d by every other tape; [`docs/tapes/env-scrub.sh`](../tapes/env-scrub.sh) strips coding-agent env vars (`CLAUDE*`, `AI_AGENT`, `CURSOR*`, `AIDER*`, `COPILOT*`, `WARP*`, `TERM_PROGRAM*`) before recording so `noorm info` doesn't leak the recording operator's tooling into a published GIF
+- [`docs/tapes/sandbox.sh`](../tapes/sandbox.sh) builds an isolated project under `/tmp/noorm-demo` (redirected `HOME`, deliberately short path) and always drops the `noorm_demo` Postgres database before each tape run, but only recreates it when `MODE != "project"` — in `project` mode it's left absent so the TUI walkthrough creates it on camera; it refuses to `rm -rf` any `DEMO_ROOT` not under `/tmp` or `$TMPDIR`
+- [`docs/tapes/shots.sh`](../tapes/shots.sh) renders `04-screenshots.tape` at one tall canvas (sized for the tallest TUI screen) then crops each PNG back to its own content with ImageMagick before writing it to [`docs/public/image/tui/`](../public/image/tui)
diff --git a/docs/wiki/mcp-rpc.md b/docs/wiki/mcp-rpc.md
index cbeb479f..073d410e 100644
--- a/docs/wiki/mcp-rpc.md
+++ b/docs/wiki/mcp-rpc.md
@@ -1,32 +1,35 @@
---
type: Domain
+description: MCP server over stdio wrapping a flat RPC command registry, permission-gated dispatch
---
# mcp-rpc
## What it does
-MCP (Model Context Protocol) server that exposes noorm operations to AI agents. The MCP server wraps an RPC registry — commands are registered by name, then dispatched by the MCP `run_noorm_cmd` tool. A second tool `noorm_help` lists available commands. Session management tracks per-config connection state across MCP calls.
+MCP (Model Context Protocol) server that exposes noorm operations to AI agents. The MCP server wraps an RPC registry — commands are registered by name, then dispatched by the MCP `run_noorm_cmd` tool. A second tool `noorm_help` lists available commands and generates parameter docs from each command's Zod schema. [`src/rpc/session.ts`](../../src/rpc/session.ts)'s `SessionManager` tracks per-config connection state (a `Map`) across MCP calls and carries the session's `channel` (`user`/`agent`).
-Every `RpcCommand` declares a `permission: Permission | 'open'` (`core/policy` permissions, or `'open'` for commands that target no config). Dispatch in [`src/mcp/server.ts`](../../src/mcp/server.ts) checks non-`'open'` commands against the resolved session's config via `checkConfigPolicy` before the handler runs.
+Every `RpcCommand` declares a `permission: Permission | 'open'` (`core/policy` permissions, or `'open'` for commands that target no config and skip the gate). Dispatch in [`src/mcp/server.ts`](../../src/mcp/server.ts) checks non-`'open'` commands against the resolved session's config via `checkConfigPolicy` before the handler runs.
## CLI code
-- [`src/mcp/server.ts`](../../src/mcp/server.ts) — `createMcpServer`; builds `McpServer` with `run_noorm_cmd` and `noorm_help` tools. Dispatch gates every non-`'open'` command via `checkConfigPolicy` (`core/policy`) against the resolved session context before the handler runs
-- [`src/mcp/init.ts`](../../src/mcp/init.ts) — `initMcpServer`; initializes RPC registry, registers all commands, wires session
-- [`src/mcp/index.ts`](../../src/mcp/index.ts) — barrel export
-- [`src/rpc/registry.ts`](../../src/rpc/registry.ts) — `RpcRegistry`; flat `Map` with register/get/list
-- [`src/rpc/session.ts`](../../src/rpc/session.ts) — `SessionManager`; tracks active Kysely connections per config name. Carries the session's `channel` (`user`/`agent`, default `'user'`) and enforces agent-channel invisibility in `connect()` — a config with `access.agent === false` (or no `access`) throws the same not-found error as an unknown config name
-- [`src/rpc/commands/changes.ts`](../../src/rpc/commands/changes.ts) — RPC commands: `list_changes`, `run_change`, `revert_change`, `ff_changes`
-- [`src/rpc/commands/config.ts`](../../src/rpc/commands/config.ts) — RPC commands: `list_configs` (`permission: 'open'`; filters out `access.agent === false` configs for the agent channel), `get_active_config`
-- [`src/rpc/commands/explore.ts`](../../src/rpc/commands/explore.ts) — RPC commands: `list_tables`, `describe_table`, `list_views`, `list_functions`
-- [`src/rpc/commands/query.ts`](../../src/rpc/commands/query.ts) — RPC commands: `sql` (dispatch-gates on `'sql:read'`; `executeRawSql` itself checks the classified statement class against the config's role), `run_sql`
-- [`src/rpc/commands/run.ts`](../../src/rpc/commands/run.ts) — RPC commands: `run_file`, `run_build`
-- [`src/rpc/commands/session.ts`](../../src/rpc/commands/session.ts) — RPC commands: `connect`, `disconnect` (both `permission: 'open'`), `overview`
-- [`src/rpc/commands/index.ts`](../../src/rpc/commands/index.ts) — command group barrel
-- [`src/rpc/types.ts`](../../src/rpc/types.ts) — `RpcCommand` (carries `permission: Permission | 'open'`), `RpcCommandInfo`, `RpcSession` (carries `readonly channel: Channel`) type definitions
-- [`src/cli/mcp/init.ts`](../../src/cli/mcp/init.ts) — `mcp init` CLI command; writes `.mcp.json` config file
-- [`src/cli/mcp/serve.ts`](../../src/cli/mcp/serve.ts) — `mcp serve` CLI command; starts MCP server over stdio
+- [`src/mcp/server.ts`](../../src/mcp/server.ts) — `createMcpServer(registry, session)`; builds `McpServer` with two tools, `run_noorm_cmd` and `noorm_help`. Dispatch gates every non-`'open'` command via `checkConfigPolicy` (`core/policy`) against the resolved session context before the handler runs; errors from a handler are logged server-side with their stack via `console.error` and returned to the client as `{ error: message }` only (no stack)
+- [`src/mcp/index.ts`](../../src/mcp/index.ts) — `startServer()`; builds `createRegistry()` + `new SessionManager('agent')`, wires `createMcpServer`, connects a `StdioServerTransport`, and registers `SIGINT`/`SIGTERM` handlers that call `session.disconnectAll()`. Never returns — the stdio event loop keeps the process alive
+- [`src/mcp/init.ts`](../../src/mcp/init.ts) — `generateMcpConfig(projectRoot, { agent })`; creates or extends `.mcp.json` (agent `claude`, the default) or `.cursor/mcp.json` (agent `cursor`) with a `noorm` entry under `mcpServers` (`{ command: 'noorm', args: ['mcp', 'serve'] }`), merging into any existing file without touching other entries
+- [`src/rpc/registry.ts`](../../src/rpc/registry.ts) — `RpcRegistry`; flat `Map` with `register`/`get`/`list`/`getHelp`. `getHelp` reads `.shape` off the Zod `inputSchema` at runtime to list parameters, plus each command's [`examples`](../../examples)
+- [`src/rpc/session.ts`](../../src/rpc/session.ts) — `SessionManager`; tracks active `Context` connections per config name. Carries the session's `channel` (`user`/`agent`, default `'user'`) and enforces agent-channel invisibility in `connect()` — a config with `access.agent === false` (or no `access`, fail-closed) throws the byte-identical error as an unknown config name. `getContext()` without a config returns the sole active connection if exactly one exists, or throws naming all active connections if there are multiple
+- [`src/rpc/commands/changes.ts`](../../src/rpc/commands/changes.ts) — RPC commands: `change_history` (`permission: 'explore'`), `change_run` (`permission: 'change:run'`), `change_ff` (`permission: 'change:ff'`), `change_revert` (`permission: 'change:revert'`)
+- [`src/rpc/commands/config.ts`](../../src/rpc/commands/config.ts) — RPC command: `list_configs` (`permission: 'open'`; filters out `access.agent === false` configs for the agent channel via `isVisibleToChannel`)
+- [`src/rpc/commands/explore.ts`](../../src/rpc/commands/explore.ts) — RPC commands (all `permission: 'explore'`): `overview` (counts by object type), `list` (by category: tables/views/procedures/functions/types/indexes/foreignKeys/triggers/locks/connections), `detail` (full detail for one object)
+- [`src/rpc/commands/query.ts`](../../src/rpc/commands/query.ts) — RPC command: `sql` (dispatch-gates on `'sql:read'`; `executeRawSql` itself classifies the statement and checks `sql:write`/`sql:ddl` against the config's resolved role for the session's channel)
+- [`src/rpc/commands/run.ts`](../../src/rpc/commands/run.ts) — RPC commands: `run_build` (`permission: 'run:build'`, checksum-based, `force` to skip), `run_file` (`permission: 'run:file'`)
+- [`src/rpc/commands/session.ts`](../../src/rpc/commands/session.ts) — RPC commands: `connect`, `disconnect`, `status` (all `permission: 'open'`). `status` reports `{ connections, activeConfig, activeConnected }`, resolving `activeConfig` the same way a bare `connect` would (env override, then state), and nulls it out on the agent channel when the config is hidden
+- [`src/rpc/commands/index.ts`](../../src/rpc/commands/index.ts) — `registerAllCommands(registry)`; registers all 14 commands (3 session + 1 config + 3 explore + 1 query + 4 changes + 2 run) into the registry
+- [`src/rpc/types.ts`](../../src/rpc/types.ts) — `RpcCommand` (carries `permission: Permission | 'open'`, `handler(input, session)`), `RpcCommandInfo`, `RpcExample`, `RpcSession` (carries `readonly channel: Channel`), `RpcError` type/class definitions
+- [`src/rpc/index.ts`](../../src/rpc/index.ts) — barrel export; `createRegistry()` builds an `RpcRegistry` and calls `registerAllCommands`
+- [`src/cli/mcp/index.ts`](../../src/cli/mcp/index.ts) — Citty `mcp` command group with subcommands `init` and `serve`
+- [`src/cli/mcp/init.ts`](../../src/cli/mcp/init.ts) — `noorm mcp init [--agent claude|cursor] [--json]`; calls `generateMcpConfig`
+- [`src/cli/mcp/serve.ts`](../../src/cli/mcp/serve.ts) — `noorm mcp serve`; calls `startServer()`, intentionally never calls `process.exit()` since stdin keeps the loop alive
## Docs
@@ -35,20 +38,22 @@ Every `RpcCommand` declares a `permission: Permission | 'open'` (`core/policy` p
## Coupling
-- MCP server wraps RPC registry — new RPC commands are automatically discoverable via `noorm_help`.
-- RPC commands delegate to core modules (same as CLI) — core API changes need RPC command updates in parallel with CLI changes.
+- MCP server wraps the RPC registry — new RPC commands registered in [`src/rpc/commands/index.ts`](../../src/rpc/commands/index.ts) are automatically discoverable via `noorm_help` and callable via `run_noorm_cmd` with no further MCP-layer wiring.
+- RPC commands delegate to core modules the same way CLI commands do (`ctx.noorm.changes.*` → `core-change`, `ctx.noorm.run.*` → `core-runner`, [`src/core/explore/operations.ts`](../../src/core/explore/operations.ts) → `core-db`, [`src/core/sql-terminal/executor.ts`](../../src/core/sql-terminal/executor.ts) → `core-identity`) — core API changes need RPC command updates in parallel with CLI changes.
- MCP dispatch gates every non-`'open'` `RpcCommand` via `checkConfigPolicy` from [`src/core/policy/`](../../src/core/policy) — `src/rpc/protection.ts` and `src/core/config/protection.ts` (the old protected-config rule checkers) were both deleted; policy is now the sole enforcement point.
-- Every `RpcCommand` declares a `permission: Permission | 'open'` ([`src/rpc/types.ts`](../../src/rpc/types.ts)) — new RPC commands must pick a `core/policy` `Permission` or `'open'`, or the dispatch gate in [`src/mcp/server.ts`](../../src/mcp/server.ts) has nothing to check.
-- `SessionManager` holds live Kysely connections — connection lifecycle must coordinate with [`src/core/connection/manager.ts`](../../src/core/connection/manager.ts).
-- [`src/cli/mcp/serve.ts`](../../src/cli/mcp/serve.ts) is the CLI entry; [`src/mcp/init.ts`](../../src/mcp/init.ts) is the wiring; [`src/mcp/server.ts`](../../src/mcp/server.ts) is the MCP layer.
+- Every `RpcCommand` declares a `permission: Permission | 'open'` ([`src/rpc/types.ts`](../../src/rpc/types.ts)) — new RPC commands must pick a `core/policy` `Permission` or `'open'`, or the dispatch gate in [`src/mcp/server.ts`](../../src/mcp/server.ts) has nothing to check. [`tests/core/rpc/permissions.test.ts`](../../tests/core/rpc/permissions.test.ts) pins the exact `permission` value per command name against a hardcoded table, so a rename or reclassification must update that test too.
+- `SessionManager` holds live `Context`/Kysely connections created via `createContext` from `sdk` ([`src/sdk/index.ts`](../../src/sdk/index.ts)) — connection lifecycle and config resolution errors (`configNotFoundMessage`) come from `core-config`'s resolver.
+- [`src/cli/mcp/serve.ts`](../../src/cli/mcp/serve.ts) is the CLI entry; [`src/mcp/index.ts`](../../src/mcp/index.ts) (`startServer`) is the wiring; [`src/mcp/server.ts`](../../src/mcp/server.ts) is the MCP dispatch layer.
+- [`src/cli/mcp/init.ts`](../../src/cli/mcp/init.ts) calls into [`src/mcp/init.ts`](../../src/mcp/init.ts)'s `generateMcpConfig`, which writes `.mcp.json`/`.cursor/mcp.json` — a `cli` domain command backed by mcp-rpc code.
## Conventions worth knowing
-- MCP transport: stdio (JSON-RPC over stdin/stdout).
-- `run_noorm_cmd` dispatches by command name string — command names are stable API surface.
-- `noorm_help` lists all registered commands with descriptions and parameter schemas.
-- `mcp init` writes `.mcp.json` with the `noorm mcp serve` invocation for Claude Desktop / IDE integration.
-- Zod schemas on each RPC command define the `payload` shape validated at dispatch time.
-- Tests in [`tests/core/mcp/`](../../tests/core/mcp) cover server init and command dispatch; [`tests/core/rpc/`](../../tests/core/rpc) covers registry, permissions, session.
-- `connect()` on the agent channel throws the identical `configNotFoundMessage` error (`core/config/resolver.ts`) for an unknown config and an invisible one (`access.agent === false`) — an agent cannot distinguish "doesn't exist" from "not permitted".
-- `SessionInfo.protected: boolean` was replaced by `SessionInfo.role: Role` — the resolved role for the session's channel (`agent` resolves `access.agent`, `user` resolves `access.user`).
+- MCP transport: stdio (JSON-RPC over stdin/stdout); `StdioServerTransport` from `@modelcontextprotocol/sdk`.
+- `run_noorm_cmd` dispatches by command name string passed in its `command` field — command names are stable API surface. Its `config` field selects which active session/connection a command targets; for session commands (`connect`/`disconnect`) it is merged into the payload instead.
+- For non-session commands invoked with a `config`, `createConfigScopedSession` in [`src/mcp/server.ts`](../../src/mcp/server.ts) wraps the `SessionManager` in a `Proxy` that intercepts only `getContext` to pin it to that config, forwarding all other session methods unchanged.
+- `noorm_help` lists all registered commands with descriptions when called with no arguments, or full parameter/example detail for a single named command.
+- `mcp init` writes `.mcp.json` (or `.cursor/mcp.json`) with a `noorm mcp serve` invocation for Claude Code / Cursor MCP discovery; existing files are merged, not overwritten.
+- Zod schemas on each RPC command define the `payload` shape validated at dispatch time; validation failures return `{ error: 'Invalid payload', details: [...] }` with per-field Zod issue paths.
+- Tests in [`tests/core/mcp/`](../../tests/core/mcp) cover `generateMcpConfig` (`init.test.ts`) and the full MCP dispatch pipeline via `InMemoryTransport` + `Client` (`server.test.ts`); [`tests/core/rpc/`](../../tests/core/rpc) covers registry behavior, command permissions, command handler logic, and session manager invisibility rules.
+- `connect()` on the agent channel throws the identical `configNotFoundMessage` error (`core/config/resolver.ts`) for an unknown config and an invisible one (`access.agent === false` or missing `access`) — an agent cannot distinguish "doesn't exist" from "not permitted".
+- `SessionInfo.role: Role` is the resolved role for the session's channel (`agent` resolves `access.agent`, `user` resolves `access.user`); an operator-role cell for the agent channel is still gated per-permission at dispatch, same as any other role.
diff --git a/docs/wiki/scan.md b/docs/wiki/scan.md
index 33101f0b..bf245fd1 100644
--- a/docs/wiki/scan.md
+++ b/docs/wiki/scan.md
@@ -7,132 +7,66 @@
│ └── opentui/ (2)
│ ├── references/ (0 files, 8 dirs)
│ └── SKILL.md (a62967f, 195L, 7253ch, 7427B)
-├── .changeset/ (73)
+├── .changeset/ (3)
│ ├── README.md (bf33c79, 8L, 510ch, 510B)
-│ ├── binary-release-automation.md (b25adb3, 6L, 110ch, 110B)
-│ ├── bold-wolves-call.md (a1927b2, 10L, 442ch, 442B)
-│ ├── brave-foxes-modify.md (da2d15a, 8L, 345ch, 347B)
-│ ├── brave-wolves-rest.md (b7910ac, 5L, 220ch, 220B)
-│ ├── bright-foxes-glow.md (9638d52, 16L, 520ch, 520B)
-│ ├── bundle-fix.md (09aaa39, 8L, 200ch, 200B)
-│ ├── calm-rivers-flow.md (146df56, 7L, 175ch, 175B)
-│ ├── change-dry-run-cli.md (563b623, 8L, 502ch, 504B)
-│ ├── change-dry-run-sdk.md (5b3059e, 6L, 263ch, 263B)
-│ ├── change-interactive-prompts.md (1f01ea9, 19L, 1977ch, 1995B)
-│ ├── citty-cli-migration.md (af9eed7, 16L, 596ch, 596B)
-│ ├── citty-sdk-next.md (3cace63, 7L, 143ch, 143B)
-│ ├── cjs-shim-fix.md (ce8f94e, 8L, 127ch, 127B)
│ ├── config.json (64bb386, 11L, 307ch, 307B)
-│ ├── crisp-taxis-pick.md (38ea970, 30L, 1170ch, 1170B)
-│ ├── dry-run-error-display.md (9ab9e9d, 7L, 358ch, 358B)
-│ ├── fix-binary-bun-pin.md (21262d2, 6L, 173ch, 175B)
-│ ├── fix-bundle-version.md (20815f5, 5L, 107ch, 107B)
-│ ├── fix-headless-error-output.md (e2fdf59, 15L, 883ch, 885B)
-│ ├── fix-helpers-loading.md (12f7ed3, 12L, 591ch, 595B)
-│ ├── fix-mssql-connection-hang.md (2219da2, 11L, 502ch, 508B)
-│ ├── fix-mssql-dialect-support-sdk.md (164eaa5, 10L, 442ch, 442B)
-│ ├── fix-mssql-dialect-support.md (dadf1da, 19L, 1407ch, 1409B)
-│ ├── fix-mssql-mysql-schema-migration.md (befbcf0, 11L, 504ch, 512B)
-│ ├── fix-mssql-tarn-bundle-interop-cli.md (dd0bfcf, 11L, 474ch, 474B)
-│ ├── fix-mssql-tarn-bundle-interop.md (fd9465e, 11L, 450ch, 450B)
-│ ├── fix-sdk-bundle-deps.md (69ad2cc, 15L, 738ch, 744B)
-│ ├── fix-shutdown-hang.md (a723a06, 8L, 367ch, 367B)
-│ ├── fix-teardown-schema-qualify-cli.md (ebd436d, 7L, 223ch, 223B)
-│ ├── fix-teardown-schema-qualify.md (4cc7e50, 7L, 230ch, 230B)
-│ ├── fix-teardown-tvp-ordering-cli.md (65aa702, 7L, 203ch, 203B)
-│ ├── fix-teardown-tvp-ordering.md (f5f4c97, 7L, 203ch, 203B)
-│ ├── generic-tvp-value.md (8d46d7b, 6L, 223ch, 225B)
-│ ├── gentle-birds-impersonate.md (d33a2be, 9L, 598ch, 600B)
-│ ├── gold-items-feel.md (90a88cd, 35L, 1391ch, 1391B)
-│ ├── hip-pigs-knock.md (d9759c9, 34L, 1504ch, 1524B)
-│ ├── humble-emus-jam.md (083c2fc, 167L, 8541ch, 8613B)
-│ ├── identity-ci-settings-edit-secret.md (9a339dc, 9L, 1071ch, 1077B)
-│ ├── init-nested-projects.md (d0ceb9e, 10L, 578ch, 578B)
-│ ├── keen-moons-glow.md (4dd2b64, 7L, 274ch, 274B)
-│ ├── kind-dogs-cheer.md (45e31cb, 6L, 176ch, 176B)
-│ ├── mcp-server.md (e4430f9, 8L, 582ch, 584B)
-│ ├── mssql-go-splitter-sdk.md (1a4d376, 11L, 708ch, 710B)
-│ ├── mssql-teardown-sdk.md (37fb6c1, 8L, 917ch, 929B)
-│ ├── noorm-ci-namespace.md (0e98b5d, 28L, 2748ch, 2762B)
-│ ├── noorm-init-sql-repl.md (19e9978, 8L, 373ch, 377B)
-│ ├── pre.json (48b4181, 83L, 2304ch, 2304B)
-│ ├── quiet-pandas-sleep.md (a17d93c, 7L, 220ch, 220B)
-│ ├── rebuild-fix.md (174afad, 5L, 69ch, 69B)
-│ ├── reset-ignores-preserve-tables-cli.md (5946a54, 13L, 594ch, 596B)
-│ ├── reset-ignores-preserve-tables.md (e8a8f6d, 13L, 575ch, 577B)
-│ ├── rich-errors-templates-headless.md (fea62cb, 18L, 1337ch, 1345B)
-│ ├── runner-observability-cli.md (aea99ea, 9L, 562ch, 562B)
-│ ├── sdk-protected-config-hardblock.md (99d3624, 37L, 1477ch, 1489B)
-│ ├── sharp-foxes-run.md (4f88802, 5L, 224ch, 224B)
-│ ├── swift-clouds-drift.md (cbb537b, 7L, 175ch, 175B)
-│ ├── teardown-mssql-check-constraint-udf-cli.md (e4a845b, 13L, 632ch, 632B)
-│ ├── teardown-mssql-check-constraint-udf.md (445a6fa, 13L, 591ch, 591B)
-│ ├── tender-lions-enter.md (aae45fa, 32L, 1240ch, 1240B)
-│ ├── tiny-dogs-yawn.md (7c0d0cc, 5L, 127ch, 127B)
-│ ├── tty-yes-flag-cli.md (a93a4e3, 8L, 723ch, 723B)
-│ ├── tvp-support.md (48e6ffe, 8L, 328ch, 330B)
-│ ├── typed-tuples-sdk.md (c803a7b, 6L, 216ch, 218B)
-│ ├── update-progress-stall.md (ab5f66a, 5L, 642ch, 644B)
-│ ├── update-resumable-download.md (4b63e28, 5L, 586ch, 588B)
-│ ├── vault-init-idempotent-sdk.md (93cbe93, 8L, 545ch, 547B)
-│ ├── version-command.md (379be83, 5L, 138ch, 138B)
-│ ├── version-debug.md (268b067, 5L, 96ch, 96B)
-│ ├── warm-apples-march.md (e8178e6, 13L, 754ch, 754B)
-│ ├── warm-tables-stay.md (4440596, 9L, 327ch, 329B)
-│ ├── wise-owls-guard.md (64ccc90, 17L, 3658ch, 3684B)
-│ └── worker-bridge.md (5aebf7c, 14L, 1107ch, 1109B)
-├── .claude/ (2)
+│ └── olive-pugs-shave.md (3298d84, 11L, 461ch, 463B)
+├── .claude/ (3)
│ ├── rules/ (4)
│ │ ├── documentation.md (69cdfde, 30L, 837ch, 837B)
│ │ ├── testing.md (3c3b98d, 58L, 1070ch, 1070B)
│ │ ├── tui-development.md (68c920f, 159L, 4031ch, 4033B)
│ │ └── typescript.md (1515159, 308L, 8136ch, 8164B)
-│ └── skills/ (2)
-│ ├── noorm-design/ (7)
-│ │ ├── assets/ (5 files, 1 dir)
-│ │ ├── preview/ (25 files, 0 dirs)
-│ │ ├── ui_kits/ (0 files, 2 dirs)
-│ │ ├── uploads/ (1 file, 0 dirs)
-│ │ ├── README.md (0afc77c, 302L, 13445ch, 13558B)
-│ │ ├── SKILL.md (0b38f2d, 29L, 1885ch, 1893B)
-│ │ └── colors_and_type.css (9e653e9, 403L, 12946ch, 13014B)
-│ └── opentui
+│ ├── skills/ (2)
+│ │ ├── noorm-design/ (7)
+│ │ │ ├── assets/ (5 files, 1 dir)
+│ │ │ ├── preview/ (25 files, 0 dirs)
+│ │ │ ├── ui_kits/ (0 files, 2 dirs)
+│ │ │ ├── uploads/ (1 file, 0 dirs)
+│ │ │ ├── README.md (0afc77c, 302L, 13445ch, 13558B)
+│ │ │ ├── SKILL.md (0b38f2d, 29L, 1885ch, 1893B)
+│ │ │ └── colors_and_type.css (9e653e9, 403L, 12946ch, 13014B)
+│ │ └── opentui
+│ └── atomic.toml (23bc3bf, 1L, 15ch, 15B)
├── .github/ (1)
│ └── workflows/ (4)
-│ ├── ci.yml (1f9faff, 445L, 18826ch, 19874B)
+│ ├── ci.yml (cc73a55, 465L, 20232ch, 21286B)
│ ├── docs.yml (1d7b2ac, 51L, 1456ch, 1456B)
-│ ├── publish.yml (b0bca54, 93L, 2354ch, 2354B)
+│ ├── publish.yml (48bab2f, 97L, 2442ch, 2442B)
│ └── release-binary.yml (56d023f, 46L, 1333ch, 1333B)
-├── docs/ (15)
-│ ├── .vitepress/ (2)
+├── docs/ (16)
+│ ├── .vitepress/ (3)
│ │ ├── theme/ (5)
-│ │ │ ├── HeroEyebrow.vue (e5ca4ad, 15L, 356ch, 361B)
+│ │ │ ├── HeroEyebrow.vue (9674567, 21L, 549ch, 554B)
│ │ │ ├── HeroStats.vue (715ce16, 26L, 531ch, 533B)
-│ │ │ ├── HeroTerminal.vue (c6d4dfd, 140L, 3400ch, 3401B)
-│ │ │ ├── brand.css (861d0a6, 425L, 11555ch, 14635B)
+│ │ │ ├── HeroTerminal.vue (95c6581, 156L, 3887ch, 3888B)
+│ │ │ ├── brand.css (09ef946, 527L, 14865ch, 18317B)
│ │ │ └── index.ts (5ce55a8, 53L, 1207ch, 1207B)
-│ │ └── config.mts (91137d2, 192L, 8350ch, 8354B)
-│ ├── cli/ (9)
-│ │ ├── flags.md (9b7f8eb, 94L, 3136ch, 3152B)
+│ │ ├── config.mts (d308dd8, 242L, 11305ch, 11316B)
+│ │ └── og-source.html (f8e2a14, 135L, 3784ch, 3793B)
+│ ├── cli/ (10)
+│ │ ├── flags.md (a9e1916, 100L, 4372ch, 4408B)
│ │ ├── help.md (bbb20fe, 47L, 1421ch, 1429B)
│ │ ├── identity.md (1578c85, 79L, 3191ch, 3209B)
│ │ ├── init.md (ca7879c, 70L, 2873ch, 2889B)
│ │ ├── run.md (654e92f, 123L, 3831ch, 3845B)
+│ │ ├── secret.md (b0496fd, 71L, 2829ch, 2859B)
│ │ ├── settings-edit.md (56b510a, 21L, 588ch, 590B)
-│ │ ├── settings-secret.md (c26ebf9, 23L, 675ch, 679B)
+│ │ ├── settings-secret.md (037f790, 23L, 650ch, 654B)
│ │ ├── sql-repl.md (10dc595, 28L, 785ch, 789B)
│ │ └── sql.md (18be39b, 70L, 2615ch, 2639B)
-│ ├── design/ (2)
+│ ├── design/ (3)
│ │ ├── .gitkeep (e3b0c44, 0L, 0ch, 0B)
-│ │ └── config-access-roles.md (bd73baa, 116L, 6566ch, 6670B)
+│ │ ├── config-access-roles.md (bd73baa, 116L, 6566ch, 6670B)
+│ │ └── v1-49-54-cli-field-defects.md (6f051d8, 242L, 12448ch, 12534B)
│ ├── dev/ (25)
-│ │ ├── change.md (1ebd1fc, 509L, 15457ch, 15519B)
+│ │ ├── change.md (b3d49ee, 526L, 16571ch, 16641B)
│ │ ├── ci.md (e602257, 205L, 6796ch, 6804B)
-│ │ ├── config-sharing.md (61852f9, 269L, 8593ch, 8611B)
-│ │ ├── config.md (82eff09, 436L, 12921ch, 12957B)
-│ │ ├── datamodel.md (de196f7, 1040L, 29861ch, 29989B)
+│ │ ├── config-sharing.md (f1018ed, 269L, 8595ch, 8613B)
+│ │ ├── config.md (3a3afc4, 436L, 13084ch, 13120B)
+│ │ ├── datamodel.md (3f99cbc, 1040L, 29867ch, 29995B)
│ │ ├── explore.md (6895cb3, 325L, 8889ch, 8959B)
-│ │ ├── headless.md (232109b, 758L, 18161ch, 18301B)
+│ │ ├── headless.md (b6555e7, 759L, 18235ch, 18379B)
│ │ ├── identity.md (4a4ea8e, 350L, 12135ch, 12159B)
│ │ ├── index.md (3b3ccb3, 42L, 1430ch, 1430B)
│ │ ├── ink-cheatsheet.md (a188494, 1427L, 28669ch, 28689B)
@@ -141,84 +75,104 @@
│ │ ├── logger.md (b8754a5, 521L, 15363ch, 16867B)
│ │ ├── project-discovery.md (c1fd8f0, 128L, 4327ch, 4335B)
│ │ ├── runner.md (ec9317d, 516L, 18416ch, 18438B)
-│ │ ├── sdk.md (2e6ed89, 1092L, 27225ch, 27275B)
-│ │ ├── secrets.md (00a6e82, 297L, 10122ch, 10192B)
-│ │ ├── settings.md (b87e542, 746L, 18539ch, 18551B)
+│ │ ├── sdk.md (946c6c9, 1092L, 27231ch, 27281B)
+│ │ ├── secrets.md (b92db7c, 310L, 10625ch, 10699B)
+│ │ ├── settings.md (c4c2619, 754L, 19178ch, 19192B)
│ │ ├── sql-terminal.md (381dcaa, 321L, 8991ch, 10355B)
-│ │ ├── state.md (840d27d, 362L, 9331ch, 9361B)
+│ │ ├── state.md (1dbe4d4, 362L, 9335ch, 9365B)
│ │ ├── teardown.md (fc58de8, 359L, 11952ch, 11984B)
-│ │ ├── template.md (e6310f7, 460L, 11515ch, 11603B)
+│ │ ├── template.md (deea016, 483L, 12808ch, 12906B)
│ │ ├── transfer.md (9bd3975, 673L, 23433ch, 23681B)
│ │ ├── vault.md (3a6045a, 520L, 16028ch, 17640B)
│ │ └── version.md (c0f51c6, 644L, 17504ch, 17516B)
│ ├── getting-started/ (4)
│ │ ├── building-your-sdk.md (be6b3e1, 752L, 16915ch, 17401B)
-│ │ ├── concepts.md (470bf64, 347L, 11180ch, 11540B)
-│ │ ├── first-build.md (6ea690f, 332L, 8338ch, 8434B)
-│ │ └── installation.md (d63e5d0, 129L, 3969ch, 4039B)
-│ ├── guide/ (6)
+│ │ ├── concepts.md (db20512, 347L, 11220ch, 11580B)
+│ │ ├── first-build.md (b5434c5, 336L, 8637ch, 8733B)
+│ │ └── installation.md (d74cbfe, 168L, 5874ch, 5952B)
+│ ├── guide/ (7)
│ │ ├── automation/ (3)
-│ │ │ ├── ci.md (83fe635, 345L, 11915ch, 11951B)
-│ │ │ ├── mcp.md (9543edb, 127L, 4891ch, 5175B)
+│ │ │ ├── ci.md (55f738d, 345L, 11979ch, 12015B)
+│ │ │ ├── mcp.md (5044dd3, 139L, 6137ch, 6423B)
│ │ │ └── non-interactive.md (556d953, 121L, 4392ch, 4406B)
│ │ ├── changes/ (3)
-│ │ │ ├── forward-revert.md (e339875, 285L, 8097ch, 8101B)
-│ │ │ ├── history.md (bbcbca7, 320L, 9917ch, 9917B)
-│ │ │ └── overview.md (5eb6602, 348L, 13623ch, 13945B)
+│ │ │ ├── forward-revert.md (a1cbf94, 285L, 8097ch, 8101B)
+│ │ │ ├── history.md (3d82be2, 320L, 9924ch, 9924B)
+│ │ │ └── overview.md (98c8ba5, 371L, 14609ch, 14953B)
│ │ ├── database/ (5)
│ │ │ ├── create.md (ec7375f, 142L, 4947ch, 4971B)
│ │ │ ├── explore.md (aed5d6e, 481L, 17845ch, 22315B)
-│ │ │ ├── teardown.md (5c1954d, 364L, 10327ch, 10341B)
+│ │ │ ├── teardown.md (59b91e4, 364L, 10327ch, 10341B)
│ │ │ ├── terminal.md (a88f37a, 225L, 7075ch, 9051B)
│ │ │ └── transfer.md (415bbf3, 369L, 10184ch, 10210B)
│ │ ├── environments/ (4)
-│ │ │ ├── configs.md (6b4f3b1, 346L, 11236ch, 11264B)
-│ │ │ ├── secrets.md (fef816e, 200L, 6828ch, 6840B)
-│ │ │ ├── stages.md (76e78ed, 258L, 7545ch, 7551B)
+│ │ │ ├── configs.md (0cc3338, 365L, 12657ch, 12691B)
+│ │ │ ├── secrets.md (7285e3b, 200L, 6908ch, 6920B)
+│ │ │ ├── stages.md (df37bdc, 258L, 7547ch, 7553B)
│ │ │ └── vault.md (19c44c0, 257L, 7681ch, 8229B)
│ │ ├── sql-files/ (3)
-│ │ │ ├── execution.md (4a53b9a, 249L, 8166ch, 8308B)
-│ │ │ ├── organization.md (1d17d3d, 327L, 7430ch, 7892B)
-│ │ │ └── templates.md (a5a8ab9, 458L, 12870ch, 12996B)
-│ │ └── troubleshooting.md (c99678d, 108L, 3639ch, 3647B)
+│ │ │ ├── execution.md (8785c67, 251L, 8934ch, 9082B)
+│ │ │ ├── organization.md (f511c7c, 335L, 8096ch, 8560B)
+│ │ │ └── templates.md (2ef8f51, 482L, 14009ch, 14137B)
+│ │ ├── relational-design.md (f1e4faa, 66L, 3512ch, 3534B)
+│ │ └── troubleshooting.md (7211c23, 112L, 3899ch, 3911B)
│ ├── public/ (3)
│ │ ├── icons/ (10)
│ │ │ ├── bolt.svg (8880fa8, 1L, 586ch, 586B)
-│ │ │ ├── code-branch.svg (027335b, 1L, 1063ch, 1063B)
│ │ │ ├── cubes.svg (477517a, 1L, 1009ch, 1009B)
│ │ │ ├── database.svg (72faaf5, 1L, 389ch, 389B)
│ │ │ ├── fast-forward.svg (fc62b4a, 1L, 302ch, 302B)
│ │ │ ├── flask.svg (ceb88e5, 1L, 676ch, 676B)
-│ │ │ ├── lock.svg (57394ae, 1L, 546ch, 546B)
+│ │ │ ├── git-branch.svg (98f49a3, 1L, 358ch, 358B)
+│ │ │ ├── lock.svg (f4c26ec, 1L, 308ch, 308B)
│ │ │ ├── terminal.svg (5102e98, 1L, 613ch, 613B)
│ │ │ ├── toolbox.svg (7cc582d, 1L, 373ch, 373B)
│ │ │ └── users.svg (7c6b4e2, 1L, 1099ch, 1099B)
-│ │ ├── image/ (3)
+│ │ ├── image/ (8)
+│ │ │ ├── tui/ (18 files, 0 dirs)
│ │ │ ├── 0105.gif (fa282b2, 31773L, 7759553ch, 8187851B)
+│ │ │ ├── build-and-change.gif (5d4590a, 2404L, 360320ch, 369821B)
+│ │ │ ├── install.gif (d932cef, 1163L, 187594ch, 193254B)
│ │ │ ├── logo.png (f273558, 100L, 44738ch, 46953B)
-│ │ │ └── logo.svg (8d46c28, 6L, 2529ch, 2529B)
+│ │ │ ├── logo.svg (8d46c28, 6L, 2529ch, 2529B)
+│ │ │ ├── og.png (fa3d4ae, 206L, 57780ch, 60183B)
+│ │ │ └── tui.gif (5aba1ce, 10298L, 1604371ch, 1651789B)
│ │ └── install.sh (0cc90a2, 116L, 2925ch, 2925B)
│ ├── reference/ (1)
-│ │ └── sdk.md (d4a45c6, 1418L, 42444ch, 42625B)
-│ ├── spec/ (3)
+│ │ └── sdk.md (183bc12, 1418L, 42467ch, 42648B)
+│ ├── spec/ (4)
│ │ ├── .gitkeep (e3b0c44, 0L, 0ch, 0B)
-│ │ ├── config-access-roles.md (d6c54a8, 155L, 18136ch, 18240B)
-│ │ └── v1-45-rewind-tiebreak.md (0e35550, 61L, 5465ch, 5507B)
+│ │ ├── config-access-roles.md (40ef290, 162L, 21805ch, 21929B)
+│ │ ├── v1-45-rewind-tiebreak.md (0e35550, 61L, 5465ch, 5507B)
+│ │ └── v1-49-54-cli-field-defects.md (438757f, 374L, 23347ch, 23495B)
│ ├── superpowers/ (1)
│ │ └── specs/ (1)
│ │ └── 2026-04-19-cli-ci-identity-design.md (6c9cc80, 938L, 32762ch, 32918B)
+│ ├── tapes/ (10)
+│ │ ├── demo-project/ (2)
+│ │ │ ├── changes/ (0 files, 2 dirs)
+│ │ │ └── sql/ (0 files, 2 dirs)
+│ │ ├── 01-install.tape (6fdf961, 65L, 2242ch, 2248B)
+│ │ ├── 02-build-and-change.tape (a8cf0c5, 56L, 1699ch, 1701B)
+│ │ ├── 03-tui.tape (d7a24bf, 191L, 4969ch, 5567B)
+│ │ ├── 04-screenshots.tape (b68ad2f, 181L, 3706ch, 4460B)
+│ │ ├── README.md (a23b108, 185L, 8560ch, 8618B)
+│ │ ├── env-scrub.sh (5a1ec5b, 20L, 930ch, 930B)
+│ │ ├── sandbox.sh (d593e66, 153L, 5529ch, 5547B)
+│ │ ├── shots.sh (56e7b59, 58L, 1701ch, 1701B)
+│ │ └── theme.tape (3ad3d47, 40L, 2063ch, 2071B)
│ ├── bun.lockb (34225b2, 190L, 125711ch, 126677B)
-│ ├── headless.md (3b951ee, 1622L, 39413ch, 39455B)
-│ ├── index.md (23a1967, 178L, 6324ch, 6362B)
+│ ├── headless.md (0b3096a, 1719L, 45034ch, 45140B)
+│ ├── index.md (dafd8aa, 147L, 5273ch, 5287B)
│ ├── package.json (b4778f0, 24L, 657ch, 657B)
-│ └── tui.md (99b066a, 407L, 13994ch, 18090B)
+│ └── tui.md (08c523a, 306L, 9014ch, 9612B)
├── examples/ (3)
│ ├── llm-memory-db-mssql/ (16)
│ │ ├── .cursor/ (1)
│ │ │ └── rules/ (1 file, 0 dirs)
│ │ ├── .noorm/ (2)
│ │ │ ├── .gitignore (14188a3, 1L, 7ch, 7B)
-│ │ │ └── settings.yml (46b45e4, 77L, 1844ch, 1844B)
+│ │ │ └── settings.yml (295c27f, 77L, 1800ch, 1800B)
│ │ ├── changes/ (1)
│ │ │ └── 2026-05-10-add-memory-tag-color/ (1 file, 2 dirs)
│ │ ├── sql/ (11)
@@ -252,13 +206,13 @@
│ │ │ └── sql/ (11 files, 0 dirs)
│ │ ├── .gitignore (ccb61cd, 40L, 446ch, 446B)
│ │ ├── .mcp.json (14f011f, 11L, 174ch, 174B)
-│ │ ├── CHANGELOG.md (8ba2d71, 39L, 655ch, 655B)
+│ │ ├── CHANGELOG.md (bfe9994, 87L, 2095ch, 2095B)
│ │ ├── CLAUDE.md (1f39d31, 111L, 2676ch, 2676B)
│ │ ├── README.md (c228f5b, 121L, 6833ch, 6869B)
│ │ ├── REPORT.md (4f3efcd, 161L, 12957ch, 13004B)
│ │ ├── mcp-config.json (14f011f, 11L, 174ch, 174B)
│ │ ├── mssql-problems.md (5083524, 328L, 23834ch, 23934B)
-│ │ ├── package.json (cf5a387, 23L, 631ch, 631B)
+│ │ ├── package.json (9ddbc72, 23L, 623ch, 623B)
│ │ └── tsconfig.json (7be3bae, 27L, 736ch, 736B)
│ ├── llm-memory-db-pg/ (17)
│ │ ├── .cursor/ (1)
@@ -302,13 +256,13 @@
│ │ │ └── mcp-discovery.test.ts (02d4035, 765L, 24559ch, 24603B)
│ │ ├── .gitignore (a94396a, 36L, 397ch, 397B)
│ │ ├── .mcp.json (14f011f, 11L, 174ch, 174B)
-│ │ ├── CHANGELOG.md (9927bb5, 39L, 652ch, 652B)
+│ │ ├── CHANGELOG.md (3d65256, 87L, 2092ch, 2092B)
│ │ ├── CLAUDE.md (1f39d31, 111L, 2676ch, 2676B)
│ │ ├── README.md (b97f95b, 205L, 10359ch, 10665B)
│ │ ├── REPORT-PHASE-1.md (59b7d41, 103L, 9610ch, 9670B)
│ │ ├── REPORT.md (98ee180, 140L, 17043ch, 17113B)
│ │ ├── mcp-config.json (14f011f, 11L, 174ch, 174B)
-│ │ ├── package.json (56ab1f1, 23L, 670ch, 670B)
+│ │ ├── package.json (ce9b0e3, 23L, 662ch, 662B)
│ │ ├── postgres-problems.md (1cbb5b5, 200L, 13674ch, 13799B)
│ │ └── tsconfig.json (4dc04b1, 30L, 735ch, 735B)
│ └── todo-db/ (10)
@@ -345,192 +299,202 @@
│ │ ├── views/ (2 files, 0 dirs)
│ │ └── preload.ts (0d97cd6, 25L, 658ch, 660B)
│ ├── .gitignore (81531bd, 5L, 57ch, 57B)
-│ ├── CHANGELOG.md (4e31aec, 45L, 712ch, 712B)
+│ ├── CHANGELOG.md (071565a, 93L, 2152ch, 2152B)
│ ├── bunfig.toml (e10e7cb, 5L, 89ch, 89B)
-│ ├── package.json (1a9a77d, 22L, 581ch, 581B)
+│ ├── package.json (53a2a0f, 22L, 573ch, 573B)
│ └── tsconfig.json (efeae48, 17L, 484ch, 484B)
├── packages/ (2)
-│ ├── cli/ (5)
+│ ├── cli/ (6)
│ │ ├── scripts/ (1)
-│ │ │ └── postinstall.js (67c8ecf, 348L, 10155ch, 10157B)
-│ │ ├── CHANGELOG.md (c98de3e, 710L, 41104ch, 41310B)
+│ │ │ └── postinstall.js (b82655f, 411L, 12511ch, 12513B)
+│ │ ├── CHANGELOG.md (0cf4523, 1475L, 105243ch, 105757B)
│ │ ├── LICENSE (42eaf96, 21L, 1070ch, 1070B)
+│ │ ├── README.md (5ec4b13, 65L, 1728ch, 1734B)
│ │ ├── noorm.js (e3d76e8, 41L, 1041ch, 1043B)
-│ │ └── package.json (f0a76ab, 31L, 540ch, 540B)
-│ └── sdk/ (3)
-│ ├── CHANGELOG.md (8d2ebd5, 548L, 27765ch, 27931B)
+│ │ └── package.json (0a869ad, 31L, 534ch, 534B)
+│ └── sdk/ (4)
+│ ├── CHANGELOG.md (9d18194, 1175L, 77104ch, 77530B)
│ ├── LICENSE (42eaf96, 21L, 1070ch, 1070B)
-│ └── package.json (cb0a089, 61L, 1115ch, 1115B)
+│ ├── README.md (0e96b2c, 81L, 2279ch, 2285B)
+│ └── package.json (403cbfc, 61L, 1106ch, 1106B)
├── scripts/ (5)
│ ├── Dockerfile (5fe0d7a, 51L, 1595ch, 1595B)
│ ├── build-binary.mjs (f598b0c, 37L, 1284ch, 1288B)
│ ├── build.mjs (303daea, 40L, 1519ch, 1519B)
-│ ├── check-json-placement.sh (5ef39f2, 34L, 1410ch, 1416B)
+│ ├── check-flag-placement.sh (fe0bf90, 64L, 3054ch, 3068B)
│ └── ralph-wiggum.sh (7182e0a, 319L, 8973ch, 8973B)
├── skills/ (1)
│ └── noorm/ (2)
│ ├── references/ (4)
-│ │ ├── cli.md (4e1c259, 1009L, 29310ch, 29354B)
-│ │ ├── config.md (7d36099, 272L, 9415ch, 10115B)
-│ │ ├── sdk.md (8a62667, 650L, 22064ch, 22163B)
+│ │ ├── cli.md (c2b2dbb, 1013L, 29905ch, 29959B)
+│ │ ├── config.md (1248d65, 300L, 10761ch, 11471B)
+│ │ ├── sdk.md (0722f21, 650L, 22066ch, 22165B)
│ │ └── templates.md (20dbd54, 386L, 11061ch, 11161B)
│ └── SKILL.md (90e425a, 82L, 4768ch, 4784B)
├── src/ (7)
-│ ├── cli/ (20)
+│ ├── cli/ (21)
│ │ ├── change/ (13)
│ │ │ ├── _prompt.ts (a32b90c, 140L, 3465ch, 3467B)
│ │ │ ├── add.ts (4356dc7, 110L, 3023ch, 3025B)
-│ │ │ ├── edit.ts (578f3a7, 129L, 3842ch, 3846B)
-│ │ │ ├── ff.ts (5dbcd50, 108L, 3047ch, 3049B)
+│ │ │ ├── edit.ts (a646168, 130L, 3887ch, 3891B)
+│ │ │ ├── ff.ts (77ac7a8, 117L, 3399ch, 3401B)
│ │ │ ├── history-detail.ts (208c91f, 181L, 5310ch, 5318B)
-│ │ │ ├── history.ts (b142e25, 72L, 1710ch, 1712B)
+│ │ │ ├── history.ts (f88c567, 52L, 1373ch, 1375B)
│ │ │ ├── index.ts (f87649d, 58L, 1518ch, 1522B)
-│ │ │ ├── list.ts (a3b191d, 74L, 1777ch, 1781B)
-│ │ │ ├── next.ts (55f3589, 110L, 3017ch, 3019B)
-│ │ │ ├── revert.ts (af5236e, 133L, 3749ch, 3751B)
-│ │ │ ├── rewind.ts (beaeaba, 132L, 3684ch, 3690B)
-│ │ │ ├── rm.ts (d07fd8f, 157L, 4945ch, 4947B)
-│ │ │ └── run.ts (5a0a0de, 133L, 3744ch, 3746B)
+│ │ │ ├── list.ts (a3b8dd7, 55L, 1720ch, 1724B)
+│ │ │ ├── next.ts (b0ad529, 119L, 3369ch, 3371B)
+│ │ │ ├── revert.ts (96d0444, 134L, 3795ch, 3797B)
+│ │ │ ├── rewind.ts (a762072, 143L, 4254ch, 4260B)
+│ │ │ ├── rm.ts (d398cab, 158L, 5016ch, 5018B)
+│ │ │ └── run.ts (4a3a859, 135L, 3794ch, 3796B)
│ │ ├── ci/ (4)
│ │ │ ├── identity/ (3 files, 0 dirs)
│ │ │ ├── index.ts (edb5806, 26L, 673ch, 679B)
-│ │ │ ├── init.ts (015ad40, 217L, 6764ch, 6772B)
-│ │ │ └── secrets.ts (5b51d3a, 231L, 6176ch, 6180B)
+│ │ │ ├── init.ts (2832f92, 285L, 9642ch, 9650B)
+│ │ │ └── secrets.ts (9b159a4, 235L, 6521ch, 6525B)
│ │ ├── config/ (10)
-│ │ │ ├── add.ts (0e20b1d, 27L, 732ch, 736B)
-│ │ │ ├── cp.ts (cc57e45, 81L, 2209ch, 2211B)
-│ │ │ ├── edit.ts (00f6992, 34L, 888ch, 892B)
-│ │ │ ├── export.ts (ef4b07a, 86L, 2441ch, 2443B)
-│ │ │ ├── import.ts (01f8e59, 105L, 3068ch, 3070B)
+│ │ │ ├── add.ts (1a513cb, 33L, 970ch, 974B)
+│ │ │ ├── cp.ts (c9e35e8, 82L, 2254ch, 2256B)
+│ │ │ ├── edit.ts (76dd499, 41L, 1167ch, 1171B)
+│ │ │ ├── export.ts (1419198, 112L, 3528ch, 3536B)
+│ │ │ ├── import.ts (69be1f7, 136L, 4248ch, 4252B)
│ │ │ ├── index.ts (54b770a, 22L, 611ch, 613B)
-│ │ │ ├── list.ts (cc81ab9, 74L, 1994ch, 1998B)
-│ │ │ ├── rm.ts (acf46db, 105L, 3098ch, 3098B)
-│ │ │ ├── use.ts (f307d0e, 79L, 2169ch, 2173B)
-│ │ │ └── validate.ts (018893b, 75L, 2086ch, 2088B)
-│ │ ├── db/ (16)
-│ │ │ ├── create.ts (b5426cf, 107L, 3023ch, 3025B)
-│ │ │ ├── drop.ts (7625328, 103L, 2880ch, 2882B)
-│ │ │ ├── explore-fks.ts (3a624f7, 79L, 1936ch, 1940B)
-│ │ │ ├── explore-functions.ts (cd47d2a, 132L, 3143ch, 3147B)
-│ │ │ ├── explore-indexes.ts (69140a5, 76L, 1762ch, 1764B)
-│ │ │ ├── explore-procedures.ts (3bfd32a, 125L, 2932ch, 2934B)
-│ │ │ ├── explore-tables-detail.ts (1e74666, 81L, 1983ch, 1985B)
-│ │ │ ├── explore-tables.ts (85ba1fe, 66L, 1430ch, 1432B)
-│ │ │ ├── explore-types.ts (03dbb1a, 136L, 3224ch, 3226B)
-│ │ │ ├── explore-views.ts (d002b8d, 128L, 3069ch, 3071B)
-│ │ │ ├── explore.ts (f62b609, 82L, 2179ch, 2181B)
+│ │ │ ├── list.ts (a5c957d, 80L, 2416ch, 2420B)
+│ │ │ ├── rm.ts (5e95318, 106L, 3169ch, 3169B)
+│ │ │ ├── use.ts (6cf7c62, 80L, 2214ch, 2218B)
+│ │ │ └── validate.ts (c0d6d41, 76L, 2131ch, 2133B)
+│ │ ├── db/ (17)
+│ │ │ ├── create.ts (bfed381, 159L, 5006ch, 5008B)
+│ │ │ ├── drop.ts (5e50c89, 156L, 5192ch, 5198B)
+│ │ │ ├── explore-fks.ts (66823d1, 90L, 2530ch, 2534B)
+│ │ │ ├── explore-functions.ts (0e26f97, 140L, 3645ch, 3649B)
+│ │ │ ├── explore-indexes.ts (b37515e, 87L, 2350ch, 2352B)
+│ │ │ ├── explore-procedures.ts (29695bc, 133L, 3435ch, 3437B)
+│ │ │ ├── explore-tables-detail.ts (38e0b33, 82L, 2028ch, 2030B)
+│ │ │ ├── explore-tables.ts (1bc203b, 77L, 2004ch, 2006B)
+│ │ │ ├── explore-triggers.ts (8377ff2, 137L, 3664ch, 3666B)
+│ │ │ ├── explore-types.ts (20cfb88, 144L, 3722ch, 3724B)
+│ │ │ ├── explore-views.ts (7edc813, 136L, 3567ch, 3569B)
+│ │ │ ├── explore.ts (bc8c8b2, 75L, 2421ch, 2423B)
│ │ │ ├── index.ts (d0fc774, 28L, 610ch, 612B)
-│ │ │ ├── reset.ts (b8343b5, 59L, 1414ch, 1416B)
-│ │ │ ├── teardown.ts (292a1a3, 77L, 2094ch, 2096B)
-│ │ │ ├── transfer.ts (88e617b, 669L, 19239ch, 19251B)
-│ │ │ └── truncate.ts (f272b7b, 63L, 1386ch, 1388B)
+│ │ │ ├── reset.ts (4859fce, 65L, 1840ch, 1842B)
+│ │ │ ├── teardown.ts (9593775, 117L, 3586ch, 3590B)
+│ │ │ ├── transfer.ts (556248b, 685L, 20260ch, 20274B)
+│ │ │ └── truncate.ts (cdd1ce4, 95L, 2681ch, 2683B)
│ │ ├── dev/ (3)
│ │ │ ├── index.ts (a926fdd, 18L, 420ch, 422B)
-│ │ │ ├── test-helpers.ts (68f7d8b, 162L, 5256ch, 5262B)
-│ │ │ └── test-workers.ts (b7586cc, 251L, 7629ch, 7639B)
+│ │ │ ├── test-helpers.ts (f5bfa13, 167L, 5518ch, 5526B)
+│ │ │ └── test-workers.ts (96ceb8e, 256L, 7897ch, 7909B)
│ │ ├── identity/ (5)
-│ │ │ ├── edit.ts (0fcb3f4, 82L, 2518ch, 2520B)
-│ │ │ ├── export.ts (99921ac, 61L, 1779ch, 1781B)
+│ │ │ ├── edit.ts (21f57dc, 116L, 4027ch, 4033B)
+│ │ │ ├── export.ts (b1f4dc3, 62L, 1824ch, 1826B)
│ │ │ ├── index.ts (8e2b3c8, 17L, 451ch, 453B)
-│ │ │ ├── init.ts (0bd5325, 66L, 2198ch, 2200B)
+│ │ │ ├── init.ts (eec5a27, 134L, 4356ch, 4362B)
│ │ │ └── list.ts (4857d87, 65L, 1853ch, 1855B)
│ │ ├── lock/ (5)
-│ │ │ ├── acquire.ts (c2ec3c3, 64L, 1445ch, 1447B)
-│ │ │ ├── force.ts (a218370, 55L, 1177ch, 1179B)
+│ │ │ ├── acquire.ts (f69a60c, 94L, 2942ch, 2944B)
+│ │ │ ├── force.ts (90b77e9, 78L, 2536ch, 2540B)
│ │ │ ├── index.ts (16e428d, 22L, 451ch, 453B)
│ │ │ ├── release.ts (1d574a0, 55L, 1126ch, 1128B)
-│ │ │ └── status.ts (7ddbee9, 80L, 1985ch, 1987B)
+│ │ │ └── status.ts (b3a0681, 54L, 1496ch, 1498B)
│ │ ├── mcp/ (3)
│ │ │ ├── index.ts (79fb558, 15L, 348ch, 350B)
│ │ │ ├── init.ts (efa2243, 57L, 1514ch, 1516B)
│ │ │ └── serve.ts (08a9e57, 28L, 693ch, 697B)
-│ │ ├── run/ (8)
-│ │ │ ├── build.ts (27cfc8c, 91L, 2373ch, 2375B)
-│ │ │ ├── dir.ts (9dd68cb, 95L, 2517ch, 2519B)
-│ │ │ ├── exec.ts (dacee37, 170L, 4757ch, 4759B)
-│ │ │ ├── file.ts (9f4b921, 79L, 2010ch, 2012B)
-│ │ │ ├── files.ts (96be84c, 111L, 3152ch, 3154B)
+│ │ ├── run/ (9)
+│ │ │ ├── _render-secrets.ts (621f487, 87L, 3297ch, 3307B)
+│ │ │ ├── build.ts (5f3aa6f, 147L, 5419ch, 5429B)
+│ │ │ ├── dir.ts (a53d65f, 129L, 3882ch, 3884B)
+│ │ │ ├── exec.ts (8c39c6c, 199L, 5858ch, 5860B)
+│ │ │ ├── file.ts (2aeb103, 80L, 2042ch, 2044B)
+│ │ │ ├── files.ts (179b30e, 112L, 3198ch, 3200B)
│ │ │ ├── index.ts (5f4eb57, 28L, 591ch, 593B)
-│ │ │ ├── inspect.ts (08320d4, 238L, 6786ch, 6788B)
-│ │ │ └── preview.ts (c385471, 105L, 3087ch, 3089B)
-│ │ ├── secret/ (4)
+│ │ │ ├── inspect.ts (2e10974, 279L, 8148ch, 8152B)
+│ │ │ └── preview.ts (b4e99e3, 155L, 5161ch, 5167B)
+│ │ ├── secret/ (5)
+│ │ │ ├── _policy.ts (5bc4cc6, 70L, 2180ch, 2182B)
│ │ │ ├── index.ts (8648e62, 13L, 339ch, 341B)
-│ │ │ ├── list.ts (731ce39, 64L, 1795ch, 1797B)
-│ │ │ ├── rm.ts (f2d3075, 90L, 2523ch, 2525B)
-│ │ │ └── set.ts (467aea4, 73L, 2130ch, 2132B)
+│ │ │ ├── list.ts (e504ddd, 66L, 1819ch, 1821B)
+│ │ │ ├── rm.ts (7d67079, 96L, 2758ch, 2760B)
+│ │ │ └── set.ts (186acfe, 99L, 2925ch, 2927B)
│ │ ├── settings/ (5)
│ │ │ ├── build.ts (d3fc961, 56L, 1551ch, 1553B)
-│ │ │ ├── edit.ts (3e373c3, 643L, 17492ch, 17498B)
+│ │ │ ├── edit.ts (cf17b1b, 644L, 17555ch, 17561B)
│ │ │ ├── index.ts (8333365, 17L, 400ch, 402B)
│ │ │ ├── init.ts (984af2c, 67L, 1833ch, 1835B)
-│ │ │ └── secret.ts (dd30d18, 384L, 9867ch, 9871B)
-│ │ ├── sql/ (5)
-│ │ │ ├── clear.ts (16b8b97, 89L, 2766ch, 2768B)
-│ │ │ ├── history.ts (7c2048c, 139L, 3970ch, 3980B)
+│ │ │ └── secret.ts (4b9a441, 385L, 9930ch, 9934B)
+│ │ ├── sql/ (6)
+│ │ │ ├── _config.ts (ee57dda, 44L, 1700ch, 1706B)
+│ │ │ ├── clear.ts (b8fd6b4, 98L, 3105ch, 3107B)
+│ │ │ ├── history.ts (1577e2c, 155L, 4748ch, 4760B)
│ │ │ ├── index.ts (427fc50, 38L, 1298ch, 1300B)
-│ │ │ ├── query.ts (84227f9, 115L, 2969ch, 2971B)
-│ │ │ └── repl.ts (f0fe0a7, 137L, 4076ch, 4080B)
-│ │ ├── vault/ (7)
-│ │ │ ├── cp.ts (2adf7aa, 195L, 5472ch, 5474B)
+│ │ │ ├── query.ts (71881c3, 123L, 3511ch, 3515B)
+│ │ │ └── repl.ts (f9b2404, 138L, 4139ch, 4143B)
+│ │ ├── vault/ (8)
+│ │ │ ├── _secret-value.ts (4c35676, 75L, 2209ch, 2211B)
+│ │ │ ├── cp.ts (af33c60, 172L, 5358ch, 5364B)
│ │ │ ├── index.ts (c96493f, 16L, 443ch, 445B)
-│ │ │ ├── init.ts (747c923, 93L, 2340ch, 2342B)
-│ │ │ ├── list.ts (1a7892b, 128L, 3447ch, 3449B)
-│ │ │ ├── propagate.ts (b3b523d, 113L, 2877ch, 2879B)
-│ │ │ ├── rm.ts (d13caa5, 91L, 2339ch, 2341B)
-│ │ │ └── set.ts (e2d219e, 91L, 2372ch, 2374B)
-│ │ ├── _utils.ts (522f462, 478L, 12116ch, 12120B)
-│ │ ├── index.ts (937328c, 343L, 11213ch, 11217B)
-│ │ ├── info.ts (63f9546, 351L, 10547ch, 10551B)
-│ │ ├── init.ts (f11a60d, 176L, 5250ch, 5252B)
+│ │ │ ├── init.ts (dac297c, 125L, 3524ch, 3526B)
+│ │ │ ├── list.ts (8524f3e, 148L, 4184ch, 4186B)
+│ │ │ ├── propagate.ts (01356d8, 256L, 8214ch, 8220B)
+│ │ │ ├── rm.ts (1d08e1b, 130L, 4043ch, 4051B)
+│ │ │ └── set.ts (df4161c, 134L, 3942ch, 3944B)
+│ │ ├── _exit.ts (ee7ef69, 75L, 2663ch, 2671B)
+│ │ ├── _utils.ts (09a6abf, 517L, 14517ch, 14527B)
+│ │ ├── index.ts (2e302ae, 391L, 13727ch, 13745B)
+│ │ ├── info.ts (fa5e2b1, 384L, 11512ch, 11516B)
+│ │ ├── init.ts (e04cc7c, 177L, 5303ch, 5305B)
│ │ ├── ui.ts (28a7a9c, 70L, 1869ch, 1875B)
-│ │ ├── update.ts (a124ee5, 202L, 5826ch, 5834B)
+│ │ ├── update.ts (13469b2, 219L, 6573ch, 6581B)
│ │ └── version.ts (ca59073, 273L, 6880ch, 6890B)
│ ├── core/ (30)
│ │ ├── change/ (9)
-│ │ │ ├── executor.ts (51410bd, 1297L, 35479ch, 36963B)
-│ │ │ ├── history.ts (aea7694, 1251L, 36075ch, 37937B)
-│ │ │ ├── index.ts (edd765a, 128L, 3031ch, 4739B)
-│ │ │ ├── manager.ts (47ed983, 632L, 16631ch, 18943B)
-│ │ │ ├── parser.ts (917e642, 570L, 13337ch, 14801B)
-│ │ │ ├── scaffold.ts (a3b3c6c, 625L, 14745ch, 16697B)
-│ │ │ ├── tracker.ts (da3742c, 245L, 7183ch, 7671B)
-│ │ │ ├── types.ts (b863c8d, 697L, 16318ch, 19008B)
+│ │ │ ├── executor.ts (1bfc380, 1324L, 36108ch, 37592B)
+│ │ │ ├── history.ts (bc9c001, 1266L, 38058ch, 39928B)
+│ │ │ ├── index.ts (b0a2184, 131L, 3071ch, 4779B)
+│ │ │ ├── manager.ts (05fe831, 653L, 17287ch, 19599B)
+│ │ │ ├── parser.ts (24d6396, 576L, 13658ch, 15124B)
+│ │ │ ├── scaffold.ts (155fb34, 649L, 15641ch, 17593B)
+│ │ │ ├── tracker.ts (e5fcd43, 266L, 8089ch, 8579B)
+│ │ │ ├── types.ts (f0cda30, 742L, 18037ch, 20735B)
│ │ │ └── validation.ts (ca6f086, 90L, 2247ch, 2247B)
│ │ ├── config/ (5)
-│ │ │ ├── index.ts (f1bc7b6, 126L, 3419ch, 3419B)
-│ │ │ ├── resolver.ts (23dc477, 505L, 13561ch, 13569B)
-│ │ │ ├── schema.ts (224a2af, 344L, 9433ch, 9927B)
-│ │ │ ├── types.ts (9516dc0, 143L, 3478ch, 3726B)
+│ │ │ ├── index.ts (3d6e44e, 127L, 3484ch, 3484B)
+│ │ │ ├── resolver.ts (f2cd9c7, 505L, 13582ch, 13590B)
+│ │ │ ├── schema.ts (79f5b64, 346L, 9526ch, 10020B)
+│ │ │ ├── types.ts (24b482c, 143L, 3480ch, 3728B)
│ │ │ └── validate.ts (7ebdb26, 89L, 2464ch, 2464B)
│ │ ├── connection/ (6)
│ │ │ ├── dialects/ (7 files, 0 dirs)
│ │ │ ├── defaults.ts (42ecd2d, 27L, 685ch, 685B)
-│ │ │ ├── factory.ts (7c5708c, 253L, 7370ch, 7370B)
-│ │ │ ├── index.ts (28dfede, 9L, 327ch, 327B)
+│ │ │ ├── factory.ts (1388e22, 311L, 9476ch, 9482B)
+│ │ │ ├── index.ts (fb5e6ea, 10L, 387ch, 387B)
│ │ │ ├── manager.ts (8152aa7, 348L, 8120ch, 8120B)
-│ │ │ └── types.ts (873d17d, 75L, 1457ch, 1457B)
-│ │ ├── db/ (5)
+│ │ │ └── types.ts (b0950c0, 86L, 1911ch, 1911B)
+│ │ ├── db/ (6)
│ │ │ ├── dialects/ (5 files, 0 dirs)
│ │ │ ├── dual.ts (baaddd4, 174L, 4914ch, 4914B)
-│ │ │ ├── index.ts (04bf6c4, 53L, 1283ch, 1283B)
-│ │ │ ├── operations.ts (81c12ed, 309L, 7998ch, 8002B)
-│ │ │ └── types.ts (9375e79, 95L, 2330ch, 2330B)
+│ │ │ ├── index.ts (7a8c9d2, 54L, 1314ch, 1314B)
+│ │ │ ├── operations.ts (0ea9ee9, 286L, 7664ch, 7668B)
+│ │ │ ├── policy.ts (d5bba8a, 88L, 3202ch, 3212B)
+│ │ │ └── types.ts (c4c4db1, 105L, 2677ch, 2677B)
│ │ ├── debug/ (2)
-│ │ │ ├── index.ts (6e232b4, 21L, 385ch, 385B)
-│ │ │ └── operations.ts (05257cd, 446L, 11657ch, 12391B)
-│ │ ├── dt/ (16)
+│ │ │ ├── index.ts (66ccc4c, 22L, 409ch, 409B)
+│ │ │ └── operations.ts (25178e2, 548L, 15077ch, 15821B)
+│ │ ├── dt/ (17)
│ │ │ ├── dialects/ (4 files, 0 dirs)
-│ │ │ ├── constants.ts (8158c4b, 75L, 1797ch, 1797B)
+│ │ │ ├── constants.ts (289c9e4, 104L, 3005ch, 3011B)
│ │ │ ├── crypto.ts (7586f37, 123L, 3783ch, 3787B)
-│ │ │ ├── deserialize.ts (1e84e10, 366L, 9011ch, 9017B)
+│ │ │ ├── deserialize.ts (8a485d0, 408L, 10546ch, 10552B)
│ │ │ ├── events.ts (360f2c4, 151L, 3984ch, 3984B)
-│ │ │ ├── index.ts (4a6e732, 1016L, 26666ch, 26686B)
+│ │ │ ├── index.ts (fec70cb, 953L, 25633ch, 25655B)
│ │ │ ├── modify.ts (bad43c7, 678L, 17964ch, 17994B)
-│ │ │ ├── paths.ts (76dade7, 103L, 2874ch, 2886B)
-│ │ │ ├── reader.ts (98b263b, 212L, 5505ch, 5509B)
-│ │ │ ├── schema.ts (94fb91b, 384L, 9629ch, 9631B)
+│ │ │ ├── paging.ts (77a6bab, 213L, 5793ch, 5797B)
+│ │ │ ├── paths.ts (a933e15, 137L, 3794ch, 3810B)
+│ │ │ ├── reader.ts (58b7c42, 378L, 10268ch, 10276B)
+│ │ │ ├── schema.ts (de5f1ba, 518L, 13025ch, 13027B)
│ │ │ ├── serialize.ts (4c8ab00, 253L, 6001ch, 6011B)
-│ │ │ ├── streamer.ts (453f365, 395L, 9296ch, 9296B)
+│ │ │ ├── streamer.ts (f3242fa, 404L, 9650ch, 9652B)
│ │ │ ├── type-map.ts (3d74dd0, 139L, 3762ch, 3766B)
│ │ │ ├── types.ts (e11c4e4, 413L, 9366ch, 9366B)
│ │ │ ├── version.ts (6a13dc2, 216L, 5118ch, 5128B)
@@ -538,16 +502,17 @@
│ │ ├── explore/ (4)
│ │ │ ├── dialects/ (5 files, 0 dirs)
│ │ │ ├── index.ts (59b350c, 52L, 1222ch, 1222B)
-│ │ │ ├── operations.ts (47770f3, 439L, 10129ch, 10133B)
-│ │ │ └── types.ts (a373e84, 393L, 7861ch, 7861B)
-│ │ ├── identity/ (9)
-│ │ │ ├── crypto.ts (26f8fa9, 350L, 9858ch, 9858B)
+│ │ │ ├── operations.ts (85ab6af, 468L, 11091ch, 11097B)
+│ │ │ └── types.ts (2ceb9ec, 394L, 8217ch, 8219B)
+│ │ ├── identity/ (10)
+│ │ │ ├── crypto.ts (d7c3c7e, 372L, 10932ch, 10938B)
│ │ │ ├── env.ts (b4c5d5f, 120L, 3024ch, 3032B)
│ │ │ ├── factory.ts (ffbf457, 357L, 8396ch, 8396B)
│ │ │ ├── hash.ts (751360b, 97L, 2350ch, 2350B)
-│ │ │ ├── index.ts (70bbcd4, 219L, 5484ch, 5484B)
+│ │ │ ├── index.ts (dc5cc86, 234L, 6345ch, 6347B)
+│ │ │ ├── provenance.ts (891917f, 74L, 3150ch, 3158B)
│ │ │ ├── resolver.ts (67fae4f, 229L, 5068ch, 5068B)
-│ │ │ ├── storage.ts (cbe771c, 525L, 12605ch, 12607B)
+│ │ │ ├── storage.ts (f219f57, 629L, 16173ch, 16183B)
│ │ │ ├── sync.ts (cd659cf, 438L, 11504ch, 11504B)
│ │ │ └── types.ts (1bb32e8, 207L, 5085ch, 5085B)
│ │ ├── lifecycle/ (4)
@@ -557,165 +522,174 @@
│ │ │ └── types.ts (acdd08c, 155L, 3586ch, 3586B)
│ │ ├── lock/ (4)
│ │ │ ├── errors.ts (33152dd, 134L, 3411ch, 3411B)
-│ │ │ ├── index.ts (57fe77e, 46L, 1049ch, 1049B)
-│ │ │ ├── manager.ts (4915e50, 597L, 16103ch, 16835B)
-│ │ │ └── types.ts (57e8f2b, 119L, 2831ch, 2831B)
+│ │ │ ├── index.ts (ff21d3e, 46L, 1069ch, 1069B)
+│ │ │ ├── manager.ts (1802112, 710L, 20494ch, 21238B)
+│ │ │ └── types.ts (39a283c, 135L, 3383ch, 3385B)
│ │ ├── logger/ (12)
│ │ │ ├── classifier.ts (4fa4422, 179L, 3747ch, 3747B)
│ │ │ ├── color.ts (1dd10fa, 192L, 4033ch, 4045B)
│ │ │ ├── formatter.ts (07ecfee, 450L, 12942ch, 12944B)
│ │ │ ├── index.ts (9e238ba, 69L, 1613ch, 1613B)
-│ │ │ ├── init.ts (fe47e04, 178L, 4739ch, 4739B)
-│ │ │ ├── logger.ts (e7f6847, 800L, 18820ch, 19552B)
+│ │ │ ├── init.ts (38fbacb, 164L, 4588ch, 4590B)
+│ │ │ ├── logger.ts (258a011, 859L, 21046ch, 21782B)
│ │ │ ├── queue.ts (b5b3918, 298L, 6507ch, 7483B)
│ │ │ ├── reader.ts (50e90ff, 150L, 3536ch, 3536B)
-│ │ │ ├── redact.ts (4314060, 389L, 9109ch, 10329B)
+│ │ │ ├── redact.ts (da1c21a, 463L, 11488ch, 12716B)
│ │ │ ├── rotation.ts (3579fb5, 250L, 5628ch, 5628B)
│ │ │ ├── timestamp.ts (e2c9452, 58L, 2205ch, 2212B)
│ │ │ └── types.ts (5036c8b, 127L, 2773ch, 2773B)
-│ │ ├── policy/ (6)
-│ │ │ ├── check.ts (44c682a, 205L, 6648ch, 6666B)
-│ │ │ ├── classify.ts (1d30fc5, 821L, 19394ch, 19426B)
-│ │ │ ├── index.ts (919c2d8, 20L, 655ch, 655B)
-│ │ │ ├── legacy-access.ts (f2b9b23, 35L, 1362ch, 1366B)
-│ │ │ ├── matrix.ts (143d78b, 28L, 1400ch, 1403B)
-│ │ │ └── types.ts (5b2506c, 74L, 2289ch, 2295B)
-│ │ ├── runner/ (6)
+│ │ ├── policy/ (8)
+│ │ │ ├── channel.ts (66ba84f, 65L, 2495ch, 2501B)
+│ │ │ ├── check.ts (aa6d8ee, 212L, 7117ch, 7133B)
+│ │ │ ├── classify.ts (1fab835, 933L, 26701ch, 26761B)
+│ │ │ ├── harness.ts (8f66fc3, 118L, 4160ch, 4172B)
+│ │ │ ├── index.ts (e7e91b4, 23L, 839ch, 839B)
+│ │ │ ├── legacy-access.ts (1cb43c7, 53L, 2292ch, 2300B)
+│ │ │ ├── matrix.ts (f323c96, 49L, 2634ch, 2639B)
+│ │ │ └── types.ts (5b1fc76, 82L, 2738ch, 2744B)
+│ │ ├── runner/ (7)
│ │ │ ├── checksum.ts (d21f46d, 99L, 2650ch, 2650B)
│ │ │ ├── index.ts (5156823, 55L, 1020ch, 1020B)
-│ │ │ ├── mssql-batches.ts (6f7535b, 166L, 4825ch, 4835B)
-│ │ │ ├── runner.ts (9e3a619, 1440L, 37499ch, 39213B)
-│ │ │ ├── tracker.ts (3547b6f, 704L, 20648ch, 20876B)
-│ │ │ └── types.ts (99ca816, 403L, 10072ch, 12026B)
+│ │ │ ├── mssql-batches.ts (3a929cd, 180L, 5670ch, 5684B)
+│ │ │ ├── runner.ts (a74216e, 1561L, 42177ch, 43895B)
+│ │ │ ├── sqlite-statements.ts (74e41da, 252L, 6407ch, 6419B)
+│ │ │ ├── tracker.ts (46b4a5c, 743L, 23780ch, 24014B)
+│ │ │ └── types.ts (f467c87, 436L, 11424ch, 13384B)
│ │ ├── settings/ (7)
│ │ │ ├── defaults.ts (3fef652, 117L, 2748ch, 2748B)
│ │ │ ├── events.ts (f8588b1, 108L, 2257ch, 2257B)
│ │ │ ├── index.ts (f096e30, 78L, 1597ch, 1597B)
-│ │ │ ├── manager.ts (6b5a0dd, 1034L, 22970ch, 25656B)
-│ │ │ ├── rules.ts (1b5776e, 288L, 6904ch, 6904B)
+│ │ │ ├── manager.ts (76e5e37, 1107L, 25307ch, 28003B)
+│ │ │ ├── rules.ts (52445be, 288L, 6906ch, 6906B)
│ │ │ ├── schema.ts (9df771c, 335L, 9305ch, 11383B)
│ │ │ └── types.ts (fda4f91, 310L, 7363ch, 7367B)
-│ │ ├── shared/ (5)
+│ │ ├── shared/ (6)
│ │ │ ├── dialect-quoting.ts (8f028eb, 66L, 1860ch, 1866B)
│ │ │ ├── errors.ts (9cb1445, 221L, 6038ch, 6528B)
-│ │ │ ├── files.ts (af32191, 83L, 2567ch, 2567B)
-│ │ │ ├── index.ts (b6f4b23, 62L, 1312ch, 1312B)
+│ │ │ ├── files.ts (aa00f6e, 170L, 5252ch, 5258B)
+│ │ │ ├── index.ts (53c6a75, 65L, 1468ch, 1468B)
+│ │ │ ├── operation-id.ts (7feb6c9, 197L, 6858ch, 6874B)
│ │ │ └── tables.ts (06b23dd, 487L, 12754ch, 14716B)
│ │ ├── sql-terminal/ (4)
│ │ │ ├── executor.ts (9c75cbc, 164L, 4951ch, 4955B)
-│ │ │ ├── history.ts (0cdd43f, 407L, 9888ch, 9888B)
-│ │ │ ├── index.ts (fabf166, 9L, 185ch, 185B)
+│ │ │ ├── history.ts (edcbcea, 428L, 11003ch, 11007B)
+│ │ │ ├── index.ts (23c3d3d, 17L, 557ch, 557B)
│ │ │ └── types.ts (cb0a264, 123L, 2519ch, 2519B)
-│ │ ├── state/ (6)
+│ │ ├── state/ (9)
│ │ │ ├── encryption/ (2 files, 0 dirs)
-│ │ │ ├── index.ts (5f90995, 71L, 1445ch, 1445B)
-│ │ │ ├── manager.ts (459c2f5, 838L, 21789ch, 23257B)
-│ │ │ ├── migrations.ts (2d1f7fc, 88L, 2913ch, 2913B)
-│ │ │ ├── types.ts (638ec9f, 92L, 2562ch, 2564B)
+│ │ │ ├── access.ts (fd7a5b4, 76L, 2616ch, 2624B)
+│ │ │ ├── index.ts (4d767a8, 72L, 1531ch, 1531B)
+│ │ │ ├── manager.ts (f37ca3c, 940L, 25032ch, 26500B)
+│ │ │ ├── merge.ts (4b22b73, 173L, 4851ch, 4851B)
+│ │ │ ├── migrations.ts (e0fc8c9, 98L, 3549ch, 3553B)
+│ │ │ ├── persistence.ts (169ca9d, 264L, 7054ch, 7060B)
+│ │ │ ├── types.ts (1e7c0fc, 103L, 3015ch, 3017B)
│ │ │ └── version.ts (11a6a0b, 26L, 649ch, 649B)
│ │ ├── teardown/ (4)
│ │ │ ├── dialects/ (5 files, 0 dirs)
│ │ │ ├── index.ts (74cbb84, 27L, 549ch, 549B)
-│ │ │ ├── operations.ts (514d1e6, 611L, 17692ch, 17730B)
-│ │ │ └── types.ts (df8debf, 282L, 7405ch, 7407B)
+│ │ │ ├── operations.ts (36a9316, 688L, 20730ch, 20774B)
+│ │ │ └── types.ts (375b750, 327L, 8970ch, 8976B)
│ │ ├── template/ (7)
│ │ │ ├── loaders/ (7 files, 0 dirs)
-│ │ │ ├── context.ts (a34f692, 245L, 6537ch, 6537B)
+│ │ │ ├── context.ts (991d1f2, 390L, 11848ch, 11864B)
│ │ │ ├── engine.ts (5eb6f57, 214L, 6145ch, 6155B)
-│ │ │ ├── helpers.ts (2c3adab, 182L, 4612ch, 4640B)
+│ │ │ ├── helpers.ts (dc37e61, 185L, 4817ch, 4845B)
│ │ │ ├── index.ts (a611347, 69L, 1570ch, 1570B)
│ │ │ ├── types.ts (eeb0f2a, 214L, 4307ch, 4307B)
-│ │ │ └── utils.ts (b4c399e, 129L, 2822ch, 2852B)
+│ │ │ └── utils.ts (66c4ca1, 187L, 4773ch, 4817B)
│ │ ├── transfer/ (7)
│ │ │ ├── dialects/ (5 files, 0 dirs)
│ │ │ ├── events.ts (a6a7783, 69L, 1687ch, 1687B)
-│ │ │ ├── executor.ts (ab388d9, 1109L, 26787ch, 26791B)
-│ │ │ ├── index.ts (58e8d72, 235L, 6324ch, 6326B)
-│ │ │ ├── planner.ts (916e6d7, 660L, 17836ch, 17838B)
-│ │ │ ├── same-server.ts (1e8aaa7, 114L, 2906ch, 2906B)
+│ │ │ ├── executor.ts (dc3eec9, 1091L, 27190ch, 27198B)
+│ │ │ ├── index.ts (55c7baa, 246L, 6720ch, 6722B)
+│ │ │ ├── planner.ts (afa2dad, 667L, 18083ch, 18089B)
+│ │ │ ├── same-server.ts (a2f0cd0, 117L, 2996ch, 3000B)
│ │ │ └── types.ts (bdbbcbf, 188L, 4630ch, 4632B)
│ │ ├── update/ (8)
-│ │ │ ├── checker.ts (445cd79, 348L, 8642ch, 8642B)
+│ │ │ ├── checker.ts (4adba06, 435L, 11470ch, 11474B)
│ │ │ ├── checksum.ts (4d40218, 159L, 4902ch, 4918B)
│ │ │ ├── global-settings.ts (ced5668, 317L, 7229ch, 7229B)
│ │ │ ├── index.ts (1985bc9, 69L, 1446ch, 1446B)
-│ │ │ ├── install-mode.ts (3331488, 167L, 4051ch, 4067B)
+│ │ │ ├── install-mode.ts (c3edf10, 173L, 4399ch, 4419B)
│ │ │ ├── registry.ts (b789b99, 177L, 4593ch, 4593B)
│ │ │ ├── types.ts (de1581f, 129L, 4183ch, 4183B)
-│ │ │ └── updater.ts (0aac442, 523L, 15551ch, 15581B)
-│ │ ├── vault/ (8)
-│ │ │ ├── copy.ts (647dcc5, 226L, 6214ch, 6214B)
+│ │ │ └── updater.ts (f2cf88c, 543L, 16238ch, 16270B)
+│ │ ├── vault/ (9)
+│ │ │ ├── copy.ts (ec647c6, 275L, 8054ch, 8060B)
│ │ │ ├── events.ts (a5a6714, 58L, 1255ch, 1255B)
-│ │ │ ├── index.ts (576eed0, 12L, 320ch, 320B)
+│ │ │ ├── index.ts (d333d90, 13L, 349ch, 349B)
│ │ │ ├── key.ts (e9e19ea, 263L, 6776ch, 6776B)
-│ │ │ ├── propagate.ts (b458963, 251L, 6873ch, 6873B)
-│ │ │ ├── resolve.ts (27db674, 195L, 5094ch, 5094B)
-│ │ │ ├── storage.ts (f4fb9cf, 593L, 15952ch, 15958B)
-│ │ │ └── types.ts (dff608b, 109L, 2465ch, 2465B)
+│ │ │ ├── policy.ts (5c17eac, 69L, 2820ch, 2830B)
+│ │ │ ├── propagate.ts (8105973, 330L, 9492ch, 9496B)
+│ │ │ ├── resolve.ts (468d35c, 235L, 6365ch, 6369B)
+│ │ │ ├── storage.ts (e2932f7, 757L, 20949ch, 21205B)
+│ │ │ └── types.ts (4060169, 151L, 3679ch, 3683B)
│ │ ├── version/ (5)
│ │ │ ├── schema/ (1 file, 1 dir)
│ │ │ ├── settings/ (1 file, 1 dir)
│ │ │ ├── state/ (1 file, 1 dir)
│ │ │ ├── index.ts (8abf6c2, 302L, 7550ch, 8282B)
-│ │ │ └── types.ts (5fcc885, 253L, 6917ch, 8381B)
-│ │ ├── worker-bridge/ (6)
-│ │ │ ├── bridge.ts (28c4868, 99L, 2619ch, 2621B)
+│ │ │ └── types.ts (a54aa04, 253L, 6917ch, 8381B)
+│ │ ├── worker-bridge/ (7)
+│ │ │ ├── bridge.ts (637bd93, 147L, 4043ch, 4047B)
│ │ │ ├── index.ts (edc4028, 12L, 315ch, 315B)
-│ │ │ ├── order-buffer.ts (83665ff, 45L, 723ch, 723B)
+│ │ │ ├── order-buffer.ts (f844dd0, 68L, 1451ch, 1453B)
│ │ │ ├── paths.ts (ba286e4, 49L, 1673ch, 1677B)
+│ │ │ ├── pending-set.ts (6a304aa, 83L, 2174ch, 2178B)
│ │ │ ├── pool.ts (aea709d, 86L, 2314ch, 2314B)
│ │ │ └── types.ts (c142541, 46L, 1770ch, 1776B)
│ │ ├── environment.ts (08ebb56, 141L, 3015ch, 3017B)
│ │ ├── index.ts (f600b25, 418L, 8876ch, 8876B)
-│ │ ├── observer.ts (e59a0e5, 255L, 9794ch, 9794B)
-│ │ ├── project-init.ts (40ae219, 166L, 4688ch, 4690B)
+│ │ ├── observer.ts (4f0e741, 264L, 10076ch, 10078B)
+│ │ ├── project-init.ts (3a2c70e, 174L, 5041ch, 5043B)
│ │ ├── project.ts (18af87e, 255L, 6533ch, 6533B)
│ │ └── theme.ts (f8e8f4a, 545L, 12937ch, 14702B)
│ ├── mcp/ (3)
-│ │ ├── index.ts (d3d7f27, 26L, 852ch, 854B)
+│ │ ├── index.ts (748a9e2, 26L, 854ch, 856B)
│ │ ├── init.ts (84b9cbf, 85L, 1953ch, 1953B)
│ │ └── server.ts (7152803, 234L, 7541ch, 7545B)
│ ├── rpc/ (5)
│ │ ├── commands/ (7)
│ │ │ ├── changes.ts (49d45ad, 94L, 2627ch, 2627B)
-│ │ │ ├── config.ts (e7d7b46, 38L, 1403ch, 1403B)
+│ │ │ ├── config.ts (c4fcce2, 38L, 1407ch, 1407B)
│ │ │ ├── explore.ts (f24be95, 91L, 3239ch, 3239B)
│ │ │ ├── index.ts (1d30237, 30L, 734ch, 734B)
│ │ │ ├── query.ts (d55e353, 40L, 1456ch, 1458B)
│ │ │ ├── run.ts (2e5fb40, 57L, 1667ch, 1667B)
-│ │ │ └── session.ts (cc8dde9, 126L, 4052ch, 4056B)
+│ │ │ └── session.ts (4f1ed45, 126L, 4060ch, 4064B)
│ │ ├── index.ts (32e718c, 21L, 630ch, 630B)
│ │ ├── registry.ts (02a1571, 109L, 2511ch, 2511B)
-│ │ ├── session.ts (9af1ad3, 196L, 5146ch, 5160B)
-│ │ └── types.ts (5ce4cf7, 78L, 2041ch, 2043B)
+│ │ ├── session.ts (fb06f61, 197L, 5226ch, 5240B)
+│ │ └── types.ts (2d76387, 78L, 2043ch, 2045B)
│ ├── sdk/ (11)
│ │ ├── impersonate/ (4)
│ │ │ ├── dialect-strategy.ts (51babcc, 101L, 2765ch, 3745B)
│ │ │ ├── index.ts (6a19d4e, 7L, 233ch, 233B)
-│ │ │ ├── scope.ts (b139628, 103L, 3170ch, 3414B)
+│ │ │ ├── scope.ts (17426e0, 130L, 4210ch, 4456B)
│ │ │ └── types.ts (ace8416, 67L, 2106ch, 2594B)
│ │ ├── namespaces/ (11)
-│ │ │ ├── changes.ts (25533e3, 479L, 12747ch, 14053B)
-│ │ │ ├── db.ts (ad6ce07, 361L, 9747ch, 10843B)
+│ │ │ ├── changes.ts (d94a868, 485L, 13218ch, 14524B)
+│ │ │ ├── db.ts (2cab8af, 417L, 11800ch, 12904B)
│ │ │ ├── dt.ts (fca374d, 117L, 3169ch, 3627B)
│ │ │ ├── index.ts (9ce85c2, 10L, 454ch, 454B)
-│ │ │ ├── lock.ts (d20eaae, 149L, 3811ch, 4269B)
-│ │ │ ├── run.ts (9f3c8c6, 261L, 7963ch, 9063B)
-│ │ │ ├── secrets.ts (860ea8b, 42L, 967ch, 1213B)
-│ │ │ ├── templates.ts (da4c186, 53L, 1492ch, 1738B)
+│ │ │ ├── lock.ts (020b4b0, 164L, 4468ch, 4926B)
+│ │ │ ├── run.ts (5786d59, 285L, 9262ch, 10362B)
+│ │ │ ├── secrets.ts (d87f8c8, 113L, 2995ch, 3247B)
+│ │ │ ├── templates.ts (b92720b, 83L, 2854ch, 3102B)
│ │ │ ├── transfer.ts (182f27c, 88L, 2562ch, 2810B)
│ │ │ ├── utils.ts (a8b8dec, 65L, 1759ch, 2005B)
-│ │ │ └── vault.ts (436f71f, 407L, 10433ch, 11779B)
+│ │ │ └── vault.ts (1984452, 445L, 11887ch, 13237B)
│ │ ├── stubs/ (1)
│ │ │ └── ansis.ts (16243d6, 19L, 488ch, 490B)
-│ │ ├── context.ts (12fd24e, 535L, 16331ch, 18437B)
-│ │ ├── guards.ts (e27300d, 156L, 4421ch, 4919B)
+│ │ ├── context.ts (b4ecfa2, 575L, 18061ch, 20171B)
+│ │ ├── guards.ts (f17c08b, 156L, 4423ch, 4921B)
│ │ ├── index.ts (cb2bbba, 300L, 9189ch, 9927B)
│ │ ├── noorm-ops.ts (9b88a4d, 178L, 3939ch, 4609B)
│ │ ├── sql.ts (397c9c9, 728L, 19985ch, 21507B)
│ │ ├── state.ts (2e875de, 54L, 1779ch, 2267B)
│ │ ├── tvp.ts (05b7f0a, 168L, 4695ch, 5433B)
-│ │ └── types.ts (8d8963e, 178L, 4777ch, 5755B)
+│ │ └── types.ts (5fec5ed, 183L, 4980ch, 5958B)
│ ├── tui/ (14)
│ │ ├── components/ (10)
│ │ │ ├── dialogs/ (6 files, 0 dirs)
@@ -734,7 +708,7 @@
│ │ │ ├── useChangeProgress.ts (12b8c8d, 111L, 3230ch, 3230B)
│ │ │ ├── useConnection.ts (516f590, 229L, 6526ch, 6526B)
│ │ │ ├── useLoadGuard.ts (e4d449c, 51L, 1054ch, 1054B)
-│ │ │ ├── useLockStatus.ts (632d89f, 145L, 3655ch, 3655B)
+│ │ │ ├── useLockStatus.ts (91d7117, 145L, 3664ch, 3664B)
│ │ │ ├── useObserver.ts (e5db37d, 158L, 4195ch, 4197B)
│ │ │ ├── useRunProgress.ts (8174572, 279L, 6494ch, 6494B)
│ │ │ ├── useSecretSource.ts (2004e16, 74L, 1995ch, 1995B)
@@ -742,7 +716,7 @@
│ │ │ ├── useTransferProgress.ts (268d0bf, 583L, 14501ch, 14501B)
│ │ │ ├── useUpdateChecker.ts (eac4e68, 167L, 4040ch, 4040B)
│ │ │ ├── useVaultConnection.ts (993630d, 141L, 3774ch, 3784B)
-│ │ │ └── useVaultSecretKeys.ts (851e11d, 113L, 3532ch, 3540B)
+│ │ │ └── useVaultSecretKeys.ts (4677fc7, 165L, 4807ch, 4821B)
│ │ ├── providers/ (1)
│ │ │ └── ConnectionProvider.tsx (b4340b1, 231L, 5790ch, 5794B)
│ │ ├── screens/ (15)
@@ -762,21 +736,21 @@
│ │ │ ├── home.tsx (7f5fdf6, 635L, 21702ch, 21712B)
│ │ │ └── not-found.tsx (135b44e, 72L, 1958ch, 1958B)
│ │ ├── utils/ (13)
-│ │ │ ├── change-context.ts (88cb5c7, 70L, 2165ch, 2165B)
+│ │ │ ├── change-context.ts (962b3e2, 78L, 2735ch, 2735B)
│ │ │ ├── change-loader.ts (4ad58de, 252L, 6631ch, 6633B)
│ │ │ ├── clipboard.ts (93a7f8c, 95L, 1991ch, 1991B)
-│ │ │ ├── config-validation.ts (0d56c80, 217L, 5890ch, 5898B)
+│ │ │ ├── config-validation.ts (425dfc6, 218L, 5936ch, 5944B)
│ │ │ ├── connection.ts (e20a2e0, 82L, 2065ch, 2065B)
│ │ │ ├── date.ts (ff07926, 20L, 391ch, 391B)
│ │ │ ├── error.ts (97c198b, 33L, 736ch, 736B)
│ │ │ ├── identity.ts (a44606a, 32L, 870ch, 870B)
-│ │ │ ├── index.ts (4a375b7, 30L, 1038ch, 1038B)
+│ │ │ ├── index.ts (7054044, 30L, 1040ch, 1040B)
│ │ │ ├── paths.ts (ee0cf63, 53L, 1329ch, 1329B)
-│ │ │ ├── run-context.ts (207450d, 66L, 1951ch, 1951B)
+│ │ │ ├── run-context.ts (ea7c320, 97L, 3095ch, 3097B)
│ │ │ ├── settings-validation.ts (b155b66, 49L, 1148ch, 1152B)
│ │ │ └── string.ts (8c22fe4, 39L, 1182ch, 1182B)
-│ │ ├── app-context.tsx (061c366, 1198L, 29675ch, 30909B)
-│ │ ├── app.tsx (e0768a4, 401L, 11536ch, 11592B)
+│ │ ├── app-context.tsx (dc19f0e, 1198L, 29681ch, 30915B)
+│ │ ├── app.tsx (10f41d3, 418L, 12294ch, 12352B)
│ │ ├── focus.tsx (342fe30, 250L, 5134ch, 5134B)
│ │ ├── keyboard.tsx (5d1144b, 401L, 9159ch, 9161B)
│ │ ├── observer-context.ts (305604b, 24L, 806ch, 806B)
@@ -788,14 +762,18 @@
│ ├── compute.ts (c7b828a, 46L, 1233ch, 1233B)
│ └── connection.ts (128339f, 241L, 5955ch, 5957B)
├── tests/ (11)
-│ ├── cli/ (30)
-│ │ ├── change/ (1)
-│ │ │ └── rm.test.ts (dce3756, 167L, 5461ch, 5467B)
-│ │ ├── ci/ (4)
+│ ├── cli/ (41)
+│ │ ├── change/ (4)
+│ │ │ ├── history.test.ts (e6ab24d, 140L, 4580ch, 4586B)
+│ │ │ ├── list.test.ts (07f0afa, 158L, 5380ch, 5386B)
+│ │ │ ├── rewind.test.ts (6f0e6c2, 214L, 6888ch, 6890B)
+│ │ │ └── rm.test.ts (53c812a, 167L, 5469ch, 5475B)
+│ │ ├── ci/ (5)
+│ │ │ ├── identity-enroll-hijack.test.ts (02cf15c, 439L, 14246ch, 14266B)
│ │ │ ├── identity-enroll.test.ts (99d0cfb, 106L, 3324ch, 3332B)
│ │ │ ├── identity-new.test.ts (2462d0f, 88L, 2587ch, 2587B)
-│ │ │ ├── init.test.ts (4834a97, 192L, 5286ch, 5286B)
-│ │ │ └── secrets.test.ts (53fa24b, 216L, 6180ch, 6180B)
+│ │ │ ├── init.test.ts (7318cc4, 367L, 10048ch, 10052B)
+│ │ │ └── secrets.test.ts (8f20782, 216L, 6180ch, 6180B)
│ │ ├── components/ (7)
│ │ │ ├── DismissableAlert.test.tsx (ad0c85f, 340L, 9391ch, 9391B)
│ │ │ ├── dialogs.test.tsx (67db743, 343L, 9979ch, 9983B)
@@ -805,86 +783,116 @@
│ │ │ ├── lists.test.tsx (3807cc8, 220L, 6656ch, 6670B)
│ │ │ └── status.test.tsx (345fc65, 191L, 5245ch, 5245B)
│ │ ├── config/ (6)
-│ │ │ ├── add.test.ts (74a4ef8, 43L, 1116ch, 1126B)
-│ │ │ ├── edit.test.ts (6dbeeea, 62L, 1800ch, 1812B)
-│ │ │ ├── export.test.ts (cddd4ad, 129L, 4198ch, 4206B)
-│ │ │ ├── import.test.ts (8d92923, 169L, 5938ch, 5948B)
-│ │ │ ├── list.test.ts (745c3f4, 136L, 4211ch, 4215B)
-│ │ │ └── rm.test.ts (53d70db, 201L, 6850ch, 6850B)
-│ │ ├── db/ (4)
-│ │ │ ├── create.test.ts (e6a0e70, 201L, 7006ch, 7018B)
-│ │ │ ├── drop.test.ts (b86f15d, 178L, 5982ch, 5992B)
-│ │ │ ├── reset.test.ts (75cac14, 257L, 8678ch, 8690B)
-│ │ │ └── transfer.test.ts (3ad7c26, 155L, 5752ch, 5770B)
-│ │ ├── hooks/ (3)
+│ │ │ ├── add.test.ts (3008ad8, 62L, 1805ch, 1815B)
+│ │ │ ├── edit.test.ts (3d84874, 79L, 2337ch, 2351B)
+│ │ │ ├── export.test.ts (90787e8, 184L, 5928ch, 5938B)
+│ │ │ ├── import.test.ts (e33b089, 228L, 8140ch, 8150B)
+│ │ │ ├── list.test.ts (5a518c3, 199L, 6194ch, 6198B)
+│ │ │ └── rm.test.ts (9b736e1, 201L, 6860ch, 6860B)
+│ │ ├── db/ (7)
+│ │ │ ├── create.test.ts (2daaa99, 224L, 7835ch, 7847B)
+│ │ │ ├── drop.test.ts (e268ce9, 193L, 6504ch, 6514B)
+│ │ │ ├── explore.test.ts (c55598b, 130L, 3980ch, 3986B)
+│ │ │ ├── lifecycle-policy.test.ts (3b34390, 311L, 10133ch, 10781B)
+│ │ │ ├── reset.test.ts (08260c7, 261L, 8966ch, 8978B)
+│ │ │ ├── teardown.test.ts (65dcedd, 128L, 3931ch, 3937B)
+│ │ │ └── transfer.test.ts (e01475c, 209L, 7012ch, 7032B)
+│ │ ├── hooks/ (4)
│ │ │ ├── useObserver.test.tsx (e827049, 392L, 10127ch, 10129B)
│ │ │ ├── useTransferProgress.test.tsx (5267f2e, 322L, 10736ch, 10744B)
-│ │ │ └── useUpdateChecker.test.tsx (3073b2c, 283L, 8301ch, 8301B)
-│ │ ├── run/ (10)
+│ │ │ ├── useUpdateChecker.test.tsx (3073b2c, 283L, 8301ch, 8301B)
+│ │ │ └── useVaultSecretKeys.test.tsx (0bb45af, 294L, 8826ch, 8828B)
+│ │ ├── identity/ (2)
+│ │ │ ├── edit.test.ts (80a5af4, 171L, 5253ch, 5255B)
+│ │ │ └── init.test.ts (999f271, 182L, 5058ch, 5064B)
+│ │ ├── lock/ (2)
+│ │ │ ├── force.test.ts (ab54903, 180L, 5287ch, 5291B)
+│ │ │ └── status.test.ts (adb4506, 130L, 3990ch, 3996B)
+│ │ ├── run/ (13)
│ │ │ ├── _setup.ts (35ce510, 167L, 4749ch, 4753B)
-│ │ │ ├── build.test.ts (a658b29, 125L, 3897ch, 3909B)
+│ │ │ ├── build-unmatched-exclude.test.ts (1563076, 109L, 3256ch, 3262B)
+│ │ │ ├── build.test.ts (5e20267, 367L, 11053ch, 11067B)
│ │ │ ├── change-ff-dryrun.test.ts (963e4be, 197L, 5697ch, 5701B)
│ │ │ ├── change-ff.test.ts (58aa688, 126L, 3522ch, 3526B)
-│ │ │ ├── change-rewind.test.ts (41e0605, 109L, 3291ch, 3297B)
+│ │ │ ├── change-rewind.test.ts (c9f324a, 109L, 3291ch, 3297B)
│ │ │ ├── change-run.test.ts (049eec9, 111L, 2909ch, 2913B)
│ │ │ ├── dir.test.ts (b5724ae, 108L, 3146ch, 3152B)
│ │ │ ├── file.test.ts (bb58559, 115L, 3539ch, 3543B)
│ │ │ ├── files.test.ts (b4ef236, 182L, 5193ch, 5201B)
+│ │ │ ├── preview-inspect-policy.test.ts (e14cc87, 123L, 4173ch, 4181B)
+│ │ │ ├── preview-inspect-vault-probe.test.ts (4be2724, 154L, 5466ch, 5470B)
│ │ │ └── sql.test.ts (29d578f, 123L, 3370ch, 3372B)
-│ │ ├── screens/ (2)
+│ │ ├── screens/ (4)
+│ │ │ ├── change/ (1 file, 0 dirs)
│ │ │ ├── config/ (2 files, 0 dirs)
+│ │ │ ├── db/ (2 files, 0 dirs)
│ │ │ └── init/ (4 files, 0 dirs)
+│ │ ├── sql/ (1)
+│ │ │ └── history-config.test.ts (9960369, 213L, 7053ch, 7065B)
+│ │ ├── utils/ (1)
+│ │ │ └── change-context.test.ts (9740ac3, 234L, 7678ch, 7678B)
+│ │ ├── VaultScreen.test.tsx (06317d5, 280L, 8956ch, 8958B)
+│ │ ├── agent-channel-escalation.test.ts (c445315, 286L, 8469ch, 8477B)
│ │ ├── app-context.test.tsx (e15ad92, 628L, 16709ch, 16711B)
│ │ ├── app.test.tsx (3a6f3d3, 241L, 6170ch, 6181B)
-│ │ ├── change-edit.test.ts (8125b19, 103L, 2985ch, 2985B)
+│ │ ├── change-edit.test.ts (295ec3a, 103L, 2985ch, 2985B)
│ │ ├── change-prompts.test.ts (9be33db, 107L, 2822ch, 2822B)
│ │ ├── citty-args.ts (f0b91d5, 27L, 816ch, 816B)
│ │ ├── citty-help.test.ts (6b20ddc, 102L, 2787ch, 2787B)
-│ │ ├── config-validation.test.ts (54338bb, 101L, 2761ch, 2761B)
+│ │ ├── cli-logger-settings.test.ts (ff0ba36, 180L, 6105ch, 6109B)
+│ │ ├── config-validation.test.ts (4218192, 101L, 2781ch, 2781B)
│ │ ├── debug-pid.test.tsx (d07de2e, 11L, 188ch, 188B)
│ │ ├── env-bootstrap.test.ts (048bf9c, 75L, 2314ch, 2314B)
+│ │ ├── exit-codes.test.ts (19c705c, 250L, 8496ch, 8504B)
│ │ ├── focus.test.tsx (77608ce, 662L, 15713ch, 15713B)
-│ │ ├── init.test.ts (abbfba0, 124L, 3924ch, 3926B)
+│ │ ├── global-flags.test.ts (4310f6f, 221L, 6996ch, 7000B)
+│ │ ├── init.test.ts (b721435, 124L, 3924ch, 3926B)
│ │ ├── insecure-flag.test.ts (bdb8f17, 104L, 2560ch, 2562B)
+│ │ ├── json-envelope.test.ts (9453611, 216L, 6200ch, 6212B)
│ │ ├── keyboard.test.tsx (83d53c1, 573L, 14942ch, 14942B)
│ │ ├── lazy-startup.test.ts (f8758c9, 143L, 4488ch, 4488B)
+│ │ ├── postinstall-source-checkout.test.ts (70f941e, 195L, 6214ch, 6214B)
│ │ ├── router.test.tsx (952530c, 489L, 13626ch, 13626B)
│ │ ├── screens.test.tsx (6edec68, 277L, 7530ch, 7530B)
-│ │ ├── settings-edit.test.ts (a5ebc82, 56L, 1416ch, 1416B)
-│ │ ├── settings-secret.test.ts (8a6f206, 53L, 1365ch, 1365B)
+│ │ ├── settings-edit.test.ts (671529f, 56L, 1416ch, 1416B)
+│ │ ├── settings-secret.test.ts (6236925, 53L, 1365ch, 1365B)
│ │ ├── settings-validation.test.ts (d64e9b7, 34L, 903ch, 903B)
-│ │ ├── sql-repl.test.ts (37b9c5a, 32L, 864ch, 864B)
+│ │ ├── sql-repl.test.ts (661771f, 32L, 864ch, 864B)
│ │ ├── types.test.ts (c9a86fb, 136L, 4786ch, 4786B)
-│ │ └── yes-flag.test.ts (768cd0f, 476L, 12490ch, 12492B)
-│ ├── core/ (28)
+│ │ └── yes-flag.test.ts (7eef63b, 476L, 12490ch, 12492B)
+│ ├── core/ (30)
│ │ ├── change/ (9)
│ │ │ ├── fixtures/ (0 files, 3 dirs)
-│ │ │ ├── executor-retry.test.ts (4ebda49, 287L, 11169ch, 11181B)
-│ │ │ ├── executor.test.ts (0364934, 388L, 14082ch, 14086B)
-│ │ │ ├── history.test.ts (de402f0, 176L, 5072ch, 5076B)
-│ │ │ ├── manager.test.ts (44983e1, 388L, 14201ch, 14205B)
-│ │ │ ├── parser.test.ts (3ce9b9e, 394L, 12279ch, 12279B)
-│ │ │ ├── scaffold.test.ts (c8b3e10, 364L, 10559ch, 10559B)
+│ │ │ ├── executor-retry.test.ts (3725104, 287L, 11171ch, 11183B)
+│ │ │ ├── executor.test.ts (b202821, 388L, 14090ch, 14094B)
+│ │ │ ├── history.test.ts (9b7b5dd, 202L, 6278ch, 6282B)
+│ │ │ ├── manager.test.ts (3ab8020, 704L, 25970ch, 25980B)
+│ │ │ ├── parser.test.ts (a2896bd, 397L, 12524ch, 12526B)
+│ │ │ ├── scaffold.test.ts (011896d, 468L, 15139ch, 15139B)
│ │ │ ├── tracker.test.ts (ab9c75c, 181L, 6273ch, 6273B)
│ │ │ └── types.test.ts (6dac816, 150L, 4669ch, 4669B)
-│ │ ├── config/ (5)
-│ │ │ ├── debug-process.test.ts (48b3b6d, 12L, 300ch, 300B)
-│ │ │ ├── env.test.ts (8677581, 496L, 13317ch, 13317B)
-│ │ │ ├── resolver.test.ts (f440ae4, 830L, 24768ch, 24770B)
-│ │ │ ├── schema.test.ts (6380dec, 505L, 14744ch, 14745B)
-│ │ │ └── validate.test.ts (6e59573, 72L, 2253ch, 2253B)
-│ │ ├── connection/ (3)
+│ │ ├── config/ (4)
+│ │ │ ├── env.test.ts (ff2bbba, 501L, 13605ch, 13605B)
+│ │ │ ├── resolver.test.ts (932661e, 830L, 24807ch, 24809B)
+│ │ │ ├── schema.test.ts (3c705a8, 505L, 14798ch, 14799B)
+│ │ │ └── validate.test.ts (b38e5f9, 72L, 2255ch, 2255B)
+│ │ ├── connection/ (4)
+│ │ │ ├── dialects/ (1 file, 0 dirs)
│ │ │ ├── defaults.test.ts (9203348, 50L, 1640ch, 1642B)
-│ │ │ ├── factory.test.ts (d4ef387, 140L, 3916ch, 3916B)
-│ │ │ └── manager.test.ts (7620812, 192L, 5447ch, 5447B)
-│ │ ├── db/ (1)
-│ │ │ └── dialects/ (3 files, 0 dirs)
-│ │ ├── dt/ (13)
+│ │ │ ├── factory.test.ts (dea2d3f, 305L, 9902ch, 10120B)
+│ │ │ └── manager.test.ts (fbf9968, 192L, 5449ch, 5449B)
+│ │ ├── db/ (2)
+│ │ │ ├── dialects/ (3 files, 0 dirs)
+│ │ │ └── operations.test.ts (7491818, 181L, 5705ch, 6133B)
+│ │ ├── debug/ (1)
+│ │ │ └── operations.test.ts (b8472ca, 621L, 19396ch, 19890B)
+│ │ ├── dt/ (14)
│ │ │ ├── crypto.test.ts (172197f, 241L, 7186ch, 7186B)
│ │ │ ├── deserialize.test.ts (f86c304, 309L, 10375ch, 10375B)
+│ │ │ ├── hostile.test.ts (2a92381, 300L, 9376ch, 9378B)
│ │ │ ├── integration.test.ts (c949c93, 800L, 27005ch, 27031B)
│ │ │ ├── modify.test.ts (aaebadd, 930L, 32501ch, 32515B)
-│ │ │ ├── paths.test.ts (34268ed, 135L, 3543ch, 3543B)
+│ │ │ ├── paths.test.ts (ed657b8, 176L, 4839ch, 4841B)
│ │ │ ├── reader.test.ts (fabb685, 285L, 7821ch, 7821B)
│ │ │ ├── roundtrip.test.ts (95212dc, 418L, 13038ch, 13050B)
│ │ │ ├── schema.test.ts (1add531, 107L, 3266ch, 3270B)
@@ -893,112 +901,150 @@
│ │ │ ├── type-map.test.ts (0d0c9d7, 508L, 15680ch, 15680B)
│ │ │ ├── worker-pipeline.test.ts (39f13d6, 323L, 10611ch, 10619B)
│ │ │ └── writer.test.ts (c97dc9e, 221L, 6635ch, 6635B)
-│ │ ├── explore/ (2)
+│ │ ├── explore/ (4)
│ │ │ ├── dialects/ (4 files, 0 dirs)
-│ │ │ └── operations.test.ts (64e9a5f, 589L, 16355ch, 16371B)
-│ │ ├── identity/ (8)
-│ │ │ ├── crypto.test.ts (8801b11, 311L, 8701ch, 8701B)
+│ │ │ ├── dispatch.test.ts (a76df89, 255L, 8761ch, 8761B)
+│ │ │ ├── operations.test.ts (64e9a5f, 589L, 16355ch, 16371B)
+│ │ │ └── recording-db.ts (cca9453, 233L, 6106ch, 6108B)
+│ │ ├── identity/ (10)
+│ │ │ ├── crypto.test.ts (03e046a, 401L, 11922ch, 11926B)
│ │ │ ├── env.test.ts (a4f0c52, 146L, 4559ch, 4561B)
│ │ │ ├── factory.test.ts (61019f8, 348L, 10115ch, 10115B)
│ │ │ ├── hash.test.ts (4ccae4e, 226L, 5558ch, 5584B)
+│ │ │ ├── key-file-corruption.test.ts (9c7b0e8, 152L, 4390ch, 4394B)
│ │ │ ├── overrides.test.ts (1a4a1c5, 100L, 2374ch, 2374B)
-│ │ │ ├── resolver.test.ts (f034831, 442L, 12034ch, 12074B)
+│ │ │ ├── provenance.test.ts (ba3fd3d, 101L, 4326ch, 4332B)
+│ │ │ ├── resolver.test.ts (e483b1d, 532L, 14725ch, 14767B)
│ │ │ ├── storage-key-permission-guard.test.ts (fa15ec7, 85L, 2584ch, 2588B)
-│ │ │ └── storage.test.ts (3de274e, 224L, 6004ch, 6004B)
+│ │ │ └── storage.test.ts (c1c1914, 273L, 7270ch, 7270B)
│ │ ├── lifecycle/ (3)
│ │ │ ├── handlers.test.ts (172ef91, 326L, 8624ch, 8624B)
│ │ │ ├── manager.test.ts (2f7ce32, 705L, 17663ch, 17663B)
│ │ │ └── types.test.ts (ddcd4ce, 82L, 1879ch, 1879B)
-│ │ ├── lock/ (3)
+│ │ ├── lock/ (7)
+│ │ │ ├── contention.test.ts (9d84380, 270L, 8426ch, 8434B)
│ │ │ ├── errors.test.ts (14e1d59, 215L, 5889ch, 5889B)
-│ │ │ ├── manager.test.ts (37f122b, 581L, 16221ch, 16221B)
+│ │ │ ├── force-policy.test.ts (85b750e, 174L, 5473ch, 5481B)
+│ │ │ ├── manager.test.ts (016bd2b, 596L, 16799ch, 16799B)
+│ │ │ ├── race-worker.ts (4bef062, 65L, 2511ch, 2515B)
+│ │ │ ├── timezone.test.ts (27fd96e, 285L, 9721ch, 9735B)
│ │ │ └── types.test.ts (2bdf84e, 48L, 1238ch, 1238B)
-│ │ ├── logger/ (9)
+│ │ ├── logger/ (11)
│ │ │ ├── classifier.test.ts (3eb2232, 188L, 5356ch, 5356B)
│ │ │ ├── formatter.test.ts (701d119, 316L, 8877ch, 8877B)
│ │ │ ├── logger.test.ts (00e1420, 512L, 13305ch, 13305B)
-│ │ │ ├── output.test.ts (a17b46f, 371L, 11119ch, 11119B)
+│ │ │ ├── output.test.ts (c6fc922, 400L, 12292ch, 12294B)
│ │ │ ├── queue.test.ts (2e16bbf, 295L, 6804ch, 6804B)
│ │ │ ├── reader.test.ts (f361ff0, 439L, 14445ch, 14445B)
+│ │ │ ├── redact-coverage.test.ts (236e4ce, 159L, 4906ch, 4912B)
│ │ │ ├── redact.test.ts (026b645, 554L, 15482ch, 15482B)
+│ │ │ ├── rotation-reopen.test.ts (47a049f, 176L, 5319ch, 5321B)
│ │ │ ├── rotation.test.ts (2aad64b, 360L, 9943ch, 9943B)
│ │ │ └── timestamp.test.ts (4026342, 72L, 2286ch, 2286B)
│ │ ├── mcp/ (2)
│ │ │ ├── init.test.ts (9cb2913, 70L, 2197ch, 2197B)
-│ │ │ └── server.test.ts (800a66e, 571L, 16481ch, 16887B)
-│ │ ├── policy/ (3)
-│ │ │ ├── check.test.ts (c8ef6c0, 323L, 9827ch, 9827B)
-│ │ │ ├── classify.test.ts (4006bc7, 446L, 13890ch, 13902B)
-│ │ │ └── visibility.test.ts (2d5f6c6, 38L, 1052ch, 1052B)
+│ │ │ └── server.test.ts (69db42d, 575L, 16705ch, 17113B)
+│ │ ├── policy/ (7)
+│ │ │ ├── agent-escalation.test.ts (12e206f, 162L, 5487ch, 5491B)
+│ │ │ ├── channel.test.ts (bed265e, 104L, 3617ch, 3621B)
+│ │ │ ├── check.test.ts (16b0f2c, 339L, 10668ch, 10670B)
+│ │ │ ├── classify-corpus.test.ts (9c8a781, 640L, 21512ch, 21548B)
+│ │ │ ├── classify.test.ts (f8935f8, 498L, 16079ch, 16095B)
+│ │ │ ├── default-access.test.ts (455d62a, 205L, 6159ch, 6167B)
+│ │ │ └── visibility.test.ts (e3bd4e3, 38L, 1072ch, 1072B)
│ │ ├── rpc/ (8)
-│ │ │ ├── commands.test.ts (a10d74f, 480L, 16121ch, 16125B)
-│ │ │ ├── list-configs.test.ts (5ba9331, 115L, 3592ch, 3598B)
+│ │ │ ├── commands.test.ts (de83e0d, 480L, 16149ch, 16153B)
+│ │ │ ├── list-configs.test.ts (3d66134, 115L, 3604ch, 3610B)
│ │ │ ├── permissions.test.ts (82cb5d4, 58L, 1739ch, 1741B)
│ │ │ ├── registry-integration.test.ts (a704a85, 231L, 6183ch, 6189B)
│ │ │ ├── registry.test.ts (f674271, 109L, 3041ch, 3041B)
-│ │ │ ├── session-not-found.test.ts (f1df070, 76L, 2649ch, 2651B)
-│ │ │ ├── session-status.test.ts (1e74c6a, 230L, 7438ch, 7440B)
-│ │ │ └── session.test.ts (2fe1518, 194L, 5712ch, 5712B)
-│ │ ├── runner/ (4)
+│ │ │ ├── session-not-found.test.ts (1e7939f, 76L, 2663ch, 2665B)
+│ │ │ ├── session-status.test.ts (a9a1f86, 230L, 7452ch, 7454B)
+│ │ │ └── session.test.ts (196c2e2, 194L, 5760ch, 5760B)
+│ │ ├── runner/ (10)
│ │ │ ├── fixtures/ (6 files, 0 dirs)
│ │ │ ├── checksum.test.ts (16c5853, 151L, 4092ch, 4092B)
-│ │ │ ├── mssql-batches.test.ts (3587bb5, 285L, 8091ch, 8097B)
-│ │ │ └── runner.test.ts (fea8194, 201L, 6199ch, 6201B)
+│ │ │ ├── dry-run-output.test.ts (5441bc4, 101L, 3456ch, 3458B)
+│ │ │ ├── execute-files.test.ts (0eb62b0, 111L, 3835ch, 3837B)
+│ │ │ ├── mssql-batches.test.ts (13579fd, 285L, 8093ch, 8099B)
+│ │ │ ├── runner.test.ts (9a75b44, 248L, 7884ch, 7886B)
+│ │ │ ├── sqlite-multi-statement.test.ts (ae75da5, 194L, 6351ch, 6355B)
+│ │ │ ├── sqlite-statements.test.ts (6e299e0, 144L, 4338ch, 4340B)
+│ │ │ ├── template-dedup.test.ts (434e39a, 165L, 5880ch, 5888B)
+│ │ │ └── tracker.test.ts (9098589, 382L, 14862ch, 14870B)
+│ │ ├── secrets/ (1)
+│ │ │ └── leakage.test.ts (e06aa00, 174L, 5317ch, 5321B)
│ │ ├── settings/ (5)
-│ │ │ ├── env-override.test.ts (a3300c6, 119L, 3276ch, 3276B)
-│ │ │ ├── manager.test.ts (3b80659, 1122L, 25257ch, 25257B)
-│ │ │ ├── rules.test.ts (cf7cfe6, 416L, 13281ch, 13281B)
+│ │ │ ├── env-override.test.ts (70e079d, 240L, 7571ch, 7573B)
+│ │ │ ├── manager.test.ts (0801cec, 1122L, 25261ch, 25261B)
+│ │ │ ├── rules.test.ts (dfc4b91, 416L, 13289ch, 13289B)
│ │ │ ├── schema.test.ts (5e7baac, 517L, 13184ch, 13184B)
│ │ │ └── setTeardown.test.ts (c2d7f2b, 84L, 2603ch, 2603B)
-│ │ ├── shared/ (2)
+│ │ ├── shared/ (4)
│ │ │ ├── errors.test.ts (e78da77, 279L, 7975ch, 9571B)
+│ │ │ ├── files.test.ts (cad503b, 233L, 6495ch, 6497B)
+│ │ │ ├── operation-id.test.ts (dd60e8d, 357L, 12035ch, 12037B)
│ │ │ └── tables.test.ts (b53c90b, 110L, 3272ch, 3272B)
│ │ ├── sql-terminal/ (2)
-│ │ │ ├── executor.test.ts (674c979, 480L, 16869ch, 16875B)
-│ │ │ └── history.test.ts (0db8ae6, 1013L, 34309ch, 34309B)
-│ │ ├── state/ (2)
+│ │ │ ├── executor.test.ts (567ba1f, 480L, 16875ch, 16881B)
+│ │ │ └── history.test.ts (caa6399, 1071L, 36485ch, 36485B)
+│ │ ├── state/ (7)
│ │ │ ├── encryption/ (1 file, 0 dirs)
-│ │ │ └── manager.test.ts (93ec167, 1015L, 33746ch, 35700B)
+│ │ │ ├── fixtures/ (1 file, 0 dirs)
+│ │ │ ├── access.test.ts (de9f179, 128L, 4410ch, 4412B)
+│ │ │ ├── durability.test.ts (396279c, 445L, 14751ch, 15733B)
+│ │ │ ├── manager.test.ts (20b1f8d, 1141L, 38399ch, 40353B)
+│ │ │ ├── merge.test.ts (5cb6c34, 172L, 4838ch, 4840B)
+│ │ │ └── migrations.test.ts (d4d9816, 147L, 4690ch, 4690B)
│ │ ├── teardown/ (2)
│ │ │ ├── dialects/ (4 files, 0 dirs)
-│ │ │ └── operations.test.ts (89b712f, 904L, 31368ch, 33364B)
-│ │ ├── template/ (6)
+│ │ │ └── operations.test.ts (c28fbea, 1088L, 38138ch, 40630B)
+│ │ ├── template/ (8)
│ │ │ ├── fixtures/ (0 files, 4 dirs)
-│ │ │ ├── engine.test.ts (f3f1e18, 388L, 11697ch, 11697B)
+│ │ │ ├── context.test.ts (e77b083, 93L, 3444ch, 3448B)
+│ │ │ ├── engine.test.ts (5bfd5f9, 403L, 12250ch, 12250B)
│ │ │ ├── helpers.test.ts (4d0a204, 130L, 3944ch, 3944B)
│ │ │ ├── loaders.test.ts (115cc74, 257L, 6369ch, 6369B)
-│ │ │ ├── security.test.ts (82855d4, 237L, 7609ch, 7609B)
-│ │ │ └── utils.test.ts (d54a7a5, 165L, 3442ch, 3442B)
-│ │ ├── transfer/ (7)
+│ │ │ ├── script-sidecars.test.ts (b17b659, 130L, 4339ch, 4347B)
+│ │ │ ├── security.test.ts (b0b47a6, 285L, 9507ch, 9509B)
+│ │ │ └── utils.test.ts (8967c7c, 174L, 3814ch, 3816B)
+│ │ ├── transfer/ (9)
│ │ │ ├── dialects/ (4 files, 0 dirs)
+│ │ │ ├── counts.test.ts (900d9b1, 187L, 6691ch, 6695B)
│ │ │ ├── events.test.ts (ab6d02e, 364L, 10614ch, 10614B)
│ │ │ ├── executor.test.ts (602b084, 420L, 13400ch, 13400B)
│ │ │ ├── fk-restore.test.ts (abd4f41, 162L, 4912ch, 5164B)
+│ │ │ ├── pagination.test.ts (3be0894, 266L, 8700ch, 8704B)
│ │ │ ├── planner.test.ts (a81c2b6, 341L, 10240ch, 10240B)
-│ │ │ ├── policy-gate.test.ts (d7abf8f, 60L, 2425ch, 2429B)
-│ │ │ └── same-server.test.ts (cf2bf8d, 369L, 10550ch, 10550B)
-│ │ ├── update/ (5)
+│ │ │ ├── policy-gate.test.ts (766a89d, 89L, 3733ch, 3739B)
+│ │ │ └── same-server.test.ts (8a0a6f0, 363L, 10660ch, 10662B)
+│ │ ├── update/ (6)
│ │ │ ├── checker.test.ts (4db209f, 191L, 5313ch, 5313B)
│ │ │ ├── checksum.test.ts (ef4d8bd, 264L, 7720ch, 7732B)
│ │ │ ├── global-settings.test.ts (1190fee, 231L, 6358ch, 6358B)
│ │ │ ├── registry.test.ts (6f2fad2, 214L, 5987ch, 5987B)
-│ │ │ └── updater.test.ts (a8edc3f, 324L, 10516ch, 10536B)
-│ │ ├── vault/ (3)
+│ │ │ ├── updater.test.ts (a8edc3f, 324L, 10516ch, 10536B)
+│ │ │ └── version-validation.test.ts (1688271, 120L, 3912ch, 3912B)
+│ │ ├── vault/ (6)
│ │ │ ├── idempotent-init.test.ts (a903fc3, 236L, 6218ch, 6218B)
│ │ │ ├── key.test.ts (1b2034e, 188L, 5742ch, 5746B)
+│ │ │ ├── policy-gate.test.ts (876acdc, 241L, 7371ch, 7375B)
+│ │ │ ├── propagate.test.ts (99f8ddc, 203L, 6704ch, 6710B)
+│ │ │ ├── resolve.test.ts (d89a554, 87L, 2922ch, 2928B)
│ │ │ └── storage.test.ts (6ce7799, 317L, 10648ch, 10650B)
│ │ ├── version/ (5)
│ │ │ ├── manager.test.ts (8ff56f8, 319L, 11020ch, 11020B)
│ │ │ ├── schema.test.ts (9fc95bd, 658L, 20341ch, 20341B)
│ │ │ ├── settings.test.ts (4681701, 294L, 7728ch, 7728B)
-│ │ │ ├── state.test.ts (28be6b2, 474L, 13484ch, 13484B)
-│ │ │ └── types.test.ts (5f0f71f, 125L, 3021ch, 3021B)
-│ │ ├── worker-bridge/ (3)
-│ │ │ ├── bridge.test.ts (4ee1750, 54L, 1440ch, 1440B)
-│ │ │ ├── order-buffer.test.ts (182cbed, 85L, 1988ch, 1992B)
+│ │ │ ├── state.test.ts (a0778b0, 675L, 21855ch, 21861B)
+│ │ │ └── types.test.ts (bdc30e3, 125L, 3021ch, 3021B)
+│ │ ├── worker-bridge/ (4)
+│ │ │ ├── bridge.test.ts (b1bbb3e, 101L, 3085ch, 3085B)
+│ │ │ ├── order-buffer.test.ts (f1728be, 130L, 3179ch, 3183B)
+│ │ │ ├── pending-set.test.ts (9dc36b2, 100L, 2238ch, 2240B)
│ │ │ └── pool.test.ts (f2754a9, 64L, 1798ch, 1798B)
│ │ ├── environment.test.ts (958bc19, 142L, 3767ch, 3769B)
-│ │ ├── project-init.test.ts (48fa3fe, 75L, 2387ch, 2387B)
+│ │ ├── project-init.test.ts (4437e25, 123L, 4046ch, 4050B)
│ │ └── project.test.ts (cd6cc41, 243L, 6575ch, 6575B)
│ ├── fixtures/ (3)
│ │ ├── ci/ (2)
@@ -1010,38 +1056,48 @@
│ │ │ ├── mysql/ (4 files, 0 dirs)
│ │ │ ├── postgres/ (5 files, 0 dirs)
│ │ │ └── sqlite/ (3 files, 0 dirs)
-│ │ └── workers/ (2)
+│ │ └── workers/ (4)
│ │ ├── adder.ts (3243e61, 15L, 443ch, 443B)
-│ │ └── echo.ts (e2e3fe3, 12L, 413ch, 413B)
-│ ├── integration/ (11)
-│ │ ├── change/ (1)
-│ │ │ └── postgres-transaction.test.ts (92fde1d, 285L, 9555ch, 9565B)
+│ │ ├── dying.ts (1d8824e, 11L, 296ch, 296B)
+│ │ ├── echo.ts (e2e3fe3, 12L, 413ch, 413B)
+│ │ └── silent.ts (77e9168, 7L, 276ch, 278B)
+│ ├── integration/ (12)
+│ │ ├── change/ (3)
+│ │ │ ├── history-dialects.test.ts (99fb7a6, 179L, 6550ch, 6556B)
+│ │ │ ├── mysql-lifecycle.test.ts (b8c508f, 178L, 5956ch, 5960B)
+│ │ │ └── postgres-transaction.test.ts (abb5d62, 285L, 9557ch, 9567B)
│ │ ├── cli/ (3)
-│ │ │ ├── db.test.ts (3f4283c, 435L, 11442ch, 12906B)
-│ │ │ ├── lock.test.ts (52b726a, 302L, 8178ch, 9398B)
+│ │ │ ├── db.test.ts (87a6c15, 447L, 12079ch, 13545B)
+│ │ │ ├── lock.test.ts (8ea85ed, 303L, 8252ch, 9472B)
│ │ │ └── setup.ts (9e503fb, 450L, 11325ch, 12301B)
+│ │ ├── connection/ (1)
+│ │ │ └── mssql-sni.test.ts (d453b28, 99L, 2780ch, 2782B)
│ │ ├── error-diagnostics/ (2)
│ │ │ ├── mssql.test.ts (38889e2, 178L, 5706ch, 5706B)
│ │ │ └── postgres.test.ts (2044f49, 103L, 3005ch, 3005B)
-│ │ ├── explore/ (5)
+│ │ ├── explore/ (6)
│ │ │ ├── edge-cases.test.ts (a99161b, 524L, 15926ch, 15926B)
│ │ │ ├── mssql.test.ts (7c4a061, 566L, 18515ch, 18515B)
+│ │ │ ├── multi-schema.test.ts (daa43b0, 463L, 14724ch, 14731B)
│ │ │ ├── mysql.test.ts (ad2a8d7, 471L, 14059ch, 14059B)
│ │ │ ├── postgres.test.ts (b4c311b, 444L, 14795ch, 14795B)
│ │ │ └── sqlite.test.ts (2b44515, 426L, 13079ch, 13079B)
│ │ ├── impersonate/ (2)
│ │ │ ├── mssql.test.ts (36695d6, 114L, 3625ch, 3629B)
-│ │ │ └── postgres.test.ts (1638202, 113L, 3593ch, 3595B)
-│ │ ├── runner/ (1)
-│ │ │ └── mssql-batches.test.ts (395ef9c, 268L, 8059ch, 8061B)
-│ │ ├── sdk/ (6)
-│ │ │ ├── db-reset.test.ts (99bb07e, 115L, 3607ch, 3611B)
+│ │ │ └── postgres.test.ts (0c9bdd6, 313L, 10055ch, 10065B)
+│ │ ├── runner/ (2)
+│ │ │ ├── mssql-batches.test.ts (b60b9e8, 268L, 8063ch, 8065B)
+│ │ │ └── tracker-dialects.test.ts (377c0de, 159L, 5889ch, 5899B)
+│ │ ├── sdk/ (7)
+│ │ │ ├── db-reset.test.ts (ddfaa6e, 119L, 3933ch, 3939B)
│ │ │ ├── dt-namespace.test.ts (3dee923, 146L, 6011ch, 6513B)
+│ │ │ ├── run-vault-secrets.test.ts (14d3073, 296L, 10407ch, 10905B)
│ │ │ ├── transfer-namespace.test.ts (cfe27cd, 180L, 6121ch, 6867B)
│ │ │ ├── tvf.test.ts (730359b, 279L, 7502ch, 8234B)
│ │ │ ├── tvp.test.ts (61c50ec, 599L, 17433ch, 19149B)
│ │ │ └── vault-namespace.test.ts (e7dffc5, 291L, 9810ch, 10312B)
-│ │ ├── sql-terminal/ (4)
+│ │ ├── sql-terminal/ (5)
+│ │ │ ├── classifier-differential.test.ts (8419403, 226L, 9384ch, 9392B)
│ │ │ ├── mssql.test.ts (1c242a3, 776L, 23168ch, 23168B)
│ │ │ ├── mysql.test.ts (19a7a01, 658L, 18773ch, 18773B)
│ │ │ ├── postgres.test.ts (a32d0a8, 942L, 29279ch, 29279B)
@@ -1050,58 +1106,62 @@
│ │ │ ├── mssql.test.ts (c5dae52, 693L, 24835ch, 25335B)
│ │ │ ├── mysql.test.ts (730f424, 400L, 12896ch, 12896B)
│ │ │ ├── postgres.test.ts (399902f, 453L, 16737ch, 16981B)
-│ │ │ ├── sdk-preserve.test.ts (8d2711e, 177L, 5673ch, 6601B)
+│ │ │ ├── sdk-preserve.test.ts (68854fd, 181L, 5907ch, 6835B)
│ │ │ └── sqlite.test.ts (04aa3c8, 396L, 13541ch, 13541B)
-│ │ ├── transfer/ (3)
+│ │ ├── transfer/ (4)
+│ │ │ ├── cross-dialect.test.ts (68d1480, 156L, 5713ch, 5717B)
│ │ │ ├── mssql.test.ts (b07cd1e, 364L, 12067ch, 12069B)
│ │ │ ├── mysql.test.ts (3ad6515, 383L, 12859ch, 12861B)
│ │ │ └── postgres.test.ts (449c11f, 371L, 12407ch, 12409B)
│ │ └── version/ (1)
│ │ └── schema.test.ts (e1c16d7, 727L, 22493ch, 23469B)
-│ ├── sdk/ (13)
+│ ├── sdk/ (15)
│ │ ├── impersonate/ (3)
│ │ │ ├── dialect-strategy.test.ts (d630381, 146L, 3515ch, 4491B)
-│ │ │ ├── impersonate.test.ts (339dd61, 297L, 7627ch, 8603B)
-│ │ │ └── scope.test.ts (b9d359a, 122L, 3310ch, 3798B)
-│ │ ├── bundle-smoke.test.ts (e141dfa, 343L, 9619ch, 11817B)
-│ │ ├── context.test.ts (341f429, 597L, 18688ch, 19432B)
-│ │ ├── db-namespace.test.ts (fe640c4, 290L, 8715ch, 9849B)
-│ │ ├── destructive-ops.test.ts (9c2dc97, 642L, 21461ch, 24521B)
+│ │ │ ├── impersonate.test.ts (9c11613, 297L, 7629ch, 8605B)
+│ │ │ └── scope.test.ts (e0c5ddc, 188L, 5387ch, 5877B)
+│ │ ├── bundle-smoke.test.ts (9c4d916, 343L, 9623ch, 11821B)
+│ │ ├── context.test.ts (bbd9699, 597L, 18690ch, 19434B)
+│ │ ├── db-namespace.test.ts (1f16e98, 293L, 8954ch, 10088B)
+│ │ ├── destructive-ops.test.ts (f28c651, 659L, 22161ch, 25223B)
│ │ ├── dts-surface.test.ts (7e8403b, 74L, 2344ch, 3080B)
-│ │ ├── guards.test.ts (d609680, 377L, 12339ch, 12341B)
+│ │ ├── guards.test.ts (d37affa, 377L, 12379ch, 12381B)
│ │ ├── lifecycle.test.ts (371af51, 126L, 3617ch, 4353B)
-│ │ ├── noorm-ops.test.ts (4cbb4ce, 338L, 9250ch, 10666B)
-│ │ ├── run-build-filtering.test.ts (8444e13, 374L, 13147ch, 13663B)
+│ │ ├── noorm-ops.test.ts (9a800e1, 345L, 9648ch, 11064B)
+│ │ ├── render-vault-tier.test.ts (df82adb, 142L, 4738ch, 5230B)
+│ │ ├── run-build-filtering.test.ts (9b4fc21, 407L, 14317ch, 14833B)
│ │ ├── sql.test.ts (959c16c, 1035L, 32675ch, 34387B)
-│ │ ├── transfer-dt-namespace.test.ts (8962723, 120L, 3859ch, 4349B)
-│ │ └── vault-namespace.test.ts (4505539, 368L, 11446ch, 11452B)
+│ │ ├── templates-policy.test.ts (7d94491, 68L, 2329ch, 2333B)
+│ │ ├── transfer-dt-namespace.test.ts (fb55bb4, 120L, 3861ch, 4351B)
+│ │ └── vault-namespace.test.ts (83e76f5, 368L, 11448ch, 11454B)
│ ├── utils/ (4)
│ │ ├── db-guard.test.ts (677fa3e, 143L, 3999ch, 4001B)
│ │ ├── db-splitter.test.ts (4db513c, 280L, 8506ch, 8506B)
-│ │ ├── db.ts (757d071, 926L, 26530ch, 26546B)
+│ │ ├── db.ts (f42c64a, 926L, 26532ch, 26548B)
│ │ └── mssql-batches.test.ts (551221c, 270L, 7309ch, 7313B)
│ ├── workers/ (2)
│ │ ├── compute.test.ts (66855f3, 72L, 2003ch, 2003B)
│ │ └── connection.test.ts (249b2cc, 77L, 2242ch, 2242B)
│ ├── global-setup.ts (15aaeca, 97L, 2596ch, 2603B)
│ ├── global-teardown.ts (1cce0b3, 39L, 936ch, 939B)
-│ ├── preload.ts (86e4ebc, 100L, 2645ch, 2649B)
+│ ├── preload.ts (f3f633a, 119L, 3533ch, 3541B)
│ └── sample.env (fd7767a, 26L, 664ch, 664B)
├── .gitignore (e2ac795, 49L, 516ch, 516B)
├── .npmrc (60376c8, 1L, 36ch, 36B)
├── .prettierignore (e3b0c44, 0L, 0ch, 0B)
├── .signalsignore (b0287a5, 17L, 662ch, 674B)
-├── CLAUDE.md (2cf97c7, 203L, 8579ch, 8785B)
+├── CLAUDE.md (e403148, 216L, 10135ch, 10355B)
├── CNAME (f3bed50, 1L, 9ch, 9B)
├── LICENSE (42eaf96, 21L, 1070ch, 1070B)
-├── README.md (6bbc797, 84L, 2482ch, 2502B)
+├── README.md (b21eeb1, 95L, 2818ch, 2840B)
├── TODO.md (afd3ad2, 284L, 17713ch, 17967B)
+├── audit.db (4b3a5f2, 9L, 57344ch, 57344B)
├── bun.lockb (ca7f592, 449L, 263701ch, 265728B)
├── bunfig.toml (dab752d, 5L, 89ch, 89B)
├── docker-compose.test.yml (d2d79cb, 59L, 1534ch, 1534B)
├── eslint.config.js (cd11fe6, 56L, 2111ch, 2111B)
├── install.sh (5cb346d, 200L, 5892ch, 5892B)
-├── package.json (7036c9a, 97L, 2870ch, 2870B)
+├── package.json (6de92e8, 97L, 2870ch, 2870B)
├── tsconfig.json (640d95f, 21L, 616ch, 616B)
├── tsconfig.sdk-types.json (47a2c39, 13L, 303ch, 303B)
├── tsconfig.test.json (a5e75f7, 10L, 221ch, 221B)
@@ -1111,22 +1171,22 @@
## Manifests
- docs/package.json: name=@noormdev/docs, scripts=[build, dev, preview]
-- examples/llm-memory-db-mssql/package.json: name=@noormdev/example-llm-memory-db-mssql, version=0.0.1-alpha.5, scripts=[test, test:watch, typecheck]
-- examples/llm-memory-db-pg/package.json: name=@noormdev/example-llm-memory-db-pg, version=0.0.1-alpha.5, scripts=[test, test:watch, typecheck]
-- examples/todo-db/package.json: name=@noormdev/example-todo-db, version=0.0.1-alpha.5, scripts=[test, test:watch, typecheck]
+- examples/llm-memory-db-mssql/package.json: name=@noormdev/example-llm-memory-db-mssql, version=0.0.1, scripts=[test, test:watch, typecheck]
+- examples/llm-memory-db-pg/package.json: name=@noormdev/example-llm-memory-db-pg, version=0.0.1, scripts=[test, test:watch, typecheck]
+- examples/todo-db/package.json: name=@noormdev/example-todo-db, version=0.0.1, scripts=[test, test:watch, typecheck]
- package.json: name=@noormdev/main, version=0.0.1, scripts=[build, build:binary, build:packages, changeset, clean, dev, lint, lint:docs, lint:fix, prepublishOnly, release, start, test, test:coverage, test:watch, typecheck, typecheck:tests, version]
-- packages/cli/package.json: name=@noormdev/cli, version=1.0.0-alpha.39, scripts=[postinstall]
-- packages/sdk/package.json: name=@noormdev/sdk, version=1.0.0-alpha.39
+- packages/cli/package.json: name=@noormdev/cli, version=1.0.0, scripts=[postinstall]
+- packages/sdk/package.json: name=@noormdev/sdk, version=1.0.0
## Languages
-- TypeScript: 213975 LOC (81%), 933 files (75%)
-- Markdown: 42955 LOC (16%), 197 files (16%)
-- JavaScript: 1198 LOC (0%), 22 files (1%)
-- YAML: 1134 LOC (0%), 16 files (1%)
-- HTML: 955 LOC (0%), 26 files (2%)
-- CSS: 913 LOC (0%), 3 files (0%)
-- Shell: 669 LOC (0%), 4 files (0%)
-- JSON: 554 LOC (0%), 23 files (1%)
-- Vue: 181 LOC (0%), 3 files (0%)
-- TOML: 10 LOC (0%), 2 files (0%)
+- TypeScript: 240775 LOC (82%), 1031 files (81%)
+- Markdown: 44873 LOC (15%), 136 files (10%)
+- JavaScript: 1261 LOC (0%), 22 files (1%)
+- YAML: 1158 LOC (0%), 16 files (1%)
+- HTML: 1090 LOC (0%), 27 files (2%)
+- CSS: 1015 LOC (0%), 3 files (0%)
+- Shell: 930 LOC (0%), 7 files (0%)
+- JSON: 471 LOC (0%), 22 files (1%)
+- Vue: 203 LOC (0%), 3 files (0%)
+- TOML: 11 LOC (0%), 3 files (0%)
diff --git a/docs/wiki/sdk.md b/docs/wiki/sdk.md
index f2505b4e..f696151e 100644
--- a/docs/wiki/sdk.md
+++ b/docs/wiki/sdk.md
@@ -1,70 +1,92 @@
---
type: Domain
+description: Programmatic API (createContext) for noorm-managed databases, plus the DT binary/text serialization format for cross-database data transfer
---
# sdk
## What it does
-Programmatic API for noorm-managed databases. `createContext` returns a `Context` object with a Kysely instance plus namespaced noorm operations (run, changes, db, dt, lock, vault, transfer, templates, secrets, utils). Published as `@noormdev/sdk` from [`packages/sdk/`](../../packages/sdk).
+`createContext` (in [`src/sdk/index.ts`](../../src/sdk/index.ts)) returns a `Context` with a raw Kysely instance (`ctx.kysely`), `proc`/`func`/`tvf`/`transaction`/`impersonate` helpers, and a `ctx.noorm` namespace object bundling changes/run/db/dt/lock/vault/secrets/templates/transfer/utils operations. Published as `@noormdev/sdk` version `1.0.0` from [`packages/sdk/`](../../packages/sdk).
-Also includes the DT (Data Transfer format) module for typed binary serialization of database rows — separate from the `transfer` domain. DT produces `.dt` files with a universal type system.
+The DT (Data Transfer) module under [`src/core/dt/`](../../src/core/dt) is a separate universal-type serialization format (`.dt`/`.dtz`/`.dtzx` files) for exporting/importing single tables across PostgreSQL, MySQL, and MSSQL — distinct from the `core-db` domain's live DB-to-DB `transfer` module, though both share row-fetch and worker-pipeline patterns.
## Artifacts
-- [`packages/sdk/package.json`](../../packages/sdk/package.json) — published package `@noormdev/sdk`, version `1.0.0-alpha.35`
-- [`packages/sdk/CHANGELOG.md`](../../packages/sdk/CHANGELOG.md) — SDK release history
+- [`packages/sdk/package.json`](../../packages/sdk/package.json) — published package `@noormdev/sdk`, version `1.0.0`; peer deps `kysely`, and optional `better-sqlite3`/`pg`/`mysql2`/`tedious`/`tarn`
+- [`packages/sdk/CHANGELOG.md`](../../packages/sdk/CHANGELOG.md) — Changesets-generated release history
+- [`packages/sdk/dist/`](../../packages/sdk/dist) — build output: `index.js` (tsup bundle, all deps inlined except peers) + `index.d.ts` (dts-bundle-generator)
## CLI code
-- [`src/sdk/index.ts`](../../src/sdk/index.ts) — `createContext` factory; resolves config, initializes state, returns `Context`
-- [`src/sdk/context.ts`](../../src/sdk/context.ts) — `Context` class; holds Kysely instance, all namespaced ops, connect/disconnect
-- [`src/sdk/namespaces/run.ts`](../../src/sdk/namespaces/run.ts) — `RunNamespace`; wraps `runFile`, `runDir`, `runBuild`, `preview`
-- [`src/sdk/namespaces/changes.ts`](../../src/sdk/namespaces/changes.ts) — `ChangesNamespace`; wraps `ChangeManager` for ff/run/revert/list
-- [`src/sdk/namespaces/db.ts`](../../src/sdk/namespaces/db.ts) — `DbNamespace`; explore, create, drop, teardown, truncate, reset
-- [`src/sdk/namespaces/dt.ts`](../../src/sdk/namespaces/dt.ts) — `DtNamespace`; export/import `.dt` files
-- [`src/sdk/namespaces/lock.ts`](../../src/sdk/namespaces/lock.ts) — `LockNamespace`; acquire/release/force-release/status
-- [`src/sdk/namespaces/vault.ts`](../../src/sdk/namespaces/vault.ts) — `VaultNamespace`; init, get/set/remove secrets, propagate, copy key
-- [`src/sdk/namespaces/transfer.ts`](../../src/sdk/namespaces/transfer.ts) — `TransferNamespace`; wraps `transferData`
-- [`src/sdk/namespaces/templates.ts`](../../src/sdk/namespaces/templates.ts) — `TemplatesNamespace`; render, process file/files
-- [`src/sdk/namespaces/secrets.ts`](../../src/sdk/namespaces/secrets.ts) — `SecretsNamespace`; stage-level secret resolution
-- [`src/sdk/namespaces/utils.ts`](../../src/sdk/namespaces/utils.ts) — `UtilsNamespace`; Kysely sql tag, connection ping
-- [`src/sdk/impersonate/scope.ts`](../../src/sdk/impersonate/scope.ts) — `ImpersonateScope`; run operations as a different identity
-- [`src/sdk/impersonate/dialect-strategy.ts`](../../src/sdk/impersonate/dialect-strategy.ts) — per-dialect identity-column handling for impersonation
-- [`src/sdk/sql.ts`](../../src/sdk/sql.ts) — `createSqlHelper`; typed SQL tag builder wrapping Kysely's `sql`
-- [`src/sdk/tvp.ts`](../../src/sdk/tvp.ts) — `createTvp`, `TvpBuilder`; MSSQL table-valued parameter construction
-- [`src/sdk/noorm-ops.ts`](../../src/sdk/noorm-ops.ts) — `NoormOps`; assembled namespace object attached to `ctx.noorm`
-- [`src/sdk/guards.ts`](../../src/sdk/guards.ts) — `checkRequireTest`; prevents SDK use in production without explicit opt-in. `checkProtectedConfig` now runs `checkConfigPolicy` (`core/policy`) and throws `ProtectedConfigError` on denial or on a `confirm` cell (the SDK has no interactive prompt)
-- [`src/sdk/types.ts`](../../src/sdk/types.ts) — `CreateContextOptions` (carries `channel?: Channel`, default `'user'`), `ContextConfig`, SDK-level types
-- [`src/core/dt/index.ts`](../../src/core/dt/index.ts) — DT module: `exportTable`, `importTable`, serialize/deserialize, versioning, crypto
-- [`src/core/dt/dialects/`](../../src/core/dt/dialects) — per-dialect type mapping for DT
-- [`src/core/dt/type-map.ts`](../../src/core/dt/type-map.ts) — `SimpleType` vs `EncodedType` classification; `text` type uses gz64 compression
-- [`src/core/dt/schema.ts`](../../src/core/dt/schema.ts) — DT file schema validation
+- [`src/sdk/index.ts`](../../src/sdk/index.ts) — `createContext` factory; resolves identity/state/settings/config, runs `checkRequireTest`, defaults `options.channel` to `'user'`, re-exports the full public type/error surface
+- [`src/sdk/context.ts`](../../src/sdk/context.ts) — `Context` class: `kysely`, `noorm` (lazy `NoormOps`), `connect`/`disconnect`, `transaction`, `proc`/`func`/`tvf`, `impersonate` (callback and explicit modes)
+- [`src/sdk/state.ts`](../../src/sdk/state.ts) — `ContextState` interface (shared mutable state between `Context` and `NoormOps`) and `requireConnection` guard
+- [`src/sdk/noorm-ops.ts`](../../src/sdk/noorm-ops.ts) — `NoormOps`; lazy per-namespace getters, wires `db.reset` to `run.build`
+- [`src/sdk/guards.ts`](../../src/sdk/guards.ts) — `checkRequireTest` (throws `RequireTestError` when `requireTest: true` and `config.isTest` is false); `checkProtectedConfig` (calls `checkConfigPolicy` from `core/policy`, throws `ProtectedConfigError` on denial or on an unconfirmed `confirm` cell — the SDK has no interactive prompt)
+- [`src/sdk/namespaces/run.ts`](../../src/sdk/namespaces/run.ts) — `RunNamespace`: `discover` (offline), `preview`, `file`/`files`/`dir`/`build`; `build` applies `settings.build.include/exclude` + `settings.rules` filtering identical to the TUI Run Build screen and reports `unmatchedInclude`/`unmatchedExclude`
+- [`src/sdk/namespaces/changes.ts`](../../src/sdk/namespaces/changes.ts) — `ChangesNamespace`: scaffold ops (`create`, `addFile`, `removeFile`, `renameFile`, `reorderFiles`, `delete`), discovery/validation (offline), execution (`apply`, `revert`, `ff`, `next`, `rewind`), status/history
+- [`src/sdk/namespaces/db.ts`](../../src/sdk/namespaces/db.ts) — `DbNamespace`: explore (`listTables`/`describeTable`/etc.), `previewTeardown`, destructive ops `truncate`/`teardown`/`reset` — gated per-action (`db:truncate`, `db:teardown`, `db:reset`) rather than sharing one permission
+- [`src/sdk/namespaces/dt.ts`](../../src/sdk/namespaces/dt.ts) — `DtNamespace`: `exportTable` (ungated), `importFile` (gated on `db:reset`)
+- [`src/sdk/namespaces/lock.ts`](../../src/sdk/namespaces/lock.ts) — `LockNamespace`: `acquire`, `release`, `status`, `withLock`, `forceRelease` (gated on `lock:force`)
+- [`src/sdk/namespaces/vault.ts`](../../src/sdk/namespaces/vault.ts) — `VaultNamespace`: `init`, `status`, CRUD (`set`/`get`/`getAll`/`list`/`delete`/`exists`), team ops (`propagate`, `copy`); read/write/decrypt ops require `privateKey`; `set()` throws `VaultAccessError` when the key yields no usable vault key, but `get()`/`getAll()` degrade silently (`null`/`{}`) instead of throwing
+- [`src/sdk/namespaces/secrets.ts`](../../src/sdk/namespaces/secrets.ts) — `SecretsNamespace`: config-scoped local secrets (`get`/`list`/`set`/`delete`), gated same as vault (`secret:read`/`secret:write`)
+- [`src/sdk/namespaces/templates.ts`](../../src/sdk/namespaces/templates.ts) — `TemplatesNamespace`: `render` (Eta template render without executing SQL; gated on `run:file` via `checkConfigPolicy`, not `checkProtectedConfig`, so a `confirm` cell doesn't block a read-only render)
+- [`src/sdk/namespaces/transfer.ts`](../../src/sdk/namespaces/transfer.ts) — `TransferNamespace`: `to` (gated against the destination config's `db:reset` permission), `plan`
+- [`src/sdk/namespaces/utils.ts`](../../src/sdk/namespaces/utils.ts) — `UtilsNamespace`: `checksum` (offline SHA-256), `testConnection` (returns `{ ok, error? }`, never throws)
+- [`src/sdk/impersonate/scope.ts`](../../src/sdk/impersonate/scope.ts) — `buildScope`; binds `proc`/`func`/`tvf`/`transaction`/`revert` to a dedicated pooled connection
+- [`src/sdk/impersonate/dialect-strategy.ts`](../../src/sdk/impersonate/dialect-strategy.ts) — per-dialect impersonate/revert SQL: MSSQL `EXECUTE AS USER`/`REVERT`, PostgreSQL `SET ROLE`/`RESET ROLE`; MySQL and SQLite are `null` (unsupported); `validateUsername` restricts to `[a-zA-Z0-9_@.\-\\]+`
+- [`src/sdk/impersonate/types.ts`](../../src/sdk/impersonate/types.ts) — `ImpersonatedScope` interface, `ImpersonationError`
+- [`src/sdk/sql.ts`](../../src/sdk/sql.ts) — `buildProcCall`/`buildFuncCall`/`buildTvfCall`; dialect-specific EXEC/CALL/SELECT builders; `quoteIdent` per dialect; TVP-aware branches delegate to DECLARE/INSERT/EXEC batch builders
+- [`src/sdk/tvp.ts`](../../src/sdk/tvp.ts) — `tvp()` factory and `TvpValue`/`isTvp`; `MSSQL_PARAM_LIMIT = 2100`; MSSQL-only, validates consistent row keys
+- [`src/sdk/stubs/ansis.ts`](../../src/sdk/stubs/ansis.ts) — no-op `ansis` replacement aliased in the tsup build (SDK doesn't write to terminals)
+- [`src/sdk/types.ts`](../../src/sdk/types.ts) — `CreateContextOptions` (`config`, `projectRoot`, `requireTest`, `stage`, `channel` default `'user'`, `yes`), `BuildOptions`, `ExportOptions`/`ImportOptions`, `ExtractArgs`/`ExtractReturn` tuple-type helpers for proc/func/tvf typing
+- [`src/core/dt/index.ts`](../../src/core/dt/index.ts) — `exportTable`/`importDtFile`; three-stage worker pipeline (keyset-paged fetch → compute-pool serialize/deserialize → `OrderBuffer` reassembly) via `WorkerBridge`/`WorkerPool`
+- [`src/core/dt/writer.ts`](../../src/core/dt/writer.ts) / [`reader.ts`](../../src/core/dt/reader.ts) — streaming JSON5-lines writer/reader; extension picks the pipeline (`.dt` raw, `.dtz` gzip, `.dtzx` gzip+AES-256-GCM)
+- [`src/core/dt/serialize.ts`](../../src/core/dt/serialize.ts) / [`deserialize.ts`](../../src/core/dt/deserialize.ts) — row ↔ `.dt` value conversion; encoded types become `[value, encoding]` tuples
+- [`src/core/dt/streamer.ts`](../../src/core/dt/streamer.ts) — `DtStreamer`; in-memory cross-dialect row conversion with no file I/O, used by DB-to-DB transfer
+- [`src/core/dt/schema.ts`](../../src/core/dt/schema.ts) — `buildDtSchema`, `validateSchema`, `queryPrimaryKeyColumns` (export pages require a PK; a key-less table falls back to one unpaginated `SELECT`)
+- [`src/core/dt/type-map.ts`](../../src/core/dt/type-map.ts) — `toUniversalType`/`toDialectType`/`isEncodedType`; delegates pattern matching to [`src/core/dt/dialects/`](../../src/core/dt/dialects)
+- [`src/core/dt/dialects/`](../../src/core/dt/dialects) — `postgres.ts`, `mysql.ts`, `mssql.ts`; MSSQL is version-aware (native `json`/`vector` types on SQL Server 2025+, `nvarchar(max)` fallback below)
+- [`src/core/dt/version.ts`](../../src/core/dt/version.ts) — `queryDatabaseVersion`; parses `SELECT version()` (PG/MySQL) or `SERVERPROPERTY` (MSSQL, internal build number mapped to marketing year)
+- [`src/core/dt/crypto.ts`](../../src/core/dt/crypto.ts) — `encryptWithPassphrase`/`decryptWithPassphrase`; AES-256-GCM with PBKDF2 (100k iterations), independent of the identity keypair system; `MIN_PASSPHRASE_LENGTH = 12` enforced on encrypt only
+- [`src/core/dt/paging.ts`](../../src/core/dt/paging.ts) — `createKeysetPager`; primary-key cursor pagination (not `LIMIT`/`OFFSET`) so concurrent writes can't skip/duplicate rows during export
+- [`src/core/dt/paths.ts`](../../src/core/dt/paths.ts) — `resolveExportExtension`/`resolveExportPath`/`resolveExportTables`/`ensureExportDirectory`; single-table vs multi-table export path resolution
+- [`src/core/dt/modify.ts`](../../src/core/dt/modify.ts) — `modifyDtFile`; recipe-based column drop/add/rename and row filter, streamed over an existing `.dt` file
+- [`src/core/dt/constants.ts`](../../src/core/dt/constants.ts) — `FORMAT_VERSION = 1`, `GZIP_THRESHOLD = 128`, `GZIP_RATIO_THRESHOLD = 0.85`, `MAX_DECOMPRESSED_VALUE_BYTES = 64MB`, `MAX_DECOMPRESSED_ARCHIVE_BYTES = 1GB`, `MAX_ROW_BYTES = 256MB`, `SIMPLE_TYPES`/`ENCODED_TYPES` classification
+- [`src/core/dt/events.ts`](../../src/core/dt/events.ts) — `DtEvents`: export/import/stream/validate/modify progress events emitted on the shared `observer`
## Docs
-- [`docs/dev/sdk.md`](../dev/sdk.md) — SDK internals reference (1094L)
-- [`docs/reference/sdk.md`](../reference/sdk.md) — public SDK API reference (1426L)
-- [`docs/dev/transfer.md`](../dev/transfer.md) — DT transfer internals
+- [`docs/dev/sdk.md`](../dev/sdk.md) — SDK internals reference
+- [`docs/reference/sdk.md`](../reference/sdk.md) — public SDK API reference
+- [`docs/dev/transfer.md`](../dev/transfer.md) — DT/transfer internals
- [`docs/getting-started/building-your-sdk.md`](../getting-started/building-your-sdk.md) — getting-started guide for SDK users
- [`skills/noorm/references/sdk.md`](../../skills/noorm/references/sdk.md) — skill reference for SDK usage patterns
## Coupling
-- `createContext` calls `initProjectContext` from [`src/core/project-init.ts`](../../src/core/project-init.ts) — same startup sequence as CLI.
-- `createContext` defaults `options.channel` to `'user'` and re-exports `Channel`/`ConfigAccess`/`Role` from [`src/core/policy/`](../../src/core/policy) — every guard in [`src/sdk/guards.ts`](../../src/sdk/guards.ts) reads `state.options.channel` for its `checkConfigPolicy` call.
-- All namespaces delegate to core modules — any core API change propagates to the namespace wrappers.
-- DT module ([`src/core/dt/`](../../src/core/dt)) is co-owned: SDK exposes it via `ctx.noorm.dt`, but it is also used standalone by [`src/cli/db/transfer.ts`](../../src/cli/db/transfer.ts).
-- Impersonation ([`src/sdk/impersonate/`](../../src/sdk/impersonate)) requires the identity domain keypair loaded.
-- TVP ([`src/sdk/tvp.ts`](../../src/sdk/tvp.ts)) is MSSQL-only — dialect guard at construction time.
-- Published package build defined in [`tsup.sdk.config.ts`](../../tsup.sdk.config.ts); types extracted via `@microsoft/api-extractor` + `dts-bundle-generator`.
+- `createContext`'s startup sequence (`initState`, `getSettingsManager`, `resolveConfig`, `getIdentityForConfig`) is not the CLI's `initProjectContext` (project-root discovery + `chdir`, called once at CLI entry in [`src/cli/index.ts`](../../src/cli/index.ts)) — individual CLI commands (e.g. [`src/cli/db/create.ts`](../../src/cli/db/create.ts)) replicate a similar `initState`/`getSettingsManager`/`resolveConfig` sequence inline, minus `getIdentityForConfig`, whose only consumer is the SDK.
+- Every namespace method that gates a write calls `checkProtectedConfig`/`checkConfigPolicy` from [`src/core/policy/`](../../src/core/policy), reading `state.options.channel` (`'user'` | `'agent'`, default `'user'`) — CLI, TUI, and MCP callers share this one enforcement path.
+- All namespaces delegate to `core/*` modules (`core/runner`, `core/change`, `core/explore`, `core/teardown`, `core/lock`, `core/vault`, `core/template`, `core/transfer`, `core/dt`) — a core API change propagates to the SDK namespace wrapper.
+- [`src/core/dt/`](../../src/core/dt) is co-owned: the SDK exposes it via `ctx.noorm.dt`, and it is also used directly by the transfer executor for cross-dialect streaming (`DtStreamer`).
+- [`src/core/dt/index.ts`](../../src/core/dt/index.ts) dispatches serialize/deserialize work to [`src/workers/compute.ts`](../../src/workers/compute.ts) via `WorkerBridge`/`WorkerPool` (`worker-bridge` domain) — a worker-bridge protocol change affects DT export/import.
+- Impersonation ([`src/sdk/impersonate/`](../../src/sdk/impersonate)) requires a live connection borrowed from the pool; `Context.disconnect()` drains any un-reverted explicit-mode scopes via `#heldConnections` before destroying the pool.
+- `Context.proc()` on PostgreSQL retries a `CALL`-against-FUNCTION failure (SQLSTATE 42809/42883) as `SELECT * FROM (...)` — see `isFunctionNotProcedureError` in [`src/sdk/context.ts`](../../src/sdk/context.ts).
+- Published package build: [`tsup.sdk.config.ts`](../../tsup.sdk.config.ts) bundles everything except peer deps (`kysely`, `better-sqlite3`, `pg`, `mysql2`, `tedious`, `tarn`) and aliases `ansis` to the stub; [`scripts/build.mjs`](../../scripts/build.mjs) runs `dts-bundle-generator` against [`src/sdk/index.ts`](../../src/sdk/index.ts) for [`packages/sdk/dist/index.d.ts`](../../packages/sdk/dist/index.d.ts).
+- [`tests/sdk/bundle-smoke.test.ts`](../../tests/sdk/bundle-smoke.test.ts) and [`tests/sdk/dts-surface.test.ts`](../../tests/sdk/dts-surface.test.ts) import the built [`packages/sdk/dist/`](../../packages/sdk/dist) output directly (skipped when `dist` doesn't exist) — they catch bundling regressions the source-level tests can't see.
## Conventions worth knowing
-- `createContext` is the only public entry point — do not instantiate `Context` directly.
-- `ctx.kysely` is a raw Kysely instance for type-safe queries.
-- `ctx.noorm` is the noorm namespace (changes, run, db, dt, lock, vault, transfer, templates, secrets, utils).
-- DT `text` type uses gz64 compression for large TEXT columns; `string` is for short VARCHAR/CHAR.
-- `checkRequireTest` throws `RequireTestError` if `options.requireTest === true` but `config.isTest === false` — prevents accidental production use of test helpers.
-- `ProtectedConfigError` on a `confirm`-cell permission names `NOORM_YES=1` (scripted opt-in) or the CLI/TUI (interactive confirm) as the way through — the SDK itself never prompts.
-- Integration tests in [`tests/sdk/`](../../tests/sdk) and [`tests/integration/sdk/`](../../tests/integration/sdk) cover TVF and TVP patterns.
+- `createContext` is the only public entry point — `Context` is never instantiated directly by consumers.
+- `ctx.kysely` is the raw, type-safe Kysely instance; `ctx.noorm` holds every noorm-specific operation, namespaced and lazily instantiated (one singleton per `Context`).
+- `checkRequireTest` throws `RequireTestError` when `options.requireTest === true` but `config.isTest` is not `true` — prevents test helpers running against a non-test config.
+- `ProtectedConfigError` on a `confirm`-cell permission points to `options.yes: true`, `NOORM_YES=1` (scripted opt-in), or the CLI/TUI (interactive confirm) — the SDK itself never prompts. `options.yes` is only consulted after policy resolution, so it can never unblock an `agent`-channel context (a `confirm` cell collapses to deny before `yes` is read on that channel).
+- Destructive `db` operations are gated per-action (`db:truncate`, `db:teardown`, `db:reset`), not a shared `db:reset` permission — `db:reset` is `allow` for the admin role and would otherwise leave `truncate`/`teardown` unguarded on a default config.
+- `db.truncate()`/`db.teardown()` check permission (not confirmation) on `dryRun: true` — the preview is deliberately reachable even to a role that must confirm the real operation, but not to a role the policy denies outright.
+- `db.reset()` does not honor `settings.teardown.preserveTables` — it rebuilds the schema from `sql/` and a preserved table would collide with a `CREATE TABLE`; `truncate()`/`teardown()` still honor it.
+- DT's encoded types (`json`, `binary`, `vector`, `array`, `text`, `custom`) use `gz64` (gzip+base64) only when the value is at least `GZIP_THRESHOLD` (128 bytes) and compresses below `GZIP_RATIO_THRESHOLD` (0.85 of raw size); otherwise they stay `raw`/`b64`. `string` (short VARCHAR/CHAR) is a simple, untupled type — `text` is the encoded counterpart for large TEXT columns.
+- TVP ([`src/sdk/tvp.ts`](../../src/sdk/tvp.ts)) is MSSQL-only; `buildProcCall`/`buildFuncCall`/`buildTvfCall` throw if a TVP marker is passed on any other dialect.
+- `ctx.tvf()` (table-valued functions) is only supported on MSSQL and PostgreSQL; MySQL and SQLite throw.
+- `ctx.impersonate()` supports MSSQL and PostgreSQL only; MySQL and SQLite throw `ImpersonationError` before a connection is borrowed.
+- Test coverage: [`tests/sdk/`](../../tests/sdk) covers namespace behavior per access role (admin/operator/viewer configs), guard errors, SQL builders, and impersonation; [`tests/integration/sdk/`](../../tests/integration/sdk) covers TVF/TVP against live MSSQL/PostgreSQL and vault/db-reset round-trips.
diff --git a/docs/wiki/tui.md b/docs/wiki/tui.md
index 7a3e1ee3..a5d5c835 100644
--- a/docs/wiki/tui.md
+++ b/docs/wiki/tui.md
@@ -1,69 +1,59 @@
---
type: Domain
+description: Ink/React TUI launched by `noorm ui` — a custom focus-stack keyboard router, per-domain screen registry, and shared providers for state, connection, and shutdown lifecycle.
---
# tui
## What it does
-Ink/React-based terminal UI launched by `noorm ui`. Full-screen interactive interface with a home screen, keyboard-driven navigation, and per-domain screens for all noorm operations. Focus management, keyboard routing, and observer-based state updates are core TUI concerns.
+- Renders the interactive terminal UI launched via `noorm ui` ([`src/cli/ui.ts`](../../src/cli/ui.ts)), the only CLI subcommand that mounts Ink.
+- Routes navigation through a 109-member string-union `Route` type ([`src/tui/types.ts`](../../src/tui/types.ts)); the flat `SCREENS` registry ([`src/tui/screens.tsx`](../../src/tui/screens.tsx)), keyed by route, registers 94 of those routes to screens — unregistered routes fall through to `NotFoundScreen`.
+- Owns a custom focus stack ([`src/tui/focus.tsx`](../../src/tui/focus.tsx)) that gates which component receives keyboard input, used instead of `@inkjs/ui`'s incompatible internal focus system.
+- Bridges core managers (`StateManager`, `SettingsManager`) and the `@logosdx/observer` event bus into React state via `AppContextProvider` ([`src/tui/app-context.tsx`](../../src/tui/app-context.tsx)).
-## CLI code
+## Artifacts
-- [`src/tui/app.tsx`](../../src/tui/app.tsx) — root component; mounts `AppContext`, `ObserverContext`, focus/keyboard providers
-- [`src/tui/app-context.tsx`](../../src/tui/app-context.tsx) — `AppContext` (1196L); global state: active config, settings, lock status, update check, screen routing
-- [`src/tui/screens.tsx`](../../src/tui/screens.tsx) — `ScreenRegistry`; maps screen IDs to components (664L)
-- [`src/tui/screens/home.tsx`](../../src/tui/screens/home.tsx) — home screen; keyboard shortcuts for all domains (635L)
-- [`src/tui/router.tsx`](../../src/tui/router.tsx) — `Router`; screen stack push/pop navigation
-- [`src/tui/focus.tsx`](../../src/tui/focus.tsx) — `FocusManager`; focus stack for nested interactive components
-- [`src/tui/keyboard.tsx`](../../src/tui/keyboard.tsx) — `KeyboardManager`; global key event routing with priority stacking (401L)
-- [`src/tui/observer-context.ts`](../../src/tui/observer-context.ts) — `ObserverContext`; provides observer singleton to React tree
-- [`src/tui/shutdown.tsx`](../../src/tui/shutdown.tsx) — graceful shutdown sequence with progress display
-- [`src/tui/types.ts`](../../src/tui/types.ts) — TUI type contracts (Screen, ScreenProps, etc.) (470L)
-- [`src/tui/components/`](../../src/tui/components) — shared UI components: dialogs, feedback, forms, layout, lists, overlays, secrets, status, terminal
-- [`src/tui/hooks/`](../../src/tui/hooks) — 14 hooks: `useObserver`, `useConnection`, `useChangeProgress`, `useRunProgress`, `useTransferProgress`, `useLockStatus`, `useVaultConnection`, `useVaultSecretKeys`, `useSettingsOperation`, `useUpdateChecker`, `useSecretSource`, `useAsyncEffect`, `useLoadGuard`
-- [`src/tui/providers/ConnectionProvider.tsx`](../../src/tui/providers/ConnectionProvider.tsx) — `ConnectionProvider`; DB connection lifecycle for TUI screens
-- [`src/tui/utils/`](../../src/tui/utils) — 12 utilities: path resolution, connection helpers, config validation, clipboard, change-loader
-- [`src/tui/screens/change/`](../../src/tui/screens/change) — 12 change-related screens
-- [`src/tui/screens/config/`](../../src/tui/screens/config) — 11 config screens
-- [`src/tui/screens/db/`](../../src/tui/screens/db) — 11 DB screens + 1 subdir
-- [`src/tui/screens/debug/`](../../src/tui/screens/debug) — 4 debug screens
-- [`src/tui/screens/identity/`](../../src/tui/screens/identity) — 6 identity screens
-- [`src/tui/screens/init/`](../../src/tui/screens/init) — 4 init wizard screens
-- [`src/tui/screens/lock/`](../../src/tui/screens/lock) — 6 lock screens
-- [`src/tui/screens/run/`](../../src/tui/screens/run) — 7 run screens
-- [`src/tui/screens/secret/`](../../src/tui/screens/secret) — 4 secret screens
-- [`src/tui/screens/settings/`](../../src/tui/screens/settings) — 17 settings screens
-- [`src/tui/screens/vault/`](../../src/tui/screens/vault) — 5 vault screens
-- [`src/tui/screens/UpdateScreen.tsx`](../../src/tui/screens/UpdateScreen.tsx) — update available prompt
-- [`src/tui/screens/MoreScreen.tsx`](../../src/tui/screens/MoreScreen.tsx) — extended help screen
+- [`.claude/rules/tui-development.md`](../../.claude/rules/tui-development.md) — path-scoped rules for this domain: focus system, `@inkjs/ui` boundary, keyboard handling, screen focus ownership, UI patterns (toast + `back()`), Ink layout, observer hooks, and testing conventions. Frontmatter scopes it to `src/tui/**/*.{ts,tsx}, tests/tui/**/*.{ts,tsx}`.
+- [`tests/cli/components/`](../../tests/cli/components) — component tests (`dialogs`, `DismissableAlert`, `form-navigation`, `forms`, `layout`, `lists`, `status`) using `ink-testing-library`.
+- [`tests/cli/hooks/`](../../tests/cli/hooks) — hook tests (`useObserver`, `useTransferProgress`, `useUpdateChecker`, `useVaultSecretKeys`).
+- [`tests/cli/screens/`](../../tests/cli/screens) — screen tests, mirroring [`src/tui/screens/`](../../src/tui/screens) subdirectories (`change/`, `config/`, `db/`, `init/`).
-## Docs
+## CLI code
-- [`docs/tui.md`](../tui.md) — TUI user guide (407L)
-- [`docs/dev/ink-cheatsheet.md`](../dev/ink-cheatsheet.md) — Ink layout reference for developers (1427L)
-- [`docs/dev/ink-testing-library-cheatsheet.md`](../dev/ink-testing-library-cheatsheet.md) — testing cheatsheet (737L)
-- [`.claude/rules/tui-development.md`](../../.claude/rules/tui-development.md) — TUI development rules (focus system, UI patterns, layout)
-- [`.claude/skills/noorm-design/`](../../.claude/skills/noorm-design) — design system assets and colors
+- [`src/tui/app.tsx`](../../src/tui/app.tsx) — root `App` component; wires the provider hierarchy (`ShutdownProvider` → `NoormObserver` → `AppContextProvider` → `ConnectionProvider` → `ToastProvider` → `FocusProvider` → `RouterProvider` → `AppShell`) and renders the header/breadcrumb, `ScreenRenderer`, and status bar.
+- [`src/tui/app-context.tsx`](../../src/tui/app-context.tsx) — `AppContextProvider`/`useAppContext` and derived hooks (`useActiveConfig`, `useLockStatus`, `useGlobalModes`, `useDryRunMode`, `useForceMode`, `useExploreFilters`); also exports `LoadingGuard`, `ConfigGuard`, `IdentityGuard`.
+- [`src/tui/focus.tsx`](../../src/tui/focus.tsx) — `FocusProvider`, `useFocusScope`, `useIsFocused`, `useActiveFocus`; stack-based, last-pushed-wins.
+- [`src/tui/keyboard.tsx`](../../src/tui/keyboard.tsx) — `GlobalKeyboard` (Ctrl+C, Shift+L log viewer, Shift+Q SQL terminal, `?` help / 4-press-within-750ms debug-mode easter egg, `D`/`F` dry-run/force toggles); `useFocusedInput`, `useListKeys`, `useQuitHandler`.
+- [`src/tui/router.tsx`](../../src/tui/router.tsx) — `RouterProvider`/`useRouter` with `navigate`/`back`/`replace`/`reset`, a history stack, and `router:navigated`/`router:popped` observer events.
+- [`src/tui/screens.tsx`](../../src/tui/screens.tsx) — the `SCREENS` route registry and `ScreenRenderer`; `getRouteLabel`, `getRegisteredRoutes`, `isRouteRegistered`, `registerScreen`.
+- [`src/tui/types.ts`](../../src/tui/types.ts) — `Route` union, `RouteParams`, `RouterContextValue`, `FocusContextValue`, `ScreenProps`/`ScreenEntry`, `getSection`/`getParentRoute`/`isNumericString`.
+- [`src/tui/shutdown.tsx`](../../src/tui/shutdown.tsx) — `ShutdownProvider`/`useShutdown`; `gracefulExit()` drives `LifecycleManager.shutdown()` and shows a phased `ShutdownScreen` before emitting the `app:exit` observer event.
+- [`src/tui/observer-context.ts`](../../src/tui/observer-context.ts) — `NoormObserver`/`useNoormObserver`, built via `@logosdx/react`'s `createObserverContext` over the shared `observer` singleton.
+- [`src/tui/components/`](../../src/tui/components) — shared UI: `layout/` (`Panel`, `Divider`, `MissingParamPanel`, `NotFoundPanel`), `lists/` (`SelectList`, `SearchableList`, `ActionList`, `StatusList`), `forms/` (`Form`, `TextInput`), `feedback/` (`Toast`/`ToastProvider`, `DismissableAlert`, plus re-exports of `@inkjs/ui`'s `Spinner`/`ProgressBar`/`Alert`/`StatusMessage`/`Badge`), `dialogs/` (`Confirm`, `ProtectedConfirm`, `SmartConfirm`, `FilePicker`, `KeyHandler`), `status/` (`ConnectionStatus`, `LockStatus`), `secrets/` (definition/value forms and lists shared by settings and stage-secret screens), `overlays/` (`LogViewerOverlay`), `terminal/` (`SqlInput`, `ResultTable` for the SQL REPL). `components/index.ts` is the primary re-export surface consumed by screens, though some screens import subdirectory barrels (or files) directly instead — e.g. [`src/tui/screens/db/SqlTerminalScreen.tsx`](../../src/tui/screens/db/SqlTerminalScreen.tsx) and `SqlHistoryScreen.tsx` from `components/terminal/`, [`src/tui/screens/debug/DebugListScreen.tsx`](../../src/tui/screens/debug/DebugListScreen.tsx) from `components/lists/`, [`src/tui/screens/init/ProjectSetup.tsx`](../../src/tui/screens/init/ProjectSetup.tsx), `IdentitySetup.tsx`, `InitScreen.tsx` from multiple subdirectory barrels, and [`src/tui/screens/identity/IdentityEditScreen.tsx`](../../src/tui/screens/identity/IdentityEditScreen.tsx) from `components/forms/`.
+- [`src/tui/hooks/`](../../src/tui/hooks) — `useObserver.ts` (`useOnEvent`, `useOnceEvent`, `useEmit`, `useOnScreenPopped`), plus `useConnection.ts`, `useVaultConnection.ts`, `useVaultSecretKeys.ts`, `useLockStatus.ts`, `useLoadGuard.ts`, `useRunProgress.ts`, `useTransferProgress.ts`, `useChangeProgress.ts`, `useUpdateChecker.ts`, `useSettingsOperation.ts`, `useSecretSource.ts`, `useAsyncEffect.ts`; re-exported from `hooks/index.ts`.
+- [`src/tui/providers/ConnectionProvider.tsx`](../../src/tui/providers/ConnectionProvider.tsx) — `ConnectionProvider`/`useConnectionContext`; holds one lazily-created Kysely connection keyed by `activeConfigName`, destroyed on config change or unmount.
+- [`src/tui/utils/`](../../src/tui/utils) — screen-facing helpers: `change-context.ts`, `change-loader.ts`, `clipboard.ts`, `config-validation.ts`, `connection.ts`, `date.ts`, `error.ts`, `identity.ts`, `paths.ts`, `run-context.ts`, `settings-validation.ts`, `string.ts`. All but `date.ts` are re-exported from `utils/index.ts`; `date.ts` is not in the barrel — consumers (`home.tsx`, `ChangeHistoryScreen.tsx`, `ChangeListScreen.tsx`, `identity/KnownUsersScreen.tsx`) import `relativeTimeAgo` directly from `../utils/date.js`.
+- [`src/tui/screens/`](../../src/tui/screens) — per-domain screen components: `change/`, `config/`, `db/` (including `db/explore/`), `debug/`, `identity/`, `init/`, `lock/`, `run/`, `secret/`, `settings/`, `vault/`, plus top-level `home.tsx`, `MoreScreen.tsx`, `not-found.tsx`, `UpdateScreen.tsx`. Each multi-screen subdirectory exports its screens through a local `index.ts` (e.g. [`src/tui/screens/config/index.ts`](../../src/tui/screens/config/index.ts)) that `screens.tsx` imports from.
## Coupling
-- All TUI screens consume observer events from [`src/core/observer.ts`](../../src/core/observer.ts) — observer event shape changes break TUI hooks.
-- `useConnection` and `ConnectionProvider` use [`src/core/connection/manager.ts`](../../src/core/connection/manager.ts) — connection manager resets affect TUI session.
-- [`src/tui/utils/paths.ts`](../../src/tui/utils/paths.ts) uses `settings.paths.sql`/`settings.paths.changes` from SettingsManager — not per-config paths (see project CLAUDE.md).
-- Lifecycle shutdown ([`src/core/lifecycle/`](../../src/core/lifecycle)) drives [`src/tui/shutdown.tsx`](../../src/tui/shutdown.tsx) — shutdown phase changes affect TUI teardown.
-- TUI is launched by [`src/cli/ui.ts`](../../src/cli/ui.ts) — CLI dependency.
-- `SmartConfirm`/`ProtectedConfirm` ([`src/tui/components/dialogs/`](../../src/tui/components/dialogs)) take `requiresConfirmation`/`confirmationPhrase` from a `PolicyCheck` (`checkConfigPolicy`, `core/policy`) instead of a config's `protected` flag — every destructive-action screen (change run/revert/ff, db create/destroy/teardown/truncate, config rm) calls `checkConfigPolicy` directly to build these props.
-- [`src/tui/utils/config-validation.ts`](../../src/tui/utils/config-validation.ts) builds `ConfigAccess` from the Add/Edit config forms' `userRole`/`mcpRole` select fields (`buildAccessFromValues`) — `ConfigAddScreen`/`ConfigEditScreen` replaced the old single `protected` checkbox with these two role selects.
-- [`src/tui/app-context.tsx`](../../src/tui/app-context.tsx) derives placeholder stage configs' `access` from `stage.defaults.protected` via `GUARDED_ACCESS`/`DEFAULT_ACCESS` (`core/policy`).
+- **core-change**: screens under [`src/tui/screens/change/`](../../src/tui/screens/change) import `ChangeHistory`, `discoverChanges`, and change types directly from [`src/core/change/`](../../src/core/change); `home.tsx` also reads `ChangeHistory` for the recent-activity panel. Changes to `ChangeHistory`'s or `UnifiedHistoryRecord`'s shape force edits here.
+- **core-config / core-state**: `app-context.tsx` instantiates `StateManager`/`SettingsManager` from [`src/core/state/`](../../src/core/state) and [`src/core/settings/`](../../src/core/settings) and mirrors their `Config`/`Settings` types; `SettingsProvider` from [`src/core/config/resolver.ts`](../../src/core/config/resolver.ts) is used by config screens (e.g. `ConfigEditScreen.tsx`).
+- **core-db** (connection/explore/transfer/teardown): `ConnectionProvider` calls `createConnection` from [`src/core/connection/`](../../src/core/connection); `testConnection` is called separately, by `ConfigAddScreen.tsx`, `ConfigEditScreen.tsx`, `SqlTerminalScreen.tsx`, and `LockForceScreen.tsx`. `db/` screens import `fetchOverview` from [`src/core/explore/`](../../src/core/explore), plus transfer/teardown/dt types from [`src/core/transfer/`](../../src/core/transfer), [`src/core/teardown/`](../../src/core/teardown), [`src/core/dt/`](../../src/core/dt).
+- **core-identity**: `identity/` screens and `app-context.tsx` use `loadExistingIdentity`/`CryptoIdentity` from [`src/core/identity/`](../../src/core/identity); `vault/` screens use [`src/core/vault/`](../../src/core/vault).
+- **core-policy**: `SmartConfirm`, `ConfigRemoveScreen`, `DbCreateScreen`, `DbDestroyScreen`, `DbTruncateScreen`, `DbTeardownScreen`, `DbTransferScreen`, `LockForceScreen`, `RunBuildScreen`, `VaultScreen`, and all `change/*` mutation screens (`ChangeFFScreen`, `ChangeNextScreen`, `ChangeRemoveScreen`, `ChangeRevertScreen`, `ChangeRewindScreen`, `ChangeRunScreen`) call `checkConfigPolicy`/`confirmationPhraseFor` from [`src/core/policy/`](../../src/core/policy) to decide plain `Confirm` vs. typed-phrase `ProtectedConfirm`; a change to `PolicyCheck`'s shape forces edits in `SmartConfirm.tsx` and every caller listed above. `app-context.tsx` also derives placeholder stage configs' `access` from `GUARDED_ACCESS`/`DEFAULT_ACCESS`.
+- **core-identity** (lock): `home.tsx` and `lock/` screens call `getLockManager()` from [`src/core/lock/`](../../src/core/lock).
+- **core-state** (observer): `app-context.tsx` subscribes to the shared `observer` singleton ([`src/core/observer.ts`](../../src/core/observer.ts)) directly via `observer.on(...)` (`state:loaded`, `config:activated`, `connection:*`, `lock:*`, etc.) and mirrors it into context state. No screen calls `useOnEvent`/`useOnceEvent`/`observer.on(...)` directly; screens instead consume domain-specific wrapper hooks — `useLockStatus`, `useConnection`, `useRunProgress`, `useTransferProgress`, `useChangeProgress`, `useVaultConnection`, `useVaultSecretKeys`, `useLoadGuard`, `useUpdateChecker` — some of which (`useRunProgress`, `useTransferProgress`, `useChangeProgress`) wrap `useOnEvent` internally, others of which derive from `app-context.tsx`'s state or don't touch the observer at all. Event-contract changes still ripple across this whole domain.
+- **cli**: [`src/cli/ui.ts`](../../src/cli/ui.ts) and [`src/cli/sql/repl.ts`](../../src/cli/sql/repl.ts) are the only entry points that mount `App`; both independently (not centrally) own suppressing logger output (an identical null `Writable` stream) so Ink's rendering isn't corrupted, and own the `app:exit` → `clear()`/`unmount()` teardown sequence.
## Conventions worth knowing
-- Tests use `ink-testing-library`: `render()` → `await new Promise(r => setTimeout(r, 50))` → `stdin.write()` → `lastFrame()` → `unmount()`.
-- Key codes for tests: Tab=`\t`, Shift+Tab=`\x1b[Z`, Down=`\x1b[B`, Up=`\x1b[A`, Enter=`\r`, Esc=`\x1b`.
-- Focus stack initialized in `useEffect` — must wait 50ms after render before sending input in tests.
-- `numberNav` prop on `SelectList` enables 1-9 quick selection.
-- Home screen hotkeys: `c`=config, `g`=changes, `r`=run, `d`=db, `l`=lock, `s`=settings, `k`=secrets, `i`=identity, `q`=quit.
-- Sub-screen hotkeys: `a`=add, `e`=edit, `d`=delete, `x`=export, `i`=import, `u`=use/activate, `v`=validate, `k`=secrets.
-- `Shift+L` toggles log viewer overlay globally.
-- `guarded()` (re-exported for the TUI as `isConfigGuarded` in [`src/tui/utils/config-validation.ts`](../../src/tui/utils/config-validation.ts)) is display-only styling, never an enforcement input — `checkConfigPolicy` is the only gate.
+- Focus routing is stack-based, not DOM-based: `useFocusScope(label)` pushes an ID on mount and pops on unmount; only the top-of-stack ID is `isFocused`. Check `isFocused` inside the `useInput` handler body — never via `useInput`'s `isActive` option — because the option skips handler registration and `isFocused` is still `false` during the first render.
+- `@inkjs/ui`'s `Select`, `MultiSelect`, and `ConfirmInput` are not used because they drive their own internal focus, invisible to this app's stack; `SelectList`, `Form`, `Confirm`/`SmartConfirm` are the custom replacements. `TextInput`, `Spinner`, `Badge`, `ProgressBar` from `@inkjs/ui` are used directly since they're display-only or externally controlled.
+- A screen whose primary content is a `Form` (or other self-focusing component) does not call `useFocusScope` at the screen level — the two scopes would compete for the same stack slot. `ConfigEditScreen.tsx` follows this: no screen-level `useFocusScope`; `Form` owns focus via its own `focusLabel` prop.
+- Global hotkeys route through `GlobalKeyboard` in [`src/tui/keyboard.tsx`](../../src/tui/keyboard.tsx); screen/component-local hotkeys are declared inside each screen's own `useInput` (e.g. `HomeScreen`'s single-letter navigation block). `?`/`D`/`F` are suppressed once `stack.length > 1` (once something below the root has claimed focus, treated as "likely a text input").
+- Route params (`RouteParams` in [`src/tui/types.ts`](../../src/tui/types.ts)) are one shared shape reused across all screens (`name`, `count`, `path`, `stage`, `schema`, `operationId`, `query`, `topic`, `table`, `rowId`, `force`, `fromInit`) rather than per-route param types.
+- Screens with multiple named exports live in a subdirectory with a local `index.ts` re-exporting each screen ([`src/tui/screens/config/index.ts`](../../src/tui/screens/config/index.ts), [`src/tui/screens/db/index.ts`](../../src/tui/screens/db/index.ts), etc.); `screens.tsx` imports only from these subdirectory index files, never individual screen files directly.
+- Success/error feedback uses `useToast().showToast(...)` followed by `back()` to pop router history, rather than a dedicated confirmation screen — exercised in `ConfigEditScreen.tsx`'s submit handler.
+- Tests wrap components in ``/`` (and `RouterProvider`/`AppContextProvider` for screen-level tests) using `ink-testing-library`'s `render`, wait ~50ms after render for the focus stack's `useEffect` to run before sending `stdin.write(...)` keystrokes, and call `unmount()` afterward to release stdin handlers.
diff --git a/docs/wiki/worker-bridge.md b/docs/wiki/worker-bridge.md
index abdc045b..960ffbd7 100644
--- a/docs/wiki/worker-bridge.md
+++ b/docs/wiki/worker-bridge.md
@@ -1,40 +1,48 @@
---
type: Domain
+description: Hub-and-spoke worker thread infrastructure — WorkerBridge/WorkerPool message routing plus the connection and compute worker entry points
---
# worker-bridge
## What it does
-Hub-and-spoke worker thread infrastructure. `WorkerBridge` (an `ObserverRelay` subclass) owns message routing between main thread and worker threads. `WorkerPool` provides round-robin dispatch to N workers. `OrderBuffer` reassembles index-ordered responses. Used for CPU-bound DT serialization/deserialization and for the persistent DB connection worker.
+[`src/core/worker-bridge/`](../../src/core/worker-bridge) implements `WorkerBridge`, an `ObserverRelay>` subclass (from `@logosdx/observer`) that wraps a `Worker` (parent side) or `parentPort` (worker side) and exposes a typed `request()`/`emit()` protocol over `worker_threads.postMessage`.
-## CLI code
+`WorkerPool` holds N `WorkerBridge` instances and dispatches `request()` calls round-robin across them.
-- [`src/core/worker-bridge/bridge.ts`](../../src/core/worker-bridge/bridge.ts) — `WorkerBridge`; ObserverRelay subclass, message correlation, error propagation
-- [`src/core/worker-bridge/pool.ts`](../../src/core/worker-bridge/pool.ts) — `WorkerPool`; round-robin N-worker dispatch
-- [`src/core/worker-bridge/order-buffer.ts`](../../src/core/worker-bridge/order-buffer.ts) — `OrderBuffer`; index-ordered response reassembly
-- [`src/core/worker-bridge/paths.ts`](../../src/core/worker-bridge/paths.ts) — `resolveWorker`; path resolution for dev/compiled contexts
-- [`src/core/worker-bridge/types.ts`](../../src/core/worker-bridge/types.ts) — `WireMessage`, `Correlated`, event contract types
-- [`src/workers/connection.ts`](../../src/workers/connection.ts) — persistent DB worker entry point; owns Kysely instance, handles all DB ops
-- [`src/workers/compute.ts`](../../src/workers/compute.ts) — stateless compute worker entry point; serialize/deserialize for DT pipeline
+[`src/workers/`](../../src/workers) holds the two worker thread entry points that run inside spawned threads: `connection.ts` (persistent, owns one Kysely instance) and `compute.ts` (stateless serialize/deserialize).
+
+## Artifacts
+
+- [`src/core/worker-bridge/bridge.ts`](../../src/core/worker-bridge/bridge.ts) — `WorkerBridge` class: constructor branches on whether a `script` arg is passed (parent, spawns `new Worker(script)`) or omitted (worker, requires `parentPort`); `request()` generates a `randomUUID()` correlation id, sends `{ ...data, __cid: cid }`, and races a `once(^event:res:cid$)` listener against a per-instance death promise
+- [`src/core/worker-bridge/pool.ts`](../../src/core/worker-bridge/pool.ts) — `WorkerPool` class: constructs `Math.max(1, options.size)` `WorkerBridge` instances in the constructor, round-robins `request()` via `#nextIndex`, `shutdown()` awaits `Promise.all` of every worker's `shutdown()`
+- [`src/core/worker-bridge/order-buffer.ts`](../../src/core/worker-bridge/order-buffer.ts) — `OrderBuffer` class: `add(index, item)` buffers into a `Map` and drains every contiguous entry starting at `#nextIndex`; throws on a non-integer index, an index below `#nextIndex`, or a duplicate index already in the buffer
+- [`src/core/worker-bridge/pending-set.ts`](../../src/core/worker-bridge/pending-set.ts) — `PendingSet` class: `track(promise)` wraps the promise with `.then(() => undefined, () => undefined)` before adding it to a `Set`, so a rejected task still removes itself; `settleAny()` races the set, `settleAll()` loops `Promise.allSettled` until the set is empty
+- [`src/core/worker-bridge/paths.ts`](../../src/core/worker-bridge/paths.ts) — `resolveWorker(name)`: returns `new URL('./workers/${name}.js', import.meta.url)` when `import.meta.url` contains `$bunfs` (compiled binary), otherwise `resolve(WORKER_DIR, '${name}.js')` where `WORKER_DIR = resolve(MODULE_DIR, '../../workers')`
+- [`src/core/worker-bridge/types.ts`](../../src/core/worker-bridge/types.ts) — `WireMessage` (`{ event: string; data: unknown }`), `ResKey` ([``](../..) `${K}:res` [``](../..) template type), `Correlated` (`T & { __cid: string }`), `ConnectionEvents`, `ComputeEvents`, `PoolOptions`; imports `DtColumn`/`DtValue` from [`src/core/dt/types.ts`](../../src/core/dt/types.ts) and `Dialect` from [`src/core/connection/types.ts`](../../src/core/connection/types.ts)
+- [`src/core/worker-bridge/index.ts`](../../src/core/worker-bridge/index.ts) — barrel export: `WorkerBridge`, `WorkerPool`, `OrderBuffer`, `resolveWorker`, and the `WireMessage`/`ResKey`/`Correlated`/`ConnectionEvents`/`ComputeEvents`/`PoolOptions` types (`PendingSet` is not re-exported here — importers reach it via `src/core/worker-bridge/pending-set.js` directly, as [`src/core/dt/index.ts`](../../src/core/dt/index.ts) does)
+- [`src/workers/connection.ts`](../../src/workers/connection.ts) — connection worker entry point: builds a `Kysely` instance via `createDialectConnection`, which dynamically imports one of `src/core/connection/dialects/{sqlite-bun,sqlite,postgres,mysql,mssql}.js` based on the `dialect` argument (sqlite branches on `typeof globalThis.Bun`); handles `connect`, `disconnect`, `query`, `query:batch`, `execute`
+- [`src/workers/compute.ts`](../../src/workers/compute.ts) — compute worker entry point: handles `serialize` (calls `serializeRow` from `src/core/dt/serialize.js`) and `deserialize` (calls `deserializeRow` from `src/core/dt/deserialize.js`), both wrapped in `attemptSync`
## Docs
-- [`docs/dev/README.md`](../dev/README.md) — worker bridge architecture overview (section in monorepo dev guide)
+None indexed.
## Coupling
-- `resolveWorker` is called wherever a worker is spawned — never hardcode worker paths.
-- `WorkerBridge` extends `ObserverRelay` from `@logosdx/observer` — observer domain is a dependency.
-- DT module ([`src/core/dt/`](../../src/core/dt)) spawns compute workers via `WorkerPool` — DT changes may require worker message-type updates.
-- Connection worker ([`src/workers/connection.ts`](../../src/workers/connection.ts)) holds the Kysely instance used by runner and change executor in worker contexts — worker restart resets all in-flight operations.
-- Bun `--compile` binary path resolution: [`src/workers/compute.ts`](../../src/workers/compute.ts) → `workers/compute.js` in `$bunfs` — the `IS_COMPILED` guard in `paths.ts` handles this.
+- [`src/core/worker-bridge/types.ts`](../../src/core/worker-bridge/types.ts) imports `DtColumn`/`DtValue` from [`src/core/dt/types.ts`](../../src/core/dt/types.ts) and `Dialect` from [`src/core/connection/types.ts`](../../src/core/connection/types.ts) — changing those types' shapes changes the worker wire contract.
+- [`src/core/dt/index.ts`](../../src/core/dt/index.ts) (sdk domain) is the primary consumer: it imports `WorkerPool`, `OrderBuffer`, `PendingSet`, and `resolveWorker` directly, constructs a `WorkerPool` via `createDefaultComputePool()`, and uses `resolveWorker('compute')` to locate the compute worker script — a change to `ComputeEvents` in `types.ts` or to `compute.ts`'s handler shape requires a matching change in [`src/core/dt/index.ts`](../../src/core/dt/index.ts).
+- [`src/core/connection/manager.ts`](../../src/core/connection/manager.ts) (core-db domain) imports `WorkerBridge` and `ConnectionEvents` as types, keeps a `Map>` in `#bridges`, and exposes `trackBridge(configName, bridge)` so the connection worker's `WorkerBridge` is shut down alongside regular connections on `app:shutdown`.
+- [`src/cli/dev/test-workers.ts`](../../src/cli/dev/test-workers.ts) (cli domain) imports `resolveWorker`, `WorkerBridge`, and `WorkerBridge.pool` to run worker thread diagnostics against both `compute` and `connection` workers.
+- `WorkerBridge` extends `ObserverRelay` from `@logosdx/observer` — its `on`/`once`/`off`/`emit`/`queue` methods and `receive()`/`shutdown()`/`isShutdown` come from that base class, not from this domain.
+- [`src/workers/connection.ts`](../../src/workers/connection.ts) dynamically imports dialect factories from [`src/core/connection/dialects/`](../../src/core/connection/dialects) — adding a dialect there requires a matching branch in `createDialectConnection`.
## Conventions worth knowing
-- `IS_COMPILED = import.meta.url.includes('$bunfs')` detects compiled binary context.
-- In compiled binary: `new URL('./workers/${name}.js', import.meta.url)` resolves against `$bunfs`.
-- In dev/dist: `resolve(WORKER_DIR, '${name}.js')` resolves to absolute path.
-- `noorm dev test-workers` runs 5 worker thread tests across all execution contexts.
-- `WireMessage` carries a correlation ID for request-response matching across thread boundary.
-- `OrderBuffer` is needed when worker results arrive out-of-order (e.g., concurrent compute workers).
+- Worker scripts live at [`src/workers/`](../../src/workers), a sibling of [`src/core/`](../../src/core), not inside [`src/core/worker-bridge/`](../../src/core/worker-bridge) — they are standalone entry points for `bun build --compile`, not library code.
+- Never hardcode a worker script path; always resolve it through `resolveWorker(name)`. It handles three execution contexts differently: Bun dev mode resolves `.js` to `.ts` automatically, Node from `dist/` resolves to `dist/workers/*.js`, and a Bun-compiled binary resolves against `import.meta.url` inside `$bunfs` because `bun build --compile` strips the [`src/`](../../src) root and compiles `.ts` to `.js`.
+- `request()`'s correlation id (`__cid`) is injected into the outgoing payload and echoed back in the response event name ([``](../..) `${event}:res:${cid}` [``](../..)); worker-side handlers destructure `__cid` from `data` (typed via `Correlated`) to build that response event name.
+- A `WorkerBridge` in parent mode listens for its worker's `exit` event; a nonzero exit code while not already shut down calls `#failPending`, which rejects every tracked pending request and stores `#deathError` so subsequent `request()` calls reject immediately rather than hanging.
+- `OrderBuffer` and `PendingSet` exist because dispatching requests to a `WorkerPool` returns responses out of order and a naive `while (inFlight > 0) await sleep(1)` backpressure loop leaks its counter when a dispatch fails before reaching its downstream callback — both classes are designed around real promise settlement instead of a counter.
+- [`src/core/worker-bridge/pending-set.ts`](../../src/core/worker-bridge/pending-set.ts)'s `track()` deliberately double-swallows rejections (the caller attaches its own handler first) so `settleAny`/`settleAll` never turn a task failure into an unhandled rejection.
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 2236ed04..12faff17 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -26,6 +26,6 @@
"license": "MIT",
"repository": {
"type": "git",
- "url": "https://github.com/noormdev/noorm"
+ "url": "git+https://github.com/noormdev/noorm.git"
}
}
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index 460ee814..7ea5d5f7 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -8,8 +8,8 @@
"types": "./dist/index.d.ts",
"exports": {
".": {
- "import": "./dist/index.js",
- "types": "./dist/index.d.ts"
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js"
}
},
"files": [
@@ -56,6 +56,6 @@
"license": "MIT",
"repository": {
"type": "git",
- "url": "https://github.com/noormdev/noorm"
+ "url": "git+https://github.com/noormdev/noorm.git"
}
}
diff --git a/src/core/change/history.ts b/src/core/change/history.ts
index ffe66937..2ecfc497 100644
--- a/src/core/change/history.ts
+++ b/src/core/change/history.ts
@@ -62,21 +62,43 @@ import type { ChangeType } from '../shared/index.js';
const RESET_MARKER = '__reset__';
/**
- * Normalizes a change-tracking timestamp column to a real `Date`.
+ * Dialects whose driver parses an offset-less timestamp column in the host's
+ * local zone rather than as UTC.
*
- * WHY: postgres/mysql/mssql drivers parse `executed_at` into a `Date`
- * automatically, but SQLite (both `bun:sqlite` and `better-sqlite3`)
- * hands back the raw `CURRENT_TIMESTAMP` text (`'YYYY-MM-DD HH:MM:SS'`,
- * always UTC, no offset marker). Parsing that string with `new Date(str)`
- * directly reads it as local time, silently shifting the result by the
- * host's UTC offset — so the string must be marked UTC explicitly first.
+ * WHY: `executed_at` is `timestamp`/`datetime2` — no time zone — and noorm
+ * always writes UTC into it. `pg` and `mysql2` both read that naive text back
+ * through the local zone, so a row stored at 05:10:57 UTC comes back as a
+ * `Date` meaning 05:10:57 local. On a UTC-4 host that is four hours in the
+ * future, which surfaces as "in 4 hours" wherever the TUI renders relative
+ * time. Both were measured, not assumed.
+ *
+ * MSSQL is deliberately absent: `tedious` was not measured, and leaving it out
+ * keeps its current behavior rather than risking a correction in the wrong
+ * direction. SQLite is absent because it returns text and takes the string
+ * path below.
+ */
+const LOCAL_PARSED_TIMESTAMP_DIALECTS: ReadonlySet = new Set(['postgres', 'mysql']);
+
+/**
+ * Normalizes a change-tracking timestamp column to a real `Date` in UTC.
+ *
+ * WHY: the column carries no time zone and noorm writes UTC into it, but every
+ * driver disagrees about how to read that back. SQLite (both `bun:sqlite` and
+ * `better-sqlite3`) hands back raw `CURRENT_TIMESTAMP` text; `pg` and `mysql2`
+ * hand back a `Date` they already misread as local. Both roads lead to the
+ * same silent shift by the host's UTC offset, so both are corrected here —
+ * the string by marking it UTC, the `Date` by reinterpreting the local
+ * calendar fields the driver produced as the UTC fields they actually were.
*
* @example
- * hydrateDate('2026-07-12 09:02:59') // -> 2026-07-12T09:02:59.000Z
- * hydrateDate(new Date('2026-07-12T09:02:59.000Z')) // -> unchanged
- * hydrateDate(null) // -> null
+ * hydrateDate('2026-07-12 09:02:59', 'sqlite') // -> 2026-07-12T09:02:59.000Z
+ * hydrateDate(pgDateFor09_02_59, 'postgres') // -> 2026-07-12T09:02:59.000Z
+ * hydrateDate(null, 'postgres') // -> null
*/
-export function hydrateDate(value: Date | string | null | undefined): Date | null {
+export function hydrateDate(
+ value: Date | string | null | undefined,
+ dialect: Dialect,
+): Date | null {
if (value === null || value === undefined) {
@@ -86,7 +108,21 @@ export function hydrateDate(value: Date | string | null | undefined): Date | nul
if (value instanceof Date) {
- return value;
+ if (!LOCAL_PARSED_TIMESTAMP_DIALECTS.has(dialect)) {
+
+ return value;
+
+ }
+
+ return new Date(Date.UTC(
+ value.getFullYear(),
+ value.getMonth(),
+ value.getDate(),
+ value.getHours(),
+ value.getMinutes(),
+ value.getSeconds(),
+ value.getMilliseconds(),
+ ));
}
@@ -216,9 +252,9 @@ export class ChangeHistory {
return {
name: record.name,
status: record.status,
- appliedAt: hydrateDate(record.executed_at),
+ appliedAt: hydrateDate(record.executed_at, this.#dialect),
appliedBy: record.executed_by,
- revertedAt: hydrateDate(revertRecord?.executed_at),
+ revertedAt: hydrateDate(revertRecord?.executed_at, this.#dialect),
errorMessage: record.error_message || null,
appliedHistoryId: record.id,
};
@@ -269,7 +305,7 @@ export class ChangeHistory {
statuses.set(record.name, {
name: record.name,
status: record.status,
- appliedAt: hydrateDate(record.executed_at),
+ appliedAt: hydrateDate(record.executed_at, this.#dialect),
appliedBy: record.executed_by,
revertedAt: null, // Will be filled in below
errorMessage: record.error_message || null,
@@ -303,7 +339,7 @@ export class ChangeHistory {
if (!seenReverts.has(revert.name) && statuses.has(revert.name)) {
const status = statuses.get(revert.name)!;
- status.revertedAt = hydrateDate(revert.executed_at);
+ status.revertedAt = hydrateDate(revert.executed_at, this.#dialect);
seenReverts.add(revert.name);
}
@@ -1055,7 +1091,7 @@ export class ChangeHistory {
status: r.status,
// Non-null: executed_at is NOT NULL with a CURRENT_TIMESTAMP
// default, always populated on write (see createOperation).
- executedAt: hydrateDate(r.executed_at)!,
+ executedAt: hydrateDate(r.executed_at, this.#dialect)!,
executedBy: r.executed_by,
durationMs: r.duration_ms,
errorMessage: r.error_message || null,
@@ -1130,7 +1166,7 @@ export class ChangeHistory {
status: r.status,
// Non-null: executed_at is NOT NULL with a CURRENT_TIMESTAMP
// default, always populated on write (see createOperation).
- executedAt: hydrateDate(r.executed_at)!,
+ executedAt: hydrateDate(r.executed_at, this.#dialect)!,
executedBy: r.executed_by,
durationMs: r.duration_ms,
errorMessage: r.error_message || null,
diff --git a/src/tui/screens/db/DbListScreen.tsx b/src/tui/screens/db/DbListScreen.tsx
index 6dd6f763..afcf9b93 100644
--- a/src/tui/screens/db/DbListScreen.tsx
+++ b/src/tui/screens/db/DbListScreen.tsx
@@ -323,7 +323,7 @@ export function DbListScreen({ params: _params }: ScreenProps): ReactElement {
- [c] Create - Build database from SQL files
+ [c] Create - Create database and tracking tables
[d] Destroy - Drop all managed objects
diff --git a/tests/core/change/history.test.ts b/tests/core/change/history.test.ts
index e0b4973b..a494ece7 100644
--- a/tests/core/change/history.test.ts
+++ b/tests/core/change/history.test.ts
@@ -56,17 +56,43 @@ describe('change: history — hydrateDate', () => {
// parses SQLite offset-less CURRENT_TIMESTAMP text as local time,
// silently shifting the result by the host UTC offset. Empirically
// verified pair from the spec (host TZ America/New_York, -240min).
- const hydrated = hydrateDate('2026-07-12 09:02:59');
+ const hydrated = hydrateDate('2026-07-12 09:02:59', 'sqlite');
expect(hydrated).toBeInstanceOf(Date);
expect(hydrated?.toISOString()).toBe('2026-07-12T09:02:59.000Z');
});
- it('should pass a Date through unchanged (pg/mysql/mssql shape)', () => {
+ it('should reinterpret a pg Date as UTC, not local time', () => {
+ // `executed_at` is `timestamp` (no zone) and noorm writes UTC into it,
+ // but `pg` reads that text back through the host zone. A row stored at
+ // 09:02:59 UTC therefore arrives as a Date meaning 09:02:59 LOCAL --
+ // four hours off on this block's pinned America/New_York. Constructed
+ // the same way the driver does, from local calendar fields.
+ const asDriverReturnsIt = new Date(2026, 6, 12, 9, 2, 59);
+ const hydrated = hydrateDate(asDriverReturnsIt, 'postgres');
+
+ expect(hydrated?.toISOString()).toBe('2026-07-12T09:02:59.000Z');
+
+ });
+
+ it('should reinterpret a mysql Date as UTC, not local time', () => {
+
+ // mysql2 was measured to have the identical skew as pg.
+ const asDriverReturnsIt = new Date(2026, 6, 12, 9, 2, 59);
+ const hydrated = hydrateDate(asDriverReturnsIt, 'mysql');
+
+ expect(hydrated?.toISOString()).toBe('2026-07-12T09:02:59.000Z');
+
+ });
+
+ it('should pass an mssql Date through unchanged', () => {
+
+ // tedious was never measured, so mssql keeps its existing behavior
+ // rather than risking a correction in the wrong direction.
const original = new Date('2026-07-12T09:02:59.000Z');
- const hydrated = hydrateDate(original);
+ const hydrated = hydrateDate(original, 'mssql');
expect(hydrated).toBe(original);
@@ -74,13 +100,13 @@ describe('change: history — hydrateDate', () => {
it('should return null for null input', () => {
- expect(hydrateDate(null)).toBeNull();
+ expect(hydrateDate(null, 'postgres')).toBeNull();
});
it('should return null for undefined input', () => {
- expect(hydrateDate(undefined)).toBeNull();
+ expect(hydrateDate(undefined, 'postgres')).toBeNull();
});