diff --git a/.changeset/khaki-jars-repeat.md b/.changeset/khaki-jars-repeat.md new file mode 100644 index 00000000..aad52bba --- /dev/null +++ b/.changeset/khaki-jars-repeat.md @@ -0,0 +1,11 @@ +--- +'@noormdev/sdk': patch +--- + +Put `types` first in the package exports map + +Export conditions are matched in order, so `types` sitting after `import` is +resolvable only by luck — it works today because there is no `require` +condition to shadow it, and would silently stop working the moment one was +added. `publint` reports it as an error. Also normalized both packages' +`repository.url` to the full `git+https://…​.git` form npm expects. diff --git a/.changeset/olive-pugs-shave.md b/.changeset/olive-pugs-shave.md new file mode 100644 index 00000000..76c8f289 --- /dev/null +++ b/.changeset/olive-pugs-shave.md @@ -0,0 +1,11 @@ +--- +'@noormdev/cli': patch +--- + +Read change-history timestamps as UTC on postgres and mysql + +`executed_at` has no time zone and noorm writes UTC into it, but `pg` and +`mysql2` both read that back through the host's local zone. On a UTC-4 host a +change applied a second ago was reported as four hours in the future, which +surfaced in the TUI as "Applied ... in 4 hours" on the home screen and in +change history. MSSQL is unchanged — its driver was not measured. diff --git a/.claude/atomic.toml b/.claude/atomic.toml new file mode 100644 index 00000000..d749d1b1 --- /dev/null +++ b/.claude/atomic.toml @@ -0,0 +1 @@ +scope = "repo" diff --git a/CLAUDE.md b/CLAUDE.md index 86c63f57..9054b32b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -162,40 +162,47 @@ For setup wizards where the target database may not exist yet, use `testConnecti Consistent hotkey conventions across all screens: -**Home navigation:** +**Home navigation** (`src/tui/screens/home.tsx`): | Key | Action | |-----|--------| +| `r` | run | | `c` | config | | `g` | changes | -| `r` | run | | `d` | db | -| `l` | lock | +| `+` | more (settings, vault, identity, lock) | | `s` | settings | -| `k` | secrets (keys) | +| `v` | vault | | `i` | identity | +| `l` | lock | +| `u` | update | +| `1` / `2` / `3` | quick actions: run build, change ff, lock status | | `q` | quit | +There is no `k` on Home — secrets belong to a config, so `k` opens them from +the config list. + **Common actions (sub-screens):** | Key | Action | Mnemonic | |-----|--------|----------| | `a` | add | | | `e` | edit | | | `d` | delete | | -| `x` | export | e**x**port | -| `i` | import | | -| `u` | use/activate | | -| `v` | validate | | -| `k` | secrets | **k**eys | +| `k` | secrets | **k**eys (from the config list) | +| `+` | more | export / import / validate live here, not on the list | +| `Enter` | use/activate | selecting a config activates it | **Context-dependent keys:** -- `[i]` = identity on Home, import in sub-screens -- `[x]` = export where applicable, extend in Lock Status +- `[i]` = identity on Home, import on the config More screen +- `[x]` = export on the config More screen, extend in Lock Status - `[s]` = settings on Home, status in Lock List +- `[c]` = config on Home, copy on the config list, create on the DB screen **Global shortcuts (available everywhere):** | Key | Action | |-----|--------| | `Shift+L` | Toggle log viewer overlay | +| `Shift+Q` | Open the SQL terminal | +| `?` | Show help | Use `numberNav` prop on `SelectList` for 1-9 quick selection in lists. diff --git a/README.md b/README.md index 028e4c91..eac7d592 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,26 @@ +
+ # noorm -A CLI for SQL-first database development. Manage SQL files, track changes, and run builds across dev, staging, and production — no ORM required. +### Write SQL. Skip the ORM. + +A SQL-first schema and change manager for **PostgreSQL**, **MySQL**, **SQLite**, and **SQL Server**.
+Your schema lives in SQL files. noorm builds it, versions it, and keeps every environment in sync. + +[![@noormdev/cli](https://img.shields.io/npm/v/@noormdev/cli?color=E05742&label=%40noormdev%2Fcli)](https://www.npmjs.com/package/@noormdev/cli) +[![@noormdev/sdk](https://img.shields.io/npm/v/@noormdev/sdk?color=916336&label=%40noormdev%2Fsdk)](https://www.npmjs.com/package/@noormdev/sdk) +[![CI](https://github.com/noormdev/noorm/actions/workflows/ci.yml/badge.svg)](https://github.com/noormdev/noorm/actions/workflows/ci.yml) +[![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE) + +**[Documentation](https://noorm.dev)** · **[Getting started](https://noorm.dev/getting-started/installation)** · **[Terminal UI](https://noorm.dev/tui)** · **[SDK](https://noorm.dev/reference/sdk)** + +
-**[Documentation](https://noorm.dev)** | **[Getting Started](https://noorm.dev/getting-started/installation)** | **[npm](https://www.npmjs.com/package/@noormdev/cli)** +
+noorm's terminal UI: adding a config, creating the database, building the schema, fast-forwarding changes, and browsing the result in the schema explorer + +
## Install @@ -17,64 +34,63 @@ Or via npm: npm install -g @noormdev/cli ``` -The install script is [fully transparent and open source](https://github.com/noormdev/noorm/blob/master/install.sh) — it downloads a prebuilt binary from [GitHub Releases](https://github.com/noormdev/noorm/releases), built from this repository's source. It installs to a user-writable directory already in your PATH when possible (e.g. `~/.local/bin`), falling back to `~/.local/bin` if none is found. +The install script is [fully transparent and open source](https://github.com/noormdev/noorm/blob/master/install.sh) — it downloads a prebuilt binary from [GitHub Releases](https://github.com/noormdev/noorm/releases), built from this repository's source. It installs to a user-writable directory already in your PATH when possible, falling back to `~/.local/bin`. > Corporate network? Use the GitHub mirror: > `curl -fsSL https://raw.githubusercontent.com/noormdev/noorm/master/install.sh | sh` -### Coding agents - -If you use an AI coding agent, install the noorm skill so it writes against the real conventions instead of guessing: +## Quick start ```bash -npx skills add noormdev/noorm/skills -``` - -It teaches the agent the SDK surface, the CLI's headless flags, template syntax, and config layout — the parts that are easy to get subtly wrong. +# Bootstrap a project — creates your identity and the sql/ and changes/ layout +noorm init +# Add a database config +noorm ui +``` -## What It Does +Adding a config is the one step with no headless equivalent: it needs connection details and a live connection test, so `noorm config add` sends you to the TUI. From there, **[c] Config → [a] Add**, then **[r] Run → [b] Build** to execute your SQL files. -- **SQL files** define your current schema — no migration archaeology -- **Changes** evolve existing databases from any state to current -- **Stages** manage dev, staging, and production with different configs -- **SDK** provides type-safe programmatic access to your database +With a config in place, everything else runs headlessly: -You write SQL. noorm executes it, tracks what ran, and keeps multiple environments in sync. +```bash +noorm run build # Build the schema from SQL files +noorm change ff # Apply pending changes +noorm db explore --json # Inspect the database as JSON +``` -Supports **PostgreSQL**, **MySQL**, **SQLite**, and **SQL Server**. +In CI there is no TUI to fall back on, so bootstrap from `NOORM_*` environment variables instead: +```bash +noorm ci init --name ci # reads NOORM_IDENTITY_* and NOORM_CONNECTION_* +noorm run build +``` -## Quick Start -```bash -# Initialize a project -noorm init +## How it works -# Launch the interactive TUI -noorm ui +Migration tools make you describe your schema twice: once in the migrations that built it, and once in your head. The current state only exists if you replay every file in order. -# Or run commands headlessly -noorm config add -noorm run build -noorm change ff -``` +noorm inverts that. -From the TUI: +| | | +|---|---| +| **SQL files** | Are your current schema. A fresh database runs them and is done. | +| **Changes** | Move an existing database from any state to current — forward/revert pairs that noorm tracks, checksums, and applies in order. | +| **Stages** | Keep dev, staging, and production apart, with access roles per environment. | +| **SDK** | Wraps it in a type-safe client — Kysely queries, stored procedures, and TVFs. | -1. **[i] Identity** — Set your name (for team tracking) -2. **[c] Config → [a] Add** — Create a database config -3. **[r] Run → Build** — Execute your SQL files +You write SQL. noorm executes it, tracks what ran, and keeps environments in sync. ## Why noorm? -ORMs push you toward surrogate IDs on every table and join-heavy queries. Proper relational design uses inherited keys, basetype-subtypes, and compound constraints — things ORMs can't express. +ORMs push you toward a surrogate ID on every table and join-heavy queries. Proper relational design uses inherited keys, basetype-subtypes, and compound constraints — things ORMs can't express, and that migration tools make painful to maintain. noorm lets you write the SQL your database was designed for, then manages execution across environments. -Read more: [noorm.dev](https://noorm.dev) +Read the argument in full: **[The case for proper relational design](https://noorm.dev/guide/relational-design)** ## SDK @@ -85,9 +101,22 @@ For programmatic access in your applications: npm install @noormdev/sdk kysely ``` -Build type-safe domain classes — consumers (queries), producers (mutations), and guards (validation). One package, used everywhere. +Build a dedicated database package with a domain class per area of your schema, each holding a typed `Context`. Kysely types mirror your database, integration tests run against a real one, and the same package works in servers, workers, and CLIs. + +**[Building your SDK →](https://noorm.dev/getting-started/building-your-sdk)** + + +## Coding agents + +If you use an AI coding agent, install the noorm skill so it writes against the real conventions instead of guessing: + +```bash +npx skills add noormdev/noorm/skills +``` + +It teaches the agent the SDK surface, the CLI's headless flags, template syntax, and config layout — the parts that are easy to get subtly wrong. -[SDK Documentation](https://noorm.dev/getting-started/building-your-sdk) +noorm also runs as an [MCP server](https://noorm.dev/guide/automation/mcp), behind per-channel access roles: admin at your terminal, read-only for the agent, or invisible entirely. ## License diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index fa9daef6..97599674 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -2,10 +2,14 @@ import { defineConfig } from 'vitepress'; import { withMermaid } from 'vitepress-plugin-mermaid'; // https://vitepress.dev/reference/site-config +const SITE_URL = 'https://noorm.dev'; +const TITLE = 'noorm — Write SQL. Skip the ORM.'; +const DESCRIPTION = 'A SQL-first schema and change manager for Postgres, MySQL, SQLite, and MSSQL. Your schema lives in SQL files. noorm builds it, versions it, and keeps every environment in sync.'; + export default withMermaid( defineConfig({ title: 'noorm', - description: 'Database Schema & Change Manager', + description: DESCRIPTION, base: process.env.VITEPRESS_BASE || '/', // `docs/wiki/` is generated repo-analysis output for tooling and @@ -14,9 +18,45 @@ export default withMermaid( // (``) is parsed as a tag with an illegal // attribute and fails the build — which is why the site stopped // deploying after 2026-07-04. - srcExclude: ['wiki/**'], + // + // The rest are the same class of thing: specs, design notes, scratch + // output, and the tape sources, none of which are published pages. + srcExclude: ['wiki/**', 'spec/**', 'design/**', 'superpowers/**', 'tmp/**', 'tapes/**'], + + markdown: { + // The terminal recordings are the heaviest assets on the site + // (tui.gif alone is ~1.6 MB) and none of them sit above the fold. + image: { lazyLoading: true }, + }, + + // `title` above only sets the suffix; titleTemplate gives the home + // page a real headline instead of the bare word "noorm". + titleTemplate: ':title · noorm', + + // Crawlers and chat apps do not run JS, so og:* must be static and absolute. + // The `title`/`description` config fields cover <title> and <meta name>; + // everything social has to be spelled out here. head: [ ['link', { rel: 'icon', href: '/image/logo.svg', type: 'image/svg+xml' }], + ['link', { rel: 'apple-touch-icon', href: '/image/logo.png' }], + ['link', { rel: 'canonical', href: `${SITE_URL}/` }], + ['meta', { name: 'theme-color', content: '#E05742' }], + + ['meta', { property: 'og:type', content: 'website' }], + ['meta', { property: 'og:site_name', content: 'noorm' }], + ['meta', { property: 'og:url', content: `${SITE_URL}/` }], + ['meta', { property: 'og:title', content: TITLE }], + ['meta', { property: 'og:description', content: DESCRIPTION }], + ['meta', { property: 'og:image', content: `${SITE_URL}/image/og.png` }], + ['meta', { property: 'og:image:width', content: '1200' }], + ['meta', { property: 'og:image:height', content: '630' }], + ['meta', { property: 'og:image:alt', content: 'noorm — Write SQL. Skip the ORM.' }], + + ['meta', { name: 'twitter:card', content: 'summary_large_image' }], + ['meta', { name: 'twitter:title', content: TITLE }], + ['meta', { name: 'twitter:description', content: DESCRIPTION }], + ['meta', { name: 'twitter:image', content: `${SITE_URL}/image/og.png` }], + ['link', { rel: 'preconnect', href: 'https://fonts.googleapis.com' }], ['link', { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' }], ['script', { async: '', src: 'https://www.googletagmanager.com/gtag/js?id=G-Y69K95866J' }], @@ -115,6 +155,7 @@ gtag('config', 'G-Y69K95866J');`], items: [ { text: 'CLI Reference', link: '/headless' }, { text: 'Terminal UI', link: '/tui' }, + { text: 'Relational Design', link: '/guide/relational-design' }, ], }, { @@ -150,6 +191,7 @@ gtag('config', 'G-Y69K95866J');`], collapsed: true, items: [ { text: 'Explorer', link: '/guide/database/explore' }, + { text: 'Transfer', link: '/guide/database/transfer' }, { text: 'Teardown', link: '/guide/database/teardown' }, { text: 'Terminal', link: '/guide/database/terminal' }, ], diff --git a/docs/.vitepress/og-source.html b/docs/.vitepress/og-source.html new file mode 100644 index 00000000..43b370e7 --- /dev/null +++ b/docs/.vitepress/og-source.html @@ -0,0 +1,135 @@ +<!doctype html> +<!-- + Source for public/image/og.png (the social-card image). Not built or served — + VitePress only publishes public/ and .vitepress/dist. Regenerate after editing: + + cd docs && "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ + --headless --disable-gpu --screenshot=public/image/og.png \ + --window-size=1200,630 --hide-scrollbars --virtual-time-budget=4000 \ + --allow-file-access-from-files .vitepress/og-source.html + + --virtual-time-budget gives the Geist webfonts time to land; without it the + card renders in the fallback face. +--> +<html> +<head> +<meta charset="utf-8"> +<link rel="preconnect" href="https://fonts.googleapis.com"> +<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> +<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;800&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet"> +<style> + * { margin: 0; padding: 0; box-sizing: border-box; } + + body { + width: 1200px; + height: 630px; + background: #161A20; + font-family: "Geist", system-ui, sans-serif; + color: #F2ECE0; + -webkit-font-smoothing: antialiased; + overflow: hidden; + position: relative; + } + + /* Hairline rule instead of a glow — the brand forbids glow effects and + treats a single 1px border as the only divider. */ + .edge { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: #E05742; + } + + .frame { + position: relative; + height: 100%; + padding: 72px 80px; + display: flex; + flex-direction: column; + justify-content: space-between; + } + + /* Bronze: ember is reserved for signal, eyebrows are the editorial register. */ + .eyebrow { + font-family: "Geist Mono", monospace; + font-size: 20px; + font-weight: 500; + letter-spacing: 0.16em; + text-transform: uppercase; + color: #D2A47A; + } + + .mark { + display: flex; + align-items: center; + gap: 20px; + margin-bottom: 26px; + } + + .mark img { width: 84px; } + + .wordmark { + font-family: "Geist Mono", monospace; + font-size: 56px; + font-weight: 500; + letter-spacing: -0.04em; + background: linear-gradient(135deg, #E05742 0%, #B47A45 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + } + + h1 { + font-size: 76px; + font-weight: 800; + letter-spacing: -0.035em; + line-height: 1.05; + } + + p { + margin-top: 22px; + font-size: 30px; + font-weight: 400; + line-height: 1.4; + color: #B8B3A8; + max-width: 830px; + } + + .footer { + display: flex; + align-items: center; + gap: 14px; + font-family: "Geist Mono", monospace; + font-size: 21px; + color: #7E8590; + } + + .sep { opacity: 0.45; } +</style> +</head> +<body> + <div class="edge"></div> + <div class="frame"> + <div class="eyebrow">Postgres · MySQL · SQLite · MSSQL</div> + + <div> + <div class="mark"> + <img src="../public/image/logo.svg" alt=""> + <span class="wordmark">noorm</span> + </div> + <h1>Write SQL. Skip the ORM.</h1> + <p>Your schema lives in SQL files. noorm builds it, versions it, and keeps every environment in sync.</p> + </div> + + <div class="footer"> + <span>single binary</span> + <span class="sep">·</span> + <span>type-safe SDK</span> + <span class="sep">·</span> + <span>noorm.dev</span> + </div> + </div> +</body> +</html> diff --git a/docs/.vitepress/theme/HeroEyebrow.vue b/docs/.vitepress/theme/HeroEyebrow.vue index f51864b9..20805fea 100644 --- a/docs/.vitepress/theme/HeroEyebrow.vue +++ b/docs/.vitepress/theme/HeroEyebrow.vue @@ -9,7 +9,13 @@ font-weight: 500; letter-spacing: 0.12em; text-transform: uppercase; - color: var(--vp-c-brand-1); + /* Bronze, not ember: the brand keeps ember for signal (primary CTA, cursor, + errors) and gives editorial lines like this one the bronze register. */ + color: #6E4A22; margin-bottom: 20px; } + +.dark .hero-eyebrow { + color: #D2A47A; +} </style> diff --git a/docs/.vitepress/theme/HeroTerminal.vue b/docs/.vitepress/theme/HeroTerminal.vue index db272e2d..9df0ee53 100644 --- a/docs/.vitepress/theme/HeroTerminal.vue +++ b/docs/.vitepress/theme/HeroTerminal.vue @@ -12,7 +12,7 @@ <div class="line"><span class="prompt">$</span> curl -fsSL <span class="url">https://noorm.dev/install.sh</span> | sh</div> <div class="line spacer"></div> <div class="line"><span class="prompt">$</span> cd /my/project && noorm init</div> - <div class="line"><span class="prompt">$</span> npm i <span class="arg">@noorm/sdk</span></div> + <div class="line"><span class="prompt">$</span> npm i <span class="arg">@noormdev/sdk</span></div> <div class="line spacer"></div> <div class="line"><span class="prompt">$</span> noorm run build<span class="cursor"></span></div> </div> @@ -25,7 +25,7 @@ import { ref } from 'vue' const copied = ref(false) function copy() { - const text = `curl -fsSL https://noorm.dev/install.sh | sh\ncd /my/project && noorm init\nnpm i @noorm/sdk\nnoorm run build` + const text = `curl -fsSL https://noorm.dev/install.sh | sh\ncd /my/project && noorm init\nnpm i @noormdev/sdk\nnoorm run build` navigator.clipboard.writeText(text) copied.value = true setTimeout(() => { copied.value = false }, 2000) @@ -36,6 +36,9 @@ function copy() { .hero-terminal { width: 100%; max-width: 520px; + margin: 0 auto; + /* The hero container centers its text below 960px; a terminal has to stay flush left. */ + text-align: left; border-radius: 10px; border: 1px solid var(--vp-c-border); background: var(--vp-code-block-bg, var(--vp-c-bg-alt)); @@ -85,6 +88,19 @@ function copy() { padding: 16px 20px; } +/* Phone widths: shrink the type so the longest command still lands on one line, + and let the body scroll rather than spill past the card border. */ +@media (max-width: 639px) { + .hero-terminal { + font-size: clamp(0.625rem, 2.7vw, 0.8125rem); + } + + .terminal-body { + padding: 14px 16px; + overflow-x: auto; + } +} + .line { white-space: pre; color: var(--vp-c-text-1); diff --git a/docs/.vitepress/theme/brand.css b/docs/.vitepress/theme/brand.css index 89e5b99f..15fcc595 100644 --- a/docs/.vitepress/theme/brand.css +++ b/docs/.vitepress/theme/brand.css @@ -210,6 +210,21 @@ code, kbd, samp, pre { color: var(--vp-c-text-3); } +/* ───────────────────────────────────────────── + Terminal recordings and TUI stills (docs/tapes/) + ───────────────────────────────────────────── */ +.vp-doc img[src$=".gif"], +.vp-doc img[src*="/image/tui/"] { + display: block; + width: 100%; + margin: 24px 0; + /* Same treatment as the code blocks these sit next to — the recordings are + terminal output, so they should read as the same kind of surface. */ + border-radius: 10px; + border: 1px solid var(--vp-c-border); + background: var(--vp-code-block-bg); +} + /* ───────────────────────────────────────────── Custom blocks — left border accent ───────────────────────────────────────────── */ @@ -234,6 +249,36 @@ code, kbd, samp, pre { visibility: hidden; } +/* The default theme only pins the nav at >=960px, so it scrolls away on phones. + Pin it here and pay back the height the fixed nav no longer occupies: + .VPContent gets the padding the theme adds above 960px, and .VPLocalNav's + sticky offset moves down to sit under the bar instead of behind it. + + The selectors are doubled on purpose. VitePress ships these as scoped styles + (`.VPNav[data-v-604a0865]`), which is specificity (0,2,0) — a bare `.VPNav` + is (0,1,0) and silently loses no matter what order the bundle lands in. + Doubling the class buys (0,2,0) and wins on source order, which brand.css + is guaranteed since theme/index.ts imports it after the default theme. */ +@media (max-width: 959px) { + .VPNav.VPNav { + position: fixed; + } + + .VPContent.VPContent { + padding-top: var(--vp-nav-height); + } + + .VPLocalNav.VPLocalNav { + top: var(--vp-nav-height); + } + + /* The home nav bar is transparent by design; once content scrolls under it, + it needs a surface. `.top` keeps the hero clean at rest. */ + .VPNavBar.home:not(.top) { + background-color: var(--vp-nav-bg-color); + } +} + .VPNavBarTitle.has-sidebar .title { border-bottom: 0; } @@ -319,6 +364,29 @@ code, kbd, samp, pre { border: 1px solid var(--vp-c-border); } +/* Below 960px the theme sizes the image slot as a fixed 320/392px square and + bleeds it upward under the nav — sized for a floating logo, not the terminal + card we render there. Size it to content and move it below the pitch. */ +/* Doubled selectors for the same reason as the nav block above — the theme's + scoped `.image[data-v-…]` would otherwise tie or win. */ +@media (max-width: 959px) { + .VPHero .image.image { + order: 3; + margin: 40px 0 0; + /* Flex items default to min-width:auto, so the terminal's `white-space: pre` + lines would set the slot's floor. Zeroing it lets the card shrink and + its own overflow-x take over. */ + min-width: 0; + } + + .VPHero .image-container.image-container { + width: 100%; + max-width: 100%; + height: auto; + min-width: 0; + } +} + .dark .VPHero .VPImage { box-shadow: 0 18px 40px -12px rgba(0, 0, 0, 0.35), 0 6px 12px rgba(0, 0, 0, 0.15); } @@ -346,6 +414,40 @@ code, kbd, samp, pre { color: var(--vp-c-text-2); } +/* ───────────────────────────────────────────── + Next steps — 3-up card row on the home page + ───────────────────────────────────────────── */ +.next-steps { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + margin-top: 24px; +} + +.next-steps p { + margin: 0; + padding: 20px; + height: 100%; + border: 1px solid var(--vp-c-border); + border-radius: 10px; + background: var(--vp-c-bg-soft); + font-size: 0.9375rem; + line-height: 1.6; + color: var(--vp-c-text-2); + transition: border-color 180ms ease-out; +} + +.next-steps p:hover { + border-color: var(--vp-c-brand-1); +} + +.next-steps a { + display: block; + margin-bottom: 4px; + font-size: 1rem; + text-decoration: none; +} + /* ───────────────────────────────────────────── Tables ───────────────────────────────────────────── */ diff --git a/docs/getting-started/first-build.md b/docs/getting-started/first-build.md index e3c44356..cc831e68 100644 --- a/docs/getting-started/first-build.md +++ b/docs/getting-started/first-build.md @@ -3,6 +3,10 @@ This tutorial takes about 5 minutes. By the end, you'll understand how noorm separates **schema definition** from **schema evolution**. +![Building a schema from SQL files, then fast-forwarding two pending changes](/image/build-and-change.gif) + +That is the whole loop: `run build` applies the SQL files, `change ff` walks an existing database forward, and `change history` records what ran. The rest of this page does it step by step. + ## What You'll Do diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 94e841c1..8ef60932 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -1,6 +1,9 @@ # Installation +![Installing noorm: the install script, creating an identity, and bootstrapping a project](/image/install.gif) + + ## Requirements - A supported database: diff --git a/docs/guide/relational-design.md b/docs/guide/relational-design.md new file mode 100644 index 00000000..6c106538 --- /dev/null +++ b/docs/guide/relational-design.md @@ -0,0 +1,66 @@ +--- +title: The case for proper relational design +description: Why inherited keys and basetype-subtypes beat the surrogate-ID-everywhere pattern ORMs push you toward. +--- + +# The case for proper relational design + + +noorm does not stop you from designing a database the way an ORM would. It just stops making that the only option you can reach. This page explains what you get back once the tool is out of the way. + + +## Inherited keys + +ORMs push you toward a pattern: every table gets a surrogate ID, relationships happen through foreign keys, and you join your way back to find what you need. It works—until you're seven joins deep trying to figure out which user owns a deeply nested entity, and your messy left joins are adding NULL rows or creating cartesian products. + +Proper relational design uses **inherited keys**. Instead of giving every entity an independent identity, child entities inherit their parent's key as part of their own. + +**Example: A todo list** + +``` +users + → user_id (surrogate, this is the root) + +todos + → user_id + created_at (inherits from user, no separate todo_id) + +todo_items + → user_id + created_at + item_index (inherits from todo) +``` + +With inherited keys, a `todo_item` carries its lineage in its identity. You don't need joins to find the user—it's right there in the key. The deeper your schema goes, the more this matters. + +*Try working that into your ORM. I'll wait...* + + +## ~~Polymorphism~~ Basetype-subtypes + +ORMs love polymorphic associations: a `comments` table with `commentable_type` and `commentable_id`. Fast, flexible—and completely breaks referential integrity. Complex app logic, no foreign keys, slow and awkward statistics, and even more awkward queries. + +Proper relational design solved this years ago with **basetype-subtypes**: + +``` +independent entities: user, group +dependent entities: profile +basetype-subtypes: post → user_post, group_post + photo → user_photo, group_photo, profile_photo, user_post_photo, ... + comment → user_comment, group_comment, post_comment, comment_comment, ... + tag → post_tag, photo_tag, comment_tag, ... +``` + +Each relationship gets its own table with proper constraints against its parent. A `user_post` has a foreign key to `user` and `post`. A `group_photo` has a foreign key to `group` and `photo`. No nulls, no type columns, no ambiguity. + +You work with existence and non-existence—not "maybe exists" or calculate. You depend on physical existence, not hopeful logic. Statistics are straightforward. Queries are clean. The database enforces integrity at every level. Illegal states become impossible. The trade-off is more tables, but the benefit is less app logic. + +You pay for bad relational design later in complexity and bugs. + + +## What this requires from a tool + +Both patterns need things ORM-shaped migration tools make hard: + +- **Compound primary keys** that you declare, not ones the tool derives from a single ID column. +- **Many more tables** than a naive design, which means execution order matters and has to be explicit. +- **Constraints, triggers, and procedures** as first-class schema objects, not escape-hatch raw SQL bolted onto a migration. + +noorm gives you all three because it never parses your SQL into an object model. Your files are the schema. See [SQL File Organization](/guide/sql-files/organization) for how execution order works, and [Concepts](/getting-started/concepts) for how files and changes divide the work. diff --git a/docs/index.md b/docs/index.md index 3275c445..81a2f03b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,10 +1,13 @@ --- layout: home +title: noorm — Write SQL. Skip the ORM. +titleTemplate: false +description: A SQL-first schema and change manager for Postgres, MySQL, SQLite, and MSSQL. Your schema lives in SQL files. noorm builds it, versions it, and keeps every environment in sync. hero: name: noorm text: "Write SQL. Skip the ORM." - tagline: "Define your schema as files. Deploy it to any environment." + tagline: "The raw SQL manager people keep asking for. Schema in files, changes in git, one CLI to prod." actions: - theme: brand text: "Get started →" @@ -14,83 +17,60 @@ hero: link: https://github.com/noormdev/noorm features: + - icon: + src: /icons/database.svg + title: Schema lives in SQL files + details: Your SQL files are the current schema. Fresh databases build from them in seconds. Existing ones catch up through versioned changes. + link: /getting-started/concepts + linkText: How it works + - icon: + src: /icons/cubes.svg + title: Real relational design + details: Compound keys, inherited keys, check constraints, subtype clusters. Model what your data actually is instead of one surrogate ID per table. + link: /guide/relational-design + linkText: The case for it + - icon: + src: /icons/git-branch.svg + title: Procedures, functions, TVFs + details: "Call stored procedures from TypeScript with typed params and result rows. Table-valued parameters included. The bridge ORMs never built." + link: /reference/sdk#stored-procedures-functions-tvfs + linkText: SDK reference - icon: src: /icons/fast-forward.svg - title: Current Schema, Always - details: SQL files define the schema as it exists today. Fresh databases build in seconds from these files. Existing databases get there through changes. + title: Data pipelines + details: Move data between databases in foreign-key order, across dialects, or out to portable files. Seed a QA environment from staging in one command. + link: /guide/database/transfer + linkText: Transfer data - icon: - src: /icons/database.svg - title: Full Relational SQL - details: Compound keys, check constraints, triggers, stored procedures. You write whatever SQL your database supports. + src: /icons/lock.svg + title: Safe to point an agent at + details: An MCP server exposes noorm to coding agents behind per-channel access roles. Admin at your terminal, read-only for the agent, invisible for prod. + link: /guide/automation/mcp + linkText: AI integration - icon: src: /icons/toolbox.svg - title: Built-in Tools - details: Schema explorer, SQL terminal, dynamic templates, encrypted secrets. One CLI replaces five browser tabs. - - icon: - src: /icons/cubes.svg - title: Type-Safe SDK - details: "Build your API layer with domain classes: consumers for queries, producers for mutations. One package shared across services and frontends." + title: One CLI, not five tabs + details: Schema explorer, SQL terminal, encrypted vault, dynamic templates. Every command runs headless and emits JSON you can pipe into CI. + link: /headless + linkText: CLI reference --- ## Why noorm? -noorm is a command-line tool for SQL-first database development. You write compound keys, check constraints, triggers, stored procedures. noorm executes them, tracks what ran, and keeps environments in sync. - -**What it does:** - -- **SQL files** define your current schema -- **Changes** move existing databases from any state to current -- **Stages** separate dev, staging, and production configs -- **SDK** gives you type-safe programmatic access - -### The Case for Proper Relational Design - -ORMs push you toward a pattern: every table gets a surrogate ID, relationships happen through foreign keys, and you join your way back to find what you need. It works—until you're seven joins deep trying to figure out which user owns a deeply nested entity, and your messy left joins are adding NULL rows or creating cartesian products. - -Proper relational design uses **inherited keys**. Instead of giving every entity an independent identity, child entities inherit their parent's key as part of their own. - -**Example: A todo list** - -``` -users - → user_id (surrogate, this is the root) - -todos - → user_id + created_at (inherits from user, no separate todo_id) - -todo_items - → user_id + created_at + item_index (inherits from todo) -``` - -With inherited keys, a `todo_item` carries its lineage in its identity. You don't need joins to find the user—it's right there in the key. The deeper your schema goes, the more this matters. - -*Try working that into your ORM. I'll wait...* - - -### Furthermore: ~~Polymorphism~~ Basetype-Subtypes - -ORMs love polymorphic associations: a `comments` table with `commentable_type` and `commentable_id`. Fast, flexible—and completely breaks referential integrity. Complex app logic, no foreign keys, slow and awkward statistics, and even more awkward queries. +Migration tools make you describe your schema twice: once in the migrations that built it, and once in your head. The current state only exists if you replay every file in order, and the moment you need a compound key or a trigger you are writing raw SQL inside a wrapper that was designed to keep you away from it. -Proper relational design solved this years ago with **basetype-subtypes**: +noorm inverts that. **Your SQL files are the current schema.** A fresh database runs them and is done. An existing database gets to the same place through **changes** — small forward/revert pairs that noorm tracks, checksums, and applies in order. -``` -independent entities: user, group -dependent entities: profile -basetype-subtypes: post → user_post, group_post - photo → user_photo, group_photo, profile_photo, user_post_photo, ... - comment → user_comment, group_comment, post_comment, comment_comment, ... - tag → post_tag, photo_tag, comment_tag, ... -``` - -Each relationship gets its own table with proper constraints against its parent. A `user_post` has a foreign key to `user` and `post`. A `group_photo` has a foreign key to `group` and `photo`. No nulls, no type columns, no ambiguity. +Everything else follows from that split: -You work with existence and non-existence—not "maybe exists" or calculate. You depend on physical existence, not hopeful logic. Statistics are straightforward. Queries are clean. The database enforces integrity at every level. Illegal states become impossible. The trade-off is more tables, but the benefit is less app logic. +- **Stages** keep dev, staging, and production configs apart, with access roles per environment +- **Templates** let one SQL file render differently per environment +- **The SDK** wraps it all in a type-safe client — Kysely queries, stored procedures, and TVFs +- **Headless mode** makes every command scriptable, with `--json` on anything worth parsing -You pay for bad relational design later in complexity and bugs. - -## Quick Start +## Quick start ```bash # Install (no sudo needed) @@ -99,42 +79,23 @@ curl -fsSL https://noorm.dev/install.sh | sh # Or via npm npm install -g @noormdev/cli -# Launch the interactive TUI -noorm ui +# Bootstrap a project +cd /my/project && noorm init ``` -> **Corporate network?** If `noorm.dev` is blocked, use the GitHub mirror: -> ```bash -> curl -fsSL https://raw.githubusercontent.com/noormdev/noorm/master/install.sh | sh -> ``` - -From the interactive TUI (`noorm ui`), set up your project: - -1. **[i] Identity** — Set your name (for team tracking) -2. **[c] Config → [a] Add** — Create a database config -3. **[r] Run → Build** — Execute your SQL files - -Or use the CLI directly. Commands run headlessly and emit structured JSON you can pipe into scripts: - +::: tip Corporate network? +If `noorm.dev` is blocked, install from the GitHub mirror: ```bash -noorm run build # Build the schema from SQL files -noorm change ff # Apply all pending changes -noorm db explore --json # Inspect the database as JSON -noorm vault set API_KEY ... # Push a team secret to the encrypted vault +curl -fsSL https://raw.githubusercontent.com/noormdev/noorm/master/install.sh | sh ``` +::: -Wizard-only operations (`config add`, `config edit`, secret management) launch the TUI automatically. Set them up once via `noorm ui`, then run everything else from scripts. - -Create your SQL files: +Write a SQL file, then build it: ```bash mkdir -p sql/01_tables echo "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT);" > sql/01_tables/001_users.sql -``` - -Build your schema: -```bash noorm run build ``` @@ -142,27 +103,35 @@ noorm run build ✓ Executed 1 file ``` -Now your schema needs to evolve. Update your SQL file AND create a change: +When the schema evolves, update the SQL file **and** add a change. The file keeps describing what the schema is; the change tells existing databases how to catch up: ```bash -# Update sql/01_tables/001_users.sql (add email column) -# Create changes/2024-01-add-email/forward.sql +# Edit sql/01_tables/001_users.sql to add an email column +# Add changes/2024-01-add-email/forward.sql -noorm change ff # Fast-forward: apply pending changes +noorm change ff # fast-forward: apply pending changes ``` -Need a fresh test database? Add another config and build—no changes needed: +A fresh database skips all of that — it just runs the files: ```bash -noorm ui # Use the wizard to add a `test` config noorm config use test -noorm run build # Fresh DB gets current schema directly +noorm run build # fresh DB gets the current schema directly ``` -**SQL files = current schema. Changes = how to get existing databases there.** +**SQL files = current schema. Changes = how existing databases get there.** + +Setup wizards (`config add`, `config edit`, secret management) run in the TUI — launch it with `noorm ui`. Everything else runs headless: + +```bash +noorm run build # Build the schema from SQL files +noorm change ff # Apply all pending changes +noorm db explore --json # Inspect the database as JSON +noorm vault set API_KEY ... # Push a team secret to the encrypted vault +``` -## Next Steps +## Next steps <div class="next-steps"> 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 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path fill="#3B82F6" d="M176 168C189.3 168 200 157.3 200 144C200 130.7 189.3 120 176 120C162.7 120 152 130.7 152 144C152 157.3 162.7 168 176 168zM256 144C256 176.8 236.3 205 208 217.3L208 288L384 288C410.5 288 432 266.5 432 240L432 217.3C403.7 205 384 176.8 384 144C384 99.8 419.8 64 464 64C508.2 64 544 99.8 544 144C544 176.8 524.3 205 496 217.3L496 240C496 301.9 445.9 352 384 352L208 352L208 422.7C236.3 435 256 463.2 256 496C256 540.2 220.2 576 176 576C131.8 576 96 540.2 96 496C96 463.2 115.7 435 144 422.7L144 217.4C115.7 205 96 176.8 96 144C96 99.8 131.8 64 176 64C220.2 64 256 99.8 256 144zM488 144C488 130.7 477.3 120 464 120C450.7 120 440 130.7 440 144C440 157.3 450.7 168 464 168C477.3 168 488 157.3 488 144zM176 520C189.3 520 200 509.3 200 496C200 482.7 189.3 472 176 472C162.7 472 152 482.7 152 496C152 509.3 162.7 520 176 520z"/></svg> \ 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 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#E05742" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="6" y1="3" x2="6" y2="15"></line><circle cx="18" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><path d="M18 9a9 9 0 0 1-9 9"></path></svg> \ 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 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path fill="#10B981" d="M256 160L256 224L384 224L384 160C384 124.7 355.3 96 320 96C284.7 96 256 124.7 256 160zM192 224L192 160C192 89.3 249.3 32 320 32C390.7 32 448 89.3 448 160L448 224C483.3 224 512 252.7 512 288L512 512C512 547.3 483.3 576 448 576L192 576C156.7 576 128 547.3 128 512L128 288C128 252.7 156.7 224 192 224z"/></svg> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#E05742" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg> \ 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" <<EOF +#!/usr/bin/env bash +exec node "$NOORM_BIN" "\$@" +EOF +chmod +x "$DEMO_ROOT/bin/noorm" + +# 01-install.tape deliberately does NOT use the shim — it runs the real +# install.sh, which defaults to \$HOME/.local/bin and therefore stays inside +# the sandbox HOME. +mkdir -p "$DEMO_ROOT/home/.local/bin" + +# Schema source. `demo` (default) is the 4-file project in demo-project/; +# `todo-db` is the full 60-file example. +# +# The default is deliberately small. noorm's diagnostic log prints one line per +# file, each carrying the absolute path twice (~250 chars), so 60 files means +# thousands of wrapped lines. VHS renders every frame, and a todo-db build does +# not finish inside a 5-minute wait — the recording times out before the build +# does. Four files keeps the GIF short, legible, and reproducible. +SCHEMA="${NOORM_DEMO_SCHEMA:-demo}" + +case "$SCHEMA" in + demo) + cp -R "$(dirname "${BASH_SOURCE[0]}")/demo-project/sql" "$DEMO_ROOT/project/" + cp -R "$(dirname "${BASH_SOURCE[0]}")/demo-project/changes" "$DEMO_ROOT/project/" + ;; + todo-db) + cp -R "$REPO_ROOT/examples/todo-db/sql" "$DEMO_ROOT/project/" + cp -R "$REPO_ROOT/examples/todo-db/changes" "$DEMO_ROOT/project/" + ;; + *) + echo "error: unknown NOORM_DEMO_SCHEMA '$SCHEMA' (demo|todo-db)" >&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 ``` +![A full pass through the TUI: adding a config, creating the database, building the schema, fast-forwarding changes, and browsing the result in the explorer](/image/tui.gif) + 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 -``` +![The noorm TUI home screen: status, quick actions, and recent activity](/image/tui/home.png) ## 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 -``` +![The configuration list screen](/image/tui/config-list.png) - `●` 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 +![The add-config form: name, dialect, connection details, and per-channel access roles](/image/tui/config-add.png) -``` -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 -``` +![The changes list, showing applied and pending changes](/image/tui/changes-list.png) - `✓` = 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: + +![Change execution history, with per-change status and the identity that applied it](/image/tui/change-history.png) + ### 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 -``` +![The run menu](/image/tui/run-menu.png) ### 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 -``` +![The database operations menu](/image/tui/database-menu.png) ### 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 -``` +![The schema explorer overview, with object counts by category](/image/tui/explore-overview.png) 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 -``` +![The explorer table list](/image/tui/explore-tables.png) 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 -``` +![Table detail: columns, indexes, and foreign keys](/image/tui/explore-table-detail.png) ### 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: + +![The built-in SQL terminal](/image/tui/sql-terminal.png) - 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 log viewer overlay, opened with Shift+L](/image/tui/log-viewer.png) + +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: + +![The More Options menu: settings, vault, identity, and lock](/image/tui/more-menu.png) + +Each of these also has a direct key from home — `+` just groups them. + + +### Settings + +![The settings screen, listing the seven setting groups](/image/tui/settings.png) + +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 + +![The identity screen, showing the current keypair and fingerprint](/image/tui/identity.png) + +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 screen before initialization](/image/tui/vault.png) + +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: + +![The secrets screen for a config](/image/tui/secrets.png) + +These are values a config needs at connection or render time. See +[Secrets](/guide/environments/secrets) for how they resolve against stages. + + +### Lock + +![The lock screen, showing current lock state](/image/tui/lock.png) + +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@<version>/noorm-<os>-<arch>`, 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 <path>` (and rejects any other flag) before the subcommand name, the only root-level flag; `rewriteBareSqlArgv` inserts a synthetic `query` token when `noorm sql "<SQL-looking text>"` 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 <path>` (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 <sub>` +- [`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@<version>`) 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 <path>` 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:<role> agent:<role|off>`) 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 <path>` 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-<description>`. -- `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-<slugified-description>` (`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 <create|drop|explore|reset|teardown|transfer|truncate>` 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 = <resolved db>` 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 <config>`, `--export <path>`, `--import <path>`); `.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 <name>`, 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 <name>`, 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<ConnectionEvents>` 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<Dialect, ...>` (or `Partial<Record<Dialect, ...>>` 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 <harness>)` 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/<config>.json` plus gzipped per-query results under `.noorm/state/history/<config>/` +- [`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 <months>` +- [`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-<config>` (`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-<config>` (`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 <path>`; 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 <path>`; 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 <path>`; executes a single file via `ctx.noorm.run.file`. +- [`src/cli/run/files.ts`](../../src/cli/run/files.ts) — `run files --paths <a,b,...>`; comma-separated file list via `ctx.noorm.run.files`. +- [`src/cli/run/inspect.ts`](../../src/cli/run/inspect.ts) — `run inspect <path>`; 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 <path>`; 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 `<projectRoot>/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 `<root>-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 `.<pid>.<random>.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 --- <wiki-type>repo</wiki-type> -<scan-sha>33101f0bf56f53ee3568f149bd7f99afd795ead2</scan-sha> +<scan-sha>bf245fd116209d22ea6385945f0d255d7dc8af92</scan-sha> <wiki-schema>1</wiki-schema> # 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-<suffix>`; 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-`<promise>DONE</promise>` 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 `<steering note: ...>` 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<string, Context>`) 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<name, RpcCommand>` 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<name, RpcCommand>` 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 <name>(...)` — 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 `<FocusProvider>`/`<NoormObserver>` (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<TEvents, Record<string, never>>` 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<T>` class: `add(index, item)` buffers into a `Map<number, T>` 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>` ([``](../..) `${K}:res` [``](../..) template type), `Correlated<T>` (`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<ComputeEvents>` 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<string, WorkerBridge<ConnectionEvents>>` 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<T>`) 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<Dialect> = 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 { <Panel title="Available Actions" paddingX={1} paddingY={1}> <Box flexDirection="column" gap={1}> <Text> - <Text color="cyan">[c]</Text> Create - Build database from SQL files + <Text color="cyan">[c]</Text> Create - Create database and tracking tables </Text> <Text> <Text color="cyan">[d]</Text> 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(); });