Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .claude/skills/fix-issue/references/orchestration.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ ${DIGEST_RULES}

() => agent(`
You are the Platform Researcher on a TablePro fix investigation. TablePro is a native macOS
app (SwiftUI + AppKit, macOS 14+) built with the Xcode at /Applications/Xcode-beta.app.
app (SwiftUI + AppKit, macOS 13+) built with the Xcode that `xcode-select -p` names.

Problem statement:
${PROBLEM}
Expand All @@ -165,11 +165,11 @@ Establish what the correct behaviour and the right API are, from the authoritati
If this is a UI or interaction problem, that source is Apple:
1. The relevant Human Interface Guidelines section, quoted and linked.
2. The right AppKit/SwiftUI API, named exactly, with its documented behaviour, its
availability against our macOS 14 target, and its gotchas. Prefer the modern API; if the
availability against our macOS 13 target, and its gotchas. Prefer the modern API; if the
only option is deprecated, say so and name the replacement.
3. Any standard system control that already does this, so we do not reinvent it.
4. Confirm every symbol against the local SDK interface, which is exact for our toolchain:
/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/<Framework>.framework/Modules/<Framework>.swiftmodule/arm64e-apple-macos.swiftinterface
$(xcrun --sdk macosx --show-sdk-path)/System/Library/Frameworks/<Framework>.framework/Modules/<Framework>.swiftmodule/arm64e-apple-macos.swiftinterface

If this is a database driver or dependency problem, that source is the vendored header and
the shipped binary, not the web docs:
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/fix-issue/references/research-sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ There are no MCP servers configured in this repo. Everything below is a built-in
### The local SDK interface files

```
/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/<Framework>.framework/Modules/<Framework>.swiftmodule/arm64e-apple-macos.swiftinterface
$(xcrun --sdk macosx --show-sdk-path)/System/Library/Frameworks/<Framework>.framework/Modules/<Framework>.swiftmodule/arm64e-apple-macos.swiftinterface
```

`AppKit`, `SwiftUI`, `Foundation`, and the rest are all there. This is the ground truth for "does this API exist and what is its signature", because it is the interface the compiler will read. Web docs describe intent; the interface file settles facts. Use both: the interface for the signature, the docs for the behaviour.
Expand All @@ -28,7 +28,7 @@ There are no MCP servers configured in this repo. Everything below is a built-in
- **AppKit**: `https://developer.apple.com/documentation/appkit`. Native windows, sheets, `NSToolbar`, `NSTableView` and `NSOutlineView`, `NSWindow` tabbing, the responder chain, menus, `NSViewController`.
- **SwiftUI**: `https://developer.apple.com/documentation/swiftui`. TablePro is SwiftUI-first with AppKit where SwiftUI falls short. Check whether a native SwiftUI modifier already does the job before dropping to AppKit, and check the reverse too: several TablePro views are AppKit precisely because the SwiftUI equivalent misbehaves, and `CLAUDE.md` records why.
- **Deprecations matter.** Name the modern API. If the only documented option is deprecated, say so and note the replacement.
- **Availability matters.** TablePro targets macOS 14. An API introduced in 15 or 26 needs an `if #available` branch and a fallback, and the blueprint has to say what the fallback is.
- **Availability matters.** TablePro targets macOS 13 (`deploymentTarget` in `project.yml`). An API introduced in 14, 15 or 26 needs an `if #available` branch and a fallback, and the blueprint has to say what the fallback is.

## Competitor apps

Expand Down
8 changes: 4 additions & 4 deletions .claude/skills/fix-issue/references/verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ Everything below is the underlying detail: read it when a verdict needs interpre

## Environment setup

`xcode-select` points at `/Library/Developer/CommandLineTools`, which has no `xcodebuild` and no `sourcekitd`. Both `xcodebuild` and `swiftlint` fail without the export below, which reads as "local builds are broken" and leads to shipping unverified code. They are not broken.
`xcodebuild` and `swiftlint` need a full Xcode. When `xcode-select -p` names `/Library/Developer/CommandLineTools` instead, which has no `xcodebuild` and no `sourcekitd`, both fail, which reads as "local builds are broken" and leads to shipping unverified code. They are not broken.

`verify.sh` resolves `DEVELOPER_DIR` itself: the Xcode `xcode-select` names when it is a full Xcode, else `/Applications/Xcode.app`, else `/Applications/Xcode-beta.app`. For a command the wrapper does not cover, export the same, once per shell command chain or as a prefix:

```bash
export DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer
export DEVELOPER_DIR="$(xcode-select -p)"
```

`/Applications/Xcode-beta.app` is the only Xcode installed. Export it once per shell command chain, or prefix each invocation.

## Regenerate before building

`TablePro.xcodeproj` is generated by XcodeGen from `project.yml` and is gitignored. XcodeGen globs sources at generation time, so a new `.swift` file is **not compiled** until you regenerate:
Expand Down
29 changes: 17 additions & 12 deletions .claude/skills/fix-issue/scripts/verify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,18 @@ report_errors() {
# ---------------------------------------------------------------------------- environment

setup_toolchain() {
if [ ! -x "${DEVELOPER_DIR:-/nonexistent}/usr/bin/xcodebuild" ]; then
if [ -d /Applications/Xcode-beta.app/Contents/Developer ]; then
export DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer
elif [ -d /Applications/Xcode.app/Contents/Developer ]; then
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
[ -x "${DEVELOPER_DIR:-/nonexistent}/usr/bin/xcodebuild" ] && return 0
# The Xcode that xcode-select names wins when it is a full Xcode. Command Line Tools has no
# xcodebuild and no sourcekitd, so fall back to whichever Xcode is installed.
local candidate
for candidate in "$(xcode-select -p 2> /dev/null)" \
/Applications/Xcode.app/Contents/Developer \
/Applications/Xcode-beta.app/Contents/Developer; do
if [ -x "$candidate/usr/bin/xcodebuild" ]; then
export DEVELOPER_DIR="$candidate"
return 0
fi
fi
done
}

wait_for_free_toolchain() {
Expand Down Expand Up @@ -512,13 +517,13 @@ case "$STEP" in
;;

docs)
# The two checks that actually read docs/. Neither runs anywhere else in this script, and
# CI runs them in the "Validate docs" job, so a local run is the only way to see a failure
# before the push.
# The three checks that actually read docs/. None runs anywhere else in this script, and
# CI runs all three in the "Validate docs" job, so a local run is the only way to see a
# failure before the push.
log="$(new_log docs)"
: > "$log"
code=0
for check in "check-writing-style.sh" "check-docs-against-source.py"; do
for check in "check-writing-style.sh" "check-docs-against-source.py" "check-links.py"; do
script="$REPO_ROOT/docs/scripts/$check"
if [ ! -f "$script" ]; then
note "missing: docs/scripts/$check"
Expand All @@ -533,12 +538,12 @@ case "$STEP" in
if [ "$STATUS" != "INCONCLUSIVE" ]; then
if [ $code -eq 0 ]; then
STATUS=PASS
note "docs/: house style and source claims both agree"
note "docs/: house style, source claims and every link agree"
else
STATUS=FAIL
# The scripts print one line per check, most of them "ok". Show the failing check
# and the file:line under it, not the twenty passes above it.
note "$(grep -A 2 -E '^FAIL' "$log" 2> /dev/null | sed 's/^/ /' | head -15)"
note "$(grep -A 2 -E '^ *FAIL' "$log" 2> /dev/null | sed 's/^/ /' | head -15)"
note "$(grep -E 'contradict|house style' "$log" 2> /dev/null | sed 's/^/ /' | head -3)"
fi
fi
Expand Down
36 changes: 31 additions & 5 deletions docs/scripts/check-links.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import re
import sys
from pathlib import Path
from urllib.parse import unquote

DOCS = Path(__file__).resolve().parent.parent
LINK = re.compile(r"\]\((/[^)\s]*?)(?:\s+\"[^\"]*\")?\)")
Expand All @@ -28,6 +29,34 @@ def nav_pages(node, out, collecting=False):
out.add(node)


HEADING = re.compile(r"^#{2,4} +(.+?)\s*$", re.M)
DROPPED = re.compile(r"[()\[\]{}:,;?!`~*+=<>|\\^$%#]")
SEPARATORS = re.compile(r"[\s.]+")
QUOTES = str.maketrans({"\"": "'", "\u201c": "'", "\u201d": "'", "\u2018": "'", "\u2019": "'"})


def heading_anchor(heading):
"""The id Mintlify gives a heading, as the published site shows it.

Measured against docs.tablepro.app on 2026-09-19 over every heading with punctuation in it:
`PL/SQL` is `pl/sql`, `SSL/TLS` is `ssl/tls`, `Users & Roles` is `users-&-roles`,
`Oracle Cloud (ADB)` is `oracle-cloud-adb`, `Breaking changes before 1.0` is
`breaking-changes-before-1-0` and `host:1433` is `host1433`. A slash, an ampersand, an at sign,
an underscore and an ellipsis stay; brackets, colons, commas, semicolons, question marks,
tildes and backticks go; a full stop separates words the way a space does.

Mintlify also curls straight quotes, and which way it curls one depends on the text around the
heading rather than on the heading alone, so quotes are compared as one character.
"""
text = DROPPED.sub("", heading.lower())
text = SEPARATORS.sub("-", text)
return re.sub(r"-{2,}", "-", text).strip("-").translate(QUOTES)


def link_anchor(anchor):
return unquote(anchor).lower().translate(QUOTES)


IMPORT = re.compile(r'^import\s+(\w+)\s+from\s+"(/snippets/[^"]+)"', re.M)


Expand Down Expand Up @@ -99,10 +128,7 @@ def main() -> int:
continue
slug = "/" + str(path.relative_to(DOCS).with_suffix(""))
body = FENCE.sub("", path.read_text())
anchors[slug] = {
"#" + re.sub(r"[^a-z0-9]+", "-", h.lower()).strip("-")
for h in re.findall(r"^#{2,4} +(.+?)\s*$", body, re.M)
}
anchors[slug] = {heading_anchor(h) for h in HEADING.findall(body)}

for path in sorted(DOCS.rglob("*.mdx")):
if "node_modules" in path.parts:
Expand All @@ -124,7 +150,7 @@ def main() -> int:
bare = page.lstrip("/")
if bare not in on_disk:
failures.append(f"{rel}:{line_no} links to {target}, which does not resolve")
elif anchor and "#" + anchor not in anchors.get(page, set()):
elif anchor and link_anchor(anchor) not in anchors.get(page, set()):
failures.append(f"{rel}:{line_no} links to {target}, but that heading does not exist")
for asset in SRC.findall(line):
if not (DOCS / asset.lstrip("/")).exists():
Expand Down
Loading