@ontrails/library 1.0.0-beta.41 → 1.0.0-beta.43
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 +12 -0
- package/README.md +7 -7
- package/package.json +3 -3
- package/src/compile.ts +43 -43
- package/src/derive.ts +25 -25
- package/src/index.ts +3 -3
- package/src/kernel.ts +1 -1
- package/src/layer-input.ts +29 -29
- package/src/surface.ts +11 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @ontrails/library
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.43
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`88a6a62`](https://github.com/outfitter-dev/trails/commit/88a6a62a9e9e230ca6d368fa78dc3ece6c816204): Complete the v1 classification-first cutover from projection/project vocabulary
|
|
8
|
+
to derive/derived for contract-owned fact production and render/rendered for
|
|
9
|
+
surface presentation. Public type, helper, rule, relation, and report names move
|
|
10
|
+
without compatibility aliases; ordinary repository/project nouns remain
|
|
11
|
+
explicit preserves or structured review inventory.
|
|
12
|
+
|
|
13
|
+
## 1.0.0-beta.42
|
|
14
|
+
|
|
3
15
|
## 1.0.0-beta.41
|
|
4
16
|
|
|
5
17
|
## 1.0.0-beta.40
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Render a Trails topo as an idiomatic TypeScript library.
|
|
4
4
|
|
|
5
|
-
`@ontrails/library` is a peer surface for plain TypeScript consumers. It reads the same contract that CLI, MCP, and HTTP read, then
|
|
5
|
+
`@ontrails/library` is a peer surface for plain TypeScript consumers. It reads the same contract that CLI, MCP, and HTTP read, then renders that graph into function calls, package-facing errors, schema exports, and generated package files.
|
|
6
6
|
|
|
7
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
8
|
|
|
@@ -11,7 +11,7 @@ The package is publishable as the runtime dependency for generated Trails librar
|
|
|
11
11
|
```ts
|
|
12
12
|
import { compile, deriveLibraryApi, surface } from '@ontrails/library';
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const renderingPlan = deriveLibraryApi(app);
|
|
15
15
|
const client = await surface(app);
|
|
16
16
|
const files = compile(app, {
|
|
17
17
|
appExportName: 'app',
|
|
@@ -20,7 +20,7 @@ const files = compile(app, {
|
|
|
20
20
|
});
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
- `deriveLibraryApi(graph, options)` is the pure
|
|
23
|
+
- `deriveLibraryApi(graph, options)` is the pure derivation. It returns the rendering plan that decides which public trails become library exports, how export names are derived, which trails are excluded, and where export-name collisions exist.
|
|
24
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
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
26
|
|
|
@@ -35,13 +35,13 @@ Generated packages use one package with subpath exports:
|
|
|
35
35
|
./trails the Trails-native topo entrypoint
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
Stateless trails
|
|
38
|
+
Stateless trails render to root named exports. Resource-bearing trails render behind a generated `createX(options)` factory so callers can provide resource configuration once and call several related methods from the same client.
|
|
39
39
|
|
|
40
40
|
Generated root and `/result` subpaths share one internal client module, so importing both subpaths does not open separate root library surfaces.
|
|
41
41
|
|
|
42
42
|
## Typed signatures
|
|
43
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
|
|
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 rendered trail id to the source trail export that owns its schema types:
|
|
45
45
|
|
|
46
46
|
```ts
|
|
47
47
|
const files = compile(app, {
|
|
@@ -57,7 +57,7 @@ const files = compile(app, {
|
|
|
57
57
|
|
|
58
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
59
|
|
|
60
|
-
Typed layer inputs are
|
|
60
|
+
Typed layer inputs are rendered 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 rendered 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-rendered inputs with `Record<string, unknown>` until layer input type exports have a source-level owner.
|
|
61
61
|
|
|
62
62
|
## Errors
|
|
63
63
|
|
|
@@ -75,7 +75,7 @@ The mapper is built with the shared Trails error taxonomy, so new categories mus
|
|
|
75
75
|
|
|
76
76
|
## Governance and dogfood
|
|
77
77
|
|
|
78
|
-
Library
|
|
78
|
+
Library-derived facts are embedded in `TopoGraph.library` by Topography. Warden's `library-render-coherence` rule checks that serialized rendering facts do not drift from the graph, including missing target trails and export name collisions.
|
|
79
79
|
|
|
80
80
|
Run the focused package checks while changing the surface:
|
|
81
81
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ontrails/library",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.43",
|
|
4
4
|
"files": [
|
|
5
5
|
"src/**/*.ts",
|
|
6
6
|
"!src/**/__tests__/**",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"clean": "rm -rf dist *.tsbuildinfo"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@ontrails/core": "^1.0.0-beta.
|
|
25
|
+
"@ontrails/core": "^1.0.0-beta.43"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@ontrails/testing": "^1.0.0-beta.
|
|
28
|
+
"@ontrails/testing": "^1.0.0-beta.43"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"zod": "^4.3.5"
|
package/src/compile.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `compile` — the package emitter. Consumes the `
|
|
2
|
+
* `compile` — the package emitter. Consumes the `LibraryRenderingPlan` (never the
|
|
3
3
|
* topo directly) and produces the source files of a generated TypeScript
|
|
4
4
|
* package: consumer-fluent root, `/result`, `/schemas`, `/trails` subpaths.
|
|
5
5
|
*
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
* change consumer code — see the runtime-kernel section of the ADR.
|
|
10
10
|
*
|
|
11
11
|
* This slice returns the file plan (path + content); writing it to disk is a
|
|
12
|
-
* thin apply step. Pure: derives the
|
|
12
|
+
* thin apply step. Pure: derives the rendering and builds strings, no I/O.
|
|
13
13
|
*/
|
|
14
14
|
import { deriveLibraryApi } from './derive.js';
|
|
15
15
|
import type {
|
|
16
16
|
DeriveLibraryApiOptions,
|
|
17
17
|
LibraryExport,
|
|
18
|
-
|
|
18
|
+
LibraryRenderingPlan,
|
|
19
19
|
} from './derive.js';
|
|
20
20
|
import type { Topo } from './kernel.js';
|
|
21
21
|
|
|
@@ -39,7 +39,7 @@ export interface CompileOptions extends DeriveLibraryApiOptions {
|
|
|
39
39
|
*/
|
|
40
40
|
readonly typeImportPath?: string;
|
|
41
41
|
/**
|
|
42
|
-
* Optional mapping from
|
|
42
|
+
* Optional mapping from rendered trail id to the source module export that
|
|
43
43
|
* owns that trail's TypeScript type. Unmapped trails intentionally keep
|
|
44
44
|
* `unknown` public signatures instead of pretending topo artifacts preserve
|
|
45
45
|
* erased source types.
|
|
@@ -56,8 +56,8 @@ export interface CompiledFile {
|
|
|
56
56
|
/** The result of compiling a topo into a generated library package. */
|
|
57
57
|
export interface CompileResult {
|
|
58
58
|
readonly packageName: string;
|
|
59
|
-
/** The resolved
|
|
60
|
-
readonly
|
|
59
|
+
/** The resolved rendering the files were emitted from. */
|
|
60
|
+
readonly rendering: LibraryRenderingPlan;
|
|
61
61
|
/** The emitted files, in stable path order. */
|
|
62
62
|
readonly files: readonly CompiledFile[];
|
|
63
63
|
}
|
|
@@ -73,16 +73,16 @@ const isStateless = (entry: LibraryExport): boolean =>
|
|
|
73
73
|
entry.resources.length === 0;
|
|
74
74
|
|
|
75
75
|
const statelessExports = (
|
|
76
|
-
|
|
77
|
-
): readonly LibraryExport[] =>
|
|
76
|
+
rendering: LibraryRenderingPlan
|
|
77
|
+
): readonly LibraryExport[] => rendering.exports.filter(isStateless);
|
|
78
78
|
|
|
79
79
|
const resourceExports = (
|
|
80
|
-
|
|
80
|
+
rendering: LibraryRenderingPlan
|
|
81
81
|
): readonly LibraryExport[] =>
|
|
82
|
-
|
|
82
|
+
rendering.exports.filter((entry) => !isStateless(entry));
|
|
83
83
|
|
|
84
|
-
const factoryName = (
|
|
85
|
-
`create${pascalCase(
|
|
84
|
+
const factoryName = (rendering: LibraryRenderingPlan): string =>
|
|
85
|
+
`create${pascalCase(rendering.app)}`;
|
|
86
86
|
|
|
87
87
|
const DEFAULT_LIBRARY_DEPENDENCY = '^1.0.0';
|
|
88
88
|
const DEFAULT_ZOD_DEPENDENCY = '^4.3.5';
|
|
@@ -121,12 +121,12 @@ const typeNamesFor = (
|
|
|
121
121
|
};
|
|
122
122
|
|
|
123
123
|
const resolveTypeBindings = (
|
|
124
|
-
|
|
124
|
+
rendering: LibraryRenderingPlan,
|
|
125
125
|
options: CompileOptions
|
|
126
126
|
): TypeBindings => {
|
|
127
127
|
const configured = options.trailTypeExports ?? {};
|
|
128
128
|
const bindings = new Map<string, ExportTypeBinding>();
|
|
129
|
-
for (const entry of
|
|
129
|
+
for (const entry of rendering.exports) {
|
|
130
130
|
const sourceExport = configured[entry.trailId];
|
|
131
131
|
if (!sourceExport) {
|
|
132
132
|
continue;
|
|
@@ -155,10 +155,10 @@ const outputTypeFor = (entry: LibraryExport, bindings: TypeBindings): string =>
|
|
|
155
155
|
bindings.get(entry.trailId)?.output ?? 'unknown';
|
|
156
156
|
|
|
157
157
|
const schemaTypeImport = (
|
|
158
|
-
|
|
158
|
+
rendering: LibraryRenderingPlan,
|
|
159
159
|
bindings: TypeBindings
|
|
160
160
|
): string | undefined => {
|
|
161
|
-
const names =
|
|
161
|
+
const names = rendering.exports.flatMap((entry) => {
|
|
162
162
|
const binding = bindings.get(entry.trailId);
|
|
163
163
|
return binding ? [binding.input, binding.output] : [];
|
|
164
164
|
});
|
|
@@ -196,7 +196,7 @@ const statelessFunction = (
|
|
|
196
196
|
[
|
|
197
197
|
jsDoc([
|
|
198
198
|
exportDescription(entry),
|
|
199
|
-
`
|
|
199
|
+
`Renders trail \`${entry.trailId}\` as a stateless library function.`,
|
|
200
200
|
]),
|
|
201
201
|
`export const ${entry.exportName} = (`,
|
|
202
202
|
` input: ${inputTypeFor(entry, bindings)}`,
|
|
@@ -210,7 +210,7 @@ const factoryMethod = (entry: LibraryExport, bindings: TypeBindings): string =>
|
|
|
210
210
|
jsDoc(
|
|
211
211
|
[
|
|
212
212
|
exportDescription(entry),
|
|
213
|
-
`
|
|
213
|
+
`Renders trail \`${entry.trailId}\` behind the resource client.`,
|
|
214
214
|
],
|
|
215
215
|
' '
|
|
216
216
|
),
|
|
@@ -222,12 +222,12 @@ const factoryMethod = (entry: LibraryExport, bindings: TypeBindings): string =>
|
|
|
222
222
|
].join('\n');
|
|
223
223
|
|
|
224
224
|
const generateIndex = (
|
|
225
|
-
|
|
225
|
+
rendering: LibraryRenderingPlan,
|
|
226
226
|
bindings: TypeBindings
|
|
227
227
|
): string => {
|
|
228
|
-
const stateless = statelessExports(
|
|
229
|
-
const resourceful = resourceExports(
|
|
230
|
-
const typedSchemas = schemaTypeImport(
|
|
228
|
+
const stateless = statelessExports(rendering);
|
|
229
|
+
const resourceful = resourceExports(rendering);
|
|
230
|
+
const typedSchemas = schemaTypeImport(rendering, bindings);
|
|
231
231
|
|
|
232
232
|
const parts: string[] = [
|
|
233
233
|
"import type { SurfaceLibraryOptions } from '@ontrails/library';",
|
|
@@ -243,7 +243,7 @@ const generateIndex = (
|
|
|
243
243
|
if (resourceful.length > 0) {
|
|
244
244
|
parts.push(
|
|
245
245
|
'',
|
|
246
|
-
`export const ${factoryName(
|
|
246
|
+
`export const ${factoryName(rendering)} = async (`,
|
|
247
247
|
' options: SurfaceLibraryOptions = {}',
|
|
248
248
|
') => {',
|
|
249
249
|
' const client = await createClient(options);',
|
|
@@ -271,12 +271,12 @@ const generateClient = (): string =>
|
|
|
271
271
|
].join('\n');
|
|
272
272
|
|
|
273
273
|
const generateSchemas = (
|
|
274
|
-
|
|
274
|
+
rendering: LibraryRenderingPlan,
|
|
275
275
|
options: CompileOptions,
|
|
276
276
|
bindings: TypeBindings
|
|
277
277
|
): string => {
|
|
278
278
|
const appExport = 'app';
|
|
279
|
-
const typedEntries =
|
|
279
|
+
const typedEntries = rendering.exports.filter((entry) =>
|
|
280
280
|
bindings.has(entry.trailId)
|
|
281
281
|
);
|
|
282
282
|
const sourceTypeExports = [
|
|
@@ -300,22 +300,22 @@ const generateSchemas = (
|
|
|
300
300
|
]
|
|
301
301
|
: []),
|
|
302
302
|
'',
|
|
303
|
-
'// Authored Zod schemas, keyed by export name,
|
|
304
|
-
'const
|
|
303
|
+
'// Authored Zod schemas, keyed by export name, rendered from the topo.',
|
|
304
|
+
'const rendering = deriveLibraryApi(app);',
|
|
305
305
|
'const byName = new Map(',
|
|
306
|
-
'
|
|
306
|
+
' rendering.exports.map((entry) => [entry.exportName, entry])',
|
|
307
307
|
');',
|
|
308
308
|
'',
|
|
309
309
|
'const requireExport = (name: string) => {',
|
|
310
310
|
' const entry = byName.get(name);',
|
|
311
311
|
' if (!entry) {',
|
|
312
|
-
" throw new Error('missing
|
|
312
|
+
" throw new Error('missing rendered library export: ' + name);",
|
|
313
313
|
' }',
|
|
314
314
|
' return entry;',
|
|
315
315
|
'};',
|
|
316
316
|
'',
|
|
317
317
|
];
|
|
318
|
-
for (const entry of
|
|
318
|
+
for (const entry of rendering.exports) {
|
|
319
319
|
const binding = bindings.get(entry.trailId);
|
|
320
320
|
if (binding) {
|
|
321
321
|
lines.push(
|
|
@@ -338,7 +338,7 @@ const generateSchemas = (
|
|
|
338
338
|
);
|
|
339
339
|
}
|
|
340
340
|
lines.push('', 'export const schemas = {');
|
|
341
|
-
for (const entry of
|
|
341
|
+
for (const entry of rendering.exports) {
|
|
342
342
|
lines.push(
|
|
343
343
|
` ${entry.exportName}: {`,
|
|
344
344
|
` input: ${entry.exportName}InputSchema,`,
|
|
@@ -396,12 +396,12 @@ const resultFactoryMethod = (
|
|
|
396
396
|
].join('\n');
|
|
397
397
|
|
|
398
398
|
const generateResult = (
|
|
399
|
-
|
|
399
|
+
rendering: LibraryRenderingPlan,
|
|
400
400
|
bindings: TypeBindings
|
|
401
401
|
): string => {
|
|
402
|
-
const stateless = statelessExports(
|
|
403
|
-
const resourceful = resourceExports(
|
|
404
|
-
const typedSchemas = schemaTypeImport(
|
|
402
|
+
const stateless = statelessExports(rendering);
|
|
403
|
+
const resourceful = resourceExports(rendering);
|
|
404
|
+
const typedSchemas = schemaTypeImport(rendering, bindings);
|
|
405
405
|
const parts = [
|
|
406
406
|
'// No-throw API: returns the Result envelope instead of unwrapping.',
|
|
407
407
|
"import { runLibraryResult } from '@ontrails/library';",
|
|
@@ -421,7 +421,7 @@ const generateResult = (
|
|
|
421
421
|
if (resourceful.length > 0) {
|
|
422
422
|
parts.push(
|
|
423
423
|
'',
|
|
424
|
-
`export const ${factoryName(
|
|
424
|
+
`export const ${factoryName(rendering)} = async (`,
|
|
425
425
|
' options: SurfaceLibraryOptions = {}',
|
|
426
426
|
') => {',
|
|
427
427
|
' const client = await createClient(options);',
|
|
@@ -464,7 +464,7 @@ const generateTsconfig = (): string =>
|
|
|
464
464
|
|
|
465
465
|
/**
|
|
466
466
|
* Compile a topo into a generated library package. Returns the emitted file
|
|
467
|
-
* plan; the resolved
|
|
467
|
+
* plan; the resolved rendering is included for inspection and governance.
|
|
468
468
|
*
|
|
469
469
|
* @example
|
|
470
470
|
* const result = compile(app, {
|
|
@@ -479,8 +479,8 @@ export const compile = (
|
|
|
479
479
|
graph: Topo,
|
|
480
480
|
options: CompileOptions
|
|
481
481
|
): CompileResult => {
|
|
482
|
-
const
|
|
483
|
-
const typeBindings = resolveTypeBindings(
|
|
482
|
+
const rendering = deriveLibraryApi(graph, options);
|
|
483
|
+
const typeBindings = resolveTypeBindings(rendering, options);
|
|
484
484
|
const files: CompiledFile[] = [
|
|
485
485
|
{ content: generatePackageJson(options), path: 'package.json' },
|
|
486
486
|
{ content: generateTsconfig(), path: 'tsconfig.json' },
|
|
@@ -489,18 +489,18 @@ export const compile = (
|
|
|
489
489
|
path: 'src/client.ts',
|
|
490
490
|
},
|
|
491
491
|
{
|
|
492
|
-
content: generateIndex(
|
|
492
|
+
content: generateIndex(rendering, typeBindings),
|
|
493
493
|
path: 'src/index.ts',
|
|
494
494
|
},
|
|
495
495
|
{
|
|
496
|
-
content: generateResult(
|
|
496
|
+
content: generateResult(rendering, typeBindings),
|
|
497
497
|
path: 'src/result.ts',
|
|
498
498
|
},
|
|
499
499
|
{
|
|
500
|
-
content: generateSchemas(
|
|
500
|
+
content: generateSchemas(rendering, options, typeBindings),
|
|
501
501
|
path: 'src/schemas.ts',
|
|
502
502
|
},
|
|
503
503
|
{ content: generateTrails(options), path: 'src/trails.ts' },
|
|
504
504
|
];
|
|
505
|
-
return { files, packageName: options.packageName,
|
|
505
|
+
return { files, packageName: options.packageName, rendering };
|
|
506
506
|
};
|
package/src/derive.ts
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `deriveLibraryApi` — the pure
|
|
2
|
+
* `deriveLibraryApi` — the pure rendering from a topo to a `LibraryRenderingPlan`.
|
|
3
3
|
*
|
|
4
4
|
* This is the single semantic authority for the library surface: trail
|
|
5
5
|
* selection, export naming, collision resolution, and the per-export contract
|
|
6
6
|
* data the emitter renders all live here. The in-memory surface and the package
|
|
7
|
-
* emitter both consume the
|
|
7
|
+
* emitter both consume the rendering; neither re-reads the topo nor reinvents
|
|
8
8
|
* selection. Pure — no fs/network/db reads (derive* purity contract).
|
|
9
9
|
*/
|
|
10
10
|
import { filterSurfaceTrails, isDraftId } from '@ontrails/core';
|
|
11
11
|
import type { Layer, Topo, Trail } from '@ontrails/core';
|
|
12
12
|
import type { ZodType } from 'zod';
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
import type {
|
|
14
|
+
import { renderLibraryInput } from './layer-input.js';
|
|
15
|
+
import type { LibraryLayerInputRendering } from './layer-input.js';
|
|
16
16
|
|
|
17
17
|
type AnyTrail = Trail<unknown, unknown, unknown>;
|
|
18
18
|
|
|
19
|
-
/** Where a
|
|
19
|
+
/** Where a rendered export name came from. */
|
|
20
20
|
export type LibraryExportSource = 'derived' | 'trail-hint' | 'package-config';
|
|
21
21
|
|
|
22
|
-
/** A single
|
|
22
|
+
/** A single rendered export: one trail rendered as a library entrypoint. */
|
|
23
23
|
export interface LibraryExport {
|
|
24
24
|
/** Consumer-native export name (camelCased trail id by default). */
|
|
25
25
|
readonly exportName: string;
|
|
26
|
-
/** The trail id this export
|
|
26
|
+
/** The trail id this export renders (the source of truth). */
|
|
27
27
|
readonly trailId: string;
|
|
28
28
|
/** How `exportName` was chosen. v0 derives; hints/config override later. */
|
|
29
29
|
readonly nameSource: LibraryExportSource;
|
|
@@ -39,27 +39,27 @@ export interface LibraryExport {
|
|
|
39
39
|
/**
|
|
40
40
|
* Input schema reference: the emitter's method-signature and `/schemas`
|
|
41
41
|
* source. An in-memory Zod reference here; it serializes to JSON Schema when
|
|
42
|
-
* the
|
|
42
|
+
* the rendering is persisted to the artifact family.
|
|
43
43
|
*/
|
|
44
44
|
readonly input: ZodType;
|
|
45
|
-
/** Layer input routing
|
|
46
|
-
readonly layerInputs: readonly
|
|
45
|
+
/** Layer input routing rendered onto this export's public library input. */
|
|
46
|
+
readonly layerInputs: readonly LibraryLayerInputRendering[];
|
|
47
47
|
/** Output schema reference, when the trail declares one. */
|
|
48
48
|
readonly output: ZodType | undefined;
|
|
49
49
|
/**
|
|
50
50
|
* Resource ids this export depends on. Empty means a stateless function
|
|
51
|
-
* export; non-empty means the emitter
|
|
51
|
+
* export; non-empty means the emitter renders the export behind a
|
|
52
52
|
* `createX()` factory/client, grouped by shared resources. Carrying ids (not
|
|
53
53
|
* just a boolean) keeps the factory-grouping decision in this authority.
|
|
54
54
|
*/
|
|
55
55
|
readonly resources: readonly string[];
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/** Why a trail did not
|
|
58
|
+
/** Why a trail did not render into the library (doctrinally-meaningful only). */
|
|
59
59
|
export type LibraryExclusionReason = 'internal' | 'draft' | 'activation';
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* A trail deliberately excluded from the
|
|
62
|
+
* A trail deliberately excluded from the rendering, recorded for legibility.
|
|
63
63
|
* `reason` is the primary (first-matched) reason in precedence order
|
|
64
64
|
* draft > activation > internal; a trail may technically satisfy more than one.
|
|
65
65
|
*/
|
|
@@ -74,11 +74,11 @@ export interface LibraryCollision {
|
|
|
74
74
|
readonly trailIds: readonly string[];
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/** The resolved library
|
|
78
|
-
export interface
|
|
77
|
+
/** The resolved library rendering — the story of a topo as a TypeScript library. */
|
|
78
|
+
export interface LibraryRenderingPlan {
|
|
79
79
|
/** The topo name. */
|
|
80
80
|
readonly app: string;
|
|
81
|
-
/**
|
|
81
|
+
/** Rendered exports, in stable (trail-id-sorted) order. */
|
|
82
82
|
readonly exports: readonly LibraryExport[];
|
|
83
83
|
/** Trails excluded by visibility, draft state, or activation. */
|
|
84
84
|
readonly excluded: readonly LibraryExclusion[];
|
|
@@ -87,7 +87,7 @@ export interface LibraryProjection {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* Options for narrowing the
|
|
90
|
+
* Options for narrowing the rendering. Selectors reuse the trail-filter
|
|
91
91
|
* grammar (exact ids, `*`, `**`). v0 exposes include/exclude only; intent-based
|
|
92
92
|
* filtering is deferred (the packet promises the filter grammar, not intent
|
|
93
93
|
* narrowing, for the library surface).
|
|
@@ -97,7 +97,7 @@ export interface DeriveLibraryApiOptions {
|
|
|
97
97
|
readonly include?: readonly string[];
|
|
98
98
|
/** Exclude patterns. */
|
|
99
99
|
readonly exclude?: readonly string[];
|
|
100
|
-
/** Surface-scope layers to
|
|
100
|
+
/** Surface-scope layers to render alongside each trail's own input. */
|
|
101
101
|
readonly layers?: readonly Layer[] | undefined;
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -126,21 +126,21 @@ const isInternal = (trail: AnyTrail): boolean =>
|
|
|
126
126
|
trail.visibility === 'internal' || trail.meta?.['internal'] === true;
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
|
-
*
|
|
129
|
+
* Derive a topo into a `LibraryRenderingPlan`. Selection composes
|
|
130
130
|
* `filterSurfaceTrails` (visibility, activation, intent, include/exclude) and
|
|
131
131
|
* adds draft exclusion. Established public, current-version trails become
|
|
132
132
|
* exports; drafts, internal, and activation-driven trails are excluded.
|
|
133
133
|
*
|
|
134
134
|
* @example
|
|
135
|
-
* const
|
|
136
|
-
* for (const entry of
|
|
135
|
+
* const rendering = deriveLibraryApi(app);
|
|
136
|
+
* for (const entry of rendering.exports) {
|
|
137
137
|
* console.log(entry.exportName, '->', entry.trailId);
|
|
138
138
|
* }
|
|
139
139
|
*/
|
|
140
140
|
export const deriveLibraryApi = (
|
|
141
141
|
graph: Topo,
|
|
142
142
|
options: DeriveLibraryApiOptions = {}
|
|
143
|
-
):
|
|
143
|
+
): LibraryRenderingPlan => {
|
|
144
144
|
const all = graph.list();
|
|
145
145
|
|
|
146
146
|
const selected = filterSurfaceTrails(all, {
|
|
@@ -183,13 +183,13 @@ export const deriveLibraryApi = (
|
|
|
183
183
|
continue;
|
|
184
184
|
}
|
|
185
185
|
namesToTrailIds.set(exportName, [trail.id]);
|
|
186
|
-
const
|
|
186
|
+
const inputRendering = renderLibraryInput(graph, trail, options.layers);
|
|
187
187
|
exports.push({
|
|
188
188
|
description: trail.description,
|
|
189
189
|
exportName,
|
|
190
|
-
input:
|
|
190
|
+
input: inputRendering.input,
|
|
191
191
|
intent: trail.intent,
|
|
192
|
-
layerInputs:
|
|
192
|
+
layerInputs: inputRendering.layers,
|
|
193
193
|
nameSource: 'derived',
|
|
194
194
|
output: trail.output,
|
|
195
195
|
resources: trail.resources.map((resource) => resource.id),
|
package/src/index.ts
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The library surface is a peer of CLI, MCP, and HTTP. Its public ladder:
|
|
5
5
|
*
|
|
6
|
-
* - `deriveLibraryApi(graph, options)` — pure
|
|
6
|
+
* - `deriveLibraryApi(graph, options)` — pure rendering → `LibraryRenderingPlan`
|
|
7
7
|
* - `surface(graph, options)` — in-memory callable client
|
|
8
8
|
* - `compile(graph, options)` — TypeScript package emitter
|
|
9
9
|
*
|
|
10
|
-
* Those land across the
|
|
10
|
+
* Those land across the rendering, surface, and emitter lanes (Linear project
|
|
11
11
|
* "Library surface & compiler"). This scaffold establishes the package and the
|
|
12
12
|
* runtime-kernel seam; see `./kernel` and the Library Surface and Compiler ADR.
|
|
13
13
|
*/
|
|
@@ -21,7 +21,7 @@ export type {
|
|
|
21
21
|
LibraryExclusionReason,
|
|
22
22
|
LibraryExport,
|
|
23
23
|
LibraryExportSource,
|
|
24
|
-
|
|
24
|
+
LibraryRenderingPlan,
|
|
25
25
|
} from './derive.js';
|
|
26
26
|
export {
|
|
27
27
|
LibraryAuthError,
|
package/src/kernel.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Surface and Compiler ADR.
|
|
15
15
|
*
|
|
16
16
|
* The kernel grows only as materialization demands: today it wraps execution;
|
|
17
|
-
* error
|
|
17
|
+
* error rendering and the context/compose shim join it as the surface and
|
|
18
18
|
* emitter lanes land. Keep it minimal and dependency-light on purpose.
|
|
19
19
|
*/
|
|
20
20
|
import { run } from '@ontrails/core';
|
package/src/layer-input.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { AttachedTypedLayer, Layer, Topo, Trail } from '@ontrails/core';
|
|
|
2
2
|
import {
|
|
3
3
|
LAYER_FIELD_RESERVED_NAMES,
|
|
4
4
|
collectAttachedTypedLayers,
|
|
5
|
-
|
|
5
|
+
renderLayerFieldName,
|
|
6
6
|
zodToJsonSchema,
|
|
7
7
|
} from '@ontrails/core';
|
|
8
8
|
import { z } from 'zod';
|
|
@@ -10,20 +10,20 @@ import { z } from 'zod';
|
|
|
10
10
|
type AnyTrail = Trail<unknown, unknown, unknown>;
|
|
11
11
|
type MutableLayerShape = Record<string, z.ZodRawShape[string]>;
|
|
12
12
|
|
|
13
|
-
export interface
|
|
13
|
+
export interface LibraryLayerFieldRendering {
|
|
14
14
|
readonly claimedName: string;
|
|
15
15
|
readonly routingTarget: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export interface
|
|
19
|
-
readonly fields: readonly
|
|
18
|
+
export interface LibraryLayerInputRendering {
|
|
19
|
+
readonly fields: readonly LibraryLayerFieldRendering[];
|
|
20
20
|
readonly input: z.ZodObject<z.ZodRawShape>;
|
|
21
21
|
readonly layerName: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export interface
|
|
24
|
+
export interface LibraryInputRendering {
|
|
25
25
|
readonly input: z.ZodType;
|
|
26
|
-
readonly layers: readonly
|
|
26
|
+
readonly layers: readonly LibraryLayerInputRendering[];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const isJsonObjectSchema = (
|
|
@@ -59,19 +59,19 @@ const objectPropertiesFor = (schema: z.ZodType): readonly string[] => {
|
|
|
59
59
|
const isJsonObjectInput = (schema: z.ZodType): boolean =>
|
|
60
60
|
isJsonObjectSchema(zodToJsonSchema(schema));
|
|
61
61
|
|
|
62
|
-
const
|
|
62
|
+
const renderLayerInputFields = (
|
|
63
63
|
attached: AttachedTypedLayer,
|
|
64
64
|
claimedNames: Set<string>,
|
|
65
65
|
layerShape: MutableLayerShape
|
|
66
|
-
):
|
|
66
|
+
): LibraryLayerInputRendering => {
|
|
67
67
|
const { layer } = attached;
|
|
68
68
|
if (layer.input === undefined) {
|
|
69
69
|
return { fields: [], input: z.object({}), layerName: layer.name };
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
const fields:
|
|
72
|
+
const fields: LibraryLayerFieldRendering[] = [];
|
|
73
73
|
for (const [fieldName, fieldSchema] of Object.entries(layer.input.shape)) {
|
|
74
|
-
const
|
|
74
|
+
const rendering = renderLayerFieldName(
|
|
75
75
|
layer.name,
|
|
76
76
|
fieldName,
|
|
77
77
|
fieldName,
|
|
@@ -80,24 +80,24 @@ const projectLayerInputFields = (
|
|
|
80
80
|
LAYER_FIELD_RESERVED_NAMES
|
|
81
81
|
);
|
|
82
82
|
fields.push({
|
|
83
|
-
claimedName:
|
|
84
|
-
routingTarget:
|
|
83
|
+
claimedName: rendering.claimedName,
|
|
84
|
+
routingTarget: rendering.routingTarget,
|
|
85
85
|
});
|
|
86
|
-
layerShape[
|
|
86
|
+
layerShape[rendering.claimedName] = fieldSchema;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
return { fields, input: layer.input, layerName: layer.name };
|
|
90
90
|
};
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
93
|
+
* Render a trail's public library input: authored trail input plus any typed
|
|
94
94
|
* layer input fields attached at topo, surface, or trail scope.
|
|
95
95
|
*/
|
|
96
|
-
export const
|
|
96
|
+
export const renderLibraryInput = (
|
|
97
97
|
graph: Topo,
|
|
98
98
|
trail: AnyTrail,
|
|
99
99
|
surfaceLayers?: readonly Layer[]
|
|
100
|
-
):
|
|
100
|
+
): LibraryInputRendering => {
|
|
101
101
|
const attachedLayers = collectAttachedTypedLayers(
|
|
102
102
|
graph,
|
|
103
103
|
trail,
|
|
@@ -111,20 +111,20 @@ export const projectLibraryInput = (
|
|
|
111
111
|
const claimedNames = new Set(trailProperties);
|
|
112
112
|
if (trailProperties.length === 0 && !isJsonObjectInput(trail.input)) {
|
|
113
113
|
throw new Error(
|
|
114
|
-
`Library layer input
|
|
114
|
+
`Library layer input rendering requires object input for trail "${trail.id}".`
|
|
115
115
|
);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
const layerShape: MutableLayerShape = {};
|
|
119
|
-
const layers:
|
|
119
|
+
const layers: LibraryLayerInputRendering[] = [];
|
|
120
120
|
for (const attached of attachedLayers) {
|
|
121
|
-
const
|
|
121
|
+
const rendering = renderLayerInputFields(
|
|
122
122
|
attached,
|
|
123
123
|
claimedNames,
|
|
124
124
|
layerShape
|
|
125
125
|
);
|
|
126
|
-
if (
|
|
127
|
-
layers.push(
|
|
126
|
+
if (rendering.fields.length > 0) {
|
|
127
|
+
layers.push(rendering);
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
@@ -140,25 +140,25 @@ export const projectLibraryInput = (
|
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
142
|
* Split a library method input back into trail input plus per-layer runtime
|
|
143
|
-
* input slots using the
|
|
143
|
+
* input slots using the rendering routing table.
|
|
144
144
|
*/
|
|
145
145
|
export const partitionLibraryInput = (
|
|
146
146
|
input: unknown,
|
|
147
|
-
|
|
147
|
+
renderings: readonly LibraryLayerInputRendering[]
|
|
148
148
|
): {
|
|
149
149
|
readonly layerInputs: Record<string, unknown>;
|
|
150
150
|
readonly trailInput: unknown;
|
|
151
151
|
} => {
|
|
152
|
-
if (
|
|
152
|
+
if (renderings.length === 0 || !isObjectRecord(input)) {
|
|
153
153
|
return { layerInputs: {}, trailInput: input };
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
const claimedKeys = new Set<string>();
|
|
157
157
|
const layerInputs: Record<string, unknown> = {};
|
|
158
|
-
for (const
|
|
158
|
+
for (const rendering of renderings) {
|
|
159
159
|
const layerInput: Record<string, unknown> = {};
|
|
160
160
|
let received = false;
|
|
161
|
-
for (const field of
|
|
161
|
+
for (const field of rendering.fields) {
|
|
162
162
|
claimedKeys.add(field.claimedName);
|
|
163
163
|
const value = input[field.claimedName];
|
|
164
164
|
if (value === undefined) {
|
|
@@ -168,13 +168,13 @@ export const partitionLibraryInput = (
|
|
|
168
168
|
received = true;
|
|
169
169
|
}
|
|
170
170
|
if (received) {
|
|
171
|
-
layerInputs[
|
|
171
|
+
layerInputs[rendering.layerName] = layerInput;
|
|
172
172
|
continue;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
const emptyInput =
|
|
175
|
+
const emptyInput = rendering.input.safeParse({});
|
|
176
176
|
if (emptyInput.success) {
|
|
177
|
-
layerInputs[
|
|
177
|
+
layerInputs[rendering.layerName] = emptyInput.data;
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
|
package/src/surface.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { deriveLibraryApi } from './derive.js';
|
|
|
17
17
|
import type {
|
|
18
18
|
DeriveLibraryApiOptions,
|
|
19
19
|
LibraryExport,
|
|
20
|
-
|
|
20
|
+
LibraryRenderingPlan,
|
|
21
21
|
} from './derive.js';
|
|
22
22
|
import { toLibraryError } from './errors.js';
|
|
23
23
|
import type { LibraryError } from './errors.js';
|
|
@@ -26,7 +26,7 @@ import { kernelRun } from './kernel.js';
|
|
|
26
26
|
import type { KernelRunOptions, Topo } from './kernel.js';
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Options for the in-memory library surface:
|
|
29
|
+
* Options for the in-memory library surface: rendering selectors plus the
|
|
30
30
|
* runtime context the client owns (e.g. a permit for permitted trails).
|
|
31
31
|
*/
|
|
32
32
|
export interface SurfaceLibraryOptions
|
|
@@ -42,14 +42,14 @@ export type LibraryResultMethod = (
|
|
|
42
42
|
input: unknown
|
|
43
43
|
) => Promise<Result<unknown, LibraryError>>;
|
|
44
44
|
|
|
45
|
-
/** The held in-memory client: one method per
|
|
45
|
+
/** The held in-memory client: one method per rendered export, plus the rendering. */
|
|
46
46
|
export interface LibraryClient {
|
|
47
47
|
/** Invoke an exported trail by its consumer-native name. */
|
|
48
48
|
readonly call: Readonly<Record<string, LibraryMethod>>;
|
|
49
49
|
/** Invoke an exported trail by name without unwrapping the Result boundary. */
|
|
50
50
|
readonly result: Readonly<Record<string, LibraryResultMethod>>;
|
|
51
|
-
/** The resolved
|
|
52
|
-
readonly
|
|
51
|
+
/** The resolved rendering this client was built from (introspection). */
|
|
52
|
+
readonly rendering: LibraryRenderingPlan;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
const prepareLibraryInput = (
|
|
@@ -96,11 +96,9 @@ export const runLibraryResult = async (
|
|
|
96
96
|
input: unknown,
|
|
97
97
|
options: SurfaceLibraryOptions = {}
|
|
98
98
|
): Promise<Result<unknown, LibraryError>> => {
|
|
99
|
-
const
|
|
99
|
+
const rendering = deriveLibraryApi(graph, options);
|
|
100
100
|
const runOptions = runtimeOptionsFor(graph, options);
|
|
101
|
-
const entry =
|
|
102
|
-
(candidate) => candidate.trailId === id
|
|
103
|
-
);
|
|
101
|
+
const entry = rendering.exports.find((candidate) => candidate.trailId === id);
|
|
104
102
|
const prepared = entry
|
|
105
103
|
? prepareLibraryInput(entry, input)
|
|
106
104
|
: { layerInputs: {}, trailInput: input };
|
|
@@ -117,7 +115,7 @@ export const runLibraryResult = async (
|
|
|
117
115
|
};
|
|
118
116
|
|
|
119
117
|
/**
|
|
120
|
-
* Materialize a topo as an in-memory library client. Each
|
|
118
|
+
* Materialize a topo as an in-memory library client. Each rendered export
|
|
121
119
|
* becomes a method that executes its trail through the shared pipeline and
|
|
122
120
|
* unwraps the Result — returning the value or throwing the error. The same
|
|
123
121
|
* export names are available under `result` for callers that want the raw
|
|
@@ -132,12 +130,12 @@ export const surface = async (
|
|
|
132
130
|
options: SurfaceLibraryOptions = {}
|
|
133
131
|
// oxlint-disable-next-line require-await -- async to match peer surfaces and allow future resource init
|
|
134
132
|
): Promise<LibraryClient> => {
|
|
135
|
-
const
|
|
133
|
+
const rendering = deriveLibraryApi(graph, options);
|
|
136
134
|
const runOptions = runtimeOptionsFor(graph, options);
|
|
137
135
|
const call: Record<string, LibraryMethod> = {};
|
|
138
136
|
const result: Record<string, LibraryResultMethod> = {};
|
|
139
137
|
|
|
140
|
-
for (const entry of
|
|
138
|
+
for (const entry of rendering.exports) {
|
|
141
139
|
const runExport: LibraryResultMethod = async (input: unknown) => {
|
|
142
140
|
const prepared = prepareLibraryInput(entry, input);
|
|
143
141
|
if (prepared instanceof ValidationError) {
|
|
@@ -169,7 +167,7 @@ export const surface = async (
|
|
|
169
167
|
|
|
170
168
|
return {
|
|
171
169
|
call: Object.freeze(call),
|
|
172
|
-
|
|
170
|
+
rendering,
|
|
173
171
|
result: Object.freeze(result),
|
|
174
172
|
};
|
|
175
173
|
};
|