@loutrejs/loutre 0.1.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 (103) hide show
  1. package/dist/adapters/aws-lambda.d.ts +52 -0
  2. package/dist/adapters/aws-lambda.js +114 -0
  3. package/dist/adapters/bun.d.ts +20 -0
  4. package/dist/adapters/bun.js +82 -0
  5. package/dist/adapters/cloudflare-workers.d.ts +18 -0
  6. package/dist/adapters/cloudflare-workers.js +55 -0
  7. package/dist/adapters/deno.d.ts +28 -0
  8. package/dist/adapters/deno.js +106 -0
  9. package/dist/adapters/electron.d.ts +27 -0
  10. package/dist/adapters/electron.js +52 -0
  11. package/dist/application/binding.d.ts +33 -0
  12. package/dist/application/binding.js +292 -0
  13. package/dist/application/host.d.ts +8 -0
  14. package/dist/application/host.js +14 -0
  15. package/dist/application/index.d.ts +62 -0
  16. package/dist/application/index.js +11 -0
  17. package/dist/application/openapi.d.ts +28 -0
  18. package/dist/application/openapi.js +362 -0
  19. package/dist/application/queue.d.ts +3 -0
  20. package/dist/application/queue.js +4 -0
  21. package/dist/application/scheduler.d.ts +2 -0
  22. package/dist/application/scheduler.js +45 -0
  23. package/dist/core/args.d.ts +16 -0
  24. package/dist/core/args.js +29 -0
  25. package/dist/core/context-key.d.ts +14 -0
  26. package/dist/core/context-key.js +14 -0
  27. package/dist/core/contract.d.ts +54 -0
  28. package/dist/core/contract.js +30 -0
  29. package/dist/core/env.d.ts +16 -0
  30. package/dist/core/env.js +29 -0
  31. package/dist/core/error.d.ts +24 -0
  32. package/dist/core/error.js +25 -0
  33. package/dist/core/execution.d.ts +78 -0
  34. package/dist/core/execution.js +61 -0
  35. package/dist/core/implementation.d.ts +34 -0
  36. package/dist/core/implementation.js +30 -0
  37. package/dist/core/index.d.ts +15 -0
  38. package/dist/core/index.js +15 -0
  39. package/dist/core/injection.d.ts +28 -0
  40. package/dist/core/injection.js +35 -0
  41. package/dist/core/layer.d.ts +109 -0
  42. package/dist/core/layer.js +52 -0
  43. package/dist/core/lifecycle.d.ts +34 -0
  44. package/dist/core/lifecycle.js +3 -0
  45. package/dist/core/module.d.ts +53 -0
  46. package/dist/core/module.js +31 -0
  47. package/dist/core/provider.d.ts +57 -0
  48. package/dist/core/provider.js +63 -0
  49. package/dist/core/runtime-input.d.ts +11 -0
  50. package/dist/core/runtime-input.js +1 -0
  51. package/dist/core/schema.d.ts +13 -0
  52. package/dist/core/schema.js +20 -0
  53. package/dist/core/token.d.ts +16 -0
  54. package/dist/core/token.js +13 -0
  55. package/dist/graph/graph.d.ts +17 -0
  56. package/dist/graph/graph.js +1078 -0
  57. package/dist/graph/index.d.ts +2 -0
  58. package/dist/graph/index.js +2 -0
  59. package/dist/graph/ir.d.ts +159 -0
  60. package/dist/graph/ir.js +1 -0
  61. package/dist/http/application.d.ts +18 -0
  62. package/dist/http/application.js +506 -0
  63. package/dist/http/basic-auth.d.ts +37 -0
  64. package/dist/http/basic-auth.js +67 -0
  65. package/dist/http/cors.d.ts +17 -0
  66. package/dist/http/cors.js +171 -0
  67. package/dist/http/definitions.d.ts +217 -0
  68. package/dist/http/definitions.js +65 -0
  69. package/dist/http/hosted-protocol.d.ts +4 -0
  70. package/dist/http/hosted-protocol.js +4 -0
  71. package/dist/http/index.d.ts +7 -0
  72. package/dist/http/index.js +6 -0
  73. package/dist/http/params.d.ts +2 -0
  74. package/dist/http/params.js +19 -0
  75. package/dist/http/path.d.ts +55 -0
  76. package/dist/http/path.js +88 -0
  77. package/dist/http/request-body.d.ts +4 -0
  78. package/dist/http/request-body.js +6 -0
  79. package/dist/http/validate.d.ts +20 -0
  80. package/dist/http/validate.js +6 -0
  81. package/dist/index.d.ts +2 -0
  82. package/dist/index.js +2 -0
  83. package/dist/message-port/environment.d.ts +2 -0
  84. package/dist/message-port/environment.js +4 -0
  85. package/dist/message-port/index.d.ts +65 -0
  86. package/dist/message-port/index.js +182 -0
  87. package/dist/runtime/application.d.ts +37 -0
  88. package/dist/runtime/application.js +324 -0
  89. package/dist/runtime/capability.d.ts +16 -0
  90. package/dist/runtime/capability.js +18 -0
  91. package/dist/runtime/di.d.ts +37 -0
  92. package/dist/runtime/di.js +441 -0
  93. package/dist/runtime/error.d.ts +10 -0
  94. package/dist/runtime/error.js +15 -0
  95. package/dist/runtime/index.d.ts +7 -0
  96. package/dist/runtime/index.js +7 -0
  97. package/dist/runtime/logger.d.ts +36 -0
  98. package/dist/runtime/logger.js +207 -0
  99. package/dist/runtime/node-capabilities.d.ts +5 -0
  100. package/dist/runtime/node-capabilities.js +18 -0
  101. package/dist/runtime/pipeline.d.ts +11 -0
  102. package/dist/runtime/pipeline.js +161 -0
  103. package/package.json +81 -0
