diff --git a/development/architecture.mdx b/development/architecture.mdx
deleted file mode 100644
index daf6b2c8..00000000
--- a/development/architecture.mdx
+++ /dev/null
@@ -1,121 +0,0 @@
----
-title: Architecture
-description: How the app, plugin system, and core services fit together
----
-
-import DriverCounts from "/snippets/driver-counts.mdx";
-
-Every database driver is a bundle the app loads at runtime. That decision shapes the rest: nothing switches on database type, no driver list is compiled in, and `DatabaseType` is a string-based struct rather than an enum, so a type from a plugin the app has never heard of still round-trips through Codable.
-
-The UI is SwiftUI with AppKit underneath it for windows, menus, and the data grid. Async work is Swift concurrency throughout. Database connectivity is native C libraries, linked as static `.a` files out of `Libs/`.
-
-[`CLAUDE.md`](https://github.com/TableProApp/TablePro/blob/main/CLAUDE.md) in the repository root is the authority on the rules a change has to satisfy: the invariants that have caused real bugs, the ABI policy, and what has to happen before a commit lands. This page is the map of where things live, not the rulebook.
-
-## Plugin system
-
-`PluginManager` (`Core/Plugins/`) discovers bundles, version-checks them, and loads them. Four pieces carry the whole system:
-
-| Component | Location | Role |
-|-----------|----------|------|
-| TableProPluginKit | `Plugins/TableProPluginKit/` | Shared framework: the `TableProPlugin`, `DriverPlugin`, and `PluginDatabaseDriver` protocols plus the transfer types |
-| PluginManager | `Core/Plugins/PluginManager.swift` | Discovers, validates, and loads plugin bundles |
-| PluginDriverAdapter | `Core/Plugins/PluginDriverAdapter.swift` | Bridges `PluginDatabaseDriver` to the app's internal `DatabaseDriver` |
-| DatabaseDriverFactory | `Core/Database/DatabaseDriver.swift` | Resolves a `DatabaseType` to a loaded plugin |
-
-`Packages/TableProCore/Sources/TableProPluginKit` is a symlink to the same files, so edit only the copy under `Plugins/TableProPluginKit/`.
-
-
-
-A driver is one of three roles a bundle can take. `ExportFormatPlugin` and `ImportFormatPlugin` are the others, which is how the CSV, JSON, SQL, XLSX, and MQL formats ship. `DocumentInspectorPlugin` is still declared in the kit, and the app no longer loads a bundle that provides it.
-
-### Opt-in protocols
-
-A driver adopts these on top of `PluginDatabaseDriver` to reach extra surfaces. The app finds them with a runtime cast, so skipping one costs nothing and no ABI bump is involved.
-
-| Protocol | What adopting it gets you |
-|----------|---------------------------|
-| `PluginDiagnosticProvider` | A driver-written explanation in front of a raw connection error |
-| `PluginProcedureFunctionSupport` | Stored procedures and functions in the structure tab |
-| `PluginDefaultSortProvider` | Per-table control of default sorting: `.useAppDefault`, `.suppress`, or `.forceColumns([String])` |
-| `PluginPrincipalManagement` | Users, roles, and grants in the Users & Roles tab |
-| `PluginBrowseFilterProvider` | Control of the sidebar object filter: glob syntax, case sensitivity, type scopes |
-| `SettablePlugin` | A settings pane of your own under **Settings > Plugins** |
-
-### PluginKit ABI
-
-TableProPluginKit builds with `BUILD_LIBRARY_FOR_DISTRIBUTION = YES`, so its public ABI is resilient: the Swift runtime fills any requirement an already-built plugin never implemented from the protocol's default, and that plugin keeps loading under a newer app. Adding a requirement with a default is free. Removing one, or adding a parameter to an existing public initializer, breaks every shipped binary.
-
-Run `scripts/check-pluginkit-abi.sh` before merging anything under `Plugins/TableProPluginKit/`. [Plugin Development](/development/plugin-development#abi-compatibility) has the additive-versus-breaking rules and what a breaking change obliges you to do.
-
-## Key services
-
-`DatabaseManager` (`Core/Database/DatabaseManager.swift`) is the connection pool and the interface every view and coordinator goes through for connect, disconnect, reconnect, and session tracking. `ConnectionHealthMonitor` pings each live connection every 30 seconds and reconnects with exponential backoff.
-
-`MainContentCoordinator` is the central coordinator, one per connection session rather than per window, built by `SessionStateFactory` and split across extension files in `Views/Main/Extensions/` (`+Alerts`, `+Filtering`, `+Pagination`, and the rest). New coordinator behavior goes in a new extension file rather than the main one.
-
-Autocomplete is three pieces. `CompletionEngine` ranks the suggestions and knows nothing about the editor framework; `QueryCompletionAdapter` bridges it to CodeEdit. `SQLContextAnalyzer` reads what the cursor sits in: a table reference, a column reference, a keyword. `SQLSchemaProvider` is an actor that caches schema and hands concurrent callers the same in-flight `Task` rather than firing a second `fetchTables()`.
-
-## Change tracking
-
-
-
- `DataChangeManager` records the cell edit as a pending change. Nothing has reached the server.
-
-
- `SQLStatementGenerator` turns the pending set into INSERT, UPDATE, and DELETE statements.
-
-
- Grid edits register on the window's `UndoManager`, which `TabWindowController.windowWillReturnUndoManager` resolves to the selected `ConnectionWorkspace.undoManager`. One window hosts several connections, so the undo stack is per connection, not per window. Structure edits use a private `UndoManager` inside `StructureChangeManager`.
-
-
-
-`AnyChangeManager` wraps whichever concrete manager is active behind the `ChangeManaging` protocol, so callers do not care which one they have.
-
-## MCP server
-
-`Core/MCP/` holds a Model Context Protocol server bound to loopback, speaking plain HTTP through `NWListener`. Nothing in it is reachable off the machine: no TLS, no remote mode. `MCPProtocolDispatcher` is an actor that resolves the protocol era from the request, checks scopes, runs each handler in its own task under a 330-second deadline, and normalizes the result envelope. `MCPServerManager` owns the lifecycle on the main actor: port binding, the bridge credential, the handshake file.
-
-The wire contract, the tool catalog, and the version policy are documented for consumers: [MCP Protocol](/developers/mcp-protocol), [MCP Tools](/developers/mcp-tools), [Versioning](/developers/versioning).
-
-## Connecting
-
-```mermaid
-flowchart TD
- UI["ConnectionFormView"] --> DM["DatabaseManager"]
- DM --> SSH["SSHTunnelManager (if SSH)"]
- DM --> Factory["DatabaseDriverFactory"]
- Factory --> PM["PluginManager"]
- PM --> Adapter["PluginDriverAdapter"]
- Adapter --> Connected["Connection ready"]
- SSH --> Connected
-```
-
-A cancelled connect is the hard case here, because `Task.cancel()` cannot interrupt a driver blocked in a C call. `CLAUDE.md` sets out what a driver owes the app on that path, written down after the same bug shipped four times.
-
-## State and storage
-
-| Pattern | What it holds | Where |
-|---------|---------------|-------|
-| `@Observable` | UI state, sessions, active tab | ViewModels |
-| Keychain | Connection passwords | `ConnectionStorage` |
-| UserDefaults | Preferences, filter defaults, favorites, recents | `Core/Storage`, via `AppSettingsStorage` |
-| SQLite FTS5 | Query history, full-text searchable | `QueryHistoryStorage` |
-| JSON files | Tab state, per-table filters | `TabPersistenceCoordinator`, `TabDiskActor` |
-
-`CLAUDE.md` carries the full storage table, including which stores sync through iCloud and which stay device-local.
-
-## Dependencies
-
-| Package | Source | Purpose |
-|---------|--------|---------|
-| TableProEditor | Local package in `Packages/`, a heavily modified fork of CodeEditSourceEditor and CodeEditTextView | The SQL editor: text engine, layout, highlighting, folding |
-| TableProGrammars | Local package in `Packages/` | The four tree-sitter grammars the editor highlights |
-| TableProCore | Local package in `Packages/` | SwiftPM modules the macOS and iOS apps share |
-| TableProOracle | Local package in `Packages/`, wraps a pinned TablePro fork of OracleNIO | Oracle wire protocol for OracleDriverPlugin |
-| Sparkle | SPM, 2.10.0+ | Auto-update with EdDSA signing |
-| swift-certificates | SPM, 1.19.0+ | X.509 types, linked by the app target |
-| Yams | SPM, 5.1.0+ | YAML parsing for project folder import |
-
-`Plugins/DamengDriverPlugin` links a Rust wire-protocol bridge built from `Native/DamengBridge`. `Plugins/HanaDriverPlugin` links no Go: it starts `tablepro-hana-helper`, a Go executable built from `Native/HanaBridge` on SAP's [go-hdb](https://github.com/SAP/go-hdb) driver, as one process per session, and exchanges JSON with it over stdin and stdout. The metadata connection pool opens drivers of its own, so one SAP HANA connection can run several helpers. Neither bridge is an SPM package.
-
-For the repository layout, see [Project Structure](/development/setup#project-structure).
diff --git a/development/building.mdx b/development/building.mdx
deleted file mode 100644
index 1037d3e7..00000000
--- a/development/building.mdx
+++ /dev/null
@@ -1,88 +0,0 @@
----
-title: Building
-description: Debug builds, the plugin compile check, clean builds, and what CI runs on a pull request
----
-
-Xcode and `xcodebuild` write the built app to different directories. `Cmd+R` leaves it in
-DerivedData; the command line leaves it in `build/Debug/` inside the repo. Both need the
-**TablePro** scheme and the **My Mac** destination.
-
-
-`TablePro.xcodeproj` is generated from `project.yml` by `scripts/generate-project.sh` and is not in
-git. Run it after cloning, after editing `project.yml` or anything in `Configs/`, and after adding,
-moving, or deleting a source file. See [Development Setup](/development/setup).
-
-
-## Debug builds
-
-In Xcode, `Cmd+B` builds and `Cmd+R` builds and runs. From the command line:
-
-```bash
-xcodebuild -project TablePro.xcodeproj -scheme TablePro -configuration Debug build -skipPackagePluginValidation
-```
-
-`-skipPackagePluginValidation` belongs on every `xcodebuild` invocation.
-`scripts/build-release.sh`, `scripts/build-plugin.sh`, `scripts/check-pluginkit-abi.sh` and every
-CI job that shells out to `xcodebuild` pass it.
-
-Debug builds sign with an Apple Development identity resolved from `TABLEPRO_DEVELOPMENT_TEAM`, so
-signing matters even locally: a plugin in the user plugins directory has to carry the same team as
-the running app, and the app derives the required team from its own signature. Set the variable in
-`Configs/Secrets.xcconfig` as [Development Setup](/development/setup) describes.
-
-## Compiling every plugin
-
-An app build compiles only the plugin bundles the app embeds, which leaves every registry-only
-driver uncompiled. The `AllPlugins` aggregate target covers all of them in one pass:
-
-```bash
-xcodebuild build -project TablePro.xcodeproj -scheme AllPlugins -destination 'platform=macOS' -skipPackagePluginValidation CODE_SIGNING_ALLOWED=NO
-```
-
-Run it after changing anything under `Plugins/TableProPluginKit/`, which every plugin links. To
-build one signed, packaged bundle instead, use `scripts/build-plugin.sh `; the target
-names are the keys under `targets:` in `project.yml`, such as `OracleDriver` or
-`SurrealDBDriverPlugin`.
-
-## Clean builds
-
-| Reach for | When |
-|---|---|
-| `Cmd+Shift+K` in Xcode | The usual case |
-| `xcodebuild -project TablePro.xcodeproj -scheme TablePro clean` | Same, from a terminal |
-| `rm -rf ~/Library/Developer/Xcode/DerivedData` | Package resolution is stuck or a stale module keeps reappearing |
-
-The last one deletes the caches for every project on the machine, and the next build starts from
-nothing.
-
-## What a pull request has to pass
-
-`.github/workflows/macos-tests.yml` starts on every pull request, on Xcode 26.4.1. Its first job
-decides whether the macOS suites run at all and skips them when nothing relevant changed, so the
-workflow still reports a result on a docs-only change.
-
-| Job | What it does |
-|---|---|
-| Detect relevant changes | Decides whether the macOS suites run |
-| Package Tests | `swift test` for TableProCore and TableProEditor |
-| Build for testing | Builds `AllPlugins`, `dlopen`s each built bundle, then builds the app and both test bundles |
-| Unit tests | `TableProTests` against the prebuilt products |
-| UI tests | `TableProUITests`, split across three shards |
-| macOS Tests Gate | Fails unless every other job reported success or skipped |
-
-A plugin that builds and signs can still fail to load, which is why the load step is separate:
-"Bundle failed to load executable" has shipped twice.
-
-`.github/workflows/repo-hygiene.yml` runs beside it on Ubuntu in under a minute. It runs
-`actionlint` over every workflow, which shells out to `shellcheck` for each inline `run:` block;
-`shellcheck` at warning severity over every script; the plugin manifest check; and the
-shared-contract drift gate in `scripts/audit-refactor-health.sh`.
-
-SwiftLint runs in its own workflow on every pull request. Run `swiftlint lint --strict` yourself
-before pushing; see [Code Style](/development/code-style).
-
-## Related
-
-Release builds, notarization, DMG packaging and the tag that publishes them are on
-[Releasing](/development/releasing). Publishing a driver plugin is on
-[Plugin Registry](/development/plugin-registry).
diff --git a/development/code-style.mdx b/development/code-style.mdx
deleted file mode 100644
index c1b3fdd4..00000000
--- a/development/code-style.mdx
+++ /dev/null
@@ -1,160 +0,0 @@
----
-title: Code Style
-description: Swift conventions, tooling, naming, and file organization rules
----
-
-Lint covers `TablePro/` and `Packages/`, which includes `TableProPluginKit` through the symlink in
-`TableProCore`. The rest of `Plugins/` and the app's own test bundles get no automated style check,
-so on a change there the conventions on this page are all there is.
-
-`.swiftlint.yml` settles the mechanical half. `AGENTS.md` carries the rest, the rules no linter can
-check: comments that say why and never what, early returns over nested conditionals, explicit
-access control, `String(localized:)` for user-facing strings, OSLog instead of `print()`.
-
-## Running the tools
-
-```bash
-swiftlint lint --strict # the pre-commit gate, and what CI runs on every pull request
-swiftlint --fix # apply the autocorrectable rules
-```
-
-`--strict` promotes every warning to an error. Several rules are configured as warnings on purpose,
-so a plain `swiftlint lint` exits 0 on code CI rejects. There is no SwiftLint
-build phase in the generated project, so nothing lints during a normal Xcode build.
-
-The scope comes from `included: [TablePro]` in `.swiftlint.yml`, and that key beats any path on the
-command line: `swiftlint lint Plugins/` lints `TablePro/` and reports nothing about your plugin. It
-exits 0, which reads like a pass.
-
-## Formatting
-
-Only the line limit is enforced. The rest is the convention the tree follows, so match the file
-around you.
-
-| Rule | Value |
-|---|---|
-| Indentation | 4 spaces, never tabs; `case` not indented inside `switch` |
-| Wrap target | 120 characters |
-| Hard line limit | Warns at 180, errors at 300 (`line_length` in `.swiftlint.yml`) |
-| Braces | K&R, `else` and `guard else` on the closing brace line |
-| Wrapped arguments | One per line, opening paren last, closing paren balanced |
-| Line endings | LF |
-| Semicolons | None |
-| `self.` | Only where the compiler needs it |
-
-`line_length` ignores URLs, function declarations, comments and interpolated strings, so a long
-signature or a long log line is not what trips it.
-
-Trailing commas are omitted throughout the tree. SwiftLint has `trailing_comma` in
-`disabled_rules`, so nothing enforces it.
-
-## Naming
-
-| Element | Convention | Example |
-|---|---|---|
-| Types and protocols | UpperCamelCase | `DatabaseConnection` |
-| Enum cases | lowerCamelCase | `.postgresql` |
-| Functions, variables, constants | lowerCamelCase | `executeQuery()`, `maxRetryAttempts` |
-| Booleans | `is`, `has`, `can` or `should` prefix | `isConnected`, `hasValidCredentials` |
-| Factory methods | `make` prefix | `makeConnection()` |
-| Acronyms | All caps, as in Apple's own APIs | `SQLStatementGenerator`, `JSONExportPlugin`, `MCPAuditLogger` |
-
-Nothing enforces the acronym row: SwiftFormat's `acronyms` rule is disabled, so no tool rewrites
-`Url` to `URL`. One corner of the tree disagrees with it. The MCP wire types under
-`TablePro/Core/MCP/Wire/` spell it `HttpRequestParser` while the files beside them spell `MCP` in
-full. New types take the all-caps form.
-
-## Imports
-
-One alphabetical block, no blank lines inside it, one blank line after it. SwiftFormat enforces
-this through `--importgrouping alpha`, `blankLinesBetweenImports` and `blankLineAfterImports`, and
-SwiftLint's `sorted_imports` catches an out-of-order import that never went through SwiftFormat.
-
-```swift
-import AppKit
-import Foundation
-import os
-import TableProEditorKit
-import TableProPluginKit
-
-final class QueryRunner { … }
-```
-
-Lowercase module names sort by their own spelling, which is why `os` lands between `Foundation` and
-`TableProEditorKit`.
-
-## Rules that bite
-
-`.swiftlint.yml` opts into 55 rules beyond the defaults. These are the ones that stop a clean-looking
-change most often:
-
-| Rule | Flags | Severity |
-|---|---|---|
-| `implicit_return` | `return` in a single-expression function, closure or getter | warning |
-| `explicit_init` | `Connection.init()` where `Connection()` compiles | warning |
-| `force_unwrapping` | `!` on an optional | warning |
-| `force_cast` | `as!` | warning |
-| `empty_count` | `items.count == 0` instead of `items.isEmpty` | error |
-| `number_separator` | `1000000` instead of `1_000_000` | warning |
-| `yoda_condition` | `if 5 == limit` | warning |
-| `sorted_imports` | An import out of alphabetical order | warning |
-| `extension_access_modifier` | Access control repeated on members instead of set on the extension | warning |
-
-Every warning in that table fails under `--strict`, which is how the gate runs it.
-
-`force_try` sits in `disabled_rules`, so nothing flags `try!`. The rule against forcing still
-applies; the linter is not what holds it up.
-
-## Custom rules
-
-Two project rules are configured at error severity, so they fire without `--strict`. Violate either
-and a UI test writes into your own store instead of a throwaway one:
-
-| Rule | Fires on | Exempt |
-|---|---|---|
-| `storage_environment_directory` | `applicationSupportDirectory` | `AppStorageEnvironment.swift` |
-| `storage_environment_defaults` | `UserDefaults.standard` | `AppStorageEnvironment.swift`, `GeneralSettings.swift`, `WorkspaceRailViewController.swift` |
-
-Resolve the directory through `AppStorageEnvironment.shared` and preferences through
-`AppStorageEnvironment.shared.defaults`. Only a preference macOS itself owns reads the standard
-domain.
-
-## Size limits
-
-`.swiftlint.yml` sets thresholds for `file_length`, `type_body_length`, `function_body_length` and
-`cyclomatic_complexity`. Read the current numbers there rather than from memory.
-
-When a type approaches one, split it into `TypeName+Category.swift` files under an `Extensions/`
-folder beside it, grouped by domain and not by line count. `MainContentCoordinator` is the worked
-example:
-
-
-
-
-
-
-
-
-
-
-
-A new file is invisible to Xcode until `scripts/generate-project.sh` runs again.
-
-## Localization
-
-`CLAUDE.md` holds the localization rule in full. The trap worth repeating is interpolation:
-
-```swift
-String(localized: "Preview \(name)")
-```
-
-That builds a different key on every call, so it never matches an entry in the strings catalog. The
-string ships untranslated and no tool reports it. Take a format argument instead, which is one key
-and one catalog entry:
-
-```swift
-String(format: String(localized: "Preview %@"), name)
-```
-
-Technical terms stay unlocalized: font names, database types, SQL keywords, encoding names.
-Exporting and merging a translation is on [Development Overview](/development/overview).
diff --git a/development/overview.mdx b/development/overview.mdx
index b249c006..dc8546d4 100644
--- a/development/overview.mdx
+++ b/development/overview.mdx
@@ -1,34 +1,19 @@
---
-title: Development Overview
-description: Build TablePro from source, contribute, or write a database driver plugin
+title: Contributing
+description: Build TablePro from source, contribute a change, or write a database driver plugin
---
-Two files in the repo root outrank these pages.
-[`CLAUDE.md`](https://github.com/TableProApp/TablePro/blob/main/CLAUDE.md) holds the architecture
-invariants and the rules a change has to meet before it merges;
-[`CONTRIBUTING.md`](https://github.com/TableProApp/TablePro/blob/main/CONTRIBUTING.md) holds branch
-names, the commit format, and the pull request checklist. Both are maintained next to the code they
-describe, and where either disagrees with a page here, it wins.
+Building and contributing to the app is documented in the app repository, next to the code it describes:
-What these pages add is the part neither file carries: the exact commands, the tool versions behind
-them, and the plugin SDK.
+- [CONTRIBUTING.md](https://github.com/TableProApp/TablePro/blob/main/CONTRIBUTING.md): setup, building, tests, code style, architecture, pull requests and translation.
+- [AGENTS.md](https://github.com/TableProApp/TablePro/blob/main/AGENTS.md): the invariants and rules every change has to meet.
+- [RELEASING.md](https://github.com/TableProApp/TablePro/blob/main/RELEASING.md): release builds, notarization and the release tag.
-
-
- Clone, download the libraries, generate the project, build.
-
-
- Module layout, plugin system, editor pipeline.
-
-
- Debug builds, the plugin compile check, tests, pull request CI.
-
-
- Release builds, notarization, DMG, static libraries, the release tag.
-
-
- Lint and format config, naming, the rules CI enforces.
-
+These pages live in [TableProApp/docs](https://github.com/TableProApp/docs). A docs change that goes with an app change is a pull request there.
+
+## Plugins
+
+
Write a driver plugin against TableProPluginKit.
@@ -39,25 +24,3 @@ them, and the plugin SDK.
Publish and distribute database driver plugins.
-
-## Translating the app
-
-Translate through Xcode's export and import, one XLIFF file per language. Never edit
-`Localizable.xcstrings` by hand: Xcode is the only tool that writes it, so a translation diff holds
-your language and nothing else.
-
-```bash
-xcodebuild -exportLocalizations -project TablePro.xcodeproj -scheme TablePro \
- -localizationPath Localization -exportLanguage fr
-xcodebuild -importLocalizations -project TablePro.xcodeproj -localizationPath Localization/fr.xcloc
-scripts/localization.py status # coverage, per language
-```
-
-Between the two commands, translate `Localization/fr.xcloc` in Xcode, or its
-`Localized Contents/fr.xliff` in any XLIFF editor. The export builds the app first. Commit the
-catalog and not the export: `Localization/` is gitignored. For the iPhone and iPad app, run the same
-commands on `TableProMobile/TableProMobile.xcodeproj` with the `TableProMobile` scheme.
-
-Plugin and package strings live in the app's catalog, marked "Managed Manually" so that Xcode keeps
-them. Run `scripts/localization.py plugins --add` after adding a `String(localized:)` to a plugin or
-a package.
diff --git a/development/plugin-development.mdx b/development/plugin-development.mdx
index 6cc14743..5380b843 100644
--- a/development/plugin-development.mdx
+++ b/development/plugin-development.mdx
@@ -75,6 +75,19 @@ These defaults are worth a second look before you accept them:
[Testing a Custom Plugin](/development/testing-plugins) covers getting the built bundle into a running app and reading the failure if it does not load.
+## Optional protocols
+
+Adopt these on top of `PluginDatabaseDriver` to reach more of the UI. The app finds them with a runtime cast, so leaving one out costs nothing.
+
+| Protocol | What adopting it gets you |
+|----------|---------------------------|
+| `PluginDiagnosticProvider` | A driver-written explanation in front of a raw connection error |
+| `PluginProcedureFunctionSupport` | Stored procedures and functions in the structure tab |
+| `PluginDefaultSortProvider` | Per-table control of default sorting: `.useAppDefault`, `.suppress`, or `.forceColumns([String])` |
+| `PluginPrincipalManagement` | Users, roles, and grants in the Users & Roles tab |
+| `PluginBrowseFilterProvider` | Control of the sidebar object filter: glob syntax, case sensitivity, type scopes |
+| `SettablePlugin` | A settings pane of your own under **Settings > Plugins** |
+
## ABI compatibility
TableProPluginKit builds with Swift Library Evolution, so a plugin built against an older PluginKit keeps loading under a newer app: the runtime fills requirements it never implemented from their defaults. Adding a requirement that has a default costs nothing.
@@ -84,7 +97,7 @@ Two changes look additive and are not. Both have shipped, and both surface the s
- **Removing a published requirement, even one that defaulted to `nil`.** Library Evolution rescues a requirement added after a plugin was built, never one removed out from under it. Removing one deletes its method descriptor and its default-implementation symbol, and every shipped plugin hard-references both in its witness table. If the app stops using a requirement, leave it in place with its default. (0.58, #1917: MongoDB, Oracle, Cassandra, Elasticsearch.)
- **Adding a parameter to an existing public initializer or function, even with a default value.** It replaces the mangled symbol. Add a new overload for the new field and mark the old one `@_disfavoredOverload`, so new code gets the full initializer and old binaries keep theirs.
-`CLAUDE.md` carries the full additive-versus-breaking list and the checklist a breaking bump obliges you to run, including re-releasing every registry plugin before the app ships.
+[`.claude/rules/plugin-system.md`](https://github.com/TableProApp/TablePro/blob/main/.claude/rules/plugin-system.md) in the app repository carries the full additive-versus-breaking list and the checklist a breaking bump obliges you to run, including re-releasing every registry plugin before the app ships.
```bash
scripts/check-pluginkit-abi.sh [base-ref]
diff --git a/development/releasing.mdx b/development/releasing.mdx
deleted file mode 100644
index d7d5adb3..00000000
--- a/development/releasing.mdx
+++ /dev/null
@@ -1,164 +0,0 @@
----
-title: Releasing
-description: Release builds, notarization, DMG packaging, static libraries, and the release tag
----
-
-`scripts/build-release.sh` is the whole release build. It signs every binary in the bundle itself,
-each plugin, helper and framework first and the app bundle last, so signing again by hand
-afterwards undoes its work.
-
-Everything on this page needs credentials the maintainer holds. A contributor building from source
-wants [Building](/development/building) instead.
-
-## Release builds
-
-```bash
-scripts/build-release.sh arm64 # Apple silicon
-scripts/build-release.sh x86_64 # Intel
-scripts/build-release.sh both # both, the default with no argument
-```
-
-Output lands in `build/Release/TablePro-arm64.app` and `build/Release/TablePro-x86_64.app`.
-
-The script extracts the matching slice from the universal static libraries, copies every
-non-system dynamic library the binaries reference into `Contents/Frameworks` and rewrites their
-install names, strips the main binary and the helpers, then signs with hardened runtime and
-`TablePro/TablePro.entitlements`.
-
-| Variable | Default | Purpose |
-|---|---|---|
-| `SIGN_IDENTITY` | The maintainer's Developer ID Application identity | Codesign identity for every binary |
-| `NOTARIZE` | `false` | Set `true` to notarize and staple after building |
-| `NOTARY_PROFILE` | `TablePro` | `notarytool` keychain profile holding the credentials |
-
-Create the notarization profile once:
-
-```bash
-xcrun notarytool store-credentials "TablePro" --apple-id … --team-id … --password …
-```
-
-Notarization zips each app, submits it, waits, and staples the ticket. `scripts/lib/notarize.sh`
-reads both the exit status and the reported status: `notarytool` has exited 0 on a rejected
-submission.
-
-## Packaging a DMG
-
-`scripts/create-dmg.sh` takes a version, an architecture (default `universal`) and a source app
-(default `build/Release/TablePro.app`). With no version it reads `MARKETING_VERSION` from
-`Configs/Version.xcconfig`, which is the only place the app version is declared.
-
-```bash
-scripts/build-release.sh arm64
-scripts/create-dmg.sh 0.67.0 arm64 build/Release/TablePro-arm64.app
-```
-
-That writes `build/Release/TablePro-0.67.0-arm64.dmg`. The script prefers `create-dmg`
-(`brew install create-dmg`) and falls back to `hdiutil` when it is missing.
-
-## Static libraries
-
-The `.a` files under `Libs/` and the xcframeworks under `Libs/ios/` live on the `libs-v1` GitHub
-Release, not in git. Each has a checksum baseline committed beside it, and
-`scripts/download-libs.sh` verifies the working copy against the baseline at `HEAD` on every run,
-including runs that download nothing.
-
-That means an archive and its baseline have to move together. Publish through the scripts, which do
-both:
-
-```bash
-scripts/publish-libs.sh libmongoc_arm64.a libmongoc_x86_64.a libmongoc_universal.a libmongoc.a
-git add Libs/checksums.sha256 && git commit -m "build: update static library checksums"
-```
-
-Name every library you rebuilt. `publish-libs.sh` verifies each library you did *not* name against
-the checksums at `HEAD`, regenerates `Libs/checksums.sha256`, and uploads the archive with
-`--clobber`. `libs-v1` predates release immutability, so GitHub keeps it mutable. Both publish
-scripts stop before touching a checksum file if it ever reports immutable.
-
-The iOS xcframeworks go through their own script, which takes `--dry-run` and refuses a publish
-that changes nothing:
-
-```bash
-scripts/publish-ios-libs.sh
-git add Libs/ios/checksums.sha256 && git commit -m "build: update iOS xcframework checksums"
-```
-
-Never write `shasum -a 256 Libs/*.a > Libs/checksums.sha256` by hand. Regenerating from a stale
-`Libs/` silently reverts every library you did not rebuild, which shipped a broken libmongoc and
-rolled DuckDB back a version. Uploading the xcframeworks with a bare `tar` and `gh release upload`
-fails from the other side: 299 MB published with no baseline behind it, and the next
-`download-libs.sh` run on every machine and every CI job stops.
-
-## Shipping a version
-
-`.github/workflows/build.yml` fires on a `v*` tag push, and otherwise only on a manual dispatch. Bump
-`Configs/Version.xcconfig` first: the release job compares the tag against `MARKETING_VERSION` and
-fails with `tag v… does not match MARKETING_VERSION …` when they disagree. It also fails when
-`CURRENT_PROJECT_VERSION` is not above the newest published release's. That number is what Sparkle
-compares as ``, so a flat build number ships a release no installed app is offered.
-
-Four jobs gate the release: SwiftLint, macOS Tests, Build (a matrix over `arm64` and `x86_64`) and
-Registry Readiness. A red suite or a registry with no plugin binary compatible with the shipping
-app blocks the tag.
-
-The release job then verifies the artifacts, signs both update archives with the Sparkle key and
-extracts the release notes from `CHANGELOG.md`. It creates the GitHub release as a draft with both
-DMGs and both ZIPs on it, publishes the draft in one step, and only then commits `appcast.xml`, whose
-download URLs 404 until the release is public. A tag containing `-alpha`, `-beta` or `-rc` publishes
-as a prerelease.
-
-A published release is immutable: GitHub refuses to add, replace or delete an asset, or to move the
-tag. When a run fails:
-
-- Before **Publish the release**, only a draft exists. Fix forward on `main`, move the tag and push it
- again. The next run reuses the draft.
-- After it, at the `appcast.xml` push for instance, use **Re-run failed jobs**. The release job finds
- the same files already published and carries on. **Re-run all jobs** rebuilds, and the release job
- refuses the new files.
-- A published build that is wrong ships as a new version.
-
-`generate_appcast` sees only the release being published, never the existing feed, and
-`scripts/ci/merge-appcast.py` splices its two items into `appcast.xml` afterwards. Items already
-published are moved byte for byte rather than regenerated, because Sparkle rewrites the download URL
-and drops the release notes of any existing item a staged archive happens to match.
-
-## Staged rollout and critical releases
-
-Every release carries `21600`. Sparkle
-spreads a release over seven cohorts keyed on a per-install random identifier, so six intervals is
-the tail: the last cohort is offered the build 36 hours after the first. A user who runs **Check for
-Updates…** is never phased and always gets the newest build.
-
-`.github/release-flags.json` decides whether a release may interrupt anyone:
-
-```json
-{ "criticalUpdate": false }
-```
-
-Setting it `true` passes `--critical-update-version ""` to `generate_appcast`, which writes
-`` with no version attribute, making the item critical for every host.
-Sparkle then bypasses phasing, hides Skip and Remind Me Later, retitles the alert, and shows the
-update even under automatic downloads.
-
-The bar is data loss, an exposed security issue, a crash or hang on a common path, or a regression
-with no workaround. Set it in the release commit and set it back in the next one. The release job
-fails when the flag is `true` and the file has not changed since the previous tag, which is what a
-leftover flag looks like.
-
-## Withdrawing a release
-
-Removing the items from `appcast.xml` is the only way to un-ship a Sparkle release.
-
-```bash
-python3 scripts/ci/pull-release.py 0.75.0 --dry-run # reports what it would remove
-python3 scripts/ci/pull-release.py 0.75.0 # edits, commits and pushes
-```
-
-The GitHub release stays up, so a direct download link still resolves. Anyone already on the bad
-build is reached by shipping the fix, not by the withdrawal.
-
-## Plugins
-
-Driver plugins release on their own `plugin-*-v*` tags through a separate workflow, and the registry
-decides which binary a user gets. [Plugin Registry](/development/plugin-registry) covers the
-manifest, the tag names, and the bulk re-release after a PluginKit ABI change.
diff --git a/development/setup.mdx b/development/setup.mdx
deleted file mode 100644
index 172956ce..00000000
--- a/development/setup.mdx
+++ /dev/null
@@ -1,236 +0,0 @@
----
-title: Development Setup
-description: Clone, configure signing, and build TablePro in Xcode
----
-
-The Xcode project is generated and the static libraries are downloaded. A fresh clone carries
-neither, so two scripts run before anything opens in Xcode. Xcode itself has to be 26.0 or newer:
-the app calls SwiftUI's `glassEffect(_:in:)` behind `if #available(macOS 26.0, *)`, and that symbol
-ships in the macOS 26 SDK. The deployment target stays at macOS 13.0, which is what the built app
-runs on, not what you build on.
-
-## Prerequisites
-
-| Software | Version | Notes |
-|----------|---------|-------|
-| Xcode | 26.0+ | Every CI job pins 26.4.1 through `.github/actions/setup-xcode` |
-| XcodeGen | 2.46.0 exactly | `brew install xcodegen`. Generated `pbxproj` output differs between XcodeGen versions, so `scripts/generate-project.sh` warns on any other version |
-| SwiftLint | any | `brew install swiftlint`. The release workflow runs `swiftlint lint --strict` |
-| SwiftFormat | any | `brew install swiftformat`. Not run in CI |
-| GitHub CLI | any | `brew install gh`. Optional: `scripts/download-libs.sh` falls back to `curl` |
-| Go | 1.27.1 | `brew install go`. Only for the SAP HANA plugin: run `scripts/build-hana.sh` with no argument, which builds the helper for both architectures, before building `HanaDriver` or `AllPlugins`. A thin `arm64` or `x86_64` helper only works in a build of that one architecture. The script builds with exactly 1.27.1 and fetches it when the installed `go` is another release |
-| Rust | 1.91.1 | Install [rustup](https://rustup.rs). Only for the Dameng plugin: run `scripts/build-dameng.sh arm64` before building `DamengDriver` or `AllPlugins`. The script installs 1.91.1 through rustup |
-
-The Swift language mode comes from `SWIFT_VERSION = 6.0` in `Configs/Base.xcconfig`, and each
-package under `Packages/` sets its own through `swift-tools-version: 6.0`. Never pass
-`SWIFT_VERSION=` on an `xcodebuild` command line to test a language-mode change: the override
-reaches the package targets too and reports their errors as yours.
-
-## Quick start
-
-
-
- ```bash
- git clone https://github.com/TableProApp/TablePro.git && cd TablePro
- ```
-
-
-
- ```bash
- brew install xcodegen swiftlint swiftformat
- ```
-
-
-
- ```bash
- scripts/download-libs.sh
- ```
-
- Two archives come off the `libs-v1` GitHub Release: the macOS `.a` files (libmariadb, libpq,
- libduckdb, libmongoc, and the rest) and the iOS xcframeworks. 257 MB downloaded, 621 MB on
- disk. Every run, including one that downloads nothing, verifies `Libs/` against the checksums
- committed at `HEAD`.
-
- None of it is in git, so a build that skips this step fails at link time on missing symbols.
-
-
-
- ```bash
- cp Configs/Secrets.xcconfig.example Configs/Secrets.xcconfig
- ```
-
- Then edit the copy. The template ships the literal `YOUR_TEAM_ID`, which fails to sign exactly
- the way leaving the file out does:
-
- ```
- TABLEPRO_DEVELOPMENT_TEAM = YOUR_TEAM_ID
- TABLEPRO_APP_BUNDLE_IDENTIFIER = com.yourhandle.TablePro
- ```
-
- A free personal team works. Debug builds sign against `TablePro/TablePro.Debug.entitlements`,
- which drops the iCloud container and CloudKit, so no paid Apple Developer Program membership is
- needed and sync turns itself off at runtime. Plugin bundles and the MCP helper sign ad-hoc and
- need no team at all.
-
- `Configs/Secrets.xcconfig` is gitignored, so these settings cannot reach a commit and they
- survive regenerating the project. Changing signing in the Xcode UI has no effect that lasts:
- the project is generated, and the next `scripts/generate-project.sh` discards the edit.
-
-
-
- ```bash
- scripts/generate-project.sh
- ```
-
- This writes `TablePro.xcodeproj` and `TableProMobile/TableProMobile.xcodeproj` from their
- `project.yml` specs. Pass `macos` or `ios` to generate just one of them. Run it again after
- editing a `project.yml` or anything in `Configs/`, and after adding, moving, or deleting a
- source file, because XcodeGen collects sources at generation time and a new file is not in
- the project until you regenerate.
-
-
-
- ```bash
- open TablePro.xcodeproj
- ```
-
- Select the **TablePro** scheme, set the destination to **My Mac**, press `Cmd+R`. Or stay on
- the command line:
-
- ```bash
- xcodebuild -project TablePro.xcodeproj -scheme TablePro -configuration Debug build -skipPackagePluginValidation
- ```
-
- The two write to different places. See [Building](/development/building).
-
-
-
-
-
-
-
-
-Writing a driver plugin? [Plugin Development](/development/plugin-development) covers building one
-and [Testing a Custom Plugin](/development/testing-plugins) covers running it in your local build.
-
-## Project structure
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`Packages/TableProCore/Sources/TableProPluginKit` is a symlink to `Plugins/TableProPluginKit`. Edit
-the files under `Plugins/` only.
-
-## Running tests
-
-```bash
-xcodebuild -project TablePro.xcodeproj -scheme TablePro test -skipPackagePluginValidation
-```
-
-Or press `Cmd+U` in Xcode. To narrow the run, pass `-only-testing:TableProTests/`, or
-`-only-testing:TableProTests//()` for one Swift Testing case. The filter matches the
-suite's Swift type name, and a filter that matches nothing still reports success, so check how many
-tests ran. `-only-testing:TableProUITests` runs the UI automation on its own.
-
-## Linting
-
-```bash
-swiftlint lint --strict
-```
-
-`--strict` turns warnings into errors, which is how CI runs it on every pull request, so a plain
-`swiftlint lint` can pass on a change CI rejects. Full tooling reference:
-[Code Style](/development/code-style).
-
-## Troubleshooting
-
-### `xcodegen: command not found`
-
-`scripts/generate-project.sh` needs XcodeGen on `PATH`. Run `brew install xcodegen`, then check
-`xcodegen --version` reports 2.46.0.
-
-### `TablePro.xcodeproj` does not exist
-
-It is generated, not committed. Run `scripts/generate-project.sh`.
-
-### `Undefined symbols for architecture arm64`
-
-`Libs/` is empty or stale. Run `scripts/download-libs.sh`, or `scripts/download-libs.sh --force` to
-overwrite what is already there.
-
-### `Build input file cannot be found: '…/tablepro-hana-helper'`
-
-`HanaDriver` and `HanaDriverTests` copy the SAP HANA helper into their bundle, and Xcode does not
-build it. Run `scripts/build-hana.sh` with no argument, then build again.
-
-### `…/tablepro-hana-helper has no … slice`
-
-The helper was built thin for the other architecture, and every architecture Xcode builds needs a
-slice. Run `scripts/build-hana.sh` with no argument, which builds both, then build again.
-
-### `Libs does not match the checksums committed in git.`
-
-`download-libs.sh` compares the extracted libraries against the baseline at `HEAD` and refuses to
-continue when they differ. Re-run with `--force`. If you rebuilt a library on purpose, publish it
-so the baseline moves with it: see [Releasing](/development/releasing).
-
-### Signing errors after a fresh clone
-
-`Configs/Base.xcconfig` defaults `TABLEPRO_DEVELOPMENT_TEAM` to the maintainer's team, and it
-includes `Secrets.xcconfig` with `#include?`, so a build without that file still runs and still
-fails to sign. Create the file and set your own team.
-
-### A new source file does not compile
-
-XcodeGen globs sources when the project is generated. Run `scripts/generate-project.sh`.
-
-### Package resolution fails
-
-Clean the build folder with `Cmd+Shift+K` and reopen Xcode. If that does not clear it,
-`rm -rf ~/Library/Developer/Xcode/DerivedData`.
diff --git a/docs.json b/docs.json
index b79aff14..564b071a 100644
--- a/docs.json
+++ b/docs.json
@@ -44,6 +44,26 @@
}
},
"redirects": [
+ {
+ "source": "/development/setup",
+ "destination": "/development/overview"
+ },
+ {
+ "source": "/development/architecture",
+ "destination": "/development/overview"
+ },
+ {
+ "source": "/development/building",
+ "destination": "/development/overview"
+ },
+ {
+ "source": "/development/releasing",
+ "destination": "/development/overview"
+ },
+ {
+ "source": "/development/code-style",
+ "destination": "/development/overview"
+ },
{
"source": "/features/csv-inspector",
"destination": "/features/data-files"
@@ -423,12 +443,7 @@
{
"group": "Build TablePro",
"pages": [
- "development/overview",
- "development/setup",
- "development/architecture",
- "development/building",
- "development/releasing",
- "development/code-style"
+ "development/overview"
]
},
{
diff --git a/images/xcode-setup-dark.png b/images/xcode-setup-dark.png
deleted file mode 100644
index 38b60310..00000000
Binary files a/images/xcode-setup-dark.png and /dev/null differ
diff --git a/images/xcode-setup.png b/images/xcode-setup.png
deleted file mode 100644
index 603f8e6f..00000000
Binary files a/images/xcode-setup.png and /dev/null differ
diff --git a/installation.mdx b/installation.mdx
index d6dfc0e6..235bf47b 100644
--- a/installation.mdx
+++ b/installation.mdx
@@ -63,4 +63,4 @@ rm ~/Library/Preferences/com.TablePro.plist
Connection passwords are in the macOS Keychain and survive both commands. Delete them in Keychain Access by searching for `com.TablePro`. Data already synced through iCloud stays in your iCloud account.
-Building from source is in [Development Setup](/development/setup). A launch that fails or a connection that will not open is in [Troubleshooting](/troubleshooting).
+Building from source is in [CONTRIBUTING.md](https://github.com/TableProApp/TablePro/blob/main/CONTRIBUTING.md#setup). A launch that fails or a connection that will not open is in [Troubleshooting](/troubleshooting).