docs(#1744): portability architecture guide + ADR-1703 acceptance (Phase 7 closeout) (#1745)

ADR-1703 Phase 7 / epic #1702 closeout. The mechanical teardown is already
complete across phases 1-6 (regex scanner retired P3, ratchet deleted P4,
allowlist portability-usage gone, windows-portability-ok comments swept,
every DEFECT.WINDOWS-* predicate rewritten per-phase). This phase delivers the
two remaining ADR-mandated closeout items:

- docs/contributing/adding-a-portability-rule.md: the forward architecture
  recipe (Diátaxis Explanation) — the five seams (rule / portability-vocab /
  platform-guard / disable-ban / ci-test-scope), the zero-escape-hatch contract,
  the shipped-rule catalog, and the step-by-step checklist for adding a new
  local/* portability rule.

- docs/adr/1703: Status Proposed -> Accepted (all seven phases shipped); a
  Phase 7 as-built note recording the two deviations from the Phase 0 catalog —
  Phase 6's rename-only scope decision (#1740) and the codex-review precision
  tightening on require-fs-op-fallback.

- docs/contributing/cross-platform-portability-rules.md: cross-link to the new
  guide from the rule reference.

No code, no test, no runtime change. Epic #1702 Phase 6 box checked; Phase 7
box + epic closure to follow at PR merge.

Closes #1744

Co-authored-by: review-bot <review-bot@gsd>
This commit is contained in:
Tom Boucher
2026-06-26 00:19:44 -04:00
committed by GitHub
parent 871621c3c8
commit 83685ea7a3
3 changed files with 182 additions and 2 deletions

View File

@@ -1,7 +1,7 @@
# ADR-1703: Cross-platform portability enforcement as AST ESLint rules
- **Status:** Proposed
- **Date:** 2026-06-25
- **Status:** Accepted
- **Date:** 2026-06-25 (Phase 0); **Accepted 2026-06-26** (Phase 7 closeout — all phases shipped)
- **Issue:** [#1703](https://github.com/open-gsd/gsd-core/issues/1703) — Phase 0 of epic [#1702](https://github.com/open-gsd/gsd-core/issues/1702)
- **Supersedes:** the regex-based `scripts/lint-windows-test-portability.cjs`, the
`tests/windows-test-parity-guard.test.cjs` named-set ratchet (G1–G6), the
@@ -186,6 +186,32 @@ Each implementation phase runs the full engineering directive (rubber-duck → l
→ qa-test-architect → strict TDD via `RuleTester` → codex adversarial → Diátaxis → rebase+PR)
and is its own approved child issue + PR under epic #1702.
## Phase 7 — as-built / acceptance (2026-06-26)
All seven phases shipped; the architecture is exercised in production and accepted. Two
as-built deviations from the Phase 0 catalog, both within this ADR's precision discipline:
- **Phase 6 scope — `require-fs-op-fallback` narrowed to rename.** The catalog row named
`DEFECT.WINDOWS-FS-OPS` for "`src/**/*.cts`, build/install". The defect's own `.fix-forward`
defines the cure as *"catch EPERM/EBUSY/EACCES, fall back to copy + unlink with retry"* — so
`copyFile`/`unlink` are the **fallback primitives**, not separate defect sites, and flagging
them would flag the cure (`unlink` also has ~30 intentional best-effort cleanup sites that would
be a FP minefield). v1 recognition is therefore `fs.rename`/`fs.renameSync` only, with the
`RENAME_RETRY_ERRNOS` retry loop as the recognized compliant shape; `copyFile`/`unlink`
transient-lock sub-classes are documented for a possible follow-up. The ADR-mandated glob
expansion to `bin/install.js` + `scripts/build-hooks.js` (L124-126) landed as specified.
Documented on [#1740](https://github.com/open-gsd/gsd-core/issues/1740).
- **Phase 6 precision tightening (codex review).** The rule's compliance shape was tightened after
an adversarial gpt-5.5 review: a catch must BOTH reference a transient errno AND carry a retry
signal (a loop `continue` backedge or a `return <call>` delegation — NOT a bare rethrow), and
only the **nearest catching** try/catch counts (an outer errno-catch is unreachable once an inner
catch intercepts). This enforces the defect's *"never silently swallow"* + cure-is-retry clauses
honestly. See [`eslint-rules/require-fs-op-fallback.cjs`](../../eslint-rules/require-fs-op-fallback.cjs).
The forward "how to add a portability rule" recipe delivered by this phase lives at
[`docs/contributing/adding-a-portability-rule.md`](../contributing/adding-a-portability-rule.md).
## Alternatives considered
1. **Keep extending the regex lint.** Rejected — the adversarial review proved it is