katagami 3.0.2 → 4.0.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.
Files changed (44) hide show
  1. package/README.md +84 -37
  2. package/dist/chunk-R66WOZUM.js +71 -0
  3. package/dist/container/index.d.cts +142 -35
  4. package/dist/container/index.d.ts +142 -35
  5. package/dist/container/policy.d.cts +36 -0
  6. package/dist/container/policy.d.ts +36 -0
  7. package/dist/disposable/index.cjs +27 -5
  8. package/dist/disposable/index.d.cts +17 -7
  9. package/dist/disposable/index.d.ts +17 -7
  10. package/dist/disposable/index.js +2 -41
  11. package/dist/entrypoint/index.d.cts +20 -0
  12. package/dist/entrypoint/index.d.ts +20 -0
  13. package/dist/index.cjs +551 -149
  14. package/dist/index.d.cts +8 -0
  15. package/dist/index.d.ts +8 -0
  16. package/dist/index.js +495 -150
  17. package/dist/internal.d.cts +65 -0
  18. package/dist/internal.d.ts +65 -0
  19. package/dist/metadata/index.d.cts +67 -0
  20. package/dist/metadata/index.d.ts +67 -0
  21. package/dist/resolver/index.d.cts +5 -0
  22. package/dist/resolver/index.d.ts +5 -0
  23. package/dist/scope/index.d.cts +49 -24
  24. package/dist/scope/index.d.ts +49 -24
  25. package/dist/scope/operations.d.cts +22 -0
  26. package/dist/scope/operations.d.ts +22 -0
  27. package/docs/README.de.md +3 -2
  28. package/docs/README.es.md +3 -2
  29. package/docs/README.fr.md +3 -2
  30. package/docs/README.ja.md +61 -27
  31. package/docs/README.ko.md +3 -2
  32. package/docs/README.zh-CN.md +3 -2
  33. package/docs/README.zh-TW.md +3 -2
  34. package/docs/ai-coding-agents.md +3 -3
  35. package/docs/articles/ai-coding-agents.md +1 -1
  36. package/docs/articles/request-scope.md +2 -1
  37. package/docs/choosing-di.md +36 -9
  38. package/docs/guide.md +35 -4
  39. package/docs/registration-policies.ja.md +261 -0
  40. package/docs/registration-policies.md +435 -0
  41. package/docs/type-safety.md +13 -4
  42. package/llms.txt +1 -0
  43. package/package.json +1 -1
  44. package/dist/chunk-J2NYR3SH.js +0 -6
