@ontrails/adapter-kit 1.0.0-beta.32 → 1.0.0-beta.39

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/adapter-kit",
3
- "version": "1.0.0-beta.32",
3
+ "version": "1.0.0-beta.39",
4
4
  "description": "Internal adapter authoring kit for Trails metadata, scaffolding, and checks.",
5
5
  "files": [
6
6
  "src/**/*.ts",
@@ -21,6 +21,9 @@
21
21
  "clean": "rm -rf dist *.tsbuildinfo"
22
22
  },
23
23
  "dependencies": {
24
- "@ontrails/core": "^1.0.0-beta.32"
24
+ "@ontrails/core": "^1.0.0-beta.39"
25
+ },
26
+ "peerDependencies": {
27
+ "zod": "^4.3.5"
25
28
  }
26
29
  }
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { checkAdapters } from './check.js';
2
+ export { isOverlay, resolveTrailsOverlays } from './overlay.js';
3
+ export type { Overlay } from './overlay.js';
2
4
  export type {
3
5
  AdapterCheckDiagnostic,
4
6
  AdapterCheckDiagnosticCode,
package/src/overlay.ts ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * The adapter overlay contract.
3
+ *
4
+ * Adapters export an overlay object describing one namespaced fact
5
+ * overlay; the app module re-exports it (conventionally as
6
+ * `trailsOverlays`), and the compile path validates `derive(topo)`
7
+ * output against the overlay's schema before embedding the facts as
8
+ * `overlays.<namespace>` in `trails.lock`. The lock schema and graph type
9
+ * never change — unknown namespaces are preserved byte-for-byte by older
10
+ * toolchains (tolerant reader).
11
+ */
12
+
13
+ import { ValidationError } from '@ontrails/core';
14
+ import type { OverlayProvenance, Topo } from '@ontrails/core';
15
+ import type { z } from 'zod';
16
+
17
+ /**
18
+ * One adapter-owned namespaced fact overlay for `trails.lock`.
19
+ *
20
+ * An overlay is authored by an adapter package and opted into by the
21
+ * app: the app module exports `trailsOverlays` next to its topo export,
22
+ * and `trails compile` runs each overlay's {@link derive} over the
23
+ * compiled topo, validates the result against {@link schema}, and embeds it
24
+ * as `overlays.<namespace>` in the committed lock.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * import type { Overlay } from '@ontrails/adapter-kit';
29
+ * import { z } from 'zod';
30
+ *
31
+ * const factsSchema = z.object({ regions: z.array(z.string()) }).strict();
32
+ *
33
+ * export const overlay = {
34
+ * namespace: 'acme',
35
+ * schema: factsSchema,
36
+ * derive: (topo) => ({
37
+ * regions: topo
38
+ * .listResources()
39
+ * .map((definition) => definition.id)
40
+ * .toSorted(),
41
+ * }),
42
+ * } satisfies Overlay;
43
+ *
44
+ * // In the app module, next to the topo export:
45
+ * // export const trailsOverlays = [overlay];
46
+ * // `trails compile` then embeds the facts as `overlays.acme`.
47
+ * ```
48
+ */
49
+ export interface Overlay {
50
+ /**
51
+ * The lock overlay this overlay owns.
52
+ *
53
+ * Dotted kebab-case: `/^[a-z][a-z0-9-]*(\.[a-z0-9-]+)*$/`. Each
54
+ * overlay owns exactly one namespace, and the facts land at
55
+ * `overlays.<namespace>` in `trails.lock`. Unknown namespaces are
56
+ * preserved byte-for-byte by older toolchains, so new overlays never
57
+ * break existing lock readers.
58
+ */
59
+ readonly namespace: string;
60
+ /**
61
+ * Who authored this overlay. Absent means adapter-derived.
62
+ *
63
+ * Surfaces obey app-authored overlays only: the well-known `surfaces`
64
+ * namespace requires `provenance: 'app-authored'` (authored via
65
+ * `surfaceOverlay()` in the app module), and the compile path rejects
66
+ * adapter-derived envelopes that claim it. Adapter overlays contribute
67
+ * facts, never bindings, and can leave this field absent.
68
+ */
69
+ readonly provenance?: OverlayProvenance | undefined;
70
+ /**
71
+ * The elevated fact schema.
72
+ *
73
+ * The compile path enforces this schema against every {@link derive}
74
+ * output before embedding the facts, so a drifting derive function fails
75
+ * `trails compile` instead of committing invalid facts.
76
+ */
77
+ readonly schema: z.ZodType;
78
+ /**
79
+ * Project the topo into this overlay's facts.
80
+ *
81
+ * Must be deterministic — the same topo always yields the same facts
82
+ * (sort any collections) — and must return JSON-plain data, because the
83
+ * result is embedded verbatim in the committed lock.
84
+ */
85
+ readonly derive: (topo: Topo) => unknown;
86
+ }
87
+
88
+ const isFunction = (value: unknown): value is (...args: unknown[]) => unknown =>
89
+ typeof value === 'function';
90
+
91
+ /**
92
+ * Structurally recognize an {@link Overlay}.
93
+ *
94
+ * Used by the compile-side collector when scanning an app module's
95
+ * `trailsOverlays` export — adapters never import this at runtime. The
96
+ * check is hand-rolled (string `namespace`, schema object exposing a
97
+ * `safeParse` function, function `derive`) so recognizing overlays
98
+ * needs no zod runtime dependency.
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * import { isOverlay } from '@ontrails/adapter-kit';
103
+ *
104
+ * const recognized = candidates.filter(isOverlay);
105
+ * ```
106
+ */
107
+ export const isOverlay = (value: unknown): value is Overlay => {
108
+ if (typeof value !== 'object' || value === null) {
109
+ return false;
110
+ }
111
+ const candidate = value as Partial<Record<keyof Overlay, unknown>>;
112
+ return (
113
+ typeof candidate.namespace === 'string' &&
114
+ typeof candidate.schema === 'object' &&
115
+ candidate.schema !== null &&
116
+ isFunction((candidate.schema as { safeParse?: unknown }).safeParse) &&
117
+ isFunction(candidate.derive)
118
+ );
119
+ };
120
+
121
+ /**
122
+ * Read an app module's `trailsOverlays` export as validated overlays.
123
+ *
124
+ * This is the one shared collection channel for app-module overlays: the
125
+ * compile path's fresh app lease and Warden's fresh topo loading both read
126
+ * the export through this function, so every fresh derivation carries the
127
+ * same overlays the committed lock embeds. An absent export returns
128
+ * `undefined`; a present export that is not an array of {@link Overlay}
129
+ * values throws a fix-forward `ValidationError` naming `sourceLabel`.
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * import { resolveTrailsOverlays } from '@ontrails/adapter-kit';
134
+ *
135
+ * const mod = await import(appModulePath);
136
+ * const overlays = resolveTrailsOverlays(
137
+ * mod as Record<string, unknown>,
138
+ * appModulePath
139
+ * );
140
+ * // => readonly Overlay[] | undefined
141
+ * ```
142
+ */
143
+ export const resolveTrailsOverlays = (
144
+ moduleExports: Record<string, unknown>,
145
+ sourceLabel: string
146
+ ): readonly Overlay[] | undefined => {
147
+ const value = moduleExports['trailsOverlays'];
148
+ if (value === undefined) {
149
+ return undefined;
150
+ }
151
+ if (!Array.isArray(value) || !value.every(isOverlay)) {
152
+ throw new ValidationError(
153
+ `trailsOverlays export in "${sourceLabel}" must be an array of overlays ({ namespace, schema, derive }). Fix the app module export and rerun \`trails compile\`.`
154
+ );
155
+ }
156
+
157
+ return value;
158
+ };