feat(#72): SPI v1 slice 3b-ii — NestJS QoL (guards/pipes/injects)#99
Conversation
📝 WalkthroughWalkthroughThis PR adds a workspace-level token-binding pre-pass to NestJS framework detection. The SPI builder now collects provider tokens upfront, threads the token map through per-file framework detection, and emits constructor injection edges, ChangesNestJS Token Binding & Decorator Framework
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Stacks the NestJS quality-of-life additions on top of slice 3b base:
* `injects` edges from the class symbol of any nest_module / nest_
controller / nest_provider class. Constructor parameters typed as a
class resolve through ts.TypeChecker (high confidence).
* `injects` edges sourced from `@Inject('TOKEN')` parameter decorators.
A workspace-wide token map is built once before the per-file pass
from every `@Module` provider list's `useClass` / `useExisting`
bindings; tokens with `useValue` / `useFactory` (no class target)
are intentionally absent and produce a diagnostic when injected.
Resolved string-token injections emit medium-confidence edges.
* `guards` / `intercepts` / `pipes` edges from the matching
`@UseGuards` / `@UseInterceptors` / `@UsePipes` decorators. Class-
level decorators emit from the controller class symbol (apply to
all routes); method-level decorators emit from the route method
symbol. Both positional `@UseGuards(A, B)` and array
`@UseGuards([A, B])` forms are supported.
* Dynamic Module shapes — `Module.forRoot(...)` / `forRootAsync(...)`
/ `forFeature(...)` / `forFeatureAsync(...)` / `register(...)` /
`registerAsync(...)` — now resolve the receiver class and emit a
low-confidence module_imports edge plus a specific
`spi.nest.module-metadata.dynamic` diagnostic recording that the
runtime providers list could not be enumerated. The 3b base test
that asserted no edge for forRoot is updated to expect the new
low-confidence behavior.
Every gap that cannot be resolved (untyped parameters, value-only
tokens, instance-literal `@UseGuards(new Foo())`, computed metadata
arrays) emits an info-level SpiDiagnostic so downstream consumers can
audit coverage instead of silently missing edges.
Default `extractor_version` bumps to `spi-v1.0.0-slice-3b-ii`. The
framework-decorator pass still piggybacks on the existing ts.Program
created for the call/type layers, plus one workspace-level pre-scan
to gather token bindings.
The slice 3b-ii rewrite of framework-nestjs.ts inherited the same unreachable inner-`if` branch CodeRabbit flagged on 3b base (#98). The fix is identical: drop the dead block, simplify the loop so any identifier-named method declaration unconditionally bumps the overload counter before the route-decorator check decides whether to emit an edge. Mirrors the regression test from 3b base so 3b-ii independently verifies that the controller_route edge targets the implementation's `#2` overload id when the route method is overloaded. When 3b base lands on main, this commit's framework-nestjs.ts fix becomes a no-op against the rebased base; the test addition still applies cleanly because 3b-ii already owns this test file.
183294f to
2cb1a29
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/pipeline/spi/framework-nestjs.ts`:
- Around line 126-140: collectNestTokenMap currently relies on the incoming
opts.sourceFiles order so "last binding wins" is non-deterministic across
platforms; make the resolution deterministic by normalizing traversal order
before registering providers (e.g., sort opts.sourceFiles by a stable key such
as sourceFile.fileName) or by collecting all provider entries first and then
resolving duplicates with a deterministic rule; update collectNestTokenMap to
iterate a sorted list (using sourceFile.fileName) or to deduplicate via a stable
sort/merge step before calling registerProviderTokens, referencing
collectNestTokenMap, registerProviderTokens, collectNestBindings, decoratorsOf,
and decoratorIdentifierName to locate the relevant logic.
- Around line 375-388: resolveDynamicModuleReceiver currently only accepts a
bare identifier receiver and skips namespace-qualified receivers like
Nest.TypeOrmModule.forRoot(); update it to accept ts.PropertyAccessExpression
receivers by extracting the rightmost identifier (e.g., receiver.name for a
PropertyAccessExpression, and walking nested PropertyAccessExpressions if
needed) and pass that identifier to resolveStaticClassFromIdentifier (keep using
ctx.checker and ctx.pathToFileId). Ensure you still verify the extracted token
is an Identifier before calling resolveStaticClassFromIdentifier and return the
same { classSymbolId, factoryName } shape when found.
- Around line 537-538: The current code uses classDecl.members.find(...) to grab
a constructor which may pick an overload signature instead of the
implementation; change the selection of ctor (the constructor declaration) to
explicitly choose the implementation constructor (the one with a body) rather
than the first match—e.g., filter members by ts.isConstructorDeclaration and
then pick the one with a .body (or iterate members in reverse to find the first
constructor with a body) before reading injected params so injected edges are
derived from the real implementation constructor.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: eb5fff7e-e609-425a-a23b-91fb17a9addc
📒 Files selected for processing (4)
src/pipeline/spi/build.tssrc/pipeline/spi/framework-nestjs.tssrc/pipeline/spi/index.tstests/unit/spi-framework-nestjs.test.ts
| export function collectNestTokenMap(opts: CollectNestTokenMapOptions): NestTokenMap { | ||
| const tokens: NestTokenMap = new Map() | ||
| for (const sourceFile of opts.sourceFiles) { | ||
| const bindings = collectNestBindings(sourceFile) | ||
| if (bindings.module.size === 0) continue | ||
| for (const stmt of sourceFile.statements) { | ||
| if (!ts.isClassDeclaration(stmt) || !stmt.name) continue | ||
| for (const decorator of decoratorsOf(stmt)) { | ||
| const name = decoratorIdentifierName(decorator) | ||
| if (!name || !bindings.module.has(name)) continue | ||
| registerProviderTokens(decorator, opts, tokens) | ||
| } | ||
| } | ||
| } | ||
| return tokens |
There was a problem hiding this comment.
Stabilize duplicate-token resolution across platforms.
collectNestTokenMap() says "last binding wins", but today that winner depends on the incoming sourceFiles order. In the builder, that order comes from recursive directory traversal, so duplicate provide keys can resolve to different classes on Ubuntu/macOS/Windows and make injects edges nondeterministic.
Suggested fix
export function collectNestTokenMap(opts: CollectNestTokenMapOptions): NestTokenMap {
const tokens: NestTokenMap = new Map()
- for (const sourceFile of opts.sourceFiles) {
+ const sourceFiles = [...opts.sourceFiles].sort((a, b) => a.fileName.localeCompare(b.fileName))
+ for (const sourceFile of sourceFiles) {
const bindings = collectNestBindings(sourceFile)
if (bindings.module.size === 0) continue
for (const stmt of sourceFile.statements) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function collectNestTokenMap(opts: CollectNestTokenMapOptions): NestTokenMap { | |
| const tokens: NestTokenMap = new Map() | |
| for (const sourceFile of opts.sourceFiles) { | |
| const bindings = collectNestBindings(sourceFile) | |
| if (bindings.module.size === 0) continue | |
| for (const stmt of sourceFile.statements) { | |
| if (!ts.isClassDeclaration(stmt) || !stmt.name) continue | |
| for (const decorator of decoratorsOf(stmt)) { | |
| const name = decoratorIdentifierName(decorator) | |
| if (!name || !bindings.module.has(name)) continue | |
| registerProviderTokens(decorator, opts, tokens) | |
| } | |
| } | |
| } | |
| return tokens | |
| export function collectNestTokenMap(opts: CollectNestTokenMapOptions): NestTokenMap { | |
| const tokens: NestTokenMap = new Map() | |
| const sourceFiles = [...opts.sourceFiles].sort((a, b) => a.fileName.localeCompare(b.fileName)) | |
| for (const sourceFile of sourceFiles) { | |
| const bindings = collectNestBindings(sourceFile) | |
| if (bindings.module.size === 0) continue | |
| for (const stmt of sourceFile.statements) { | |
| if (!ts.isClassDeclaration(stmt) || !stmt.name) continue | |
| for (const decorator of decoratorsOf(stmt)) { | |
| const name = decoratorIdentifierName(decorator) | |
| if (!name || !bindings.module.has(name)) continue | |
| registerProviderTokens(decorator, opts, tokens) | |
| } | |
| } | |
| } | |
| return tokens | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pipeline/spi/framework-nestjs.ts` around lines 126 - 140,
collectNestTokenMap currently relies on the incoming opts.sourceFiles order so
"last binding wins" is non-deterministic across platforms; make the resolution
deterministic by normalizing traversal order before registering providers (e.g.,
sort opts.sourceFiles by a stable key such as sourceFile.fileName) or by
collecting all provider entries first and then resolving duplicates with a
deterministic rule; update collectNestTokenMap to iterate a sorted list (using
sourceFile.fileName) or to deduplicate via a stable sort/merge step before
calling registerProviderTokens, referencing collectNestTokenMap,
registerProviderTokens, collectNestBindings, decoratorsOf, and
decoratorIdentifierName to locate the relevant logic.
| function resolveDynamicModuleReceiver( | ||
| expr: ts.Expression, | ||
| ctx: DetectNestFrameworkContext, | ||
| ): { classSymbolId: string; factoryName: string } | null { | ||
| if (!ts.isCallExpression(expr)) return null | ||
| const callee = expr.expression | ||
| if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.name)) return null | ||
| const factoryName = callee.name.text | ||
| if (!DYNAMIC_MODULE_FACTORY_METHODS.has(factoryName)) return null | ||
| const receiver = callee.expression | ||
| if (!ts.isIdentifier(receiver)) return null | ||
| const id = resolveStaticClassFromIdentifier(receiver, ctx.checker, ctx.pathToFileId) | ||
| if (!id) return null | ||
| return { classSymbolId: id, factoryName } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/pipeline/spi/framework-nestjs.ts | sed -n '375,388p'Repository: mohanagy/graphify-ts
Length of output: 832
🏁 Script executed:
rg -n "resolveStaticClassReference" src/pipeline/spi/framework-nestjs.tsRepository: mohanagy/graphify-ts
Length of output: 245
🏁 Script executed:
rg -n "resolveStaticClassFromIdentifier" src/pipeline/spi/framework-nestjs.tsRepository: mohanagy/graphify-ts
Length of output: 470
🏁 Script executed:
rg -n "function resolve|const resolve" src/pipeline/spi/framework-nestjs.ts | head -20Repository: mohanagy/graphify-ts
Length of output: 305
🏁 Script executed:
cat -n src/pipeline/spi/framework-nestjs.ts | sed -n '618,632p'Repository: mohanagy/graphify-ts
Length of output: 595
🏁 Script executed:
cat -n src/pipeline/spi/framework-nestjs.ts | sed -n '633,650p'Repository: mohanagy/graphify-ts
Length of output: 945
🏁 Script executed:
cat -n src/pipeline/spi/framework-nestjs.ts | sed -n '325,330p'Repository: mohanagy/graphify-ts
Length of output: 317
🏁 Script executed:
cat -n src/pipeline/spi/framework-nestjs.ts | sed -n '489,495p'Repository: mohanagy/graphify-ts
Length of output: 384
Handle namespace-qualified dynamic module receivers too.
This only resolves SomeModule.forRoot(). A namespace import like Nest.TypeOrmModule.forRoot() is still statically recoverable, but this path drops the new low-confidence edge because it requires the receiver to be a bare identifier.
Suggested fix
function resolveDynamicModuleReceiver(
expr: ts.Expression,
ctx: DetectNestFrameworkContext,
): { classSymbolId: string; factoryName: string } | null {
if (!ts.isCallExpression(expr)) return null
const callee = expr.expression
if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.name)) return null
const factoryName = callee.name.text
if (!DYNAMIC_MODULE_FACTORY_METHODS.has(factoryName)) return null
- const receiver = callee.expression
- if (!ts.isIdentifier(receiver)) return null
- const id = resolveStaticClassFromIdentifier(receiver, ctx.checker, ctx.pathToFileId)
+ const id = resolveStaticClassReference(callee.expression, ctx)
if (!id) return null
return { classSymbolId: id, factoryName }
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pipeline/spi/framework-nestjs.ts` around lines 375 - 388,
resolveDynamicModuleReceiver currently only accepts a bare identifier receiver
and skips namespace-qualified receivers like Nest.TypeOrmModule.forRoot();
update it to accept ts.PropertyAccessExpression receivers by extracting the
rightmost identifier (e.g., receiver.name for a PropertyAccessExpression, and
walking nested PropertyAccessExpressions if needed) and pass that identifier to
resolveStaticClassFromIdentifier (keep using ctx.checker and ctx.pathToFileId).
Ensure you still verify the extracted token is an Identifier before calling
resolveStaticClassFromIdentifier and return the same { classSymbolId,
factoryName } shape when found.
| const ctor = classDecl.members.find((m): m is ts.ConstructorDeclaration => ts.isConstructorDeclaration(m)) | ||
| if (!ctor) return |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd /repo && find . -name "framework-nestjs.ts" -type fRepository: mohanagy/graphify-ts
Length of output: 120
🏁 Script executed:
cat -n src/pipeline/spi/framework-nestjs.ts | sed -n '520,560p'Repository: mohanagy/graphify-ts
Length of output: 1552
🏁 Script executed:
cat -n src/pipeline/spi/framework-nestjs.ts | sed -n '530,550p'Repository: mohanagy/graphify-ts
Length of output: 941
🏁 Script executed:
cat package.json | grep -A 2 -B 2 '"typescript"'Repository: mohanagy/graphify-ts
Length of output: 307
🏁 Script executed:
# Check if there are any test files or examples showing constructor overloads
grep -r "constructor.*overload\|constructor.*signature" . --include="*.ts" --include="*.js" -l | head -5Repository: mohanagy/graphify-ts
Length of output: 46
🏁 Script executed:
cat -n src/pipeline/spi/framework-nestjs.ts | sed -n '500,600p'Repository: mohanagy/graphify-ts
Length of output: 4142
🏁 Script executed:
find . -name "tsconfig.json" -o -name "tsconfig*.json" | head -3Repository: mohanagy/graphify-ts
Length of output: 137
🏁 Script executed:
cat tsconfig.json | grep -A 5 -B 5 "target\|lib"Repository: mohanagy/graphify-ts
Length of output: 365
🏁 Script executed:
# Check if findLast is used anywhere in the codebase
grep -r "findLast" . --include="*.ts" --include="*.js" | head -5Repository: mohanagy/graphify-ts
Length of output: 375
🏁 Script executed:
# Verify TypeScript constructor overload behavior and check for test cases
grep -r "constructor" src/pipeline/spi/framework-nestjs.ts -A 2 -B 2Repository: mohanagy/graphify-ts
Length of output: 671
🏁 Script executed:
# Look for any test files related to this function
find . -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "emitConstructorInjects\|framework-nestjs" 2>/dev/null | head -3Repository: mohanagy/graphify-ts
Length of output: 46
🏁 Script executed:
# Check the broader context to understand how parameter decorators work
grep -n "decoratorsOf\|injectTokenFromParameter" src/pipeline/spi/framework-nestjs.ts | head -10Repository: mohanagy/graphify-ts
Length of output: 525
🏁 Script executed:
# Check if there's a helper or polyfill for findLast
grep -r "findLast\|Array.prototype.findLast" . --include="*.ts" --include="*.js" -B 2 -A 2Repository: mohanagy/graphify-ts
Length of output: 826
🏁 Script executed:
# Look for any usages of constructor with multiple signatures in test data or examples
find . -type f \( -name "*.ts" -o -name "*.js" \) -exec grep -l "constructor.*\|.*constructor" {} \; | xargs grep -l "@Injectable\|@Controller\|@Module" | head -3Repository: mohanagy/graphify-ts
Length of output: 184
🏁 Script executed:
# Check the TypeScript AST behavior - look at how constructors are handled elsewhere in the codebase
grep -n "ts.isConstructorDeclaration\|ConstructorDeclaration" src/pipeline/spi/framework-nestjs.ts -A 3 -B 1Repository: mohanagy/graphify-ts
Length of output: 257
🏁 Script executed:
# Verify if there are any examples or tests showing constructor overloads
find . -type f \( -name "*.test.ts" -o -name "*.spec.ts" -o -name "*.fixture.ts" \) -exec grep -l "constructor" {} \; | head -5Repository: mohanagy/graphify-ts
Length of output: 299
🏁 Script executed:
# Check if the code handles methods similarly - do they have the same issue?
grep -n "\.members\.find" src/pipeline/spi/framework-nestjs.ts -B 3 -A 3Repository: mohanagy/graphify-ts
Length of output: 325
🏁 Script executed:
# Check the test file for constructor-related tests
cat tests/unit/spi-framework-nestjs.test.ts | head -100Repository: mohanagy/graphify-ts
Length of output: 3363
🏁 Script executed:
# Search for constructor overload examples in test files
grep -n "constructor" tests/unit/spi-framework-nestjs.test.ts -A 5 -B 2 | head -50Repository: mohanagy/graphify-ts
Length of output: 1892
🏁 Script executed:
# Check if Array.prototype.findLast is available in ES2022
# Let's see if there are any polyfills or compatibility checks
grep -r "ES2022\|ES2023\|findLast" . --include="*.json" --include="*.ts" | grep -v node_modules | head -20Repository: mohanagy/graphify-ts
Length of output: 513
🏁 Script executed:
# Look at the fixture file to understand real test scenarios
cat tests/fixtures/nest-auth.controller.tsRepository: mohanagy/graphify-ts
Length of output: 831
🏁 Script executed:
# Check if there are multiple constructors in any TypeScript file in the codebase
find . -type f -name "*.ts" ! -path "*/node_modules/*" -exec grep -l "constructor.*constructor" {} \; | head -5Repository: mohanagy/graphify-ts
Length of output: 124
🏁 Script executed:
# Let's check if there are any constructor overload examples in TypeScript files
find . -type f -name "*.ts" ! -path "*/node_modules/*" -exec grep -l "constructor(.*)" {} \; | xargs grep -B 2 "constructor(.*)" | grep -A 2 "constructor(.*)" | head -30Repository: mohanagy/graphify-ts
Length of output: 1295
🏁 Script executed:
# Verify Array.prototype.findLast availability - check TypeScript lib definitions
grep -r "findLast" . --include="*.d.ts" 2>/dev/null | head -5Repository: mohanagy/graphify-ts
Length of output: 46
🏁 Script executed:
# Look for any existing polyfills or Array extensions
grep -r "findLast\|Array\.prototype" src/ --include="*.ts" | grep -v "\.test\." | head -10Repository: mohanagy/graphify-ts
Length of output: 371
🏁 Script executed:
# Check the actual behavior - can we use an alternative to findLast that works in ES2022?
# Let's look at what methods are used with .members in the codebase
grep -n "\.members\." src/pipeline/spi/framework-nestjs.ts | head -20Repository: mohanagy/graphify-ts
Length of output: 177
🏁 Script executed:
# Let's check if there are actual constructor overload examples in real code
grep -r "constructor(" tests/ examples/ --include="*.ts" | grep -B 5 "constructor(" | head -40Repository: mohanagy/graphify-ts
Length of output: 2002
🏁 Script executed:
# Verify the logic: does ctor.parameters only exist on implementation?
# Let's create a minimal test to understand TypeScript behavior
cat > /tmp/test_constructor.ts << 'EOF'
class Test {
constructor(x: number)
constructor(y: string)
constructor(p: number | string) {}
}
EOF
# This file shows the pattern - overloads come first without body, implementation last with body
cat /tmp/test_constructor.tsRepository: mohanagy/graphify-ts
Length of output: 166
🏁 Script executed:
# Check how to properly handle this in ES2022 - look for similar patterns in codebase
grep -n "\.filter\|\.reverse\|for.*backward\|\.at\(" src/pipeline/spi/framework-nestjs.ts | head -20Repository: mohanagy/graphify-ts
Length of output: 88
Select the implementation constructor before reading injected params.
find() returns the first constructor declaration, which may be an overload signature when the class uses constructor overloads. Parameter decorators and parameters exist only on the implementation constructor (the last one with a body), so selecting an overload signature causes emitted injects edges to be wrong or missing.
The suggested fix uses Array.prototype.findLast(), which is not available in the project's ES2022 target. Use an ES2022-compatible alternative instead:
Suggested fix
- const ctor = classDecl.members.find((m): m is ts.ConstructorDeclaration => ts.isConstructorDeclaration(m))
+ const ctor =
+ [...classDecl.members]
+ .reverse()
+ .find(
+ (m): m is ts.ConstructorDeclaration => ts.isConstructorDeclaration(m) && m.body !== undefined,
+ ) ??
+ classDecl.members.find((m): m is ts.ConstructorDeclaration => ts.isConstructorDeclaration(m))
if (!ctor) return📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const ctor = classDecl.members.find((m): m is ts.ConstructorDeclaration => ts.isConstructorDeclaration(m)) | |
| if (!ctor) return | |
| const ctor = | |
| [...classDecl.members] | |
| .reverse() | |
| .find( | |
| (m): m is ts.ConstructorDeclaration => ts.isConstructorDeclaration(m) && m.body !== undefined, | |
| ) ?? | |
| classDecl.members.find((m): m is ts.ConstructorDeclaration => ts.isConstructorDeclaration(m)) | |
| if (!ctor) return |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pipeline/spi/framework-nestjs.ts` around lines 537 - 538, The current
code uses classDecl.members.find(...) to grab a constructor which may pick an
overload signature instead of the implementation; change the selection of ctor
(the constructor declaration) to explicitly choose the implementation
constructor (the one with a body) rather than the first match—e.g., filter
members by ts.isConstructorDeclaration and then pick the one with a .body (or
iterate members in reverse to find the first constructor with a body) before
reading injected params so injected edges are derived from the real
implementation constructor.
…ata (#120) Releases the v0.14 SPI substrate (issue #72) and all framework metadata work shipped in PRs #98 #99 #100..#119. See CHANGELOG.md for the full release notes. Highlights: - Complete SPI v1 substrate with TypeScript type-checker-backed file/symbol/call/type/test layers. - Five framework detectors (NestJS, Express, Next.js, React Router, Redux Toolkit) with route_path metadata, dynamic-segment normalization, cross-file mount-prefix resolution, and structural factory metadata. - SPI → ExtractionData projector that propagates framework_role + framework_metadata onto every ExtractionNode. - Express trailing-slash normalization closes the most visible legacy/SPI divergence. - Full byte-equivalence with the legacy extract() on demo-repo remains deferred (documented taxonomy divergence).
Summary
Token-map design
A workspace-level pre-pass walks every `@Module` decorator before per-file framework detection, building a single `Map<token, { classSymbolId, confidence }>`. Only `useClass` / `useExisting` bindings register; `useValue` and `useFactory` providers are intentionally absent because they have no well-defined class target. `@Inject('TOKEN')` against a value-only or factory-only or unknown token produces an info-level `spi.nest.inject-token.unresolved` diagnostic instead of a spurious edge.
If two modules bind the same token to different classes, the last one wins (token uniqueness is the user's responsibility).
Diagnostic surface
Every unresolvable case produces an info-level `SpiDiagnostic` so coverage gaps stay auditable:
Test plan
What's next
Refs #70 (design), #72 (implementation umbrella), #98 (parent slice 3b base).
Summary by CodeRabbit
Release Notes