Research 002 — Monorepo wiring
Status: complete
Step 1.5 output, written between the spec draft and the approval gate. Both [NEEDS VERIFICATION] markers in spec.md (V1 on R1, V2 on R5) have a verdict here, so this file is complete. No [NEEDS CLARIFICATION] markers were open.
Verified against, on 2026-07-23: Node v26.5.0 (≥22.18), pnpm 11.15.1, TypeScript 7.0.2, Vitest 4.1.10, @biomejs/biome 2.5.5, @types/node 26.1.1. Evidence comes from a throwaway spike workspace built outside the repo (scratchpad/spike-002/, since discarded per the constitution's second exception) that mirrored the repo's tsconfig shape — nodenext, allowImportingTsExtensions, verbatimModuleSyntax, noEmit — with a package @gw2priory/domain exporting { ".": "./src/index.ts" } and a root test importing it by name.
V1 — Does a workspace package whose exports points at raw .ts resolve under both tsc (TS7, nodenext) and Vitest 4, with no build?
Question. R1 assumes a package can be consumed as TypeScript source — exports → ./src/index.ts, no dist/ — and still resolve by name at both typecheck (tsc --noEmit) and test (Vitest) time.
Verdict. Confirmed, conditional on one thing: the consumer must declare the package as a workspace:* dependency (see F3). With that declaration, source packages need no build step and R1 holds as written.
Evidence.
- Without a
workspace:*dependency on the consumer: no symlink is created (ls node_modules/@gw2priory/→ "No such file or directory"),tsc --noEmitfails withTS2307: Cannot find module '@gw2priory/domain', and Vitest fails withCannot find package '@gw2priory/domain'. The co-located test (relative./index.tsimport) passed regardless. - After adding
"@gw2priory/domain": "workspace:*"to the consumer'sdevDependenciesand re-runningpnpm install: the symlink appears (node_modules/@gw2priory/domain -> ../../packages/domain),tsc --noEmitexits 0, andpnpm exec vitest runreports 2 files / 3 tests passed, including the by-name cross-workspace import intests/resolution.test.ts.
Caveat. The two resolvers succeed by different routes. tsc's nodenext resolution reads the package exports and accepts the .ts target because allowImportingTsExtensions + noEmit are set. Vitest resolves through Vite's own resolver (esbuild transform), independent of Node's runtime type stripping — so this result does not by itself prove node packages/.../foo.ts at runtime, only typecheck and Vitest, which is all R1/R3/R4 require. The fallback named in R1 (exports conditions) was not needed.
V2 — Does Biome's current release run cleanly on Node ≥22.18 with one config, staying independent of the tsc API (TS7-safe)?
Question. R5 makes Biome the sole linter/formatter and rests on it being immune to the TypeScript 7 compatibility risk that couples typescript-eslint to the tsc API.
Verdict. Confirmed. Biome runs cleanly, from one config, and never touches tsc — so TS7 poses no risk to it.
Evidence.
@biomejs/biome2.5.5 installed and ran on Node v26.5.0;biome --version→2.5.5.biome initgenerated a singlebiome.json(schema2.5.5);biome check .checked 9 files in 6 ms.- Independence from
tsc: the installed@biomejs/biomepackage.json reports emptydependenciesandpeerDependencies— it is a bundled Rust binary that parses TypeScript itself. There is no path by which a TypeScript 7 change could break it.
Caveat. biome check . exited non-zero, but every diagnostic was a style default (tab indent, double quotes, import sorting) differing from the repo's current 2-space / single-quote / no-semicolon style — Biome working, not failing. The plan must (a) choose the format config and (b) run one biome check --write pass; expect it to reformat existing files (scripts/new-spec.ts, .vitepress/config.mts, tests/). This is an adoption decision, not a blocker, and it refutes nothing in the spec.
F3 — Cross-workspace consumption requires an explicit workspace:* dependency in the consumer
Not asked about directly, but load-bearing. The apps/* / packages/* glob in pnpm-workspace.yaml makes a package part of the workspace; it does not make it importable elsewhere. pnpm only symlinks a workspace package into a consumer that declares a dependency on it. Importing by name without that declaration fails identically at typecheck (TS2307) and at test time (Cannot find package) — see V1's "without" evidence.
Why it matters. This is the mechanism behind SC1 ("importable by its @gw2priory/* name") and R8 (the root tests/ importing @gw2priory/domain). The plan must add "@gw2priory/domain": "workspace:*" to the root package.json, and later each app that consumes a package must do the same. Nothing in the spec is refuted — R8 stands — but the plan carries this as an explicit step rather than assuming the glob is enough.
Refuted claims
None. Both spec claims were confirmed; F3 adds an implementation requirement for the plan without contradicting any spec statement. The spec does not return to step 1.
Graduation
Graduated at step 6 into a new docs/architecture/monorepo.md (a dedicated doc rather than stack.md, so the additions do not collide with spec 001's R8 verbatim check on stack.md):
- The source-package convention —
packages/*consumed as TypeScript source viaexports→./src/index.ts, no build,private: true— and its precondition that every consumer declares the packageworkspace:*(V1 + F3). ✓ moved. - Biome as the project's linter/formatter, TS7-agnostic, one
biome.json, with the chosen style config (V2). ✓ moved.
CLAUDE.md's Reference section links the new doc.