diff --git a/.claude/skills/fix-issue/SKILL.md b/.claude/skills/fix-issue/SKILL.md index b8882a289d..4424a147c1 100644 --- a/.claude/skills/fix-issue/SKILL.md +++ b/.claude/skills/fix-issue/SKILL.md @@ -85,7 +85,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` 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. +**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 `paths:` header 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. @@ -123,7 +123,7 @@ Fix what is real and inside the scope boundary without asking. A real finding ab 1. **Stage explicit paths**, never `git add -A`, `.` or a directory, and compare `git -C diff --cached --stat` with what you changed. 2. **Run `scripts/check-banned-words.sh --staged`**, and the same script over the PR body file. -3. **Commit, then push, in separate calls**, after `git -C branch --show-current` in a call of its own. The PR title becomes the squash commit subject; its format, types and scopes are in the top comment of `.github/pull_request_template.md`. Never override the git identity. If SSH fails: `git -c credential.helper='!gh auth git-credential' push https://github.com/TableProApp/TablePro.git `. +3. **Commit, then push, in separate calls**, after `git -C branch --show-current` in a call of its own. The PR title becomes the squash commit subject; its format and types are in the top comment of `.github/pull_request_template.md`. Never override the git identity. If SSH fails: `git -c credential.helper='!gh auth git-credential' push https://github.com/TableProApp/TablePro.git `. 4. **Open the PR, ready for review**: `gh pr create --repo TableProApp/TablePro --base --head --title "" --body-file `, where `` is the profile's pull request base. The body follows `.github/pull_request_template.md` and reads in under a minute. It is not a log: no investigation story, no review history, no list of every test run. 5. **Screenshots**, only when the change is visible: before and after, light and dark, from an isolated build captured the way the profile's Screen section says. Look at each one before using it. Attach with `gh pr create --attach './after.png#After'` and reference it as `![After](./after.png)`. diff --git a/.claude/skills/fix-issue/references/platform-apple.md b/.claude/skills/fix-issue/references/platform-apple.md index 4c53db7709..269af75989 100644 --- a/.claude/skills/fix-issue/references/platform-apple.md +++ b/.claude/skills/fix-issue/references/platform-apple.md @@ -16,7 +16,7 @@ The macOS app and the iOS app, with everything they build from. Every profile ha ## Done -- 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. +- The tree's `apple/AGENTS.md`, the root `AGENTS.md` it points to, and every `.claude/rules/` file whose `paths:` header matches 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 @@ -43,7 +43,6 @@ The macOS app and the iOS app, with everything they build from. Every profile ha | `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 | | `docs` | `[docs-checkout]`, default `tablepro-docs` beside the main checkout | AGENTS Every change 4 applies, or a menu, shortcut, driver list or docs.tablepro.app link changed | -- `agent-docs` runs on every platform; AGENTS Every change 5 says when. - Pick tests by the types you changed, not the test files you edited: `verify.sh` finds a type name in every test target of the tree. - Extra review pass: the ABI pass when PluginKit changed. - Test names, verdicts, the remedies `verify.sh` does not print, and red CI checks: `references/verification-apple.md`. diff --git a/.claude/skills/fix-issue/references/verification-apple.md b/.claude/skills/fix-issue/references/verification-apple.md index a58dc65f62..6b3b822946 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 `apple/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 are in `apple/AGENTS.md`, Commands; when each step runs is in `references/platform-apple.md`, Verify. ## Usage @@ -89,7 +89,7 @@ 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. +- `lint` is SwiftLint only. - `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. diff --git a/.claude/skills/fix-issue/scripts/verify.sh b/.claude/skills/fix-issue/scripts/verify.sh index e3ebf2dbd0..1a9780c62e 100755 --- a/.claude/skills/fix-issue/scripts/verify.sh +++ b/.claude/skills/fix-issue/scripts/verify.sh @@ -8,7 +8,6 @@ # # Usage: # verify.sh [options] [args] # a step of the platform, listed below -# verify.sh [options] agent-docs # the agent docs (check-doc-symbols.sh lists them) against the tree # verify.sh tail [lines] # re-read a stored log without rerunning # verify.sh parse # re-read the verdict for a stored log # @@ -216,21 +215,6 @@ case "$STEP" in emit "$1" "-" ;; - agent-docs) - # Kept out of `lint`, so one stale reference on main never turns a code lint red. - log="$(new_log agent-docs)" - run_logged "$log" "$REPO_ROOT/scripts/check-doc-symbols.sh" - code=$? - if [ $code -eq 0 ]; then - STATUS=PASS - note "$(tail -1 "$log")" - else - STATUS=FAIL - note "$(grep -E '^((apple|linux)/)?(CLAUDE|AGENTS|CONTRIBUTING|RELEASING)|^\.claude|^\.github' "$log" | sed 's/^/ /' | head -10)" - fi - emit "$log" $code - ;; - *) load_platform platform_step "$@" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 80a3b3b368..a1d5997414 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,32 +1,11 @@ - + -## Why +## Summary - + -## Change +## Test plan - - -## Verified - - - -## Screenshots - - - -## Not in this PR - - + Fixes # - diff --git a/AGENTS.md b/AGENTS.md index 8aa7d911de..34993a76d1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,77 +1,33 @@ # TablePro -Native database clients, one per platform, sharing no code: `apple/` holds the macOS and iOS apps (Swift, AppKit and SwiftUI), `linux/` the Linux app (Rust, GTK 4). This guide holds the rules both follow. Claude Code loads it through `CLAUDE.md`, Codex directly. - -Each app has its own guide beside its code: `apple/AGENTS.md` and `linux/AGENTS.md`. Claude Code loads one through its `CLAUDE.md` once it reads a file in that folder; Codex reads one when started there, so otherwise read the guide before working in its folder. - -## Principles - -1. **Security first.** Validate at system boundaries; never introduce injection, credential exposure or a widened permission by accident. -2. **Fix the cause, not the symptom.** Diagnose (add logging if needed), then fix the cause. No workaround left behind. -3. **Tests define behavior.** Every testable change has a test. When a test fails, fix the code, never the test. A test that pins behavior the PR changes on purpose is updated in the same PR and named in its description. +Native database clients that share no code: `apple/` holds the macOS and iOS apps (Swift), `linux/` the Linux app (Rust, GTK 4). Read `apple/AGENTS.md` or `linux/AGENTS.md` before working in that folder. Claude Code loads it through the folder's `CLAUDE.md`, Codex only when started there. ## Repository map -- `apple/`: the macOS and iOS apps, their plugins, packages, native bridges, scripts and changelog. -- `linux/`: the GTK 4 Linux app, built with Meson and Cargo, with its own changelog. -- `.github/`: workflows, actions, the PR template, `plugin-registry.json` and the CI baselines. -- `.claude/`: the path-scoped rules (Area rules below) and the `fix-issue` and `release` skills. -- `scripts/`: what both apps share: `banned-words.txt` with `check-banned-words.sh`, `check-doc-symbols.sh`, the PR title, changelog and changed-path checks in `scripts/ci`, and the towncrier pin in `scripts/release`. -- `appcast.xml`: the legacy feed apps up to 0.81 read from `main` (`apple/RELEASING.md`, Channels). It stays at the root. -- Docs site: TableProApp/docs, cloned beside this repo as `../tablepro-docs`, under its own `AGENTS.md` and `STYLE.md`. - -## Verification +- `apple/`, `linux/`: each app with its scripts and changelog. +- `.github/`: workflows, actions, the PR template and `plugin-registry.json`. +- `.claude/`: the path-scoped rules and the `fix-issue` and `release` skills, which Codex sees through `.agents/skills/`. +- `scripts/`: `banned-words.txt` with `check-banned-words.sh`, the PR title and changed-path checks in `scripts/ci`, and the towncrier pin in `scripts/release`. +- `appcast.xml`: the legacy feed apps up to 0.81 read from `main` (`apple/RELEASING.md`, Channels). +- Docs site: TableProApp/docs, cloned beside this repo as `../tablepro-docs`, under its own `AGENTS.md`. -`.claude/skills/fix-issue/scripts/verify.sh` runs every step from the repository root, keeps the full log and prints `PASS`, `FAIL` or `INCONCLUSIVE`. It picks the platform from the machine; `--platform linux` names one. `INCONCLUSIVE` is never a pass. Steps per platform: the `fix-issue` references, `platform-apple.md` and `platform-linux.md`. - -## Writing style and docs +## Principles -- All writing: short sentences, plain words, specific numbers and names, no em dash, no filler. -- `scripts/banned-words.txt` is the list; never copy its terms. `scripts/check-banned-words.sh --staged` checks a staged change. -- One home per fact; others point to it by repo path and section. No drifting counts or timings; a plan is not a fact. -- Code comments say why, never what. Delete what-comments in code you touch. -- A path in a platform's docs is relative to its folder; a root doc writes it from the repository root. -- Budgets: a platform guide 16 KiB, a rule 200 lines, a `SKILL.md` 500, a skill description 1,536 characters. -- `verify.sh agent-docs` checks backticked names in the guides, `CONTRIBUTING.md`, `RELEASING.md`, `.github/pull_request_template.md` and `.claude/`. Run it when one changes. Never state a plugin count in prose. +- Tests define behavior. When a test fails, fix the code, never the test. A test that pins behavior the PR changes on purpose is updated in the same PR and named in its description. +- Verify with `.claude/skills/fix-issue/scripts/verify.sh` from the repository root. It prints `PASS`, `FAIL` or `INCONCLUSIVE`; `INCONCLUSIVE` is never a pass. +- Writing: short sentences, plain words, no em dash, no term from `scripts/banned-words.txt`. Code comments say why, never what. +- One home per fact; other docs point to it by path and section. ## Commits and PRs -- The PR title is the squash commit subject; branch commits use the same format. The body follows `.github/pull_request_template.md`, whose top comment holds the format, types and scopes. The `PR title` check (`scripts/ci/check-pr-title.py`) enforces the format, not scopes. -- A user-visible change adds a fragment to its app's `changelog.d/`; only a release PR edits a `CHANGELOG.md`. Format: `.claude/rules/changelog.md`. +- The PR title is the squash commit subject, `type(scope): description`. The body follows `.github/pull_request_template.md`. - A rename or signature change updates every caller and test in the same commit. +- Agents merge on green with `gh pr merge --auto --squash`. They create a `release/*` branch or a tag only when the maintainer says to release, and use `--admin` only on the maintainer's word. ## CI -- A PR into `main` or `release/*` merges on green macOS Tests, iOS and Package Tests, SwiftLint, Repo Hygiene, PR title and Linux. Nothing bypasses them. -- Read why a CI check failed before any re-run. -- Workflows: `.claude/rules/ci.md`. Red checks: `.claude/skills/fix-issue/references/verification-apple.md` and `verification-linux.md`, Reading CI. +A PR merges once its required checks pass: `.claude/rules/ci.md`, or `gh api repos/TableProApp/TablePro/rules/branches/main` for the live list. Read why a check failed before any re-run. ## Area rules -Claude Code loads a rule when it reads or edits a file the rule covers. Under Codex, before editing a file, read every rule whose row matches it. Each row is the rule's `paths:` with a trailing `/**/*` dropped; `verify.sh agent-docs` keeps them equal. - -| Rule | Read before editing | -| --- | --- | -| `ai-mcp-security.md` | `apple/TablePro/Core/AI`, `apple/TablePro/Core/MCP`, `apple/Packages/TableProMacCore/Sources/TableProMCP` | -| `changelog.md` | `**/CHANGELOG.md`, `**/changelog.d`, `**/towncrier.toml` | -| `ci.md` | `.github/workflows`, `.github/actions`, `.github/actionlint.yaml`, `scripts/ci`, `apple/scripts/ci`, `scripts/release`, `apple/scripts/release`, `apple/.swiftlint.yml`, `**/.swiftlint.yml` | -| `connection-window.md` | `apple/TablePro/Core/Services/Infrastructure`, `apple/TablePro/Models/Connection`, `apple/TablePro/Core/Database/DatabaseManager*.swift`, `apple/Packages/TableProCore/Sources/TableProDatabase`, `apple/TablePro/Views/Main` | -| `data-grid.md` | `apple/TablePro/Views/Results`, `apple/TablePro/Core/DataGrid`, `apple/TableProTests/Views/Results` | -| `data-sync-storage.md` | `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` | -| `editor.md` | `apple/Packages/TableProEditor`, `apple/TablePro/Theme`, `apple/TablePro/Views/Editor`, `apple/TablePro/Views/RowInspector` | -| `libs.md` | `apple/Libs`, `apple/scripts/download-libs.sh`, `apple/scripts/publish-libs.sh` | -| `mongodb-driver.md` | `apple/Plugins/MongoDBDriverPlugin`, `apple/Plugins/MQLExportPlugin` | -| `mysql-driver.md` | `apple/Plugins/MySQLDriverPlugin`, `apple/Packages/TableProCore/Sources/TableProMySQLCore`, `apple/TableProMobile/**/*MySQL*`, `apple/TableProMobile/**/*MariaDB*` | -| `native-bridges.md` | `apple/Native`, `apple/Plugins/DamengDriverPlugin`, `apple/Plugins/HanaDriverPlugin`, `apple/scripts/build-dameng.sh`, `apple/scripts/build-hana.sh` | -| `plugin-system.md` | `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`, `apple/scripts/*plugin*` | -| `redis-driver.md` | `apple/Plugins/RedisDriverPlugin`, `apple/Packages/TableProCore/Sources/TableProRedisCore` | -| `split-views.md` | `apple/TablePro/**/*Split*.swift`, `apple/TablePro/Core/Services/Infrastructure/WorkspacePane*.swift` | -| `tests.md` | `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` | -| `theme.md` | `apple/TablePro/Theme`, `apple/TablePro/Views`, `apple/TablePro/Core/Services/Infrastructure/*Window*`, `apple/Packages/TableProCore/Sources/TableProTheme` | -| `ui-lifecycle.md` | `apple/TablePro/Views`, `apple/TablePro/ViewModels`, `apple/TablePro/Core/Services/Infrastructure`, `apple/Packages/TableProEditor` | -| `ui-tests.md` | `apple/TableProUITests`, `apple/TestPlans/UI.xctestplan` | -| `welcome-library.md` | `apple/TablePro/Views/Welcome`, `apple/TablePro/ViewModels/WelcomeViewModel*.swift`, `apple/Packages/TableProCore/Sources/TableProConnectionLibrary` | - -## Skills - -`fix-issue` takes a problem to one green PR and holds `verify.sh` and its references; `release` ships a macOS version (the Linux runbook is `linux/docs/releasing.md`). Both live in `.claude/skills/`; Codex sees them through `.agents/skills/`. +Before editing a file, read each `.claude/rules/*.md` whose `paths:` header matches it. Claude Code loads them itself. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f0cd3876e8..e2937bd7b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ Branch off `main`: `feat/add-cassandra-support`, `fix/query-editor-crash`. PRs t ## Pull requests -One logical change per PR, described with the template the PR form opens with. The title format, types and scopes are in the top comment of `.github/pull_request_template.md`. A PR merges when the required checks in [`AGENTS.md`](AGENTS.md), CI, pass; what each workflow runs is in `.claude/rules/ci.md`. What a PR for each app carries: its `CONTRIBUTING.md`, Pull requests. +One logical change per PR, described with the template the PR form opens with. The title format and types are in the top comment of `.github/pull_request_template.md`. A PR merges when the required checks in [`AGENTS.md`](AGENTS.md), CI, pass; what each workflow runs is in `.claude/rules/ci.md`. What a PR for each app carries: its `AGENTS.md`, Every change. ## Reporting bugs diff --git a/apple/AGENTS.md b/apple/AGENTS.md index 97c3c050f9..13aa87b0fe 100644 --- a/apple/AGENTS.md +++ b/apple/AGENTS.md @@ -1,16 +1,11 @@ # TablePro for macOS and iOS -A native macOS database client (SwiftUI and AppKit), a lightweight alternative to TablePlus. macOS 13.0+, Swift 6 language mode for the app targets (`Configs/Base.xcconfig`), Universal Binary. `TableProMobile/` is the iOS app. - -This is the guide for work under `apple/`; a path here is relative to it, except `.claude/` and `.github/`, which sit at the repository root. The root `AGENTS.md` holds what every app shares: the principles, writing, commits and PRs, CI and the Area rules. Claude Code loads this file through `apple/CLAUDE.md` once a file under `apple/` is read; Codex reads it when started in `apple/`. +A native macOS database client (AppKit and SwiftUI), macOS 13.0+, Swift 6 language mode for the app targets. `TableProMobile/` is the iOS app. A path here is relative to `apple/`, except `.claude/` and `.github/` at the repository root. ## Principles -The root `AGENTS.md` principles apply: security first, fix the cause (add OSLog to diagnose), tests define behavior. For the Apple apps also: - -1. **Native only.** AppKit, SwiftUI and system frameworks; no cross-platform layer, no web view for native UI. Follow the documented Apple API and the HIG. -2. **Clean architecture.** Separation of concerns, protocol-oriented design, dependency injection where it earns its place. Weigh each change's effect on the design. -3. **Open plugin domain.** `DatabaseType` is a string-backed struct (static constants, `allKnownTypes`), so a future plugin's type round-trips. A `switch` over an open type keeps a `default:`; one over a closed enum stays exhaustive. +- **Native only.** AppKit, SwiftUI and system frameworks, used as Apple documents them and the HIG describes. No cross-platform layer, no web view for native UI. +- **Open plugin domain.** `DatabaseType` is a string-backed struct, so a plugin's type round-trips. A `switch` over an open type keeps a `default:`; one over a closed enum stays exhaustive. ## Scope @@ -29,23 +24,12 @@ Every capability is core, a module or a plugin. The first rule that fits decides ## Repository map -- `TablePro/`: the app (`Core/`, `Views/`, `Models/`, `ViewModels/`, `Extensions/`, `Theme/`). -- `Plugins/`: drivers and import/export formats as `.tableplugin` bundles, plus `TableProPluginKit`. Bundled plugins are the app's `copy: { destination: plugins }` targets in `project.yml`; the rest ship as assets of each app release, and the app never offers a registry install or update over a bundled one. -- `Packages/`: `TableProCore` (both apps), `TableProMacCore` (macOS-only modules; never in the iOS project), `TableProOracle`, `TableProEditor`, `TableProGrammars`. `TableProCore`'s PluginKit source is a symlink: edit `Plugins/TableProPluginKit/` only. Each manifest sets its own Swift language mode, so never pass `SWIFT_VERSION=` to `xcodebuild`. -- Tests: `TableProTests/`, `TableProUITests/`, `Plugins/*Tests/`, `Packages/*/Tests/`, `TableProMobile/TableProMobileTests/`; plans in `TestPlans/`. Which holds what: `.claude/rules/tests.md`. -- `Native/`: first-party Rust and Go bridges (`.claude/rules/native-bridges.md`). -- `Libs/`: static libraries and iOS xcframeworks from `scripts/download-libs.sh`, not in git. -- `scripts/`: the Apple build, library, release and CI scripts; the scripts both apps use stay in the root `scripts/`. -- `CHANGELOG.md`, `changelog.d/` and `towncrier.toml`: the macOS release notes. -- `TablePro.xcodeproj` and `TableProMobile/TableProMobile.xcodeproj` are generated by XcodeGen from their `project.yml`, gitignored except their `Package.resolved`; never hand-edit them. Signing overrides go in `Configs/Secrets.xcconfig`. - -## Architecture - -- **Plugins.** `PluginManager` loads `DriverPlugin` bundles; `DatabaseDriverFactory` finds one by `pluginTypeId`; `PluginDriverAdapter` bridges its `PluginDatabaseDriver` to `DatabaseDriver`; `DatabaseManager` owns sessions. A new requirement gets a default implementation, then the adapter (`.claude/rules/plugin-system.md`). -- **Main window.** `MainContentCoordinator`, split into `Views/Main/Extensions/`; one `ConnectionWorkspace` per connection (`.claude/rules/connection-window.md`). -- **Change tracking.** `DataChangeManager` holds edits behind `AnyChangeManager`; `SQLStatementGenerator` writes them on Save. Undo: `StructureChangeManager`'s `UndoManager`, `ConnectionWorkspace.undoManager`. -- **Editor and theme.** `TableProTheme` holds slots, fallbacks, the file format and `ThemeResolver` (picks the light and dark pair). `ThemeCatalog` owns and syncs the theme files; `ThemeEngine` owns the mode and theme ids, follows the catalog, and holds the palette and font caches; fonts come from `TypographySettings`. A theme colors content and window backdrop, never chrome (`.claude/rules/theme.md`). `QueryCompletionAdapter` bridges `CompletionEngine`; `EditorTabStrip` draws tabs (`.claude/rules/editor.md`). -- **Storage.** Passwords in the Keychain, preferences in UserDefaults, query history in SQLite FTS5, tab state as JSON (`.claude/rules/data-sync-storage.md`). +- `TablePro/`: the app. +- `Plugins/`: drivers and import/export formats as `.tableplugin` bundles, plus `TableProPluginKit`. Bundled plugins are the `copy: { destination: plugins }` targets in `project.yml`; the app never offers a registry install or update over one. +- `Packages/`: `TableProCore` (both apps), `TableProMacCore` (macOS only, never in the iOS project), `TableProOracle`, `TableProEditor`, `TableProGrammars`. Edit PluginKit in `Plugins/TableProPluginKit/` only; `TableProCore` symlinks it. Never pass `SWIFT_VERSION=` to `xcodebuild`. +- `Native/`: Rust and Go bridges. `Libs/`: static libraries from `scripts/download-libs.sh`, not in git. +- XcodeGen generates `TablePro.xcodeproj` and `TableProMobile/TableProMobile.xcodeproj`; never hand-edit them. Signing overrides go in `Configs/Secrets.xcconfig`. +- Architecture lives in `.claude/rules/`: `plugin-system.md`, `connection-window.md`, `editor.md`, `theme.md` and `data-sync-storage.md`. ## Commands @@ -53,58 +37,36 @@ From the repository root: ```bash apple/scripts/download-libs.sh # first time, and after a libs update -apple/scripts/generate-project.sh [both|macos|ios] # needs the libs; rerun after editing a project.yml or Configs/, or adding, moving or deleting a source file +apple/scripts/generate-project.sh [both|macos|ios] # after a project.yml or Configs/ edit, or a new, moved or deleted source file .claude/skills/fix-issue/scripts/verify.sh build -.claude/skills/fix-issue/scripts/verify.sh test [Test...] # no Test: the whole Unit plan +.claude/skills/fix-issue/scripts/verify.sh test [|/()|/...] # none: the Unit plan .claude/skills/fix-issue/scripts/verify.sh lint ... ``` -- `verify.sh` logs to disk and prints `PASS`, `FAIL` or `INCONCLUSIVE`, exit 0, 1 or 2; 3 is a usage error that ran nothing. -- A Test is `` (a Swift type name), `/()`, or either behind its target, `/`. -- `INCONCLUSIVE` is never a pass. -- A run that executed nothing is `FAIL`, unless a named environment cause makes it `INCONCLUSIVE`. -- `verify.sh --help` lists every step. Steps per change: `.claude/skills/fix-issue/references/platform-apple.md`, Verify. Test names, verdicts, remedies, red CI checks: `.claude/skills/fix-issue/references/verification-apple.md`. -- A raw `xcodebuild` runs in `apple/` and passes `-skipPackagePluginValidation`, plus `-testPlan Unit` or `UI` for macOS tests. Raw commands: `CONTRIBUTING.md`, Tests. -- The macOS plans own their run settings: never add `-parallel-testing-enabled`, `-test-iterations` or `-retry-tests-on-failure`, except to reproduce a flake (`.claude/rules/tests.md`). iOS tests have no plan and pass `-parallel-testing-enabled NO` (`.claude/rules/ci.md`, iOS and Package Tests). +- `verify.sh --help` lists every step. When each runs: `.claude/skills/fix-issue/references/platform-apple.md`, Verify. Verdicts, test names and red CI checks: `.claude/skills/fix-issue/references/verification-apple.md`. +- A raw `xcodebuild` runs in `apple/` with `-skipPackagePluginValidation`, plus `-testPlan Unit` or `UI` for macOS tests (`CONTRIBUTING.md`, Tests). +- Never add `-parallel-testing-enabled`, `-test-iterations` or `-retry-tests-on-failure` to a macOS run, except to reproduce a flake: the plans own those. iOS tests pass `-parallel-testing-enabled NO`. +- Never run `swiftlint --fix`. Formatting SwiftLint leaves open: `CONTRIBUTING.md`, Code style. - Releases: `RELEASING.md` and the `release` skill. ## Invariants that apply everywhere -- **AppKit lifecycle.** `main.swift` starts the app; `MainMenuBuilder.install` builds the menus in `applicationWillFinishLaunching`; windows are `NSWindowController`s. A SwiftUI `App` rewrites `NSApp.mainMenu`: never add one. +- **AppKit lifecycle.** `main.swift` starts the app, `MainMenuBuilder.install` builds the menus, windows are `NSWindowController`s. Never add a SwiftUI `App`: it rewrites `NSApp.mainMenu`. - **A refresh never clears what it refreshes.** Fetch, then commit over the old value; `.loading` only when nothing is loaded; keep good data on failure; `prepareForReload` to reload, `invalidate` on disconnect. -- **A canceled connect keeps running.** `Task.cancel()` cannot interrupt a blocking C call: poll the connect or resume through `runCancellableBlocking` (the late call closes its handle), then check the `ConnectionAttemptRegistry` generation before adopting a driver. -- **Storage resolves through `AppStorageEnvironment.shared`** (stores, defaults suite, keychain); the `storage_environment_*` SwiftLint rules fail other Application Support or standard-defaults use. -- **An AppKit autosave name** (split view, window frame, table) **goes through `SplitViewAutosaveName.current(_:)`**: AppKit writes the record to the real defaults domain, which the sandbox does not redirect. +- **A canceled connect keeps running.** `Task.cancel()` cannot interrupt a blocking C call: poll the connect or resume through `runCancellableBlocking`, then check the `ConnectionAttemptRegistry` generation before adopting a driver. +- **Storage resolves through `AppStorageEnvironment.shared`**: stores, defaults suite and keychain. +- **An AppKit autosave name** (split view, window frame, table) **goes through `SplitViewAutosaveName.current(_:)`**: the sandbox does not redirect the defaults domain AppKit writes it to. - **No new test-only branch in production code**: tests use injection or `TABLEPRO_UI_TEST_SANDBOX`. - **macOS files the iOS app compiles** (listed in `TableProMobile/project.yml`) give each top-level declaration explicit isolation, usually `nonisolated`, since iOS defaults to `MainActor`. The iOS build is the check. -- **No package product in a macOS binary depends on the `TableProPluginKit` package**: a plugin would carry its own PluginKit types (`.claude/rules/plugin-system.md`). +- **No package product in a macOS binary depends on the `TableProPluginKit` package**: a plugin would carry its own PluginKit types. - **A source-text rule is a SwiftLint rule or a `scripts/ci` check**, never a test reading source. - -## Code style - -- `.swiftlint.yml` is the source of truth. Never run `swiftlint --fix`. -- **Comments say why, never what.** A short `///` only where code would surprise a reader (a platform quirk, an invariant, a workaround's reason). Delete what-comments in code you touch. -- Early returns with `guard`, small functions, self-explanatory names. -- Explicit access control, declared on the extension. -- No force unwrap or force cast. -- Log with `Logger(subsystem: "com.TablePro", category: ...)`, never `print()`. -- Imports: one alphabetical block (`sorted_imports`). -- Booleans start with `is`, `has`, `can` or `should`; factories with `make`; acronyms all caps (`SQLStatementGenerator`). -- Near a size limit, split into `TypeName+Category.swift` by domain. `file_length` skips comment-only lines: trust `swiftlint lint --strict --no-cache `, not `wc -l`. -- Formatting beyond SwiftLint: `CONTRIBUTING.md`, Code style. - -## Performance pitfalls - - Never `ForEach($bindable.array) { $item in }` on an `@Observable` array that can shrink: index bindings crash. Use `ForEach(array)` and a manual binding. -- On large strings use `(string as NSString).length` and `character(at:)`, never `.count` or `index(_:offsetBy:)` in a loop. -- Never call `ensureLayout(forCharacterRange:)`: it defeats non-contiguous layout. -- A SQL dump line can hold millions of characters: cap regex and highlight ranges at 10k. +- Log with `Logger(subsystem: "com.TablePro", category: ...)`, never `print()`. ## Every change -1. **Tests.** Every testable change has a test in the target owning the code. Read `.claude/rules/tests.md` before creating a test file: a path rule loads only once a matching file is open. A UI flow only for a new or changed end-to-end flow (`.claude/rules/ui-tests.md`). -2. **Changelog.** A user-visible change adds `changelog.d/..md`; only a release PR edits `CHANGELOG.md`. Format: `.claude/rules/changelog.md`. -3. **Localization.** `String(localized:)` outside SwiftUI literals, never interpolated: `String(format: String(localized: "Preview %@"), name)`. Technical terms stay unlocalized. A plugin or package string lives in the host app's catalog: add it from `apple/` with `python3 scripts/localization.py plugins --add`, plus `--target ios` for a package the iOS app links. Never hand-edit a `.xcstrings`. +1. **Tests.** A test in the target owning the code. Read `.claude/rules/tests.md` before creating a test file. A UI flow only for a new or changed end-to-end flow (`.claude/rules/ui-tests.md`). +2. **Changelog.** A user-visible change adds `changelog.d/..md`; only a release PR edits `CHANGELOG.md` (`.claude/rules/changelog.md`). +3. **Localization.** `String(localized:)` outside SwiftUI literals, never interpolated: `String(format: String(localized: "Preview %@"), name)`. Technical terms stay unlocalized. A plugin or package string goes in the host app's catalog: run `python3 scripts/localization.py plugins --add` from `apple/`, plus `--target ios` for a package the iOS app links. Never hand-edit a `.xcstrings`. 4. **Docs.** A new shortcut, UI or settings change, or driver change gets a paired draft PR in TableProApp/docs naming this one; it merges after this one. `verify.sh docs` runs its checks here first. -5. **Lint** each changed Swift file with `verify.sh lint`. Run `verify.sh agent-docs` when a guide or anything under `.claude/` changed. -6. **Commits and PRs**: the root `AGENTS.md`, Commits and PRs. +5. **Lint** each changed Swift file with `verify.sh lint`. diff --git a/apple/CONTRIBUTING.md b/apple/CONTRIBUTING.md index 06c348196f..f810b012ea 100644 --- a/apple/CONTRIBUTING.md +++ b/apple/CONTRIBUTING.md @@ -73,7 +73,7 @@ xcodebuild build -project TablePro.xcodeproj -scheme AllPlugins -destination 'pl From the repository root, prefer `.claude/skills/fix-issue/scripts/verify.sh test ` ([`AGENTS.md`](AGENTS.md), Commands): it finds the target that declares the suite and fails a run that executed nothing. Where a test goes and how to write one: `.claude/rules/tests.md`; UI flows: `.claude/rules/ui-tests.md`. -The raw equivalents, in order: a suite in the Unit plan, a UI flow (needs Automation Mode, see Troubleshooting), a TableProCore or TableProOracle suite (no Xcode plan lists those two packages), and the iOS app: +The raw equivalents, in order: a suite in the Unit plan, a UI flow (needs Automation Mode: `.claude/skills/fix-issue/references/verification-apple.md`, INCONCLUSIVE), a TableProCore or TableProOracle suite (no Xcode plan lists those two packages), and the iOS app: ```bash xcodebuild -project TablePro.xcodeproj -scheme TablePro test -testPlan Unit -skipPackagePluginValidation -only-testing:/ @@ -96,11 +96,11 @@ xcodebuild -project TableProMobile/TableProMobile.xcodeproj -scheme TableProMobi | Trailing commas | None | | `self.` | Only where the compiler needs it | -Naming, imports, access control, force unwrapping, logging, comments, storage, file size and localization: [`AGENTS.md`](AGENTS.md), Code style, Invariants that apply everywhere, and Every change. +Logging, storage and localization: [`AGENTS.md`](AGENTS.md), Invariants that apply everywhere and Every change. ## Architecture -The architecture and its invariants: [`AGENTS.md`](AGENTS.md), Architecture and Invariants that apply everywhere. +The architecture is in the area rules under `.claude/rules/`, and its invariants in [`AGENTS.md`](AGENTS.md), Invariants that apply everywhere. | Dependency | Source | Purpose | |---|---|---| @@ -113,16 +113,6 @@ The architecture and its invariants: [`AGENTS.md`](AGENTS.md), Architecture and | Yams | SPM | YAML for project folder import | | swift-nio, swift-nio-ssl, zstd | SPM | The Kafka plugin's transport, TLS and zstd codec | -## Pull requests - -Branch naming and the pull request rules are in the root [`CONTRIBUTING.md`](../CONTRIBUTING.md). Before you open one for the Apple apps: - -- Tests added or updated. -- A changelog fragment in `changelog.d/` for a user-visible change (`.claude/rules/changelog.md`). -- User-facing behavior documented in a paired pull request in [TableProApp/docs](https://github.com/TableProApp/docs) that links this one. -- User-facing strings localized. -- SwiftLint clean. - ## Adding a database driver A new engine ships as a registry plugin: a `.tableplugin` bundle under `Plugins/` that implements `DriverPlugin` and `PluginDatabaseDriver` from `TableProPluginKit`, with a target in `project.yml`. The checklist is in `.claude/rules/plugin-system.md`, Structure. Guides: [Plugin Development](https://docs.tablepro.app/development/plugin-development) and [Plugin Registry](https://docs.tablepro.app/development/plugin-registry). @@ -153,5 +143,4 @@ The export builds the app first, so run `scripts/download-libs.sh` and `scripts/ | `error: …/tablepro-hana-helper is missing or has no slice` | Run `scripts/build-hana.sh` with no argument, which builds both architectures | | Signing errors after a fresh clone | `Configs/Base.xcconfig` defaults to the maintainer's team and includes `Secrets.xcconfig` optionally, so a build without it still runs and fails to sign. Create the file with your team | | Package resolution fails | `Cmd+Shift+K` and reopen Xcode; then `rm -rf ~/Library/Developer/Xcode/DerivedData` | -| `Timed out while enabling automation mode`, and UI tests run nothing | Answer the Enable UI Automation prompt with an admin password, or run `sudo automationmodetool enable-automationmode-without-authentication` once | | Test runs left defaults domains (`com.TablePro.tests.*`, `com.TablePro.uitest.*`) or temp folders behind | Quit TablePro, then run `scripts/clean-test-residue.sh`. It only reports until you pass `--apply` | diff --git a/linux/AGENTS.md b/linux/AGENTS.md index 48046e7eca..2020b323e8 100644 --- a/linux/AGENTS.md +++ b/linux/AGENTS.md @@ -6,7 +6,7 @@ This is the guide for work under `linux/`. The root `AGENTS.md` holds the rules ## Principles -1. **Security first, fix the cause, tests define behavior**, as in the root `AGENTS.md`. +1. **Tests define behavior**, as in the root `AGENTS.md`. 2. **Native only.** GTK 4 and libadwaita, used the way the GNOME Human Interface Guidelines describe, with the widget the toolkit provides before anything drawn by hand. No web views, no cross-platform toolkit. 3. **One-directional crates.** The domain crate (`tablepro-core`) never imports the GUI or a driver, and a new database engine is one crate. `ARCHITECTURE.md` has the graph. 4. **Drivers are linked in at compile time.** There is no plugin system and no plugin ABI (`docs/decisions/0001-no-plugin-system.md`). diff --git a/linux/CONTRIBUTING.md b/linux/CONTRIBUTING.md index 1f1b73b389..516cbc4ed4 100644 --- a/linux/CONTRIBUTING.md +++ b/linux/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to TablePro Linux -This file governs the Linux subproject only. [AGENTS.md](AGENTS.md) is the short guide to it, and the repository-level [AGENTS.md](../AGENTS.md) holds the rules every app shares: security first, root-cause fixes, tests define behavior and the writing style. +This file governs the Linux subproject only. [AGENTS.md](AGENTS.md) is the short guide to it, and the repository-level [AGENTS.md](../AGENTS.md) holds the rules both apps share. ## Dev environment @@ -72,8 +72,6 @@ object per line. RUST_LOG=debug meson devenv -C _build ./crates/app/tablepro ``` -The gtk Meson suite sets `GTK_A11Y=test`, and `meson devenv` does not. With `GTK_A11Y=none` GTK records no accessible properties, and the accessibility helpers report that the test backend is missing. - ### Dependencies `data/licenses` holds the license notices of every crate the app ships, written from `Cargo.lock` by cargo-about. The About dialog embeds `third-party.json`. The packages ship the notices as their license files: `%license` in the RPM, `debian/copyright` in the .deb, and `share/licenses` in the Flatpak. After a change to `Cargo.lock`, regenerate them and commit the result; the Supply chain CI job fails until they match: @@ -96,15 +94,13 @@ The script stops and names each crate it read no license file for, since the not | Edition | 2024 | Set per workspace. Do not override per crate. | | MSRV | 1.97 | `rust-version` in `Cargo.toml`: the `rustc-1.97` the PPA builds with, checked by CI. `rust-toolchain.toml` pins 1.98, the Flatpak rust-stable extension's. | -The workspace denies `unwrap_used`, `expect_used`, `panic`, `todo`, `unimplemented`, `dbg_macro`, `print_stdout`, `print_stderr`, `allow_attributes`, `allow_attributes_without_reason`, `ignore_without_reason` and `too_many_lines`, plus `unsafe_op_in_unsafe_fn`. `clippy.toml` lets tests unwrap, expect, panic, print and use `dbg!`, and bans `serde_json::Map` and `Value::as_object`, whose maps reorder a server's JSON. Clippy reads only the `clippy.toml` nearest a crate, so `crates/app/clippy.toml` and `crates/session/clippy.toml` replace the root file for their crate. Both keep its test allowances and its JSON bans, and ban reaching for the ambient tokio runtime: background work goes through `Tasks`. The app's also bans the Relm4 spawns, `MainContext::block_on` and `ApplicationExt::quit`. +The denied lints are `[workspace.lints]` in `Cargo.toml`; the test allowances and the banned types and methods, each with its reason, are in the `clippy.toml` files. Clippy reads only the `clippy.toml` nearest a crate, so `crates/app/clippy.toml` and `crates/session/clippy.toml` replace the root one for their crate. Conventions, beyond what `rustfmt` decides: - **Comments say why, never what.** A short comment belongs where the code would surprise a careful reader: a hidden constraint, an invariant, a platform quirk. Code is otherwise self-documenting through naming. -- **No `unwrap()` or `expect()` in production paths.** Tests are the only callers; the workspace lints deny the rest. -- **No `panic!`, `todo!`, `unimplemented!` in merged code.** Stub a real `Err` variant instead. - **One public type per module file** when the type's surface is non-trivial. Internal helpers stay private. -- **A function holds at most 100 lines of code**, tests included. The workspace denies `clippy::too_many_lines` at its default threshold, so split a long function by what each part does instead of allowing the lint. +- **Split a long function** by what each part does; never silence `clippy::too_many_lines`. - **User-facing strings go through `crate::i18n`.** `gettext` for a plain string, `gettext_f` with named `{placeholders}` when values are interpolated, `ngettext_f` for counts, `pgettext` when a word's sense diff --git a/scripts/check-doc-symbols.sh b/scripts/check-doc-symbols.sh deleted file mode 100755 index 260b4d29f1..0000000000 --- a/scripts/check-doc-symbols.sh +++ /dev/null @@ -1,489 +0,0 @@ -#!/usr/bin/env bash -# -# Check that the agent-facing docs still describe a repository that exists. -# -# Prose does not fail a build when the code moves underneath it, so a renamed type or a deleted -# script stays named in the docs until something asks. This asks. -# -# Scope: CLAUDE.md, AGENTS.md, CONTRIBUTING.md and RELEASING.md at the root and in apple/, -# .github/pull_request_template.md, linux/CLAUDE.md, linux/AGENTS.md, and every .md under -# .claude/rules and .claude/skills. -# Documents named on the command line replace that set. -# -# What is checked. Only mechanical claims, because those are the ones that rot silently: -# paths a backticked repo-relative path must exist -# symbols a backticked CamelCase identifier must exist in this tree (Swift, C headers or Rust), -# the macOS SDK, the installed GObject Introspection files, or the Swift toolchain's -# feature list -# scripts every .sh named must exist, and be executable unless it is a sourced lib/ file -# skills every Skill(name) and hyphenated $name reference must resolve -# counts a stated plugin-bundle count must match the tree -# rows each AGENTS.md Area rules row must list exactly its rule's paths: globs, a trailing -# /**/* or /** dropped, because Codex finds a rule only through that row -# globs each paths: glob must match a file, or the rule never loads -# -# Fenced code blocks are stripped first: a claim in prose is a claim, a name inside an example is -# an example. A placeholder (`SomeType`, `TypeName`) and an absolute or home path are not claims -# about the tree either. Without Xcode the SDK half is skipped, and a framework-prefixed name (NS, -# CK, UI and the like) that the tree itself does not use is counted as skipped rather than stale. -# GTK, libadwaita and GLib names (Gtk, Adw, G and the like) are skipped the same way on a machine -# without their introspection files. -# -# Usage: -# scripts/check-doc-symbols.sh # exit 1 if anything is stale -# scripts/check-doc-symbols.sh --list # also print what passed and what was skipped -# scripts/check-doc-symbols.sh [--list] linux/ARCHITECTURE.md ... # check only these - -set -uo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$REPO_ROOT" || exit 3 -REPO_REAL="$(pwd -P)" - -LIST=0 -if [ "${1:-}" = "--list" ]; then - LIST=1 - shift -fi - -DOCS=() -if [ "$#" -gt 0 ]; then - for doc in "$@"; do - [ -f "$doc" ] || { echo "no such document: $doc" >&2; exit 2; } - DOCS+=("$doc") - done -else - for top in CLAUDE.md AGENTS.md CONTRIBUTING.md RELEASING.md .github/pull_request_template.md \ - apple/CLAUDE.md apple/AGENTS.md apple/CONTRIBUTING.md apple/RELEASING.md \ - linux/CLAUDE.md linux/AGENTS.md; do - [ -f "$top" ] && DOCS+=("$top") - done - while IFS= read -r f; do DOCS+=("$f"); done < <( - find .claude/rules .claude/skills -name '*.md' -type f 2> /dev/null | sort - ) -fi - -BUILTIN_SKILLS="code-review security-review simplify swiftui-pro run init update-config loop schedule" - -# Claude Code tool names. They are backticked CamelCase in these docs and are not Swift types. -HARNESS_TOOLS="Read Write Edit Bash Glob Grep Agent Skill Workflow Task TodoWrite WebSearch WebFetch -AskUserQuestion ExitPlanMode EnterPlanMode SendMessage ListAgents Monitor NotebookEdit LSP -ReportFindings Artifact PushNotification TaskOutput TaskStop" - -# Environment variables that read as CamelCase, so the ALL_CAPS filter does not catch them. -ENV_NAMES="XCTestConfigurationFilePath XCTestSessionIdentifier XCTestBundlePath" - -SDK_FRAMEWORKS="AppKit SwiftUI Foundation Combine CoreData Observation UniformTypeIdentifiers -CloudKit Security CoreText QuartzCore CoreGraphics OSLog" - -PLACEHOLDER_PATTERN='^(Some|My|Your|Foo|Bar|Example)[A-Z][a-z]|^(TypeName|ClassName|SuiteType|TestClassName)$' -FRAMEWORK_PREFIX_PATTERN='^(NS|CK|CF|CG|CA|CT|UI|XC|OS)[A-Z]' -GNOME_PREFIX_PATTERN='^(Gtk|Gdk|Gsk|Adw|GtkSource|Gio|GLib|GObject|Pango|Secret|G)[A-Z]' -# The GObject Introspection files the Linux app's ground truth comes from, when installed. -GIR_FILES="Gtk-4.0 Gdk-4.0 Gsk-4.0 Adw-1 GtkSource-5 Gio-2.0 GObject-2.0 GLib-2.0 Pango-1.0 Secret-1" -TAB="$(printf '\t')" -SCRIPT_PATH_REGEX='(\.claude/hooks|([A-Za-z0-9_.-]+/)*scripts)(/[A-Za-z0-9_-]+)*/[A-Za-z0-9_.-]+\.sh' - -# The docs site is its own repository, cloned beside this one. A path under it names that repo, so -# it is checked against the clone when one is there and skipped when it is not. -DOCS_REPO="../tablepro-docs" - -findings=0 -checked=0 -skipped=0 -sdk_indexed=0 -gir_indexed=0 -WORK="$(mktemp -d)" -trap 'rm -rf "$WORK"' EXIT - -report() { - findings=$((findings + 1)) - checked=$((checked + 1)) - printf '%s: %s\n' "$1" "$2" -} - -pass() { - checked=$((checked + 1)) - [ "$LIST" -eq 1 ] && printf ' ok %-52s %s\n' "$1" "$2" - return 0 -} - -skip() { - skipped=$((skipped + 1)) - [ "$LIST" -eq 1 ] && printf ' skip %-52s %s\n' "$1" "$2" - return 0 -} - -# ------------------------------------------------------------------ symbol index - -# Reads file paths on stdin and prints the identifiers left once comments are stripped, so a name -# that survives only in somebody's `//` note does not count as resolved. String literals stay: a -# name used in a literal is still a name the tree uses. -0777 reads each file as one record, so a -# /* */ match never spans two files, and one perl process serves every file. -uncommented_identifiers() { - tr '\n' '\0' | - xargs -0 perl -0777 -ne 's{/\*.*?\*/}{}gs; s{//[^\n]*}{}g; print "$_\n"' 2> /dev/null | - grep -oE '\b[A-Z][A-Za-z0-9_]{3,}\b' -} - -sdk_frameworks_dir() { - local sdk - command -v xcrun > /dev/null 2>&1 || return 1 - sdk="$(xcrun --sdk macosx --show-sdk-path 2> /dev/null)" || return 1 - [ -d "$sdk/System/Library/Frameworks" ] || return 1 - printf '%s\n' "$sdk/System/Library/Frameworks" -} - -build_symbol_index() { - local frameworks_dir fw iface - { - find apple/TablePro apple/Plugins apple/Packages apple/TableProTests apple/TableProUITests \ - apple/TableProMobile -name '*.swift' -type f 2> /dev/null | uncommented_identifiers - find linux/crates -name '*.rs' -type f 2> /dev/null | uncommented_identifiers - grep -rhoE '\b[A-Za-z][A-Za-z0-9_]{3,}\b' --include='*.h' apple/Plugins 2> /dev/null - grep -hoE '^ [A-Za-z][A-Za-z0-9_+-]*:' apple/project.yml apple/TableProMobile/project.yml 2> /dev/null | - tr -d ' :' - printf '%s\n' $HARNESS_TOOLS $ENV_NAMES - if command -v xcrun > /dev/null 2>&1; then - xcrun swift-frontend -print-supported-features 2> /dev/null | - sed -n 's/.*"name": "\([A-Za-z0-9_]*\)".*/\1/p' - fi - } > "$WORK/symbols.raw" - - if frameworks_dir="$(sdk_frameworks_dir)"; then - sdk_indexed=1 - : > "$WORK/sdk-headers" - for fw in $SDK_FRAMEWORKS; do - iface="$frameworks_dir/$fw.framework/Modules/$fw.swiftmodule/arm64e-apple-macos.swiftinterface" - [ -f "$iface" ] && grep -hoE '\b[A-Z][A-Za-z0-9_]{3,}\b' "$iface" >> "$WORK/symbols.raw" - # Headers is a symlink into Versions/Current, and find follows a symlinked starting - # point only when the path ends in a slash. - find "$frameworks_dir/$fw.framework/Headers/" -name '*.h' -type f 2> /dev/null >> "$WORK/sdk-headers" - done - # XCTest ships in the platform, not the SDK. - find "${frameworks_dir%/SDKs/*}/Library/Frameworks/XCTest.framework/Headers/" \ - -name '*.h' -type f 2> /dev/null >> "$WORK/sdk-headers" - uncommented_identifiers < "$WORK/sdk-headers" >> "$WORK/symbols.raw" - fi - local gir dir - for gir in $GIR_FILES; do - for dir in /usr/share/gir-1.0 /usr/lib/*/gir-1.0; do - [ -f "$dir/$gir.gir" ] || continue - gir_indexed=1 - grep -hoE 'c:type="[A-Za-z_][A-Za-z0-9_]*"' "$dir/$gir.gir" | sed 's/^c:type="//; s/"$//' >> "$WORK/symbols.raw" - done - done - LC_ALL=C sort -u "$WORK/symbols.raw" > "$WORK/symbols" -} - -# ------------------------------------------------------------------ prose extraction - -# Strip fenced code blocks, then emit "line:content" for what is left. -prose() { - awk '/^[[:space:]]*```/ { fence = !fence; next } { print NR ":" (fence ? "" : $0) }' "$1" -} - -# Prints "line:match" for every match of the regex in the prose, with the first and last -# characters (the backticks) trimmed when trim is 1. The regex goes through the environment -# because awk -v rewrites the backslash escapes it carries. -matches() { - MATCH_PATTERN="$1" awk -v trim="$2" '{ - pattern = ENVIRON["MATCH_PATTERN"] - line = $0; sub(/:.*/, "", line); body = substr($0, length(line) + 2) - while (match(body, pattern)) { - print line ":" substr(body, RSTART + trim, RLENGTH - 2 * trim) - body = substr(body, RSTART + RLENGTH) - } - }' "$WORK/prose" -} - -# A path written relative to the repo, the doc, the skill root, or the app or UI-test target. -# Prints the path as it exists on disk, a tab, and how it was resolved. -resolve_path() { - local token="$1" dir="$2" candidate - for candidate in "$token" "$dir/$token" "$dir/../$token" "apple/TablePro/$token" "apple/TableProUITests/$token"; do - [ -e "$candidate" ] || continue - case "$candidate" in - "$token") printf '%s\t\n' "$candidate" ;; - "$dir/$token") printf '%s\t (relative to the doc)\n' "$candidate" ;; - "$dir/../$token") printf '%s\t (relative to the skill root)\n' "$candidate" ;; - *) printf '%s\t (target-relative shorthand)\n' "$candidate" ;; - esac - return 0 - done - # A gitignored path is per-developer or downloaded, so a fresh checkout not having it is the - # expected state, and flagging Secrets.xcconfig or apple/Libs/*.a would train everyone to ignore this. - # A pattern ending in a slash, like DerivedData/, matches only a directory, and git cannot tell - # that a path it has never seen is one, so an absent directory is asked about with the slash. - for candidate in "$token" "$dir/$token" "$dir/../$token"; do - if git check-ignore -q "$candidate" 2> /dev/null || git check-ignore -q "$candidate/" 2> /dev/null; then - printf '%s\t (gitignored, optional by design)\n' "$candidate" - return 0 - fi - done - return 1 -} - -# ------------------------------------------------------------------ checks - -check_paths() { - local doc="$1" dir="$2" hit line token resolved - while IFS= read -r hit; do - line="${hit%%:*}"; token="${hit#*:}" - case "$token" in - http* | /* | '~'* | *' '* | *'*'* | *'<'* | *'$'* | *'|'* | *'{'*) continue ;; - esac - case "$token" in - "$DOCS_REPO" | "$DOCS_REPO"/*) - if [ ! -d "$DOCS_REPO" ]; then - skip "$token (docs repo, not cloned)" "$doc:$line" - elif [ -e "${token%/}" ]; then - pass "$token (docs repo)" "$doc:$line" - else - report "$doc:$line" "path does not exist in the docs repo: $token" - fi - continue - ;; - # Another repository cloned beside this one, present on some machines only. - .. | ../*) - skip "$token (sibling checkout)" "$doc:$line" - continue - ;; - esac - # A dotted first segment is a hostname (github.com/...), unless the dot leads (.claude/...). - case "${token%%/*}" in - .*) ;; - *.*) continue ;; - esac - # Prose reads like a path when it is a pair of lowercase words: if/else, and/or, read/write. - case "$token" in - [a-z]*/[a-z]*) - case "$token" in - *.* | */*/*) ;; - *) continue ;; - esac - ;; - esac - token="${token%/}" - [ -n "$token" ] || continue - # A script is check_scripts' claim, so it is reported once, there. - [[ "$token" =~ ^$SCRIPT_PATH_REGEX$ ]] && continue - if resolved="$(resolve_path "$token" "$dir")"; then - pass "$token${resolved#*"$TAB"}" "$doc:$line" - else - report "$doc:$line" "path does not exist: $token" - fi - done < <(matches '`[A-Za-z0-9_./+-]+/[A-Za-z0-9_./+-]*`' 1) -} - -check_symbols() { - local doc="$1" hit line token - matches '`[A-Z][A-Za-z0-9_]{3,}`' 1 | sort -u -t: -k2 > "$WORK/symbol-hits" - cut -d: -f2 "$WORK/symbol-hits" | LC_ALL=C sort -u | LC_ALL=C comm -12 - "$WORK/symbols" > "$WORK/resolved" - while IFS= read -r hit; do - line="${hit%%:*}"; token="${hit#*:}" - # ALL_CAPS is an environment variable, a build setting, or a verdict word, never a Swift - # type. Use the POSIX class: outside the C locale [a-z] collates to include uppercase. - case "$token" in - *[[:lower:]]*) ;; - *) continue ;; - esac - if grep -qxF "$token" "$WORK/resolved"; then - pass "$token" "$doc:$line" - elif [ -e "$token" ]; then - # A folder, as an Area rules row writes `apple/Libs` for apple/Libs/**/*. - pass "$token (top-level path)" "$doc:$line" - elif printf '%s\n' "$token" | grep -qE "$PLACEHOLDER_PATTERN"; then - skip "$token (placeholder)" "$doc:$line" - elif [ "$sdk_indexed" -eq 0 ] && printf '%s\n' "$token" | grep -qE "$FRAMEWORK_PREFIX_PATTERN"; then - skip "$token (SDK name, no SDK to check against)" "$doc:$line" - elif [ "$gir_indexed" -eq 0 ] && printf '%s\n' "$token" | grep -qE "$GNOME_PREFIX_PATTERN"; then - skip "$token (GNOME name, no introspection files to check against)" "$doc:$line" - else - report "$doc:$line" "symbol is in no Swift or Rust source, SDK header, GIR file or toolchain feature list: $token" - fi - done < "$WORK/symbol-hits" -} - -check_scripts() { - local doc="$1" dir="$2" hit line token resolved path - while IFS= read -r hit; do - line="${hit%%:*}"; token="${hit#*:}" - resolved="$(resolve_path "$token" "$dir")" || resolved="" - path="${resolved%%"$TAB"*}" - if [ ! -f "$path" ]; then - report "$doc:$line" "script does not exist: $token" - elif [ -x "$path" ] || [[ "$token" == */lib/* ]]; then - pass "$token${resolved#*"$TAB"}" "$doc:$line" - else - report "$doc:$line" "script exists but is not executable: $token" - fi - done < <(matches "$SCRIPT_PATH_REGEX" 0 | sort -u -t: -k2) -} - -check_skills() { - local doc="$1" hit line token - while IFS= read -r hit; do - line="${hit%%:*}"; token="${hit#*:}" - token="${token#Skill(}"; token="${token%)}"; token="${token#\$}" - if [ -d ".claude/skills/$token" ] || [ -d ".agents/skills/$token" ]; then - pass "skill $token" "$doc:$line" - elif printf '%s\n' $BUILTIN_SKILLS | grep -qx "$token"; then - pass "built-in skill $token" "$doc:$line" - else - report "$doc:$line" "skill does not resolve: $token" - fi - done < <(matches 'Skill\([a-z-]+\)|\$[a-z]+-[a-z-]+' 0 | sort -u -t: -k2) -} - -check_doc() { - local doc="$1" dir - dir="$(dirname "$doc")" - prose "$doc" > "$WORK/prose" - check_paths "$doc" "$dir" - check_symbols "$doc" - check_scripts "$doc" "$dir" - check_skills "$doc" -} - -check_counts() { - local total doc line stated - total="$(find apple/Plugins -mindepth 1 -maxdepth 1 -name '*Plugin' | wc -l | tr -d ' ')" - for doc in "${DOCS[@]}"; do - while IFS=: read -r line stated; do - if [ "$stated" = "$total" ]; then - pass "$stated plugin bundles" "$doc:$line" - else - report "$doc:$line" "states $stated plugin bundles, the tree has $total" - fi - done < <(grep -noE '(The|all) [0-9]+ plugin' "$doc" 2> /dev/null | - sed -E 's/^([0-9]+):[^0-9]*([0-9]+) plugin$/\1:\2/') - done -} - -# "" for each entry of a rule's paths: list, quotes stripped. -rule_globs() { - awk 'NR == 1 { if ($0 !~ /^---[[:space:]]*$/) exit; next } - /^---[[:space:]]*$/ { exit } - /^paths:/ { inside = 1; next } - /^[^[:space:]-]/ { inside = 0 } - inside && /^[[:space:]]*-[[:space:]]/ { - glob = $0 - sub(/^[[:space:]]*-[[:space:]]*/, "", glob) - sub(/[[:space:]]+$/, "", glob) - gsub(/^["\047]|["\047]$/, "", glob) - print NR "\t" glob - }' "$1" -} - -# "" for every backticked token in the other cells of each row -# of AGENTS.md's Area rules table, plus one "" line per row. -agents_rule_rows() { - awk '/^## / { inside = ($0 ~ /^## Area rules/); next } - inside && /^\|/ { - body = substr($0, 2) - cut = index(body, "|") - if (!cut) next - first = substr(body, 1, cut - 1) - rest = substr(body, cut + 1) - name = ""; names = 0 - while (match(first, /`[^`]+`/)) { - name = substr(first, RSTART + 1, RLENGTH - 2); names++ - first = substr(first, RSTART + RLENGTH) - } - if (!names) next - if (names > 1) name = "*" names - print NR "\t" name "\t" - while (match(rest, /`[^`]+`/)) { - print NR "\t" name "\t" substr(rest, RSTART + 1, RLENGTH - 2) - rest = substr(rest, RSTART + RLENGTH) - } - }' AGENTS.md -} - -check_rule_rows() { - local rule name rows row_line extra missing - [ -f AGENTS.md ] || return 0 - agents_rule_rows > "$WORK/rows" - while IFS="$TAB" read -r row_line rows; do - report "AGENTS.md:$row_line" "an Area rules row names $rows rules; give each rule its own row" - done < <(awk -F'\t' '$3 == "" && $2 ~ /^\*/ { print $1 "\t" substr($2, 2) }' "$WORK/rows") - for rule in .claude/rules/*.md; do - [ -f "$rule" ] || continue - name="${rule##*/}" - rows="$(awk -F'\t' -v r="$name" '$2 == r && $3 == "" { print $1 }' "$WORK/rows")" - if [ -z "$rows" ]; then - report "AGENTS.md" "no Area rules row for $rule, so Codex never reads it" - continue - fi - row_line="$(printf '%s\n' "$rows" | head -1)" - if [ "$rows" != "$row_line" ]; then - report "AGENTS.md:$row_line" "more than one Area rules row for $rule" - continue - fi - rule_globs "$rule" | cut -f2 | sed -E 's#/\*\*(/\*)?$##' | LC_ALL=C sort -u > "$WORK/want" - awk -F'\t' -v r="$name" '$2 == r && $3 != "" { print $3 }' "$WORK/rows" | LC_ALL=C sort -u > "$WORK/have" - missing="$(LC_ALL=C comm -23 "$WORK/want" "$WORK/have" | tr '\n' ' ')" - extra="$(LC_ALL=C comm -13 "$WORK/want" "$WORK/have" | tr '\n' ' ')" - if [ -z "$missing$extra" ]; then - pass "row $name equals its paths:" "AGENTS.md:$row_line" - continue - fi - [ -n "$missing" ] && report "AGENTS.md:$row_line" "the $name row lacks paths: globs ${missing% }" - [ -n "$extra" ] && report "AGENTS.md:$row_line" "the $name row lists what its paths: does not: ${extra% }" - done - while IFS="$TAB" read -r row_line name; do - [ -f ".claude/rules/$name" ] && continue - report "AGENTS.md:$row_line" "an Area rules row names $name, which is not a file in .claude/rules" - done < <(awk -F'\t' '$3 == "" && $2 !~ /^\*/ { print $1 "\t" $2 }' "$WORK/rows") -} - -# An untracked file counts, so a rule can name a file the same uncommitted change adds. -glob_matches_a_file() { - local glob="$1" dir target - [ -n "$(git ls-files -- ":(glob)$glob" 2> /dev/null | head -1)" ] && return 0 - [ -n "$(git ls-files --others --exclude-standard -- ":(glob)$glob" 2> /dev/null | head -1)" ] && return 0 - # git lists a symlinked directory as one entry, so match what is under its target instead. - dir="${glob%%[*?[]*}" - dir="${dir%/*}" - [ -L "$dir" ] && [ -d "$dir" ] || return 1 - target="$(cd "$dir" && pwd -P)" || return 1 - case "$target" in - "$REPO_REAL"/*) glob_matches_a_file "${target#"$REPO_REAL"/}${glob#"$dir"}" ;; - *) return 1 ;; - esac -} - -check_rule_globs() { - local rule line glob - for rule in .claude/rules/*.md; do - [ -f "$rule" ] || continue - while IFS="$TAB" read -r line glob; do - if glob_matches_a_file "$glob"; then - pass "$glob (paths: glob)" "$rule:$line" - else - report "$rule:$line" "paths: glob matches no file, so the rule never loads: $glob" - fi - done < <(rule_globs "$rule") - done -} - -# ------------------------------------------------------------------ run - -build_symbol_index -echo "checking ${#DOCS[@]} documents against the tree" -[ "$sdk_indexed" -eq 1 ] || echo "no macOS SDK found: SDK names the tree does not use are skipped" -for doc in "${DOCS[@]}"; do check_doc "$doc"; done -check_counts -check_rule_rows -check_rule_globs - -echo -if [ "$findings" -eq 0 ]; then - echo "clean: $checked references check out, $skipped skipped" - exit 0 -fi -echo "$findings stale reference(s) out of $checked checked, $skipped skipped" -echo "Each is a claim these docs make that the tree does not support. Fix the doc, or fix the" -echo "code if the doc describes the intent and the code is what drifted." -exit 1 diff --git a/scripts/ci/test_detect_changed_paths.py b/scripts/ci/test_detect_changed_paths.py index 3cb1d22d2e..62b443420e 100644 --- a/scripts/ci/test_detect_changed_paths.py +++ b/scripts/ci/test_detect_changed_paths.py @@ -89,7 +89,7 @@ def test_linux_skips_the_apple_apps(self): self.assertEqual(self.verdict_after("linux", path), "false") def test_linux_skips_the_shared_doc_scripts(self): - for path in ["scripts/check-doc-symbols.sh", "scripts/check-banned-words.sh", "scripts/banned-words.txt"]: + for path in ["scripts/check-banned-words.sh", "scripts/banned-words.txt"]: with self.subTest(path=path): self.assertEqual(self.verdict_after("linux", path), "false")