Repository navigation
writ segment matching ignores its own suffix grammar: order unvalidated, dimensions unchecked, ties undefined #369
Description
The layer directory grammar is [<project>|common][.<os>][.<distro>][.<arch>][.<custom>...]. cmd/writ/writ/segment/segment.go:18 states the contract on the Segments type — "Order matters: OS, DISTRO, ARCH, then custom segments." Nothing enforces or uses it.
Mechanism
ParseDirName (cmd/writ/writ/segment/segment.go:79) splits on . and returns parts[1:] as a flat []string. Position is discarded at parse time.
Segments.Match (cmd/writ/writ/segment/segment.go:93) then tests each suffix for membership in AllValues() — a single unordered set containing every dimension's value plus the OS family. A suffix matches if it equals any segment's value, regardless of which slot it occupies.
MatchResult.Specificity() (cmd/writ/writ/segment/segment.go:120) is len(m.Suffixes) — a count, blind to dimension.
Consequences
1. Suffix order is not validated. common.arm64.Darwin matches exactly as well as common.Darwin.arm64. Both report specificity 2. One violates the documented grammar and nothing says so.
2. Position does not select a dimension. With a custom segment ROLE=desktop, the directory common.desktop.Darwin matches — a ROLE value sitting in the OS slot, an ARCH value could sit in the DISTRO slot, and so on. Any value from any dimension satisfies any position.
3. Cross-dimension collisions match the wrong thing. Because AllValues() is flat, a custom segment whose value collides with an OS, distro, or arch name silently satisfies a check meant for that other dimension.
4. Specificity cannot rank dimensions, so real ties occur. common.Linux (OS) and common.Debian (DISTRO) are both specificity 1 and both match on a Debian machine — a distro-specific file ties with an OS-generic one. Likewise common.Unix and common.Darwin on macOS, since AllValues adds the OS family (cmd/writ/writ/segment/segment.go:64); Darwin should beat Unix and does not. noblefactor.Unix is a naming form the codebase already documents at cmd/writ/writ/migrate/session.go:316, so this is a live case, not a hypothetical.
5. Ties are resolved by an unstable sort. cmd/writ/writ/segment/matcher.go:61 and cmd/writ/writ/segment/matcher.go:108 use sort.Slice, which is not stable. Among equal-specificity matches there is no guaranteed order. The collision rule in cmd/writ/writ/tree/builder.go:280 resolves same-layer ties with specificity >= existing.specificity — "last processed wins" — and "last" is undefined by contract. Same machine, same files, no specified winner.
That last branch has zero test coverage, confirmed via make cover: builder.go:280.45,282.7 1 0. It is reachable only through a tie, and every existing test avoids ties because MatchDirectories sorts most-specific-first.
Why counting cannot fix this
Specificity-as-a-count can never express that DISTRO is more specific than OS, or that Darwin is more specific than Unix. Enforcing the grammar positionally makes specificity a dimension tuple with a total order, and exact ties then require two identically-named directories — impossible within one directory. Fixing the grammar dissolves the tie-break problem rather than patching it.
Found while reviewing buildMultiSource for the issue #365 phase 1b decomposition. It blocks that work: a behavior-preserving refactor of a predicate whose only reachable branch has no defined behavior is not possible, because there is no behavior to preserve.
Activity
docs: two bugs found during the phase 1b review, and the measured 1b …
lint(migrate): delete a dead complexity directive, argue the live one (…