From e983226186547b63a9dfb9bab7b813dc24669703 Mon Sep 17 00:00:00 2001 From: plx Date: Wed, 1 Jul 2026 15:06:09 -0500 Subject: [PATCH 1/8] add vale prose linting --- .github/CI_CD_DOCUMENTATION.md | 8 +- .github/WORKFLOW_ARCHITECTURE.md | 5 +- .github/workflows/build.yml | 5 +- .github/workflows/pr-validation.yml | 4 +- .vale.ini | 9 + .vale/README.md | 17 ++ .vale/styles/Terminology/Apple.yml | 19 ++ CLAUDE.md | 3 +- SPELLCHECK.md | 6 +- VALIDATION.md | 24 +- justfile | 8 +- package-lock.json | 239 +++++++++++++++++- package.json | 8 +- .../objective-c/the-three-file-pattern.md | 3 +- 14 files changed, 331 insertions(+), 27 deletions(-) create mode 100644 .vale.ini create mode 100644 .vale/README.md create mode 100644 .vale/styles/Terminology/Apple.yml diff --git a/.github/CI_CD_DOCUMENTATION.md b/.github/CI_CD_DOCUMENTATION.md index 13c7822..e53cd18 100644 --- a/.github/CI_CD_DOCUMENTATION.md +++ b/.github/CI_CD_DOCUMENTATION.md @@ -13,6 +13,9 @@ This repository implements comprehensive quality gates to ensure code quality an All pull requests to `main` undergo automated validation: - **Linting** - Code style and quality checks via ESLint +- **Unit Tests** - Vitest coverage for TypeScript utilities +- **Spell Checking** - Source and generated HTML spelling checks via CSpell +- **Prose Linting** - Article terminology and style checks via Vale - **Type Checking** - TypeScript validation via `astro check` - **Build Verification** - Ensures the site builds successfully - **Link Validation** - Checks all internal links are valid @@ -32,7 +35,8 @@ Custom Node.js script that: **Usage:** ```bash npm run validate:links # Run link validation -npm run validate:all # Run all validation (lint, build, links) +npm run lint:prose # Run Vale prose linting +npm run validate:all # Run all validation (lint, prose, build, links) ``` ### 3. Deployment Workflow @@ -151,4 +155,4 @@ Changes are automatically deployed to GitHub Pages. - [Branch Protection Setup](./BRANCH_PROTECTION.md) - [PR Validation Workflow](./workflows/pr-validation.yml) - [Deployment Workflow](./workflows/deploy.yml) -- [Link Validation Script](../scripts/validate-links.js) \ No newline at end of file +- [Link Validation Script](../scripts/validate-links.js) diff --git a/.github/WORKFLOW_ARCHITECTURE.md b/.github/WORKFLOW_ARCHITECTURE.md index 9f3eb5c..4d7e5e5 100644 --- a/.github/WORKFLOW_ARCHITECTURE.md +++ b/.github/WORKFLOW_ARCHITECTURE.md @@ -14,7 +14,9 @@ This repository uses a modular GitHub Actions workflow architecture to ensure co - Accepts optional `checkout-ref` input for specific git references - Performs all validation steps: - Linting (ESLint) + - Unit tests - Spell checking (source files) + - Prose linting (Vale) - TypeScript checking and Astro build - Spell checking (generated HTML) - Internal link validation @@ -64,6 +66,7 @@ Multiple safeguards prevent accidental deployment: PRs undergo the exact same validation as deployment, including: - All linting and type checking - Spell checking (both source and generated HTML) +- Prose linting - Full site build - Link validation @@ -120,4 +123,4 @@ if: | ### Permission Model - **PR Validation**: Read-only access (can't modify repository or deploy) - **Deployment**: Write access only when pushing to main branch -- **Manual Workflow**: Deployment only allowed from main branch with explicit flag \ No newline at end of file +- **Manual Workflow**: Deployment only allowed from main branch with explicit flag diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f6d6a7a..95bd51b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,6 +51,9 @@ jobs: - name: Run Spell Check (Source) run: npm run spellcheck + + - name: Run Prose Lint (Vale) + run: npm run lint:prose - name: Build Site run: npm run build @@ -93,4 +96,4 @@ jobs: echo "artifact-uploaded=true" >> $GITHUB_OUTPUT else echo "artifact-uploaded=false" >> $GITHUB_OUTPUT - fi \ No newline at end of file + fi diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index c4da95b..e5d0949 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -42,7 +42,9 @@ jobs: // Build check list with actual results const checks = [ '✓ Linting', + '✓ Unit tests', '✓ Spell check (source)', + '✓ Prose lint (Vale)', '✓ Type checking & Build', '✓ Spell check (HTML)', '✓ Internal link validation', @@ -58,4 +60,4 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, body: `## PR Validation ${icon}\n\n${message}\n\n### Checks Performed:\n${checkList}\n\n*This is a complete dry-run of the deployment process, ensuring your changes will deploy successfully when merged.*` - }); \ No newline at end of file + }); diff --git a/.vale.ini b/.vale.ini new file mode 100644 index 0000000..021af31 --- /dev/null +++ b/.vale.ini @@ -0,0 +1,9 @@ +StylesPath = .vale/styles +MinAlertLevel = warning +IgnoredScopes = code, tt, img + +[formats] +mdx = md + +[src/content/**/*.{md,mdx}] +BasedOnStyles = Terminology diff --git a/.vale/README.md b/.vale/README.md new file mode 100644 index 0000000..ea211e6 --- /dev/null +++ b/.vale/README.md @@ -0,0 +1,17 @@ +# Vale Prose Linting + +Vale checks article prose in `src/content`. + +## Structure + +- `.vale.ini`: selects the content paths and enabled style namespaces. +- `.vale/styles/Terminology/`: consistency checks for product, platform, API, and brand terms. + +Add new rule files under the most specific style namespace that fits the rule. If a new family of checks does not fit `Terminology`, add another directory under `.vale/styles` and include it in `.vale.ini` with `BasedOnStyles`. + +## Usage + +```bash +npm run lint:prose +just lint-prose +``` diff --git a/.vale/styles/Terminology/Apple.yml b/.vale/styles/Terminology/Apple.yml new file mode 100644 index 0000000..85d1016 --- /dev/null +++ b/.vale/styles/Terminology/Apple.yml @@ -0,0 +1,19 @@ +extends: substitution +message: "Use '%s' instead of '%s'." +level: error +ignorecase: true +swap: + '\bios\b': iOS + '\bipad\b': iPad + '\bipad[ -]?os\b': iPadOS + '\biphone\b': iPhone + '\bmac[ -]?os\b': macOS + '\bwatch[ -]?os\b': watchOS + '\btv[ -]?os\b': tvOS + '\bvision[ -]?os\b': visionOS + '\bxcode\b': Xcode + '\bswift[ -]?ui\b': SwiftUI + '\bobjective[ -]?c\b': Objective-C + '\bapp\s+store\b': App Store + '\btest\s?flight\b': TestFlight + '\bapple\s+silicon\b': Apple silicon diff --git a/CLAUDE.md b/CLAUDE.md index bd4ff16..387de5a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,8 @@ Some key commands are: - just spellcheck-html: checks spelling in built HTML output - just lint: runs ESLint on all files - just lint-fix: auto-fixes ESLint issues where possible -- just validate: runs all validation checks (lint + spellcheck + build + links) +- just lint-prose: runs Vale on article content +- just validate: runs all validation checks (lint + spellcheck + prose + build + links) ## Key Technical Decisions diff --git a/SPELLCHECK.md b/SPELLCHECK.md index f6599aa..fd29b8f 100644 --- a/SPELLCHECK.md +++ b/SPELLCHECK.md @@ -1,6 +1,6 @@ # Spell Checking Setup -This project uses CSpell for spell checking both source files and generated HTML output. +This project uses CSpell for spell checking both source files and generated HTML output. Vale is used separately for prose style and terminology consistency. ## Features @@ -68,11 +68,11 @@ We evaluated several spell-checking solutions: - **Hunspell/Aspell**: Traditional spell checkers, but poor at handling technical terms and camelCase - **LanguageTool**: Excellent for grammar but overkill for basic spell checking - **textlint**: Good but requires more configuration for technical content -- **Vale**: Great for style guides but complex setup for simple spell checking +- **Vale**: Great for style guides, and now used for prose style and terminology checks rather than spelling CSpell was chosen because it: - Has built-in understanding of code conventions - Includes extensive technical dictionaries - Integrates easily with npm scripts and CI/CD - Provides fast performance -- Offers smart suggestions for technical terms \ No newline at end of file +- Offers smart suggestions for technical terms diff --git a/VALIDATION.md b/VALIDATION.md index bef90d8..b4d0d21 100644 --- a/VALIDATION.md +++ b/VALIDATION.md @@ -45,19 +45,25 @@ npm run spellcheck:html npm run spellcheck:all ``` -### 3. Build +### 3. Prose Linting +Checks article content for Vale style and terminology rules: +```bash +npm run lint:prose +``` + +### 4. Build Generates the static site: ```bash npm run build ``` -### 4. Link Validation +### 5. Link Validation Checks for broken internal links (requires build first): ```bash npm run validate:links ``` -### 5. All Validations +### 6. All Validations Runs everything in sequence: ```bash npm run validate:all @@ -70,9 +76,10 @@ npm run test:ci The GitHub Actions workflow runs these exact same checks: 1. Linting (`npm run lint`) 2. Source spell check (`npm run spellcheck`) -3. Build (`npm run build`) -4. HTML spell check (`npm run spellcheck:html`) -5. Link validation (`npm run validate:links`) +3. Prose lint (`npm run lint:prose`) +4. Build (`npm run build`) +5. HTML spell check (`npm run spellcheck:html`) +6. Link validation (`npm run validate:links`) ## Troubleshooting @@ -122,7 +129,8 @@ Before pushing changes: 1. ✅ Run `npm run test:ci` locally 2. ✅ Fix any issues that arise 3. ✅ If adding new terms, update `cspell.json` -4. ✅ Commit all changes including config updates +4. ✅ If adding new terminology rules, update `.vale/styles/` +5. ✅ Commit all changes including config updates ## Common Gotchas @@ -141,4 +149,4 @@ To ensure local development matches CI: 4. If CI fails but local passes, check for: - Missing files in git - Different Node.js versions - - Platform-specific issues (Linux CI vs local macOS/Windows) \ No newline at end of file + - Platform-specific issues (Linux CI vs local macOS/Windows) diff --git a/justfile b/justfile index 2e0c8fd..c4db1c3 100644 --- a/justfile +++ b/justfile @@ -108,7 +108,11 @@ lint-fix: lint-markdown: npm run lint:markdown -# Validate: runs all validation checks (lint + spellcheck + build + links) +# Lint-prose: runs Vale on article content +lint-prose: + npm run lint:prose + +# Validate: runs all validation checks (lint + spellcheck + prose + build + links) validate: npm run validate:all @@ -134,4 +138,4 @@ qa-report: # QA-codegen: opens Playwright code generator qa-codegen: - npm run qa:codegen \ No newline at end of file + npm run qa:codegen diff --git a/package-lock.json b/package-lock.json index 4edcae1..c2bbe90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "@cspell/dict-typescript": "^3.2.3", "@playwright/test": "^1.59.1", "@types/node": "^24.12.2", + "@vvago/vale": "^3.15.1", "cspell": "^10.0.0", "markdownlint-cli2": "^0.22.0", "prettier": "^3.8.3", @@ -3881,6 +3882,22 @@ "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==", "license": "MIT" }, + "node_modules/@vvago/vale": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/@vvago/vale/-/vale-3.15.1.tgz", + "integrity": "sha512-2tVObU1Tprq/ItvQNGnWFKYd5rAqlMw5BPuqSTBz0yYI2g8B7xPf/xQXUi7FtmNpwD4VImvuZtvpTh6eQO1AZg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "rimraf": "^6.1.3", + "tar": "^7.5.15", + "unzipper": "^0.12.3" + }, + "bin": { + "vale": "bin/vale" + } + }, "node_modules/accessible-astro-components": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/accessible-astro-components/-/accessible-astro-components-4.1.3.tgz", @@ -4540,6 +4557,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -5051,6 +5075,13 @@ "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5561,6 +5592,16 @@ "node": ">=4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.343", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.343.tgz", @@ -6400,6 +6441,21 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -6484,6 +6540,24 @@ "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", "license": "ISC" }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6545,6 +6619,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/h3": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", @@ -6965,6 +7046,13 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, "node_modules/ini": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", @@ -7186,6 +7274,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -7267,6 +7362,19 @@ "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "license": "MIT" }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonpointer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", @@ -8828,6 +8936,13 @@ "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", "license": "MIT" }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-mock-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", @@ -9038,6 +9153,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/package-manager-detector": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", @@ -9181,6 +9303,33 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -9473,6 +9622,13 @@ "node": ">=6" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -9615,6 +9771,22 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -10025,6 +10197,26 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", @@ -10092,6 +10284,13 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -10311,6 +10510,16 @@ "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==", "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -10600,9 +10809,9 @@ } }, "node_modules/tar": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "version": "7.5.19", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz", + "integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -11021,6 +11230,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unstorage": { "version": "1.17.5", "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", @@ -11154,6 +11373,20 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/unzipper": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.5.tgz", + "integrity": "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "11.3.1", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", diff --git a/package.json b/package.json index dee19d2..c7abfd5 100644 --- a/package.json +++ b/package.json @@ -18,14 +18,15 @@ "lint": "eslint .", "lint:fix": "eslint . --fix", "lint:markdown": "markdownlint-cli2 \"src/content/**/*.{md,mdx}\"", + "lint:prose": "vale --config=.vale.ini src/content", "spellcheck": "cspell \"src/**/*.{md,mdx,ts,tsx,js,jsx,astro}\" --no-progress", "spellcheck:html": "cspell \"dist/**/*.html\" --no-progress", "spellcheck:all": "npm run spellcheck && npm run build && npm run spellcheck:html", "validate:links": "node scripts/validate-links.js", - "validate:all": "npm run lint && npm run spellcheck && npm run build && npm run spellcheck:html && npm run validate:links", + "validate:all": "npm run lint && npm run spellcheck && npm run lint:prose && npm run build && npm run spellcheck:html && npm run validate:links", "test:unit": "vitest run", - "test:ci": "npm run lint && npm run test:unit && npm run spellcheck && npm run build && npm run spellcheck:html && npm run validate:links", - "test:ci:verbose": "echo '🔍 Running CI validation locally...' && npm run lint && echo '✓ Linting passed' && npm run test:unit && echo '✓ Unit tests passed' && npm run spellcheck && echo '✓ Source spell check passed' && npm run build && echo '✓ Build succeeded' && npm run spellcheck:html && echo '✓ HTML spell check passed' && npm run validate:links && echo '✓ Link validation passed' && echo '✅ All CI checks passed!'", + "test:ci": "npm run lint && npm run test:unit && npm run spellcheck && npm run lint:prose && npm run build && npm run spellcheck:html && npm run validate:links", + "test:ci:verbose": "echo '🔍 Running CI validation locally...' && npm run lint && echo '✓ Linting passed' && npm run test:unit && echo '✓ Unit tests passed' && npm run spellcheck && echo '✓ Source spell check passed' && npm run lint:prose && echo '✓ Prose lint passed' && npm run build && echo '✓ Build succeeded' && npm run spellcheck:html && echo '✓ HTML spell check passed' && npm run validate:links && echo '✓ Link validation passed' && echo '✅ All CI checks passed!'", "qa": "playwright test --ignore-snapshots", "qa:headed": "playwright test --headed --ignore-snapshots", "qa:ui": "playwright test --ui", @@ -71,6 +72,7 @@ "@cspell/dict-typescript": "^3.2.3", "@playwright/test": "^1.59.1", "@types/node": "^24.12.2", + "@vvago/vale": "^3.15.1", "cspell": "^10.0.0", "markdownlint-cli2": "^0.22.0", "prettier": "^3.8.3", diff --git a/src/content/briefs/objective-c/the-three-file-pattern.md b/src/content/briefs/objective-c/the-three-file-pattern.md index d6ea1eb..9cfc2a2 100644 --- a/src/content/briefs/objective-c/the-three-file-pattern.md +++ b/src/content/briefs/objective-c/the-three-file-pattern.md @@ -34,7 +34,7 @@ As an example illustrating both, let's say we're writing a class cluster that: - represents an in-memory OpenCV image - is essentially "just a wrapper around an OpenCV `cv::Mat` value" - has subclasses for various concrete types (e.g. monochrome, RGB, BGRA, etc.) -- should have a purely objective-c API (e.g. no C++ "leakage") +- should have a purely Objective-C API (e.g. no C++ "leakage") - will have a large API surface area, broken down into topic-specific categories In that case, our base class might look like this: @@ -180,4 +180,3 @@ The closest matches I could find were the below, neither of which feels like the - [Microsoft's (legacy?) Objective-C style guide](https://microsoft.github.io/objc-guide/Headers/Factoring.html) - [A WWDC talk from 2011](https://nonstrict.eu/wwdcindex/wwdc2011/322/) (strangely hard to locate) - From c934390f9b60d1a7f3df81ddd16c16a275f21a3e Mon Sep 17 00:00:00 2001 From: plx Date: Wed, 1 Jul 2026 21:06:07 -0500 Subject: [PATCH 2/8] Address Vale review feedback --- .github/CI_CD_DOCUMENTATION.md | 6 +- .github/WORKFLOW_ARCHITECTURE.md | 2 + .github/workflows/build.yml | 3 + .github/workflows/pr-validation.yml | 1 + .vale.ini | 3 + .vale/README.md | 17 ++++ .vale/fixtures/apple-terminology-invalid.md | 3 + .vale/fixtures/apple-terminology-valid.md | 5 ++ .vale/styles/Terminology/Apple.yml | 30 +++---- CLAUDE.md | 3 +- SPELLCHECK.md | 2 +- VALIDATION.md | 28 ++++-- justfile | 6 +- package.json | 7 +- scripts/check-vale-fixtures.js | 96 +++++++++++++++++++++ 15 files changed, 180 insertions(+), 32 deletions(-) create mode 100644 .vale/fixtures/apple-terminology-invalid.md create mode 100644 .vale/fixtures/apple-terminology-valid.md create mode 100644 scripts/check-vale-fixtures.js diff --git a/.github/CI_CD_DOCUMENTATION.md b/.github/CI_CD_DOCUMENTATION.md index e53cd18..4cae8f4 100644 --- a/.github/CI_CD_DOCUMENTATION.md +++ b/.github/CI_CD_DOCUMENTATION.md @@ -16,6 +16,7 @@ All pull requests to `main` undergo automated validation: - **Unit Tests** - Vitest coverage for TypeScript utilities - **Spell Checking** - Source and generated HTML spelling checks via CSpell - **Prose Linting** - Article terminology and style checks via Vale +- **Vale Fixture Tests** - Terminology rule fixtures that assert expected pass/fail behavior - **Type Checking** - TypeScript validation via `astro check` - **Build Verification** - Ensures the site builds successfully - **Link Validation** - Checks all internal links are valid @@ -36,7 +37,8 @@ Custom Node.js script that: ```bash npm run validate:links # Run link validation npm run lint:prose # Run Vale prose linting -npm run validate:all # Run all validation (lint, prose, build, links) +npm run test:vale # Run Vale terminology fixture tests +npm run validate:all # Run validation (lint, prose, Vale fixtures, build, links) ``` ### 3. Deployment Workflow @@ -88,7 +90,7 @@ Edit files, add content, modify styles, etc. ### 3. Test Locally ```bash -npm run validate:all # Run all checks locally +npm run test:ci # Run all CI checks locally ``` ### 4. Create Pull Request diff --git a/.github/WORKFLOW_ARCHITECTURE.md b/.github/WORKFLOW_ARCHITECTURE.md index 4d7e5e5..47c23d8 100644 --- a/.github/WORKFLOW_ARCHITECTURE.md +++ b/.github/WORKFLOW_ARCHITECTURE.md @@ -17,6 +17,7 @@ This repository uses a modular GitHub Actions workflow architecture to ensure co - Unit tests - Spell checking (source files) - Prose linting (Vale) + - Vale fixture tests - TypeScript checking and Astro build - Spell checking (generated HTML) - Internal link validation @@ -67,6 +68,7 @@ PRs undergo the exact same validation as deployment, including: - All linting and type checking - Spell checking (both source and generated HTML) - Prose linting +- Vale fixture tests - Full site build - Link validation diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 95bd51b..9ad53ad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,6 +54,9 @@ jobs: - name: Run Prose Lint (Vale) run: npm run lint:prose + + - name: Run Vale Fixture Tests + run: npm run test:vale - name: Build Site run: npm run build diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index e5d0949..783214a 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -45,6 +45,7 @@ jobs: '✓ Unit tests', '✓ Spell check (source)', '✓ Prose lint (Vale)', + '✓ Vale fixture tests', '✓ Type checking & Build', '✓ Spell check (HTML)', '✓ Internal link validation', diff --git a/.vale.ini b/.vale.ini index 021af31..996dc70 100644 --- a/.vale.ini +++ b/.vale.ini @@ -7,3 +7,6 @@ mdx = md [src/content/**/*.{md,mdx}] BasedOnStyles = Terminology + +[.vale/fixtures/*.md] +BasedOnStyles = Terminology diff --git a/.vale/README.md b/.vale/README.md index ea211e6..f951ded 100644 --- a/.vale/README.md +++ b/.vale/README.md @@ -9,9 +9,26 @@ Vale checks article prose in `src/content`. Add new rule files under the most specific style namespace that fits the rule. If a new family of checks does not fit `Terminology`, add another directory under `.vale/styles` and include it in `.vale.ini` with `BasedOnStyles`. +## Rule Notes + +Apple terminology substitutions are case-sensitive and list the incorrect forms they should catch. That keeps correctly cased terms from depending on case-insensitive substitution behavior. + +Historical Apple platform names such as `classic Mac OS` and `Mac OS X` are valid when discussing older systems. If a passage intentionally uses a form that a rule flags, keep the exception local: + +```markdown + +classic mac os spelling retained for quoted or historical context + +``` + +MDX files are treated as Markdown. JSX component attribute values can be linted as prose, so use local Vale disable comments around intentional non-prose values when needed. + +The local `@vvago/vale` package downloads the Vale binary during npm install. The lint command itself uses the checked-in rules and does not run `vale sync`. + ## Usage ```bash npm run lint:prose +npm run test:vale just lint-prose ``` diff --git a/.vale/fixtures/apple-terminology-invalid.md b/.vale/fixtures/apple-terminology-invalid.md new file mode 100644 index 0000000..6f6f2f1 --- /dev/null +++ b/.vale/fixtures/apple-terminology-invalid.md @@ -0,0 +1,3 @@ +# Invalid Apple Terminology + +An ios app built in xcode can mention Swift UI, Objective C, Test Flight, MacOS, and Apple Silicon. diff --git a/.vale/fixtures/apple-terminology-valid.md b/.vale/fixtures/apple-terminology-valid.md new file mode 100644 index 0000000..6f13b39 --- /dev/null +++ b/.vale/fixtures/apple-terminology-valid.md @@ -0,0 +1,5 @@ +# Valid Apple Terminology + +An iOS app built in Xcode can mention SwiftUI, Objective-C, TestFlight, macOS, and Apple silicon. + +Historical names such as classic Mac OS and Mac OS X can be correct in older platform context. diff --git a/.vale/styles/Terminology/Apple.yml b/.vale/styles/Terminology/Apple.yml index 85d1016..5716011 100644 --- a/.vale/styles/Terminology/Apple.yml +++ b/.vale/styles/Terminology/Apple.yml @@ -1,19 +1,19 @@ extends: substitution message: "Use '%s' instead of '%s'." level: error -ignorecase: true +ignorecase: false swap: - '\bios\b': iOS - '\bipad\b': iPad - '\bipad[ -]?os\b': iPadOS - '\biphone\b': iPhone - '\bmac[ -]?os\b': macOS - '\bwatch[ -]?os\b': watchOS - '\btv[ -]?os\b': tvOS - '\bvision[ -]?os\b': visionOS - '\bxcode\b': Xcode - '\bswift[ -]?ui\b': SwiftUI - '\bobjective[ -]?c\b': Objective-C - '\bapp\s+store\b': App Store - '\btest\s?flight\b': TestFlight - '\bapple\s+silicon\b': Apple silicon + '\b(ios|IOS|Ios)\b': iOS + '\b(ipad|IPad|Ipad|IPAD)\b': iPad + '\b(ipad[ -]?os|ipad[ -]?OS|iPad[ -]os|iPad[ -]OS|IPad[ -]?OS|Ipad[ -]?OS|IPADOS)\b': iPadOS + '\b(iphone|IPhone|Iphone|IPHONE)\b': iPhone + '\b(mac[ -]?os|MacOS|Mac-OS|MAC[ -]?OS)\b': macOS + '\b(watch[ -]?os|watch[ -]OS|Watch[ -]?OS|Watch[ -]?os|WATCH[ -]?OS)\b': watchOS + '\b(tv[ -]?os|tv[ -]OS|TV[ -]?OS|Tv[ -]?OS)\b': tvOS + '\b(vision[ -]?os|vision[ -]OS|Vision[ -]?OS|Vision[ -]?os|VISION[ -]?OS)\b': visionOS + '\b(xcode|XCode|XCODE)\b': Xcode + '\b(swift[ -]?ui|swift[ -]?UI|Swift[ -]UI|Swift[ -]?Ui|SWIFT[ -]?UI)\b': SwiftUI + '\b(objective[ -]?c|Objective C|Objective-c|ObjectiveC|OBJECTIVE[ -]?C)\b': Objective-C + '\b(app\s+store|App\s+store|app\s+Store|APP\s+STORE)\b': App Store + '\b(test\s*flight|Test\s+Flight|Testflight|TEST\s*FLIGHT)\b': TestFlight + '\b(apple\s+silicon|Apple\s+Silicon|APPLE\s+SILICON)\b': Apple silicon diff --git a/CLAUDE.md b/CLAUDE.md index 387de5a..1c8688e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,7 +22,8 @@ Some key commands are: - just lint: runs ESLint on all files - just lint-fix: auto-fixes ESLint issues where possible - just lint-prose: runs Vale on article content -- just validate: runs all validation checks (lint + spellcheck + prose + build + links) +- just test-vale: verifies Vale terminology fixture behavior +- just validate: runs all validation checks (lint + spellcheck + prose + Vale fixtures + build + links) ## Key Technical Decisions diff --git a/SPELLCHECK.md b/SPELLCHECK.md index fd29b8f..a645e21 100644 --- a/SPELLCHECK.md +++ b/SPELLCHECK.md @@ -68,7 +68,7 @@ We evaluated several spell-checking solutions: - **Hunspell/Aspell**: Traditional spell checkers, but poor at handling technical terms and camelCase - **LanguageTool**: Excellent for grammar but overkill for basic spell checking - **textlint**: Good but requires more configuration for technical content -- **Vale**: Great for style guides, and now used for prose style and terminology checks rather than spelling +- **Vale**: Strong for style guides; used here for prose style and terminology consistency, not spelling CSpell was chosen because it: - Has built-in understanding of code conventions diff --git a/VALIDATION.md b/VALIDATION.md index b4d0d21..398eeb3 100644 --- a/VALIDATION.md +++ b/VALIDATION.md @@ -51,23 +51,30 @@ Checks article content for Vale style and terminology rules: npm run lint:prose ``` -### 4. Build +### 4. Vale Fixture Tests +Checks that Vale terminology fixtures fail and pass as expected: +```bash +npm run test:vale +``` + +### 5. Build Generates the static site: ```bash npm run build ``` -### 5. Link Validation +### 6. Link Validation Checks for broken internal links (requires build first): ```bash npm run validate:links ``` -### 6. All Validations +### 7. All Validations Runs everything in sequence: ```bash npm run validate:all -# OR equivalently: + +# Full CI parity, including unit tests: npm run test:ci ``` @@ -75,11 +82,13 @@ npm run test:ci The GitHub Actions workflow runs these exact same checks: 1. Linting (`npm run lint`) -2. Source spell check (`npm run spellcheck`) -3. Prose lint (`npm run lint:prose`) -4. Build (`npm run build`) -5. HTML spell check (`npm run spellcheck:html`) -6. Link validation (`npm run validate:links`) +2. Unit tests (`npm run test:unit`) +3. Source spell check (`npm run spellcheck`) +4. Prose lint (`npm run lint:prose`) +5. Vale fixture tests (`npm run test:vale`) +6. Build (`npm run build`) +7. HTML spell check (`npm run spellcheck:html`) +8. Link validation (`npm run validate:links`) ## Troubleshooting @@ -138,6 +147,7 @@ Before pushing changes: 2. **HTML spell check requires build**: Always run `npm run build` first 3. **CI uses exact npm scripts**: Don't rely on different local commands 4. **Case sensitivity**: File paths are case-sensitive in CI (Linux) but may not be locally (macOS/Windows) +5. **Vale fixture failures**: Update `.vale/fixtures/` and `scripts/check-vale-fixtures.js` when changing expected terminology rule behavior ## Maintaining CI/CD Parity diff --git a/justfile b/justfile index c4db1c3..7205d30 100644 --- a/justfile +++ b/justfile @@ -112,7 +112,11 @@ lint-markdown: lint-prose: npm run lint:prose -# Validate: runs all validation checks (lint + spellcheck + prose + build + links) +# Test-vale: verifies Vale terminology fixture behavior +test-vale: + npm run test:vale + +# Validate: runs validation checks (lint + spellcheck + prose + Vale fixtures + build + links) validate: npm run validate:all diff --git a/package.json b/package.json index c7abfd5..0d0278f 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,15 @@ "lint:fix": "eslint . --fix", "lint:markdown": "markdownlint-cli2 \"src/content/**/*.{md,mdx}\"", "lint:prose": "vale --config=.vale.ini src/content", + "test:vale": "node scripts/check-vale-fixtures.js", "spellcheck": "cspell \"src/**/*.{md,mdx,ts,tsx,js,jsx,astro}\" --no-progress", "spellcheck:html": "cspell \"dist/**/*.html\" --no-progress", "spellcheck:all": "npm run spellcheck && npm run build && npm run spellcheck:html", "validate:links": "node scripts/validate-links.js", - "validate:all": "npm run lint && npm run spellcheck && npm run lint:prose && npm run build && npm run spellcheck:html && npm run validate:links", + "validate:all": "npm run lint && npm run spellcheck && npm run lint:prose && npm run test:vale && npm run build && npm run spellcheck:html && npm run validate:links", "test:unit": "vitest run", - "test:ci": "npm run lint && npm run test:unit && npm run spellcheck && npm run lint:prose && npm run build && npm run spellcheck:html && npm run validate:links", - "test:ci:verbose": "echo '🔍 Running CI validation locally...' && npm run lint && echo '✓ Linting passed' && npm run test:unit && echo '✓ Unit tests passed' && npm run spellcheck && echo '✓ Source spell check passed' && npm run lint:prose && echo '✓ Prose lint passed' && npm run build && echo '✓ Build succeeded' && npm run spellcheck:html && echo '✓ HTML spell check passed' && npm run validate:links && echo '✓ Link validation passed' && echo '✅ All CI checks passed!'", + "test:ci": "npm run lint && npm run test:unit && npm run spellcheck && npm run lint:prose && npm run test:vale && npm run build && npm run spellcheck:html && npm run validate:links", + "test:ci:verbose": "echo '🔍 Running CI validation locally...' && npm run lint && echo '✓ Linting passed' && npm run test:unit && echo '✓ Unit tests passed' && npm run spellcheck && echo '✓ Source spell check passed' && npm run lint:prose && echo '✓ Prose lint passed' && npm run test:vale && echo '✓ Vale fixture tests passed' && npm run build && echo '✓ Build succeeded' && npm run spellcheck:html && echo '✓ HTML spell check passed' && npm run validate:links && echo '✓ Link validation passed' && echo '✅ All CI checks passed!'", "qa": "playwright test --ignore-snapshots", "qa:headed": "playwright test --headed --ignore-snapshots", "qa:ui": "playwright test --ui", diff --git a/scripts/check-vale-fixtures.js b/scripts/check-vale-fixtures.js new file mode 100644 index 0000000..3ee0e59 --- /dev/null +++ b/scripts/check-vale-fixtures.js @@ -0,0 +1,96 @@ +import { spawnSync } from "node:child_process"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +const cases = [ + { + file: ".vale/fixtures/apple-terminology-valid.md", + expectedMatches: [], + }, + { + file: ".vale/fixtures/apple-terminology-invalid.md", + expectedMatches: [ + "ios", + "xcode", + "Swift UI", + "Objective C", + "Test Flight", + "MacOS", + "Apple Silicon", + ], + }, +]; + +function runVale(file) { + const result = spawnSync( + "vale", + ["--config=.vale.ini", "--output=JSON", file], + { + cwd: root, + encoding: "utf8", + }, + ); + + if (result.error) { + throw result.error; + } + + const stdout = result.stdout.trim(); + let output; + + try { + output = stdout ? JSON.parse(stdout) : {}; + } catch (error) { + throw new Error( + `Could not parse Vale JSON output for ${file}:\n${stdout}`, + { cause: error }, + ); + } + + return { + status: result.status, + stderr: result.stderr.trim(), + alerts: Object.values(output).flat(), + }; +} + +let failed = false; + +for (const { file, expectedMatches } of cases) { + const { status, stderr, alerts } = runVale(file); + const matches = new Set(alerts.map((alert) => alert.Match)); + + if (expectedMatches.length === 0) { + if (status !== 0 || alerts.length !== 0) { + failed = true; + console.error(`${file} should pass without Vale alerts.`); + console.error(JSON.stringify(alerts, null, 2)); + } + + continue; + } + + if (status === 0 || alerts.length === 0) { + failed = true; + console.error(`${file} should fail with Vale alerts.`); + } + + for (const expectedMatch of expectedMatches) { + if (!matches.has(expectedMatch)) { + failed = true; + console.error(`${file} did not report expected match: ${expectedMatch}`); + } + } + + if (stderr) { + console.error(stderr); + } +} + +if (failed) { + process.exit(1); +} + +console.log("Vale fixture checks passed."); From 3c80cdee6a72b18b073521639e8bb694345198c4 Mon Sep 17 00:00:00 2001 From: plx Date: Thu, 2 Jul 2026 08:53:37 -0500 Subject: [PATCH 3/8] Fix Vale runner in CI --- package.json | 2 +- scripts/check-vale-fixtures.js | 4 +++- scripts/run-vale.js | 27 +++++++++++++++++++++++++++ scripts/vale.js | 28 ++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 scripts/run-vale.js create mode 100644 scripts/vale.js diff --git a/package.json b/package.json index 0d0278f..5447052 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "lint": "eslint .", "lint:fix": "eslint . --fix", "lint:markdown": "markdownlint-cli2 \"src/content/**/*.{md,mdx}\"", - "lint:prose": "vale --config=.vale.ini src/content", + "lint:prose": "node scripts/run-vale.js --config=.vale.ini src/content", "test:vale": "node scripts/check-vale-fixtures.js", "spellcheck": "cspell \"src/**/*.{md,mdx,ts,tsx,js,jsx,astro}\" --no-progress", "spellcheck:html": "cspell \"dist/**/*.html\" --no-progress", diff --git a/scripts/check-vale-fixtures.js b/scripts/check-vale-fixtures.js index 3ee0e59..3f70e49 100644 --- a/scripts/check-vale-fixtures.js +++ b/scripts/check-vale-fixtures.js @@ -1,8 +1,10 @@ import { spawnSync } from "node:child_process"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { requireValeExecutable } from "./vale.js"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const valeExecutable = requireValeExecutable(root); const cases = [ { @@ -25,7 +27,7 @@ const cases = [ function runVale(file) { const result = spawnSync( - "vale", + valeExecutable, ["--config=.vale.ini", "--output=JSON", file], { cwd: root, diff --git a/scripts/run-vale.js b/scripts/run-vale.js new file mode 100644 index 0000000..f700925 --- /dev/null +++ b/scripts/run-vale.js @@ -0,0 +1,27 @@ +import { spawnSync } from "node:child_process"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { requireValeExecutable } from "./vale.js"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +let executable; + +try { + executable = requireValeExecutable(root); +} catch (error) { + console.error(error.message); + process.exit(1); +} + +const result = spawnSync(executable, process.argv.slice(2), { + cwd: root, + stdio: "inherit", + windowsHide: true, +}); + +if (result.error) { + console.error(result.error.message); + process.exit(1); +} + +process.exit(result.status ?? 1); diff --git a/scripts/vale.js b/scripts/vale.js new file mode 100644 index 0000000..7ccbf3b --- /dev/null +++ b/scripts/vale.js @@ -0,0 +1,28 @@ +import { accessSync, constants } from "node:fs"; +import { resolve } from "node:path"; + +export function getValeExecutable(root) { + return resolve( + root, + "node_modules", + "@vvago", + "vale", + "bin", + process.platform === "win32" ? "vale.exe" : "vale", + ); +} + +export function requireValeExecutable(root) { + const executable = getValeExecutable(root); + + try { + accessSync(executable, constants.F_OK | constants.X_OK); + } catch (error) { + throw new Error( + `Vale executable was not found at ${executable}. Run npm ci before invoking Vale.`, + { cause: error }, + ); + } + + return executable; +} From f76644ba55937bce12e38cfcd9f8d78746d8feef Mon Sep 17 00:00:00 2001 From: plx Date: Thu, 2 Jul 2026 22:08:51 -0500 Subject: [PATCH 4/8] Tighten Vale terminology fixtures --- .vale/fixtures/apple-terminology-invalid.md | 4 ++++ .vale/fixtures/apple-terminology-valid.md | 4 ++++ .vale/styles/Terminology/Apple.yml | 2 +- scripts/check-vale-fixtures.js | 22 +++++++++++++++------ 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.vale/fixtures/apple-terminology-invalid.md b/.vale/fixtures/apple-terminology-invalid.md index 6f6f2f1..ecd90ac 100644 --- a/.vale/fixtures/apple-terminology-invalid.md +++ b/.vale/fixtures/apple-terminology-invalid.md @@ -1,3 +1,7 @@ # Invalid Apple Terminology An ios app built in xcode can mention Swift UI, Objective C, Test Flight, MacOS, and Apple Silicon. + +An ipad tablet can pair with an iphone, while ipad os, watch os, tv os, and vision os keep platform names consistent. + +Publish through the app store. diff --git a/.vale/fixtures/apple-terminology-valid.md b/.vale/fixtures/apple-terminology-valid.md index 6f13b39..373a9ac 100644 --- a/.vale/fixtures/apple-terminology-valid.md +++ b/.vale/fixtures/apple-terminology-valid.md @@ -2,4 +2,8 @@ An iOS app built in Xcode can mention SwiftUI, Objective-C, TestFlight, macOS, and Apple silicon. +An iPad tablet can pair with an iPhone, while iPadOS, watchOS, tvOS, and visionOS keep platform names consistent. + +Publish through the App Store. + Historical names such as classic Mac OS and Mac OS X can be correct in older platform context. diff --git a/.vale/styles/Terminology/Apple.yml b/.vale/styles/Terminology/Apple.yml index 5716011..5217a2c 100644 --- a/.vale/styles/Terminology/Apple.yml +++ b/.vale/styles/Terminology/Apple.yml @@ -4,7 +4,7 @@ level: error ignorecase: false swap: '\b(ios|IOS|Ios)\b': iOS - '\b(ipad|IPad|Ipad|IPAD)\b': iPad + '\b(ipad(?![ -]?(?:os|OS)\b)|IPad(?![ -]?OS\b)|Ipad(?![ -]?OS\b)|IPAD)\b': iPad '\b(ipad[ -]?os|ipad[ -]?OS|iPad[ -]os|iPad[ -]OS|IPad[ -]?OS|Ipad[ -]?OS|IPADOS)\b': iPadOS '\b(iphone|IPhone|Iphone|IPHONE)\b': iPhone '\b(mac[ -]?os|MacOS|Mac-OS|MAC[ -]?OS)\b': macOS diff --git a/scripts/check-vale-fixtures.js b/scripts/check-vale-fixtures.js index 3f70e49..291e1f6 100644 --- a/scripts/check-vale-fixtures.js +++ b/scripts/check-vale-fixtures.js @@ -15,9 +15,16 @@ const cases = [ file: ".vale/fixtures/apple-terminology-invalid.md", expectedMatches: [ "ios", + "ipad", + "ipad os", + "iphone", "xcode", + "watch os", + "tv os", + "vision os", "Swift UI", "Objective C", + "app store", "Test Flight", "MacOS", "Apple Silicon", @@ -62,7 +69,8 @@ let failed = false; for (const { file, expectedMatches } of cases) { const { status, stderr, alerts } = runVale(file); - const matches = new Set(alerts.map((alert) => alert.Match)); + const matches = alerts.map((alert) => alert.Match).sort(); + const sortedExpectedMatches = expectedMatches.toSorted(); if (expectedMatches.length === 0) { if (status !== 0 || alerts.length !== 0) { @@ -79,11 +87,13 @@ for (const { file, expectedMatches } of cases) { console.error(`${file} should fail with Vale alerts.`); } - for (const expectedMatch of expectedMatches) { - if (!matches.has(expectedMatch)) { - failed = true; - console.error(`${file} did not report expected match: ${expectedMatch}`); - } + if (JSON.stringify(matches) !== JSON.stringify(sortedExpectedMatches)) { + failed = true; + console.error(`${file} reported unexpected Vale matches.`); + console.error("Expected:"); + console.error(JSON.stringify(sortedExpectedMatches, null, 2)); + console.error("Actual:"); + console.error(JSON.stringify(matches, null, 2)); } if (stderr) { From c59f2808150cd2811c53b4a4122e1ed2709574b5 Mon Sep 17 00:00:00 2001 From: plx Date: Thu, 2 Jul 2026 22:08:57 -0500 Subject: [PATCH 5/8] Lint Vale prose in image alt text --- .vale.ini | 2 +- .vale/README.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.vale.ini b/.vale.ini index 996dc70..fb2ed3c 100644 --- a/.vale.ini +++ b/.vale.ini @@ -1,6 +1,6 @@ StylesPath = .vale/styles MinAlertLevel = warning -IgnoredScopes = code, tt, img +IgnoredScopes = code, tt [formats] mdx = md diff --git a/.vale/README.md b/.vale/README.md index f951ded..c1006c6 100644 --- a/.vale/README.md +++ b/.vale/README.md @@ -13,6 +13,8 @@ Add new rule files under the most specific style namespace that fits the rule. I Apple terminology substitutions are case-sensitive and list the incorrect forms they should catch. That keeps correctly cased terms from depending on case-insensitive substitution behavior. +The substitution list is a regression-tested guardrail for common incorrect forms, not an exhaustive casing detector. Add explicit variants when a new mistake shows up in content or review. + Historical Apple platform names such as `classic Mac OS` and `Mac OS X` are valid when discussing older systems. If a passage intentionally uses a form that a rule flags, keep the exception local: ```markdown @@ -23,6 +25,8 @@ classic mac os spelling retained for quoted or historical context MDX files are treated as Markdown. JSX component attribute values can be linted as prose, so use local Vale disable comments around intentional non-prose values when needed. +Image alt text is linted with the rest of article prose because it ships to readers and assistive technology. + The local `@vvago/vale` package downloads the Vale binary during npm install. The lint command itself uses the checked-in rules and does not run `vale sync`. ## Usage From 6f7d9156551ecf30b4f47a62678304e8167fa3db Mon Sep 17 00:00:00 2001 From: plx Date: Thu, 2 Jul 2026 22:47:23 -0500 Subject: [PATCH 6/8] Handle mixed-case iPadOS terminology --- .vale/fixtures/apple-terminology-invalid.md | 2 +- .vale/styles/Terminology/Apple.yml | 4 ++-- scripts/check-vale-fixtures.js | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.vale/fixtures/apple-terminology-invalid.md b/.vale/fixtures/apple-terminology-invalid.md index ecd90ac..17c75ac 100644 --- a/.vale/fixtures/apple-terminology-invalid.md +++ b/.vale/fixtures/apple-terminology-invalid.md @@ -2,6 +2,6 @@ An ios app built in xcode can mention Swift UI, Objective C, Test Flight, MacOS, and Apple Silicon. -An ipad tablet can pair with an iphone, while ipad os, watch os, tv os, and vision os keep platform names consistent. +An ipad tablet can pair with an iphone, while ipad os, IPad os, watch os, tv os, and vision os keep platform names consistent. Publish through the app store. diff --git a/.vale/styles/Terminology/Apple.yml b/.vale/styles/Terminology/Apple.yml index 5217a2c..af9aa6b 100644 --- a/.vale/styles/Terminology/Apple.yml +++ b/.vale/styles/Terminology/Apple.yml @@ -4,8 +4,8 @@ level: error ignorecase: false swap: '\b(ios|IOS|Ios)\b': iOS - '\b(ipad(?![ -]?(?:os|OS)\b)|IPad(?![ -]?OS\b)|Ipad(?![ -]?OS\b)|IPAD)\b': iPad - '\b(ipad[ -]?os|ipad[ -]?OS|iPad[ -]os|iPad[ -]OS|IPad[ -]?OS|Ipad[ -]?OS|IPADOS)\b': iPadOS + '\b(ipad(?![ -]?(?:os|OS)\b)|IPad(?![ -]?(?:os|OS)\b)|Ipad(?![ -]?(?:os|OS)\b)|IPAD)\b': iPad + '\b(ipad[ -]?os|ipad[ -]?OS|iPad[ -]os|iPad[ -]OS|IPad[ -]?os|IPad[ -]?OS|Ipad[ -]?os|Ipad[ -]?OS|IPADOS)\b': iPadOS '\b(iphone|IPhone|Iphone|IPHONE)\b': iPhone '\b(mac[ -]?os|MacOS|Mac-OS|MAC[ -]?OS)\b': macOS '\b(watch[ -]?os|watch[ -]OS|Watch[ -]?OS|Watch[ -]?os|WATCH[ -]?OS)\b': watchOS diff --git a/scripts/check-vale-fixtures.js b/scripts/check-vale-fixtures.js index 291e1f6..af69e69 100644 --- a/scripts/check-vale-fixtures.js +++ b/scripts/check-vale-fixtures.js @@ -17,6 +17,7 @@ const cases = [ "ios", "ipad", "ipad os", + "IPad os", "iphone", "xcode", "watch os", From c65131d9858dc5eeab418f6394495cfd768920dd Mon Sep 17 00:00:00 2001 From: plx Date: Fri, 3 Jul 2026 06:04:42 -0500 Subject: [PATCH 7/8] Fix all-caps iPadOS terminology lint --- .vale/fixtures/apple-terminology-invalid.md | 2 +- .vale/styles/Terminology/Apple.yml | 4 ++-- scripts/check-vale-fixtures.js | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.vale/fixtures/apple-terminology-invalid.md b/.vale/fixtures/apple-terminology-invalid.md index 17c75ac..c481ab0 100644 --- a/.vale/fixtures/apple-terminology-invalid.md +++ b/.vale/fixtures/apple-terminology-invalid.md @@ -2,6 +2,6 @@ An ios app built in xcode can mention Swift UI, Objective C, Test Flight, MacOS, and Apple Silicon. -An ipad tablet can pair with an iphone, while ipad os, IPad os, watch os, tv os, and vision os keep platform names consistent. +An ipad tablet can pair with an iphone, while ipad os, IPad os, IPAD OS, IPAD-OS, watch os, tv os, and vision os keep platform names consistent. Publish through the app store. diff --git a/.vale/styles/Terminology/Apple.yml b/.vale/styles/Terminology/Apple.yml index af9aa6b..037ae1f 100644 --- a/.vale/styles/Terminology/Apple.yml +++ b/.vale/styles/Terminology/Apple.yml @@ -4,8 +4,8 @@ level: error ignorecase: false swap: '\b(ios|IOS|Ios)\b': iOS - '\b(ipad(?![ -]?(?:os|OS)\b)|IPad(?![ -]?(?:os|OS)\b)|Ipad(?![ -]?(?:os|OS)\b)|IPAD)\b': iPad - '\b(ipad[ -]?os|ipad[ -]?OS|iPad[ -]os|iPad[ -]OS|IPad[ -]?os|IPad[ -]?OS|Ipad[ -]?os|Ipad[ -]?OS|IPADOS)\b': iPadOS + '\b(ipad(?![ -]?(?:os|OS)\b)|IPad(?![ -]?(?:os|OS)\b)|Ipad(?![ -]?(?:os|OS)\b)|IPAD(?![ -]?(?:os|OS)\b))\b': iPad + '\b(ipad[ -]?os|ipad[ -]?OS|iPad[ -]os|iPad[ -]OS|IPad[ -]?os|IPad[ -]?OS|Ipad[ -]?os|Ipad[ -]?OS|IPAD[ -]?OS|IPADOS)\b': iPadOS '\b(iphone|IPhone|Iphone|IPHONE)\b': iPhone '\b(mac[ -]?os|MacOS|Mac-OS|MAC[ -]?OS)\b': macOS '\b(watch[ -]?os|watch[ -]OS|Watch[ -]?OS|Watch[ -]?os|WATCH[ -]?OS)\b': watchOS diff --git a/scripts/check-vale-fixtures.js b/scripts/check-vale-fixtures.js index af69e69..947c612 100644 --- a/scripts/check-vale-fixtures.js +++ b/scripts/check-vale-fixtures.js @@ -18,6 +18,8 @@ const cases = [ "ipad", "ipad os", "IPad os", + "IPAD OS", + "IPAD-OS", "iphone", "xcode", "watch os", From 96add180960eb119b5f3373058689cef110641a1 Mon Sep 17 00:00:00 2001 From: plx Date: Fri, 3 Jul 2026 11:53:05 -0500 Subject: [PATCH 8/8] Fix iPadOS Vale mixed-case suffix handling --- .vale/fixtures/apple-terminology-invalid.md | 2 +- .vale/styles/Terminology/Apple.yml | 4 ++-- scripts/check-vale-fixtures.js | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.vale/fixtures/apple-terminology-invalid.md b/.vale/fixtures/apple-terminology-invalid.md index c481ab0..d19264d 100644 --- a/.vale/fixtures/apple-terminology-invalid.md +++ b/.vale/fixtures/apple-terminology-invalid.md @@ -2,6 +2,6 @@ An ios app built in xcode can mention Swift UI, Objective C, Test Flight, MacOS, and Apple Silicon. -An ipad tablet can pair with an iphone, while ipad os, IPad os, IPAD OS, IPAD-OS, watch os, tv os, and vision os keep platform names consistent. +An ipad tablet can pair with an iphone, while ipad os, ipad-Os, IPad os, IPAD OS, IPAD-OS, watch os, tv os, and vision os keep platform names consistent. Publish through the app store. diff --git a/.vale/styles/Terminology/Apple.yml b/.vale/styles/Terminology/Apple.yml index 037ae1f..afdb3cb 100644 --- a/.vale/styles/Terminology/Apple.yml +++ b/.vale/styles/Terminology/Apple.yml @@ -4,8 +4,8 @@ level: error ignorecase: false swap: '\b(ios|IOS|Ios)\b': iOS - '\b(ipad(?![ -]?(?:os|OS)\b)|IPad(?![ -]?(?:os|OS)\b)|Ipad(?![ -]?(?:os|OS)\b)|IPAD(?![ -]?(?:os|OS)\b))\b': iPad - '\b(ipad[ -]?os|ipad[ -]?OS|iPad[ -]os|iPad[ -]OS|IPad[ -]?os|IPad[ -]?OS|Ipad[ -]?os|Ipad[ -]?OS|IPAD[ -]?OS|IPADOS)\b': iPadOS + '\b(?:ipad|IPad|Ipad|IPAD)(?![ -]?[oO][sS]\b)\b': iPad + '\b((?:ipad|IPad|Ipad|IPAD)[ -]?[oO][sS]|iPad(?:[ -][oO][sS]|os|Os|oS))\b': iPadOS '\b(iphone|IPhone|Iphone|IPHONE)\b': iPhone '\b(mac[ -]?os|MacOS|Mac-OS|MAC[ -]?OS)\b': macOS '\b(watch[ -]?os|watch[ -]OS|Watch[ -]?OS|Watch[ -]?os|WATCH[ -]?OS)\b': watchOS diff --git a/scripts/check-vale-fixtures.js b/scripts/check-vale-fixtures.js index 947c612..0ca2e01 100644 --- a/scripts/check-vale-fixtures.js +++ b/scripts/check-vale-fixtures.js @@ -16,6 +16,7 @@ const cases = [ expectedMatches: [ "ios", "ipad", + "ipad-Os", "ipad os", "IPad os", "IPAD OS",