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 <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-09-10 14:37:15 -04:00
committed by GitHub
parent 557a0b2876
commit 1e47560e34
27 changed files with 719 additions and 23 deletions

View File

@@ -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.

View File

@@ -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