Research 011 — NestJS conventions & architecture guards
Status: complete
Step 1.5 output, written between the spec draft and the approval gate. Both [NEEDS VERIFICATION] markers in spec.md (R1, R2) have a verdict below. V2 refuted R2 as written — R2 named a mechanism (ts.createSourceFile) absent from this repo's TypeScript — but the goal survives via @swc/core, an already-present parser. The human approved that revision (see Refuted claims); R2 was transcribed and spec.md remains approved, so the plan gate is clear.
Verified against. TypeScript 7.0.2 (the native "tsgo" port) and @swc/core 1.15.46, both resolved from apps/api; Node.js v26.5.0; worktree .claude/worktrees/011-nestjs-conventions (branch 011-nestjs-conventions, base origin/main @ 8e3a40f). All probes run 2026-08-05. Spike code was throwaway and has been discarded (constitution step 1.5). These findings go stale if the api bumps its TypeScript or SWC major.
V1 — Can a Vitest test in apps/api read the src tree from disk? (spec R1, SC10)
Question. R1 says the guard suite reads apps/api/src/**/*.ts from disk, offline, under the api's Vitest config. That requires reliable file discovery and path resolution independent of process.cwd().
Verdict. Confirmed. Node's fs.readdirSync(dir, { recursive: true }) enumerates the source tree, and fs.readFileSync reads each file — no network, no running app. A guard anchors its base path to its own location, so cwd is irrelevant.
Evidence. apps/api/vitest.config.mts:19 sets root: import.meta.dirname (= apps/api/) with include: ['src/**/*.test.ts'] — so *.arch.test.ts under src/ is picked up, and import.meta.dirname inside the test resolves paths deterministically. A spike using readdirSync('<api>/src', {recursive:true}) returned the full non-test .ts list; readFileSync on recipe-graph.controller.ts returned its source. Both are plain Node APIs, unaffected by the SWC transform Vitest applies to the test file itself.
V2 — Is TypeScript 7's compiler API (ts.createSourceFile + AST walking) usable in a Vitest test? (spec R2)
Question. R2 says the scanner parses source "with the TypeScript compiler API (ts.createSourceFile)". The premise is that the classic TS compiler API is available to apps/api at test time.
Verdict. Refuted as written — the classic API does not exist in TS 7. The repo's typescript is 7.0.2, the native (Go) port, whose npm package removed the top-level compiler API: the package's . export is only version.cjs, so ts.createSourceFile / ts.ScriptTarget / ts.SyntaxKind are undefined. A full AST is reachable only through an explicitly unstable, tsconfig/project-based bridge to the native binary, which is heavier and API-unstable. The goal is salvaged without either: @swc/core — already an apps/api dependency — parses TS to an AST that distinguishes type-only imports and exposes decorators and constructor params, covering every guard. R2's mechanism must change from ts.createSourceFile to @swc/core.parseSync; the requirement (a pure AST-based scanner, offline, no new dependency) stands and is in fact strengthened.
Evidence.
typescript@7.0.2/package.jsonexports:"." → "./lib/version.cjs"; the real surface is under subpaths./unstable/sync,./unstable/ast,./unstable/ast/is, … (all labelled unstable).require('typescript').ScriptTarget→undefined;ts.createSourceFile(...)threwTypeError: Cannot read properties of undefined (reading 'Latest').typescript/unstable/astexposesScriptTarget,SyntaxKind,is*guards and acreateScanner(tokens), but no standalone parser; a full AST comes only viaunstable/syncAPI → Snapshot → Program → getSourceFile, which needs project/tsconfig wiring (a barenew API({cwd}).updateSnapshot()yieldedgetProjects().length === 0— no program without more setup).@swc/core@1.15.46parseSync(src, { syntax:'typescript', decorators:true })onrecipe-graph.controller.tsreturned: import../gw2/gw2.service→{ typeOnly:false }forGw2Service(a value import — what G2 asserts),@gw2priory/recipe-graph→{ typeOnly:true }; class decorators['Controller']; constructor paramsrecipeGraph: RecipeGraphService,gw2: Gw2Service. Per-specifierisTypeOnlyis also present. This is exactly the data G1–G7 need.
Caveat. SWC's AST node shapes (Constructor, TsTypeReference, ImportDeclaration.typeOnly) are SWC's, not TypeScript's — the scanner's queries are written against SWC's schema and pinned by source-model.ts's own unit test (spec R2/SC8), so an SWC shape change is caught there.
F3 — TypeScript 7 is the native port; the classic compiler API is gone (touches R2; graduation)
Why it matters. Any later spec that reaches for ts.createSourceFile, ts.createProgram, or the typescript default export will hit the same wall this one did. The durable fact: in this repo, typescript (7.x) is typecheck-and-unstable-API only; for programmatic AST work, use @swc/core (already present for the api build). Graduation candidate for docs/architecture/typescript.md.
F4 — The worktree has no installed per-package deps yet (touches step 4/5 prerequisites)
Why it matters. @swc/core (and vitest) are not resolvable from the worktree — a require('@swc/core') based in .claude/worktrees/011-.../apps/api threw MODULE_NOT_FOUND. Only the repo-wide, root-hoisted typescript resolved (via parent-dir walk into the main tree's node_modules). node_modules is per-working-tree and gitignored, so pnpm install must be run in this worktree before the guard suite (or any api test) can run in step 4/5. Not a spec change; a prerequisite to record so verification doesn't fail spuriously on a missing install.
Refuted claims
Recorded rather than silently patched, per the workflow. Unlike a draft-stage refutation, spec.md here was already approved — so it was not folded in unilaterally. It was surfaced to the human, who approved the @swc/core revision on 2026-08-05 (choosing it over the TS 7 unstable-API and new-dependency alternatives); R2 was then transcribed and spec.md remains approved.
- "parsed with the TypeScript compiler API (
ts.createSourceFile)" (R2). Believed: the classic TS compiler API is available toapps/api. True:typescript@7.0.2is the native port with no top-level compiler API;ts.createSourceFileisundefined(V2). Proposed change: R2's mechanism becomes@swc/core.parseSync({ syntax:'typescript', decorators:true }) — already an api dependency, already inpnpm-workspace.yamlallowBuilds, and it exposes importtypeOnly/ per-specifierisTypeOnly, decorators, and constructor param types (the exact needs of G1–G7). The scanner file name/role and every other requirement are unchanged; SC8 (scanner's own unit test) already guards the SWC-shaped queries. This is a mechanism correction that removes a false dependency, not a collapse of the premise — but it edits an approved requirement, so the human re-approves.
Graduation
Candidates to move to docs/architecture/ at step 6 (they outlive this feature; the next spec should not re-derive them):
- F3 — TypeScript 7 is the native port; no classic compiler API; use
@swc/corefor programmatic AST work in this repo. Fitsdocs/architecture/typescript.md. - The convention set itself is already destined for
docs/architecture/nestjs.md(spec R10) — not a research graduation, just noted so the two aren't duplicated.