@loutrejs/loutre 0.1.0 → 0.3.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 (43) hide show
  1. package/README.md +69 -0
  2. package/dist/adapters/aws-lambda.js +2 -0
  3. package/dist/adapters/bun.d.ts +3 -1
  4. package/dist/adapters/bun.js +90 -33
  5. package/dist/adapters/cloudflare-workers.js +2 -0
  6. package/dist/adapters/deno.d.ts +3 -1
  7. package/dist/adapters/deno.js +99 -33
  8. package/dist/adapters/electron.js +2 -0
  9. package/dist/application/openapi.d.ts +6 -0
  10. package/dist/application/openapi.js +14 -19
  11. package/dist/core/contract.d.ts +47 -7
  12. package/dist/core/contract.js +59 -10
  13. package/dist/generated/version.d.ts +1 -0
  14. package/dist/generated/version.js +2 -0
  15. package/dist/graph/compiler.d.ts +4 -0
  16. package/dist/graph/compiler.js +228 -0
  17. package/dist/graph/graph.js +65 -40
  18. package/dist/graph/index.d.ts +2 -1
  19. package/dist/graph/index.js +2 -1
  20. package/dist/graph/ir.d.ts +24 -8
  21. package/dist/http/application.js +5 -5
  22. package/dist/http/basic-auth.js +1 -1
  23. package/dist/http/client.d.ts +72 -0
  24. package/dist/http/client.js +258 -0
  25. package/dist/http/cors.js +4 -4
  26. package/dist/http/definitions.d.ts +13 -5
  27. package/dist/http/definitions.js +6 -2
  28. package/dist/http/index.d.ts +1 -0
  29. package/dist/http/index.js +1 -0
  30. package/dist/message-port/index.d.ts +12 -4
  31. package/dist/message-port/index.js +13 -6
  32. package/dist/presentation.d.ts +33 -0
  33. package/dist/presentation.js +233 -0
  34. package/dist/runtime/capability.js +1 -1
  35. package/dist/runtime/di.js +2 -2
  36. package/dist/runtime/engine.d.ts +3 -0
  37. package/dist/runtime/engine.js +38 -0
  38. package/dist/runtime/index.d.ts +2 -0
  39. package/dist/runtime/index.js +2 -0
  40. package/dist/runtime/pipeline.js +13 -13
  41. package/dist/runtime/server-url.d.ts +1 -0
  42. package/dist/runtime/server-url.js +5 -0
  43. package/package.json +5 -1