@@ -0,0 +1,435 @@
1
+ # Registration policies and operations
2
+
3
+ Japanese: [登録の属性と公開操作](./registration-policies.ja.md)
4
+
5
+ You can attach metadata to registrations and derive resolution-time checks and public operations from
6
+ those same registrations. Define the shared policy once as `policy` and pass the same object reference
7
+ to each module. You do not need to maintain a separate list of target tokens. What the metadata means,
8
+ and what to allow or deny, is decided by your code.
9
+
10
+ The plain `createContainer()` and `createScope(container)` keep working as they are. The policies and
11
+ operations described here are features you add when you need them.
12
+
13
+ ## Assemble modules with one policy
14
+
15
+ ```ts
16
+ import { createContainer, createMetadataKey, createScope, entrypoint } from 'katagami';
17
+ import type { ContainerPolicy } from 'katagami';
18
+
19
+ const EXPOSURE = createMetadataKey<'internal' | 'public'>()('exposure');
20
+ const policy = {
21
+ name: 'reports',
22
+ requiredMetadata: [EXPOSURE] as const,
23
+ beforeReturn({ registrations }) {
24
+ if (registrations.some(registration =>
25
+ registration.metadata.require(EXPOSURE) === 'internal'
26
+ )) {
27
+ throw new Error('Internal dependency instances cannot be exposed');
28
+ }
29
+ },
30
+ } satisfies ContainerPolicy;
31
+
32
+ function createReports(options: { policy: typeof policy }) {
33
+ return createContainer({ policy: options.policy })
34
+ .registerSingleton('repository', () => ({
35
+ read: (id: string) => `report:${id}`,
36
+ }), {
37
+ metadata: [EXPOSURE('internal')],
38
+ })
39
+ .registerScoped('readReport', entrypoint(resolver => {
40
+ const repository = resolver.resolve('repository');
41
+ return (id: string) => ({ id, title: repository.read(id) });
42
+ }), {
43
+ metadata: [EXPOSURE('public')],
44
+ });
45
+ }
46
+
47
+ const container = createContainer({ policy }).use(createReports({ policy }));
48
+ await using operations = createScope(container, { access: 'operations' });
49
+ const readReport = operations.get('readReport');
50
+ const report = await readReport('42');
51
+ ```
52
+
53
+ `entrypoint(factory)` marks a factory that returns a function to expose. In the example above, only
54
+ `readReport` can be invoked. The resolver used to resolve `repository` stays inside the factory, and
55
+ callers pass ordinary arguments. The operation produces the result; `policy` does not transform it.
56
+
57
+ `ContainerPolicy` is a public type for use with `satisfies`. It types the hook arguments while keeping
58
+ the concrete keys of `requiredMetadata: [EXPOSURE] as const`. If you widen the type with
59
+ `const policy: ContainerPolicy` or `as ContainerPolicy`, individual required keys can no longer be
60
+ type-checked.
61
+
62
+ Pass the original `policy` reference to modules. Rebuilding the outer `{ policy }` argument still keeps
63
+ the policy shared, but copying the policy itself with `{ ...policy }` creates a different policy. The
64
+ module in this example supports policies it can satisfy by attaching `EXPOSURE`; it makes no promise to
65
+ support policies with arbitrary required metadata.
66
+
67
+ A runnable example in the repository is
68
+ [examples/registration-policies.ts](https://github.com/hiroiku/katagami/blob/master/examples/registration-policies.ts).
69
+ After building, run it with `bun examples/registration-policies.ts`.
70
+
71
+ ## Metadata and required keys
72
+
73
+ `createMetadataKey<T>()('name')` separates specifying the value type from inferring the key name. The
74
+ name can be a string literal or a unique symbol. A key with a different name does not satisfy a
75
+ required key, even if its value type is the same. Separately created keys with the same name cannot be
76
+ told apart by type alone, so the runtime check uses the identity of the key object. Share key
77
+ definitions as well.
78
+
79
+ Setting `policy.requiredMetadata` requires each registration's `metadata` to include the required keys.
80
+ Missing keys are checked both by type and at runtime, at registration and on `.use()`. Older
81
+ registrations for the same token are still resolved by `resolveAll`, so they are not exempt from the
82
+ check. The runtime check is not skipped even when the type has lost the registration history. Duplicate
83
+ or invalid entries are also rejected, and you can register additional metadata that is not required.
84
+
85
+ `container.getMetadata(token)` returns only the metadata of the last registration and does not run the
86
+ factory. An unregistered token throws `ContainerError`. The reader has the following methods.
87
+
88
+ | Method | Result |
89
+ | --- | --- |
90
+ | `get(key)` | The metadata value, or `undefined` if not set |
91
+ | `require(key)` | The metadata value; throws if not set |
92
+ | `has(key)` | Whether the metadata exists |
93
+
94
+ Readers and entries are read-only, but objects you pass as metadata values are not frozen internally.
95
+
96
+ ### Conditions for `.use()`
97
+
98
+ | Source module | Target container | Behavior |
99
+ | --- | --- | --- |
100
+ | No policy | No policy | Ordinary composition |
101
+ | No policy | Has a policy | Checks every registration for the target's required metadata, then composes |
102
+ | Same policy | Same policy | Checks every registration, then composes |
103
+ | Different policy | Has a policy | Rejected |
104
+ | Has a policy | No policy | Rejected |
105
+
106
+ A rejected `.use()` never applies only some of the registrations. Composition copies registration
107
+ definitions; it does not merge the module's cache into the target. If you create each module with
108
+ `createContainer({ policy })`, missing metadata is also detected when the module registers.
109
+
110
+ ## Policy identity and fixed settings
111
+
112
+ Containers that use a `policy` with the same original object reference share its settings and the
113
+ record of instance origins. A different object with the same `name` is a different policy. `name` is
114
+ a label for telling policies apart in your own code; katagami does not include it in errors or events,
115
+ and never uses it as a key for sharing or authorization. Containers without an explicit
116
+ policy get no implicit shared return check.
117
+
118
+ On first use, katagami validates the settings, copies the array of required keys and fixes its internal
119
+ settings. Hooks are also taken as the function references at that moment. Katagami neither modifies nor
120
+ freezes your `policy` object or its arrays, so `Object.freeze` is unnecessary. `as const` and
121
+ `satisfies` do not freeze anything at runtime either.
122
+
123
+ Changing the original definition after first use does not affect existing containers. Passing the same,
124
+ modified object to `createContainer({ policy })` again is rejected. To change `name`, the order or
125
+ references of the required keys, or the hook references, prepare a new policy object.
126
+
127
+ Use data properties for settings. Accessors are rejected without running their getters. Detecting that
128
+ an object is a Proxy is not guaranteed.
129
+
130
+ Katagami does not fix the state that hook closures refer to, or the internals of metadata values. Keep
131
+ per-request authorization state out of the shared policy and pass it to the scope's `beforeResolve`
132
+ instead. Even with the same policy, separate roots do not share singleton caches, request state or
133
+ resource ownership.
134
+
135
+ ## Instance origins and return checks
136
+
137
+ ### Recording resolved references
138
+
139
+ For each object or function actually resolved within one policy, katagami records the reference and the
140
+ details of its registration at that time (token, lifetime, whether it is an `entrypoint`, and metadata).
141
+ Primitives such as strings, numbers and symbols are not tracked, even when the values are equal.
142
+
143
+ - Resolving the same instance under another token adds an origin. Existing origins are not removed.
144
+ - Overriding a token keeps the old origins of instances already created.
145
+ - Origins whose registration details are all identical are merged into one, even across different
146
+ registrations or containers. Even if you rebuild the container for every request, the record for one
147
+ instance grows no larger than the number of origins `beforeReturn` can tell apart. Metadata is
148
+ compared regardless of order, by key reference and value identity (`Object.is`). If you use values
149
+ that change per request, such as a request ID, or objects created anew for each request, as metadata
150
+ values or tokens, each request counts as a separate origin and the record grows by that many.
151
+ - A new instance does not inherit the origins of an old one.
152
+ - For an `async` factory or a factory that returns a native `Promise`, katagami builds a chain that awaits
153
+ completion and records the fulfilled value inside it. Callers receive the end of this chain, not the
154
+ promise the factory returned. Custom properties attached to that promise are not carried over, so if
155
+ you want to hand out an object as the value, make it a non-Promise instance or a custom thenable. The
156
+ cache also holds this chain end, and resolving again in the same scope returns the same promise. A
157
+ top-level resolution under a policy with a return check returns a promise per call, as described below.
158
+ - Records are shared across caches, every lifetime, parent/child and sibling scopes, and separate roots
159
+ that use the same policy. Records of different policies never mix.
160
+
161
+ A custom thenable (a user-defined object with `then`) returned by a synchronous factory is recorded by
162
+ reference as an ordinary dependency instance. Katagami keeps the original instance, cache and custom
163
+ methods, does not touch `then` or its getter, and does not subscribe to its completion. It does not
164
+ automatically record an origin for its completion value. If you want the completion value treated as a
165
+ registered dependency, make that explicit with an ordinary async factory, such as
166
+ `async () => await thenable`.
167
+
168
+ To create an alias, `resolve` the original token inside an ordinary factory. The original origin is then
169
+ observed as well. Katagami does not guess origins for factories that have not run, or for references
170
+ brought in without going through a registration. Nor does it replace old origins with the current
171
+ `getMetadata(token)`.
172
+
173
+ Records are held through weak references so they do not keep instances alive unnecessarily, and they are
174
+ not cleared when a scope ends. A record holds only the origin details (token, lifetime, whether it is an
175
+ `entrypoint`, and metadata keys and values). It holds no registrations, factories or their closures,
176
+ caches, scopes or containers, so even when an instance or the policy outlives the container, none of
177
+ these outlive the container. However, if a token or a metadata value itself references the container,
178
+ the container stays alive through the record. Settings and records are also shared between the ESM and
179
+ CJS builds of the same package when they receive the same policy reference. This is not guaranteed
180
+ across different versions or realms.
181
+
182
+ ### Promises you receive are left untouched
183
+
184
+ For its bookkeeping and disposal, katagami never attaches `then` / `catch` / `finally` to a promise it has
185
+ handed to a caller. It does its bookkeeping inside a chain of its own and hands out the end of that chain. The creation results that
186
+ disposal uses to know what to close, and the completion notices that closing uses to wait for running
187
+ operations, are also kept by katagami itself, separately from the promises it hands out.
188
+
189
+ As a result, if you drop a promise you received and it fails, the failure surfaces as an unhandled
190
+ rejection, as the runtime does by default. Even when scope disposal or the closing of an operations
191
+ scope overlaps, katagami never takes over that failure before you do. Always receive the return value of
192
+ `resolve` and the promise an operation returns, and either `await` it or handle it with `catch`.
193
+
194
+ A promise that a singleton or scoped creation put in the cache is shared with later resolutions,
195
+ though. A top-level resolution under a policy with a return check, and a call through an operations
196
+ scope, build their own promise from the shared one and wait on it. From then on, the failure of the
197
+ shared promise is handled and reaches that later caller, even if an earlier caller dropped the same
198
+ promise, just as when another caller `await`s the same cached promise.
199
+
200
+ Failures of creations that could not be handed to a caller are not hidden either. If another
201
+ registration fails synchronously partway through `resolveAll`, failures of asynchronous creations that
202
+ had already started surface as unhandled rejections. Singleton and scoped creations remain in the cache,
203
+ so resolving the same token again gives you the same failure. Transient registrations run the factory
204
+ again on every resolution, so the original failure cannot be recovered.
205
+
206
+ ### What `beforeReturn` covers
207
+
208
+ `beforeReturn({ registrations })` is a synchronous check that keeps values the policy forbids from
209
+ leaving through any surface. It covers the following two surfaces, and both look only at observed
210
+ origins.
211
+
212
+ | Surface | Values checked |
213
+ | --- | --- |
214
+ | Ordinary resolver scope | Values returned by top-level resolution with `resolve`, `resolveAll`, `tryResolve` and `tryResolveAll` |
215
+ | Operations scope | Fulfillment values returned directly by public operations |
216
+
217
+ Resolutions requested by a factory (those that carry a `requester` in `beforeResolve`) are inside the
218
+ surface, so they are not checked. Factories are free to receive the dependencies they need for assembly,
219
+ and the policy decides only what goes out. The path by which an operations scope resolves the invoking
220
+ function of a public operation is also inside the surface. To `beforeResolve`, this internal resolution
221
+ looks the same as a top-level one, with no `requester`, so you cannot tell inside from outside the
222
+ surface by `beforeResolve`'s `requester` alone. An optional resolution of an unregistered token produces
223
+ no value, so it returns `undefined` without a check. `lazy` calls `resolve` on the first property
224
+ access, so the check happens at that point.
225
+
226
+ Origins are tracked only for objects and functions. A primitive result is checked with empty
227
+ `registrations`, so if a registration tagged with `area` returns a string or number, a decision based
228
+ on origins does not stop it. If you pass a factory's resolver itself out through an operation result or a
229
+ closure, resolutions through that resolver are treated as inside the surface and are not checked.
230
+ Register things in a way that keeps the resolver from leaving. A factory that synchronously calls the
231
+ public methods of the scope building it resolves inside its own construction as well: lifetime and cycle
232
+ checks apply, and the return check does not.
233
+
234
+ A synchronous factory's value is checked on the spot; for an asynchronous factory, the resolved value is
235
+ checked before it is handed to the caller. With a policy that has a return check, each top-level
236
+ asynchronous resolution is checked per call, so every call returns its own promise for the same
237
+ creation. For an operation that returns a promise or thenable, the result is `await`ed and only the
238
+ known origins of the fulfillment value are checked. The origin of a custom thenable itself is not
239
+ assumed to carry over to its completion value. `registrations` is a read-only snapshot of the origins
240
+ observed for the result reference. The operation's own registration is never passed in place of the
241
+ result's origins. For primitives and results of unknown origin, it is an empty array.
242
+
243
+ To allow, return nothing; to deny, throw. Async hooks cannot be used, and the hook cannot transform the
244
+ return value. The value is not handed to the caller until the check completes. On denial, a synchronous
245
+ resolution throws, and an asynchronous resolution or an operation rejects its promise. The caller
246
+ receives the error the hook threw, with its `cause` intact.
247
+
248
+ In the opening example, the return is denied if any observed origin is `internal`. Katagami does not
249
+ define what that classification means or which value takes precedence.
250
+
251
+ The check is not recursive. It does not inspect nested values such as `{ repository }`, the inside of a
252
+ `Result`, exceptions or their `cause`, closures, or values a stream emits later. The reason an operation
253
+ rejects with is also out of scope. It is neither a mechanism for detecting arbitrary information leaks
254
+ nor a JavaScript security sandbox.
255
+
256
+ ## Getting and invoking operations
257
+
258
+ ```ts
259
+ import { createContainer, createScope, entrypoint } from 'katagami';
260
+
261
+ const container = createContainer()
262
+ .registerScoped('repository', () => ({ read: (id: string) => `report:${id}` }))
263
+ .registerScoped('readReport', entrypoint(resolver => {
264
+ const repository = resolver.resolve('repository');
265
+ return (id: string, prefix = '', ...labels: string[]) =>
266
+ `${prefix}${repository.read(id)}${labels.join(',')}`;
267
+ }));
268
+
269
+ await using operations = createScope(container, { access: 'operations' });
270
+ const readReport = operations.get('readReport');
271
+ const report: string = await readReport('42', 'Report: ', 'draft');
272
+ // @ts-expect-error — a non-public registration cannot be retrieved as an operation; wrap its factory in entrypoint() to expose it
273
+ operations.get('repository');
274
+ ```
275
+
276
+ `createScope(container, { access: 'operations' })` always creates a new scope, and its public surface is
277
+ only `get` and `Symbol.asyncDispose`. Passing an ordinary resolver scope or a disposable scope as the
278
+ source also creates a new child scope. It is not an API that turns the source into a view with the same
279
+ scoped cache and lifetime.
280
+
281
+ `get(name)` synchronously checks that the token is public and returns an invoking function bound to the
282
+ scope. **Getting the function does not create any dependency.** It returns neither the raw registered
283
+ function, nor a resolver, nor the registration map. On invocation, it checks whether the scope is
284
+ closing, re-checks the public registration, resolves normally, runs the operation and applies the
285
+ return check, in that order.
286
+
287
+ If `.use()` or an additional registration makes the last registration non-public, calls through
288
+ previously obtained functions are rejected too. The authorization result at retrieval time is not
289
+ remembered to skip the resolution checks at call time. `beforeResolve` runs even when the value comes
290
+ from the cache.
291
+
292
+ `entrypoint` supports singleton, transient and scoped registrations, and async factories. It does not
293
+ automatically expose every method of a class. For a method that needs `this`, `bind` it to the owning
294
+ instance inside the factory. Make operations that use request state scoped.
295
+
296
+ Invocations always return a promise, even for synchronous operations. Fixed, optional and rest
297
+ parameters and the result type are inferred from the registration. For overloads, the last signature is
298
+ used; not every signature is kept. Generic functions also lose the correspondence between input and
299
+ output types. Expose such functions wrapped in a function with concrete parameter and result types.
300
+
301
+ You can pass just the operation functions you need to other code, but they cannot be used beyond the
302
+ lifetime of the scope they were obtained from. The ordinary resolver `createScope(container)` and its
303
+ `resolve` and other methods remain as they are. The `entrypoint`-only exposure restriction belongs to the
304
+ operations scope surface alone and does not apply to paths that pass an ordinary resolver. `beforeReturn`
305
+ applies to the "outside" of each surface; for an ordinary resolver scope, that is top-level resolution.
306
+
307
+ ## Closing and long-running work
308
+
309
+ With `await using operations = createScope(container, { access: 'operations' })`, leaving the block
310
+ waits for the scope to close.
311
+
312
+ Once closing starts, new invocations are rejected, including those through functions already obtained.
313
+ Invocations that started before closing are awaited to completion, including internal resolution after
314
+ `await` and `beforeReturn`; whether they succeed or fail, the scope is disposed only after the result has
315
+ reached the invocation's promise. Duplicate close requests join the same closing process. Singletons
316
+ remain owned by the original container and are not disposed when an operations scope closes.
317
+
318
+ Disposal is responsible only for closing instances that were successfully created. A registration whose
319
+ async factory rejected yields nothing to close, so closing awaits it, skips it, continues disposing the
320
+ remaining instances, and does not include it in the `AggregateError`. The creation failure is received by
321
+ the caller that resolved the token (the return value of `resolve`, or the promise an operation returns).
322
+ This way, business or authorization failures and resources that genuinely could not be disposed are each
323
+ reported once, through their own promise. A failure you drop without receiving surfaces as is, as an
324
+ unhandled rejection in the runtime.
325
+
326
+ If an operation's promise returns a stream or async iterator, the invocation completes when that value
327
+ is returned. Consumption, completion and cancellation of the stream are not tracked automatically. If
328
+ consumption or cleanup needs scoped resources, keep the `await using` block open until it completes.
329
+ Also complete notifications, job records, lease releases and the like within the lifetime they need.
330
+
331
+ When authorization state changes, do not rewrite the state of a running scope; switch to a new
332
+ operations scope with the new fixed state. Start closing the old scope to stop new invocations, and wait
333
+ for the work already started and for closing to finish. Functions already obtained belong to the old
334
+ scope and are not automatically rebound to the new one.
335
+
336
+ ## What resolution hooks cover
337
+
338
+ ```ts
339
+ import { createContainer, createMetadataKey, createScope, entrypoint } from 'katagami';
340
+ import type { ContainerPolicy, OperationsScopeOptions } from 'katagami';
341
+
342
+ const AREA = createMetadataKey<'reports' | 'settings'>()('area');
343
+ const policy = {
344
+ name: 'application',
345
+ requiredMetadata: [AREA] as const,
346
+ } satisfies ContainerPolicy;
347
+ const container = createContainer({ policy })
348
+ .registerScoped('countReports', entrypoint(() => () => 3), {
349
+ metadata: [AREA('reports')],
350
+ });
351
+
352
+ const allowedAreas = new Set(['reports']);
353
+ const options = {
354
+ access: 'operations',
355
+ beforeResolve(event) {
356
+ if (!allowedAreas.has(event.metadata.require(AREA))) {
357
+ throw new Error('This dependency is not available');
358
+ }
359
+ },
360
+ } satisfies OperationsScopeOptions;
361
+ await using operations = createScope(container, options);
362
+ const count = await operations.get('countReports')();
363
+ ```
364
+
365
+ When you store the options in a variable, `satisfies OperationsScopeOptions` keeps the type of `access`
366
+ as `'operations'` and types the hook arguments too. An object whose `access` has widened to `string`, or
367
+ a misspelled `access`, cannot be passed to `createScope`. Even if you pass it with the types stripped,
368
+ any `access` other than omitted or `'operations'` is rejected at runtime with `ContainerError`; it is
369
+ never created as an ordinary scope.
370
+
371
+ In this example, `allowedAreas` holds authorization that your code settled in advance, and it does not
372
+ change during the scope's lifetime. Katagami does not make business authorization decisions.
373
+
374
+ `beforeResolve` is a synchronous function. To deny, throw; to allow, return nothing. Async hooks are
375
+ rejected. It is also available on ordinary resolver scopes.
376
+
377
+ It covers registered dependencies for `resolve`, `resolveAll`, `tryResolve` and `tryResolveAll`, and it
378
+ also runs before a cached value is returned. Optional resolution of an unknown token returns `undefined`,
379
+ as before. Child scopes inherit the parent's policy, and additional hooks cannot remove the parent's
380
+ checks.
381
+
382
+ The event has `token`, `lifetime`, `metadata`, `entrypoint`, `requester` and `path`. A factory's resolver
383
+ keeps its owning scope and requester, and they stay the same after `await`. Dependencies held by cached
384
+ instances, and method calls on an already resolved `lazy`, cause no new resolution, so the hook does not
385
+ re-check the past dependency graph every time.
386
+
387
+ ### Singletons and policy ownership
388
+
389
+ When a singleton factory keeps its resolver in a closure, that resolver stays bound to the scope that
390
+ created the singleton. Even if you obtain and call the singleton from another scope, subsequent `resolve`
391
+ calls use the creating scope's hooks. Once the creating scope is closed, that resolver cannot be used. It
392
+ is not rebound to the calling scope.
393
+
394
+ Even when a resolver kept by a singleton factory obtains a scoped dependency later, in a callback or after
395
+ `await`, it is rejected as a captive dependency. Make request-dependent callbacks scoped, or inject
396
+ dependencies with an appropriate lifetime at creation time. Make only request-independent shared
397
+ infrastructure singleton.
398
+
399
+ ## Preserve type information
400
+
401
+ Let the registration chain and the return value of `.use()` be inferred.
402
+ `RegisteredTokens<typeof container>` gets the types of the tokens the registration chain tracked as
403
+ registered. A container created from a predeclared type map is tracked only from its first registration
404
+ with metadata or an `entrypoint`; before that, and without such a registration, the result is `never`. A predeclared
405
+ service type is not proof that a token is registered.
406
+
407
+ Assigning to an existing `Container<T>` annotation keeps the ordinary resolver types but erases the
408
+ registration history. Erased history is never used as grounds for required metadata or public
409
+ operations. Public operations added explicitly afterwards are inferred. If you need the required
410
+ metadata guarantee of `.use()`, keep the inference from `createContainer({ policy })`.
411
+
412
+ When you combine a predeclared service type with a policy, specify the type arguments as follows,
413
+ because of TypeScript's limits on partial type argument inference. You do not need to list the required
414
+ keys again in the type arguments.
415
+
416
+ ```ts
417
+ import { createContainer, createMetadataKey } from 'katagami';
418
+ import type { ContainerPolicy } from 'katagami';
419
+
420
+ const AREA = createMetadataKey<string>()('area');
421
+ const policy = {
422
+ name: 'clock',
423
+ requiredMetadata: [AREA] as const,
424
+ } satisfies ContainerPolicy;
425
+ interface Dependencies { clock: () => number }
426
+ const container = createContainer<Dependencies, Record<never, never>, typeof policy>({ policy })
427
+ .registerSingleton('clock', () => Date.now, { metadata: [AREA('core')] })
428
+ .registerScoped('now', resolver => resolver.resolve('clock')(), {
429
+ metadata: [AREA('core')],
430
+ });
431
+ ```
432
+
433
+ Types cannot fully prove arbitrary changes through mutable aliases, arrays declared with broad value
434
+ types, or type assertions. Use the runtime checks as well, and create scopes after you finish assembling
435
+ the registrations.
@@ -113,10 +113,19 @@ Use a predeclared map when order independence is useful and verify the compositi
113
113
  ## Runtime checks and limits
114
114
 
115
115
  Runtime checks report missing registrations, disposed scopes and circular resolution paths.
116
- The captive-dependency guard detects scoped resolution in an active singleton call chain,
117
- including indirect synchronous calls. It does not prove all lifetime relationships: in particular,
118
- do not rely on it for work resumed after an `await` or for dependencies captured from another scope.
119
- Use the typed factory API and runtime tests together.
116
+ The captive-dependency guard detects scoped resolution from a singleton factory, including indirect calls.
117
+ The resolver passed to a factory keeps its requester and the singleton restriction, so resolutions made
118
+ after an `await`, or later through a stored resolver, are checked too. A factory that calls the scope
119
+ building it directly, instead of its resolver, is checked only while it runs synchronously; after an
120
+ `await`, such a call counts as a call from outside. Dependencies captured from another scope through a
121
+ closure are not covered. While an async creation is pending, the factories building it cannot resolve the
122
+ same token again, even without waiting for it. A creation that waits for its own Promise through a
123
+ dependency that has already finished building is not detected and never settles. Synchronous cycles are
124
+ reported whichever resolver or scope they pass through. Use the typed factory API and runtime tests
125
+ together.
126
+
127
+ See [registration policies and operations](./registration-policies.md) for the additional guarantees of
128
+ required metadata and public operations, and for their limits when types are erased.
120
129
 
121
130
  ## Verification
122
131
 
package/llms.txt CHANGED
@@ -8,6 +8,7 @@
8
8
  - [AI coding agents](https://github.com/hiroiku/katagami/blob/master/docs/ai-coding-agents.md): workflow, diagnostics and project instructions.
9
9
  - [Type safety](https://github.com/hiroiku/katagami/blob/master/docs/type-safety.md): accumulated tokens, structural classes, declared maps and limits.
10
10
  - [API guide](https://github.com/hiroiku/katagami/blob/master/docs/guide.md): lifetimes, factories, modules, disposal and lazy resolution.
11
+ - [Registration policies](https://github.com/hiroiku/katagami/blob/master/docs/registration-policies.md): a shared policy object, required metadata, beforeReturn and operations scopes, with their contracts and limits.
11
12
  - [Request-scope starter](https://github.com/hiroiku/katagami/tree/master/examples/request-scope): a runnable example with fake injection and cleanup.
12
13
  - [Choosing DI](https://github.com/hiroiku/katagami/blob/master/docs/choosing-di.md): use cases and trade-offs.
13
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "katagami",
3
- "version": "3.0.2",
3
+ "version": "4.0.0",
4
4
  "description": "Type-safe dependency injection for TypeScript, with inferred types and scope checks for AI-assisted development. No decorators or reflect-metadata.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +0,0 @@
1
- // src/internal.ts
2
- var INTERNALS = /* @__PURE__ */ Symbol.for("katagami.internals.v3");
3
-
4
- export {
5
- INTERNALS
6
- };