@@ -0,0 +1,1078 @@
1
+ import { argumentsProvider, asModuleInstance, childPipelineOf, contextKeyName, defineModule, isArgsClass, isEnvClass, layerDefinitionOf, normalizeProvider, queueRuntimeToken, tokenName, } from '../core/index.js';
2
+ import { Container, Logger } from '../runtime/index.js';
3
+ export class StaticValidationError extends Error {
4
+ diagnostics;
5
+ graph;
6
+ constructor(diagnostics, graph) {
7
+ super(diagnostics
8
+ .map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`)
9
+ .join('\n'));
10
+ this.diagnostics = diagnostics;
11
+ this.graph = graph;
12
+ this.name = 'StaticValidationError';
13
+ }
14
+ }
15
+ export function compileApplication(input) {
16
+ const publicTasks = input.tasks ?? [];
17
+ const triggers = input.triggers ?? [];
18
+ const tasks = uniqueTasks([
19
+ ...publicTasks,
20
+ ...triggers.map((trigger) => trigger.task),
21
+ ]);
22
+ const argumentModule = input.arguments
23
+ ? defineModule(() => ({
24
+ name: '@loutrejs/application-arguments',
25
+ providers: [argumentsProvider(input.arguments)],
26
+ }))()
27
+ : undefined;
28
+ const modules = collectModules(argumentModule ? [...input.modules, argumentModule] : input.modules);
29
+ const visibleModules = argumentModule
30
+ ? modules.filter((module) => module !== argumentModule)
31
+ : modules;
32
+ const providers = modules.flatMap((module) => (module.definition.providers ?? []).map(normalizeProvider));
33
+ const queueTriggers = triggers.filter((trigger) => trigger.trigger === 'queue-consumer');
34
+ const queues = uniqueQueues(queueTriggers.map((trigger) => trigger.queue));
35
+ const diagnostics = [];
36
+ validateNamedDescriptors(tasks, 'LUTRE_TASK_001', diagnostics);
37
+ validateNamedDescriptors(triggers, 'LUTRE_TRIGGER_DUPLICATE', diagnostics);
38
+ validateNamedDescriptors(queues, 'LUTRE_QUEUE_DUPLICATE', diagnostics);
39
+ validateCronTriggers(triggers, diagnostics);
40
+ diagnostics.push(...validateQueueDrivers(input.modules, queueTriggers));
41
+ const contractNames = new Map();
42
+ let contractSequence = 0;
43
+ const nameContract = (contract) => {
44
+ const current = contractNames.get(contract);
45
+ if (current)
46
+ return current;
47
+ const name = contract.name ?? `Contract${++contractSequence}`;
48
+ contractNames.set(contract, name);
49
+ return name;
50
+ };
51
+ const descriptors = modules.flatMap((module) => module.definition.implementations ?? []);
52
+ const targets = [];
53
+ for (const implementation of descriptors) {
54
+ for (const procedureName of implementation.procedures) {
55
+ const procedure = implementation.contract.procedures[procedureName];
56
+ const protocol = procedure?.protocols[implementation.protocol];
57
+ if (!procedure || !protocol) {
58
+ diagnostics.push({
59
+ code: 'LUTRE_IMPL_003',
60
+ message: `${procedureName} is not declared for protocol ${implementation.protocol}`,
61
+ path: `${nameContract(implementation.contract)}.${procedureName}.${implementation.protocol}`,
62
+ });
63
+ continue;
64
+ }
65
+ targets.push({
66
+ implementation,
67
+ contractName: nameContract(implementation.contract),
68
+ procedure: procedureName,
69
+ protocol: implementation.protocol,
70
+ dispatchKey: protocol.dispatchKey,
71
+ pipeline: protocol.pipeline ?? protocol.definition?.pipeline ?? [],
72
+ interaction: protocol.interaction ?? protocol.definition?.interaction ?? 'unary',
73
+ ...(protocol.definition?.responses === undefined
74
+ ? {}
75
+ : { responses: protocol.definition.responses }),
76
+ });
77
+ }
78
+ }
79
+ validateDispatchKeys(targets, diagnostics);
80
+ validateCoverage(descriptors, contractNames, diagnostics);
81
+ validateDuplicateProviders(modules, diagnostics);
82
+ const tokensById = collectCustomTokens(providers, targets, diagnostics);
83
+ const contextKeysByName = collectContextKeys(targets, diagnostics);
84
+ const pipelines = [];
85
+ const implementations = [];
86
+ for (const target of targets) {
87
+ validatePipeline(target, diagnostics);
88
+ pipelines.push({
89
+ contract: target.contractName,
90
+ procedure: target.procedure,
91
+ protocol: target.protocol,
92
+ layers: target.pipeline.map(toLayerIR),
93
+ });
94
+ implementations.push({
95
+ contract: target.contractName,
96
+ procedure: target.procedure,
97
+ protocol: target.protocol,
98
+ implementation: target.implementation.name,
99
+ method: target.procedure,
100
+ });
101
+ }
102
+ const dependencyGraph = buildDependencyGraph(modules, descriptors, targets, tasks, diagnostics);
103
+ const probedTokenIds = dependencyGraph.nodes
104
+ .filter((node) => node.kind === 'token')
105
+ .map((node) => node.label);
106
+ const publicTaskSet = new Set(publicTasks);
107
+ const graph = {
108
+ modules: visibleModules.map((module) => {
109
+ const index = modules.indexOf(module);
110
+ return {
111
+ id: `module:${index + 1}`,
112
+ ...(module.definition.name === undefined
113
+ ? {}
114
+ : { name: module.definition.name }),
115
+ ...(module.definition.description === undefined
116
+ ? {}
117
+ : { description: module.definition.description }),
118
+ imports: (module.definition.imports ?? []).map((imported) => {
119
+ const importedIndex = modules.indexOf(imported);
120
+ return `module:${importedIndex + 1}`;
121
+ }),
122
+ environment: (module.definition.environment ?? []).map((environment) => environment.name),
123
+ providers: (module.definition.providers ?? [])
124
+ .map(normalizeProvider)
125
+ .filter((provider) => provider.kind !== 'environment')
126
+ .map((provider) => tokenName(provider.provide)),
127
+ exports: (module.definition.exports ?? []).map((value) => typeof value === 'function'
128
+ ? value.name
129
+ : typeof value === 'object' &&
130
+ value !== null &&
131
+ 'kind' in value &&
132
+ value.kind === 'token' &&
133
+ 'id' in value
134
+ ? String(value.id)
135
+ : String(value)),
136
+ lifecycle: Object.keys(module.definition.lifecycle ?? {}),
137
+ requires: module.definition.requires ?? [],
138
+ };
139
+ }),
140
+ ...(input.arguments ? { arguments: { name: input.arguments.name } } : {}),
141
+ providers: dedupeProviders(providers).map((provider) => ({
142
+ token: tokenName(provider.provide),
143
+ kind: provider.kind,
144
+ scope: provider.scope,
145
+ dependencies: provider.kind === 'factory'
146
+ ? provider.inject.map(tokenName)
147
+ : provider.kind === 'conditional'
148
+ ? [tokenName(provider.select.contract)]
149
+ : [],
150
+ })),
151
+ tokens: [...new Set([...tokensById.keys(), ...probedTokenIds])].map((id) => ({ id })),
152
+ contextKeys: [...contextKeysByName.keys()].map((name) => ({ name })),
153
+ contracts: [...contractNames.values()],
154
+ pipelines,
155
+ implementations,
156
+ tasks: tasks.map((task) => ({
157
+ id: `task:${task.name}`,
158
+ name: task.name,
159
+ public: publicTaskSet.has(task),
160
+ })),
161
+ queues: queues.map(toQueueIR),
162
+ executions: [
163
+ ...targets.map((target) => ({
164
+ id: `protocol:${target.protocol}:${target.contractName}.${target.procedure}`,
165
+ kind: 'protocol',
166
+ protocol: target.protocol,
167
+ contract: target.contractName,
168
+ procedure: target.procedure,
169
+ implementation: target.implementation.name,
170
+ })),
171
+ ...publicTasks.map((task) => ({
172
+ id: `task:${task.name}`,
173
+ kind: 'task',
174
+ name: task.name,
175
+ })),
176
+ ...triggers.map(toTriggerExecution),
177
+ ],
178
+ capabilities: [
179
+ ...targets.flatMap((target) => {
180
+ const requiredBy = `${target.contractName}.${target.procedure}`;
181
+ return [
182
+ { name: 'crypto.random', scope: 'execution', requiredBy },
183
+ ...(target.protocol === 'http'
184
+ ? [{ name: 'http.server', scope: 'execution', requiredBy }]
185
+ : []),
186
+ ...(target.protocol === 'messagePort'
187
+ ? [
188
+ {
189
+ name: 'messagePort.send',
190
+ scope: 'execution',
191
+ requiredBy,
192
+ },
193
+ {
194
+ name: 'messagePort.receive',
195
+ scope: 'execution',
196
+ requiredBy,
197
+ },
198
+ ]
199
+ : []),
200
+ ...(target.interaction === 'server-stream'
201
+ ? [
202
+ {
203
+ name: 'stream.readable',
204
+ scope: 'execution',
205
+ requiredBy,
206
+ },
207
+ ...(target.protocol === 'http'
208
+ ? [
209
+ {
210
+ name: 'http.response.streaming',
211
+ scope: 'execution',
212
+ requiredBy,
213
+ },
214
+ ]
215
+ : []),
216
+ ]
217
+ : []),
218
+ ];
219
+ }),
220
+ ...visibleModules.flatMap((module) => {
221
+ const index = modules.indexOf(module);
222
+ return [
223
+ ...(module.definition.environment?.length
224
+ ? [
225
+ {
226
+ name: 'env.runtime',
227
+ scope: 'application',
228
+ requiredBy: `module:${index + 1}`,
229
+ },
230
+ ]
231
+ : []),
232
+ ...(module.definition.requires ?? []).map((name) => ({
233
+ name,
234
+ scope: 'application',
235
+ requiredBy: `module:${index + 1}`,
236
+ })),
237
+ ];
238
+ }),
239
+ ...(input.arguments
240
+ ? [
241
+ {
242
+ name: 'args.runtime',
243
+ scope: 'application',
244
+ requiredBy: 'application',
245
+ },
246
+ ]
247
+ : []),
248
+ ],
249
+ hostCapabilities: collectHostCapabilities(input.modules),
250
+ ...dependencyGraph,
251
+ diagnostics,
252
+ };
253
+ return { graph, diagnostics };
254
+ }
255
+ function validateDispatchKeys(targets, diagnostics) {
256
+ const targetsByKey = new Map();
257
+ for (const target of targets) {
258
+ if (target.dispatchKey === null)
259
+ continue;
260
+ const path = `${target.contractName}.${target.procedure}.${target.protocol}`;
261
+ const existing = targetsByKey.get(target.dispatchKey);
262
+ if (existing) {
263
+ if (existing.implementation.contract === target.implementation.contract &&
264
+ existing.procedure === target.procedure &&
265
+ existing.protocol === target.protocol) {
266
+ continue;
267
+ }
268
+ const existingPath = `${existing.contractName}.${existing.procedure}.${existing.protocol}`;
269
+ diagnostics.push({
270
+ code: 'LUTRE_PROTOCOL_001',
271
+ message: `Duplicate protocol dispatch key "${target.dispatchKey}": ${existingPath}, ${path}`,
272
+ path,
273
+ });
274
+ continue;
275
+ }
276
+ targetsByKey.set(target.dispatchKey, target);
277
+ }
278
+ }
279
+ export const buildApplicationGraph = compileApplication;
280
+ function validateNamedDescriptors(descriptors, code, diagnostics) {
281
+ const names = new Map();
282
+ for (const descriptor of descriptors) {
283
+ const existing = names.get(descriptor.name);
284
+ if (existing && existing !== descriptor) {
285
+ diagnostics.push({
286
+ code,
287
+ message: `Name ${descriptor.name} is declared by multiple descriptors.`,
288
+ path: descriptor.name,
289
+ });
290
+ continue;
291
+ }
292
+ names.set(descriptor.name, descriptor);
293
+ }
294
+ }
295
+ function validateCronTriggers(triggers, diagnostics) {
296
+ for (const trigger of triggers) {
297
+ if (trigger.trigger !== 'cron')
298
+ continue;
299
+ if (!isValidCronExpression(trigger.expression)) {
300
+ diagnostics.push({
301
+ code: 'LUTRE_TRIGGER_INVALID_CRON',
302
+ message: `Trigger ${trigger.name} must use a portable 5-field cron expression.`,
303
+ path: `trigger:${trigger.name}`,
304
+ });
305
+ }
306
+ try {
307
+ new Intl.DateTimeFormat('en-US', {
308
+ timeZone: trigger.timezone,
309
+ }).format();
310
+ }
311
+ catch {
312
+ diagnostics.push({
313
+ code: 'LUTRE_TRIGGER_INVALID_TIMEZONE',
314
+ message: `Trigger ${trigger.name} must use a valid IANA timezone.`,
315
+ path: `trigger:${trigger.name}`,
316
+ });
317
+ }
318
+ }
319
+ }
320
+ function isValidCronExpression(expression) {
321
+ const fields = expression.trim().split(/\s+/);
322
+ if (fields.length !== 5)
323
+ return false;
324
+ const ranges = [
325
+ [0, 59],
326
+ [0, 23],
327
+ [1, 31],
328
+ [1, 12],
329
+ [0, 7],
330
+ ];
331
+ return fields.every((field, index) => {
332
+ const range = ranges[index];
333
+ return range !== undefined && isValidCronField(field, range[0], range[1]);
334
+ });
335
+ }
336
+ function isValidCronField(field, minimum, maximum) {
337
+ return field.split(',').every((segment) => {
338
+ const parts = segment.split('/');
339
+ if (parts.length > 2)
340
+ return false;
341
+ const rangeExpression = parts[0];
342
+ const stepExpression = parts[1];
343
+ if (!rangeExpression)
344
+ return false;
345
+ if (stepExpression !== undefined &&
346
+ (!/^\d+$/.test(stepExpression) || Number(stepExpression) <= 0)) {
347
+ return false;
348
+ }
349
+ if (rangeExpression === '*')
350
+ return true;
351
+ const bounds = rangeExpression.split('-');
352
+ if (bounds.length > 2 || bounds.some((bound) => !/^\d+$/.test(bound))) {
353
+ return false;
354
+ }
355
+ const start = Number(bounds[0]);
356
+ const end = bounds[1] === undefined ? start : Number(bounds[1]);
357
+ return start >= minimum && end <= maximum && start <= end;
358
+ });
359
+ }
360
+ function uniqueTasks(tasks) {
361
+ return [...new Set(tasks)];
362
+ }
363
+ function uniqueQueues(queues) {
364
+ return [...new Set(queues)];
365
+ }
366
+ function toTriggerExecution(trigger) {
367
+ switch (trigger.trigger) {
368
+ case 'cron':
369
+ return {
370
+ id: `trigger:${trigger.name}`,
371
+ kind: 'trigger',
372
+ trigger: 'cron',
373
+ name: trigger.name,
374
+ expression: trigger.expression,
375
+ timezone: trigger.timezone,
376
+ overlap: trigger.overlap,
377
+ task: trigger.task.name,
378
+ };
379
+ case 'fixed-delay':
380
+ return {
381
+ id: `trigger:${trigger.name}`,
382
+ kind: 'trigger',
383
+ trigger: 'fixed-delay',
384
+ name: trigger.name,
385
+ delay: trigger.delay,
386
+ immediate: trigger.immediate,
387
+ task: trigger.task.name,
388
+ };
389
+ case 'queue-consumer':
390
+ return {
391
+ id: `trigger:${trigger.name}`,
392
+ kind: 'trigger',
393
+ trigger: 'queue-consumer',
394
+ name: trigger.name,
395
+ queue: trigger.queue.name,
396
+ task: trigger.task.name,
397
+ };
398
+ }
399
+ }
400
+ function toQueueIR(queue) {
401
+ return {
402
+ id: `queue:${queue.name}`,
403
+ name: queue.name,
404
+ payloadSchema: schemaIdentity(queue.payload),
405
+ };
406
+ }
407
+ function schemaIdentity(schema) {
408
+ const standard = schema['~standard'];
409
+ const vendor = typeof standard.vendor === 'string' ? standard.vendor : 'standard-schema';
410
+ const version = typeof standard.version === 'number' || typeof standard.version === 'string'
411
+ ? String(standard.version)
412
+ : '1';
413
+ return `${vendor}@${version}`;
414
+ }
415
+ function validateQueueDrivers(roots, triggers) {
416
+ const providers = collectModules(roots).flatMap((module) => (module.definition.providers ?? []).map(normalizeProvider));
417
+ return triggers.flatMap((trigger) => {
418
+ const runtimeToken = queueRuntimeToken(trigger.queue);
419
+ return providers.some((provider) => provider.provide === runtimeToken)
420
+ ? []
421
+ : [
422
+ {
423
+ code: 'LUTRE_QUEUE_DRIVER_UNBOUND',
424
+ message: `Queue ${trigger.queue.name} has a consumer trigger but no runtime driver binding.`,
425
+ path: `trigger:${trigger.name}`,
426
+ },
427
+ ];
428
+ });
429
+ }
430
+ function collectHostCapabilities(roots) {
431
+ return [
432
+ ...new Set(collectModules(roots).flatMap((module) => (module.definition.implementations ?? []).flatMap((implementation) => implementation.capabilities))),
433
+ ];
434
+ }
435
+ export function validateGraph(graph) {
436
+ return graph.diagnostics;
437
+ }
438
+ function buildDependencyGraph(modules, descriptors, targets, tasks, diagnostics) {
439
+ const nodes = [];
440
+ const edges = [];
441
+ const ids = new Map();
442
+ const implementationIds = new Map();
443
+ const taskIds = new Map();
444
+ const modulesByProvider = new Map();
445
+ const providersByToken = new Map();
446
+ const customTokensById = new Map();
447
+ modules.forEach((module, index) => {
448
+ const moduleId = `module:${index + 1}`;
449
+ for (const declaration of module.definition.providers ?? []) {
450
+ const provider = normalizeProvider(declaration);
451
+ if (!modulesByProvider.has(provider.provide)) {
452
+ modulesByProvider.set(provider.provide, moduleId);
453
+ }
454
+ if (!providersByToken.has(provider.provide)) {
455
+ providersByToken.set(provider.provide, provider);
456
+ }
457
+ }
458
+ });
459
+ const ensureNode = (token, overrides = {}) => {
460
+ if (typeof token !== 'function') {
461
+ const registered = customTokensById.get(token.id);
462
+ if (registered &&
463
+ registered !== token &&
464
+ !diagnostics.some((diagnostic) => diagnostic.code === 'LUTRE_TOKEN_001' &&
465
+ diagnostic.message.includes(token.id))) {
466
+ diagnostics.push({
467
+ code: 'LUTRE_TOKEN_001',
468
+ message: `Token ID ${token.id}が異なるtoken declarationで重複しています`,
469
+ path: `dependency:${token.id}`,
470
+ });
471
+ }
472
+ else if (!registered) {
473
+ customTokensById.set(token.id, token);
474
+ }
475
+ }
476
+ const current = ids.get(token);
477
+ if (current)
478
+ return current;
479
+ const base = isArgsClass(token)
480
+ ? `arguments:${token.name}`
481
+ : typeof token === 'function'
482
+ ? `class:${token.name}`
483
+ : `token:${token.id}`;
484
+ let id = base;
485
+ let sequence = 2;
486
+ while (nodes.some((node) => node.id === id))
487
+ id = `${base}:${sequence++}`;
488
+ ids.set(token, id);
489
+ const provider = providersByToken.get(token);
490
+ const scope = overrides.scope ?? provider?.scope;
491
+ const module = provider?.kind === 'arguments'
492
+ ? undefined
493
+ : (overrides.module ?? modulesByProvider.get(token));
494
+ nodes.push({
495
+ id,
496
+ label: tokenName(token),
497
+ kind: overrides.kind ??
498
+ (provider?.kind === 'environment' || isEnvClass(token)
499
+ ? 'environment'
500
+ : provider?.kind === 'arguments' || isArgsClass(token)
501
+ ? 'arguments'
502
+ : typeof token === 'function'
503
+ ? 'class'
504
+ : 'token'),
505
+ ...(scope === undefined ? {} : { scope }),
506
+ ...(module === undefined ? {} : { module }),
507
+ });
508
+ return id;
509
+ };
510
+ const ensureImplementationNode = (implementation) => {
511
+ const current = implementationIds.get(implementation);
512
+ if (current)
513
+ return current;
514
+ const id = `implementation:${implementationIds.size + 1}`;
515
+ implementationIds.set(implementation, id);
516
+ nodes.push({
517
+ id,
518
+ label: implementation.name,
519
+ kind: 'implementation',
520
+ scope: 'application',
521
+ });
522
+ return id;
523
+ };
524
+ const ensureTaskNode = (task) => {
525
+ const current = taskIds.get(task);
526
+ if (current)
527
+ return current;
528
+ const id = `task:${task.name}`;
529
+ taskIds.set(task, id);
530
+ nodes.push({
531
+ id,
532
+ label: task.name,
533
+ kind: 'task',
534
+ scope: 'application',
535
+ });
536
+ return id;
537
+ };
538
+ const addEdge = (edge) => {
539
+ if (!edges.some((candidate) => candidate.from === edge.from &&
540
+ candidate.to === edge.to &&
541
+ candidate.kind === edge.kind &&
542
+ candidate.source === edge.source &&
543
+ candidate.condition?.key === edge.condition?.key &&
544
+ candidate.condition?.equals === edge.condition?.equals)) {
545
+ edges.push(edge);
546
+ }
547
+ };
548
+ const validateDeclaredDependency = (dependency, path) => {
549
+ if (providersByToken.has(dependency) ||
550
+ dependency === Logger)
551
+ return;
552
+ if (isEnvClass(dependency)) {
553
+ diagnostics.push({
554
+ code: 'LUTRE_ENV_002',
555
+ message: `${dependency.name} is injected but is not declared by any Module.environment.`,
556
+ path,
557
+ });
558
+ return;
559
+ }
560
+ if (isArgsClass(dependency)) {
561
+ diagnostics.push({
562
+ code: 'LUTRE_ARGS_002',
563
+ message: `${dependency.name} is injected but is not declared by Application.arguments.`,
564
+ path,
565
+ });
566
+ return;
567
+ }
568
+ diagnostics.push({
569
+ code: 'LUTRE_DI_UNRESOLVED',
570
+ message: `${path} requires ${tokenName(dependency)}, but no provider is declared for ${tokenName(dependency)}.`,
571
+ path,
572
+ });
573
+ };
574
+ for (const provider of providersByToken.values()) {
575
+ const providerId = ensureNode(provider.provide);
576
+ if (provider.kind === 'class' && provider.provide !== provider.useClass) {
577
+ addEdge({
578
+ from: providerId,
579
+ to: ensureNode(provider.useClass, { scope: provider.scope }),
580
+ kind: 'framework',
581
+ source: 'declared',
582
+ });
583
+ }
584
+ if (provider.kind === 'factory') {
585
+ for (const dependency of provider.inject) {
586
+ validateDeclaredDependency(dependency, tokenName(provider.provide));
587
+ addEdge({
588
+ from: providerId,
589
+ to: ensureNode(dependency),
590
+ kind: 'factory',
591
+ source: 'declared',
592
+ });
593
+ }
594
+ if (provider.useFactory.constructor.name === 'AsyncFunction') {
595
+ diagnostics.push({
596
+ code: 'LUTRE_DI_ASYNC_FACTORY',
597
+ message: 'Async factory providers are not supported. Move asynchronous resource initialization to application lifecycle.',
598
+ path: tokenName(provider.provide),
599
+ });
600
+ }
601
+ }
602
+ if (provider.kind === 'conditional') {
603
+ validateDeclaredDependency(provider.select.contract, tokenName(provider.provide));
604
+ addEdge({
605
+ from: providerId,
606
+ to: ensureNode(provider.select.contract),
607
+ kind: 'conditional',
608
+ source: 'declared',
609
+ });
610
+ for (const equals of Reflect.ownKeys(provider.mapping)) {
611
+ const candidate = provider.mapping[equals];
612
+ if (!candidate)
613
+ continue;
614
+ addEdge({
615
+ from: providerId,
616
+ to: ensureNode(candidate, { scope: provider.scope }),
617
+ kind: 'conditional',
618
+ source: 'declared',
619
+ condition: {
620
+ source: provider.select.source,
621
+ contract: tokenName(provider.select.contract),
622
+ key: provider.select.key,
623
+ equals,
624
+ },
625
+ });
626
+ }
627
+ }
628
+ }
629
+ for (const implementation of descriptors) {
630
+ ensureImplementationNode(implementation);
631
+ }
632
+ modules.forEach((module, index) => {
633
+ const lifecycle = module.definition.lifecycle;
634
+ if (!lifecycle)
635
+ return;
636
+ for (const [hookName, hook] of Object.entries(lifecycle)) {
637
+ if (!hook)
638
+ continue;
639
+ const hookId = `lifecycle:module:${index + 1}:${hookName}`;
640
+ nodes.push({
641
+ id: hookId,
642
+ label: `${hookName} (module:${index + 1})`,
643
+ kind: 'framework',
644
+ });
645
+ for (const dependency of hook.inject) {
646
+ validateDeclaredDependency(dependency, hookId);
647
+ addEdge({
648
+ from: hookId,
649
+ to: ensureNode(dependency),
650
+ kind: 'lifecycle',
651
+ source: 'declared',
652
+ });
653
+ }
654
+ }
655
+ });
656
+ const recorder = {
657
+ record(consumer, dependency) {
658
+ addEdge({
659
+ from: ensureConsumerNode(consumer, nodes, ensureNode),
660
+ to: ensureNode(dependency),
661
+ kind: 'inject',
662
+ source: 'probed',
663
+ });
664
+ },
665
+ };
666
+ const container = new Container([...providersByToken.values()], {
667
+ recorder,
668
+ probe: true,
669
+ });
670
+ const managedClasses = new Set();
671
+ for (const provider of providersByToken.values()) {
672
+ if (provider.kind === 'class')
673
+ managedClasses.add(provider.useClass);
674
+ if (provider.kind === 'conditional') {
675
+ for (const key of Reflect.ownKeys(provider.mapping)) {
676
+ const candidate = provider.mapping[key];
677
+ if (candidate)
678
+ managedClasses.add(candidate);
679
+ }
680
+ }
681
+ }
682
+ for (const target of managedClasses) {
683
+ try {
684
+ container.probeClass(target);
685
+ }
686
+ catch (error) {
687
+ const message = error instanceof Error ? error.message : String(error);
688
+ diagnostics.push({
689
+ code: diagnosticCode(message),
690
+ message,
691
+ path: target.name,
692
+ });
693
+ }
694
+ }
695
+ const uniqueImplementations = [...new Set(descriptors)];
696
+ uniqueImplementations.forEach((implementation, index) => {
697
+ const consumer = {
698
+ kind: 'implementation-consumer',
699
+ id: ensureImplementationNode(implementation),
700
+ name: implementation.name,
701
+ };
702
+ try {
703
+ container.probeImplementation(implementation, consumer);
704
+ }
705
+ catch (error) {
706
+ const message = error instanceof Error ? error.message : String(error);
707
+ diagnostics.push({
708
+ code: diagnosticCode(message),
709
+ message,
710
+ path: `implementation:${index + 1}:${implementation.name}`,
711
+ });
712
+ }
713
+ });
714
+ tasks.forEach((task) => {
715
+ const consumer = {
716
+ kind: 'task-consumer',
717
+ id: ensureTaskNode(task),
718
+ name: task.name,
719
+ };
720
+ try {
721
+ container.probeTask(task, consumer);
722
+ }
723
+ catch (error) {
724
+ const message = error instanceof Error ? error.message : String(error);
725
+ diagnostics.push({
726
+ code: diagnosticCode(message),
727
+ message,
728
+ path: consumer.id,
729
+ });
730
+ }
731
+ });
732
+ for (const target of targets) {
733
+ visitPipelineItems(target.pipeline, (item, indexPath) => {
734
+ if (item.kind !== 'layer')
735
+ return;
736
+ const definition = layerDefinitionOf(item);
737
+ const consumer = {
738
+ kind: 'layer-consumer',
739
+ id: `layer:${target.contractName}:${target.procedure}:${target.protocol}:${indexPath.join('.')}`,
740
+ name: definition.name,
741
+ };
742
+ if (!nodes.some(({ id }) => id === consumer.id)) {
743
+ nodes.push({ id: consumer.id, label: consumer.name, kind: 'layer' });
744
+ }
745
+ try {
746
+ container.probeLayer(definition, consumer);
747
+ }
748
+ catch (error) {
749
+ const message = error instanceof Error ? error.message : String(error);
750
+ diagnostics.push({
751
+ code: diagnosticCode(message),
752
+ message,
753
+ path: consumer.id,
754
+ });
755
+ }
756
+ });
757
+ }
758
+ return { nodes, edges };
759
+ }
760
+ function diagnosticCode(message) {
761
+ for (const code of [
762
+ 'LUTRE_ENV_001',
763
+ 'LUTRE_ENV_002',
764
+ 'LUTRE_ENV_004',
765
+ 'LUTRE_ARGS_001',
766
+ 'LUTRE_ARGS_002',
767
+ 'LUTRE_DI_CYCLE',
768
+ 'LUTRE_DI_ASYNC_FACTORY',
769
+ 'LUTRE_DI_CONSTRUCTOR',
770
+ 'LUTRE_IMPL_ASYNC_FACTORY',
771
+ 'LUTRE_IMPL_FACTORY_RESULT',
772
+ 'LUTRE_IMPL_004',
773
+ 'LUTRE_LAYER_ASYNC_FACTORY',
774
+ 'LUTRE_LAYER_FACTORY_RESULT',
775
+ 'LUTRE_TASK_ASYNC_FACTORY',
776
+ 'LUTRE_TASK_FACTORY_RESULT',
777
+ ]) {
778
+ if (message.includes(code))
779
+ return code;
780
+ }
781
+ return 'LUTRE_DI_UNRESOLVED';
782
+ }
783
+ function validateDuplicateProviders(modules, diagnostics) {
784
+ const declarations = new Map();
785
+ modules.forEach((module, moduleIndex) => {
786
+ const moduleName = describeModule(module, moduleIndex);
787
+ for (const declaration of module.definition.providers ?? []) {
788
+ const provider = normalizeProvider(declaration);
789
+ const existing = declarations.get(provider.provide);
790
+ if (existing) {
791
+ if (existing.provider.kind === 'environment' &&
792
+ provider.kind === 'environment') {
793
+ continue;
794
+ }
795
+ if (existing.provider.kind === 'environment' ||
796
+ provider.kind === 'environment') {
797
+ diagnostics.push({
798
+ code: 'LUTRE_ENV_001',
799
+ message: `Environment ${tokenName(provider.provide)} is runtime-managed and cannot also be declared as a normal provider.`,
800
+ path: `${moduleName}.environment.${tokenName(provider.provide)}`,
801
+ });
802
+ continue;
803
+ }
804
+ diagnostics.push({
805
+ code: 'LUTRE_DI_003',
806
+ message: `Provider ${tokenName(provider.provide)}が${existing.module}と${moduleName}で重複しています`,
807
+ path: `${moduleName}.providers.${tokenName(provider.provide)}`,
808
+ });
809
+ continue;
810
+ }
811
+ declarations.set(provider.provide, { module: moduleName, provider });
812
+ }
813
+ });
814
+ }
815
+ function describeModule(module, index) {
816
+ const id = `module:${index + 1}`;
817
+ return module.definition.description === undefined
818
+ ? id
819
+ : `${id} (${module.definition.description})`;
820
+ }
821
+ function collectCustomTokens(providers, targets, diagnostics) {
822
+ const tokens = new Map();
823
+ const register = (candidate, path) => {
824
+ if (typeof candidate === 'function')
825
+ return;
826
+ const existing = tokens.get(candidate.id);
827
+ if (existing && existing !== candidate) {
828
+ diagnostics.push({
829
+ code: 'LUTRE_TOKEN_001',
830
+ message: `Token ID ${candidate.id}が異なるtoken declarationで重複しています`,
831
+ path,
832
+ });
833
+ return;
834
+ }
835
+ tokens.set(candidate.id, candidate);
836
+ };
837
+ for (const provider of providers) {
838
+ register(provider.provide, `provider:${tokenName(provider.provide)}`);
839
+ if (provider.kind === 'factory') {
840
+ for (const dependency of provider.inject) {
841
+ register(dependency, `provider:${tokenName(provider.provide)}`);
842
+ }
843
+ }
844
+ }
845
+ return tokens;
846
+ }
847
+ function collectContextKeys(targets, diagnostics) {
848
+ const keys = new Map();
849
+ for (const target of targets) {
850
+ const path = `${target.contractName}.${target.procedure}.${target.protocol}`;
851
+ visitPipelineItems(target.pipeline, (item) => {
852
+ if (item.kind !== 'layer')
853
+ return;
854
+ for (const key of [...item.requires, ...item.provides]) {
855
+ const existing = keys.get(key.name);
856
+ if (existing && existing !== key) {
857
+ diagnostics.push({
858
+ code: 'LUTRE_CONTEXT_002',
859
+ message: `Context Key ${key.name}が異なる宣言で重複しています`,
860
+ path,
861
+ });
862
+ continue;
863
+ }
864
+ keys.set(key.name, key);
865
+ }
866
+ });
867
+ }
868
+ return keys;
869
+ }
870
+ export function assertValidCompilation(result) {
871
+ if (result.diagnostics.length > 0) {
872
+ throw new StaticValidationError(result.diagnostics, result.graph);
873
+ }
874
+ return result.graph;
875
+ }
876
+ function collectModules(roots) {
877
+ const modules = [];
878
+ const seen = new Set();
879
+ const visit = (candidate) => {
880
+ const module = asModuleInstance(candidate);
881
+ if (seen.has(module))
882
+ return;
883
+ seen.add(module);
884
+ for (const imported of module.definition.imports ?? [])
885
+ visit(imported);
886
+ modules.push(module);
887
+ };
888
+ for (const root of roots)
889
+ visit(root);
890
+ return modules;
891
+ }
892
+ function validateCoverage(implementations, contractNames, diagnostics) {
893
+ const contracts = new Set(implementations.map((implementation) => implementation.contract));
894
+ for (const contract of contracts) {
895
+ const protocols = new Set(Object.values(contract.procedures).flatMap((procedure) => Object.keys(procedure.protocols)));
896
+ for (const protocol of protocols) {
897
+ for (const [procedureName, procedure] of Object.entries(contract.procedures)) {
898
+ if (!(protocol in procedure.protocols))
899
+ continue;
900
+ const covering = implementations.filter((implementation) => implementation.contract === contract &&
901
+ implementation.protocol === protocol &&
902
+ implementation.procedures.includes(procedureName));
903
+ const path = `${contractNames.get(contract) ?? 'Contract'}.${procedureName}.${protocol}`;
904
+ if (covering.length === 0) {
905
+ diagnostics.push({
906
+ code: 'LUTRE_IMPL_001',
907
+ message: `Missing implementation for ${path}`,
908
+ path,
909
+ });
910
+ }
911
+ else if (covering.length > 1) {
912
+ diagnostics.push({
913
+ code: 'LUTRE_IMPL_002',
914
+ message: `Duplicate implementation for ${path}`,
915
+ path,
916
+ });
917
+ }
918
+ }
919
+ }
920
+ }
921
+ }
922
+ function validatePipeline(target, diagnostics) {
923
+ const path = `${target.contractName}.${target.procedure}.${target.protocol}`;
924
+ const flattened = [];
925
+ visitPipelineItems(target.pipeline, (item, indexPath) => {
926
+ flattened.push({ item, indexPath });
927
+ });
928
+ const terminals = flattened.filter(({ item }) => item.kind === 'terminal');
929
+ if (terminals.length !== 1) {
930
+ diagnostics.push({
931
+ code: 'LUTRE_PIPELINE_001',
932
+ message: `Pipeline must contain exactly one ${target.protocol} terminal`,
933
+ path,
934
+ });
935
+ }
936
+ else {
937
+ const terminal = terminals[0];
938
+ if (terminal !== flattened.at(-1)) {
939
+ diagnostics.push({
940
+ code: 'LUTRE_PIPELINE_002',
941
+ message: `${terminal.item.name} must be the final Pipeline item`,
942
+ path,
943
+ });
944
+ }
945
+ if (terminal.item.kind === 'terminal' &&
946
+ terminal.item.protocol !== target.protocol) {
947
+ diagnostics.push({
948
+ code: 'LUTRE_PIPELINE_003',
949
+ message: `${terminal.item.name} does not match protocol ${target.protocol}`,
950
+ path,
951
+ });
952
+ }
953
+ }
954
+ const available = new Set();
955
+ const validated = new Set();
956
+ for (const { item } of flattened) {
957
+ if (item.kind === 'validation') {
958
+ validated.add(item.part);
959
+ continue;
960
+ }
961
+ if (item.kind !== 'layer')
962
+ continue;
963
+ for (const required of item.requiresValidated) {
964
+ if (!validated.has(required)) {
965
+ diagnostics.push({
966
+ code: 'LUTRE_VALIDATION_001',
967
+ message: `${item.name}にはvalidation済みの${required}が必要ですが、validate.${required}より前に配置されています`,
968
+ path,
969
+ });
970
+ }
971
+ }
972
+ for (const required of item.requires) {
973
+ if (!available.has(required)) {
974
+ diagnostics.push({
975
+ code: 'LUTRE_PIPELINE_004',
976
+ message: `${item.name}が必要とするContext Key ${contextKeyName(required)}は利用できません`,
977
+ path,
978
+ });
979
+ }
980
+ }
981
+ for (const provided of item.provides) {
982
+ if (available.has(provided)) {
983
+ diagnostics.push({
984
+ code: 'LUTRE_CONTEXT_003',
985
+ message: `${item.name}は既存のContext Key ${contextKeyName(provided)}を暗黙に上書きできません`,
986
+ path,
987
+ });
988
+ }
989
+ available.add(provided);
990
+ }
991
+ for (const shortCircuit of item.shortCircuits) {
992
+ if (shortCircuit.protocol !== target.protocol)
993
+ continue;
994
+ const response = target.responses?.[shortCircuit.variant];
995
+ if (!response) {
996
+ diagnostics.push({
997
+ code: 'LUTRE_SHORT_CIRCUIT_001',
998
+ message: `${item.name}のshort circuit variant ${shortCircuit.variant}がresponseに宣言されていません`,
999
+ path,
1000
+ });
1001
+ continue;
1002
+ }
1003
+ const expectedStatus = shortCircuit.response?.status;
1004
+ if (typeof expectedStatus === 'number' &&
1005
+ response.status !== expectedStatus) {
1006
+ diagnostics.push({
1007
+ code: 'LUTRE_SHORT_CIRCUIT_002',
1008
+ message: `${item.name}のshort circuit variant ${shortCircuit.variant}はHTTP ${expectedStatus}である必要があります`,
1009
+ path,
1010
+ });
1011
+ }
1012
+ }
1013
+ }
1014
+ }
1015
+ function toLayerIR(item, index) {
1016
+ const child = item.kind === 'layer' ? childPipelineOf(item) : undefined;
1017
+ return {
1018
+ index,
1019
+ name: item.name,
1020
+ role: item.role,
1021
+ requires: item.kind === 'layer' ? item.requires.map(contextKeyName) : [],
1022
+ provides: item.kind === 'layer' ? item.provides.map(contextKeyName) : [],
1023
+ requiresValidated: item.kind === 'layer' ? item.requiresValidated : [],
1024
+ ...(child === undefined ? {} : { pipeline: child.map(toLayerIR) }),
1025
+ ...(item.kind !== 'layer' || item.shortCircuits.length === 0
1026
+ ? {}
1027
+ : {
1028
+ shortCircuits: item.shortCircuits.map((shortCircuit) => ({
1029
+ protocol: shortCircuit.protocol,
1030
+ variant: shortCircuit.variant,
1031
+ ...(shortCircuit.response === undefined
1032
+ ? {}
1033
+ : { response: shortCircuit.response }),
1034
+ })),
1035
+ }),
1036
+ };
1037
+ }
1038
+ function visitPipelineItems(pipeline, visit, parentPath = []) {
1039
+ pipeline.forEach((item, index) => {
1040
+ const indexPath = [...parentPath, index];
1041
+ visit(item, indexPath);
1042
+ if (item.kind === 'layer') {
1043
+ const child = childPipelineOf(item);
1044
+ if (child)
1045
+ visitPipelineItems(child, visit, indexPath);
1046
+ }
1047
+ });
1048
+ }
1049
+ function ensureConsumerNode(consumer, nodes, ensureTokenNode) {
1050
+ if (typeof consumer === 'function' || consumer.kind === 'token') {
1051
+ return ensureTokenNode(consumer);
1052
+ }
1053
+ if (!nodes.some(({ id }) => id === consumer.id)) {
1054
+ nodes.push({
1055
+ id: consumer.id,
1056
+ label: consumer.name,
1057
+ kind: consumer.kind === 'implementation-consumer'
1058
+ ? 'implementation'
1059
+ : consumer.kind === 'task-consumer'
1060
+ ? 'task'
1061
+ : 'layer',
1062
+ });
1063
+ }
1064
+ return consumer.id;
1065
+ }
1066
+ function dedupeProviders(providers) {
1067
+ const result = [];
1068
+ const seenEnvironment = new Set();
1069
+ for (const provider of providers) {
1070
+ if (provider.kind === 'environment') {
1071
+ if (seenEnvironment.has(provider.provide))
1072
+ continue;
1073
+ seenEnvironment.add(provider.provide);
1074
+ }
1075
+ result.push(provider);
1076
+ }
1077
+ return result;
1078
+ }