Skip to content

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.json exports: "." → "./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(...) threw TypeError: Cannot read properties of undefined (reading 'Latest').
  • typescript/unstable/ast exposes ScriptTarget, SyntaxKind, is* guards and a createScanner (tokens), but no standalone parser; a full AST comes only via unstable/syncAPI → Snapshot → Program → getSourceFile, which needs project/tsconfig wiring (a bare new API({cwd}).updateSnapshot() yielded getProjects().length === 0 — no program without more setup).
  • @swc/core@1.15.46 parseSync(src, { syntax:'typescript', decorators:true }) on recipe-graph.controller.ts returned: import ../gw2/gw2.service → { typeOnly:false } for Gw2Service (a value import — what G2 asserts), @gw2priory/recipe-graph → { typeOnly:true }; class decorators ['Controller']; constructor params recipeGraph: RecipeGraphService, gw2: Gw2Service. Per-specifier isTypeOnly is 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.

  1. "parsed with the TypeScript compiler API (ts.createSourceFile)" (R2). Believed: the classic TS compiler API is available to apps/api. True: typescript@7.0.2 is the native port with no top-level compiler API; ts.createSourceFile is undefined (V2). Proposed change: R2's mechanism becomes @swc/core.parseSync ({ syntax:'typescript', decorators:true }) — already an api dependency, already in pnpm-workspace.yaml allowBuilds, and it exposes import typeOnly / per-specifier isTypeOnly, 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/core for programmatic AST work in this repo. Fits docs/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.