@@ -1,15 +1,64 @@
1
- export function procedure(definition) {
2
- return { kind: 'procedure', protocols: definition.protocols };
1
+ export function protocolGroup(protocol, procedures) {
2
+ return Object.freeze({
3
+ kind: 'protocol-group',
4
+ protocol,
5
+ procedures: Object.freeze({ ...procedures }),
6
+ });
3
7
  }
4
- export function contract(procedures, options = {}) {
5
- assertUniqueDispatchKeys(procedures, options.name);
6
- return {
8
+ function defineContract(groups) {
9
+ if (groups.length === 0) {
10
+ throw new Error('contract([]) requires at least one protocol group');
11
+ }
12
+ const procedures = {};
13
+ for (const group of groups) {
14
+ for (const [procedureName, descriptor] of Object.entries(group.procedures)) {
15
+ const current = procedures[procedureName];
16
+ if (Object.hasOwn(current?.protocols ?? {}, group.protocol)) {
17
+ throw new Error(`Duplicate contract procedure protocol: ${procedureName}.${group.protocol}`);
18
+ }
19
+ procedures[procedureName] = {
20
+ kind: 'procedure',
21
+ protocols: {
22
+ ...current?.protocols,
23
+ [group.protocol]: descriptor,
24
+ },
25
+ };
26
+ }
27
+ }
28
+ assertUniqueDispatchKeys(procedures);
29
+ return Object.freeze({
30
+ kind: 'contract',
31
+ procedures: Object.freeze(procedures),
32
+ });
33
+ }
34
+ function mergeContracts(contracts) {
35
+ if (contracts.length === 0) {
36
+ throw new Error('contract.merge([]) requires at least one Contract');
37
+ }
38
+ const procedures = {};
39
+ for (const source of contracts) {
40
+ for (const [procedureName, procedureDefinition] of Object.entries(source.procedures)) {
41
+ const current = procedures[procedureName];
42
+ const protocols = { ...current?.protocols };
43
+ for (const [protocolName, descriptor] of Object.entries(procedureDefinition.protocols)) {
44
+ if (Object.hasOwn(protocols, protocolName)) {
45
+ throw new Error(`Duplicate contract procedure protocol: ${procedureName}.${protocolName}`);
46
+ }
47
+ protocols[protocolName] = descriptor;
48
+ }
49
+ procedures[procedureName] = { kind: 'procedure', protocols };
50
+ }
51
+ }
52
+ assertUniqueDispatchKeys(procedures);
53
+ return Object.freeze({
7
54
  kind: 'contract',
8
- ...(options.name === undefined ? {} : { name: options.name }),
9
- procedures,
10
- };
55
+ procedures: Object.freeze(procedures),
56
+ });
11
57
  }
12
- function assertUniqueDispatchKeys(procedures, contractName) {
58
+ export const contract = Object.assign(defineContract, {
59
+ merge: mergeContracts,
60
+ });
61
+ function assertUniqueDispatchKeys(procedures) {
13
62
  const paths = new Map();
14
63
  for (const [procedureName, procedureDefinition] of Object.entries(procedures)) {
15
64
  for (const [protocolName, protocol] of Object.entries(procedureDefinition.protocols)) {
@@ -19,7 +68,7 @@ function assertUniqueDispatchKeys(procedures, contractName) {
19
68
  if (typeof dispatchKey !== 'string') {
20
69
  throw new Error(`Protocol dispatchKey must be a string or null: ${procedureName}.${protocolName}`);
21
70
  }
22
- const path = `${contractName ?? 'Contract'}.${procedureName}.${protocolName}`;
71
+ const path = `${procedureName}.${protocolName}`;
23
72
  const existing = paths.get(dispatchKey);
24
73
  if (existing) {
25
74
  throw new Error(`Duplicate protocol dispatch key "${dispatchKey}" between ${existing} and ${path}`);
@@ -0,0 +1 @@
1
+ export declare const LOUTRE_VERSION = "0.3.0";
@@ -0,0 +1,2 @@
1
+ // package manifestとのずれを避けるため、scripts/generate-version.mjsがbuild前に更新する。
2
+ export const LOUTRE_VERSION = '0.3.0';
@@ -0,0 +1,4 @@
1
+ import { type ApplicationCompilationInput } from './graph.js';
2
+ import type { CompilationResult } from './ir.js';
3
+ export declare function compileApplication(input: ApplicationCompilationInput): CompilationResult;
4
+ export declare const buildApplicationGraph: typeof compileApplication;
@@ -0,0 +1,228 @@
1
+ import { asModuleInstance, normalizeProvider, tokenName, } from '../core/index.js';
2
+ import { compileApplication as compileBaseApplication, } from './graph.js';
3
+ export function compileApplication(input) {
4
+ const base = compileBaseApplication(input);
5
+ const modules = collectModules(input.modules);
6
+ const moduleIds = new Map(modules.map((module, index) => [module, `module:${index + 1}`]));
7
+ const implementationModules = collectImplementationModules(modules, moduleIds);
8
+ const implementationNames = new Map(base.graph.implementations.map((implementation) => [
9
+ implementation.id,
10
+ implementation.name,
11
+ ]));
12
+ const graphModules = new Map(base.graph.modules.map((module) => [module.id, module]));
13
+ const executionModules = new Map();
14
+ for (const execution of base.graph.executions) {
15
+ if (execution.kind !== 'protocol')
16
+ continue;
17
+ const implementationName = implementationNames.get(execution.implementation);
18
+ const module = implementationName
19
+ ? implementationModules.get(implementationName)
20
+ : undefined;
21
+ if (!module)
22
+ continue;
23
+ executionModules.set(`${execution.contract}:${execution.procedure}:${execution.protocol}`, module);
24
+ }
25
+ const providerModules = collectProviderModules(modules, moduleIds);
26
+ const nodes = base.graph.nodes.map((node) => {
27
+ const module = node.module ??
28
+ providerModules.get(node.label) ??
29
+ moduleForImplementation(node, implementationModules) ??
30
+ moduleForLifecycle(node) ??
31
+ moduleForLayer(node, executionModules);
32
+ if (!module)
33
+ return node;
34
+ const owner = graphModules.get(module);
35
+ if (!owner)
36
+ return node;
37
+ return {
38
+ ...node,
39
+ module,
40
+ visibility: owner.exports.includes(node.label) ? 'exported' : 'private',
41
+ };
42
+ });
43
+ const diagnostics = [
44
+ ...base.diagnostics,
45
+ ...validateModuleExports(base.graph.modules),
46
+ ...validateModuleVisibility(base.graph, nodes),
47
+ ];
48
+ const graph = {
49
+ ...base.graph,
50
+ nodes,
51
+ diagnostics,
52
+ };
53
+ return { graph, diagnostics };
54
+ }
55
+ export const buildApplicationGraph = compileApplication;
56
+ function collectModules(roots) {
57
+ const modules = [];
58
+ const seen = new Set();
59
+ const visit = (candidate) => {
60
+ const module = asModuleInstance(candidate);
61
+ if (seen.has(module))
62
+ return;
63
+ seen.add(module);
64
+ for (const imported of module.definition.imports ?? [])
65
+ visit(imported);
66
+ modules.push(module);
67
+ };
68
+ for (const root of roots)
69
+ visit(root);
70
+ return modules;
71
+ }
72
+ function collectProviderModules(modules, moduleIds) {
73
+ const result = new Map();
74
+ const ambiguous = new Set();
75
+ for (const module of modules) {
76
+ const moduleId = moduleIds.get(module);
77
+ if (!moduleId)
78
+ continue;
79
+ for (const declaration of module.definition.providers ?? []) {
80
+ const provider = normalizeProvider(declaration);
81
+ const labels = [
82
+ tokenName(provider.provide),
83
+ ...(provider.kind === 'class' ? [provider.useClass.name] : []),
84
+ ...(provider.kind === 'conditional'
85
+ ? Reflect.ownKeys(provider.mapping).flatMap((key) => {
86
+ const candidate = provider.mapping[key];
87
+ return candidate ? [candidate.name] : [];
88
+ })
89
+ : []),
90
+ ];
91
+ for (const label of labels) {
92
+ const current = result.get(label);
93
+ if (current && current !== moduleId)
94
+ ambiguous.add(label);
95
+ else
96
+ result.set(label, moduleId);
97
+ }
98
+ }
99
+ }
100
+ for (const label of ambiguous)
101
+ result.delete(label);
102
+ return result;
103
+ }
104
+ function collectImplementationModules(modules, moduleIds) {
105
+ const result = new Map();
106
+ const ambiguous = new Set();
107
+ for (const module of modules) {
108
+ const moduleId = moduleIds.get(module);
109
+ if (!moduleId)
110
+ continue;
111
+ for (const implementation of module.definition.implementations ?? []) {
112
+ const current = result.get(implementation.name);
113
+ if (current && current !== moduleId)
114
+ ambiguous.add(implementation.name);
115
+ else
116
+ result.set(implementation.name, moduleId);
117
+ }
118
+ }
119
+ for (const name of ambiguous)
120
+ result.delete(name);
121
+ return result;
122
+ }
123
+ function moduleForImplementation(node, modules) {
124
+ return node.kind === 'implementation' ? modules.get(node.label) : undefined;
125
+ }
126
+ function moduleForLifecycle(node) {
127
+ if (node.kind !== 'framework' || !node.id.startsWith('lifecycle:module:')) {
128
+ return undefined;
129
+ }
130
+ const match = /^lifecycle:(module:\d+):/.exec(node.id);
131
+ return match?.[1];
132
+ }
133
+ function moduleForLayer(node, executionModules) {
134
+ if (node.kind !== 'layer')
135
+ return undefined;
136
+ const match = /^layer:(contract:\d+)\/([^/]+)\/([^/]+)\//.exec(node.id);
137
+ if (!match)
138
+ return undefined;
139
+ const [, contract, procedure, protocol] = match;
140
+ return executionModules.get(`${contract}:${procedure}:${protocol}`);
141
+ }
142
+ function validateModuleExports(modules) {
143
+ const byId = new Map(modules.map((module) => [module.id, module]));
144
+ return modules.flatMap((module) => module.exports.flatMap((token) => declaresToken(module, token) ||
145
+ canResolveExport(module, token, byId, new Set())
146
+ ? []
147
+ : [
148
+ {
149
+ code: 'LUTRE_MODULE_EXPORT_UNRESOLVED',
150
+ message: `${describeModule(module)} exports ${token}, but the token is neither declared nor re-exported from an imported Module.`,
151
+ path: `${module.id}.exports.${token}`,
152
+ },
153
+ ]));
154
+ }
155
+ function declaresToken(module, token) {
156
+ return module.providers.includes(token) || module.environment.includes(token);
157
+ }
158
+ function canResolveExport(source, token, modules, visited) {
159
+ if (visited.has(source.id))
160
+ return false;
161
+ visited.add(source.id);
162
+ for (const importedId of source.imports) {
163
+ const imported = modules.get(importedId);
164
+ if (!imported || !imported.exports.includes(token))
165
+ continue;
166
+ if (declaresToken(imported, token))
167
+ return true;
168
+ if (canResolveExport(imported, token, modules, new Set(visited)))
169
+ return true;
170
+ }
171
+ return false;
172
+ }
173
+ function validateModuleVisibility(graph, nodes) {
174
+ const modules = new Map(graph.modules.map((module) => [module.id, module]));
175
+ const nodesById = new Map(nodes.map((node) => [node.id, node]));
176
+ const seen = new Set();
177
+ const diagnostics = [];
178
+ for (const edge of graph.edges) {
179
+ const consumer = nodesById.get(edge.from);
180
+ const dependency = nodesById.get(edge.to);
181
+ if (!consumer?.module ||
182
+ !dependency?.module ||
183
+ consumer.module === dependency.module ||
184
+ !modules.has(consumer.module) ||
185
+ !modules.has(dependency.module)) {
186
+ continue;
187
+ }
188
+ const key = `${consumer.id}->${dependency.id}`;
189
+ if (seen.has(key))
190
+ continue;
191
+ seen.add(key);
192
+ if (isTokenVisibleFrom(consumer.module, dependency.module, dependency.label, modules)) {
193
+ continue;
194
+ }
195
+ diagnostics.push({
196
+ code: 'LUTRE_MODULE_VISIBILITY',
197
+ message: `${consumer.label} in ${describeModuleId(consumer.module, modules)} depends on private ${dependency.label} from ${describeModuleId(dependency.module, modules)}. Export the dependency and import its Module explicitly.`,
198
+ path: consumer.id,
199
+ });
200
+ }
201
+ return diagnostics;
202
+ }
203
+ function isTokenVisibleFrom(sourceId, targetId, token, modules, visited = new Set()) {
204
+ if (visited.has(sourceId))
205
+ return false;
206
+ visited.add(sourceId);
207
+ const source = modules.get(sourceId);
208
+ if (!source)
209
+ return false;
210
+ for (const importedId of source.imports) {
211
+ const imported = modules.get(importedId);
212
+ if (!imported || !imported.exports.includes(token))
213
+ continue;
214
+ if (importedId === targetId)
215
+ return true;
216
+ if (isTokenVisibleFrom(importedId, targetId, token, modules, new Set(visited))) {
217
+ return true;
218
+ }
219
+ }
220
+ return false;
221
+ }
222
+ function describeModule(module) {
223
+ return module.name ?? module.description ?? module.id;
224
+ }
225
+ function describeModuleId(id, modules) {
226
+ const module = modules.get(id);
227
+ return module ? describeModule(module) : id;
228
+ }
@@ -38,17 +38,29 @@ export function compileApplication(input) {
38
38
  validateNamedDescriptors(queues, 'LUTRE_QUEUE_DUPLICATE', diagnostics);
39
39
  validateCronTriggers(triggers, diagnostics);
40
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);
41
+ const contractIds = new Map();
42
+ const idContract = (contract) => {
43
+ const current = contractIds.get(contract);
45
44
  if (current)
46
45
  return current;
47
- const name = contract.name ?? `Contract${++contractSequence}`;
48
- contractNames.set(contract, name);
49
- return name;
46
+ const id = `contract:${contractIds.size + 1}`;
47
+ contractIds.set(contract, id);
48
+ return id;
49
+ };
50
+ const implementationIds = new Map();
51
+ const idImplementation = (implementation) => {
52
+ const current = implementationIds.get(implementation);
53
+ if (current)
54
+ return current;
55
+ const id = `implementation:${implementationIds.size + 1}`;
56
+ implementationIds.set(implementation, id);
57
+ return id;
50
58
  };
51
59
  const descriptors = modules.flatMap((module) => module.definition.implementations ?? []);
60
+ for (const implementation of new Set(descriptors)) {
61
+ idContract(implementation.contract);
62
+ idImplementation(implementation);
63
+ }
52
64
  const targets = [];
53
65
  for (const implementation of descriptors) {
54
66
  for (const procedureName of implementation.procedures) {
@@ -58,13 +70,14 @@ export function compileApplication(input) {
58
70
  diagnostics.push({
59
71
  code: 'LUTRE_IMPL_003',
60
72
  message: `${procedureName} is not declared for protocol ${implementation.protocol}`,
61
- path: `${nameContract(implementation.contract)}.${procedureName}.${implementation.protocol}`,
73
+ path: `${idContract(implementation.contract)}.${procedureName}.${implementation.protocol}`,
62
74
  });
63
75
  continue;
64
76
  }
65
77
  targets.push({
66
78
  implementation,
67
- contractName: nameContract(implementation.contract),
79
+ contractId: idContract(implementation.contract),
80
+ implementationId: idImplementation(implementation),
68
81
  procedure: procedureName,
69
82
  protocol: implementation.protocol,
70
83
  dispatchKey: protocol.dispatchKey,
@@ -77,28 +90,27 @@ export function compileApplication(input) {
77
90
  }
78
91
  }
79
92
  validateDispatchKeys(targets, diagnostics);
80
- validateCoverage(descriptors, contractNames, diagnostics);
93
+ validateCoverage(descriptors, contractIds, diagnostics);
81
94
  validateDuplicateProviders(modules, diagnostics);
82
95
  const tokensById = collectCustomTokens(providers, targets, diagnostics);
83
96
  const contextKeysByName = collectContextKeys(targets, diagnostics);
84
97
  const pipelines = [];
85
- const implementations = [];
86
98
  for (const target of targets) {
87
99
  validatePipeline(target, diagnostics);
88
100
  pipelines.push({
89
- contract: target.contractName,
101
+ contract: target.contractId,
90
102
  procedure: target.procedure,
91
103
  protocol: target.protocol,
92
104
  layers: target.pipeline.map(toLayerIR),
93
105
  });
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
106
  }
107
+ const implementations = [...new Set(descriptors)].map((implementation) => ({
108
+ id: idImplementation(implementation),
109
+ name: implementation.name,
110
+ contract: idContract(implementation.contract),
111
+ protocol: implementation.protocol,
112
+ procedures: implementation.procedures,
113
+ }));
102
114
  const dependencyGraph = buildDependencyGraph(modules, descriptors, targets, tasks, diagnostics);
103
115
  const probedTokenIds = dependencyGraph.nodes
104
116
  .filter((node) => node.kind === 'token')
@@ -150,7 +162,7 @@ export function compileApplication(input) {
150
162
  })),
151
163
  tokens: [...new Set([...tokensById.keys(), ...probedTokenIds])].map((id) => ({ id })),
152
164
  contextKeys: [...contextKeysByName.keys()].map((name) => ({ name })),
153
- contracts: [...contractNames.values()],
165
+ contracts: [...contractIds.entries()].map(([contract, id]) => toContractIR(contract, id)),
154
166
  pipelines,
155
167
  implementations,
156
168
  tasks: tasks.map((task) => ({
@@ -161,12 +173,12 @@ export function compileApplication(input) {
161
173
  queues: queues.map(toQueueIR),
162
174
  executions: [
163
175
  ...targets.map((target) => ({
164
- id: `protocol:${target.protocol}:${target.contractName}.${target.procedure}`,
176
+ id: `protocol:${target.contractId}:${target.procedure}:${target.protocol}`,
165
177
  kind: 'protocol',
166
178
  protocol: target.protocol,
167
- contract: target.contractName,
179
+ contract: target.contractId,
168
180
  procedure: target.procedure,
169
- implementation: target.implementation.name,
181
+ implementation: target.implementationId,
170
182
  })),
171
183
  ...publicTasks.map((task) => ({
172
184
  id: `task:${task.name}`,
@@ -177,7 +189,7 @@ export function compileApplication(input) {
177
189
  ],
178
190
  capabilities: [
179
191
  ...targets.flatMap((target) => {
180
- const requiredBy = `${target.contractName}.${target.procedure}`;
192
+ const requiredBy = `${target.contractId}.${target.procedure}`;
181
193
  return [
182
194
  { name: 'crypto.random', scope: 'execution', requiredBy },
183
195
  ...(target.protocol === 'http'
@@ -257,7 +269,7 @@ function validateDispatchKeys(targets, diagnostics) {
257
269
  for (const target of targets) {
258
270
  if (target.dispatchKey === null)
259
271
  continue;
260
- const path = `${target.contractName}.${target.procedure}.${target.protocol}`;
272
+ const path = `${target.contractId}.${target.procedure}.${target.protocol}`;
261
273
  const existing = targetsByKey.get(target.dispatchKey);
262
274
  if (existing) {
263
275
  if (existing.implementation.contract === target.implementation.contract &&
@@ -265,7 +277,7 @@ function validateDispatchKeys(targets, diagnostics) {
265
277
  existing.protocol === target.protocol) {
266
278
  continue;
267
279
  }
268
- const existingPath = `${existing.contractName}.${existing.procedure}.${existing.protocol}`;
280
+ const existingPath = `${existing.contractId}.${existing.procedure}.${existing.protocol}`;
269
281
  diagnostics.push({
270
282
  code: 'LUTRE_PROTOCOL_001',
271
283
  message: `Duplicate protocol dispatch key "${target.dispatchKey}": ${existingPath}, ${path}`,
@@ -465,7 +477,7 @@ function buildDependencyGraph(modules, descriptors, targets, tasks, diagnostics)
465
477
  diagnostic.message.includes(token.id))) {
466
478
  diagnostics.push({
467
479
  code: 'LUTRE_TOKEN_001',
468
- message: `Token ID ${token.id}が異なるtoken declarationで重複しています`,
480
+ message: `Token ID ${token.id} is duplicated across different token declarations`,
469
481
  path: `dependency:${token.id}`,
470
482
  });
471
483
  }
@@ -736,7 +748,7 @@ function buildDependencyGraph(modules, descriptors, targets, tasks, diagnostics)
736
748
  const definition = layerDefinitionOf(item);
737
749
  const consumer = {
738
750
  kind: 'layer-consumer',
739
- id: `layer:${target.contractName}:${target.procedure}:${target.protocol}:${indexPath.join('.')}`,
751
+ id: `layer:${target.contractId}/${target.procedure}/${target.protocol}/${indexPath.join('.')}`,
740
752
  name: definition.name,
741
753
  };
742
754
  if (!nodes.some(({ id }) => id === consumer.id)) {
@@ -803,7 +815,7 @@ function validateDuplicateProviders(modules, diagnostics) {
803
815
  }
804
816
  diagnostics.push({
805
817
  code: 'LUTRE_DI_003',
806
- message: `Provider ${tokenName(provider.provide)}が${existing.module}と${moduleName}で重複しています`,
818
+ message: `Provider ${tokenName(provider.provide)} is duplicated in ${existing.module} and ${moduleName}`,
807
819
  path: `${moduleName}.providers.${tokenName(provider.provide)}`,
808
820
  });
809
821
  continue;
@@ -827,7 +839,7 @@ function collectCustomTokens(providers, targets, diagnostics) {
827
839
  if (existing && existing !== candidate) {
828
840
  diagnostics.push({
829
841
  code: 'LUTRE_TOKEN_001',
830
- message: `Token ID ${candidate.id}が異なるtoken declarationで重複しています`,
842
+ message: `Token ID ${candidate.id} is duplicated across different token declarations`,
831
843
  path,
832
844
  });
833
845
  return;
@@ -847,7 +859,7 @@ function collectCustomTokens(providers, targets, diagnostics) {
847
859
  function collectContextKeys(targets, diagnostics) {
848
860
  const keys = new Map();
849
861
  for (const target of targets) {
850
- const path = `${target.contractName}.${target.procedure}.${target.protocol}`;
862
+ const path = `${target.contractId}.${target.procedure}.${target.protocol}`;
851
863
  visitPipelineItems(target.pipeline, (item) => {
852
864
  if (item.kind !== 'layer')
853
865
  return;
@@ -856,7 +868,7 @@ function collectContextKeys(targets, diagnostics) {
856
868
  if (existing && existing !== key) {
857
869
  diagnostics.push({
858
870
  code: 'LUTRE_CONTEXT_002',
859
- message: `Context Key ${key.name}が異なる宣言で重複しています`,
871
+ message: `Context Key ${key.name} is duplicated across different declarations`,
860
872
  path,
861
873
  });
862
874
  continue;
@@ -867,6 +879,19 @@ function collectContextKeys(targets, diagnostics) {
867
879
  }
868
880
  return keys;
869
881
  }
882
+ function toContractIR(contract, id) {
883
+ return {
884
+ id,
885
+ procedures: Object.entries(contract.procedures).map(([procedureName, procedure]) => ({
886
+ name: procedureName,
887
+ protocols: Object.entries(procedure.protocols).map(([protocolName, protocol]) => ({
888
+ name: protocolName,
889
+ dispatchKey: protocol.dispatchKey,
890
+ interaction: protocol.interaction ?? 'unary',
891
+ })),
892
+ })),
893
+ };
894
+ }
870
895
  export function assertValidCompilation(result) {
871
896
  if (result.diagnostics.length > 0) {
872
897
  throw new StaticValidationError(result.diagnostics, result.graph);
@@ -889,7 +914,7 @@ function collectModules(roots) {
889
914
  visit(root);
890
915
  return modules;
891
916
  }
892
- function validateCoverage(implementations, contractNames, diagnostics) {
917
+ function validateCoverage(implementations, contractIds, diagnostics) {
893
918
  const contracts = new Set(implementations.map((implementation) => implementation.contract));
894
919
  for (const contract of contracts) {
895
920
  const protocols = new Set(Object.values(contract.procedures).flatMap((procedure) => Object.keys(procedure.protocols)));
@@ -900,7 +925,7 @@ function validateCoverage(implementations, contractNames, diagnostics) {
900
925
  const covering = implementations.filter((implementation) => implementation.contract === contract &&
901
926
  implementation.protocol === protocol &&
902
927
  implementation.procedures.includes(procedureName));
903
- const path = `${contractNames.get(contract) ?? 'Contract'}.${procedureName}.${protocol}`;
928
+ const path = `${contractIds.get(contract) ?? 'contract:unknown'}.${procedureName}.${protocol}`;
904
929
  if (covering.length === 0) {
905
930
  diagnostics.push({
906
931
  code: 'LUTRE_IMPL_001',
@@ -920,7 +945,7 @@ function validateCoverage(implementations, contractNames, diagnostics) {
920
945
  }
921
946
  }
922
947
  function validatePipeline(target, diagnostics) {
923
- const path = `${target.contractName}.${target.procedure}.${target.protocol}`;
948
+ const path = `${target.contractId}.${target.procedure}.${target.protocol}`;
924
949
  const flattened = [];
925
950
  visitPipelineItems(target.pipeline, (item, indexPath) => {
926
951
  flattened.push({ item, indexPath });
@@ -964,7 +989,7 @@ function validatePipeline(target, diagnostics) {
964
989
  if (!validated.has(required)) {
965
990
  diagnostics.push({
966
991
  code: 'LUTRE_VALIDATION_001',
967
- message: `${item.name}にはvalidation済みの${required}が必要ですが、validate.${required}より前に配置されています`,
992
+ message: `${item.name} requires validated ${required} but appears before validate.${required}`,
968
993
  path,
969
994
  });
970
995
  }
@@ -973,7 +998,7 @@ function validatePipeline(target, diagnostics) {
973
998
  if (!available.has(required)) {
974
999
  diagnostics.push({
975
1000
  code: 'LUTRE_PIPELINE_004',
976
- message: `${item.name}が必要とするContext Key ${contextKeyName(required)}は利用できません`,
1001
+ message: `Context Key ${contextKeyName(required)} required by ${item.name} is unavailable`,
977
1002
  path,
978
1003
  });
979
1004
  }
@@ -982,7 +1007,7 @@ function validatePipeline(target, diagnostics) {
982
1007
  if (available.has(provided)) {
983
1008
  diagnostics.push({
984
1009
  code: 'LUTRE_CONTEXT_003',
985
- message: `${item.name}は既存のContext Key ${contextKeyName(provided)}を暗黙に上書きできません`,
1010
+ message: `${item.name} cannot implicitly overwrite existing Context Key ${contextKeyName(provided)}`,
986
1011
  path,
987
1012
  });
988
1013
  }
@@ -995,7 +1020,7 @@ function validatePipeline(target, diagnostics) {
995
1020
  if (!response) {
996
1021
  diagnostics.push({
997
1022
  code: 'LUTRE_SHORT_CIRCUIT_001',
998
- message: `${item.name}のshort circuit variant ${shortCircuit.variant}がresponseに宣言されていません`,
1023
+ message: `Short-circuit variant ${shortCircuit.variant} from ${item.name} is not declared in the response`,
999
1024
  path,
1000
1025
  });
1001
1026
  continue;
@@ -1005,7 +1030,7 @@ function validatePipeline(target, diagnostics) {
1005
1030
  response.status !== expectedStatus) {
1006
1031
  diagnostics.push({
1007
1032
  code: 'LUTRE_SHORT_CIRCUIT_002',
1008
- message: `${item.name}のshort circuit variant ${shortCircuit.variant}はHTTP ${expectedStatus}である必要があります`,
1033
+ message: `Short-circuit variant ${shortCircuit.variant} from ${item.name} must use HTTP ${expectedStatus}`,
1009
1034
  path,
1010
1035
  });
1011
1036
  }
@@ -1,2 +1,3 @@
1
1
  export * from './ir.js';
2
- export * from './graph.js';
2
+ export { StaticValidationError, assertValidCompilation, validateGraph, type ApplicationCompilationInput, } from './graph.js';
3
+ export { buildApplicationGraph, compileApplication } from './compiler.js';
@@ -1,2 +1,3 @@
1
1
  export * from './ir.js';
2
- export * from './graph.js';
2
+ export { StaticValidationError, assertValidCompilation, validateGraph, } from './graph.js';
3
+ export { buildApplicationGraph, compileApplication } from './compiler.js';