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

@@ -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) || '' }}

View File

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

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

View File

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

View File

@@ -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 <path> # override the tests/ root (tests only)
* node scripts/gen-platform-conformance-tier.cjs --out <path> # 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,
};

View File

@@ -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",
],
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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',
);
});
});