diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7addaed5..89871a97 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,10 +6,6 @@ on: pull_request: push: branches: [main] - # The app repository does not run these checks, so an app change that contradicts a page is - # caught here, against the app's main, within a day. - schedule: - - cron: '17 6 * * *' workflow_dispatch: concurrency: @@ -60,12 +56,6 @@ jobs: working-directory: docs run: npm run a11y - # The three claims that rot fastest, each checked against the source that - # defines it. All three were wrong at once in August 2026. - - name: Check docs against the source - working-directory: docs - run: python3 scripts/check-docs-against-source.py --app ../app - - name: Check writing style working-directory: docs run: bash scripts/check-writing-style.sh ../app diff --git a/.github/workflows/source-check.yml b/.github/workflows/source-check.yml new file mode 100644 index 00000000..5eb7c6ce --- /dev/null +++ b/.github/workflows/source-check.yml @@ -0,0 +1,81 @@ +name: Source check + +# Not on pull requests: it reads the app's main, so an app change turned unrelated docs pull requests red. +on: + schedule: + - cron: '17 6 * * *' + workflow_dispatch: + +# Queued, not canceled: two runs at once could each open an issue. +concurrency: + group: source-check + cancel-in-progress: false + +permissions: {} + +jobs: + source-check: + name: Check docs against the app source + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: docs + persist-credentials: false + + - name: Check out the app + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: TableProApp/TablePro + ref: main + path: app + persist-credentials: false + + - name: Check docs against the source + id: check + working-directory: docs + run: | + echo "app-sha=$(git -C ../app rev-parse HEAD)" >> "$GITHUB_OUTPUT" + set +e + python3 scripts/check-docs-against-source.py --app ../app > "$RUNNER_TEMP/output.txt" 2>&1 + echo "status=$?" >> "$GITHUB_OUTPUT" + cat "$RUNNER_TEMP/output.txt" + + - name: Open, update or close the issue + env: + GH_TOKEN: ${{ github.token }} + STATUS: ${{ steps.check.outputs.status }} + APP_SHA: ${{ steps.check.outputs.app-sha }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + TITLE: Docs contradict the app source + run: | + number=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --limit 100 \ + --json number,title --jq 'map(select(.title == env.TITLE)) | first | .number // empty') + + if [ "$STATUS" = 0 ]; then + if [ -n "$number" ]; then + gh issue close "$number" --repo "$GITHUB_REPOSITORY" \ + --comment "Passes against TableProApp/TablePro@$APP_SHA: $RUN_URL" + fi + exit 0 + fi + + body="$RUNNER_TEMP/body.md" + { + echo "\`scripts/check-docs-against-source.py\` fails against TableProApp/TablePro@$APP_SHA: $RUN_URL" + echo + echo '```text' + head -n 300 "$RUNNER_TEMP/output.txt" + echo '```' + echo + echo 'Fix the page, or the check when the app moved a file it reads. The next passing run closes this issue.' + } > "$body" + + if [ -n "$number" ]; then + gh issue edit "$number" --repo "$GITHUB_REPOSITORY" --body-file "$body" + else + gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body-file "$body" + fi diff --git a/AGENTS.md b/AGENTS.md index b07aa861..1cc8c7d0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ Claude Code loads this file through `CLAUDE.md`; Codex loads it directly. - `snippets/`: a fact two pages share lives here once. See `STYLE.md` 9. - `images/`: screenshots, each a light and dark pair. - `consent.js`: Mintlify loads every `.js` file in the repository on every page. Add no other. -- `scripts/`: the three checks CI runs, `make-placeholder.py` for a shot not taken yet, `proper-nouns.txt` for the heading-case check. +- `scripts/`: the three checks CI runs, `make-placeholder.py` for a shot not taken yet. - `STYLE.md`: the spec for every page. ## Commands @@ -25,7 +25,7 @@ bash scripts/check-writing-style.sh ../TablePro # house style; ba python3 scripts/check-links.py --app ../TablePro # links, navigation, and the app's links into this site ``` -Run the three scripts against an app checkout at the `main` the change describes. CI checks out the app's `main`. +Run the three scripts against an app checkout at the `main` the change describes. CI checks out the app's `main`: pull requests run all but `check-docs-against-source.py`, which runs nightly. ## Writing a page @@ -38,8 +38,8 @@ Run the three scripts against an app checkout at the `main` the change describes ## Working with the app repository -- **A change that documents an app change** opens as a draft pull request whose description names the app pull request (`TableProApp/TablePro#1234`). CI checks against the app's `main`, so it goes green once the app pull request merges; mark it ready then. -- **The daily run** catches an app change that broke a claim here. Fix the page, not the check. +- **A change that documents an app change** opens as a draft pull request whose description names the app pull request (`TableProApp/TablePro#1234`). Mark it ready once the app pull request merges. +- **The nightly source check** runs `check-docs-against-source.py` against the app's `main` and opens or updates one issue, "Docs contradict the app source", while it fails. Fix the page, or the check when the app moved a file it reads. The next passing run closes the issue. - **Releases.** For each stable release, the app's `release` skill opens a `docs(changelog): vX.Y.Z` pull request whose `` block in `changelog.mdx` is written by the app's `apple/scripts/release/notes.py docs`, and merges it once the release is published. Every version in the app's `apple/CHANGELOG.md` must be on that page. ## Writing style diff --git a/README.md b/README.md index de3a5801..0eb39433 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,18 @@ Preview at `http://localhost:3000`. ## Checks -CI runs these on every pull request, on every push to `main`, and daily against the app's `main`. The scripts read the app source, so they take a checkout of it: +CI runs these on every pull request and every push to `main`. The scripts read the app source, so they take a checkout of it: ```bash npm run validate npm run broken-links npm run a11y -python3 scripts/check-docs-against-source.py --app ../TablePro bash scripts/check-writing-style.sh ../TablePro python3 scripts/check-links.py --app ../TablePro ``` +`python3 scripts/check-docs-against-source.py --app ../TablePro` runs nightly against the app's `main` instead, and reports a failure as an issue here. + ## Contributing Read [STYLE.md](STYLE.md) before writing a page. A page that documents an app change is checked against the app's `main`, so open it alongside the app pull request and merge it after that one merges. diff --git a/STYLE.md b/STYLE.md index aef1b3cd..b010da33 100644 --- a/STYLE.md +++ b/STYLE.md @@ -162,10 +162,9 @@ normal reader should pick and who should pick differently. Exhaustiveness is not ### Headings -Sentence case. Capitals only for acronyms, names with an interior capital (`MySQL`, `PGlite`), and -headings that are literally a control or menu item in the app. `scripts/proper-nouns.txt` holds -the product names whose second word is an ordinary English word: Google **Cloud** SQL, DynamoDB -**Local**. +Sentence case. Capitals only for acronyms, names with an interior capital (`MySQL`, `PGlite`), +product names (Google Cloud SQL, DynamoDB Local), and headings that are literally a control or +menu item in the app. Stop at H3; Mintlify's table of contents ignores H4. Never write an H1, the frontmatter `title` is the H1. Never `## Features`: it fitted 17 pages, so it is not a heading. @@ -291,7 +290,7 @@ it.** If linking reads badly, the sentence is wrong, not the rule. Every count contradiction this corpus has shipped came from one fact typed on two pages: the driver count, the database count, the AI provider count, the Settings tab count, the PluginKit ABI version. -Three claims rot faster than anything else and are checked in CI against the source that defines +Three claims rot faster than anything else and are checked nightly against the source that defines them: menu paths, keyboard shortcuts, and the PluginKit ABI. All three were wrong at once in August 2026. @@ -332,9 +331,9 @@ Every `` holds either zero images or a light and dark pair at identical d ## 11. Before you open the pull request ```bash -cd docs -bash scripts/check-writing-style.sh -python3 scripts/check-docs-against-source.py +bash scripts/check-writing-style.sh ../TablePro +python3 scripts/check-docs-against-source.py --app ../TablePro +python3 scripts/check-links.py --app ../TablePro mint validate mint broken-links --check-anchors --check-redirects mint a11y diff --git a/scripts/check-docs-against-source.py b/scripts/check-docs-against-source.py index e819661c..d1ae091d 100755 --- a/scripts/check-docs-against-source.py +++ b/scripts/check-docs-against-source.py @@ -254,48 +254,6 @@ def check_changelog_anchors(root: Path, docs: Path) -> list[str]: return [f"changelog.mdx: label {label!r} is used more than once" for label in sorted(repeated)] -def check_heading_case(root: Path, docs: Path) -> list[str]: - """Headings are sentence case, except where the heading is a name. - - An acronym, a name with an interior capital, and a heading that is literally a - control or menu item in the app all keep their capitals. The last exemption reads - the app's own strings, so a renamed control never leaves a stale allowlist behind. - """ - ui = set() - sources = [root / "TablePro", root / "Plugins", *sorted((root / "Packages").glob("*/Sources"))] - for swift in (path for source in sources for path in source.rglob("*.swift")): - ui |= set(re.findall(r'String\(\s*localized:\s*"((?:[^"\\]|\\.)*)"', swift.read_text())) - ui |= {re.sub(r"%(?:\d+\$)?(?:@|l{0,2}d)", "…", s) for s in ui} - - acronym = re.compile(r"^[A-Z0-9]{2,}$") - inner = re.compile(r"^[A-Za-z][a-z0-9.]*[A-Z]") - known = {w for s in ui for w in s.split() if acronym.match(w) or inner.match(w)} - nouns = Path(__file__).with_name("proper-nouns.txt").read_text().splitlines() - known |= {n.strip() for n in nouns if n.strip() and not n.startswith("#")} - - failures = [] - for page in doc_pages(docs): - for line_no, line in enumerate(page.read_text().splitlines(), start=1): - match = re.match(r"^#{2,4} (.+)$", line) - if not match: - continue - heading = re.sub(r"^\d+\.\s*", "", match.group(1).strip()) - if heading in ui or match.group(1).strip() in ui: - continue - for word in heading.split()[1:]: - core = word.strip("`*_()[],.:;?!\"'") - if not core or not core[0].isupper(): - continue - if acronym.match(core) or inner.match(core) or core in known: - continue - failures.append( - f"{page.relative_to(docs)}:{line_no}: sentence case, so {core!r} " - f"in {heading!r} is capitalised without being a name" - ) - break - return failures - - DOC_NAMES = { "SQL Server": "Microsoft SQL Server", "Oracle": "Oracle Database", @@ -461,7 +419,6 @@ def main() -> int: ("PluginKit version", check_pluginkit_version), ("changelog parity", check_changelog_parity), ("changelog anchors", check_changelog_anchors), - ("heading case", check_heading_case), ("database table", check_database_table), ("engine counts", check_engine_counts), ) diff --git a/scripts/check-writing-style.sh b/scripts/check-writing-style.sh index 9157d2a2..b79b8278 100755 --- a/scripts/check-writing-style.sh +++ b/scripts/check-writing-style.sh @@ -12,9 +12,6 @@ # # changelog.mdx is excluded throughout. It is 116 releases of shipped history and # what 0.41.0's notes called a button cannot be rewritten now. -# -# Heading case is checked by check-docs-against-source.py, which needs the app's own -# UI strings to tell a Title Case slip from a control's real name. set -uo pipefail diff --git a/scripts/proper-nouns.txt b/scripts/proper-nouns.txt deleted file mode 100644 index e749715b..00000000 --- a/scripts/proper-nouns.txt +++ /dev/null @@ -1,42 +0,0 @@ -# Words that keep their capital inside a sentence-case heading. -# -# Acronyms (SQL, URL, IAM), names with an interior capital (MySQL, PGlite), and any -# heading that is literally a control or menu item in the app are recognised -# automatically from the source. This file is for the rest: product names whose -# second word is an ordinary English word, which no rule can tell from prose. -# -# One per line. Comments start with #. -Agent -Atlas -Authentication -Beancount -Cloud -Code -Connector -Database -Drive -Entra -Finder -Firestore -Homebrew -Insights -Instant -Kafka -Kerberos -Keyspaces -Library -Local -Mac -Mac's -Microsoft -Mode -Proxy -Quack -Realtime -Safe -Shortcuts -Studio -Sync -Tunnel -Turso -Vantage