@ontrails/library 1.0.0-beta.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # @ontrails/library
2
+
3
+ ## 1.0.0-beta.24
4
+
5
+ Initial prerelease changelog for the library surface package.
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # @ontrails/library
2
+
3
+ Render a Trails topo as an idiomatic TypeScript library.
4
+
5
+ `@ontrails/library` is a peer surface for plain TypeScript consumers. It reads the same contract that CLI, MCP, and HTTP read, then projects that graph into function calls, package-facing errors, schema exports, and generated package files.
6
+
7
+ The package is publishable as the runtime dependency for generated Trails libraries. Generated packages can depend on it while keeping their consumer-facing API idiomatic and package-local.
8
+
9
+ ## API
10
+
11
+ ```ts
12
+ import { compile, deriveLibraryApi, surface } from '@ontrails/library';
13
+
14
+ const projection = deriveLibraryApi(app);
15
+ const client = await surface(app);
16
+ const files = compile(app, {
17
+ appExportName: 'app',
18
+ appImportPath: '@acme/app',
19
+ packageName: '@acme/generated',
20
+ });
21
+ ```
22
+
23
+ - `deriveLibraryApi(graph, options)` is the pure projection. It decides which public trails become library exports, how export names are derived, which trails are excluded, and where export-name collisions exist.
24
+ - `surface(graph, options)` returns an in-memory callable client. The root call lane unwraps `Result.ok` into a return value and maps `Result.err` into typed `LibraryError` subclasses.
25
+ - `compile(graph, options)` returns a stable file plan for a generated package. Writing those files is intentionally a thin apply step outside the compiler.
26
+
27
+ ## Generated package shape
28
+
29
+ Generated packages use one package with subpath exports:
30
+
31
+ ```text
32
+ . consumer-fluent root functions and createX factories
33
+ ./result no-throw Result-returning functions
34
+ ./schemas authored Zod schemas and optional schema-owned type aliases
35
+ ./trails the Trails-native topo entrypoint
36
+ ```
37
+
38
+ Stateless trails project to root named exports. Resource-bearing trails project behind a generated `createX(options)` factory so callers can provide resource configuration once and call several related methods from the same client.
39
+
40
+ Generated root and `/result` subpaths share one internal client module, so importing both subpaths does not open separate root library surfaces.
41
+
42
+ ## Typed signatures
43
+
44
+ Topo artifacts carry durable contract facts, but they do not preserve erased source-level TypeScript generics. Generated packages therefore stay honest by defaulting method signatures to `unknown` unless the caller binds a projected trail id to the source trail export that owns its schema types:
45
+
46
+ ```ts
47
+ const files = compile(app, {
48
+ appExportName: 'app',
49
+ appImportPath: '../fixture-app',
50
+ packageName: '@acme/generated',
51
+ trailTypeExports: {
52
+ 'widget.ping': 'pingTrail',
53
+ },
54
+ typeImportPath: '../fixture-trails',
55
+ });
56
+ ```
57
+
58
+ With that binding, `/schemas` emits aliases such as `WidgetPingInput = TrailInput<typeof pingTrail>` and the root and `/result` subpaths use those aliases in their public signatures.
59
+
60
+ Typed layer inputs are projected into the same public method input object as trail fields. When a layer field collides with a trail field or reserved surface name, the generated library input uses the same deterministic `<layerName><Field>` rename rule as other object-shaped surfaces. Runtime calls validate the projected input, strip layer-owned fields before trail validation, and route them to the layer's own input slot. When a source trail type binding is provided, generated signatures widen layer-projected inputs with `Record<string, unknown>` until layer input type exports have a source-level owner.
61
+
62
+ ## Errors
63
+
64
+ The root API throws package-facing `LibraryError` subclasses. This is a surface mapping, not a blaze behavior change: blazes still return `Result`.
65
+
66
+ The `/result` subpath preserves the no-throw envelope:
67
+
68
+ ```ts
69
+ import { widgetPing } from '@acme/generated/result';
70
+
71
+ const result = await widgetPing(input);
72
+ ```
73
+
74
+ The mapper is built with the shared Trails error taxonomy, so new categories must be covered before the package can typecheck.
75
+
76
+ ## Governance and dogfood
77
+
78
+ Library projection facts are embedded in `TopoGraph.library` by Topographer. Warden's `library-projection-coherence` rule checks that serialized projection facts do not drift from the graph, including missing target trails and export name collisions.
79
+
80
+ Run the focused package checks while changing the surface:
81
+
82
+ ```bash
83
+ bun run library:smoke
84
+ bun run library:dogfood:warden
85
+ ```
86
+
87
+ `library:dogfood:warden` compiles the Warden topo into a generated package, typechecks that generated package, runs a generated consumer test through root, `/result`, `/schemas`, and `/trails`, then dry-run packs it.
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@ontrails/library",
3
+ "version": "1.0.0-beta.24",
4
+ "files": [
5
+ "src/**/*.ts",
6
+ "!src/**/__tests__/**",
7
+ "!src/**/*.test.ts",
8
+ "!src/**/*.test-d.ts",
9
+ "README.md",
10
+ "CHANGELOG.md"
11
+ ],
12
+ "type": "module",
13
+ "exports": {
14
+ ".": "./src/index.ts",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc -b",
19
+ "test": "bun test --timeout 30000",
20
+ "typecheck": "tsc --noEmit",
21
+ "lint": "oxlint ./src",
22
+ "clean": "rm -rf dist *.tsbuildinfo"
23
+ },
24
+ "dependencies": {
25
+ "@ontrails/core": "^1.0.0-beta.24"
26
+ },
27
+ "devDependencies": {
28
+ "@ontrails/testing": "^1.0.0-beta.24"
29
+ },
30
+ "peerDependencies": {
31
+ "zod": "^4.3.5"
32
+ }
33
+ }
package/src/compile.ts ADDED
@@ -0,0 +1,506 @@
1
+ /**
2
+ * `compile` — the package emitter. Consumes the `LibraryProjection` (never the
3
+ * topo directly) and produces the source files of a generated TypeScript
4
+ * package: consumer-fluent root, `/result`, `/schemas`, `/trails` subpaths.
5
+ *
6
+ * v0 is runtime-backed: the generated package imports the source topo and the
7
+ * `@ontrails/library` runtime (the kernel seam), so execution delegates to the
8
+ * shared pipeline. The standalone trajectory (vendoring the kernel) does not
9
+ * change consumer code — see the runtime-kernel section of the ADR.
10
+ *
11
+ * This slice returns the file plan (path + content); writing it to disk is a
12
+ * thin apply step. Pure: derives the projection and builds strings, no I/O.
13
+ */
14
+ import { deriveLibraryApi } from './derive.js';
15
+ import type {
16
+ DeriveLibraryApiOptions,
17
+ LibraryExport,
18
+ LibraryProjection,
19
+ } from './derive.js';
20
+ import type { Topo } from './kernel.js';
21
+
22
+ /** Options for emitting a generated library package. */
23
+ export interface CompileOptions extends DeriveLibraryApiOptions {
24
+ /** The generated package name (e.g. `@acme/core`). */
25
+ readonly packageName: string;
26
+ /** Import specifier the generated code uses to reach the source topo. */
27
+ readonly appImportPath: string;
28
+ /** The exported binding name of the topo at `appImportPath` (default `app`). */
29
+ readonly appExportName?: string;
30
+ /** Runtime dependency range for `@ontrails/library` in emitted package.json. */
31
+ readonly libraryDependency?: string;
32
+ /** Generated package version. Defaults to `0.0.0`. */
33
+ readonly version?: string;
34
+ /** Peer dependency range for Zod in emitted package.json. */
35
+ readonly zodDependency?: string;
36
+ /**
37
+ * Import specifier for source trail type bindings. Defaults to
38
+ * `appImportPath` when `trailTypeExports` is provided.
39
+ */
40
+ readonly typeImportPath?: string;
41
+ /**
42
+ * Optional mapping from projected trail id to the source module export that
43
+ * owns that trail's TypeScript type. Unmapped trails intentionally keep
44
+ * `unknown` public signatures instead of pretending topo artifacts preserve
45
+ * erased source types.
46
+ */
47
+ readonly trailTypeExports?: Readonly<Record<string, string>>;
48
+ }
49
+
50
+ /** A single emitted file: project-relative path and full contents. */
51
+ export interface CompiledFile {
52
+ readonly path: string;
53
+ readonly content: string;
54
+ }
55
+
56
+ /** The result of compiling a topo into a generated library package. */
57
+ export interface CompileResult {
58
+ readonly packageName: string;
59
+ /** The resolved projection the files were emitted from. */
60
+ readonly projection: LibraryProjection;
61
+ /** The emitted files, in stable path order. */
62
+ readonly files: readonly CompiledFile[];
63
+ }
64
+
65
+ const pascalCase = (value: string): string =>
66
+ value
67
+ .split(/[.\-_]/u)
68
+ .filter((word) => word.length > 0)
69
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
70
+ .join('');
71
+
72
+ const isStateless = (entry: LibraryExport): boolean =>
73
+ entry.resources.length === 0;
74
+
75
+ const statelessExports = (
76
+ projection: LibraryProjection
77
+ ): readonly LibraryExport[] => projection.exports.filter(isStateless);
78
+
79
+ const resourceExports = (
80
+ projection: LibraryProjection
81
+ ): readonly LibraryExport[] =>
82
+ projection.exports.filter((entry) => !isStateless(entry));
83
+
84
+ const factoryName = (projection: LibraryProjection): string =>
85
+ `create${pascalCase(projection.app)}`;
86
+
87
+ const DEFAULT_LIBRARY_DEPENDENCY = '^1.0.0';
88
+ const DEFAULT_ZOD_DEPENDENCY = '^4.3.5';
89
+
90
+ const sanitizeJsDocLine = (value: string): string =>
91
+ value.replaceAll('*/', '* /').trim();
92
+
93
+ const jsDoc = (lines: readonly string[], indent = ''): string =>
94
+ [
95
+ `${indent}/**`,
96
+ ...lines
97
+ .map(sanitizeJsDocLine)
98
+ .filter((line) => line.length > 0)
99
+ .map((line) => `${indent} * ${line}`),
100
+ `${indent} */`,
101
+ ].join('\n');
102
+
103
+ const exportDescription = (entry: LibraryExport): string =>
104
+ entry.description ?? `Call the \`${entry.trailId}\` trail.`;
105
+
106
+ const IDENTIFIER_PATTERN = /^[A-Za-z_$][\w$]*$/u;
107
+
108
+ interface ExportTypeBinding {
109
+ readonly input: string;
110
+ readonly output: string;
111
+ readonly sourceExport: string;
112
+ }
113
+
114
+ type TypeBindings = ReadonlyMap<string, ExportTypeBinding>;
115
+
116
+ const typeNamesFor = (
117
+ entry: LibraryExport
118
+ ): Pick<ExportTypeBinding, 'input' | 'output'> => {
119
+ const base = pascalCase(entry.exportName);
120
+ return { input: `${base}Input`, output: `${base}Output` };
121
+ };
122
+
123
+ const resolveTypeBindings = (
124
+ projection: LibraryProjection,
125
+ options: CompileOptions
126
+ ): TypeBindings => {
127
+ const configured = options.trailTypeExports ?? {};
128
+ const bindings = new Map<string, ExportTypeBinding>();
129
+ for (const entry of projection.exports) {
130
+ const sourceExport = configured[entry.trailId];
131
+ if (!sourceExport) {
132
+ continue;
133
+ }
134
+ if (!IDENTIFIER_PATTERN.test(sourceExport)) {
135
+ throw new Error(
136
+ `trailTypeExports["${entry.trailId}"] must be an exported identifier`
137
+ );
138
+ }
139
+ bindings.set(entry.trailId, { ...typeNamesFor(entry), sourceExport });
140
+ }
141
+ return bindings;
142
+ };
143
+
144
+ const inputTypeFor = (entry: LibraryExport, bindings: TypeBindings): string => {
145
+ const input = bindings.get(entry.trailId)?.input;
146
+ if (entry.layerInputs.length === 0) {
147
+ return input ?? 'unknown';
148
+ }
149
+ return input === undefined
150
+ ? 'Record<string, unknown>'
151
+ : `${input} & Record<string, unknown>`;
152
+ };
153
+
154
+ const outputTypeFor = (entry: LibraryExport, bindings: TypeBindings): string =>
155
+ bindings.get(entry.trailId)?.output ?? 'unknown';
156
+
157
+ const schemaTypeImport = (
158
+ projection: LibraryProjection,
159
+ bindings: TypeBindings
160
+ ): string | undefined => {
161
+ const names = projection.exports.flatMap((entry) => {
162
+ const binding = bindings.get(entry.trailId);
163
+ return binding ? [binding.input, binding.output] : [];
164
+ });
165
+ if (names.length === 0) {
166
+ return undefined;
167
+ }
168
+ return `import type { ${names.join(', ')} } from './schemas.js';`;
169
+ };
170
+
171
+ const generatePackageJson = (options: CompileOptions): string => {
172
+ const manifest = {
173
+ dependencies: {
174
+ '@ontrails/library':
175
+ options.libraryDependency ?? DEFAULT_LIBRARY_DEPENDENCY,
176
+ zod: options.zodDependency ?? DEFAULT_ZOD_DEPENDENCY,
177
+ },
178
+ exports: {
179
+ '.': './src/index.ts',
180
+ './package.json': './package.json',
181
+ './result': './src/result.ts',
182
+ './schemas': './src/schemas.ts',
183
+ './trails': './src/trails.ts',
184
+ },
185
+ name: options.packageName,
186
+ type: 'module',
187
+ version: options.version ?? '0.0.0',
188
+ };
189
+ return `${JSON.stringify(manifest, null, 2)}\n`;
190
+ };
191
+
192
+ const statelessFunction = (
193
+ entry: LibraryExport,
194
+ bindings: TypeBindings
195
+ ): string =>
196
+ [
197
+ jsDoc([
198
+ exportDescription(entry),
199
+ `Projects trail \`${entry.trailId}\` as a stateless library function.`,
200
+ ]),
201
+ `export const ${entry.exportName} = (`,
202
+ ` input: ${inputTypeFor(entry, bindings)}`,
203
+ `): Promise<${outputTypeFor(entry, bindings)}> =>`,
204
+ ' // The runtime validates declared output schemas before this unwrap returns.',
205
+ ` rootClient.call.${entry.exportName}(input) as Promise<${outputTypeFor(entry, bindings)}>;`,
206
+ ].join('\n');
207
+
208
+ const factoryMethod = (entry: LibraryExport, bindings: TypeBindings): string =>
209
+ [
210
+ jsDoc(
211
+ [
212
+ exportDescription(entry),
213
+ `Projects trail \`${entry.trailId}\` behind the resource client.`,
214
+ ],
215
+ ' '
216
+ ),
217
+ ` ${entry.exportName}: (`,
218
+ ` input: ${inputTypeFor(entry, bindings)}`,
219
+ ` ): Promise<${outputTypeFor(entry, bindings)}> =>`,
220
+ ' // The runtime validates declared output schemas before this unwrap returns.',
221
+ ` client.call.${entry.exportName}(input) as Promise<${outputTypeFor(entry, bindings)}>,`,
222
+ ].join('\n');
223
+
224
+ const generateIndex = (
225
+ projection: LibraryProjection,
226
+ bindings: TypeBindings
227
+ ): string => {
228
+ const stateless = statelessExports(projection);
229
+ const resourceful = resourceExports(projection);
230
+ const typedSchemas = schemaTypeImport(projection, bindings);
231
+
232
+ const parts: string[] = [
233
+ "import type { SurfaceLibraryOptions } from '@ontrails/library';",
234
+ ...(typedSchemas ? [typedSchemas] : []),
235
+ '',
236
+ "import { createClient, rootClient } from './client.js';",
237
+ ];
238
+
239
+ for (const entry of stateless) {
240
+ parts.push('', statelessFunction(entry, bindings));
241
+ }
242
+
243
+ if (resourceful.length > 0) {
244
+ parts.push(
245
+ '',
246
+ `export const ${factoryName(projection)} = async (`,
247
+ ' options: SurfaceLibraryOptions = {}',
248
+ ') => {',
249
+ ' const client = await createClient(options);',
250
+ ' return {',
251
+ resourceful.map((entry) => factoryMethod(entry, bindings)).join('\n'),
252
+ ' };',
253
+ '};'
254
+ );
255
+ }
256
+
257
+ return `${parts.join('\n')}\n`;
258
+ };
259
+
260
+ const generateClient = (): string =>
261
+ [
262
+ "import { surface } from '@ontrails/library';",
263
+ "import type { SurfaceLibraryOptions } from '@ontrails/library';",
264
+ '',
265
+ `import { app } from './trails.js';`,
266
+ '',
267
+ 'export const rootClient = await surface(app);',
268
+ 'export const createClient = (options: SurfaceLibraryOptions = {}) =>',
269
+ ' surface(app, options);',
270
+ '',
271
+ ].join('\n');
272
+
273
+ const generateSchemas = (
274
+ projection: LibraryProjection,
275
+ options: CompileOptions,
276
+ bindings: TypeBindings
277
+ ): string => {
278
+ const appExport = 'app';
279
+ const typedEntries = projection.exports.filter((entry) =>
280
+ bindings.has(entry.trailId)
281
+ );
282
+ const sourceTypeExports = [
283
+ ...new Set(
284
+ typedEntries.map((entry) => {
285
+ const binding = bindings.get(entry.trailId);
286
+ if (binding === undefined) {
287
+ throw new Error(`missing type binding for trail "${entry.trailId}"`);
288
+ }
289
+ return binding.sourceExport;
290
+ })
291
+ ),
292
+ ].toSorted((left, right) => left.localeCompare(right));
293
+ const lines = [
294
+ `import { ${appExport} } from './trails.js';`,
295
+ "import { deriveLibraryApi } from '@ontrails/library';",
296
+ ...(typedEntries.length > 0
297
+ ? [
298
+ "import type { TrailInput, TrailOutput } from '@ontrails/library';",
299
+ `import type { ${sourceTypeExports.join(', ')} } from '${options.typeImportPath ?? options.appImportPath}';`,
300
+ ]
301
+ : []),
302
+ '',
303
+ '// Authored Zod schemas, keyed by export name, projected from the topo.',
304
+ 'const projection = deriveLibraryApi(app);',
305
+ 'const byName = new Map(',
306
+ ' projection.exports.map((entry) => [entry.exportName, entry])',
307
+ ');',
308
+ '',
309
+ 'const requireExport = (name: string) => {',
310
+ ' const entry = byName.get(name);',
311
+ ' if (!entry) {',
312
+ " throw new Error('missing projected library export: ' + name);",
313
+ ' }',
314
+ ' return entry;',
315
+ '};',
316
+ '',
317
+ ];
318
+ for (const entry of projection.exports) {
319
+ const binding = bindings.get(entry.trailId);
320
+ if (binding) {
321
+ lines.push(
322
+ '',
323
+ `export type ${binding.input} = TrailInput<typeof ${binding.sourceExport}>;`,
324
+ `export type ${binding.output} = TrailOutput<typeof ${binding.sourceExport}>;`
325
+ );
326
+ }
327
+ lines.push(
328
+ '',
329
+ jsDoc([
330
+ `Authored input schema for \`${entry.trailId}\`, exported as \`${entry.exportName}\`.`,
331
+ ]),
332
+ `export const ${entry.exportName}InputSchema = requireExport('${entry.exportName}').input;`,
333
+ '',
334
+ jsDoc([
335
+ `Authored output schema for \`${entry.trailId}\`, if the trail declares one.`,
336
+ ]),
337
+ `export const ${entry.exportName}OutputSchema = requireExport('${entry.exportName}').output;`
338
+ );
339
+ }
340
+ lines.push('', 'export const schemas = {');
341
+ for (const entry of projection.exports) {
342
+ lines.push(
343
+ ` ${entry.exportName}: {`,
344
+ ` input: ${entry.exportName}InputSchema,`,
345
+ ` output: ${entry.exportName}OutputSchema,`,
346
+ ' },'
347
+ );
348
+ }
349
+ lines.push('} as const;');
350
+ return `${lines.join('\n')}\n`;
351
+ };
352
+
353
+ const generateTrails = (options: CompileOptions): string => {
354
+ const appExport = options.appExportName ?? 'app';
355
+ return [
356
+ '// Full Trails-native entrypoint: the resolved topo for composition,',
357
+ '// contract tests, and graph inspection.',
358
+ `export { ${appExport} as app } from '${options.appImportPath}';`,
359
+ '',
360
+ ].join('\n');
361
+ };
362
+
363
+ const resultStatelessFunction = (
364
+ entry: LibraryExport,
365
+ bindings: TypeBindings
366
+ ): string =>
367
+ [
368
+ jsDoc([
369
+ exportDescription(entry),
370
+ `Returns the raw Result boundary for trail \`${entry.trailId}\`.`,
371
+ ]),
372
+ `export const ${entry.exportName} = (`,
373
+ ` input: ${inputTypeFor(entry, bindings)}`,
374
+ `): Promise<Result<${outputTypeFor(entry, bindings)}, LibraryError>> =>`,
375
+ ' // The runtime validates declared output schemas before this Result resolves.',
376
+ ` resultClient.result.${entry.exportName}(input) as Promise<Result<${outputTypeFor(entry, bindings)}, LibraryError>>;`,
377
+ ].join('\n');
378
+
379
+ const resultFactoryMethod = (
380
+ entry: LibraryExport,
381
+ bindings: TypeBindings
382
+ ): string =>
383
+ [
384
+ jsDoc(
385
+ [
386
+ exportDescription(entry),
387
+ `Returns the raw Result boundary for trail \`${entry.trailId}\`.`,
388
+ ],
389
+ ' '
390
+ ),
391
+ ` ${entry.exportName}: (`,
392
+ ` input: ${inputTypeFor(entry, bindings)}`,
393
+ ` ): Promise<Result<${outputTypeFor(entry, bindings)}, LibraryError>> =>`,
394
+ ' // The runtime validates declared output schemas before this Result resolves.',
395
+ ` client.result.${entry.exportName}(input) as Promise<Result<${outputTypeFor(entry, bindings)}, LibraryError>>,`,
396
+ ].join('\n');
397
+
398
+ const generateResult = (
399
+ projection: LibraryProjection,
400
+ bindings: TypeBindings
401
+ ): string => {
402
+ const stateless = statelessExports(projection);
403
+ const resourceful = resourceExports(projection);
404
+ const typedSchemas = schemaTypeImport(projection, bindings);
405
+ const parts = [
406
+ '// No-throw API: returns the Result envelope instead of unwrapping.',
407
+ "import { runLibraryResult } from '@ontrails/library';",
408
+ "import type { LibraryError, Result, SurfaceLibraryOptions } from '@ontrails/library';",
409
+ ...(typedSchemas ? [typedSchemas] : []),
410
+ '',
411
+ "import { createClient, rootClient } from './client.js';",
412
+ "import { app } from './trails.js';",
413
+ '',
414
+ 'const resultClient = rootClient;',
415
+ ];
416
+
417
+ for (const entry of stateless) {
418
+ parts.push('', resultStatelessFunction(entry, bindings));
419
+ }
420
+
421
+ if (resourceful.length > 0) {
422
+ parts.push(
423
+ '',
424
+ `export const ${factoryName(projection)} = async (`,
425
+ ' options: SurfaceLibraryOptions = {}',
426
+ ') => {',
427
+ ' const client = await createClient(options);',
428
+ ' return {',
429
+ resourceful
430
+ .map((entry) => resultFactoryMethod(entry, bindings))
431
+ .join('\n'),
432
+ ' };',
433
+ '};'
434
+ );
435
+ }
436
+
437
+ parts.push(
438
+ '',
439
+ 'export const call = (',
440
+ ' id: string,',
441
+ ' input: unknown,',
442
+ ' options: SurfaceLibraryOptions = {}',
443
+ ') => runLibraryResult(app, id, input, options);',
444
+ ''
445
+ );
446
+
447
+ return `${parts.join('\n')}\n`;
448
+ };
449
+
450
+ const generateTsconfig = (): string =>
451
+ `${JSON.stringify(
452
+ {
453
+ compilerOptions: {
454
+ module: 'preserve',
455
+ moduleResolution: 'bundler',
456
+ strict: true,
457
+ target: 'esnext',
458
+ },
459
+ include: ['src'],
460
+ },
461
+ null,
462
+ 2
463
+ )}\n`;
464
+
465
+ /**
466
+ * Compile a topo into a generated library package. Returns the emitted file
467
+ * plan; the resolved projection is included for inspection and governance.
468
+ *
469
+ * @example
470
+ * const result = compile(app, {
471
+ * packageName: '@acme/core',
472
+ * appImportPath: '@acme/app',
473
+ * });
474
+ * for (const file of result.files) {
475
+ * console.log(file.path);
476
+ * }
477
+ */
478
+ export const compile = (
479
+ graph: Topo,
480
+ options: CompileOptions
481
+ ): CompileResult => {
482
+ const projection = deriveLibraryApi(graph, options);
483
+ const typeBindings = resolveTypeBindings(projection, options);
484
+ const files: CompiledFile[] = [
485
+ { content: generatePackageJson(options), path: 'package.json' },
486
+ { content: generateTsconfig(), path: 'tsconfig.json' },
487
+ {
488
+ content: generateClient(),
489
+ path: 'src/client.ts',
490
+ },
491
+ {
492
+ content: generateIndex(projection, typeBindings),
493
+ path: 'src/index.ts',
494
+ },
495
+ {
496
+ content: generateResult(projection, typeBindings),
497
+ path: 'src/result.ts',
498
+ },
499
+ {
500
+ content: generateSchemas(projection, options, typeBindings),
501
+ path: 'src/schemas.ts',
502
+ },
503
+ { content: generateTrails(options), path: 'src/trails.ts' },
504
+ ];
505
+ return { files, packageName: options.packageName, projection };
506
+ };