di-bag 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,72 +1,40 @@
1
1
  # DI Bag
2
2
 
3
- Dependency injection for agentic development.
3
+ TypeScript dependency composition and resource ownership for agentic development, LLM harnesses, and agent graphs.
4
4
 
5
- [Documentation](https://dany-fedorov.github.io/di-bag/) · [Quickstart](#quickstart) · [Comparison](#how-it-compares) · [Tutorial](docs/guides/tutorial.md) · [API reference](docs/guides/api-reference.md)
5
+ [Documentation](https://dany-fedorov.github.io/di-bag/) · [Quickstart](#quickstart) · [Agent harnesses](docs/guides/agent-harnesses-and-graphs.md) · [Comparison](#how-it-compares) · [Tutorial](docs/guides/tutorial.md) · [API reference](docs/guides/api-reference.md)
6
6
 
7
7
  ## Why DI Bag?
8
8
 
9
- DI Bag is designed to make modular applications easier for coding agents to
10
- work on. Modularity is a practical context-engineering technique: clear
11
- feature boundaries can reduce the code and dependencies an agent needs to
12
- consider for a task. DI Bag is a TypeScript dependency injection library built
13
- around that idea—helping you define those boundaries, replace dependencies in
14
- tests, and check how the pieces fit together.
15
-
16
- - **[Radical modularity for agentic development](docs/guides/examples-modularity.md).**
17
- Compose small features with private internals and explicit contracts. Give
18
- humans and coding agents focused units to implement, replace, and test
19
- independently, then check their composition together.
20
- - **[TypeScript-first composition](docs/guides/examples-type-checking.md).**
21
- Catch missing dependencies, incompatible service contracts, and invalid
22
- replacements at compile time—not just incorrect arguments at the call site.
23
- Coding agents can shorten their evaluation loop by checking wiring without
24
- starting the app or running integration tests.
25
- - **[A programmable DI layer for custom tooling](docs/guides/examples-extensibility.md).**
26
- Give agents the building blocks to create the tooling your workflow needs:
27
- custom inspectors, diagnostics, metadata-driven actions, and lifecycle tools.
28
- Combine application-defined metadata, composable provider wrappers, and
29
- configurable observers without changing your services.
9
+ Compose ordinary TypeScript factories into reusable features. DI Bag checks
10
+ declared dependencies, keeps module internals private, and manages resource
11
+ creation and cleanup. Build services, tools, or graph nodes against explicit
12
+ contracts, then replace their dependencies for tests.
13
+
14
+ - **[Reusable modules with private bindings](docs/guides/examples-modularity.md).**
15
+ Compose independently developed features without exposing their internals.
16
+ - **[Compile-time wiring checks](docs/guides/examples-type-checking.md).**
17
+ Catch missing dependencies and incompatible replacements before starting the app.
18
+ - **[Metadata inspection without service startup](docs/guides/examples-extensibility.md).**
19
+ Build capability catalogs without running factories or opening clients.
30
20
  - **[Inject anything with a simple factory function](docs/guides/examples-plain-services.md).**
31
- DI Bag is TypeScript-first, but injecting services is as simple as writing an
32
- ordinary JavaScript function: receive dependencies and return a value.
33
- Functions, class instances, configuration, clients, or promises—no decorators,
34
- reflection metadata, or special base classes required.
35
-
36
- Each guide above contains three complete application examples. Lazy creation,
37
- configurable lifetimes, scopes, and dependency-ordered cleanup support these
38
- patterns; the [tutorial](docs/guides/tutorial.md) explains how.
39
-
40
- For a small graph, passing dependencies directly is often simpler. Other DI
41
- libraries also offer typed composition and resource management; the
42
- [comparison below](#how-it-compares) explains the tradeoffs.
21
+ Supply functions, objects, clients, or promises—no decorators or base classes.
43
22
 
44
23
  ## Install
45
24
 
46
- This checkout is a pre-1.0 release candidate, currently versioned `0.1.0`.
47
- It includes breaking API changes, so expect to review migrations when updating.
48
- These instructions install the current checkout without assuming an npm release
49
- is available. Build a local package with Node 24 and npm:
25
+ Install [di-bag from npm](https://www.npmjs.com/package/di-bag):
50
26
 
51
27
  ```sh
52
- git clone https://github.com/dany-fedorov/di-bag.git
53
- cd di-bag
54
- npm ci
55
- npm pack
28
+ npm install di-bag
56
29
  ```
57
30
 
58
- Then, from your application, install the generated archive using its local path:
59
-
60
- ```sh
61
- npm install /path/to/di-bag/di-bag-0.1.0.tgz
62
- ```
31
+ The API is pre-1.0 and includes breaking changes, so review the changelog and
32
+ migration guides when updating.
63
33
 
64
34
  The minimum supported TypeScript version is **6.0.3**; enable `strict` in your
65
35
  `tsconfig.json`. The repository checks classic TypeScript 6.0.3 and native 7.0.2.
66
36
  For browsers and Deno, see [runtime support](#runtime-support).
67
37
 
68
- For an older checkout, follow the [single builder](docs/migrations/single-builder.md) and [API renaming](docs/migrations/api-renaming.md) migration guides.
69
-
70
38
  ## Quickstart
71
39
 
72
40
  A **service** can be a configuration object, a database client, or a function.
@@ -122,8 +90,9 @@ try {
122
90
  }
123
91
  ```
124
92
 
125
- The replacement must satisfy the original service contract. Each fork creates
126
- its own instances and owns its own cleanup; close it separately.
93
+ The replacement must satisfy the original service contract. Each fork has
94
+ independent acquisition and cleanup ownership; close it separately. Factories
95
+ can still return shared objects captured outside the fork.
127
96
 
128
97
  ## Work with async services
129
98
 
@@ -192,6 +161,33 @@ The [tutorial](docs/guides/tutorial.md) also covers modules with private service
192
161
  typed tokens, class and function adapters, optional and lazy dependencies,
193
162
  collections, startup, metadata, observers, and plugin validation.
194
163
 
164
+ ## LLM harnesses and agent graphs
165
+
166
+ Compose an **LLM harness** from model clients, tools, context sources, and
167
+ ordinary functions that serve as **agent graph** nodes.
168
+
169
+ - **Explicit feature boundaries:** give each node or tool a small contract
170
+ and private implementation. Keep feature contracts and focused tests together;
171
+ select tools and context for the LLM in the harness.
172
+ - **Contract checks and fixture tests:** check declared wiring before a model call,
173
+ then fork the composition with typed model and tool fixtures for deterministic
174
+ behavioral tests. Keep live-model evals for quality and task success.
175
+ - **Inspectable capability descriptions:** describe public nodes and tools next
176
+ to their factories. Inspect that metadata without creating services, and use
177
+ it in application-defined catalogs, diagnostics, or dispatch policies.
178
+
179
+ DI Bag's dependency graph describes how services are supplied. The agent graph
180
+ describes execution: which node runs next and what state it receives. Your
181
+ harness or graph framework owns routing, retries, persistence, and execution;
182
+ DI Bag supplies checked composition and resource ownership.
183
+
184
+ The [agent harness and graph guide](docs/guides/agent-harnesses-and-graphs.md)
185
+ combines private feature modules, an LLM-backed node, metadata inspection, and
186
+ fork-based fixture tests in one runnable example. `inspectGraph()` lists every
187
+ binding and the edges observed at runtime; declared edges come from the static
188
+ graph tool. Observers track acquisition, not ordinary node calls. Use your graph
189
+ framework for workflow checkpoints.
190
+
195
191
  ## How it compares
196
192
 
197
193
  DI Bag's appeal is the combination of object-parameter factories, checks across
@@ -250,12 +246,13 @@ for both setup options.
250
246
  | [Complete tutorial](docs/guides/tutorial.md) | Learn every public API through examples, from first composition to advanced ownership. |
251
247
  | [API reference](docs/guides/api-reference.md) | Exact generated signatures, overloads, type parameters, and API inventories. |
252
248
  | [Server guide](docs/guides/server-integration.md) | Node HTTP, Express, Fastify, Bun, and Deno: shared services, request scopes, startup, and shutdown. |
249
+ | [Agent harnesses and graphs](docs/guides/agent-harnesses-and-graphs.md) | Compose model and tool dependencies, inspect metadata, and test nodes with typed fixtures. |
250
+ | [Static dependency graph](docs/guides/agent-harnesses-and-graphs.md#export-the-declared-dependency-graph) | Export every builder chain, declared edge, and cycle to JSON with `di-bag-graph`. |
253
251
  | [Runnable examples](examples) | Modules, tokens, composition, collections, plugins, observers, scopes, and provider metadata. |
254
252
  | [Integration guide](docs/guides/enterprise-integration.md) | Tested recipes for request ownership, substitutions, and dynamic features. |
255
253
  | [Comparison with alternatives](docs/guides/comparison.md) | When DI Bag or another approach may be a better fit, with primary sources. |
256
- | [Migration guides](docs/migrations/single-builder.md) | Before/after examples for the single builder and the [earlier API renaming](docs/migrations/api-renaming.md). |
257
254
  | [Development and verification](docs/guides/development.md) | Full checks, portable runtime testing, compiler scale, and performance evidence. |
258
- | [Documentation map](docs/README.md) | Current guides, migration history, and archived research and design notes. |
255
+ | [Documentation map](docs/README.md) | Every guide, the generated reference, and the contributor documents. |
259
256
 
260
257
  ## Working on DI Bag
261
258
 
@@ -1,6 +1,6 @@
1
1
  import type { Provider } from './provider';
2
2
  import type { Factory } from './registration';
3
- import type { Acquired, AcquisitionMode, NativeOutput, ModeOptions } from './acquisition-mode';
3
+ import type { Acquired, AcquisitionMode, AutoOutput, NativeOutput, ModeOptions } from './acquisition-mode';
4
4
  import type { TokenDependencyContract } from './token-types';
5
5
  /** Cooperative cancellation information supplied to a context-aware acquisition. */
6
6
  export interface AcquisitionContext {
@@ -26,7 +26,7 @@ type FactoryOptions<M extends AcquisitionMode> = 'auto' extends M ? [options?: {
26
26
  * @typeParam F - The complete callback signature, retaining dependency and output inference.
27
27
  * @typeParam M - The raw, nativePromise, or configured auto acquisition policy.
28
28
  */
29
- export declare function fromFactory<F extends (this: void, deps: never, context: AcquisitionContext) => ('nativePromise' extends M ? Promise<unknown> : unknown), M extends AcquisitionMode = 'auto'>(callback: F, options: {
29
+ export declare function fromFactory<F extends (this: void, deps: never, context: AcquisitionContext) => ('nativePromise' extends M ? Promise<unknown> : unknown), M extends AcquisitionMode = 'auto'>(callback: F & AutoOutput<ReturnType<NoInfer<F>>, NoInfer<M>>, options: {
30
30
  readonly context: 'acquisition';
31
31
  } & ModeOptions<M>): Provider<ContextualFactory<F>, Readonly<{}>, readonly [], TokenDependencyContract, Acquired<ReturnType<F>, M>>;
32
32
  /**
@@ -38,5 +38,5 @@ export declare function fromFactory<F extends (this: void, deps: never, context:
38
38
  * @typeParam F - The exact factory signature and exposed result.
39
39
  * @typeParam M - The raw, nativePromise, or configured auto acquisition policy.
40
40
  */
41
- export declare function fromFactory<F extends Factory, M extends AcquisitionMode = 'auto'>(callback: F & NativeOutput<ReturnType<NoInfer<F>>, NoInfer<M>>, ...options: FactoryOptions<M>): Provider<F, Readonly<{}>, readonly [], TokenDependencyContract, Acquired<ReturnType<F>, M>>;
41
+ export declare function fromFactory<F extends Factory, M extends AcquisitionMode = 'auto'>(callback: F & NativeOutput<ReturnType<NoInfer<F>>, NoInfer<M>> & AutoOutput<ReturnType<NoInfer<F>>, NoInfer<M>>, ...options: FactoryOptions<M>): Provider<F, Readonly<{}>, readonly [], TokenDependencyContract, Acquired<ReturnType<F>, M>>;
42
42
  export {};
@@ -26,6 +26,11 @@ export declare class AcquisitionFamily {
26
26
  leave(): void;
27
27
  ancestry(bindingId: BindingId, ownerId: symbol, label: string, from?: AttemptIdentity): AcquisitionHistory | undefined;
28
28
  dependencyPath(from: AttemptIdentity, dependency: string): readonly string[];
29
+ /** Distinct consumer-to-dependency binding edges recorded by live attempts, in attempt order. */
30
+ observedEdges(): readonly {
31
+ readonly from: BindingId;
32
+ readonly to: BindingId;
33
+ }[];
29
34
  retireIncoming(attempt: AttemptIdentity): void;
30
35
  recordEdge(from: AttemptIdentity, to: AttemptIdentity): void;
31
36
  private path;
@@ -78,6 +78,26 @@ class AcquisitionFamily {
78
78
  history.reverse();
79
79
  return Object.freeze([...history, from.label, dependency]);
80
80
  }
81
+ /** Distinct consumer-to-dependency binding edges recorded by live attempts, in attempt order. */
82
+ observedEdges() {
83
+ const seen = new Map();
84
+ const edges = [];
85
+ for (const attempt of this.attempts.values()) {
86
+ for (const dependency of attempt.dependencies) {
87
+ const target = this.attempts.get(dependency);
88
+ if (!target)
89
+ continue;
90
+ let targets = seen.get(attempt.bindingId);
91
+ if (!targets)
92
+ seen.set(attempt.bindingId, targets = new Set());
93
+ if (targets.has(target.bindingId))
94
+ continue;
95
+ targets.add(target.bindingId);
96
+ edges.push(Object.freeze({ from: attempt.bindingId, to: target.bindingId }));
97
+ }
98
+ }
99
+ return Object.freeze(edges);
100
+ }
81
101
  retireIncoming(attempt) {
82
102
  const consumers = this.incoming.get(attempt.id);
83
103
  if (!consumers)
@@ -1,5 +1,5 @@
1
1
  import type { LifecycleObservers } from './observers';
2
- import type { Unsatisfied } from './types';
2
+ import type { StructuralThenable, Unsatisfied } from './types';
3
3
  /**
4
4
  * How an acquisition stage treats its returned value: configured classification,
5
5
  * the exact raw value, or an observed native Promise fulfillment.
@@ -28,6 +28,8 @@ export type StageOptions<M extends AcquisitionMode> = 'auto' extends M ? [option
28
28
  readonly acquisitionMode: M;
29
29
  }];
30
30
  export type NativeOutput<O, M extends AcquisitionMode> = 'nativePromise' extends M ? [O] extends [Promise<unknown>] ? unknown : Unsatisfied<'nativePromise acquisition requires a Promise output', {}> : unknown;
31
+ /** Reject a structural thenable output when the stage would classify it automatically. */
32
+ export type AutoOutput<O, M extends AcquisitionMode> = 'auto' extends M ? true extends StructuralThenable<O> ? Unsatisfied<'factory output is a structural thenable; return a native Promise or select acquisitionMode raw or nativePromise', {}> : unknown : unknown;
31
33
  export declare function acquisitionMode(options: {
32
34
  readonly acquisitionMode?: AcquisitionMode;
33
35
  } | undefined, fallback?: AcquisitionMode): AcquisitionMode;
@@ -32,6 +32,10 @@ export declare class ScopeAcquisitions {
32
32
  isTransient(bindingId: BindingId, path?: readonly BindingId[]): boolean;
33
33
  /** Relationship uses the effective owner graph; frames use canonical attempts. */
34
34
  inspectDescription(bindingId: BindingId): Pick<RegistrationSnapshot<object, readonly unknown[]>, 'registrationMetadata' | 'aliasTarget'>;
35
+ observedEdges(): readonly {
36
+ readonly from: BindingId;
37
+ readonly to: BindingId;
38
+ }[];
35
39
  private assertAliasPath;
36
40
  assertOpen(): void;
37
41
  close(beforeDispose?: Promise<void>, cause?: unknown): Promise<void>;
@@ -115,6 +115,7 @@ class ScopeAcquisitions {
115
115
  const owner = this.owner(bindingId);
116
116
  return owner === this ? { registrationMetadata: description.metadata } : owner.inspectDescription(bindingId);
117
117
  }
118
+ observedEdges() { return this.family.observedEdges(); }
118
119
  assertAliasPath(bindingId, path) {
119
120
  if (path.includes(bindingId))
120
121
  throw (0, errors_1.libraryError)('DI_BAG_CYCLE', `alias cycle: ${[...path, bindingId].map(id => this.graph.label(id)).join(' -> ')}`, { path: Object.freeze([...path, bindingId].map(id => this.graph.label(id))) });
@@ -239,8 +240,12 @@ class ScopeAcquisitions {
239
240
  return target === undefined ? undefined : this.takeExposed(this.resolveBinding(target, attempt));
240
241
  };
241
242
  const references = new Map(description.references.map(reference => [reference.slot, reference]));
243
+ const invalidAccess = (access) => (0, errors_1.libraryError)('DI_BAG_INVALID_DEPENDENCY_ACCESS', `Cannot inspect the dependencies of ${JSON.stringify(attempt.label)}: ${access} is not supported. Read each named dependency directly; the dependency object resolves lazily.`, { operation: 'resolve', consumer: attempt.label, access });
242
244
  const deps = new Proxy(Object.create(null), {
243
245
  get: (_, key) => {
246
+ // JSON.stringify probes toJSON through get before enumerating; name the real operation.
247
+ if (key === 'toJSON')
248
+ throw invalidAccess('JSON.stringify');
244
249
  const reference = typeof key === 'symbol' ? references.get(key) : undefined;
245
250
  if (reference)
246
251
  return reference.kind === 'lazy' ? () => read(reference.key)
@@ -249,6 +254,10 @@ class ScopeAcquisitions {
249
254
  return undefined;
250
255
  return read(key);
251
256
  },
257
+ // Only `get` is lazy and checked; every other reflection would silently report an empty object.
258
+ has: (_, key) => { throw invalidAccess(`'${String(key)}' in deps`); },
259
+ ownKeys: () => { throw invalidAccess('enumeration (Object.keys, spread, JSON.stringify)'); },
260
+ getOwnPropertyDescriptor: (_, key) => { throw invalidAccess(`descriptor of '${String(key)}'`); },
252
261
  });
253
262
  this.observeAttempt(attempt, 'acquisition-started');
254
263
  this.family.enter(attempt);
@@ -0,0 +1,13 @@
1
+ import type { Builder } from './di-bag';
2
+ import type { CheckedLifetimes } from './lifetime-types';
3
+ import type { CheckedConstraints, CompleteConstraints, NeedConstraint } from './module-types';
4
+ import type { CheckDependencyCompatibility, CheckDependencyCompleteness, Entry, RegistrationsFromEntries } from './types';
5
+ type ReportOf<Check> = unknown extends Check ? never : Check;
6
+ type Reports<E extends Entry, C extends NeedConstraint> = ReportOf<CheckDependencyCompatibility<RegistrationsFromEntries<E>>> | ReportOf<CheckDependencyCompleteness<RegistrationsFromEntries<E>>> | ReportOf<CheckedConstraints<C, RegistrationsFromEntries<E>>> | ReportOf<CompleteConstraints<C, RegistrationsFromEntries<E>>> | ReportOf<CheckedLifetimes<RegistrationsFromEntries<E>, C>>;
7
+ /**
8
+ * The compile-time verdict for a builder: `void` when `build()` would be accepted,
9
+ * otherwise the same failure `build()` reports, including its details.
10
+ * Read it through `builder.verifyGraph() satisfies void;` or as `CompositionReport<typeof builder>`.
11
+ */
12
+ export type CompositionReport<B> = B extends Builder<infer E, infer C> ? [Reports<E, C>] extends [never] ? void : Reports<E, C> : never;
13
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,4 +1,4 @@
1
- import type { Acquired, AcquisitionMode, NativeOutput, StageOptions } from './acquisition-mode';
1
+ import type { Acquired, AcquisitionMode, AutoOutput, NativeOutput, StageOptions } from './acquisition-mode';
2
2
  import type { Provider } from './provider';
3
3
  import type { DependencyReference } from './dependency-references';
4
4
  import type { TokenArguments, ReferenceGraph, DependencyTupleAdmission } from './token-types';
@@ -19,7 +19,7 @@ export type CompositionFunction<T extends readonly DependencyReference[], O = un
19
19
  * @returns A lazy provider retaining the dependency graph and exact return type.
20
20
  * @typeParam F - The exact positional function signature retained by the provider.
21
21
  */
22
- export declare function fromFunction<const T extends readonly DependencyReference[], F extends CompositionFunction<NoInfer<T>, 'nativePromise' extends M ? Promise<unknown> : unknown>, M extends AcquisitionMode = 'auto'>(tokens: T & DependencyTupleAdmission<T>, callback: F & CompositionArguments<TokenArguments<NoInfer<T>>, Parameters<NoInfer<F>>> & NativeOutput<ReturnType<NoInfer<F>>, NoInfer<M>>, ...modeOptions: StageOptions<M>): Provider<OutputFactory<ReturnType<F>>, Readonly<{}>, readonly [], ReferenceGraph<T>, Acquired<ReturnType<F>, M>>;
22
+ export declare function fromFunction<const T extends readonly DependencyReference[], F extends CompositionFunction<NoInfer<T>, 'nativePromise' extends M ? Promise<unknown> : unknown>, M extends AcquisitionMode = 'auto'>(tokens: T & DependencyTupleAdmission<T>, callback: F & CompositionArguments<TokenArguments<NoInfer<T>>, Parameters<NoInfer<F>>> & NativeOutput<ReturnType<NoInfer<F>>, NoInfer<M>> & AutoOutput<ReturnType<NoInfer<F>>, NoInfer<M>>, ...modeOptions: StageOptions<M>): Provider<OutputFactory<ReturnType<F>>, Readonly<{}>, readonly [], ReferenceGraph<T>, Acquired<ReturnType<F>, M>>;
23
23
  /**
24
24
  * Adapt a positional function whose parameters exactly match the selected dependency values.
25
25
  * @param tokens - A finite tuple of typed tokens and dependency references.
@@ -28,7 +28,7 @@ export declare function fromFunction<const T extends readonly DependencyReferenc
28
28
  * @returns A reusable provider; no dependency or result is implicitly awaited.
29
29
  * @typeParam F - The exact positional function signature retained by the provider.
30
30
  */
31
- export declare function fromFunction<const T extends readonly DependencyReference[], F extends CompositionFunction<NoInfer<T>>, M extends AcquisitionMode = 'auto'>(tokens: T & DependencyTupleAdmission<T>, callback: F & CompositionArguments<TokenArguments<NoInfer<T>>, Parameters<NoInfer<F>>> & NativeOutput<ReturnType<NoInfer<F>>, NoInfer<M>>, ...modeOptions: StageOptions<M>): Provider<OutputFactory<ReturnType<F>>, Readonly<{}>, readonly [], ReferenceGraph<T>, Acquired<ReturnType<F>, M>>;
31
+ export declare function fromFunction<const T extends readonly DependencyReference[], F extends CompositionFunction<NoInfer<T>>, M extends AcquisitionMode = 'auto'>(tokens: T & DependencyTupleAdmission<T>, callback: F & CompositionArguments<TokenArguments<NoInfer<T>>, Parameters<NoInfer<F>>> & NativeOutput<ReturnType<NoInfer<F>>, NoInfer<M>> & AutoOutput<ReturnType<NoInfer<F>>, NoInfer<M>>, ...modeOptions: StageOptions<M>): Provider<OutputFactory<ReturnType<F>>, Readonly<{}>, readonly [], ReferenceGraph<T>, Acquired<ReturnType<F>, M>>;
32
32
  /**
33
33
  * Adapt a concrete constructor while preserving its prototype, private fields, and `new.target`.
34
34
  * @param tokens - A finite tuple whose dependency values match the constructor parameters.
@@ -37,5 +37,5 @@ export declare function fromFunction<const T extends readonly DependencyReferenc
37
37
  * @returns A lazy provider that constructs one instance per acquisition attempt.
38
38
  * @throws When the supplied runtime value is not constructable.
39
39
  */
40
- export declare function fromClass<const T extends readonly DependencyReference[], C extends new (...args: TokenArguments<NoInfer<T>>) => unknown, M extends AcquisitionMode = 'auto'>(tokens: T & DependencyTupleAdmission<T>, constructor: C & CompositionArguments<TokenArguments<NoInfer<T>>, ConstructorParameters<NoInfer<C>>> & NativeOutput<InstanceType<NoInfer<C>>, NoInfer<M>>, ...modeOptions: StageOptions<M>): Provider<() => InstanceType<C>, Readonly<{}>, readonly [], ReferenceGraph<T>, Acquired<InstanceType<C>, M>>;
40
+ export declare function fromClass<const T extends readonly DependencyReference[], C extends new (...args: TokenArguments<NoInfer<T>>) => unknown, M extends AcquisitionMode = 'auto'>(tokens: T & DependencyTupleAdmission<T>, constructor: C & CompositionArguments<TokenArguments<NoInfer<T>>, ConstructorParameters<NoInfer<C>>> & NativeOutput<InstanceType<NoInfer<C>>, NoInfer<M>> & AutoOutput<InstanceType<NoInfer<C>>, NoInfer<M>>, ...modeOptions: StageOptions<M>): Provider<() => InstanceType<C>, Readonly<{}>, readonly [], ReferenceGraph<T>, Acquired<InstanceType<C>, M>>;
41
41
  export {};
package/dist/di-bag.d.ts CHANGED
@@ -6,6 +6,7 @@ import { withDisposal } from './registration';
6
6
  import type { FactoryWithDisposal, Factory, Registration, Registrations } from './registration';
7
7
  import { BindingGraph, BagRuntime } from './runtime';
8
8
  import type { Module } from './module';
9
+ import type { CompositionReport } from './composition-report';
9
10
  import type { CheckedConstraints, CompleteConstraints, ExternalRequirements, IncrementalConstraints, ModulePublicProviders, ModuleSealedConstraints, NeedConstraint } from './module-types';
10
11
  import type { CheckedLifetimes } from './lifetime-types';
11
12
  import { withLifetime } from './lifetime';
@@ -17,13 +18,13 @@ import { withMetadata, transformService } from './provider';
17
18
  import { fromFunction, fromClass } from './composition';
18
19
  import type { RuntimeContext, RuntimeOptions } from './acquisition-mode';
19
20
  import type { ProviderRegistrationMetadata, ProviderAcquisitionMetadata } from './provider';
20
- import type { RegistrationSnapshot } from './inspection';
21
+ import type { GraphSnapshot, RegistrationSnapshot } from './inspection';
21
22
  import { token } from './tokens';
22
23
  import type { PluginProviderFactory } from './plugins';
23
24
  import type { TokenBase, TokenKey, TokenService } from './tokens';
24
25
  import type { TokenBinding, BindingOutput, TokenMember, TokenTupleAdmission, SelectionKey, ReboundSelection } from './token-types';
25
26
  import type { BuilderReplacementRegistration, ReplacementAdmission, ReplacedEntries, ZeroDependencyAdmission } from './replacement-types';
26
- import type { CheckDependencyCompatibility, CheckDependencyCompleteness, RegistrationEntries, Entry, EntryKeys, OverrideFactoryContext, RegistrationsFromEntries, IncrementalChecked, Introduces, IntroducesKeys, OverrideRegistrations, Overrides, ServicesOf, ReplacementKeyOf, ReplacementOutput, SelectedRegistrations, Selection, NamedAdmission } from './types';
27
+ import type { CheckDependencyCompatibility, CheckDependencyCompleteness, RegistrationEntries, Entry, EntryKeys, OverrideFactoryContext, RegistrationsFromEntries, IncrementalChecked, Introduces, IntroducesKeys, OverrideRegistrations, Overrides, ServicesOf, ReplacementKeyOf, ReplacementOutput, SelectedRegistrations, Selection, NamedAdmission, ThenableAdmission } from './types';
27
28
  type ReplacementFactory<O> = (this: void) => O;
28
29
  declare const constraintInvariant: unique symbol;
29
30
  /**
@@ -66,6 +67,13 @@ declare class Bag<R extends Registrations, C extends NeedConstraint = never> {
66
67
  * @returns A frozen point-in-time snapshot. Application-owned metadata payloads are not frozen.
67
68
  */
68
69
  inspect<K extends (keyof R & string) | TokenBase>(token: K & ([K] extends [string] ? unknown : TokenMember<R, K>)): RegistrationSnapshot<ProviderRegistrationMetadata<R[SelectionKey<K> & keyof R]>, ProviderAcquisitionMetadata<R[SelectionKey<K> & keyof R]>>;
70
+ /**
71
+ * Describe every binding this bag can resolve and the dependency edges observed so far.
72
+ * Nothing is acquired. Named dependencies declared on factory parameters are not visible
73
+ * until the factory runs; the static graph tool reports them from source.
74
+ * @returns A frozen point-in-time snapshot; application-owned metadata payloads are not frozen.
75
+ */
76
+ inspectGraph(): GraphSnapshot;
69
77
  /**
70
78
  * Create a tracked child that borrows selected parent acquisitions.
71
79
  * @param options - A checked selection of non-transient services to share lazily.
@@ -129,14 +137,14 @@ declare class Builder<E extends Entry, C extends NeedConstraint = never> {
129
137
  */
130
138
  register<N extends {
131
139
  [K in keyof N]: Registration;
132
- }>(more: N & Registrations & ([N] extends [never] ? never : NamedAdmission<N> & IntroducesKeys<EntryKeys<E>, keyof N> & IncrementalChecked<E, N> & CheckedConstraints<C, OverrideRegistrations<RegistrationsFromEntries<E>, N>>)): Builder<E | RegistrationEntries<N>, C>;
140
+ }>(more: N & Registrations & ([N] extends [never] ? never : NamedAdmission<N> & ThenableAdmission<N> & IntroducesKeys<EntryKeys<E>, keyof N> & IncrementalChecked<E, N> & CheckedConstraints<C, OverrideRegistrations<RegistrationsFromEntries<E>, N>>)): Builder<E | RegistrationEntries<N>, C>;
133
141
  /**
134
142
  * Register a provider to a typed token.
135
143
  * @param token - A new typed token identity.
136
144
  * @param registration - A registration whose exposed output satisfies the token service type.
137
145
  * @returns A new builder retaining the provider's metadata, lifetime, dependencies, and ownership stages.
138
146
  */
139
- register<T extends TokenBase, V extends Registration>(token: T & TokenTupleAdmission<readonly [T]> & IntroducesKeys<EntryKeys<E>, TokenKey<T>>, registration: V & Registration & BindingOutput<NoInfer<T>, NoInfer<V>> & IncrementalChecked<E, Record<TokenKey<T>, TokenBinding<NoInfer<T>, NoInfer<V>>>> & CheckedConstraints<C, OverrideRegistrations<RegistrationsFromEntries<E>, Record<TokenKey<T>, TokenBinding<NoInfer<T>, NoInfer<V>>>>>): Builder<E | {
147
+ register<T extends TokenBase, V extends Registration>(token: T & TokenTupleAdmission<readonly [T]> & IntroducesKeys<EntryKeys<E>, TokenKey<T>>, registration: V & Registration & BindingOutput<NoInfer<T>, NoInfer<V>> & ThenableAdmission<Record<TokenKey<T>, NoInfer<V>>> & IncrementalChecked<E, Record<TokenKey<T>, TokenBinding<NoInfer<T>, NoInfer<V>>>> & CheckedConstraints<C, OverrideRegistrations<RegistrationsFromEntries<E>, Record<TokenKey<T>, TokenBinding<NoInfer<T>, NoInfer<V>>>>>): Builder<E | {
140
148
  key: TokenKey<T>;
141
149
  registration: TokenBinding<T, V>;
142
150
  }, C>;
@@ -180,6 +188,13 @@ declare class Builder<E extends Entry, C extends NeedConstraint = never> {
180
188
  * @returns A new builder exposing only the module's selected exports.
181
189
  */
182
190
  installModule<P extends object, R extends object, MC extends NeedConstraint, D extends Registrations>(module: Module<P, R, MC, D> & IntroducesKeys<EntryKeys<E>, keyof D> & IncrementalChecked<E, D> & IncrementalConstraints<C, MC, RegistrationsFromEntries<E>, D>): Builder<E | RegistrationEntries<D>, C | MC>;
191
+ /**
192
+ * Report at the type level why this graph would not build; the runtime call does nothing.
193
+ * Write `builder.verifyGraph() satisfies void;` so a rejected graph fails on that line with
194
+ * the complete message and details, instead of at the start of the builder expression.
195
+ * @returns `void` for a buildable graph; otherwise the failure that `build()` would report.
196
+ */
197
+ verifyGraph<Self extends Builder<E, C>>(this: Self): CompositionReport<Self>;
183
198
  /**
184
199
  * Seal this graph as a reusable module and select its public names and typed tokens.
185
200
  * Unselected registrations stay private to each installation; unmet dependencies
package/dist/di-bag.js CHANGED
@@ -41,6 +41,13 @@ class Bag {
41
41
  inspect(token) {
42
42
  return this.#runtime.inspect(typeof token === 'string' ? token : (0, tokens_1.readTokenKey)(token));
43
43
  }
44
+ /**
45
+ * Describe every binding this bag can resolve and the dependency edges observed so far.
46
+ * Nothing is acquired. Named dependencies declared on factory parameters are not visible
47
+ * until the factory runs; the static graph tool reports them from source.
48
+ * @returns A frozen point-in-time snapshot; application-owned metadata payloads are not frozen.
49
+ */
50
+ inspectGraph() { return this.#runtime.inspectGraph(); }
44
51
  createScope(...args) {
45
52
  this.#runtime.assertOpen();
46
53
  const { graph, shared } = (0, scope_selection_1.selectScope)(this.#graph, args, key => this.#runtime.isTransient(key));
@@ -154,6 +161,7 @@ class Builder {
154
161
  installModule(module) {
155
162
  return new Builder(this.#graph.withInstallation((0, module_1.moduleGraph)(module)), this.context);
156
163
  }
164
+ verifyGraph() { return undefined; }
157
165
  /**
158
166
  * Seal this graph as a reusable module and select its public names and typed tokens.
159
167
  * Unselected registrations stay private to each installation; unmet dependencies
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { DiBagCleanupError, DiBagPluginValidationError, DiBagStartupError, DiBag
3
3
  export type { CleanupFailure, DiBagErrorCode, DiBagDiagnostic } from './errors';
4
4
  export type { Bag, Builder, DiBagApi, ConfigurationOptions } from './di-bag';
5
5
  export type { Module } from './module';
6
+ export type { CompositionReport } from './composition-report';
6
7
  export type { ModuleExportedServices, ModuleRequiredServices, ModuleConstraints, ModuleSealedConstraints, SealedConstraints, PublicProviders, ModulePublicProviders, Renamed } from './module-types';
7
8
  export type { FactoryWithDisposal, Registration } from './registration';
8
9
  export type { Provider, ProviderFactory, ProviderGraphContract, ProviderOutput, ProviderAcquiredValue, ProviderNamedDependencies, ProviderRegistrationMetadata, ProviderAcquisitionMetadata, ProviderRequiredTokens, ProviderOptionalTokens, ProviderCollectionTokens } from './provider';
@@ -14,8 +15,9 @@ export type { ScopeOptions, DisjointScopeSelection, UnsharedAliases, ScopedAlias
14
15
  export type { Token, TokenBase, TokenKey, TokenService } from './tokens';
15
16
  export type { TokenBinding, TokenMember, TokenDependencyContract, ReboundProviders, ReboundSelection, SelectionKey } from './token-types';
16
17
  export type { CheckedLifetimes, CheckedScopeLifetimes, LexicalContext, ModuleScope, Enclosed, RenamedContext, RenamedLifetimeObligation, EnclosedLifetimeObligation, RenamedLifetimeProviders } from './lifetime-types';
18
+ export type { DiBagPolicy } from './types';
17
19
  export type { CheckDependencyCompatibility, CheckDependencyCompleteness, RegistrationEntries, OverrideFactoryContext, RegistrationsFromEntries, OverrideRegistrations, Overrides, ServicesOf, SelectedRegistrations, Selection } from './types';
18
- export type { Presence, AcquisitionMetadataPresence, AcquisitionSnapshot, RegistrationSnapshot } from './inspection';
20
+ export type { Presence, AcquisitionMetadataPresence, AcquisitionSnapshot, RegistrationSnapshot, BindingSnapshot, GraphSnapshot } from './inspection';
19
21
  export type { CompositionArguments, CompositionFunction } from './composition';
20
22
  export type { OptionalDependency, LazyDependency, CollectionDependency, DependencyReference } from './dependency-references';
21
23
  export type { PluginProviderFactory, PluginAcquisitionMode, PluginOptions, PluginOutputValidator, PluginProvider } from './plugins';
@@ -1,3 +1,5 @@
1
+ import type { AcquisitionMode } from './acquisition-mode';
2
+ import type { Lifetime } from './lifetime';
1
3
  /** Structural optional presence; payloads are application-owned and not frozen. */
2
4
  export type Presence<T> = {
3
5
  readonly present: false;
@@ -34,3 +36,36 @@ export interface RegistrationSnapshot<M = Readonly<{}>, A extends readonly unkno
34
36
  /** Point-in-time attempts; inspection does not retain failed-attempt history. */
35
37
  readonly acquisitions: readonly AcquisitionSnapshot<A>[];
36
38
  }
39
+ /** One binding of a bag's graph, described without acquiring it. */
40
+ export interface BindingSnapshot<M = Readonly<{}>, A extends readonly unknown[] = readonly []> extends RegistrationSnapshot<M, A> {
41
+ /** Public names or token symbols that select this binding, in registration order; empty for a private module binding. */
42
+ readonly keys: readonly (string | symbol)[];
43
+ readonly lifetime: Lifetime;
44
+ readonly acquisitionMode: AcquisitionMode;
45
+ /** True when some stage of the provider accepts ownership through a disposer. */
46
+ readonly owned: boolean;
47
+ /** Typed-token dependencies declared positionally through tokens, `optional`, `lazy`, or `all` references. */
48
+ readonly tokenDependencies: readonly {
49
+ readonly key: symbol;
50
+ readonly kind: 'required' | 'optional' | 'lazy' | 'all';
51
+ }[];
52
+ }
53
+ /**
54
+ * A frozen description of every binding a bag can resolve, plus the edges observed so far.
55
+ * Named dependencies read from a factory's object parameter are not knowable until the factory
56
+ * runs; `observedEdges` records them after acquisition. Use the static graph tool for declared edges.
57
+ */
58
+ export interface GraphSnapshot {
59
+ readonly scopeId: symbol;
60
+ /** Public bindings in registration order, then contributions in group order, then remaining private bindings. */
61
+ readonly bindings: readonly BindingSnapshot<object, readonly unknown[]>[];
62
+ readonly contributions: readonly {
63
+ readonly token: symbol;
64
+ readonly bindingIds: readonly symbol[];
65
+ }[];
66
+ /** Consumer-to-dependency edges recorded by acquisitions in this bag's ownership family. */
67
+ readonly observedEdges: readonly {
68
+ readonly from: symbol;
69
+ readonly to: symbol;
70
+ }[];
71
+ }
@@ -3,8 +3,17 @@ import type { Registration, Registrations } from './registration';
3
3
  import type { Provider, ProviderFactory, ProviderGraphContract, ProviderNamedDependencies, ProviderRequiredTokens, ProviderOptionalTokens, ProviderCollectionTokens, ProviderRegistrationMetadata, ProviderAcquisitionMetadata, ProviderAcquiredValue } from './provider';
4
4
  import type { GraphContract } from './token-types';
5
5
  import type { TokenKey } from './tokens';
6
- import type { CheckDependencyCompatibility, CheckDependencyCompleteness, Unsatisfied } from './types';
6
+ import type { CheckDependencyCompatibility, CheckDependencyCompleteness, NameText, Unsatisfied } from './types';
7
7
  import type { CheckedConstraints, CompleteConstraints, NeedConstraint, Renamed } from './module-types';
8
+ type SiteText<S> = S extends {
9
+ readonly key: infer K;
10
+ } ? NameText<K> : S extends {
11
+ readonly kind: 'contribution';
12
+ } ? 'contribution' : never;
13
+ type CaptiveText<C> = C extends {
14
+ readonly root: infer R;
15
+ readonly dependency: infer D;
16
+ } ? `${SiteText<R>} -> ${SiteText<D>}` : never;
8
17
  /**
9
18
  * A module provider's retained local registrations and public-to-local export mapping.
10
19
  * Contexts chain through `parent` when a module was sealed inside another module:
@@ -178,13 +187,13 @@ type OverrideCaptives<R extends Registrations, O extends Registrations, G> = {
178
187
  /** Reject root providers introduced by a scope override when they capture scoped dependencies. */
179
188
  export type CheckedScopeLifetimes<R extends Registrations, O extends Registrations, G = never> = [
180
189
  OverrideCaptives<R, O, G>
181
- ] extends [never] ? unknown : Unsatisfied<'root lifetime cannot capture scoped dependency', {
190
+ ] extends [never] ? unknown : Unsatisfied<`root lifetime cannot capture scoped dependency: ${CaptiveText<OverrideCaptives<R, O, G>>}`, {
182
191
  readonly captives: OverrideCaptives<R, O, G>;
183
192
  }>;
184
193
  /** Reject strict root providers that transitively capture scoped dependencies. */
185
194
  export type CheckedLifetimes<R extends Registrations, C extends NeedConstraint> = [
186
195
  Captives<R, C>
187
- ] extends [never] ? unknown : unknown extends CheckDependencyCompatibility<R> & CheckDependencyCompleteness<R> & CheckedConstraints<C, R> & CompleteConstraints<C, R> ? Unsatisfied<'root lifetime cannot capture scoped dependency', {
196
+ ] extends [never] ? unknown : unknown extends CheckDependencyCompatibility<R> & CheckDependencyCompleteness<R> & CheckedConstraints<C, R> & CompleteConstraints<C, R> ? Unsatisfied<`root lifetime cannot capture scoped dependency: ${CaptiveText<Captives<R, C>>}`, {
188
197
  readonly captives: Captives<R, C>;
189
198
  }> : unknown;
190
199
  type PolicyEnclosing<H extends Registrations, C, K, Visited> = C extends {
@@ -1,7 +1,7 @@
1
1
  import type { ContributionConstraint, CheckedContributions, CompleteContributions, RenamedContribution, ModuleContributionConstraints } from './contribution-types';
2
2
  import type { Module } from './module';
3
3
  import type { Registrations } from './registration';
4
- import type { Entry, Needs, RegistrationsFromEntries, ServicesOf, Singleton, Unsatisfied } from './types';
4
+ import type { Entry, NameText, Needs, RegistrationsFromEntries, ServicesOf, Singleton, Unsatisfied } from './types';
5
5
  import type { MetadataKeyUnion, Provider, ProviderOutput, ProviderNamedDependencies, ProviderRegistrationMetadata, ProviderAcquisitionMetadata, ProviderAcquiredValue, ProviderGraphContract, ProviderRequiredTokens, ProviderOptionalTokens, ProviderCollectionTokens, BoundToken } from './provider';
6
6
  import type { TokenDependencyContract, WrongToken, MissingToken } from './token-types';
7
7
  import type { TokenBase, TokenKey, TokenService } from './tokens';
@@ -76,7 +76,7 @@ export type IncrementalConstraints<C extends NeedConstraint, MC extends NeedCons
76
76
  }>] extends [never] ? unknown extends CheckedConstraints<C, Incoming> ? CheckedConstraints<MC, import('./types').OverrideRegistrations<Old, Incoming>> : CheckedConstraints<C, Incoming> : CheckedConstraints<C | MC, import('./types').OverrideRegistrations<Old, Incoming>>;
77
77
  export type CompleteConstraints<C extends NeedConstraint, A extends Registrations> = [
78
78
  MissingConstraint<C, ServicesOf<A>> | MissingTokenConstraint<C, A>
79
- ] extends [never] ? CompleteContributions<C, A> : Unsatisfied<'required service registrations are missing', {
79
+ ] extends [never] ? CompleteContributions<C, A> : Unsatisfied<`required service registrations are missing: ${NameText<MissingConstraint<C, ServicesOf<A>> | MissingTokenConstraint<C, A>>}`, {
80
80
  missing: MissingConstraint<C, ServicesOf<A>> | MissingTokenConstraint<C, A>;
81
81
  relationships: MissingConstraintRelationships<C, ServicesOf<A>>;
82
82
  }>;
package/dist/runtime.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ScopeAcquisitions } from './acquisition';
2
2
  import { normalize } from './registration';
3
3
  import type { Registration, Registrations } from './registration';
4
- import type { RegistrationSnapshot } from './inspection';
4
+ import type { GraphSnapshot, RegistrationSnapshot } from './inspection';
5
5
  import type { RuntimeContext } from './acquisition-mode';
6
6
  export type BindingId = symbol;
7
7
  export type BindingKey = string | symbol;
@@ -60,6 +60,16 @@ export declare class BindingGraph {
60
60
  * is kept to produce this order.
61
61
  */
62
62
  describe(): GraphDescription;
63
+ /** Every retained binding in `describe()` order, with the public keys that select it. */
64
+ bindingSummaries(): readonly {
65
+ readonly id: BindingId;
66
+ readonly keys: readonly BindingKey[];
67
+ }[];
68
+ /** Every contribution group with its member bindings in contribution order. */
69
+ contributionGroups(): readonly {
70
+ readonly token: symbol;
71
+ readonly bindingIds: readonly BindingId[];
72
+ }[];
63
73
  /** Install disjoint public slots atomically, retaining lexical private refs. */
64
74
  withInstallation(description: GraphDescription): BindingGraph;
65
75
  }
@@ -80,6 +90,7 @@ export declare class BagRuntime {
80
90
  acquire(key: BindingKey): Promise<void>;
81
91
  isTransient(key: BindingKey): boolean;
82
92
  inspect(key: BindingKey): RegistrationSnapshot<object, readonly unknown[]>;
93
+ inspectGraph(): GraphSnapshot;
83
94
  private inspectBinding;
84
95
  assertOpen(): void;
85
96
  scope(graph?: BindingGraph, shared?: readonly BindingId[]): BagRuntime;
package/dist/runtime.js CHANGED
@@ -267,6 +267,28 @@ class BindingGraph {
267
267
  publicSlots.set(key, id);
268
268
  return { bindings, publicSlots, contributions };
269
269
  }
270
+ /** Every retained binding in `describe()` order, with the public keys that select it. */
271
+ bindingSummaries() {
272
+ const keysById = new Map();
273
+ if (this.#publicOrder)
274
+ for (const key of (0, persistent_sequence_1.materialize)(this.#publicOrder)) {
275
+ const id = this.#publicSlots.get(key);
276
+ if (id === undefined)
277
+ continue;
278
+ const keys = keysById.get(id) ?? [];
279
+ if (!keys.includes(key))
280
+ keys.push(key);
281
+ keysById.set(id, keys);
282
+ }
283
+ return Object.freeze([...this.describe().bindings.keys()].map(id => Object.freeze({ id, keys: Object.freeze(keysById.get(id) ?? []) })));
284
+ }
285
+ /** Every contribution group with its member bindings in contribution order. */
286
+ contributionGroups() {
287
+ const groups = [];
288
+ for (const [key] of this.#contributions)
289
+ groups.push(Object.freeze({ token: key, bindingIds: this.contributionBindings(key) }));
290
+ return Object.freeze(groups);
291
+ }
270
292
  /** Install disjoint public slots atomically, retaining lexical private refs. */
271
293
  withInstallation(description) {
272
294
  for (const key of description.publicSlots.keys()) {
@@ -338,6 +360,25 @@ class BagRuntime {
338
360
  inspect(key) {
339
361
  return this.inspectBinding(this.graph.publicBinding(key));
340
362
  }
363
+ inspectGraph() {
364
+ const bindings = this.graph.bindingSummaries().map(({ id, keys }) => {
365
+ const description = this.graph.registration(id);
366
+ return Object.freeze({
367
+ ...this.inspectBinding(id),
368
+ keys,
369
+ lifetime: description.lifetime.kind,
370
+ acquisitionMode: description.acquisitionMode,
371
+ owned: description.dispose !== undefined || description.operations.some(operation => operation.kind === 'owned'),
372
+ tokenDependencies: Object.freeze(description.references.map(reference => Object.freeze({ key: reference.key, kind: reference.kind }))),
373
+ });
374
+ });
375
+ return Object.freeze({
376
+ scopeId: this.acquisitions.ownerId,
377
+ bindings: Object.freeze(bindings),
378
+ contributions: this.graph.contributionGroups(),
379
+ observedEdges: this.acquisitions.observedEdges(),
380
+ });
381
+ }
341
382
  inspectBinding(bindingId) {
342
383
  return Object.freeze({
343
384
  bindingId,
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { FactoryWithDisposal, Registration, Registrations } from './registration';
1
+ import type { Factory, FactoryWithDisposal, Registration, Registrations } from './registration';
2
2
  import type { ProviderContext, ProviderNamedDependencies, ProviderOutput, ProviderGraphContract, ProviderRequiredTokens, ProviderOptionalTokens } from './provider';
3
3
  import type { InvalidGraphs, MissingTokens, SelectionKey, TokenMember, ValidToken, TokenDependencyContract, WrongToken } from './token-types';
4
4
  export type Needs<R extends Registration> = ProviderNamedDependencies<R>;
@@ -24,6 +24,33 @@ export type RegistrationsFromEntries<E extends Entry> = {
24
24
  };
25
25
  /** Replace overlapping registrations in `F` with registrations from `N`. */
26
26
  export type OverrideRegistrations<F extends Registrations, N extends Registrations> = Omit<F, keyof N> & N;
27
+ /**
28
+ * Project-wide compile-time policy switches. Augment it to relax a check:
29
+ * `declare module 'di-bag' { interface DiBagPolicy { readonly structuralThenables: 'allow' } }`.
30
+ */
31
+ export interface DiBagPolicy {
32
+ }
33
+ type StructuralThenablesAllowed = DiBagPolicy extends {
34
+ readonly structuralThenables: 'allow';
35
+ } ? true : false;
36
+ type IsAny<T> = 0 extends 1 & T ? true : false;
37
+ /** True for a declared output with a callable `then` that is not a native Promise; `any` is exempt. */
38
+ export type StructuralThenable<O> = StructuralThenablesAllowed extends true ? false : IsAny<O> extends true ? false : O extends infer T & {} ? T extends Promise<unknown> ? false : T extends {
39
+ then(...args: never[]): unknown;
40
+ } ? true : false : false;
41
+ type ThenableOutputs<R extends Registrations> = {
42
+ [K in keyof R]: R[K] extends Factory | FactoryWithDisposal<Factory> ? true extends StructuralThenable<ProviderOutput<R[K]>> ? K : never : never;
43
+ }[keyof R];
44
+ /** Reject plain or disposable factories whose declared output auto acquisition would reject at runtime. */
45
+ export type ThenableAdmission<R extends Registrations> = [ThenableOutputs<R>] extends [never] ? unknown : Unsatisfied<`factory output is a structural thenable: ${NameText<ThenableOutputs<R>>}; return a native Promise or use DiBag.fromFactory with acquisitionMode raw or nativePromise`, {
46
+ tokens: ThenableOutputs<R>;
47
+ }>;
48
+ /**
49
+ * Render dependency names inside diagnostic messages; typed tokens have no printable name.
50
+ * Use it only in checks that run once per graph (build, module completeness, lifetimes, key selection):
51
+ * per-call wrong-shape checks stay plain because templates there cost instantiations on valid graphs.
52
+ */
53
+ export type NameText<K> = K extends string ? K : K extends number ? `${K}` : 'typed token';
27
54
  declare const diBagTypeError: unique symbol;
28
55
  export type Unsatisfied<Message extends string, Details> = {
29
56
  readonly [diBagTypeError]: Message;
@@ -107,7 +134,7 @@ export type CheckDependencyCompleteness<R extends Registrations> = [
107
134
  Exclude<RequiredOf<R>, keyof R> | MissingTokens<CompletionMap<R>>
108
135
  ] extends [never] ? [InvalidGraphs<CompletionMap<R>>] extends [never] ? unknown : Unsatisfied<'token dependency has an incompatible or opaque contract', {
109
136
  tokens: InvalidGraphs<CompletionMap<R>>;
110
- }> : Unsatisfied<'required service registrations are missing', {
137
+ }> : Unsatisfied<`required service registrations are missing: ${NameText<Exclude<RequiredOf<R>, keyof R> | MissingTokens<CompletionMap<R>>>}`, {
111
138
  missing: Exclude<RequiredOf<R>, keyof R> | MissingTokens<CompletionMap<R>>;
112
139
  relationships: MissingRelationships<R>;
113
140
  }>;
@@ -117,9 +144,9 @@ type BadOverrides<F extends Registrations, O extends Registrations> = {
117
144
  /** Admit overrides only for existing keys whose service values remain assignable. */
118
145
  export type Overrides<F extends Registrations, O extends Registrations> = [
119
146
  Exclude<keyof O, keyof F>
120
- ] extends [never] ? [BadOverrides<F, O>] extends [never] ? unknown : Unsatisfied<'override value is not assignable to the original token', {
147
+ ] extends [never] ? [BadOverrides<F, O>] extends [never] ? unknown : Unsatisfied<`override value is not assignable to the original token: ${NameText<BadOverrides<F, O>>}`, {
121
148
  tokens: BadOverrides<F, O>;
122
- }> : Unsatisfied<'fork accepts existing names or typed tokens only', {
149
+ }> : Unsatisfied<`fork accepts existing names or typed tokens only: unknown ${NameText<Exclude<keyof O, keyof F>>}`, {
123
150
  extra: Exclude<keyof O, keyof F>;
124
151
  }>;
125
152
  export type Introduces<F extends Registrations, N extends Registrations> = [
@@ -132,14 +159,14 @@ export type IntroducesKeys<Known extends PropertyKey, New extends PropertyKey> =
132
159
  duplicates: Known & New;
133
160
  }>;
134
161
  export type Singleton<K> = [K] extends [never] ? false : [K] extends [string] ? true extends IsUnion<K> ? false : [NonFiniteKeys<Record<K & string, never>>] extends [never] ? true : false : false;
135
- export type ReplacementKey<R extends Registrations, K extends string> = Singleton<K> extends true ? K extends keyof R ? unknown : Unsatisfied<'replace requires one existing singleton string-literal key', {
162
+ export type ReplacementKey<R extends Registrations, K extends string> = Singleton<K> extends true ? K extends keyof R ? unknown : Unsatisfied<`replace requires one existing singleton string-literal key: ${NameText<K>}`, {
136
163
  key: K;
137
- }> : Unsatisfied<'replace requires one existing singleton string-literal key', {
164
+ }> : Unsatisfied<`replace requires one existing singleton string-literal key: ${NameText<K>}`, {
138
165
  key: K;
139
166
  }>;
140
- export type ReplacementKeyOf<Keys extends PropertyKey, K extends string> = Singleton<K> extends true ? K extends Keys ? unknown : Unsatisfied<'replace requires one existing singleton string-literal key', {
167
+ export type ReplacementKeyOf<Keys extends PropertyKey, K extends string> = Singleton<K> extends true ? K extends Keys ? unknown : Unsatisfied<`replace requires one existing singleton string-literal key: ${NameText<K>}`, {
141
168
  key: K;
142
- }> : Unsatisfied<'replace requires one existing singleton string-literal key', {
169
+ }> : Unsatisfied<`replace requires one existing singleton string-literal key: ${NameText<K>}`, {
143
170
  key: K;
144
171
  }>;
145
172
  type ReplacementRequirement<N, K extends PropertyKey> = K extends keyof N ? (value: N[K]) => void : never;
@@ -157,7 +184,7 @@ type InvalidElements<K extends readonly unknown[]> = {
157
184
  }[number];
158
185
  type InvalidMembers<R extends Registrations, T> = T extends string ? never : unknown extends TokenMember<R, T> ? never : T;
159
186
  /** Validate a finite tuple of existing singleton names or genuine typed tokens. */
160
- export type Selection<R extends Registrations, K extends readonly unknown[], Operation extends string = 'fork'> = true extends IsUnion<K> ? InvalidSelection<Operation> : number extends K['length'] ? InvalidSelection<Operation> : K extends Required<K> ? [InvalidElements<K>] extends [never] ? [Exclude<SelectionKey<K[number]>, keyof R> | InvalidMembers<R, K[number]>] extends [never] ? unknown : Unsatisfied<`${Operation} accepts existing names or typed tokens only`, {
187
+ export type Selection<R extends Registrations, K extends readonly unknown[], Operation extends string = 'fork'> = true extends IsUnion<K> ? InvalidSelection<Operation> : number extends K['length'] ? InvalidSelection<Operation> : K extends Required<K> ? [InvalidElements<K>] extends [never] ? [Exclude<SelectionKey<K[number]>, keyof R> | InvalidMembers<R, K[number]>] extends [never] ? unknown : Unsatisfied<`${Operation} accepts existing names or typed tokens only: unknown ${NameText<Exclude<SelectionKey<K[number]>, keyof R> | InvalidMembers<R, K[number]>>}`, {
161
188
  extra: Exclude<SelectionKey<K[number]>, keyof R> | InvalidMembers<R, K[number]>;
162
189
  }> : InvalidSelection<Operation> : InvalidSelection<Operation>;
163
190
  type InvalidSelection<Operation extends string> = Unsatisfied<`${Operation} requires a finite tuple of singleton string-literal names or typed tokens`, {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "di-bag",
3
- "version": "0.1.0",
4
- "description": "Type-checked dependency composition with lazy factories, forks, and optional disposal.",
3
+ "version": "0.2.0",
4
+ "description": "Type-checked dependency composition, private modules, and resource ownership for TypeScript apps and LLM harnesses.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "exports": {
@@ -18,7 +18,9 @@
18
18
  "dist"
19
19
  ],
20
20
  "scripts": {
21
- "test": "bun test tests",
21
+ "test": "npm run test:fast && npm run test:compiler",
22
+ "test:fast": "node scripts/test-lane.mjs fast",
23
+ "test:compiler": "node scripts/test-lane.mjs compiler",
22
24
  "typecheck": "tsc6 -p tsconfig.json",
23
25
  "typecheck:native": "tsc -p tsconfig.json",
24
26
  "build:native": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
@@ -33,6 +35,7 @@
33
35
  "check:platform": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/platform-evidence.ts --required",
34
36
  "docs:generate": "node tools/docs/generate.mjs",
35
37
  "docs:check": "node --test --test-isolation=none tools/docs/test/*.test.mjs && node tools/docs/generate.mjs --check && node tools/docs/site.mjs check",
38
+ "graph:check": "node --test tools/graph/test/*.test.mjs",
36
39
  "docs:build": "node tools/docs/site.mjs build",
37
40
  "docs:dev": "node tools/docs/site.mjs dev",
38
41
  "docs:preview": "node tools/docs/site.mjs preview",
@@ -49,7 +52,20 @@
49
52
  },
50
53
  "keywords": [
51
54
  "di",
52
- "bag"
55
+ "bag",
56
+ "dependency-injection",
57
+ "typescript",
58
+ "modularity",
59
+ "context-engineering",
60
+ "agentic",
61
+ "agentic-development",
62
+ "llm",
63
+ "harness",
64
+ "agent-harness",
65
+ "graph",
66
+ "agent-graph",
67
+ "evals",
68
+ "metadata"
53
69
  ],
54
70
  "author": "Dany Fedorov",
55
71
  "license": "MIT",