Skip to content

fix: Phase A workspace-architecture defects, round two - #37

Merged
Mearman merged 12 commits into
mainfrom
fix/workspace-architecture-defects
Sep 26, 2026
Merged

Mearman merged 12 commits into
mainfrom
fix/workspace-architecture-defects

Conversation

@Mearman

@Mearman Mearman commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

Fixes the defects an adversarial review found in the workspace-architecture rules from PR #35, on a fresh branch off main since #35 had already merged and released (2.21.1) by the time this work started.

A second adversarial review then found six further defects in this PR's own diff, fixed in the later commits below:

  • workspace-glob.ts: resolveWorkspacePackageDirs no longer throws when a positive packages glob currently matches no directory (an empty group awaiting its first member); it resolves to no packages instead, the same as pnpm itself. The throw was breaking real consumers, including exchange-platform's own pnpm-workspace.yaml globs over groups that today hold only a README. segmentToRegExp's character-class close search is also simplified (indexOf from bodyStart + 1 unconditionally, since it can never match a character that isn't there).

  • workspace-graph.ts: manifestRelativeDir now compares root and the linted file's own directory through fs.realpathSync before taking their relative path, so an explicit root option and ESLint's context.filename spelling the same real directory two different ways (a symlink, macOS /tmp vs /private/tmp) no longer makes every workspace-architecture rule silently report nothing. deriveRank's nameRanks and deriveSlice's namePrefix branch now match against a package's genuine declared name only, never against the relativeDir fallback its graph identity key uses when it declares none. readDeclaredManifest no longer wraps the fs.readFileSync call in the same try as JSON.parse, so a real read failure is never misreported as a JSON parse error.

  • package-name-mirrors-path.ts: a package that declares no name at all is now reported (a new missingName message), rather than silently skipped, since it plainly cannot mirror its path either.

  • workspace-options.ts / workspace-graph.ts: readDeclaredManifest and validateRankRulePatterns narrow their caught errors through a new shared assertIsError guard and thread the original error through cause, instead of a String(error) fallback for a non-Error throw neither call site can actually produce. The redundant typeof value === 'number' guard ahead of Number.isInteger in isInteger is removed.

  • README: documents each naming strategy's exact segment derivation (drop-group, keep-group, basename), since this PR changes keep-group's own semantics (only the group's own last path segment is kept, not every container segment above it), and documents the empty-glob and nameless-package behaviour above.

  • Added regression tests throughout, including one per rule for a nested object with no name of its own resolving, via the relativeDir fallback, to the same graph entry as the real top-level manifest (the new relativeDir confirmation had started masking this case).

  • A further pair of otherwise-unreachable assertIsError call-site contexts (in readDeclaredManifest and validateRankRulePatterns) are extracted into their own named functions in workspace-errors.ts (jsonParseContext, regExpConstructorContext) and tested directly, since neither call site's assertIsError throw is ever reachable through a real JSON.parse or RegExp failure.

pnpm lint, pnpm typecheck, pnpm test and pnpm build all pass; CI is green. pnpm test:mutation (stryker.config.ts, threshold 100%) passes clean.

resolveWorkspacePackageDirs previously matched nothing for a "packages"
glob written with a "./" prefix or a "."/".." segment, since the
matcher walked pnpm-workspace.yaml's raw, unnormalised segments;
segments are now normalised the way path.posix.normalize would before
matching, and a positive glob that still resolves to zero directories
throws, naming the pattern, rather than silently linting nothing.
listRealSubdirectories now excludes bower_components as well as
node_modules, matching the installed pnpm binary's own exclusion list
(the header comment's "resolves through fast-glob" claim is corrected
against that same binary, a compiled Rust executable with no such
library in it). segmentToRegExp now takes a "]" immediately after "["
or a negation marker as a literal class member instead of an empty
class, and honours a backslash as an escape for the character after
it.
readDeclaredManifest previously treated a package.json with no usable
"name" the same as one that fails to parse at all, so buildWorkspaceGraph
dropped it from the graph entirely; pnpm allows a workspace package to
omit "name", so such a package is now kept and keyed by its own
relativeDir, letting its outgoing dependencies still be checked instead
of silently skipped. readDeclaredManifest also now wraps JSON.parse and
names the manifest's own path in the thrown error, rather than
surfacing a bare SyntaxError with no indication of which file. Adds
manifestRelativeDir, the shared helper the three workspace-architecture
rules use to confirm a linted manifest against the graph entry they
resolved it from.
…graph entry

no-uphill-dependency, no-dependency-cycle and package-name-mirrors-path
each identified "self" purely by the manifest's own declared name, so a
stale or duplicated copy of a real package's package.json elsewhere in
the tree (a build output directory that copied it verbatim, say) was
checked as that same real package: reporting edges the copy never
declared, or double-reporting a real cycle once per copy. Each rule now
also confirms, via manifestRelativeDir, that context.filename's own
directory is the resolved graph entry's own relativeDir, and skips a
mismatch instead of reporting it. no-uphill-dependency and
no-dependency-cycle also now fall back to identifying self by directory
when a manifest declares no name at all, matching how buildWorkspaceGraph
now keys such a package, so its own dependencies are checked rather
than silently skipped.
…segment

expectedPackageName's 'keep-group' strategy kept every segment of a
package's relativeDir, including container directories above a
group's own nested path. A group declared with a multi-segment path
(such as { path: 'packages/test' }) derived a name prefixed with
every one of those segments instead of just its own; it now keeps
only the group's own path/name segment (the last one, for a nested
path) ahead of the remaining segments, matching what a group with no
nested path already produced.
rankSkip.maxDistance accepted a negative or fractional value (a
negative maxDistance flags every same-rank dependency as a rankSkip
violation), isolatedGroups accepted a pair naming the same group
twice (turning every intra-group dependency into a false violation),
nameRanks patterns were never checked for validity until deriveRank
compiled one deep inside a lint run, and rank/defaultRank/exemptRanks
accepted a non-integer number. readWorkspaceArchitectureOptions and
the JSON schema now reject each of these: maxDistance and every rank
field require an integer (maxDistance also non-negative), an
isolatedGroups pair must name two distinct groups, and every
nameRanks pattern is compiled up front, naming the option and the
pattern in the error rather than surfacing a bare "Invalid regular
expression" later with no indication of which option produced it.
…ectory

A positive "packages" glob matching zero directories (an empty group
awaiting its first member) now resolves to no packages, the same as
pnpm itself, instead of throwing and aborting every workspace-architecture
rule. This was breaking real consumers whose workspace declares a glob
over a group with no members yet (a freshly scaffolded vertical, a group
holding only a README).

Also simplifies segmentToRegExp's character-class close search: the
"]" immediately after bodyStart is always searched for one position past
bodyStart regardless of whether that position holds a literal "]", since
indexOf can never match a character that is not there. Adds tests for
consecutive leading ".." segments and a "]" immediately after a "^"
negation marker.
An assertion function narrowing a caught unknown value back to Error in
place, for the two call sites (JSON.parse in workspace-graph.ts, the
RegExp constructor in workspace-options.ts) that only ever throw a real
Error on failure. It asserts rather than renaming the value specifically
so a re-thrown error's own "cause" can still reference the exact same
identifier the catch clause bound, which this package's own
preserve-caught-error lint rule requires.
…ss-package matching

manifestRelativeDir now resolves both the workspace root and the linted
file's own directory through fs.realpathSync before comparing. An
explicit "root" option and ESLint's own context.filename can spell the
identical real directory two different ways (a symlink anywhere on
either path, macOS /tmp vs /private/tmp being the recurring real case),
and a purely lexical relative() between the two spellings computed a
path nowhere near the graph entry's own relativeDir, silently making
every workspace-architecture rule report nothing for a correctly
configured package. WorkspaceFs gains a realpathSync seam for this.

deriveRank and deriveSlice's own 'namePrefix' branch now skip nameRanks
and sliceByNamePrefix respectively for a package with no declared name
at all (pnpm allows omitting it): both match a pattern against the
package's own declared name, never against the relativeDir fallback
collectCandidates uses as its graph identity key, so a nameless package
falls straight through to its group's rank/slice instead of accidentally
matching a pattern against its own directory path.

readDeclaredManifest now narrows its caught JSON.parse error via
assertIsError rather than a ternary String(error) fallback, and moves
the manifest read itself outside the try so a real read failure (EACCES,
a race that removes the file) is never misreported as a JSON parse error.
…'u' flag and cause

validateRankRulePatterns now narrows its caught RegExp SyntaxError via
assertIsError rather than a ternary String(error) fallback, matching
readDeclaredManifest's own fix. Adds a test compiling a nameRanks
pattern that is valid without the 'u' flag but invalid with it (an
unnecessary "\-" escape), which deriveRank itself later compiles with
that same flag, and a test asserting the thrown error's own "cause".

isInteger drops its redundant "typeof value === 'number'" guard:
Number.isInteger already returns false for any non-number input, so the
guard could only ever agree with what it already decides.
…rrors-path

Each rule factory now takes a bundled deps object ({ loadGraph?, fs? })
instead of two trailing optional positional parameters, satisfying this
package's own prefer-options-object-param rule, and threads a WorkspaceFs
through to manifestRelativeDir for its realpath resolution.

package-name-mirrors-path no longer returns early for a manifest with no
declared "name" at all: it is looked up in the graph by its own
relativeDir, the same fallback buildWorkspaceGraph itself uses, and
reported under a new "missingName" message id, since a package that
declares no name plainly cannot mirror its path either.

Adds a regression test to each of the three rules for a nested object
with no name of its own resolving, via that same relativeDir fallback,
to the exact same graph entry as the real top-level manifest: the new
relativeDir confirmation had started masking the existing "nested real
name" tests, since a mismatched relativeDir alone was enough to skip a
nested object even with the top-level parent.type === 'Document' guard
bypassed.
…dling

The groups.naming field's own three strategies (drop-group, keep-group,
basename) now state their exact segment derivation, since this PR
changes keep-group's own semantics (only the group's own last path
segment is kept, not every container segment above it). Also documents
that an empty positive packages glob resolves to no packages rather than
throwing, that nameRanks skips a nameless package entirely, and that
package-name-mirrors-path reports one too.
…y testable

readDeclaredManifest and validateRankRulePatterns each pass a template-literal
context string to assertIsError, but assertIsError's own throw is unreachable
at both call sites: JSON.parse and the RegExp constructor only ever throw a
real Error, never a non-Error value, so no test exercising the real call path
can observe which context string was passed. Extracts each into its own
named function (jsonParseContext, regExpConstructorContext) in
workspace-errors.ts and asserts their exact output directly, the same
"Unreachable, tested directly" treatment this package already gives
requireChar/findDependencyEntry/findGroupSpec/last.
@Mearman
Mearman marked this pull request as ready for review September 26, 2026 22:51
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ✅ Completed 2026-09-26T22:56:25.193762Z 8ae3a4d Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Mearman
Mearman merged commit 20f4af7 into main Sep 26, 2026
6 checks passed
@Mearman
Mearman deleted the fix/workspace-architecture-defects branch September 26, 2026 22:52
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 2.22.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant