diff --git a/.claude/rules/ai-mcp-security.md b/.claude/rules/ai-mcp-security.md index afc7b0aea7..40d04f0794 100644 --- a/.claude/rules/ai-mcp-security.md +++ b/.claude/rules/ai-mcp-security.md @@ -1,8 +1,8 @@ --- paths: - - "TablePro/Core/AI/**/*" - - "TablePro/Core/MCP/**/*" - - "Packages/TableProMacCore/Sources/TableProMCP/**/*" + - "apple/TablePro/Core/AI/**/*" + - "apple/TablePro/Core/MCP/**/*" + - "apple/Packages/TableProMacCore/Sources/TableProMCP/**/*" --- # AI and MCP diff --git a/.claude/rules/changelog.md b/.claude/rules/changelog.md index 223539ca16..f47624fa28 100644 --- a/.claude/rules/changelog.md +++ b/.claude/rules/changelog.md @@ -7,13 +7,13 @@ paths: # Changelog -Unreleased changes are towncrier fragments in `changelog.d/`. A release turns them into a `CHANGELOG.md` section with `scripts/release/notes.py assemble` (`RELEASING.md`, Notes). The Linux app is a second towncrier project under the same rules, in `linux/changelog.d/` and `linux/CHANGELOG.md`; its release runs `towncrier build --dir linux` (`linux/docs/releasing.md`). +Unreleased changes are towncrier fragments in `apple/changelog.d/`. A release turns them into an `apple/CHANGELOG.md` section with `apple/scripts/release/notes.py assemble` (`apple/RELEASING.md`, Notes). The Linux app is a second towncrier project under the same rules, in `linux/changelog.d/` and `linux/CHANGELOG.md`; its release runs `towncrier build --dir linux` (`linux/docs/releasing.md`). -- **One file per user-visible change**, named `changelog.d/..md`: `` is the pull request that adds it, `` is `added`, `changed`, `deprecated`, `removed`, `fixed` or `security`. A second entry of the same type in one PR adds a counter, as `towncrier create` does: `3452.fixed.1.md`, then `.2`. The counter does not order entries. +- **One file per user-visible change**, named `apple/changelog.d/..md`: `` is the pull request that adds it, `` is `added`, `changed`, `deprecated`, `removed`, `fixed` or `security`. A second entry of the same type in one PR adds a counter, as `towncrier create` does: `3452.fixed.1.md`, then `.2`. The counter does not order entries. - **One line, at most 120 characters, ending with a period.** Added and Changed name the new thing (`Open in Window for row inspector text fields.`); Fixed names the bug, not the fix (`Empty context menu when right-clicking a read-only inspector field.`). No file paths or type names. - **No refs, no credit, no `- ` prefix.** towncrier links the PR named by the file and adds the bullet; `notes.py` adds ` by @login` when the PR's author is not a maintainer. - **One entry per thing a user would notice.** Defects with the same visible symptom share an entry. - **No fragment** for a fix to something still unreleased (edit that fragment instead) or for a docs-only change. -- **Only a `chore(release): vX.Y.Z` PR edits `CHANGELOG.md`**, and only a `chore(release): linux-vX.Y.Z` PR edits `linux/CHANGELOG.md`. -- **The lead block and `[CRITICAL]`**: `RELEASING.md`, Notes and Channels. -- `scripts/ci/check-changelog.py` checks both projects' fragment text, headings and lead blocks in Repo Hygiene, then `towncrier build --draft --version 0.0.0` renders the root fragments; `--dir linux` renders the Linux ones. towncrier refuses an unknown type and a non-numeric PR number (`ignore`, `issue_pattern`). In PR title, `--pr` checks that a PR into main names each new fragment after itself, that only a release PR edits a `CHANGELOG.md`, and that a release PR changes nothing else; a Linux one also sets the version. +- **Only a `chore(release): vX.Y.Z` PR edits `apple/CHANGELOG.md`**, and only a `chore(release): linux-vX.Y.Z` PR edits `linux/CHANGELOG.md`. +- **The lead block and `[CRITICAL]`**: `apple/RELEASING.md`, Notes and Channels. +- `scripts/ci/check-changelog.py` checks both projects' fragment text, headings and lead blocks in Repo Hygiene, then `towncrier build --draft --version 0.0.0` renders the fragments of each, with `--dir apple` and `--dir linux`. towncrier refuses an unknown type and a non-numeric PR number (`ignore`, `issue_pattern`). In PR title, `--pr` checks that a PR into main names each new fragment after itself, that only a release PR edits a `CHANGELOG.md`, and that a release PR changes nothing else; a Linux one also sets the version. diff --git a/.claude/rules/ci.md b/.claude/rules/ci.md index 715f41e411..6fd605d078 100644 --- a/.claude/rules/ci.md +++ b/.claude/rules/ci.md @@ -5,14 +5,16 @@ paths: - ".github/actionlint.yaml" - ".github/*baseline*" - "scripts/ci/**/*" + - "apple/scripts/ci/**/*" - "scripts/release/**/*" - - ".swiftlint.yml" + - "apple/scripts/release/**/*" + - "apple/.swiftlint.yml" - "**/.swiftlint.yml" --- # CI -The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/skills/fix-issue/references/verification-apple.md`, Reading CI. Release tags: `RELEASING.md`. +The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/skills/fix-issue/references/verification-apple.md`, Reading CI. Release tags: `apple/RELEASING.md`. ## Rulesets @@ -24,7 +26,7 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/ | 23957966 "main and release branches require green gates" | default branch, `release/*` | the required checks, `do_not_enforce_on_create` | none | | 25129799 "branches keep their history" | default branch, `release/*`, `gh-pages` | deletion, non-fast-forward | none | -- `gh-pages` takes no required checks (`RELEASING.md`, Keys and environments). +- `gh-pages` takes no required checks (`apple/RELEASING.md`, Keys and environments). - The Apple workflows have no gate job. A test job runs on `!cancelled() && needs.changes.outputs.run != 'false'`. A job its `if:` skips reports success, and a failed changes job runs the suite rather than skipping it. - `Changes (macOS)`, `Changes (iOS)`, `Changes (Linux)` and `Changes (Release)` are not required checks. - A PR need not be up to date with main to merge (`strict_required_status_checks_policy: false`). @@ -39,27 +41,28 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/ - A workflow never selects Xcode another way, and never runs an Xcode tool under `sudo`, which drops `DEVELOPER_DIR`. A test host gets it from the Unit plan (`.claude/rules/tests.md`, Plans). - A stable tag builds only on Apple's released Xcode: `setup-xcode` with `require-release` needs Apple's released build and `licenseType` `GM` in the bundle's `LicenseInfo.plist`. The folder name is no signal: the image names the released 27A9275 `Xcode_27.1_Release_Candidate.app`. - XcodeGen comes from `.github/actions/setup-xcodegen`, pinned by version and SHA-256. It installs into the runner's tool cache without `sudo` and goes first on `PATH`, ahead of a Homebrew `xcodegen`. -- `scripts/release/feed.py` runs Sparkle's `generate_appcast` and `sign_update` from `SPARKLE_BIN`, which `.github/actions/setup-sparkle` sets. The action reads the version and revision from the app's `Package.resolved` (`project.yml` pins Sparkle with `exactVersion`) and checks the archive against the checksum in Sparkle's `Package.swift` at that revision. -- The compilation cache is off on CI (`COMPILATION_CACHE_ENABLE_CACHING=NO`); `Configs/Base.xcconfig` turns it on locally. +- `apple/scripts/release/feed.py` runs Sparkle's `generate_appcast` and `sign_update` from `SPARKLE_BIN`, which `.github/actions/setup-sparkle` sets. The action reads the version and revision from the app's `Package.resolved` (`apple/project.yml` pins Sparkle with `exactVersion`) and checks the archive against the checksum in Sparkle's `Package.swift` at that revision. +- The Apple jobs that build or test run with `defaults.run.working-directory: apple`, so a `run:` path is relative to `apple/`; an `actions/*` input such as `path:` or `go-version-file:` is still relative to the workspace root. +- The compilation cache is off on CI (`COMPILATION_CACHE_ENABLE_CACHING=NO`); `apple/Configs/Base.xcconfig` turns it on locally. ## macOS Tests (`macos-tests.yml`) - Triggers: pull request, push to main or `release/**`, dispatch. A PR keeps only its newest run; each pushed commit has its own concurrency group and is never cancelled. - `Changes (macOS)` outputs `run` (detector `macos`) and `plugins` (detector `plugins`). The test job has a 120-minute limit and sets `NSUnbufferedIO=YES`. -- Setup, in order: checkout, setup-xcode, prepare-build (setup-xcodegen, `scripts/download-libs.sh`, `generate-project.sh macos`, SPM restore), resolve, SPM save. +- Setup, in order: checkout, setup-xcode, prepare-build (setup-xcodegen, `apple/scripts/download-libs.sh`, `generate-project.sh macos`, SPM restore), resolve, SPM save. - One `build-for-testing` with no `-testPlan` builds both plans and writes one `.xctestrun` per plan, with signing, the index store and the compilation cache off. -- `COMPILE_PLUGINS` is true when the `plugins` detector is not false, on a PR and a push alike; a dispatch has no base, so it always compiles. It adds Go, the Dameng and HANA bridge builds, `xcodebuild build -scheme AllPlugins`, then `scripts/ci/verify-plugin-loads.sh`. -- With `COMPILE_PLUGINS`, `scripts/check-pluginkit-abi.sh` then checks PluginKit against the newest `v*` tag, `--frozen` on a PR into `release/*`; for that tag the job checks out with `fetch-depth: 0` and `filter: blob:none`. +- `COMPILE_PLUGINS` is true when the `plugins` detector is not false, on a PR and a push alike; a dispatch has no base, so it always compiles. It adds Go, the Dameng and HANA bridge builds, `xcodebuild build -scheme AllPlugins`, then `apple/scripts/ci/verify-plugin-loads.sh`. +- With `COMPILE_PLUGINS`, `apple/scripts/check-pluginkit-abi.sh` then checks PluginKit against the newest `v*` tag, `--frozen` on a PR into `release/*`; for that tag the job checks out with `fetch-depth: 0` and `filter: blob:none`. - `test-without-building -testPlan Unit`, then `-testPlan UI`. The UI plan runs after a Unit failure too, so one round trip reports both. - No shards, and no parallel, iteration or retry flags: the plans decide. -- `scripts/ci/summarize-xcresult.sh` always runs. Evidence uploads only on failure (`.claude/skills/fix-issue/references/verification-apple.md`, Reading CI). +- `apple/scripts/ci/summarize-xcresult.sh` always runs. Evidence uploads only on failure (`.claude/skills/fix-issue/references/verification-apple.md`, Reading CI). ## iOS and Package Tests (`ios-tests.yml`) - Triggers and concurrency as macOS Tests. `Changes (iOS)` outputs `run` (detector `ios`) and `oracle` (detector `oracle`). - 50 minutes on `xcode-27`, simulator `iPhone 18 Pro`. The iOS platform is downloaded only when that simulator is missing. -- `TableProMobileTests` runs with `-parallel-testing-enabled NO`: a second booted simulator killed runs with "(ipc/mig) server died" on the 3-core runner. `TableProMobile/project.yml` says `parallelizable: true`, which every run overrides. -- `swift test --package-path Packages/TableProCore --parallel`, then TableProOracle with `--force-resolved-versions` when its detector is not false. Each step has `if: !cancelled()`, so one red step never hides the next. +- `TableProMobileTests` runs with `-parallel-testing-enabled NO`: a second booted simulator killed runs with "(ipc/mig) server died" on the 3-core runner. `apple/TableProMobile/project.yml` says `parallelizable: true`, which every run overrides. +- `swift test --package-path apple/Packages/TableProCore --parallel`, then TableProOracle with `--force-resolved-versions` when its detector is not false. Each step has `if: !cancelled()`, so one red step never hides the next. - The summary step always runs. Evidence uploads only on failure. ## Release Build (`release-build.yml`) @@ -67,21 +70,21 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/ - Nightly at 19:00 UTC on main, push to `release/**`, and dispatch; never a PR. Never cancelled. Not a required check. - `Changes (Release)` outputs `run`. A dispatch always builds. A nightly runs the `release` detector against the head of the last nightly that ended green, read with `gh api`, so a quiet day builds nothing. A `release/**` push diffs `github.event.before`, and builds when the push created the branch. - `Release Build` runs on the self-hosted Mac, or on `xcode-27` when a dispatch picks `runner: github` or, for a push or nightly, the repository variable `RELEASE_BUILD_RUNNER` is `github`. It has a 150-minute limit and VERSION 0.0.0-dev and BUILD 9999999, so build.sh passes the version settings a tag passes. -- While the Mac is offline a run waits for it, and GitHub cancels a job still queued after 24 hours. The tag gate refuses a commit with any red run, and a `runner: github` dispatch on the same commit does not replace it: `RELEASING.md`, When a run fails. +- While the Mac is offline a run waits for it, and GitHub cancels a job still queued after 24 hours. The tag gate refuses a commit with any red run, and a `runner: github` dispatch on the same commit does not replace it: `apple/RELEASING.md`, When a run fails. - Setup as macOS Tests up to the SPM restore, which it never saves; then Go, `build-dameng.sh both` and `build-hana.sh both`. -- `scripts/release/build.sh app --unsigned` archives the `TablePro` scheme; `build.sh plugins --unsigned` builds `RegistryPlugins` and copies the publish set, the targets `scripts/release/registry.py targets` reads from `.github/plugin-registry.json`, to `build/release/plugins`. Both build universal. They run `lipo -verify_arch` once per arch (arm64, x86_64), since Xcode 27's `lipo` takes one arch per call, then `verify-plugin-loads.sh`. -- `scripts/release/package.sh app --unsigned` writes the release assets: the DMG, the `.tar.xz` (extracted and compared with the app) and the dSYMs zip; `package.sh plugins` writes one zip per plugin and the plugin dSYMs zip. -- The plugins build runs after an app failure too. The xcodebuild logs in `build/release/logs` upload only on failure. +- `apple/scripts/release/build.sh app --unsigned` archives the `TablePro` scheme; `build.sh plugins --unsigned` builds `RegistryPlugins` and copies the publish set, the targets `apple/scripts/release/registry.py targets` reads from `.github/plugin-registry.json`, to `apple/build/release/plugins`. Both build universal. They run `lipo -verify_arch` once per arch (arm64, x86_64), since Xcode 27's `lipo` takes one arch per call, then `verify-plugin-loads.sh`. +- `apple/scripts/release/package.sh app --unsigned` writes the release assets: the DMG, the `.tar.xz` (extracted and compared with the app) and the dSYMs zip; `package.sh plugins` writes one zip per plugin and the plugin dSYMs zip. +- The plugins build runs after an app failure too. The xcodebuild logs in `apple/build/release/logs` upload only on failure. ## Release, Withdraw and Pages -- A `v*` tag runs only `release.yml`; its jobs: `RELEASING.md`, What a tag runs. `withdraw.yml` runs only by dispatch, and `pages.yml` runs from both or by dispatch. Their environments allow only `v*` tags, so a dispatch names one with `--ref`. +- A `v*` tag runs only `release.yml`; its jobs: `apple/RELEASING.md`, What a tag runs. `withdraw.yml` runs only by dispatch, and `pages.yml` runs from both or by dispatch. Their environments allow only `v*` tags, so a dispatch names one with `--ref`. - Jobs read the tag, version, build and channel only from `verify` outputs passed through `env:`, never through `${{ }}` inside `run:`. - The default permission is `contents: read`, and each job lists every scope it needs. A job that calls `pages.yml` grants `pages: write` and `id-token: write`, since a called workflow can only lower what it receives. -## Summaries (`scripts/ci/summarize-xcresult.sh`) +## Summaries (`apple/scripts/ci/summarize-xcresult.sh`) -- Runs `scripts/lib/xcresult-cases.py summary`. `verify.sh` reads its verdicts through the same parser, and `scripts/lib/test_xcresult_cases.py` tests it in Repo Hygiene. +- Runs `apple/scripts/lib/xcresult-cases.py summary`. `verify.sh` reads its verdicts through the same parser, and `apple/scripts/lib/test_xcresult_cases.py` tests it in Repo Hygiene. - Reads `.xcresult` bundles and `swift test` xunit files, and skips a missing input. - Writes build errors with file and 1-based line, runner failures ("System Failures"), per-bundle counts and minutes, up to 50 failed cases and the 20 slowest to the step summary. - Exits 1 when a bundle or a `swift test` run executed nothing, because `xcodebuild` and `swift test` both call that success. A runner failure is not an executed test. @@ -91,43 +94,43 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/ ## SwiftLint (`swiftlint.yml`) - PR, push to main or `release/**`, dispatch; Ubuntu, 10 minutes. -- Runs the digest-pinned `ghcr.io/realm/swiftlint` image over the whole repo with `lint --strict`. The image's entrypoint is `swiftlint`, so the command never repeats it. +- Runs the digest-pinned `ghcr.io/realm/swiftlint` image over `apple/`, run from that folder so its `.swiftlint.yml` applies, with `lint --strict`. Each annotation's path then gets the `apple/` prefix, so it lands on the diff. The image's entrypoint is `swiftlint`, so the command never repeats it. - Fails when stderr has `Invalid configuration` or `is not a valid rule identifier`, because SwiftLint drops a custom rule whose regex does not compile and still exits 0. ## PR title (`pr-title.yml`) - Runs on opened, edited, reopened and synchronize, so every head commit gets the check. -- Runs `scripts/ci/check-pr-title.py`, then `scripts/ci/check-changelog.py --pr` on the merge commit's diff to its first parent, which also refuses a release PR that changes more than `CHANGELOG.md` and `changelog.d/`, then `scripts/check-banned-words.sh` on the PR's added lines and on its body. The banned-words step runs both and exits with the worse result. +- Runs `scripts/ci/check-pr-title.py`, then `scripts/ci/check-changelog.py --pr` on the merge commit's diff to its first parent, which also refuses a release PR that changes more than `apple/CHANGELOG.md` and `apple/changelog.d/`, then `scripts/check-banned-words.sh` on the PR's added lines and on its body. The banned-words step runs both and exits with the worse result. ## Repo Hygiene (`repo-hygiene.yml`) - PR, push to main or `release/**`, dispatch, no path filter; Ubuntu, 15 minutes. - Steps, in order: - actionlint, which also shellchecks every inline `run:` block; - - `shellcheck --severity=warning` on every `*.sh` under `scripts` and `.claude/skills`; - - the tooling tests: `scripts/ci/test_*.py`, `scripts/lib/test_*.py`, `scripts/test_localization.py` and `scripts/ci/test_libs_release.sh`; - - `scripts/release/test_*.py` on Python 3.9 from `actions/setup-python`, in a venv that installs `scripts/release/requirements.txt` with `pip --require-hashes`; - - in the same venv, `check-changelog.py` and `towncrier build --draft --version 0.0.0` for the root and `--dir linux`, which fails on a fragment name it cannot parse; + - `shellcheck --severity=warning` on every `*.sh` under `scripts`, `apple/scripts` and `.claude/skills`; + - the tooling tests: `scripts/ci/test_*.py`, `apple/scripts/ci/test_*.py`, `apple/scripts/lib/test_*.py`, `apple/scripts/test_localization.py` and `apple/scripts/ci/test_libs_release.sh`; + - `apple/scripts/release/test_*.py` on Python 3.9 from `actions/setup-python`, in a venv that installs `scripts/release/requirements.txt` with `pip --require-hashes`; + - in the same venv, `check-changelog.py` and `towncrier build --draft --version 0.0.0` with `--dir apple` and `--dir linux`, which fails on a fragment name it cannot parse; - `check-lib-sourcing.py`, `check-plugin-manifest.py`, `check-plugin-provides.py`, `check-deployment-target.sh`, `check-ios-shared-isolation.py`; - `localization.py plugins` and `plugins --target ios`; - `check-string-catalogs.py`, `check-license-inventory.py`, `check-entitlements.py`, `check-test-plans.py`; - - `scripts/audit-refactor-health.sh --check`, `check-hub-ratchet.py`; + - `apple/scripts/audit-refactor-health.sh --check`, `check-hub-ratchet.py`; - the Go checks on the HANA helper, `govulncheck` among them, when the `hana` detector is true. New advisories between changes arrive as Dependabot alerts. -- `audit-refactor-health.sh --check` fails a file defining a production `DatabaseType` outside `Packages/TableProCore/Sources/TableProCoreTypes/DatabaseType.swift`, unless `.github/duplicate-contract-baseline.txt` lists its path. Moving a listed file moves its baseline path in the same commit. Never add an entry to let a new copy pass. +- `audit-refactor-health.sh --check` fails a file defining a production `DatabaseType` outside `apple/Packages/TableProCore/Sources/TableProCoreTypes/DatabaseType.swift`, unless `.github/duplicate-contract-baseline.txt` lists its path. Moving a listed file moves its baseline path in the same commit. Never add an entry to let a new copy pass. - `check-hub-ratchet.py` fails when an app hub grows past `.github/hub-ratchet-baseline.json`. `--update` only lowers a value, so growing one is a hand edit a reviewer sees. ## Change detector (`scripts/ci/detect-changed-paths.sh`) - `detect-changed-paths.sh [--base ] macos|plugins|ios|oracle|hana|release` prints `true` or `false`. An unknown suite or flag exits 2. - A PR passes its base SHA, a push `github.event.before`. No base prints true. The all-zero SHA of a new ref diffs against `git merge-base HEAD origin/main`, so a branch cut from main skips what main already ran; with no merge base it prints true. -- `macos` watches the app, test, plugin, package, native, config and libs folders, `TestPlans/`, `project.yml`, the app's `Package.resolved`, the build scripts and CI scripts it runs, the setup actions and its workflow. `Packages/` covers TableProMacCore. -- `plugins` watches `Plugins/`, `Packages/TableProCore/`, `Packages/TableProOracle/`, `Native/`, `Libs/`, `Configs/`, `project.yml`, the app's `Package.resolved`, the project, libs and bridge scripts, `verify-plugin-loads.sh`, `scripts/check-pluginkit-abi.sh` and the setup actions. -- `ios` watches `TableProMobile/`, `Libs/`, `Plugins/TableProPluginKit/` (the package target is a symlink to it), and every `../` path it reads out of `TableProMobile/project.yml`. -- `oracle` watches `Packages/TableProOracle/`; `hana` watches `Native/HanaBridge/`. Each suite also watches its own workflow. -- `release` watches what the Release build reads: the app, plugin, package, native, config and libs folders, `project.yml`, the app's `Package.resolved`, `.github/plugin-registry.json`, the release scripts it runs (`build.sh`, `package.sh`, `registry.py`, `version.py`, `notes.py`), the build and CI scripts it runs and the setup actions. It skips `Plugins/*Tests/` and `Packages/*/Tests/`. +- `macos` watches the app, test, plugin, package, native, config and libs folders, `apple/TestPlans/`, `apple/project.yml`, the app's `Package.resolved`, the build scripts and CI scripts it runs, the setup actions and its workflow. `apple/Packages/` covers TableProMacCore. +- `plugins` watches `apple/Plugins/`, `apple/Packages/TableProCore/`, `apple/Packages/TableProOracle/`, `apple/Native/`, `apple/Libs/`, `apple/Configs/`, `apple/project.yml`, the app's `Package.resolved`, the project, libs and bridge scripts, `verify-plugin-loads.sh`, `apple/scripts/check-pluginkit-abi.sh` and the setup actions. +- `ios` watches `apple/TableProMobile/`, `apple/Libs/`, `apple/Plugins/TableProPluginKit/` (the package target is a symlink to it), and every `../` path it reads out of `apple/TableProMobile/project.yml`. +- `oracle` watches `apple/Packages/TableProOracle/`; `hana` watches `apple/Native/HanaBridge/`. Each suite also watches its own workflow. +- `release` watches what the Release build reads: the app, plugin, package, native, config and libs folders, `apple/project.yml`, the app's `Package.resolved`, `.github/plugin-registry.json`, the release scripts it runs (`build.sh`, `package.sh`, `registry.py`, `version.py`, `notes.py`), the build and CI scripts it runs and the setup actions. It skips `apple/Plugins/*Tests/` and `apple/Packages/*/Tests/`. - `linux` watches `linux/`, the docs and banned-words scripts the Linux fast job runs, the detector itself and the three Linux workflows. - A path a suite newly reads joins that suite's pattern in the same change, or the suite skips on it. -- `CHANGELOG.md` and `changelog.d/` are on no suite's pattern: the What's New phase writes nothing at `0.0.0-dev`, and Repo Hygiene renders every fragment and section instead. +- `apple/CHANGELOG.md` and `apple/changelog.d/` are on no suite's pattern: the What's New phase writes nothing at `0.0.0-dev`, and Repo Hygiene renders every fragment and section instead. ## What runs where @@ -150,7 +153,7 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/ - A SwiftLint custom rule is severity `error`, has a `message` that says what to do and why, and names its files with `included:` or `excluded:`. - A `scripts/ci/check-*.py` gets a `test_*.py` beside it and a step in Repo Hygiene. - Prove a new SwiftLint rule fires: plant a violation, run `swiftlint lint --strict --no-cache `, remove it. -- Nested `.swiftlint.yml` files narrow the rules with `only_rules` under `TableProTests`, `Plugins` (except `Plugins/TableProPluginKit`, linted in full), `TableProMobile` and `TableProUITests`. A new rule applies there only once it joins that file's list. +- Nested `.swiftlint.yml` files narrow the rules with `only_rules` under `apple/TableProTests`, `apple/Plugins` (except `apple/Plugins/TableProPluginKit`, linted in full), `apple/TableProMobile` and `apple/TableProUITests`. A new rule applies there only once it joins that file's list. - A local `swiftlint` run only prints the invalid-rule warning that fails CI (SwiftLint above), so read stderr after editing a rule. - Run `actionlint` and `shellcheck --severity=warning` on what you changed before pushing; Repo Hygiene runs both. - Write macOS `run:` blocks for bash 3.2, the `/bin/bash` the runner has used. `done < <(script)` throws away the script's exit status, so write the output to a file and read that. diff --git a/.claude/rules/connection-window.md b/.claude/rules/connection-window.md index 338a5f2b73..e385dd0b59 100644 --- a/.claude/rules/connection-window.md +++ b/.claude/rules/connection-window.md @@ -1,10 +1,10 @@ --- paths: - - "TablePro/Core/Services/Infrastructure/**/*" - - "TablePro/Models/Connection/**/*" - - "TablePro/Core/Database/DatabaseManager*.swift" - - "Packages/TableProCore/Sources/TableProDatabase/**/*" - - "TablePro/Views/Main/**/*" + - "apple/TablePro/Core/Services/Infrastructure/**/*" + - "apple/TablePro/Models/Connection/**/*" + - "apple/TablePro/Core/Database/DatabaseManager*.swift" + - "apple/Packages/TableProCore/Sources/TableProDatabase/**/*" + - "apple/TablePro/Views/Main/**/*" --- # Connection windows and sessions diff --git a/.claude/rules/data-grid.md b/.claude/rules/data-grid.md index 5bfab98701..3a88677def 100644 --- a/.claude/rules/data-grid.md +++ b/.claude/rules/data-grid.md @@ -1,8 +1,8 @@ --- paths: - - "TablePro/Views/Results/**/*" - - "TablePro/Core/DataGrid/**/*" - - "TableProTests/Views/Results/**/*" + - "apple/TablePro/Views/Results/**/*" + - "apple/TablePro/Core/DataGrid/**/*" + - "apple/TableProTests/Views/Results/**/*" --- # Data grid diff --git a/.claude/rules/data-sync-storage.md b/.claude/rules/data-sync-storage.md index d9de4aa06b..890f26c96f 100644 --- a/.claude/rules/data-sync-storage.md +++ b/.claude/rules/data-sync-storage.md @@ -1,20 +1,20 @@ --- paths: - - "TablePro/Core/**/*Storage*.swift" - - "TablePro/Core/**/*Sync*.swift" - - "TablePro/Core/Database/**/*" - - "TablePro/Core/Plugins/PluginMetadataRegistry*.swift" - - "TablePro/Core/Services/Query/**/*" - - "Packages/TableProCore/Sources/TableProSyncTransport/**/*" - - "CloudKit/**/*" + - "apple/TablePro/Core/**/*Storage*.swift" + - "apple/TablePro/Core/**/*Sync*.swift" + - "apple/TablePro/Core/Database/**/*" + - "apple/TablePro/Core/Plugins/PluginMetadataRegistry*.swift" + - "apple/TablePro/Core/Services/Query/**/*" + - "apple/Packages/TableProCore/Sources/TableProSyncTransport/**/*" + - "apple/CloudKit/**/*" --- # Storage, sync and the database core This path holds user data. A silent wrong answer here looks exactly like an empty database, so prove late-completion and cancellation behavior with a test. -- **A synced CKRecord type or field reaches CloudKit Production before anything writes it.** Both apps pin the container to Production, where nothing is auto-created, and CloudKit rejects a whole record that carries an undeclared field. Write fields only through `record.fields(SomeSyncField.self)`, never `record["..."]`. To ship one: add it in Development, deploy to Production in the Console, run `scripts/export-cloudkit-schema.sh`, commit `CloudKit/production-schema.ckdb`, then add it to the matching `verifiedInProduction`. `ProductionSchemaParityTests` checks code against the snapshot in both directions, not against the server, so never edit the snapshot by hand: `scripts/export-cloudkit-schema.sh --check` compares it with Production. -- **A storage format stays readable by the previous stable.** Sparkle never downgrades, and a beta user can turn betas off (`RELEASING.md`, Channels). +- **A synced CKRecord type or field reaches CloudKit Production before anything writes it.** Both apps pin the container to Production, where nothing is auto-created, and CloudKit rejects a whole record that carries an undeclared field. Write fields only through `record.fields(SomeSyncField.self)`, never `record["..."]`. To ship one: add it in Development, deploy to Production in the Console, run `apple/scripts/export-cloudkit-schema.sh`, commit `apple/CloudKit/production-schema.ckdb`, then add it to the matching `verifiedInProduction`. `ProductionSchemaParityTests` checks code against the snapshot in both directions, not against the server, so never edit the snapshot by hand: `apple/scripts/export-cloudkit-schema.sh --check` compares it with Production. +- **A storage format stays readable by the previous stable.** Sparkle never downgrades, and a beta user can turn betas off (`apple/RELEASING.md`, Channels). - **Persist, then notify.** `SyncChangeTracker.markDeleted()` runs after the storage write, because its notification can start a sync that re-uploads the record from the stale file. - **Concurrent schema loads await the one in-flight `loadTask`** in `SQLSchemaProvider`; never guard with a flag that returns without data. - **Every metadata read goes through `DatabaseManager.withMetadataDriver`**, never `MetadataConnectionPool.shared` directly, so an embedded engine with `supportsConnectionPooling = false` (DuckDB, PGlite) stays on its session driver; a second connection to it is a different, empty database. diff --git a/.claude/rules/editor.md b/.claude/rules/editor.md index 073dbdcc35..fcbd890592 100644 --- a/.claude/rules/editor.md +++ b/.claude/rules/editor.md @@ -1,9 +1,9 @@ --- paths: - - "Packages/TableProEditor/**/*" - - "TablePro/Theme/**/*" - - "TablePro/Views/Editor/**/*" - - "TablePro/Views/RowInspector/**/*" + - "apple/Packages/TableProEditor/**/*" + - "apple/TablePro/Theme/**/*" + - "apple/TablePro/Views/Editor/**/*" + - "apple/TablePro/Views/RowInspector/**/*" --- # Editor diff --git a/.claude/rules/libs.md b/.claude/rules/libs.md index 7361d3eaf9..739866a330 100644 --- a/.claude/rules/libs.md +++ b/.claude/rules/libs.md @@ -1,12 +1,12 @@ --- paths: - - "Libs/**/*" - - "scripts/download-libs.sh" - - "scripts/publish-libs.sh" + - "apple/Libs/**/*" + - "apple/scripts/download-libs.sh" + - "apple/scripts/publish-libs.sh" --- # Static libraries -- **`Libs/` is published only through `scripts/publish-libs.sh `.** Each run creates the next immutable `libs-` release and writes `Libs/release` and both `checksums.sha256` files, which are committed together. Never edit them by hand: a stale `Libs/` silently reverts other libraries. +- **`apple/Libs/` is published only through `apple/scripts/publish-libs.sh `.** Each run creates the next immutable `libs-` release and writes `apple/Libs/release` and both `checksums.sha256` files, which are committed together. Never edit them by hand: a stale `apple/Libs/` silently reverts other libraries. - A published `libs-` never changes; a fix is the next ``. `libs-v1` stays frozen for older commits. -- **`scripts/download-libs.sh` fetches the release `Libs/release` names at `HEAD` and verifies every file against the checksums at `HEAD`** on every run; `--force` re-downloads. +- **`apple/scripts/download-libs.sh` fetches the release `apple/Libs/release` names at `HEAD` and verifies every file against the checksums at `HEAD`** on every run; `--force` re-downloads. diff --git a/.claude/rules/mongodb-driver.md b/.claude/rules/mongodb-driver.md index 004a6ecedd..7a814d0388 100644 --- a/.claude/rules/mongodb-driver.md +++ b/.claude/rules/mongodb-driver.md @@ -1,7 +1,7 @@ --- paths: - - "Plugins/MongoDBDriverPlugin/**/*" - - "Plugins/MQLExportPlugin/**/*" + - "apple/Plugins/MongoDBDriverPlugin/**/*" + - "apple/Plugins/MQLExportPlugin/**/*" --- # MongoDB driver diff --git a/.claude/rules/mysql-driver.md b/.claude/rules/mysql-driver.md index 6ffab23820..692f3623c6 100644 --- a/.claude/rules/mysql-driver.md +++ b/.claude/rules/mysql-driver.md @@ -1,13 +1,13 @@ --- paths: - - "Plugins/MySQLDriverPlugin/**/*" - - "Packages/TableProCore/Sources/TableProMySQLCore/**/*" - - "TableProMobile/**/*MySQL*" - - "TableProMobile/**/*MariaDB*" + - "apple/Plugins/MySQLDriverPlugin/**/*" + - "apple/Packages/TableProCore/Sources/TableProMySQLCore/**/*" + - "apple/TableProMobile/**/*MySQL*" + - "apple/TableProMobile/**/*MariaDB*" --- # MySQL and MariaDB driver - **Every connect sets the session character set with `MariaDBCharacterSet.establishSession`**, because a server may ignore the handshake's charset. Outgoing SQL stays the UTF-8 bytes of the Swift string, and each result cell decodes by its field's own `charsetnr` through `MySQLColumnDecoding`. -- **`MySQLCharacterSet` is a transcription of the server's tables.** Change it only after running `scripts/probes/check-mysql-charset-decoding.sh` against a live server. +- **`MySQLCharacterSet` is a transcription of the server's tables.** Change it only after running `apple/scripts/probes/check-mysql-charset-decoding.sh` against a live server. - **After a reconnect the user did not ask for, replay a statement only when `MySQLSessionFootprint.isClean`** (`mysqlMayReplay`): the new session has lost variables, `USE`, `sql_mode` and any open transaction. Statements whose answer is session-scoped (`LAST_INSERT_ID` and similar) and the bodies of `/*!NNNNN ... */` count toward the footprint. diff --git a/.claude/rules/native-bridges.md b/.claude/rules/native-bridges.md index b42b88184e..1392a67b2d 100644 --- a/.claude/rules/native-bridges.md +++ b/.claude/rules/native-bridges.md @@ -1,15 +1,15 @@ --- paths: - - "Native/**/*" - - "Plugins/DamengDriverPlugin/**/*" - - "Plugins/HanaDriverPlugin/**/*" - - "scripts/build-dameng.sh" - - "scripts/build-hana.sh" + - "apple/Native/**/*" + - "apple/Plugins/DamengDriverPlugin/**/*" + - "apple/Plugins/HanaDriverPlugin/**/*" + - "apple/scripts/build-dameng.sh" + - "apple/scripts/build-hana.sh" --- # Native bridges -- **First-party bridge code lives in `Native/Bridge`**, built by `scripts/build-.sh`, with output in a gitignored folder beside the source, never in `Libs/`. +- **First-party bridge code lives in `apple/Native/Bridge`**, built by `apple/scripts/build-.sh`, with output in a gitignored folder beside the source, never in `apple/Libs/`. - **Rust may run in the app process; Go may not.** The Dameng staticlib is built with `panic = "unwind"`, and every export that does driver work wraps it in `catch_unwind`; a new export that does work takes the same guard. Go's `recover()` cannot catch a panic on a goroutine go-hdb starts, so HANA runs out of process as `tablepro-hana-helper`, one per driver session. - **Toolchains and upstream code are pinned**: `rust-toolchain.toml`, `Cargo.lock` with `--locked`, the `toolchain` line of `go.mod`, `go.sum`. A protocol change for Dameng lands on the [TableProApp/rust-dameng](https://github.com/TableProApp/rust-dameng) fork first, then the pinned `rev` moves. -- **Every nested Mach-O is signed with Developer ID, the hardened runtime and a timestamp** before the main binary. The `HanaHelperEmbedding` copy phase in `project.yml` signs the HANA helper with the plugin's identity; the plugins' notary submission rejects any executable signed otherwise, and `scripts/release/notarize.sh` prints the log. +- **Every nested Mach-O is signed with Developer ID, the hardened runtime and a timestamp** before the main binary. The `HanaHelperEmbedding` copy phase in `apple/project.yml` signs the HANA helper with the plugin's identity; the plugins' notary submission rejects any executable signed otherwise, and `apple/scripts/release/notarize.sh` prints the log. diff --git a/.claude/rules/plugin-system.md b/.claude/rules/plugin-system.md index ea0d5c7242..a958c92578 100644 --- a/.claude/rules/plugin-system.md +++ b/.claude/rules/plugin-system.md @@ -1,16 +1,16 @@ --- paths: - - "Plugins/**/*" - - "Packages/TableProCore/Sources/TableProPluginKit/**/*" - - "Packages/TableProCore/Sources/TableProMySQLCore/**/*" - - "Packages/TableProCore/Sources/TableProPostgreSQLCore/**/*" - - "Packages/TableProCore/Sources/TableProRedisCore/**/*" - - "Packages/*/Package.swift" - - "project.yml" - - "TableProMobile/project.yml" - - "Configs/PluginKit.xcconfig" + - "apple/Plugins/**/*" + - "apple/Packages/TableProCore/Sources/TableProPluginKit/**/*" + - "apple/Packages/TableProCore/Sources/TableProMySQLCore/**/*" + - "apple/Packages/TableProCore/Sources/TableProPostgreSQLCore/**/*" + - "apple/Packages/TableProCore/Sources/TableProRedisCore/**/*" + - "apple/Packages/*/Package.swift" + - "apple/project.yml" + - "apple/TableProMobile/project.yml" + - "apple/Configs/PluginKit.xcconfig" - ".github/plugin-registry.json" - - "scripts/*plugin*" + - "apple/scripts/*plugin*" --- # Plugins and PluginKit @@ -24,11 +24,11 @@ TableProPluginKit is built with Library Evolution, so an already-built plugin ke - Add a requirement only with a default implementation, then bridge it in `PluginDriverAdapter`. - Additive: a new requirement with a default, a new field on a non-`@frozen` struct, a new case on a non-`@frozen` enum, a reordering. - Breaking: a changed, removed or renamed requirement or signature, a requirement without a default, a new case on a `@frozen` enum, a change to a frozen type's layout. -- Every ABI change sets `TABLEPRO_PLUGINKIT_VERSION` in `Configs/PluginKit.xcconfig` to one past the kit of the newest `v*` tag reachable from HEAD; later changes before the next tag reuse it. A `release/*` line never bumps it. +- Every ABI change sets `TABLEPRO_PLUGINKIT_VERSION` in `apple/Configs/PluginKit.xcconfig` to one past the kit of the newest `v*` tag reachable from HEAD; later changes before the next tag reuse it. A `release/*` line never bumps it. - The build stamps that value into `TableProPluginKitVersion` of the app and of every plugin. A plugin's `Info.plist` holds `TABLEPRO_PLUGINKIT_VERSION` and is preprocessed, because every shipped app reads the key with `as? Int`; the source file therefore fails `plutil -lint`. - After an additive change, built plugins keep loading, and the bump makes an older app refuse a plugin rebuilt against the new API instead of failing its load. - A breaking change also raises `TABLEPRO_PLUGINKIT_MINIMUM_VERSION`. Every tag ships every registry plugin built against its own kit, so the release that carries the change carries plugins in its range. -- `scripts/check-pluginkit-abi.sh [--frozen] [base]` (`verify.sh abi [base]`) builds PluginKit at the base, by default the newest `v*` tag reachable from HEAD, and at HEAD with Xcode's ABI checker (`RUN_SWIFT_ABI_CHECKER_TOOL`). When CI runs it: `.claude/rules/ci.md`, macOS Tests. +- `apple/scripts/check-pluginkit-abi.sh [--frozen] [base]` (`verify.sh abi [base]`) builds PluginKit at the base, by default the newest `v*` tag reachable from HEAD, and at HEAD with Xcode's ABI checker (`RUN_SWIFT_ABI_CHECKER_TOOL`). When CI runs it: `.claude/rules/ci.md`, macOS Tests. - A checker error is a breaking change: the kit and the minimum must both be one past the base's kit, so the host refuses every plugin built before it. A baseline that changed with no error needs the kit one past the base's and the base's minimum; an identical one needs the base's kit and minimum. `--frozen` fails on any ABI change. - The checker reports breakage only; an additive change shows as a changed baseline. Its errors are in the log `verify.sh abi` prints. - Mark a public enum `@frozen` only when its case set is closed. `PluginCapability` and the transfer structs stay non-frozen so they can grow. @@ -36,7 +36,7 @@ TableProPluginKit is built with Library Evolution, so an already-built plugin ke ## One PluginKit per process -- R1 (the rule is in `AGENTS.md`, Invariants that apply everywhere): a plugin that linked a package product depending on the `TableProPluginKit` package would carry its own copy of PluginKit's types, and the host's casts across that boundary fail. Products a plugin and the iOS app share stay PluginKit-free for that reason. +- R1 (the rule is in `apple/AGENTS.md`, Invariants that apply everywhere): a plugin that linked a package product depending on the `TableProPluginKit` package would carry its own copy of PluginKit's types, and the host's casts across that boundary fail. Products a plugin and the iOS app share stay PluginKit-free for that reason. - The R1 check runs per binary (each plugin, each plugin test bundle, the app) and must print nothing: ```bash @@ -48,23 +48,23 @@ TableProPluginKit is built with Library Evolution, so an already-built plugin ke - Without the demangle filter, `TablePro.debug.dylib` prints one false positive: an extension the app declares on a PluginKit type. `TableProPluginKit.framework` itself prints many lines, which makes it the positive control. - R2: `/Build/Products/Debug/TableProPluginKit.swiftmodule` must not exist. The only PluginKit module is inside `TableProPluginKit.framework`. - No CI check runs R1 or R2. Run both after changing a package manifest, a package dependency or a plugin's linkage. -- A bundled plugin and its test bundle share one linkage template in `project.yml`, so they never link differently. +- A bundled plugin and its test bundle share one linkage template in `apple/project.yml`, so they never link differently. ## Building -- The `TablePro` scheme builds only the bundled plugins, and a registry plugin's compile error still ends in `BUILD SUCCEEDED`. Run `verify.sh plugins` (the `AllPlugins` aggregate) after any change under `Plugins/`. -- `scripts/ci/verify-plugin-loads.sh ...` dlopens every built `.tableplugin` and names any that will not load. It does not instantiate the principal class. +- The `TablePro` scheme builds only the bundled plugins, and a registry plugin's compile error still ends in `BUILD SUCCEEDED`. Run `verify.sh plugins` (the `AllPlugins` aggregate) after any change under `apple/Plugins/`. +- `apple/scripts/ci/verify-plugin-loads.sh ...` dlopens every built `.tableplugin` and names any that will not load. It does not instantiate the principal class. ## Structure - PluginKit-free logic a plugin shares with the iOS app lives in a TableProCore product, such as `TableProMySQLCore`, `TableProPostgreSQLCore` or `TableProRedisCore`. -- A new driver needs: its `project.yml` target and a `RegistryPlugins` entry (`BundledPlugins` and the app's `copy: { destination: plugins }` dependency for a bundled one), a `DatabaseType` constant, for a registry one an entry in `.github/plugin-registry.json` keyed by its target, with `name` (a bundled one stays out of it), a curated snapshot in `PluginMetadataRegistry` (without it the type never appears in the New Connection picker), its `TableProProvides*` keys (below), a row in `../tablepro-docs/databases/index.mdx` through a paired docs PR, and a changelog fragment. -- A new engine ships as a registry plugin (`AGENTS.md`, Scope). +- A new driver needs: its `apple/project.yml` target and a `RegistryPlugins` entry (`BundledPlugins` and the app's `copy: { destination: plugins }` dependency for a bundled one), a `DatabaseType` constant, for a registry one an entry in `.github/plugin-registry.json` keyed by its target, with `name` (a bundled one stays out of it), a curated snapshot in `PluginMetadataRegistry` (without it the type never appears in the New Connection picker), its `TableProProvides*` keys (below), a row in `../tablepro-docs/databases/index.mdx` through a paired docs PR, and a changelog fragment. +- A new engine ships as a registry plugin (`apple/AGENTS.md`, Scope). - `PluginManager` registers a plugin from the `TableProProvidesDatabaseTypeIds`, `TableProProvidesExportFormatIds` and `TableProProvidesImportFormatIds` arrays in its `Info.plist` and loads its code on first use. A plugin with none of them loads at launch. The first database type id is the primary one. -- `scripts/ci/check-plugin-provides.py` checks those arrays against the class's `databaseTypeId`, `additionalDatabaseTypeIds` and `formatId` in Repo Hygiene; `--write` stamps them. -- Where a plugin's tests go, and how a test target is added in `project.yml`: `.claude/rules/tests.md`, Where a test goes and Adding a test target. +- `apple/scripts/ci/check-plugin-provides.py` checks those arrays against the class's `databaseTypeId`, `additionalDatabaseTypeIds` and `formatId` in Repo Hygiene; `--write` stamps them. +- Where a plugin's tests go, and how a test target is added in `apple/project.yml`: `.claude/rules/tests.md`, Where a test goes and Adding a test target. ## Releases -- Registry plugins have no tags of their own: each app tag builds and publishes all of them at the app's version (`RELEASING.md`, Plugins). +- Registry plugins have no tags of their own: each app tag builds and publishes all of them at the app's version (`apple/RELEASING.md`, Plugins). - A driver fix reaches stable users through a patch on `release/`. diff --git a/.claude/rules/redis-driver.md b/.claude/rules/redis-driver.md index ac1c6fa551..4d0eb30ea8 100644 --- a/.claude/rules/redis-driver.md +++ b/.claude/rules/redis-driver.md @@ -1,11 +1,11 @@ --- paths: - - "Plugins/RedisDriverPlugin/**/*" - - "Packages/TableProCore/Sources/TableProRedisCore/**/*" + - "apple/Plugins/RedisDriverPlugin/**/*" + - "apple/Packages/TableProCore/Sources/TableProRedisCore/**/*" --- # Redis driver - **Cluster routing follows the server's `COMMAND` reply, merged over the curated table** in `RedisCommandRouting`, keyed `container|sub`. Redis 6 reports no routing tips, so the curated table is what makes `DBSIZE`, `KEYS` and `FLUSHDB` fan out. - **An unknown command routes as keyless**, never by hashing its first argument. -- **After editing the curated table, run `scripts/probes/check-redis-command-routing.sh`** against Redis 7+. +- **After editing the curated table, run `apple/scripts/probes/check-redis-command-routing.sh`** against Redis 7+. diff --git a/.claude/rules/split-views.md b/.claude/rules/split-views.md index 40dd661b7d..8c2693f365 100644 --- a/.claude/rules/split-views.md +++ b/.claude/rules/split-views.md @@ -1,7 +1,7 @@ --- paths: - - "TablePro/**/*Split*.swift" - - "TablePro/Core/Services/Infrastructure/WorkspacePane*.swift" + - "apple/TablePro/**/*Split*.swift" + - "apple/TablePro/Core/Services/Infrastructure/WorkspacePane*.swift" --- # Split views diff --git a/.claude/rules/tests.md b/.claude/rules/tests.md index ba597cf703..3195864426 100644 --- a/.claude/rules/tests.md +++ b/.claude/rules/tests.md @@ -1,13 +1,13 @@ --- paths: - - "TableProTests/**/*" - - "TableProUITests/**/*" - - "Plugins/*Tests/**/*" - - "Packages/*/Tests/**/*" - - "TableProMobile/TableProMobileTests/**/*" - - "TestPlans/**/*" - - "scripts/ci/check-test-plans.py" - - "scripts/clean-test-residue.sh" + - "apple/TableProTests/**/*" + - "apple/TableProUITests/**/*" + - "apple/Plugins/*Tests/**/*" + - "apple/Packages/*/Tests/**/*" + - "apple/TableProMobile/TableProMobileTests/**/*" + - "apple/TestPlans/**/*" + - "apple/scripts/ci/check-test-plans.py" + - "apple/scripts/clean-test-residue.sh" --- # Tests @@ -18,27 +18,27 @@ UI flows: `.claude/rules/ui-tests.md`. Running tests and reading verdicts: `.cla | Code under test | Target | Folder | Host | Runs in | | --- | --- | --- | --- | --- | -| A TableProCore module | `Tests` | `Packages/TableProCore/Tests/Tests/` | none | `swift test`, iOS and Package Tests | -| A PluginKit API | `TableProPluginKitTests` | `Packages/TableProCore/Tests/TableProPluginKitTests/` | none | `swift test`, iOS and Package Tests | -| Logic a plugin and the iOS app share without PluginKit | the test target of its TableProCore product (`TableProMySQLCoreTests`, `TableProMSSQLCoreTests`) | `Packages/TableProCore/Tests/` | none | `swift test`, iOS and Package Tests | -| Oracle wire logic | `TableProOracleCoreTests` | `Packages/TableProOracle/Tests/TableProOracleCoreTests/` | none | `swift test`, iOS and Package Tests when the `oracle` detector fires | -| A TableProMacCore module | `Tests` | `Packages/TableProMacCore/Tests/Tests/` | none | Unit plan | -| The editor or the grammars | `TableProEditorKitTests`, `TableProTextEngineTests`, `TableProGrammarsTests` | `Packages/TableProEditor/Tests/`, `Packages/TableProGrammars/Tests/` | none | Unit plan | -| A bundled driver's logic | `DriverTests` | `Plugins/DriverTests/` | none | Unit plan | -| A bundled format plugin | `FormatPluginTests`, one bundle for all of them | `Plugins/FormatPluginTests/` | none | Unit plan | -| App code that needs the app host: AppKit windows, `DatabaseManager.shared`, a coordinator | `TableProTests` | `TableProTests/` | TablePro.app | Unit plan | -| The iOS app | `TableProMobileTests` | `TableProMobile/TableProMobileTests/` | the iOS app on a simulator | iOS and Package Tests, `verify.sh ios` | -| A new end-to-end user flow | `TableProUITests` | `TableProUITests/` | XCUITest drives TablePro.app | UI plan | +| A TableProCore module | `Tests` | `apple/Packages/TableProCore/Tests/Tests/` | none | `swift test`, iOS and Package Tests | +| A PluginKit API | `TableProPluginKitTests` | `apple/Packages/TableProCore/Tests/TableProPluginKitTests/` | none | `swift test`, iOS and Package Tests | +| Logic a plugin and the iOS app share without PluginKit | the test target of its TableProCore product (`TableProMySQLCoreTests`, `TableProMSSQLCoreTests`) | `apple/Packages/TableProCore/Tests/` | none | `swift test`, iOS and Package Tests | +| Oracle wire logic | `TableProOracleCoreTests` | `apple/Packages/TableProOracle/Tests/TableProOracleCoreTests/` | none | `swift test`, iOS and Package Tests when the `oracle` detector fires | +| A TableProMacCore module | `Tests` | `apple/Packages/TableProMacCore/Tests/Tests/` | none | Unit plan | +| The editor or the grammars | `TableProEditorKitTests`, `TableProTextEngineTests`, `TableProGrammarsTests` | `apple/Packages/TableProEditor/Tests/`, `apple/Packages/TableProGrammars/Tests/` | none | Unit plan | +| A bundled driver's logic | `DriverTests` | `apple/Plugins/DriverTests/` | none | Unit plan | +| A bundled format plugin | `FormatPluginTests`, one bundle for all of them | `apple/Plugins/FormatPluginTests/` | none | Unit plan | +| App code that needs the app host: AppKit windows, `DatabaseManager.shared`, a coordinator | `TableProTests` | `apple/TableProTests/` | TablePro.app | Unit plan | +| The iOS app | `TableProMobileTests` | `apple/TableProMobile/TableProMobileTests/` | the iOS app on a simulator | iOS and Package Tests, `verify.sh ios` | +| A new end-to-end user flow | `TableProUITests` | `apple/TableProUITests/` | XCUITest drives TablePro.app | UI plan | - Take the first row that can hold the test. A hostless target beats `TableProTests`. - A registry-only driver gets no test target; `AllPlugins` only compiles it. An app-side gate around a registry engine (Safe Mode, the MCP write tier, the query classifier) is app code with a hosted test. `TableProMSSQLCoreTests` and `TableProOracleCoreTests` stay, because the iOS app links that code. -- A check that needs a live server, or a service no workflow starts, is a hand-run probe in `scripts/probes/`, never a test. No workflow runs a probe. +- A check that needs a live server, or a service no workflow starts, is a hand-run probe in `apple/scripts/probes/`, never a test. No workflow runs a probe. ## Plans -- `TestPlans/Unit.xctestplan` is the `TablePro` scheme's default plan. `TestPlans/UI.xctestplan` holds `TableProUITests` alone. +- `apple/TestPlans/Unit.xctestplan` is the `TablePro` scheme's default plan. `apple/TestPlans/UI.xctestplan` holds `TableProUITests` alone. - Unit holds `TableProTests`, the hostless plugin bundles, the Editor and Grammars package targets and the TableProMacCore package targets. -- TableProCore and TableProOracle test targets are in no plan and run under `swift test`. Some depend on the `TableProPluginKit` package, and a plan target that does makes the app and plugins compile against that module instead of the resilient framework. A package joins the Unit plan only when nothing it builds depends on that package; the top of `Packages/TableProMacCore/Package.swift` says so. +- TableProCore and TableProOracle test targets are in no plan and run under `swift test`. Some depend on the `TableProPluginKit` package, and a plan target that does makes the app and plugins compile against that module instead of the resilient framework. A package joins the Unit plan only when nothing it builds depends on that package; the top of `apple/Packages/TableProMacCore/Package.swift` says so. - Unit plan settings: language `en`, region `US`, timeouts on with a 60 s default and a 120 s maximum per case, no code coverage, the sandbox variable (Isolation below) and `DEVELOPER_DIR` set to `$(DEVELOPER_DIR)`. UI plan settings are in `.claude/rules/ui-tests.md`. - The host gets no shell environment, so without that entry a `/usr/bin` shim a test runs (`python3`) resolves through the Mac's selected Xcode, not the one running the tests. On `xcode-27` that is 27.0, and the shim asked to install the Command Line Tools instead. - The Unit plan never retries. A red case is a bug, or a flake to fix. @@ -49,9 +49,9 @@ UI flows: `.claude/rules/ui-tests.md`. Running tests and reading verdicts: `.cla ## Adding a test target -- Xcode test targets are defined in `project.yml`. A bundled plugin's target uses the `PluginTests` template plus that plugin's linkage template, and compiles `Plugins/`, `Plugins/Shared` and `Plugins/` against the PluginKit framework. +- Xcode test targets are defined in `apple/project.yml`. A bundled plugin's target uses the `PluginTests` template plus that plugin's linkage template, and compiles `apple/Plugins/`, `apple/Plugins/Shared` and `apple/Plugins/` against the PluginKit framework. - Name the folder after the target: `verify.sh` finds a test's target by that path component. -- `TableProTests` compiles only `TableProTests/`. Never add a plugin or package source to it. +- `TableProTests` compiles only `apple/TableProTests/`. Never add a plugin or package source to it. - `TableProTests` depends only on what the app does not link: `TableProTestSupport`, `TableProPostgreSQLCore`, `TableProMCPTestSupport`. The app's packages reach it through `TEST_HOST`. - A second link compiles a second copy of a module into the test bundle. Check with `nm -m` on `/Build/Products/Debug/TablePro.app/Contents/PlugIns/TableProTests.xctest/Contents/MacOS/TableProTests`: it must define no `Mn` descriptor of a module the app links. For TableProMCP (the number is the name's length) this prints nothing: @@ -59,14 +59,14 @@ UI flows: `.claude/rules/ui-tests.md`. Running tests and reading verdicts: `.cla nm -m "$BIN" | grep -v undefined | grep -E '_\$s11TableProMCP[0-9A-Za-z_]*Mn$' ``` -- Add the target to its plan. An Xcode target goes in by its PBXNativeTarget id from the generated `TablePro.xcodeproj/project.pbxproj`, never the scheme's id. A package target goes in with `containerPath: container:Packages/` and its name as identifier. -- `scripts/ci/check-test-plans.py`, run by Repo Hygiene, fails on: a test target missing from its plan or in the wrong one; a plan entry with no target; a TableProCore or TableProOracle target in a plan; a wrong `containerPath`; a duplicate entry; an Xcode test target with no folder of its name; `enabled: false`, `skippedTests` or `selectedTests`. -- It reads target types from `project.yml`, templates included, and test targets from every `Packages/*/Package.swift`, so a new package needs no script change. +- Add the target to its plan. An Xcode target goes in by its PBXNativeTarget id from the generated `apple/TablePro.xcodeproj/project.pbxproj`, never the scheme's id. A package target goes in with `containerPath: container:Packages/` and its name as identifier. +- `apple/scripts/ci/check-test-plans.py`, run by Repo Hygiene, fails on: a test target missing from its plan or in the wrong one; a plan entry with no target; a TableProCore or TableProOracle target in a plan; a wrong `containerPath`; a duplicate entry; an Xcode test target with no folder of its name; `enabled: false`, `skippedTests` or `selectedTests`. +- It reads target types from `apple/project.yml`, templates included, and test targets from every `apple/Packages/*/Package.swift`, so a new package needs no script change. ## Writing a test - New tests are Swift Testing. Convert an XCTest file you rewrite. `TableProUITests` stays XCTest. -- No bare top-level `@Suite` in `TableProTests`, `Plugins/*Tests` or `Packages/TableProMacCore/Tests`. Each top-level `@Suite` expansion walks every other one, so compile time grows with their square. Keep `@Suite` only to carry a trait. SwiftLint's `suite_without_trait` enforces it in those folders; the other package test folders are not covered yet, so follow it there by hand. +- No bare top-level `@Suite` in `apple/TableProTests`, `apple/Plugins/*Tests` or `apple/Packages/TableProMacCore/Tests`. Each top-level `@Suite` expansion walks every other one, so compile time grows with their square. Keep `@Suite` only to carry a trait. SwiftLint's `suite_without_trait` enforces it in those folders; the other package test folders are not covered yet, so follow it there by hand. - Hosted tests reach app code through `@testable import TablePro`. MemberImportVisibility is on, so a file that names a PluginKit enum case also imports `TableProPluginKit`. - A plugin bundle test compiles the plugin's sources into its own module: it imports `TableProPluginKit` and `Testing`, never `@testable import`. - A test does not depend on the machine. Pin the locale, use `.recordingClipboard` rather than the real pasteboard, keep the network out (`OfflineRegistry` for the plugin registry), inject the clock (`ManualClock`). @@ -76,11 +76,11 @@ UI flows: `.claude/rules/ui-tests.md`. Running tests and reading verdicts: `.cla ## Isolation and the app host -- When `TABLEPRO_UI_TEST_SANDBOX` is set, `AppStorageEnvironment.shared` resolves every store, the defaults suite and the keychain inside it. `main.swift` calls `AppStorageEnvironment.bootstrap()` before any static initializer runs. The rule for production code is in `AGENTS.md`, Invariants that apply everywhere. -- The Unit plan sets the sandbox to `$(TARGET_TEMP_DIR)/UnitTestStorage`, under DerivedData. The unit host deletes the sandbox's `TablePro/` folder at every launch, so every run starts empty. +- When `TABLEPRO_UI_TEST_SANDBOX` is set, `AppStorageEnvironment.shared` resolves every store, the defaults suite and the keychain inside it. `main.swift` calls `AppStorageEnvironment.bootstrap()` before any static initializer runs. The rule for production code is in `apple/AGENTS.md`, Invariants that apply everywhere. +- The Unit plan sets the sandbox to `$(TARGET_TEMP_DIR)/UnitTestStorage`, under DerivedData. The unit host deletes the sandbox's `TablePro` folder at every launch, so every run starts empty. - The defaults suite is `com.TablePro.uitest.<16 hex digits>`, a digest of the sandbox path, so each plan, checkout and worktree gets its own. A sandbox without the `.defaults-suite` marker is new and empties its suite, since cfprefsd keeps a suite after its folder is deleted. Plugins derive the same suite through `PluginHostStorage`. - Under isolation the keychain is `SandboxKeychainStore`, a `keychain.json` in the sandbox. -- AppKit autosave names (split views, window frames, tables) are nil in the unit host and under the sandbox, because AppKit writes those records to the real domain. The rule for a new one is in `AGENTS.md`, Invariants that apply everywhere. +- AppKit autosave names (split views, window frames, tables) are nil in the unit host and under the sandbox, because AppKit writes those records to the real domain. The rule for a new one is in `apple/AGENTS.md`, Invariants that apply everywhere. - Stores default to `AppStorageEnvironment.shared.defaults`. ManagedPolicy, AppleLanguages, Sparkle and AppKit stay on `.standard`. - Sparkle reads the real domain, so under isolation the app starts neither the updater nor sync (`runPostLaunchActivationIfNeeded` in `AppDelegate`). - `TABLEPRO_UI_TESTING=1` without a sandbox makes the app log a fault and exit. The app cannot detect XCUITest by itself. @@ -89,7 +89,7 @@ UI flows: `.claude/rules/ui-tests.md`. Running tests and reading verdicts: `.cla - `PluginManager.waitForInitialLoad()` returns at once unless `loadPlugins()` started a load. Its 10 s timeout, on the injected `initialLoadClock`, still covers a load that hangs. - A known write to the real domain, accepted: a test that changes the general settings runs `AppLanguage.apply()`, which writes `AppleLanguages` to `.standard`. - Proof a change writes nothing real: `ls ~/Library/Preferences | wc -l` and `defaults read com.TablePro | wc -l` give the same numbers before and after one Unit run and one UI run. -- `scripts/clean-test-residue.sh` lists what older test runs left on the Mac: test defaults domains, test keys and sandboxed autosave records in `com.TablePro`, `mcp-audit-test_` databases, test folders in the user temp directory. It never touches per-connection keys. +- `apple/scripts/clean-test-residue.sh` lists what older test runs left on the Mac: test defaults domains, test keys and sandboxed autosave records in `com.TablePro`, `mcp-audit-test_` databases, test folders in the user temp directory. It never touches per-connection keys. - Its `--apply` backs everything up to `~/TablePro-test-residue-/` before removing it, and refuses while TablePro or a TablePro test run is active. Give the user the commands (dry run, `--apply`, dry run again until every "Would remove" count is 0) rather than running `--apply` on their Mac. ## Shared helpers @@ -102,17 +102,17 @@ Use one before writing a local helper. | `AsyncGate` | `TableProTestSupport` | hold callers until the case calls `open()`; `arrived(_:)` waits for that many callers | | `ManualClock` | `TableProTestSupport` | time moves only on `advance(by:)`; production code takes `clock: any Clock = ContinuousClock()` | | `BoundedCall`, `HeldOpenWriter`, `BackgroundPipeWriter`, `BackgroundPipeReader` | `TableProTestSupport` | bound a call that can block; drive a pipe from another thread | -| `GlobalStateSuites` | `TableProTests/Helpers/` | parent of every suite that swaps a process-wide value | -| `EphemeralDefaults` | `TableProTests/Helpers/` | a defaults suite that deletes its domain and plist with its owner | -| `.appSettings { }`, `.recordingClipboard` | `TableProTests/Helpers/` | traits that swap the app settings or the clipboard for one case | -| `CoordinatorFixture` | `TableProTests/Helpers/` | a coordinator that is torn down with the fixture | -| `OfflineRegistry` | `TableProTests/Helpers/` | a registry client that never reaches the network | -| `InMemoryColumnLayoutStore`, `InMemoryKeychain`, `PluginDriverStub`, `TestFixtures` | `TableProTests/Helpers/` | stores and driver doubles | -| `AlwaysAllowGate`, `AlwaysDenyGate` | `TableProTests/Helpers/StubExecutionGates.swift` | an `ExecutionGate` that never raises the real alert | -| `TableProMCPTestSupport` | `Packages/TableProMacCore/Tests/TableProMCPTestSupport/` | MCP doubles: `MCPTestClock`, `MockHttpServer`, `RecordingBridgeLogger` | -| `TableProTLSTestFixtures` | `Packages/TableProCore/Tests/TableProTLSTestFixtures/` | TLS fixtures, also used by the iOS tests | - -`TableProTestSupport` is the TableProCore product at `Packages/TableProCore/Tests/TableProTestSupport/`. +| `GlobalStateSuites` | `apple/TableProTests/Helpers/` | parent of every suite that swaps a process-wide value | +| `EphemeralDefaults` | `apple/TableProTests/Helpers/` | a defaults suite that deletes its domain and plist with its owner | +| `.appSettings { }`, `.recordingClipboard` | `apple/TableProTests/Helpers/` | traits that swap the app settings or the clipboard for one case | +| `CoordinatorFixture` | `apple/TableProTests/Helpers/` | a coordinator that is torn down with the fixture | +| `OfflineRegistry` | `apple/TableProTests/Helpers/` | a registry client that never reaches the network | +| `InMemoryColumnLayoutStore`, `InMemoryKeychain`, `PluginDriverStub`, `TestFixtures` | `apple/TableProTests/Helpers/` | stores and driver doubles | +| `AlwaysAllowGate`, `AlwaysDenyGate` | `apple/TableProTests/Helpers/StubExecutionGates.swift` | an `ExecutionGate` that never raises the real alert | +| `TableProMCPTestSupport` | `apple/Packages/TableProMacCore/Tests/TableProMCPTestSupport/` | MCP doubles: `MCPTestClock`, `MockHttpServer`, `RecordingBridgeLogger` | +| `TableProTLSTestFixtures` | `apple/Packages/TableProCore/Tests/TableProTLSTestFixtures/` | TLS fixtures, also used by the iOS tests | + +`TableProTestSupport` is the TableProCore product at `apple/Packages/TableProCore/Tests/TableProTestSupport/`. ## Known failures and flakes diff --git a/.claude/rules/theme.md b/.claude/rules/theme.md index 2ed78301bb..b4892bd9fd 100644 --- a/.claude/rules/theme.md +++ b/.claude/rules/theme.md @@ -1,9 +1,9 @@ --- paths: - - "TablePro/Theme/**/*" - - "TablePro/Views/**/*" - - "TablePro/Core/Services/Infrastructure/*Window*" - - "Packages/TableProCore/Sources/TableProTheme/**/*" + - "apple/TablePro/Theme/**/*" + - "apple/TablePro/Views/**/*" + - "apple/TablePro/Core/Services/Infrastructure/*Window*" + - "apple/Packages/TableProCore/Sources/TableProTheme/**/*" --- # Theme @@ -21,7 +21,7 @@ paths: ## The model: `TableProTheme` -- **`TableProTheme` (`Packages/TableProCore/Sources/TableProTheme`) is the theme model, Foundation only**: `ThemeSlot`, `ThemeColor` (RGBA8 with compositing, mixing and WCAG contrast), `ThemeDefinition`, `ThemeFileFormat`, `ThemeLoadError`, `ThemeResolver`, `ThemeOrdering`, `DefaultSyntaxPalette` and `ThemeFileImporter`. AppKit and SwiftUI stay in `TablePro/Theme`, so the iOS app can adopt the package. Its tests are `TableProThemeTests`, under `swift test`. +- **`TableProTheme` (`apple/Packages/TableProCore/Sources/TableProTheme`) is the theme model, Foundation only**: `ThemeSlot`, `ThemeColor` (RGBA8 with compositing, mixing and WCAG contrast), `ThemeDefinition`, `ThemeFileFormat`, `ThemeLoadError`, `ThemeResolver`, `ThemeOrdering`, `DefaultSyntaxPalette` and `ThemeFileImporter`. AppKit and SwiftUI stay in `apple/TablePro/Theme`, so the iOS app can adopt the package. Its tests are `TableProThemeTests`, under `swift test`. - **`ThemeSlot` is the one registry.** The decoder, the encoder, the theme editor and the VS Code mapping enumerate it. Each slot declares `group` (its file key), `section` (where the theme editor lists it), `label`, `accessibilityLabel` (unique, one whole localized phrase), `yieldsToIncreasedContrast`, `requiresOpaque`, `offersOpacity` and `fallback`, each an exhaustive switch, so a new slot cannot ship without every decision. A slot exists only with a reader: nothing checks for one no view reads, so removing a slot's last reader removes the slot. - **Every slot is optional, and `fallback` decides what an unset one draws.** `ThemeDefinition.value(for:highContrast:)` returns `.themed`, `.system(slot)` or `.defaultSyntax(slot)`: - `.system`: the slot's native color, `ThemeSlot.nativeColor`. @@ -59,9 +59,9 @@ paths: - **`ThemeEngine` owns the selection**: `mode`, `lightThemeId` and `darkThemeId`, set through `apply(mode:)`, `select(lightThemeId:darkThemeId:)` and `apply(mode:lightThemeId:darkThemeId:)`. It never writes settings. It resolves with `ThemeResolver` over the catalog's themes in `ThemeOrdering` order and follows `ThemeCatalog.$themes` itself, so a save, trash, import, sync or file changed on disk repaints with no view involved; a view never asks it to reapply. A burst of catalog or draft changes costs one resolve per run loop turn, in the common modes, because a color well drag runs the event-tracking mode. - **`ThemeResolver` is pure.** A selected theme that is missing, rejected or of the other appearance draws that slot's default, and the saved id stays, so the theme comes back when its file does. - **Draft overlay.** `beginPreview(_:)` stands a draft in for the theme with its id until `endPreview(id:)`, so the theme editor repaints the real windows on every change with no file write. A draft no pair slot selects draws nothing. -- **`ThemeCatalog` owns the files.** Built-ins ship as JSON in `TablePro/Resources/Themes` (ids `tablepro.*`, read-only), besides Default Light and Default Dark from the package. User themes are JSON files in Application Support's `TablePro/Themes` folder, resolved through `AppStorageEnvironment`, with ids from `ThemeIdentifier.generated()`. A user theme is its JSON `id`, in whatever file it loaded from. The catalog reloads whenever TablePro becomes active. +- **`ThemeCatalog` owns the files.** Built-ins ship as JSON in `apple/TablePro/Resources/Themes` (ids `tablepro.*`, read-only), besides Default Light and Default Dark from the package. User themes are JSON files in Application Support's `TablePro/Themes` folder, resolved through `AppStorageEnvironment`, with ids from `ThemeIdentifier.generated()`. A user theme is its JSON `id`, in whatever file it loaded from. The catalog reloads whenever TablePro becomes active. - **A file changed on disk wins.** A save, rename or trash of a theme whose file changed or vanished since it was read throws `changedOnDisk` or `deletedOnDisk` and reloads; it never overwrites the file or recreates it under another name. `trash(id:)` moves the file to the Trash, or into the storage environment's own folder when that is isolated, and returns a `ThemeTrashReceipt` that `restore(_:)` takes back. `rename(id:to:)` refuses a taken name (`nameTaken`, case-insensitive); duplicate and import number theirs. Two files with one name show as "Name 2" in the list only, never in the file, so a display fix never syncs. -- **A built-in theme carries its source's license**: an entry in `TablePro/Resources/ThirdPartyLicenses/licenses.yml` and its text in `TablePro/Resources/ThirdPartyLicenses/texts`, checked by `scripts/ci/check-license-inventory.py`. +- **A built-in theme carries its source's license**: an entry in `apple/TablePro/Resources/ThirdPartyLicenses/licenses.yml` and its text in `apple/TablePro/Resources/ThirdPartyLicenses/texts`, checked by `apple/scripts/ci/check-license-inventory.py`. ## The theme editor diff --git a/.claude/rules/ui-lifecycle.md b/.claude/rules/ui-lifecycle.md index 5361606c79..bee3f72690 100644 --- a/.claude/rules/ui-lifecycle.md +++ b/.claude/rules/ui-lifecycle.md @@ -1,9 +1,9 @@ --- paths: - - "TablePro/Views/**/*" - - "TablePro/ViewModels/**/*" - - "TablePro/Core/Services/Infrastructure/**/*" - - "Packages/TableProEditor/**/*" + - "apple/TablePro/Views/**/*" + - "apple/TablePro/ViewModels/**/*" + - "apple/TablePro/Core/Services/Infrastructure/**/*" + - "apple/Packages/TableProEditor/**/*" --- # Views and lifecycle diff --git a/.claude/rules/ui-tests.md b/.claude/rules/ui-tests.md index c6e92c9988..4b86f08513 100644 --- a/.claude/rules/ui-tests.md +++ b/.claude/rules/ui-tests.md @@ -1,7 +1,7 @@ --- paths: - - "TableProUITests/**/*" - - "TestPlans/UI.xctestplan" + - "apple/TableProUITests/**/*" + - "apple/TestPlans/UI.xctestplan" --- # UI tests @@ -16,13 +16,13 @@ The UI plan holds end-to-end flows only. Swift Testing rules, isolation and shar ## The UI plan -- `TestPlans/UI.xctestplan`: timeouts on, 180 s default and 300 s maximum per case, no code coverage, `retryOnFailure` with `maximumTestRepetitions` 2, so a failed case runs once more. +- `apple/TestPlans/UI.xctestplan`: timeouts on, 180 s default and 300 s maximum per case, no code coverage, `retryOnFailure` with `maximumTestRepetitions` 2, so a failed case runs once more. - A case that passed only on the retry is a flake. Fix it in the same PR, or open an issue for it. `verify.sh` lists it and CI annotates it (`.claude/skills/fix-issue/references/verification-apple.md`, Reading CI). - The plan has no language keys: `launchApp` passes them as launch arguments. ## The harness -- Every suite subclasses `UITestCase` (`TableProUITests/Support/UITestCase.swift`), the only launch path that names a sandbox. The SwiftLint rule `ui_test_base_class` fails `XCUIApplication()` or `: XCTestCase` anywhere else. +- Every suite subclasses `UITestCase` (`apple/TableProUITests/Support/UITestCase.swift`), the only launch path that names a sandbox. The SwiftLint rule `ui_test_base_class` fails `XCUIApplication()` or `: XCTestCase` anywhere else. - The sandbox is one folder per checkout under `$TMPDIR/TableProUITests/`, recreated for each test. Teardown terminates every launched app before deleting it, because the app writes on the way down. - `continueAfterFailure` is false. A failed test attaches each launched app's element tree as `element-tree-`. - An interruption monitor answers a system privacy alert, since XCTest's own handler misses macOS 26's local-network alert. diff --git a/.claude/rules/welcome-library.md b/.claude/rules/welcome-library.md index 84363a8c99..68b610ce59 100644 --- a/.claude/rules/welcome-library.md +++ b/.claude/rules/welcome-library.md @@ -1,8 +1,8 @@ --- paths: - - "TablePro/Views/Welcome/**/*" - - "TablePro/ViewModels/WelcomeViewModel*.swift" - - "Packages/TableProCore/Sources/TableProConnectionLibrary/**/*" + - "apple/TablePro/Views/Welcome/**/*" + - "apple/TablePro/ViewModels/WelcomeViewModel*.swift" + - "apple/Packages/TableProCore/Sources/TableProConnectionLibrary/**/*" --- # Welcome connection list diff --git a/.claude/skills/fix-issue/SKILL.md b/.claude/skills/fix-issue/SKILL.md index 7074c58ed2..f128ddea5a 100644 --- a/.claude/skills/fix-issue/SKILL.md +++ b/.claude/skills/fix-issue/SKILL.md @@ -81,7 +81,7 @@ Write `/.analysis//plan.md`, under 80 lines, opening with a progress - the tests that fail before the fix, in the targets the profile's Done section says to check first; - the changelog fragment, docs, localization and screenshots the change needs. -**Rules read.** List the files `plan.md` touches. Read `/AGENTS.md`, then every `/.claude/rules/*.md` whose row in its Area rules table matches one of them, and name them on a `Rules read:` line in `plan.md`. A file added later gets the same check before you edit it. The tree's copies are the ones that apply; a harness may have loaded another checkout's. +**Rules read.** List the files `plan.md` touches. Read `/AGENTS.md` and the platform guide the profile's Done section names, then every `/.claude/rules/*.md` whose row in the root Area rules table matches one of them, and name them on a `Rules read:` line in `plan.md`. A file added later gets the same check before you edit it. The tree's copies are the ones that apply; a harness may have loaded another checkout's. In Full mode the `critic` lane attacks the plan; fold in what survives an evidence check. Then implement. @@ -102,7 +102,7 @@ Run every step through the tree's wrapper, written out in full each time, since The profile lists the platform's steps and when each runs; the guide's Every change section adds the rest. -- Check that the `root:` line names your tree, then read the verdict line. What each verdict means is in the guide (`AGENTS.md`, Commands). +- Check that the `root:` line names your tree, then read the verdict line. What each verdict means is in the platform guide's Commands section. - **Verify in batches**: build once after a coherent set of edits, then re-run only a failed step. - **Never run two builds in one tree.** `--no-wait` does not wait for a build already running there, and the second one fails on the locked build database. - **Pick tests by what you changed**, not by the test files you edited, and compare the executed count with what you meant to run. diff --git a/.claude/skills/fix-issue/references/claude-code.md b/.claude/skills/fix-issue/references/claude-code.md index 3a2e0ffa24..188d14cad0 100644 --- a/.claude/skills/fix-issue/references/claude-code.md +++ b/.claude/skills/fix-issue/references/claude-code.md @@ -5,7 +5,7 @@ - Keep a task list with the phases this run uses (`TodoWrite` or the task tools, when present), and progress in `plan.md` either way. - Ask with `AskUserQuestion`, recommendation first, only for the cases in Autonomy. - The Bash tool's working directory resets to the main checkout between calls: start every command for the tree with `cd &&`, or pass `-C `. -- Claude Code injects `AGENTS.md` and path rules from the session's checkout, which can be on another branch than the tree. The tree's copies, read in `SKILL.md` Phase 2 (Rules read), win. +- Claude Code injects `AGENTS.md`, a platform guide and path rules from the session's checkout, which can be on another branch than the tree. The tree's copies, read in `SKILL.md` Phase 2 (Rules read), win. ## Lanes diff --git a/.claude/skills/fix-issue/references/codex.md b/.claude/skills/fix-issue/references/codex.md index b1ff130a84..053d312242 100644 --- a/.claude/skills/fix-issue/references/codex.md +++ b/.claude/skills/fix-issue/references/codex.md @@ -4,7 +4,7 @@ Codex loads the skill through `.agents/skills/fix-issue` and the project guide f ## Rules and tracking -- Codex loads the root `AGENTS.md` of the checkout it started in and nothing else by itself. The tree's guide and rules come from `SKILL.md` Phase 2 (Rules read). When the profile's Done section names another guide, read it. +- Codex loads the root `AGENTS.md` of the checkout it started in, and a platform guide only when started in its folder. The tree's guides and rules come from `SKILL.md` Phase 2 (Rules read): read the guide the profile's Done section names. - Keep progress in `plan.md`, and in the plan or goal tool your session offers, if any. - Ask with `request_user_input`, recommendation first, only for the cases in Autonomy. diff --git a/.claude/skills/fix-issue/references/platform-apple.md b/.claude/skills/fix-issue/references/platform-apple.md index d384e9155f..4c53db7709 100644 --- a/.claude/skills/fix-issue/references/platform-apple.md +++ b/.claude/skills/fix-issue/references/platform-apple.md @@ -5,9 +5,9 @@ The macOS app and the iOS app, with everything they build from. Every profile ha ## Covers - A report from macOS or iOS. -- Code: `TablePro/`, `Plugins/`, `Packages/`, `Native/`, `TableProMobile/` and the build scripts in `scripts/`. -- Tests: `TableProTests/`, `TableProUITests/`, `Plugins/*Tests/`, `Packages/*/Tests/` and `TableProMobile/TableProMobileTests/`. The plans are in `TestPlans/`. -- A fix lands in the app, a bundled plugin, a registry-only plugin (name its target), a package or `TableProMobile/`. +- Code: `apple/TablePro/`, `apple/Plugins/`, `apple/Packages/`, `apple/Native/`, `apple/TableProMobile/` and the build scripts in `apple/scripts/`. +- Tests: `apple/TableProTests/`, `apple/TableProUITests/`, `apple/Plugins/*Tests/`, `apple/Packages/*/Tests/` and `apple/TableProMobile/TableProMobileTests/`. The plans are in `apple/TestPlans/`. +- A fix lands in the app, a bundled plugin, a registry-only plugin (name its target), a package or `apple/TableProMobile/`. ## Base @@ -16,28 +16,28 @@ The macOS app and the iOS app, with everything they build from. Every profile ha ## Done -- The tree's `AGENTS.md` and every `.claude/rules/` file its Area rules table matches to what you edit (`SKILL.md`, Phase 2, Rules read). The per-change checklist is its Every change section. +- The tree's `apple/AGENTS.md`, the root `AGENTS.md` it points to, and every `.claude/rules/` file the root Area rules table matches to what you edit (`SKILL.md`, Phase 2, Rules read). The per-change checklist is the Every change section of `apple/AGENTS.md`. Under Claude Code, `apple/CLAUDE.md` loads it once a file under `apple/` is read; read it by path anyway in Phase 0. - Before naming a test in `plan.md`, read `.claude/rules/tests.md` (Where a test goes): a path rule loads only once a matching file is open. Add a UI flow only when `.claude/rules/ui-tests.md` (What earns a UI flow) says so. ## Native standard - The AppKit or SwiftUI mechanism Apple documents, and the Human Interface Guidelines rule behind it. -- The version floor is the deployment target: macOS 13 (`project.yml`) and iOS 18 (`TableProMobile/project.yml`). An API introduced later needs an `if #available` branch, and the plan names the fallback. +- The version floor is the deployment target: macOS 13 (`apple/project.yml`) and iOS 18 (`apple/TableProMobile/project.yml`). An API introduced later needs an `if #available` branch, and the plan names the fallback. - Sources, the API ground truth and how to build a probe: `references/research-apple.md`. ## Verify -`verify.sh` runs these steps on a Mac without `--platform`. `build`, `test` and `lint` take the arguments in `AGENTS.md`, Commands. +`verify.sh` runs these steps on a Mac without `--platform`. `build`, `test` and `lint` take the arguments in `apple/AGENTS.md`, Commands. | Step | Arguments | When | | --- | --- | --- | -| `generate` | none | a new tree; a new, moved or deleted file; a `project.yml` change; first after a merge from the base. Runs `scripts/generate-project.sh` for both projects | +| `generate` | none | a new tree; a new, moved or deleted file; an `apple/project.yml` change; first after a merge from the base. Runs `apple/scripts/generate-project.sh` for both projects | | `build` | AGENTS | always | | `test` | AGENTS | always, naming the suites that own the types you changed | | `uitest` | `[Test...]`; none runs the whole UI plan | an end-to-end flow changed or was added | -| `package` | ` [regex]` | `Packages/` changed and you want the whole package; `test ` already routes a package suite | -| `ios` | ` [Suite...]`, at least one | `TableProMobile/` or a file it compiles changed | -| `plugins` | none | anything under `Plugins/` changed. Builds `AllPlugins`, building the HANA helper first when it is missing and Go is installed | +| `package` | ` [regex]` | `apple/Packages/` changed and you want the whole package; `test ` already routes a package suite | +| `ios` | ` [Suite...]`, at least one | `apple/TableProMobile/` or a file it compiles changed | +| `plugins` | none | anything under `apple/Plugins/` changed. Builds `AllPlugins`, building the HANA helper first when it is missing and Go is installed | | `abi` | `[base]`, default the newest `v*` tag | PluginKit changed (`.claude/rules/plugin-system.md`, ABI) | | `lint` | AGENTS | every changed Swift file | | `l10n` | none | strings, or plugin or package messages, changed. Passes only when every plugin and package string is in both the macOS and iOS catalogs, and prints the `--add` command for each target that failed | @@ -58,4 +58,4 @@ The macOS app and the iOS app, with everything they build from. Every profile ha - Stage a string catalog only for the keys you added. - A fix for a bug on the current beta or stable gets the label `backport release/`. -- A probe worth keeping goes under `scripts/probes/`. +- A probe worth keeping goes under `apple/scripts/probes/`. diff --git a/.claude/skills/fix-issue/references/platform-linux.md b/.claude/skills/fix-issue/references/platform-linux.md index 3d03b4d8ad..ffe6b27ce2 100644 --- a/.claude/skills/fix-issue/references/platform-linux.md +++ b/.claude/skills/fix-issue/references/platform-linux.md @@ -49,6 +49,6 @@ Run the app from the tree with `meson devenv -C _build ./crates/app/tablepro` in ## Ship -- A user-visible change gets one fragment in `linux/changelog.d/` (`.claude/rules/changelog.md`), never in the root `changelog.d/`, which is the macOS release notes. +- A user-visible change gets one fragment in `linux/changelog.d/` (`.claude/rules/changelog.md`), never in `apple/changelog.d/`, which is the macOS release notes. - Commit scope `linux`, or `linux-` for one area of the app. - A probe worth keeping becomes a test in the crate it measures; the Linux workspace keeps no separate probe directory. diff --git a/.claude/skills/fix-issue/references/research-apple.md b/.claude/skills/fix-issue/references/research-apple.md index 324ed54c81..7dd6686912 100644 --- a/.claude/skills/fix-issue/references/research-apple.md +++ b/.claude/skills/fix-issue/references/research-apple.md @@ -16,9 +16,9 @@ The method is in the research brief (`references/lanes.md`); this file is the so The source is the vendored header and the library we ship, not a web page. -- Headers live under `Plugins/*/C*/include/` and `Libs/`. State the version we link, since a build script has named a version we never shipped. +- Headers live under `apple/Plugins/*/C*/include/` and `apple/Libs/`. State the version we link, since a build script has named a version we never shipped. - Quote the header's doc comments for each symbol in play, above all deprecation notes and what a call returns when it cannot do the job. -- Where the header does not settle it, build a probe and report its output verbatim: C compiled against `Libs/*.a`, a `swiftc` harness, or the vendored CLI. +- Where the header does not settle it, build a probe and report its output verbatim: C compiled against `apple/Libs/*.a`, a `swiftc` harness, or the vendored CLI. ## Comparable clients diff --git a/.claude/skills/fix-issue/references/verification-apple.md b/.claude/skills/fix-issue/references/verification-apple.md index bf3924c0fc..a58dc65f62 100644 --- a/.claude/skills/fix-issue/references/verification-apple.md +++ b/.claude/skills/fix-issue/references/verification-apple.md @@ -1,6 +1,6 @@ # Verification on Apple -Read this when a verdict, a test name or a red CI check needs explaining. The commands, the verdict words and the verdict rules are in `AGENTS.md`, Commands; when each step runs is in `references/platform-apple.md`, Verify. +Read this when a verdict, a test name or a red CI check needs explaining. The commands, the verdict words and the verdict rules are in `apple/AGENTS.md`, Commands; when each step runs is in `references/platform-apple.md`, Verify. ## Usage @@ -33,7 +33,7 @@ Read this when a verdict, a test name or a red CI check needs explaining. The co ## Verdicts -- A test verdict comes from the result bundle or the xunit file, never the console banner. `scripts/lib/xcresult-cases.py` reads both, as it does for the CI summary. +- A test verdict comes from the result bundle or the xunit file, never the console banner. `apple/scripts/lib/xcresult-cases.py` reads both, as it does for the CI summary. - The result-bundle rules, first match wins: 1. nothing executed and the bundle holds build errors: FAIL, "the test build failed"; 2. nothing executed and `xcodebuild` exited nonzero: FAIL; @@ -70,7 +70,7 @@ Read this when a verdict, a test name or a red CI check needs explaining. The co - MemberImportVisibility errors on a new file that uses `Combine` or `TableProPluginKit` members are real: add the import. - CI selects its Xcode in `.github/actions/setup-xcode/action.yml`. A compile error only CI reports is real: compare `xcodebuild -version` with that file, then fix the error. Never re-run hoping it passes. - SourceKit diagnostics in the editor are noise. Only an `xcodebuild` run counts. -- A plugin's scheme is its target name in `project.yml`, not always `Driver`: `MSSQLDriver`, but `SnowflakeDriverPlugin`. `xcodebuild -list` lists them, and `verify.sh build ` builds one. +- A plugin's scheme is its target name in `apple/project.yml`, not always `Driver`: `MSSQLDriver`, but `SnowflakeDriverPlugin`. `xcodebuild -list` lists them, and `verify.sh build ` builds one. - After a package's public API or a type's stored layout changes, a target can still compile against the old `Build/Products/Debug/.swiftmodule` in DerivedData. The link then fails on the old symbol, or the test host crashes in an outlined destroy. Delete that module's `.swiftmodule` and `.o`, or the checkout's DerivedData folder, and rebuild. A clean CI build never sees it. ## Test runs @@ -90,10 +90,10 @@ Read this when a verdict, a test name or a red CI check needs explaining. The co ## Lint - `lint` is SwiftLint only. `agent-docs` is its own step, so a stale doc reference never turns a code lint red; `AGENTS.md`, Writing style and docs, says what it checks. -- `.swiftlint.yml` `included:` holds six roots: `TablePro`, `Packages`, `Plugins`, `TableProMobile`, `TableProTests`, `TableProUITests`. +- `apple/.swiftlint.yml` `included:` holds six roots: `TablePro`, `Packages`, `Plugins`, `TableProMobile`, `TableProTests`, `TableProUITests`. - Some folders lint with fewer rules through a nested `only_rules` config: `.claude/rules/ci.md`, Writing workflow and lint code. - A directory outside `included:` lints nothing and reports 0 violations; the step names it under "NOT LINTED". Pass files. -- Paths go to SwiftLint as given, resolved against the shell's current directory, not `--root`. Pass absolute paths when the shell is elsewhere. +- A relative path resolves against the shell's current directory, not `--root`; the step makes it absolute and runs SwiftLint in `apple/`, so `apple/.swiftlint.yml` applies. Pass absolute paths when the shell is elsewhere. - `force_cast` and `force_unwrapping` are warnings, so only `--strict` fails them. A plain `swiftlint lint` passes code CI rejects. ## Reading CI @@ -107,7 +107,7 @@ Read this when a verdict, a test name or a red CI check needs explaining. The co ## Worktrees -- `.claude/skills/fix-issue/scripts/worktree.sh [base]` creates `.claude/worktrees/` from a fresh fetch of the base, `origin/main` by default. It links `Configs/Secrets.xcconfig`, `Libs/*.a`, `Libs/dylibs`, `Libs/ios/*.xcframework` and the native bridge outputs from the main checkout, and prints the path to pass as `--root`. Run `generate` before its first build. +- `.claude/skills/fix-issue/scripts/worktree.sh [base]` creates `.claude/worktrees/` from a fresh fetch of the base, `origin/main` by default. It links `apple/Configs/Secrets.xcconfig`, `apple/Libs/*.a`, `apple/Libs/dylibs`, `apple/Libs/ios/*.xcframework` and the native bridge outputs from the main checkout, and prints the path to pass as `--root`. Run `generate` before its first build. - `worktree.sh --prune-merged [--dry-run]` removes each tree whose pull requests are all merged or closed, with its DerivedData folder. It removes one only when `git status` is clean and no process has its working directory inside; a kept tree says why. A Codex broker started in a tree is the usual process that keeps it. - `worktree.sh --remove ` keeps the branch and refuses a tree with uncommitted or untracked changes. - Each tree builds into its own DerivedData. `build`, `plugins`, `test`, `uitest` and `ios` wait only for an `xcodebuild` of the same checkout, found by its project path or working directory, so `--no-wait` is safe in your own tree. diff --git a/.claude/skills/fix-issue/scripts/lib/platform-apple.sh b/.claude/skills/fix-issue/scripts/lib/platform-apple.sh index eb0e696ca1..da55f598af 100644 --- a/.claude/skills/fix-issue/scripts/lib/platform-apple.sh +++ b/.claude/skills/fix-issue/scripts/lib/platform-apple.sh @@ -5,9 +5,9 @@ # verify.sh [options] generate # verify.sh [options] build [Scheme] # default scheme: TablePro # verify.sh [options] plugins # AllPlugins aggregate -# verify.sh [options] test [Test...] # TestPlans/Unit.xctestplan, all of it when no Test is named -# verify.sh [options] uitest [Test...] # TestPlans/UI.xctestplan, all of it when no Test is named -# verify.sh [options] package [regex] # swift test in Packages/ +# verify.sh [options] test [Test...] # apple/TestPlans/Unit.xctestplan, all of it when no Test is named +# verify.sh [options] uitest [Test...] # apple/TestPlans/UI.xctestplan, all of it when no Test is named +# verify.sh [options] package [regex] # swift test in apple/Packages/ # verify.sh [options] ios [Suite...] # TableProMobileTests on a simulator # verify.sh [options] abi [base] # PluginKit ABI checker; default base: the newest v* tag # verify.sh [options] lint [path...] @@ -22,6 +22,7 @@ # Here --no-wait skips the wait for another xcodebuild, and --offline pins SwiftPM to Package.resolved. MAX_WAIT_SECONDS=1800 +APPLE_ROOT="$REPO_ROOT/apple" # Known false failures in this checkout. Each one presents as a code defect and is not one. diagnose_environment() { @@ -39,7 +40,7 @@ diagnose_environment() { found=0 fi if grep -q 'Unable to open base configuration reference file' "$log" 2> /dev/null; then - note "cause: the generated project names an xcconfig this checkout lacks. Rerun scripts/generate-project.sh (verify.sh generate)." + note "cause: the generated project names an xcconfig this checkout lacks. Rerun apple/scripts/generate-project.sh (verify.sh generate)." found=0 fi if grep -q 'The test runner hung before establishing connection' "$log" 2> /dev/null; then @@ -85,8 +86,10 @@ setup_toolchain() { checkout_busy() { local pid for pid in $(pgrep -f 'Developer/usr/bin/xcodebuild' 2> /dev/null); do - ps -o args= -p "$pid" 2> /dev/null | grep -qF "$REPO_ROOT/TablePro.xcodeproj" && return 0 - [ "$(lsof -a -p "$pid" -d cwd -Fn 2> /dev/null | sed -n 's/^n//p')" = "$REPO_ROOT" ] && return 0 + ps -o args= -p "$pid" 2> /dev/null | grep -qF "$APPLE_ROOT/TablePro.xcodeproj" && return 0 + case "$(lsof -a -p "$pid" -d cwd -Fn 2> /dev/null | sed -n 's/^n//p')" in + "$REPO_ROOT" | "$APPLE_ROOT") return 0 ;; + esac done return 1 } @@ -119,11 +122,11 @@ xcodebuild_flags() { test_targets_table() { local plan manifest package target table="" for plan in Unit UI; do - [ -f "$REPO_ROOT/TestPlans/$plan.xctestplan" ] || continue + [ -f "$APPLE_ROOT/TestPlans/$plan.xctestplan" ] || continue table="$table$(jq -r --arg plan "$plan" '.testTargets[].target.name + "\t" + $plan' \ - "$REPO_ROOT/TestPlans/$plan.xctestplan")"$'\n' + "$APPLE_ROOT/TestPlans/$plan.xctestplan")"$'\n' done - for manifest in "$REPO_ROOT"/Packages/*/Package.swift; do + for manifest in "$APPLE_ROOT"/Packages/*/Package.swift; do [ -f "$manifest" ] || continue package="${manifest%/Package.swift}" package="${package##*/}" @@ -146,7 +149,7 @@ route_of() { # prints as ?. targets_declaring() { local file part target - git -C "$REPO_ROOT" grep --untracked -l -E \ + git -C "$APPLE_ROOT" grep --untracked -l -E \ "^[^/\"]*(struct|class|enum|actor|extension)[[:space:]]+$1([^A-Za-z0-9_]|\$)" -- '*Tests/*.swift' 2> /dev/null \ | while IFS= read -r file; do target="" @@ -208,14 +211,14 @@ test_regex() { # ---------------------------------------------------------------------------- test results # Beside this script, not under --root, so the parser always matches the tally format read here. -XCRESULT_CASES="$SCRIPT_DIR/../../../../scripts/lib/xcresult-cases.py" +XCRESULT_CASES="$SCRIPT_DIR/../../../../apple/scripts/lib/xcresult-cases.py" # Sets RUN_EXECUTED, RUN_FAILED, RUN_ZERO and RUN_SYSTEM from the cases in a result bundle or xunit # file, counted against the run's filters ("