Amends 0005 (decision 3: a constructor parameter
whose type is a class or a same-named token needs no
@Inject; @Inject is for named instances),
0042 (decision 2: tokens are same-named
values, and gen writes them), 0132
(decision 4: a policy method is a permission verb, bound by gen),
0143 (registration: forRoot
takes code only, and a module’s own providers are wired by gen),
0144 (gen reads constructor types as well as
decorator metadata),
0145 (decision 1: a
defineToken value exists for every interface port, and defineConfig returns a same-named type)
and 0146 (values live
in the config layers, and forRoot takes no values). String injection stays banned.Context
Dependency injection answers “which implementation, for whom, now” at runtime. It swaps a driver per deploy mode, scopes a request to its tenant, actor and transaction, lets a test replace a provider, and manages lifecycle. Codegen answers “what exists” at build time. The authoring cost of the container was the explicit wiring each author wrote by hand: an@Inject token on every constructor parameter,
a providers list and module imports for the author’s own classes, and two places where a value could be set. The owner settled on
2026-10-21 that the container stays, gen writes the wiring, and the rules below apply.
Decision
- DI stays. The container of 0005 and the scopes and lifecycle of 0143 are unchanged. No runtime reflection or auto-wiring replaces them.
- Authors write plain classes. A class that another class depends on is decorated with a kind decorator, or with
@Injectable()when it carries none, and declares typed constructor parameters. The author writes no@Injecttoken, noproviderslist and no module imports for the author’s own classes. - Same-name rule for ports and config. Every interface port has a same-named
defineTokenvalue, anddefineConfig(key, schema, defaults)returns a value with a same-named type. A constructor parameter typed with either needs no@Inject. This holds for one implementer or many, because the token exists for the interface, not for the number of drivers. A class is its own token. @Injectis for named instances only.@Injectremains only where a type alone is ambiguous, which is a named instance fromforRoot({ instances })(0143).- Gen reads the types.
stackra genreads the constructor parameter types at build time with ts-morph. It uses noreflect-metadataand nodesign:paramtypesat runtime (0005 decision 3 stands). - Module file: four facts. The
@Modulefile holdskey,exports, configurableimportsanddependsOn. Thekeyis the namespace defined by 0264. The file holds noproviderslist, and noname. - Folder is the kind. Gen discovers the providers of a module from its folder: every class with a kind decorator, and every class with
an
@OnEventmethod. The scope comes from the class (@Injectable({ scope })). - Sibling edges are inferred. When a class injects a class that a sibling module exports, gen adds the edge. Gen fails when the
injected class is not exported, and on any module cycle.
dependsOnis only for an edge gen cannot see, such as a link-only dependency.importslists configurable modules only, never a sibling. - Config is data;
forRootis code. Values, including the built-in driver choice, live in the config layers of 0146: package defaults, then the host’sconfig/<pkg>.config.tsthroughhostConfig(...), then env, thenrunway.yaml.forRoot()takes no values; it takes a custom driver class or a factory.forFeature(Model)registers per-module things.ConfigurableModuleBuildergainssetDriverSelection(Port, { fromConfig, drivers, override }), so every package selects drivers the same way. - Policy methods are permission verbs. Gen binds
ctx.authorize(Permissions.x, record)to the policy method named by the verb of the permission. Gen fails when a verb has no permission. The binding is written towiring.ts. - Gen emits the wiring.
stackra genwrites.stackra/gen/wiring.ts, which lists every provider with its module, scope and constructor tokens (with the source line), the module graph, the port table and the check results (0261). - Test overrides. A test replaces a provider with
createTestingModule(Module).override(Port, fake). The test never edits the real wiring. - Strings stay banned. A string is never an injection token (0145).
Consequences
- An author reads a class’s dependencies in its constructor, and reviewers no longer check a provider list in a module file.
- A port that gains a second driver adds no token change. An agent changes the driver, not the wiring.
- A wiring mistake moves to build time. An unresolved constructor type, an injection of a non-exported class, or a cycle fails
stackra gen, not the first request. - Config has one place per value. A reviewer finds a value in a layer file, and
forRootchanges only when code changes. - Gen becomes load-bearing: the wiring file is generated and must be correct for every member.
Enforcement
planned —stackra gen fails on an unresolved constructor parameter type, on an injection of a non-exported class, on a module cycle, on a
policy verb with no permission, and on a string injection token. Planned: pnpm validate rejects a providers array in a *.module.ts
file and a name field in a @Module, and the conformance test of 0144 checks that
wiring.ts agrees with the Explorers.