From 1e47560e3480792b792c8bae4c04fb4e16b13689 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 10 Sep 2026 14:37:15 -0400 Subject: [PATCH] feat(#4593): add a macOS-specific conformance tier, final phase of epic #4589 (#4607) test-conformance's macos-latest leg (Phase 2, #4591) has been running the same 546-file, Windows-oriented conformance-tier list as windows-latest -- built from signals like windows-shell-token/windows-env-var that have nothing to do with macOS. Issue #4593 asked for macOS coverage sized to its own evidence-backed surface (zsh dispatch, case-sensitivity, darwin- specific behavior) instead. Issue #4593 was filed before Phase 5 (#4603) existed and referenced updating test-full's macOS legs -- that job is gone. Corrected the issue's body before any code was touched: the "shrink from full replay" half of the original ask was already done by Phase 5; what remained was narrowing the still-Windows-oriented tier macOS was inheriting. Two design assumptions were measured and rejected before accepting a design (documented in docs/adr/4593-macos-conformance-tier-architecture.md): - Reusing the general tier's signals minus its 3 Windows-specific categories barely narrows anything (546 -> 424, 78% retained) -- most files match multiple signals and only need one to survive exclusion. - A standalone CRLF/autocrlf signal, despite the issue naming "CRLF-checkout behavior": even narrowed to /\bCRLF\b|autocrlf/i it hit 143/930 files. Root cause: CRLF is primarily a Windows checkout concern in this codebase (ADR-1703 files it under DEFECT.WINDOWS-TEST- PORTABILITY), so the signal was really re-selecting Windows-relevant files already covered by the general tier, not narrowing macOS specifically. Built 5 new, genuinely macOS-specific signals instead: darwin-literal (darwin alone, not the general tier's win32-OR-darwin), zsh-dispatch, case-sensitivity, plus chmod-mode-bit and symlink-keyword reused verbatim from the general tier (genuinely Unix-relevant, not Windows-motivated). Measured against the real tree: 196 of 930 eligible unit-suite files (21%), versus the general tier's 546 (59%) -- a real, evidence-backed narrowing. scripts/gen-platform-conformance-tier.cjs gains classifyMacosContent/ classifyMacosTree/renderMacosGeneratedFile and a --target windows (default, unchanged)/--target macos CLI flag, so the same generator produces two independent, gated outputs rather than needing a second script. New committed output: scripts/lib/macos-conformance-tier. generated.cjs. .github/workflows/test.yml's test-conformance job: only the macos-latest leg's file-list source changes; windows-latest is byte-for-byte untouched. New shipped-file ripples handled proactively (19 install-tree fixtures regenerated, bin/install.js registered). An isolated code-review pass found one real defect: the ADR's per- category count table had drifted by 1 (zsh-dispatch, case-sensitivity) because the new test file's own fixture strings joined the tree it classifies after the table was authored -- fixed, with the union total (196, what CI actually gates on) confirmed unaffected. An isolated security-review pass found no qualifying findings. The ADR also records an explicit requirement for any future widening proposal: check whether the motivating regression is already covered by Phase 1's no-rendered-text-length-assert lint rule (#4590) before re-proposing full macOS/Linux parity, since that is exactly what #4421's root cause was (a rendered-text-length assertion, not a real behavioral divergence). Co-authored-by: sim Co-authored-by: Claude Sonnet 5 --- .github/workflows/test.yml | 14 +- bin/install.js | 2 +- ...593-macos-conformance-tier-architecture.md | 144 ++++++++++++ docs/adr/README.md | 1 + package.json | 4 +- scripts/gen-platform-conformance-tier.cjs | 158 ++++++++++++-- .../lib/macos-conformance-tier.generated.cjs | 206 ++++++++++++++++++ tests/fixtures/install-tree/antigravity.json | 1 + tests/fixtures/install-tree/augment.json | 1 + tests/fixtures/install-tree/claude-local.json | 1 + tests/fixtures/install-tree/claude.json | 1 + tests/fixtures/install-tree/cline.json | 1 + tests/fixtures/install-tree/codebuddy.json | 1 + tests/fixtures/install-tree/codex.json | 1 + tests/fixtures/install-tree/copilot.json | 1 + tests/fixtures/install-tree/cursor.json | 1 + tests/fixtures/install-tree/hermes.json | 1 + tests/fixtures/install-tree/kilo.json | 1 + tests/fixtures/install-tree/kimi-code.json | 1 + tests/fixtures/install-tree/kimi.json | 1 + tests/fixtures/install-tree/opencode.json | 1 + tests/fixtures/install-tree/pi.json | 1 + tests/fixtures/install-tree/qwen.json | 1 + tests/fixtures/install-tree/trae.json | 1 + tests/fixtures/install-tree/windsurf.json | 1 + tests/fixtures/install-tree/zcode.json | 1 + tests/platform-conformance-tier.test.cjs | 194 ++++++++++++++++- 27 files changed, 719 insertions(+), 23 deletions(-) create mode 100644 docs/adr/4593-macos-conformance-tier-architecture.md create mode 100644 scripts/lib/macos-conformance-tier.generated.cjs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6a17c4644..119564105 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -703,10 +703,20 @@ jobs: # scripts can `require()` it directly) — run-tests.cjs --files-from # expects a plain newline-delimited text file, so this step bridges # the two, one path per line, matching the existing scoped-lane - # convention (.ci-selected-tests.txt). - - name: Prepare conformance-tier test list + # convention (.ci-selected-tests.txt). Windows keeps the general, + # Windows-inclusive list. + - name: Prepare conformance-tier test list (windows) + if: matrix.os == 'windows-latest' run: node -e "require('./scripts/lib/platform-conformance-tier.generated.cjs').CONFORMANCE_TIER_FILES.forEach(f => console.log(f))" > .ci-conformance-tests.txt + # #4593: macOS reads a narrower, macOS-specific file list instead of the + # shared/Windows-oriented one above — see + # docs/adr/4593-macos-conformance-tier-architecture.md. Writes into the + # same conventional filename so the run step below is unchanged. + - name: Prepare conformance-tier test list (macos) + if: matrix.os == 'macos-latest' + run: node -e "require('./scripts/lib/macos-conformance-tier.generated.cjs').MACOS_CONFORMANCE_TIER_FILES.forEach(f => console.log(f))" > .ci-conformance-tests.txt + - name: Run platform-conformance-tier tests run: node scripts/run-tests.cjs --files-from .ci-conformance-tests.txt${{ matrix.shard && format(' --shard {0}', matrix.shard) || '' }} diff --git a/bin/install.js b/bin/install.js index fe07435bf..519ad4c06 100755 --- a/bin/install.js +++ b/bin/install.js @@ -436,7 +436,7 @@ const GSD_CHANGESET_FILES = [ 'github-release-notes.cjs', 'lint.cjs', 'new.cjs', 'README.md', // documentation only — not user-authored ]; -const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs', 'drift-scan.cjs', 'alias-drift-families.cjs', 'exit-code-registry.cjs', 'ndjson-reporter.cjs', 'ci-job-timing.cjs', 'shellcheck-fetch.cjs', 'npm-version-check-diagnosis.cjs', 'platform-conformance-tier.generated.cjs', 'suite-detection.cjs']; +const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs', 'drift-scan.cjs', 'alias-drift-families.cjs', 'exit-code-registry.cjs', 'ndjson-reporter.cjs', 'ci-job-timing.cjs', 'shellcheck-fetch.cjs', 'npm-version-check-diagnosis.cjs', 'platform-conformance-tier.generated.cjs', 'suite-detection.cjs', 'macos-conformance-tier.generated.cjs']; /** * Resolve a runtime's shared-hooks directory name from its descriptor. diff --git a/docs/adr/4593-macos-conformance-tier-architecture.md b/docs/adr/4593-macos-conformance-tier-architecture.md new file mode 100644 index 000000000..359cd3742 --- /dev/null +++ b/docs/adr/4593-macos-conformance-tier-architecture.md @@ -0,0 +1,144 @@ +# ADR-4593: A macOS-specific conformance-tier classifier, separate from the Windows-oriented one + +- **Status:** Accepted +- **Date:** 2026-09-10 +- **Issue:** [#4593](https://github.com/open-gsd/gsd-core/issues/4593) — Phase 4 (final) of epic [#4589](https://github.com/open-gsd/gsd-core/issues/4589) +- **Twin of:** [ADR-1703](./1703-portability-enforcement-architecture.md) (Windows portability enforcement architecture) — this ADR + applies ADR-1703's evidence-first, static-classifier discipline to a second, macOS-specific surface, + rather than reusing ADR-1703's Windows-oriented signal set unmodified. + +## Context + +`test-conformance`'s `macos-latest` CI leg (#4591, epic #4589 Phase 2) runs only the +`platform-conformance-tier` file list — a content classifier over `tests/**/*.test.cjs` that flags +files needing real-OS coverage, replacing a full-suite replay. That list's signal set +(`scripts/gen-platform-conformance-tier.cjs`'s `CATEGORIES`) was built for Windows: `windows-shell-token`, +`windows-env-var`, `hardcoded-path-vs-path-call`, etc. `macos-latest` today runs the identical +546-file list, which is Windows-oriented, not evidence-backed for macOS specifically. + +#4593 was originally filed against `test-full`'s macOS legs, a job Phase 5 (#4603) has since +deleted; the issue was corrected before design work started to target `test-conformance`'s +`macos-latest` leg instead — the "shrink from full replay" half of the original ask was already +done by Phase 2. + +Naively narrowing the general tier to macOS by simply dropping its three Windows-specific +categories (`windows-shell-token`, `windows-env-var`, `hardcoded-path-vs-path-call`) barely +narrows anything: measured, 546 -> 424 files (78% retained), because most files match multiple +signals simultaneously and only need one surviving signal to stay in the tier. That is nowhere +near the narrow, evidence-backed surface the issue asks for. + +## Decision + +Build a second, macOS-specific classifier (`classifyMacosContent` / `MACOS_CATEGORIES`) in the +same generator file, matching the issue's own named surface (zsh dispatch, case-sensitivity, +darwin-specific behavior) plus two categories reused verbatim from the general tier that are +genuinely Unix-relevant rather than Windows-motivated: + +| Category | Test | Rationale | Files matched (of 930 eligible) | +|---|---|---|---| +| `darwin-literal` | `/\bdarwin\b/` (darwin alone, not the general tier's `win32-darwin-literal` OR) | The general tier's combined `win32\|darwin` category can't distinguish which branch fired; a file mentioning only `win32` says nothing about macOS. | 8 | +| `zsh-dispatch` | `/\bzsh\b/i` | Directly names the issue's own "zsh shell dispatch" surface. | 14 | +| `case-sensitivity` | `/case.?insensitiv\|case.?sensitiv/i` | Directly names the issue's own "case-sensitivity" surface (macOS's default case-insensitive-but-preserving filesystem). | 74 | +| `chmod-mode-bit` | reused verbatim from the general tier's `CATEGORIES` | Unix permission-bit semantics — genuinely macOS/Linux-relevant, not a Windows category (Windows has no chmod). | 73 | +| `symlink-keyword` | reused verbatim from the general tier's `CATEGORIES` | Symlink handling differs materially on macOS (case-insensitive-but-preserving FS, different default symlink permissions) — not Windows-motivated the way `windows-shell-token` etc. are. | 86 | + +Applied to the real `tests/` unit-suite tree (930 files eligible under the same `suiteOf(f) === null` +gate the general tier uses), the union is **196 files** (21% of eligible files, vs. the general +tier's 546/930, 59%) — a materially narrower, macOS-evidence-backed tier. The per-category counts +above include `tests/platform-conformance-tier.test.cjs`'s own new fixture strings for +`zsh-dispatch`/`case-sensitivity` (e.g. `"shell: 'zsh {0}'"`) — self-referential by one file each, +since this test file lives in the same tree it classifies. Caught by an isolated code-review pass +comparing this table against a fresh live count; the union total was unaffected (that file was +already in the tier via an unrelated signal). + +### Rejected: a standalone CRLF/`autocrlf` signal + +The issue's own surface description names "CRLF-checkout behavior" as part of macOS's risk. This +was measured and rejected as a standalone macOS signal: + +- The obvious first attempt, a literal `/\r\n/` match, hit 197/952 files (21% of all test files) — + far too broad, because it matches routine defensive newline-normalization code + (`.replace(/\r\n/g, '\n')`) present throughout the suite for unrelated reasons, not files + specifically at CRLF-checkout risk. +- Narrowing to the actual named terms, `/\bCRLF\b|autocrlf/i`, still hit 143 files — still too + broad to be a precise macOS-only signal. +- Root cause: in this codebase, CRLF is primarily a **Windows** checkout concern — ADR-1703 files + its `no-crlf-fragile-split` rule under `DEFECT.WINDOWS-TEST-PORTABILITY`, not a macOS defect + class. A CRLF-keyword signal was therefore pulling in files already covered by the general, + Windows-oriented tier, not narrowing macOS coverage specifically. + +The issue's "CRLF-checkout" framing is treated as "this is one of the risk categories macOS needs +coverage for" — already satisfied, for files that actually need it, by the general tier — not "this +is a macOS-exclusive signal to build." + +### Implementation + +- `scripts/gen-platform-conformance-tier.cjs` gains `MACOS_CATEGORIES`, `classifyMacosContent`, + `classifyMacosTree`, and `renderMacosGeneratedFile`, mirroring the general tier's + `CATEGORIES`/`classifyContent`/`classifyTree`/`renderGeneratedFile` exactly (same `suiteOf(f) + === null` eligibility gate — see ADR-1703's sibling doc-comment in that file for why suite-tagged + files are excluded entirely rather than merely deprioritized). A new `--target windows` + (default, unchanged) / `--target macos` CLI flag selects which of the two independent generated + outputs a given `--check`/`--write`/print invocation targets, so `package.json`'s + `lint:generated-sync`/`regen:derived` chains invoke the same script twice rather than needing a + second script file. +- New committed output `scripts/lib/macos-conformance-tier.generated.cjs`, exporting + `MACOS_CONFORMANCE_TIER_FILES` — same generated-file convention (one array entry per line, + sorted, `GENERATED FILE` header banner) as `platform-conformance-tier.generated.cjs`. Gated by + the same `lint:generated-sync --check` fail-safe as the general tier — no new fail-safe mechanism + needed; this is a second instance of an already-proven pattern. +- `.github/workflows/test.yml`'s `test-conformance` job: the `macos-latest` leg's "prepare + conformance-tier test list" step reads `MACOS_CONFORMANCE_TIER_FILES` instead of the shared + `CONFORMANCE_TIER_FILES`, writing into the same conventional `.ci-conformance-tests.txt` + filename so the downstream `run-tests.cjs --files-from` step is unchanged. The `windows-latest` + legs, their shard count, and everything else about the job are untouched — they keep using the + general, Windows-inclusive tier. + +## Explicit requirement for future widening proposals + +Per issue #4593's "Done when": **before any future proposal to widen macOS coverage beyond this +196-file tier, check whether the motivating regression class is already covered by Phase 1's +`no-rendered-text-length-assert` ESLint rule (#4590, cataloged in ADR-1703)** — an author-time, +zero-escape-hatch rule that catches exactly the shape behind #4421 (a rendered-text-length +assertion embedding an OS-derived path, e.g. `os.tmpdir()`, whose length differs between macOS's +`/private/var/folders/…` prefix and Linux's shorter one). #4421's root cause was that +static-assertion shape, not a real macOS behavioral divergence requiring dynamic real-OS +execution to catch. A future explorer proposing full macOS/Linux parity coverage should confirm +new evidence of a *behavioral* divergence this static classifier and `no-rendered-text-length-assert` +both miss, rather than re-proposing parity on the strength of #4421 alone — #4421 is already +covered. + +## Consequences + +**Positive:** `macos-latest` now runs a narrower, macOS-evidence-backed 196-file tier (down from +the shared 546-file Windows-oriented list) — a real reduction (~64%) in real-OS macOS CI minutes +for files with no macOS-specific signal, without dropping coverage for files that do carry one. +Same generator, same fail-safe convention, same install/build ripple discipline as the general +tier — no new mechanism class. + +**Cost / risk:** a second static classifier is still subject to the same disclosed limit as the +general tier (ADR-1703 / `gen-platform-conformance-tier.cjs`'s own header doc-comment): it is +content-based, not a real per-file behavioral diff. Mitigated the same way — the most recent +full-matrix `next` run was green on every file in this classification before it was built. The +legacy full-matrix macOS job's actual safety-net window was much shorter than originally planned: +Phase 2 (#4591) added it as a non-gating safety net intended for "one release cycle," but Phase 5 +(#4603) retired it roughly 4 hours later, the same day, after discovering it was purely additive to +the new conformance job (10 OS-specific CI jobs per PR instead of the intended reduction) rather +than a genuine transition period — see #4603 for the full accounting. `macos-latest`'s coverage +here has not yet had an extended real-world safety-net window of its own; that is an accepted, +disclosed risk of shipping Phase 4 promptly rather than waiting, consistent with this epic's general +preference for fast, evidence-driven iteration over a long unmonitored parallel-running period. + +## Alternatives considered + +1. **Apply the general tier's signals minus the three Windows-specific categories.** Rejected — + measured at 424/546 files (78% retained), nowhere near the narrow, macOS-evidence-backed + surface the issue asks for; see "Context" above. +2. **A standalone CRLF/`autocrlf` signal.** Rejected — see "Rejected: a standalone CRLF/`autocrlf` + signal" above; both attempted regexes (197 and 143 files) were too broad and were really + re-selecting Windows-relevant files already covered by the general tier. +3. **A single, unified classifier covering both Windows and macOS signals, with per-OS filtering + at CI-invocation time.** Rejected — would still need two independent signal sets internally to + produce two independently-narrow lists, so it does not actually simplify anything over two + sibling classifier functions in the same file; a single combined list reproduces the 78%-retained + problem from alternative 1. diff --git a/docs/adr/README.md b/docs/adr/README.md index 7f46c3984..06155ea96 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -268,6 +268,7 @@ These govern the system as it stands. Cite these. | [ADR-3660](3660-runtime-artifact-layout-module.md) | Runtime Artifact Layout Module owns per-runtime artifact placement | Accepted | [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) | | [ADR-3806](3806-review-dispositions-ledger.md) | Review Dispositions Ledger canonizes where and how reviews-mode records incorporate/defer decisions in PLAN.md | Accepted | — | | [ADR-4139](4139-compact-content-seam.md) | The compact-content seam — shrink the eager window, never the guarantee | Accepted | — | +| [ADR-4593](4593-macos-conformance-tier-architecture.md) | A macOS-specific conformance-tier classifier, separate from the Windows-oriented one | Accepted | — | ### Proposed diff --git a/package.json b/package.json index f20c28ab8..95d65080e 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "gen:registry": "node scripts/gen-registry.cjs --write", "gen:install-tree": "node scripts/gen-install-tree-fixtures.cjs", "gen:section-manifest": "node scripts/gen-section-manifest.cjs --write", - "regen:derived": "npm run build && npm run gen:registry && node scripts/gen-adr-index.cjs --write && node scripts/gen-features.cjs --write && node scripts/gen-capability-matrix.cjs --write && node scripts/gen-inventory-manifest.cjs --write && node scripts/gen-context-index.cjs --write && node scripts/gen-state-md-docs.cjs --write && npm run gen:section-manifest && node scripts/sync-manifest-versions.cjs && npm run gen:install-tree && node scripts/gen-scripts-cli-exit.cjs --write && node scripts/gen-hooks-cli-exit.cjs --write && node scripts/gen-exit-code-registry.cjs --write && node scripts/gen-exit-code-docs.cjs --write && node scripts/gen-platform-conformance-tier.cjs --write", + "regen:derived": "npm run build && npm run gen:registry && node scripts/gen-adr-index.cjs --write && node scripts/gen-features.cjs --write && node scripts/gen-capability-matrix.cjs --write && node scripts/gen-inventory-manifest.cjs --write && node scripts/gen-context-index.cjs --write && node scripts/gen-state-md-docs.cjs --write && npm run gen:section-manifest && node scripts/sync-manifest-versions.cjs && npm run gen:install-tree && node scripts/gen-scripts-cli-exit.cjs --write && node scripts/gen-hooks-cli-exit.cjs --write && node scripts/gen-exit-code-registry.cjs --write && node scripts/gen-exit-code-docs.cjs --write && node scripts/gen-platform-conformance-tier.cjs --write && node scripts/gen-platform-conformance-tier.cjs --target macos --write", "validate:registry": "node scripts/validate-registry.cjs", "prepack": "npm run build:lib", "prepare": "npm run build:lib", @@ -132,7 +132,7 @@ "lint:test-file-count": "node scripts/lint-test-file-count.cjs", "lint:pr-checks": "node scripts/lint-pr-check-project-dir.cjs", "lint:changeset": "node scripts/changeset/lint.cjs", - "lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check && node scripts/gen-adr-index.cjs --check && node scripts/gen-features.cjs --check && node scripts/check-glossary-refs.cjs --check && node scripts/lint-compiled-artifact-sync.cjs --check && node scripts/gen-context-index.cjs --check && node scripts/gen-section-manifest.cjs --check && node scripts/gen-health-docs.cjs --check && node scripts/gen-state-md-docs.cjs --check && node scripts/gen-scripts-cli-exit.cjs --check && node scripts/gen-hooks-cli-exit.cjs --check && node scripts/gen-exit-code-registry.cjs --check && node scripts/gen-exit-code-docs.cjs --check && node scripts/gen-platform-conformance-tier.cjs --check", + "lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check && node scripts/gen-adr-index.cjs --check && node scripts/gen-features.cjs --check && node scripts/check-glossary-refs.cjs --check && node scripts/lint-compiled-artifact-sync.cjs --check && node scripts/gen-context-index.cjs --check && node scripts/gen-section-manifest.cjs --check && node scripts/gen-health-docs.cjs --check && node scripts/gen-state-md-docs.cjs --check && node scripts/gen-scripts-cli-exit.cjs --check && node scripts/gen-hooks-cli-exit.cjs --check && node scripts/gen-exit-code-registry.cjs --check && node scripts/gen-exit-code-docs.cjs --check && node scripts/gen-platform-conformance-tier.cjs --check && node scripts/gen-platform-conformance-tier.cjs --target macos --check", "lint:docs": "node scripts/lint-docs-required.cjs", "lint:qa-smells": "node scripts/qa-smell-ratchet.cjs", "lint:legacy-name": "node scripts/lint-legacy-dir-name.cjs", diff --git a/scripts/gen-platform-conformance-tier.cjs b/scripts/gen-platform-conformance-tier.cjs index f9d44c7f8..7c8940b81 100644 --- a/scripts/gen-platform-conformance-tier.cjs +++ b/scripts/gen-platform-conformance-tier.cjs @@ -58,9 +58,19 @@ * node scripts/gen-platform-conformance-tier.cjs # print summary to stdout * node scripts/gen-platform-conformance-tier.cjs --write # write the generated file * node scripts/gen-platform-conformance-tier.cjs --check # exit 1 if the committed file is stale + * node scripts/gen-platform-conformance-tier.cjs --target macos ... # same three modes, macOS-specific list (#4593) * node scripts/gen-platform-conformance-tier.cjs --tests-dir # override the tests/ root (tests only) * node scripts/gen-platform-conformance-tier.cjs --out # override the generated-file path (tests only) * + * `--target` selects which of the two independent generated outputs this + * invocation targets: `windows` (default, the original #4591 behavior — + * omitting the flag is unchanged) or `macos` (#4593's narrower, macOS- + * specific list). Both write into the SAME committed-file conventions + * (`scripts/lib/platform-conformance-tier.generated.cjs` / + * `scripts/lib/macos-conformance-tier.generated.cjs`), so `package.json`'s + * `lint:generated-sync`/`regen:derived` chains invoke this script twice, once + * per target, rather than needing a second script file. + * * `--tests-dir`/`--out` (or the TESTS_DIR/OUT_PATH env vars, flag takes * precedence) exist solely so tests/platform-conformance-tier.test.cjs can * point the CLI at a small temp fixture tree instead of this repo's real, @@ -77,10 +87,17 @@ const { suiteOf } = require('./lib/suite-detection.cjs'); const ROOT = path.resolve(__dirname, '..'); const DEFAULT_TESTS_DIR = path.join(ROOT, 'tests'); const DEFAULT_OUT_PATH = path.join(ROOT, 'scripts', 'lib', 'platform-conformance-tier.generated.cjs'); +const DEFAULT_MACOS_OUT_PATH = path.join(ROOT, 'scripts', 'lib', 'macos-conformance-tier.generated.cjs'); const GENERATED_HEADER = '// GENERATED FILE — do not hand-edit. Run `node scripts/gen-platform-conformance-tier.cjs --write` to regenerate.\n'; +const MACOS_GENERATED_HEADER = + '// GENERATED FILE — do not hand-edit. Run `node scripts/gen-platform-conformance-tier.cjs --target macos --write` to regenerate.\n' + + '// macOS-specific conformance tier (#4593), separate from and narrower than the general/\n' + + '// Windows-oriented tier in platform-conformance-tier.generated.cjs — see\n' + + '// docs/adr/4593-macos-conformance-tier-architecture.md for the full rationale.\n'; + /** * Detection categories (#4591 design doc). Each entry's `test` receives the * raw file content string and returns true when that category's signal is @@ -158,6 +175,31 @@ const CATEGORIES = [ // to 28 files, all verified to carry a genuine platform-conditional branch. const NOISY_FOR_SOURCE_REACHABILITY = new Set(['hardcoded-path-vs-path-call', 'symlink-keyword']); +/** + * macOS-specific detection categories (#4593, design doc + * .gsd/phase/chore-4593-macos-conformance-tier/40-design.md). Built new, + * rather than reusing CATEGORIES above minus its Windows-specific entries, + * because that naive exclusion barely narrows anything (measured: 546 -> 424 + * files, 78%) — most files match multiple general-tier signals simultaneously + * and only need ONE surviving signal to stay in. `chmod-mode-bit` and + * `symlink-keyword` ARE deliberately duplicated verbatim from CATEGORIES: + * both are genuinely Unix-relevant (chmod bits and symlink semantics differ + * materially on macOS), not Windows-motivated the way the rest of CATEGORIES + * is. A standalone CRLF/`autocrlf` signal was considered and rejected: even + * narrowed to `/\bCRLF\b|autocrlf/i` it still hit 143/930 files (15%) — CRLF + * is primarily a Windows checkout concern in this codebase (ADR-1703's + * `no-crlf-fragile-split` files it under DEFECT.WINDOWS-TEST-PORTABILITY), + * so a CRLF signal pulls in Windows-relevant files already covered by the + * general tier, not a macOS-narrowing one. + */ +const MACOS_CATEGORIES = [ + { name: 'darwin-literal', test: (content) => /\bdarwin\b/.test(content) }, + { name: 'zsh-dispatch', test: (content) => /\bzsh\b/i.test(content) }, + { name: 'case-sensitivity', test: (content) => /case.?insensitiv|case.?sensitiv/i.test(content) }, + { name: 'chmod-mode-bit', test: (content) => /chmodSync|chmod\(/.test(content) || /0o[0-7]{3,4}\b/.test(content) }, + { name: 'symlink-keyword', test: (content) => /\bsymlink/i.test(content) }, +]; + /** * Pure classifier: given a test file's raw string content, returns which * platform-conformance categories matched and whether the file needs real-OS @@ -175,6 +217,22 @@ function classifyContent(content) { return { needsRealOs: signals.length > 0, signals }; } +/** + * Pure classifier, macOS-specific signal set (#4593). Same shape as + * classifyContent, against MACOS_CATEGORIES instead of CATEGORIES. + * + * @param {string} content + * @returns {{needsRealOs: boolean, signals: string[]}} + */ +function classifyMacosContent(content) { + const text = typeof content === 'string' ? content : ''; + const signals = []; + for (const category of MACOS_CATEGORIES) { + if (category.test(text)) signals.push(category.name); + } + return { needsRealOs: signals.length > 0, signals }; +} + /** * Recursively collect every `*.test.cjs` file beneath `dir`. * @param {string} dir @@ -227,6 +285,29 @@ function classifyTree(testsDir) { return { total: absoluteFiles.length, files: flagged }; } +/** + * Same walk/eligibility as classifyTree, classified with the macOS-specific + * signal set (#4593). + * + * @param {string} testsDir + * @returns {{ total: number, files: string[] }} + */ +function classifyMacosTree(testsDir) { + const absoluteFiles = walkTestFiles(testsDir); + const unitFiles = absoluteFiles.filter((absPath) => suiteOf(absPath) === null); + const flagged = []; + for (const absPath of unitFiles) { + const content = fs.readFileSync(absPath, 'utf8'); + const { needsRealOs } = classifyMacosContent(content); + if (needsRealOs) { + const rel = path.relative(testsDir, absPath).replace(/\\/g, '/'); + flagged.push('tests/' + rel); + } + } + flagged.sort(); + return { total: absoluteFiles.length, files: flagged }; +} + /** * Render the generated `.cjs` module body — one array entry per line for a * readable diff, matching scripts/lib/portability-vocab.cjs's array-literal @@ -248,10 +329,43 @@ function renderGeneratedFile(files) { ); } -/** Resolve the effective tests-dir / out-path from argv/env, flag beats env. */ +/** + * Render scripts/lib/macos-conformance-tier.generated.cjs's module body, + * mirroring renderGeneratedFile exactly against the macOS export name. + * + * @param {string[]} files - already sorted. + * @returns {string} + */ +function renderMacosGeneratedFile(files) { + const lines = files.map((f) => ` ${JSON.stringify(f)},`).join('\n'); + return ( + MACOS_GENERATED_HEADER + + "'use strict';\n\n" + + 'module.exports = {\n' + + ' MACOS_CONFORMANCE_TIER_FILES: [\n' + + (lines.length > 0 ? lines + '\n' : '') + + ' ],\n' + + '};\n' + ); +} + +/** Resolve the effective target/tests-dir/out-path from argv/env, flag beats env. */ function resolveOverrides(argv) { + let target = 'windows'; + for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--target') { + const value = argv[i + 1]; + if (value !== 'windows' && value !== 'macos') { + throw new ExitError(1, 'gen-platform-conformance-tier: --target requires "windows" or "macos"'); + } + target = value; + i++; + } + } + + const defaultOutPath = target === 'macos' ? DEFAULT_MACOS_OUT_PATH : DEFAULT_OUT_PATH; let testsDir = process.env.TESTS_DIR || DEFAULT_TESTS_DIR; - let outPath = process.env.OUT_PATH || DEFAULT_OUT_PATH; + let outPath = process.env.OUT_PATH || defaultOutPath; for (let i = 0; i < argv.length; i++) { if (argv[i] === '--tests-dir') { @@ -271,19 +385,22 @@ function resolveOverrides(argv) { } } - return { testsDir: path.resolve(testsDir), outPath: path.resolve(outPath) }; + return { target, testsDir: path.resolve(testsDir), outPath: path.resolve(outPath) }; } function main() { const argv = process.argv.slice(2); - const { testsDir, outPath } = resolveOverrides(argv); + const { target, testsDir, outPath } = resolveOverrides(argv); const mode = argv.includes('--check') ? 'check' : argv.includes('--write') ? 'write' : 'print'; - const { total, files } = classifyTree(testsDir); + const isMacos = target === 'macos'; + const label = isMacos ? 'gen-platform-conformance-tier --target macos' : 'gen-platform-conformance-tier'; + const exportKey = isMacos ? 'MACOS_CONFORMANCE_TIER_FILES' : 'CONFORMANCE_TIER_FILES'; + const { total, files } = isMacos ? classifyMacosTree(testsDir) : classifyTree(testsDir); if (mode === 'write') { fs.mkdirSync(path.dirname(outPath), { recursive: true }); - fs.writeFileSync(outPath, renderGeneratedFile(files)); + fs.writeFileSync(outPath, isMacos ? renderMacosGeneratedFile(files) : renderGeneratedFile(files)); process.stdout.write(`Wrote ${outPath} (${files.length} conformance-tier file(s))\n`); return; } @@ -299,14 +416,12 @@ function main() { } catch (err) { throw new ExitError( 1, - `gen-platform-conformance-tier: could not load ${outPath} — run ` + - '`node scripts/gen-platform-conformance-tier.cjs --write` first ' + + `${label}: could not load ${outPath} — run ` + + `\`node scripts/gen-platform-conformance-tier.cjs${isMacos ? ' --target macos' : ''} --write\` first ` + `(${err && err.message ? err.message : err})`, ); } - const committedFiles = Array.isArray(committed.CONFORMANCE_TIER_FILES) - ? committed.CONFORMANCE_TIER_FILES - : []; + const committedFiles = Array.isArray(committed[exportKey]) ? committed[exportKey] : []; const committedSet = new Set(committedFiles); const liveSet = new Set(files); @@ -316,22 +431,20 @@ function main() { if (added.length > 0 || removed.length > 0) { process.stderr.write( `${path.relative(ROOT, outPath).replace(/\\/g, '/')} is stale. Run:\n` + - ' node scripts/gen-platform-conformance-tier.cjs --write\n\n', + ` node scripts/gen-platform-conformance-tier.cjs${isMacos ? ' --target macos' : ''} --write\n\n`, ); for (const f of added) process.stderr.write(' + ' + f + '\n'); for (const f of removed) process.stderr.write(' - ' + f + '\n'); throw new ExitError(1); } - process.stdout.write( - `ok gen-platform-conformance-tier: ${files.length} conformance-tier files, list matches\n`, - ); + process.stdout.write(`ok ${label}: ${files.length} conformance-tier files, list matches\n`); return; } // No flag: print a classification summary, write nothing. process.stdout.write( - `gen-platform-conformance-tier: ${total} file(s) scanned, ` + + `${label}: ${total} file(s) scanned, ` + `${files.length} need real OS, ${total - files.length} excluded (Linux-only conformance tier eligible)\n`, ); } @@ -341,4 +454,15 @@ if (require.main === module) { runMain(main); } -module.exports = { classifyContent, CATEGORIES, NOISY_FOR_SOURCE_REACHABILITY, walkTestFiles, classifyTree, renderGeneratedFile }; +module.exports = { + classifyContent, + CATEGORIES, + NOISY_FOR_SOURCE_REACHABILITY, + walkTestFiles, + classifyTree, + renderGeneratedFile, + classifyMacosContent, + MACOS_CATEGORIES, + classifyMacosTree, + renderMacosGeneratedFile, +}; diff --git a/scripts/lib/macos-conformance-tier.generated.cjs b/scripts/lib/macos-conformance-tier.generated.cjs new file mode 100644 index 000000000..c52e2426c --- /dev/null +++ b/scripts/lib/macos-conformance-tier.generated.cjs @@ -0,0 +1,206 @@ +// GENERATED FILE — do not hand-edit. Run `node scripts/gen-platform-conformance-tier.cjs --target macos --write` to regenerate. +// macOS-specific conformance tier (#4593), separate from and narrower than the general/ +// Windows-oriented tier in platform-conformance-tier.generated.cjs — see +// docs/adr/4593-macos-conformance-tier-architecture.md for the full rationale. +'use strict'; + +module.exports = { + MACOS_CONFORMANCE_TIER_FILES: [ + "tests/adr-index-gate.test.cjs", + "tests/adr-parser.property.test.cjs", + "tests/adr-parser.unit.test.cjs", + "tests/agent-classification-parity.test.cjs", + "tests/agent-install-check.test.cjs", + "tests/agent-skills.test.cjs", + "tests/api-coverage-gate-e2e.test.cjs", + "tests/api-coverage.test.cjs", + "tests/ask-user-questions-fallback.test.cjs", + "tests/audit-command-cutover.test.cjs", + "tests/broken-windows.test.cjs", + "tests/capability-cli.test.cjs", + "tests/capability-command-dispatch.test.cjs", + "tests/capability-consent.test.cjs", + "tests/capability-ledger.test.cjs", + "tests/capability-lifecycle.test.cjs", + "tests/capability-loader.test.cjs", + "tests/capability-lock-mkdir-failure-3987.test.cjs", + "tests/capability-registry.test.cjs", + "tests/capability-source.test.cjs", + "tests/capability-state.test.cjs", + "tests/capability-trust.test.cjs", + "tests/changeset-parse.test.cjs", + "tests/check-contract-drift.test.cjs", + "tests/check-ui-safety-gate.test.cjs", + "tests/check-update-config-dir.test.cjs", + "tests/chunked-planning-parallel.test.cjs", + "tests/ci-docs-guard-registry.test.cjs", + "tests/ci-test-scope.test.cjs", + "tests/cline-install.test.cjs", + "tests/code-review-pipeline-regression.test.cjs", + "tests/codex-config-agents.test.cjs", + "tests/codex-config-hooks.test.cjs", + "tests/codex-config-install.test.cjs", + "tests/codex-config.test.cjs", + "tests/commands.test.cjs", + "tests/commit-docs-bypass.test.cjs", + "tests/commit-files-pathspec.test.cjs", + "tests/commonjs-marker.test.cjs", + "tests/completion-ratio-scope-withholding.test.cjs", + "tests/config-loader.test.cjs", + "tests/config-schema.property.test.cjs", + "tests/config.test.cjs", + "tests/contributor-standards.test.cjs", + "tests/core-utils.test.cjs", + "tests/cursor-subagent-isolation.test.cjs", + "tests/debugger-semantic-recall.test.cjs", + "tests/default-flip-documentation-lint.test.cjs", + "tests/discuss-phase-power.test.cjs", + "tests/docs-parity-live-registry.test.cjs", + "tests/effort-surface-axis.test.cjs", + "tests/effort-sync-installed-runtime.test.cjs", + "tests/emitted-attribution.test.cjs", + "tests/ensure-runtime-build.test.cjs", + "tests/executed-plan.test.cjs", + "tests/executor-mvp-tdd-section.test.cjs", + "tests/external-job.test.cjs", + "tests/failing-direction.test.cjs", + "tests/fallow-runner.test.cjs", + "tests/feat-2483-review-claude-mds-guard.test.cjs", + "tests/features-index-gate.test.cjs", + "tests/frontmatter.unit.test.cjs", + "tests/gap-checker.property.test.cjs", + "tests/gen-context-index.test.cjs", + "tests/gen-section-manifest.test.cjs", + "tests/git-base-branch.test.cjs", + "tests/graphify-query.test.cjs", + "tests/graphify-visualization.test.cjs", + "tests/gsd-secret-read-guard.test.cjs", + "tests/gsd-settings-advanced.test.cjs", + "tests/gsd-statusline.test.cjs", + "tests/gsd-tools-path-refs.test.cjs", + "tests/gsd-validate-commit-crash-policy.test.cjs", + "tests/gsd-write-guard.test.cjs", + "tests/health-diagnostic-rules/worktree-health.test.cjs", + "tests/health-diagnostic.test.cjs", + "tests/helpers-cleanup.test.cjs", + "tests/helpers-process-isolation.test.cjs", + "tests/hooks-crash-policy.test.cjs", + "tests/hooks-opt-in.test.cjs", + "tests/host-integration.test.cjs", + "tests/init-manager.test.cjs", + "tests/init.test.cjs", + "tests/install-minimal-hooks.test.cjs", + "tests/install-path-detection.test.cjs", + "tests/install-regressions.test.cjs", + "tests/install-runtime-artifacts.test.cjs", + "tests/install-write-confinement.test.cjs", + "tests/install.test.cjs", + "tests/installer-migration-antigravity-retire-confighome-artifacts.test.cjs", + "tests/installer-migration-config-root-marker.test.cjs", + "tests/installer-migration-pi-retire-hooks-dir.test.cjs", + "tests/installer-migration-prune-stale-pristine.test.cjs", + "tests/installer-migration-rename-gsd-core.test.cjs", + "tests/installer-migrations.test.cjs", + "tests/intel.test.cjs", + "tests/inventory-nested-families.test.cjs", + "tests/isolation-sentinel.test.cjs", + "tests/issue-version-gate.test.cjs", + "tests/kimi-upgrades.test.cjs", + "tests/lint-docs-command-form.test.cjs", + "tests/lint-workflow-shellcheck-fetch.test.cjs", + "tests/list-seeds.test.cjs", + "tests/markdown-sectionizer.test.cjs", + "tests/mcp-catalog.test.cjs", + "tests/milestone-archive.test.cjs", + "tests/milestone-window-single-owner.test.cjs", + "tests/model-catalog.unit.test.cjs", + "tests/model-resolver.test.cjs", + "tests/no-exact-case-env-access.rule.test.cjs", + "tests/no-pending-3212-markers.test.cjs", + "tests/no-phantom-issue-refs.test.cjs", + "tests/no-posix-mode-bit-assert.rule.test.cjs", + "tests/no-private-binary-resolution.rule.test.cjs", + "tests/no-unguarded-nonportable-exec.rule.test.cjs", + "tests/observability/event.test.cjs", + "tests/onboard-command.test.cjs", + "tests/opencode-plugin-adapter.test.cjs", + "tests/pause-work-context-detection.test.cjs", + "tests/pause-work-improvements.test.cjs", + "tests/perf-317-context-monitor-fs.test.cjs", + "tests/phase-completion-single-owner.test.cjs", + "tests/phase-estimation.test.cjs", + "tests/phase-id-drift-guard.test.cjs", + "tests/phase-id.test.cjs", + "tests/phase-locator.test.cjs", + "tests/phase.test.cjs", + "tests/plan-count-single-owner.test.cjs", + "tests/plan-phase-stall-detection.test.cjs", + "tests/plan-review-convergence.test.cjs", + "tests/planning-inspect.test.cjs", + "tests/planning-inspect.unit.test.cjs", + "tests/planning-lock-mkdir-failure-1884.test.cjs", + "tests/planning-snapshot.test.cjs", + "tests/planning-workspace.test.cjs", + "tests/platform-conformance-tier.test.cjs", + "tests/plugin-manifest.test.cjs", + "tests/policy-shell-pinning.test.cjs", + "tests/portability-rule-disable-ban.test.cjs", + "tests/precommit-alias-drift-hook.test.cjs", + "tests/prepush-enterprise-email-hook.test.cjs", + "tests/process-seam.test.cjs", + "tests/profile-output.test.cjs", + "tests/profile-pipeline.test.cjs", + "tests/quick-batch.test.cjs", + "tests/quick-branching.test.cjs", + "tests/quick-research.test.cjs", + "tests/reapply-verify-hunks.test.cjs", + "tests/refactor-1390-t3-characterization.test.cjs", + "tests/require-issue-link-policy.test.cjs", + "tests/response-language-coverage.test.cjs", + "tests/retired-artifact-cleanup.test.cjs", + "tests/review-build-prompt-optional-sections.test.cjs", + "tests/review-lane-runner.test.cjs", + "tests/review-lane-windows-spawn-resolution.test.cjs", + "tests/review-parallel-lanes.test.cjs", + "tests/review-plan-coverage-manifest.test.cjs", + "tests/review-reviewer-selection.test.cjs", + "tests/reviewer-manifest-body.test.cjs", + "tests/reviewer-step-dispatch.test.cjs", + "tests/roadmap-mode-field.test.cjs", + "tests/roadmap-parser.test.cjs", + "tests/roadmap-phase-fallback.test.cjs", + "tests/roadmap.test.cjs", + "tests/runtime-artifact-layout.test.cjs", + "tests/runtime-identity.test.cjs", + "tests/runtime-launcher-parity.test.cjs", + "tests/security.test.cjs", + "tests/settings-jsonc.test.cjs", + "tests/shared-hooks-dir-resolution.test.cjs", + "tests/shell-command-projection-dispatch.test.cjs", + "tests/spawn-liveness-banner.test.cjs", + "tests/state-document.test.cjs", + "tests/state-transition.test.cjs", + "tests/state.test.cjs", + "tests/todos-workstream-scope.test.cjs", + "tests/tracer-bullet.test.cjs", + "tests/uat.test.cjs", + "tests/ui-safety-gate.test.cjs", + "tests/ui-spec-inventory-provenance.test.cjs", + "tests/unreachable-guard-drift.test.cjs", + "tests/unreachable-shell-guard.test.cjs", + "tests/unusable-input.test.cjs", + "tests/update-custom-backup.test.cjs", + "tests/user-artifact-staging.test.cjs", + "tests/verification-overrides.test.cjs", + "tests/verification-status.test.cjs", + "tests/verify-archive-dirs-live-path.test.cjs", + "tests/verify-command-grounding.test.cjs", + "tests/verify.test.cjs", + "tests/workflow-fragments.test.cjs", + "tests/workflow-shell-pinning.test.cjs", + "tests/workstream-inventory.test.cjs", + "tests/workstream.test.cjs", + "tests/worktree-safety.test.cjs", + "tests/worktree.test.cjs", + ], +}; diff --git a/tests/fixtures/install-tree/antigravity.json b/tests/fixtures/install-tree/antigravity.json index 6394abe9c..65a8ac7fe 100644 --- a/tests/fixtures/install-tree/antigravity.json +++ b/tests/fixtures/install-tree/antigravity.json @@ -622,6 +622,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/augment.json b/tests/fixtures/install-tree/augment.json index 0c93b86c4..9fdeb8749 100644 --- a/tests/fixtures/install-tree/augment.json +++ b/tests/fixtures/install-tree/augment.json @@ -693,6 +693,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/claude-local.json b/tests/fixtures/install-tree/claude-local.json index f013bdedd..a52dd441d 100644 --- a/tests/fixtures/install-tree/claude-local.json +++ b/tests/fixtures/install-tree/claude-local.json @@ -557,6 +557,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/claude.json b/tests/fixtures/install-tree/claude.json index 611519571..d2da97035 100644 --- a/tests/fixtures/install-tree/claude.json +++ b/tests/fixtures/install-tree/claude.json @@ -621,6 +621,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/cline.json b/tests/fixtures/install-tree/cline.json index 64d22d177..5318bb567 100644 --- a/tests/fixtures/install-tree/cline.json +++ b/tests/fixtures/install-tree/cline.json @@ -583,6 +583,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/codebuddy.json b/tests/fixtures/install-tree/codebuddy.json index 7b035ffe7..c11385747 100644 --- a/tests/fixtures/install-tree/codebuddy.json +++ b/tests/fixtures/install-tree/codebuddy.json @@ -693,6 +693,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/codex.json b/tests/fixtures/install-tree/codex.json index a7bb472f8..d3ef6ca90 100644 --- a/tests/fixtures/install-tree/codex.json +++ b/tests/fixtures/install-tree/codex.json @@ -621,6 +621,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/copilot.json b/tests/fixtures/install-tree/copilot.json index 4b60ade55..2876dc69d 100644 --- a/tests/fixtures/install-tree/copilot.json +++ b/tests/fixtures/install-tree/copilot.json @@ -583,6 +583,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/cursor.json b/tests/fixtures/install-tree/cursor.json index 2d8c8c71f..3e4c1ee1b 100644 --- a/tests/fixtures/install-tree/cursor.json +++ b/tests/fixtures/install-tree/cursor.json @@ -594,6 +594,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/hermes.json b/tests/fixtures/install-tree/hermes.json index ed62ec76a..530029ab3 100644 --- a/tests/fixtures/install-tree/hermes.json +++ b/tests/fixtures/install-tree/hermes.json @@ -621,6 +621,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/kilo.json b/tests/fixtures/install-tree/kilo.json index 52a04da47..ab1d87e09 100644 --- a/tests/fixtures/install-tree/kilo.json +++ b/tests/fixtures/install-tree/kilo.json @@ -696,6 +696,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/kimi-code.json b/tests/fixtures/install-tree/kimi-code.json index cec17e031..7bb433635 100644 --- a/tests/fixtures/install-tree/kimi-code.json +++ b/tests/fixtures/install-tree/kimi-code.json @@ -622,6 +622,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/kimi.json b/tests/fixtures/install-tree/kimi.json index 37683b3be..5c14148d7 100644 --- a/tests/fixtures/install-tree/kimi.json +++ b/tests/fixtures/install-tree/kimi.json @@ -589,6 +589,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/opencode.json b/tests/fixtures/install-tree/opencode.json index 0d0cb6b41..135989b9f 100644 --- a/tests/fixtures/install-tree/opencode.json +++ b/tests/fixtures/install-tree/opencode.json @@ -696,6 +696,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/pi.json b/tests/fixtures/install-tree/pi.json index 319b699ba..aa92e91c2 100644 --- a/tests/fixtures/install-tree/pi.json +++ b/tests/fixtures/install-tree/pi.json @@ -423,6 +423,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/qwen.json b/tests/fixtures/install-tree/qwen.json index 906b2e318..96d61bf75 100644 --- a/tests/fixtures/install-tree/qwen.json +++ b/tests/fixtures/install-tree/qwen.json @@ -621,6 +621,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/trae.json b/tests/fixtures/install-tree/trae.json index 57ca1a0b5..eca69ebfd 100644 --- a/tests/fixtures/install-tree/trae.json +++ b/tests/fixtures/install-tree/trae.json @@ -581,6 +581,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/windsurf.json b/tests/fixtures/install-tree/windsurf.json index 6baca149a..072362bcc 100644 --- a/tests/fixtures/install-tree/windsurf.json +++ b/tests/fixtures/install-tree/windsurf.json @@ -516,6 +516,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/fixtures/install-tree/zcode.json b/tests/fixtures/install-tree/zcode.json index ee09be87a..871afda8c 100644 --- a/tests/fixtures/install-tree/zcode.json +++ b/tests/fixtures/install-tree/zcode.json @@ -653,6 +653,7 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", + "scripts/lib/macos-conformance-tier.generated.cjs", "scripts/lib/ndjson-reporter.cjs", "scripts/lib/npm-version-check-diagnosis.cjs", "scripts/lib/platform-conformance-tier.generated.cjs", diff --git a/tests/platform-conformance-tier.test.cjs b/tests/platform-conformance-tier.test.cjs index f0c799be3..654b5bdda 100644 --- a/tests/platform-conformance-tier.test.cjs +++ b/tests/platform-conformance-tier.test.cjs @@ -22,11 +22,18 @@ const path = require('node:path'); const { createTempDir, cleanup } = require('./helpers.cjs'); const { runNode } = require('./helpers/process-seam.cjs'); -const { classifyContent, classifyTree, NOISY_FOR_SOURCE_REACHABILITY } = require('../scripts/gen-platform-conformance-tier.cjs'); +const { + classifyContent, + classifyTree, + NOISY_FOR_SOURCE_REACHABILITY, + classifyMacosContent, + classifyMacosTree, +} = require('../scripts/gen-platform-conformance-tier.cjs'); const ROOT = path.resolve(__dirname, '..'); const SCRIPT = path.join(ROOT, 'scripts', 'gen-platform-conformance-tier.cjs'); const GENERATED_PATH = path.join(ROOT, 'scripts', 'lib', 'platform-conformance-tier.generated.cjs'); +const MACOS_GENERATED_PATH = path.join(ROOT, 'scripts', 'lib', 'macos-conformance-tier.generated.cjs'); // ─── Rows 1-15: classifyContent, pure fixtures ──────────────────────────────── @@ -319,3 +326,188 @@ describe('gen-platform-conformance-tier.cjs — real repo tree (regression)', () ); }); }); + +// ─── #4593: macOS-specific classifier (classifyMacosContent / MACOS_CATEGORIES) + +describe('classifyMacosContent — happy-path signals', () => { + test('flags darwin literal', () => { + const { needsRealOs, signals } = classifyMacosContent("const platforms = ['darwin', 'linux'];"); + assert.equal(needsRealOs, true); + assert.ok(signals.includes('darwin-literal')); + }); + + test('flags zsh dispatch', () => { + const { needsRealOs, signals } = classifyMacosContent("shell: 'zsh {0}'"); + assert.equal(needsRealOs, true); + assert.ok(signals.includes('zsh-dispatch')); + }); + + test('flags case-sensitivity phrasing', () => { + for (const fixture of [ + '// verify the case-insensitive lookup', + '// verify the case-sensitive lookup', + '// verify case insensitivity', + ]) { + const { needsRealOs, signals } = classifyMacosContent(fixture); + assert.equal(needsRealOs, true, fixture); + assert.ok(signals.includes('case-sensitivity'), fixture); + } + }); + + test('flags chmod mode-bit octal', () => { + const { needsRealOs, signals } = classifyMacosContent('fs.chmodSync(target, 0o755);'); + assert.equal(needsRealOs, true); + assert.ok(signals.includes('chmod-mode-bit')); + }); + + test('flags symlink keyword', () => { + const { needsRealOs, signals } = classifyMacosContent('fs.symlinkSync(target, link);'); + assert.equal(needsRealOs, true); + assert.ok(signals.includes('symlink-keyword')); + }); +}); + +describe('classifyMacosContent — negative case', () => { + test('does not flag a clean file with none of the 5 macOS signals', () => { + const fixture = + "const assert = require('node:assert/strict');\n" + + "if (process.platform === 'win32') { doThing(); }\n" + + "test('adds numbers', () => { assert.equal(1 + 1, 2); });"; + const { needsRealOs, signals } = classifyMacosContent(fixture); + assert.equal(needsRealOs, false); + assert.deepEqual(signals, []); + }); + + test('does not crash on empty content', () => { + assert.doesNotThrow(() => classifyMacosContent('')); + const { needsRealOs, signals } = classifyMacosContent(''); + assert.equal(needsRealOs, false); + assert.deepEqual(signals, []); + }); +}); + +// ─── #4593: CLI --target macos ───────────────────────────────────────────── + +describe('gen-platform-conformance-tier.cjs CLI --target macos (temp fixture tree)', () => { + test('--target macos --check passes when the generated file is fresh', () => { + const tmpDir = createTempDir('gen-platform-conformance-tier-macos-'); + try { + const testsDir = path.join(tmpDir, 'tests'); + fs.mkdirSync(testsDir, { recursive: true }); + fs.writeFileSync(path.join(testsDir, 'flagged.test.cjs'), 'fs.symlinkSync(target, link);\n'); + fs.writeFileSync(path.join(testsDir, 'clean.test.cjs'), "assert.equal(1 + 1, 2);\n"); + const outPath = path.join(tmpDir, 'macos-conformance-tier.generated.cjs'); + + const write = runGen(['--target', 'macos', '--write', '--tests-dir', testsDir, '--out', outPath]); + assert.equal(write.exitCode, 0, write.stderr); + assert.ok(fs.existsSync(outPath)); + + delete require.cache[require.resolve(outPath)]; + const generated = require(outPath); + assert.deepEqual(generated.MACOS_CONFORMANCE_TIER_FILES, ['tests/flagged.test.cjs']); + + const check = runGen(['--target', 'macos', '--check', '--tests-dir', testsDir, '--out', outPath]); + assert.equal(check.exitCode, 0, check.stderr); + assert.match(check.stdout, /ok gen-platform-conformance-tier --target macos/); + } finally { + cleanup(tmpDir); + } + }); + + test('--target macos --check fails and names the drift when the list is stale', () => { + const tmpDir = createTempDir('gen-platform-conformance-tier-macos-'); + try { + const testsDir = path.join(tmpDir, 'tests'); + fs.mkdirSync(testsDir, { recursive: true }); + fs.writeFileSync(path.join(testsDir, 'flagged.test.cjs'), 'fs.symlinkSync(target, link);\n'); + const outPath = path.join(tmpDir, 'macos-conformance-tier.generated.cjs'); + + const write = runGen(['--target', 'macos', '--write', '--tests-dir', testsDir, '--out', outPath]); + assert.equal(write.exitCode, 0, write.stderr); + + fs.writeFileSync(path.join(testsDir, 'new-signal.test.cjs'), "const platforms = ['darwin'];\n"); + + const check = runGen(['--target', 'macos', '--check', '--tests-dir', testsDir, '--out', outPath]); + assert.equal(check.exitCode, 1); + const combined = check.stdout + check.stderr; + assert.match(combined, /tests\/new-signal\.test\.cjs/, 'the drift report must name the new file'); + } finally { + cleanup(tmpDir); + } + }); + + test('a suite-suffixed file is excluded even with a qualifying macOS content signal', () => { + const tmpDir = createTempDir('gen-platform-conformance-tier-macos-suite-'); + try { + const testsDir = path.join(tmpDir, 'tests'); + fs.mkdirSync(testsDir, { recursive: true }); + const signal = 'fs.symlinkSync(target, link);\n'; + fs.writeFileSync(path.join(testsDir, 'foo.test.cjs'), signal); + fs.writeFileSync(path.join(testsDir, 'foo.install.test.cjs'), signal); + const outPath = path.join(tmpDir, 'macos-conformance-tier.generated.cjs'); + + const write = runGen(['--target', 'macos', '--write', '--tests-dir', testsDir, '--out', outPath]); + assert.equal(write.exitCode, 0, write.stderr); + + delete require.cache[require.resolve(outPath)]; + const generated = require(outPath); + + assert.deepEqual( + generated.MACOS_CONFORMANCE_TIER_FILES, + ['tests/foo.test.cjs'], + 'the install-suite file must be excluded even though its content would otherwise qualify', + ); + } finally { + cleanup(tmpDir); + } + }); +}); + +// ─── #4593: real tests/ tree, regression against the committed macOS artifact + +describe('gen-platform-conformance-tier.cjs — real repo tree, macOS target (regression)', () => { + test('real tests/ tree macOS classification matches the committed list', () => { + const realTestsDir = path.join(ROOT, 'tests'); + + let fresh; + assert.doesNotThrow(() => { + fresh = classifyMacosTree(realTestsDir); + }, 'a full sweep of the real tests/ tree must complete without throwing'); + + // Measured 196/930 at authoring time (#4593 design doc). Sanity ballpark + // with headroom for organic test-suite growth, not a brittle exact match. + assert.ok( + fresh.files.length >= 100 && fresh.files.length <= 350, + `expected a real, current, sanity-checked count in [100, 350], got ${fresh.files.length}`, + ); + + delete require.cache[require.resolve(MACOS_GENERATED_PATH)]; + const committed = require(MACOS_GENERATED_PATH); + + assert.equal( + committed.MACOS_CONFORMANCE_TIER_FILES.length, + fresh.files.length, + 'the committed macOS generated file must be fresh — run ' + + '`node scripts/gen-platform-conformance-tier.cjs --target macos --write`', + ); + assert.deepEqual( + committed.MACOS_CONFORMANCE_TIER_FILES.slice().sort(), + fresh.files.slice().sort(), + 'the committed macOS list must match a fresh sweep exactly, not just in length', + ); + }); + + test('the committed macOS conformance-tier list contains zero suite-tagged files', () => { + delete require.cache[require.resolve(MACOS_GENERATED_PATH)]; + const { MACOS_CONFORMANCE_TIER_FILES } = require(MACOS_GENERATED_PATH); + + const suiteTaggedPattern = /\.(install|security|slow|integration|qa)\.test\.cjs$/; + const offenders = MACOS_CONFORMANCE_TIER_FILES.filter((f) => suiteTaggedPattern.test(f)); + + assert.deepEqual( + offenders, + [], + 'suite-tagged files must never appear in the macOS conformance-tier list either', + ); + }); +});