Proposal (rationale)
WI-073 made module emission a complete preflighted operation, but its portability comparator uses
NFD plus ECMAScript /iu matching. That matcher applies Unicode simple case folding and cannot
expand multi-code-point storage aliases. It therefore treats ß/ss and fi/fi as different
destinations even though default case-insensitive macOS APFS addresses each pair as one inode.
This reaches bundled behavior without an external module. On exact main 9b99350, all three of
these production dry runs exited zero instead of refusing; the emitted-path implementation did not
change between that commit and this branch’s exact green base 6c3b846:
session.path=docs/straße/README.md,session.archive=docs/strasse;session.path=docs/fi/README.md,session.archive=docs/fi;- structural
session.path=docs/ß,session.archive=docs/ss.
A direct planUpgrade/applyUpgrade probe with the bundled session module also accepted both
exact aliases while missing and wrote both outputs. On APFS the second write replaces the first;
a file/descendant alias can instead fail after earlier modules have already written. The accepted
preflight therefore permits the partial or silently collapsed operation it exists to prevent.
Decision
accepted — 2026-09-05, by the user during the Arena Lab bootstrap. Treat F-051
as release-blocking and close the missing portability class before v0.4.0 or an APFS consumer is
allowed to run add or upgrade --apply.
Plan
Requirements
- One deterministic storage key must represent the Windows/default-APFS collision relation used by both managed Git refs and emitted consumer paths.
- Complete-operation preflight must reject exact and file/descendant
ß/ssandfi/fialiases on every host, before direct install or upgrade writes any file. - Planning and application must each validate stored-record paths independently; a caller-supplied upgrade plan cannot bypass the fixed comparison.
- Existing canonical-equivalence, J-caron, sigma, case, containment, reserved-sink and atomicity behavior must remain intact.
Impacts
- The Unicode storage key currently private to
src/concurrency.tsbecomes a small shared source used bysrc/emitted-path.tsfor exact and per-segment ancestry comparison. test/emitted-path.test.jsgains portability and operation-level regressions using the bundledsessionmodule and its configurable live/archive paths.- The pending v0.4.0 changelog records the newly refused alias class.
Approach
Use WI-079’s already-reviewed, locale-independent key: NFKD, lower case, upper case, then NFKD again. Full conversion expands the aliases simple folding misses, and compatibility decomposition keeps the comparison conservative for a plan that may move between filesystems. Keep the key’s contract explicitly scoped to one already-separated segment. Compare exact absolute paths only after splitting on the native separator and requiring the same number of segments; compare ancestors the same way over the shared prefix. This preserves the existing rule that normalization never creates path structure because U+FF3C and U+FF0F decompose to ASCII separators under NFKD.
Move the key into a dependency-free helper rather than copying it. Querying the current filesystem was rejected because both destinations are still missing when preflight matters, and because a plan accepted on Linux must remain safe after checkout on macOS or Windows. Extending the regex with a hand-maintained exception table was rejected because it would encode examples, not the storage relation.
Acceptance criteria / tests
- Unit preflight refuses both candidate orders and both alias orientations for sharp-S and ligature exact and file/descendant pairs; the existing J-caron and sigma regressions still pass. Flat filenames containing fullwidth reverse solidus or fullwidth solidus remain distinct from an ASCII nested path in both candidate orders, with real coexistence exercised where available.
- Real bundled
rungs add sessioninvocations for exact and structural aliases fail before dependencies, session files, gates, render output or the install record are written. - A stored
sessionrecord with either alias family is refused duringplanUpgrade; a forged missing-file plan is independently refused byapplyUpgrade, and each consumer stays empty. - Focused emitted-path tests, full
npm test, every registered Rungs gate, package dry-run and diff checks pass locally; the exact pushed commit is left for independent review and the full six-cell OS/Node plus site CI matrix.
Out of scope
- F-034 CRLF parsing and byte-preserving record updates.
- New path syntax, external module roots, or changes to existing symlink, junction, hard-link and time-of-check boundaries.
- Cutting or publishing v0.4.0, changing Arena Lab, landing this branch, or deleting worktrees.
Execution
Branch feature/WI-080-apfs-emitted-path-collisions was created with rungs session start from
exact verified green/main commit 6c3b846bbbc21d6255db84323db54facc08ddef2. The plan, finding
promotion and epic links were committed first as 36121c9.
Red-first execution reproduced the gap at every intended boundary. The focused file had three
failures: raw preflight accepted the first sharp-S exact pair, stored upgrade planning accepted the
same pair, and the real bundled CLI completed the same session install instead of refusing it.
The other nine tests passed and two file-symlink tests skipped for Windows privilege.
src/storage-key.ts now owns the dependency-free NFKD/lower/upper/NFKD key previously private to
the concurrency loop. Both concurrency.ts and emitted-path.ts use that one comparison, while
emitted-path ancestry remains segment-based. The regression matrix covers sharp-S and ligature
forms in both textual orientations and both candidate orders, exact and structural. The operation
tests exercise eight real session CLI installs and eight stored-record cases; planning and a
forged application each refuse, with every consumer still empty.
Independent review of the first implementation candidate 8d481119 found that its exact-path
comparison passed a whole native path to the compatibility key. NFKD turns U+FF3C into reverse
solidus and U+FF0F into solidus, so the comparison could falsely report a flat filename as the same
destination as a nested path. A red-first Windows regression reproduced the U+FF3C false collision.
The shared helper is now named and documented as segment-only; exact emitted paths split on the
native separator, require equal segment counts, and compare corresponding segments. Both fullwidth
characters are accepted against the nested ASCII spelling in either order, and both pairs coexisted
as separately readable files on the local NTFS volume.
Local verification on 2026-09-05:
- focused
test/emitted-path.test.js: 15 total, 13 pass, 2 expected Windows privilege skips; - full
npm test: 129 total, 126 pass, 3 expected platform/privilege skips, 0 fail; node src/cli.ts check: 30 pass, 0 fail; it separately reports the existing 45 fixtures without builders as not-run rather than passes (F-018);npm pack --dry-run --json: 112 entries, 368,708-byte package, includingsrc/storage-key.ts;git diff --check: clean.
After WI-081 landed, exact main 2c897fd5 was merged into the branch as dca45eb8.
Conflicts were documentation-only; production comparator code merged unchanged. The combined tree
passed the 15 focused cases (13 pass and two expected Windows privilege skips), the full suite
(131 pass and three expected skips), all 30 gates, a 113-entry 376,758-byte package dry-run and
git diff --check. Exact CI run 33976213033 passed the site job and all six OS/Node cells. rungs land then verified 30/30 gates in the merged scratch and atomically advanced main plus
green/main to implementation merge e29313c0.
Review
Self-review on 2026-09-05 found the change remains within the accepted comparator and regression boundary. Criterion 1 is met locally by the exact/structural unit matrix and the retained J-caron and sigma cases, plus the fullwidth-separator non-collision matrix and NTFS coexistence probe. Criterion 2 is met by actual non-dry-run CLI refusal with empty target directories. Criterion 3 is met by independent plan and apply assertions across every alias/shape orientation. The local half of criterion 4 is met by the focused, full, gate, package and diff evidence above.
Independent review rejected the first candidate because whole-path compatibility normalization
could manufacture separators from U+FF3C and U+FF0F. It approved superseding exact tip 7e1df454
after verifying per-segment comparison, both-order separator non-collisions on NTFS, every
sharp-S/ligature orientation, real CLI and stored-plan atomic refusals, focused/full/gate/package
evidence, and exact CI run 33974420656. The later main integration had no production-code conflict
and exact run 33976213033 passed all seven jobs, completing criterion 4.