@ontrails/core 1.0.0-beta.14 → 1.0.0-beta.15
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/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +11 -0
- package/README.md +17 -13
- package/dist/branded.d.ts +1 -1
- package/dist/branded.d.ts.map +1 -1
- package/dist/branded.js +1 -1
- package/dist/branded.js.map +1 -1
- package/dist/context.d.ts +13 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +19 -3
- package/dist/context.js.map +1 -1
- package/dist/contour.d.ts +81 -0
- package/dist/contour.d.ts.map +1 -0
- package/dist/contour.js +176 -0
- package/dist/contour.js.map +1 -0
- package/dist/cross-schema.d.ts +18 -0
- package/dist/cross-schema.d.ts.map +1 -0
- package/dist/cross-schema.js +29 -0
- package/dist/cross-schema.js.map +1 -0
- package/dist/draft.d.ts +5 -5
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +28 -27
- package/dist/draft.js.map +1 -1
- package/dist/errors.d.ts +62 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +44 -1
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +26 -9
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +441 -20
- package/dist/execute.js.map +1 -1
- package/dist/fire.d.ts +43 -0
- package/dist/fire.d.ts.map +1 -0
- package/dist/fire.js +185 -0
- package/dist/fire.js.map +1 -0
- package/dist/index.d.ts +31 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -13
- package/dist/index.js.map +1 -1
- package/dist/internal/cross-batch.d.ts +44 -0
- package/dist/internal/cross-batch.d.ts.map +1 -0
- package/dist/internal/cross-batch.js +58 -0
- package/dist/internal/cross-batch.js.map +1 -0
- package/dist/internal/fork-ctx.d.ts +41 -0
- package/dist/internal/fork-ctx.d.ts.map +1 -0
- package/dist/internal/fork-ctx.js +47 -0
- package/dist/internal/fork-ctx.js.map +1 -0
- package/dist/internal/signal-ref.d.ts +15 -0
- package/dist/internal/signal-ref.d.ts.map +1 -0
- package/dist/internal/signal-ref.js +41 -0
- package/dist/internal/signal-ref.js.map +1 -0
- package/dist/internal/topo-saves.d.ts +8 -2
- package/dist/internal/topo-saves.d.ts.map +1 -1
- package/dist/internal/topo-saves.js +162 -23
- package/dist/internal/topo-saves.js.map +1 -1
- package/dist/internal/topo-snapshots.d.ts +57 -0
- package/dist/internal/topo-snapshots.d.ts.map +1 -0
- package/dist/internal/topo-snapshots.js +372 -0
- package/dist/internal/topo-snapshots.js.map +1 -0
- package/dist/internal/topo-store-read.d.ts +23 -20
- package/dist/internal/topo-store-read.d.ts.map +1 -1
- package/dist/internal/topo-store-read.js +82 -83
- package/dist/internal/topo-store-read.js.map +1 -1
- package/dist/internal/topo-store.d.ts +19 -5
- package/dist/internal/topo-store.d.ts.map +1 -1
- package/dist/internal/topo-store.js +174 -124
- package/dist/internal/topo-store.js.map +1 -1
- package/dist/internal/tracing.d.ts +98 -0
- package/dist/internal/tracing.d.ts.map +1 -0
- package/dist/internal/tracing.js +102 -0
- package/dist/internal/tracing.js.map +1 -0
- package/dist/internal/trails-db.d.ts +2 -2
- package/dist/internal/trails-db.d.ts.map +1 -1
- package/dist/internal/trails-db.js +8 -8
- package/dist/internal/trails-db.js.map +1 -1
- package/dist/internal/zod-wrappers.d.ts +40 -0
- package/dist/internal/zod-wrappers.d.ts.map +1 -0
- package/dist/internal/zod-wrappers.js +59 -0
- package/dist/internal/zod-wrappers.js.map +1 -0
- package/dist/layer.d.ts +22 -5
- package/dist/layer.d.ts.map +1 -1
- package/dist/layer.js +20 -3
- package/dist/layer.js.map +1 -1
- package/dist/path-security.d.ts +1 -1
- package/dist/path-security.d.ts.map +1 -1
- package/dist/path-security.js +1 -1
- package/dist/path-security.js.map +1 -1
- package/dist/resilience.d.ts +1 -1
- package/dist/resilience.d.ts.map +1 -1
- package/dist/resilience.js +3 -3
- package/dist/resilience.js.map +1 -1
- package/dist/resource-config.d.ts +22 -0
- package/dist/resource-config.d.ts.map +1 -0
- package/dist/resource-config.js +210 -0
- package/dist/resource-config.js.map +1 -0
- package/dist/resource.d.ts +74 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +61 -0
- package/dist/resource.js.map +1 -0
- package/dist/run.d.ts +4 -3
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +5 -4
- package/dist/run.js.map +1 -1
- package/dist/serialization.d.ts.map +1 -1
- package/dist/serialization.js +2 -1
- package/dist/serialization.js.map +1 -1
- package/dist/store/accessor-protocol.d.ts +51 -0
- package/dist/store/accessor-protocol.d.ts.map +1 -0
- package/dist/store/accessor-protocol.js +2 -0
- package/dist/store/accessor-protocol.js.map +1 -0
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +2 -0
- package/dist/store/index.js.map +1 -0
- package/dist/surface-filter.d.ts +13 -0
- package/dist/surface-filter.d.ts.map +1 -0
- package/dist/surface-filter.js +76 -0
- package/dist/surface-filter.js.map +1 -0
- package/dist/topo-store.d.ts +36 -22
- package/dist/topo-store.d.ts.map +1 -1
- package/dist/topo-store.js +215 -71
- package/dist/topo-store.js.map +1 -1
- package/dist/topo.d.ts +23 -9
- package/dist/topo.d.ts.map +1 -1
- package/dist/topo.js +175 -46
- package/dist/topo.js.map +1 -1
- package/dist/trail.d.ts +86 -19
- package/dist/trail.d.ts.map +1 -1
- package/dist/trail.js +59 -4
- package/dist/trail.js.map +1 -1
- package/dist/trails/derive-trail.d.ts +66 -0
- package/dist/trails/derive-trail.d.ts.map +1 -0
- package/dist/trails/derive-trail.js +378 -0
- package/dist/trails/derive-trail.js.map +1 -0
- package/dist/trails/index.d.ts +5 -0
- package/dist/trails/index.d.ts.map +1 -0
- package/dist/trails/index.js +3 -0
- package/dist/trails/index.js.map +1 -0
- package/dist/trails/ingest.d.ts +24 -0
- package/dist/trails/ingest.d.ts.map +1 -0
- package/dist/trails/ingest.js +63 -0
- package/dist/trails/ingest.js.map +1 -0
- package/dist/transport-error-map.d.ts +106 -0
- package/dist/transport-error-map.d.ts.map +1 -0
- package/dist/transport-error-map.js +24 -0
- package/dist/transport-error-map.js.map +1 -0
- package/dist/type-checks.test-d.d.ts +71 -0
- package/dist/type-checks.test-d.d.ts.map +1 -0
- package/dist/type-checks.test-d.js +12 -0
- package/dist/type-checks.test-d.js.map +1 -0
- package/dist/type-utils.d.ts +11 -2
- package/dist/type-utils.d.ts.map +1 -1
- package/dist/type-utils.js.map +1 -1
- package/dist/types.d.ts +113 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/validate-established-topo.js +2 -2
- package/dist/validate-established-topo.js.map +1 -1
- package/dist/validate-topo.d.ts.map +1 -1
- package/dist/validate-topo.js +24 -7
- package/dist/validate-topo.js.map +1 -1
- package/dist/workspace.d.ts +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +1 -1
- package/dist/workspace.js.map +1 -1
- package/package.json +8 -2
- package/src/__tests__/branded.test.ts +5 -5
- package/src/__tests__/context.test.ts +3 -3
- package/src/__tests__/contour.test.ts +263 -0
- package/src/__tests__/derive-trail.test.ts +668 -0
- package/src/__tests__/errors.test.ts +118 -0
- package/src/__tests__/execute.test.ts +1179 -141
- package/src/__tests__/fire.test.ts +1056 -0
- package/src/__tests__/fork-ctx.test.ts +83 -0
- package/src/__tests__/ingest.test.ts +191 -0
- package/src/__tests__/{gate.test.ts → layer.test.ts} +21 -21
- package/src/__tests__/path-security.test.ts +7 -7
- package/src/__tests__/resilience.test.ts +8 -8
- package/src/__tests__/resource.test.ts +204 -0
- package/src/__tests__/run.test.ts +13 -13
- package/src/__tests__/service-config.test.ts +28 -32
- package/src/__tests__/surface-filter.test.ts +196 -0
- package/src/__tests__/topo-store-read.test.ts +41 -36
- package/src/__tests__/topo-store.test.ts +493 -93
- package/src/__tests__/topo.test.ts +350 -54
- package/src/__tests__/trail.test.ts +189 -24
- package/src/__tests__/trails-db.test.ts +32 -25
- package/src/__tests__/transport-error-map.test.ts +82 -0
- package/src/__tests__/type-utils.test.ts +29 -1
- package/src/__tests__/validate-topo.test.ts +147 -25
- package/src/__tests__/workspace.test.ts +6 -6
- package/src/__tests__/zod-wrappers.test.ts +62 -0
- package/src/branded.ts +1 -1
- package/src/context.ts +24 -4
- package/src/contour.ts +344 -0
- package/src/cross-schema.ts +36 -0
- package/src/draft.ts +56 -52
- package/src/errors.ts +62 -18
- package/src/execute.ts +840 -29
- package/src/fire.ts +295 -0
- package/src/index.ts +116 -29
- package/src/internal/cross-batch.ts +77 -0
- package/src/internal/fork-ctx.ts +69 -0
- package/src/internal/signal-ref.ts +87 -0
- package/src/internal/topo-snapshots.ts +517 -0
- package/src/internal/topo-store-read.ts +140 -132
- package/src/internal/topo-store.ts +291 -186
- package/src/internal/tracing.ts +187 -0
- package/src/internal/trails-db.ts +9 -11
- package/src/internal/zod-wrappers.ts +78 -0
- package/src/layer.ts +50 -0
- package/src/path-security.ts +1 -1
- package/src/resilience.ts +3 -3
- package/src/{provision-config.ts → resource-config.ts} +109 -123
- package/src/resource.ts +155 -0
- package/src/run.ts +5 -4
- package/src/serialization.ts +2 -0
- package/src/store/accessor-protocol.ts +56 -0
- package/src/store/index.ts +4 -0
- package/src/surface-filter.ts +182 -0
- package/src/topo-store.ts +313 -104
- package/src/topo.ts +328 -57
- package/src/trail.ts +199 -31
- package/src/trails/derive-trail.ts +842 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +146 -0
- package/src/transport-error-map.ts +56 -0
- package/src/type-checks.test-d.ts +106 -0
- package/src/type-utils.ts +17 -2
- package/src/types.ts +148 -11
- package/src/validate-established-topo.ts +2 -2
- package/src/validate-topo.ts +32 -8
- package/src/workspace.ts +1 -1
- package/tsconfig.tests.json +10 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/adapters.d.ts +0 -39
- package/dist/adapters.d.ts.map +0 -1
- package/dist/adapters.js +0 -2
- package/dist/adapters.js.map +0 -1
- package/dist/dispatch.d.ts +0 -27
- package/dist/dispatch.d.ts.map +0 -1
- package/dist/dispatch.js +0 -34
- package/dist/dispatch.js.map +0 -1
- package/dist/event.d.ts +0 -8
- package/dist/event.d.ts.map +0 -1
- package/dist/event.js +0 -7
- package/dist/event.js.map +0 -1
- package/dist/gate.d.ts +0 -17
- package/dist/gate.d.ts.map +0 -1
- package/dist/gate.js +0 -21
- package/dist/gate.js.map +0 -1
- package/dist/health.d.ts +0 -18
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -5
- package/dist/health.js.map +0 -1
- package/dist/job.d.ts +0 -24
- package/dist/job.d.ts.map +0 -1
- package/dist/job.js +0 -17
- package/dist/job.js.map +0 -1
- package/dist/provision-config.d.ts +0 -22
- package/dist/provision-config.d.ts.map +0 -1
- package/dist/provision-config.js +0 -210
- package/dist/provision-config.js.map +0 -1
- package/dist/provision.d.ts +0 -71
- package/dist/provision.d.ts.map +0 -1
- package/dist/provision.js +0 -56
- package/dist/provision.js.map +0 -1
- package/dist/service-config.d.ts +0 -22
- package/dist/service-config.d.ts.map +0 -1
- package/dist/service-config.js +0 -210
- package/dist/service-config.js.map +0 -1
- package/dist/service.d.ts +0 -71
- package/dist/service.d.ts.map +0 -1
- package/dist/service.js +0 -56
- package/dist/service.js.map +0 -1
- package/src/__tests__/service.test.ts +0 -197
- package/src/event.ts +0 -15
- package/src/gate.ts +0 -44
- package/src/internal/topo-saves.ts +0 -429
- package/src/provision.ts +0 -148
package/src/contour.ts
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import type { Branded } from './branded.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Runtime options for a contour declaration.
|
|
7
|
+
*/
|
|
8
|
+
export interface ContourOptions<
|
|
9
|
+
TShape extends z.ZodRawShape,
|
|
10
|
+
TIdentity extends keyof TShape & string,
|
|
11
|
+
> {
|
|
12
|
+
/** Field name that acts as the contour's primary identity. */
|
|
13
|
+
readonly identity: TIdentity;
|
|
14
|
+
/** Example instances validated against the contour schema at declaration time. */
|
|
15
|
+
readonly examples?: readonly z.output<z.ZodObject<TShape>>[] | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Type-level brand name applied to a contour's identity schema. */
|
|
19
|
+
export type ContourIdBrand<TName extends string> = `${Capitalize<TName>}Id`;
|
|
20
|
+
|
|
21
|
+
type BrandedSchema<
|
|
22
|
+
TSchema extends z.core.$ZodType,
|
|
23
|
+
TBrand extends string,
|
|
24
|
+
> = TSchema & z.ZodType<Branded<z.output<TSchema>, TBrand>>;
|
|
25
|
+
|
|
26
|
+
type BrandableSchema<TSchema extends z.core.$ZodType> = TSchema & {
|
|
27
|
+
brand<TBrand extends string>(): BrandedSchema<TSchema, TBrand>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Output value of a branded contour identity schema. */
|
|
31
|
+
export type ContourIdValue<
|
|
32
|
+
TSchema extends z.core.$ZodType,
|
|
33
|
+
TName extends string,
|
|
34
|
+
> = Branded<z.output<TSchema>, ContourIdBrand<TName>>;
|
|
35
|
+
|
|
36
|
+
/** Runtime metadata attached to schemas returned from `contour.id()`. */
|
|
37
|
+
export interface ContourIdMetadata<
|
|
38
|
+
TName extends string = string,
|
|
39
|
+
TIdentity extends string = string,
|
|
40
|
+
> {
|
|
41
|
+
readonly contour: TName;
|
|
42
|
+
readonly identity: TIdentity;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A structural contour reference declared by another contour field schema. */
|
|
46
|
+
export interface ContourReference<
|
|
47
|
+
TName extends string = string,
|
|
48
|
+
TIdentity extends string = string,
|
|
49
|
+
> extends ContourIdMetadata<TName, TIdentity> {
|
|
50
|
+
readonly field: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Symbol used to tag branded contour reference schemas at runtime. */
|
|
54
|
+
export const CONTOUR_ID_METADATA = Symbol.for('@ontrails/core/contour-id');
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Module-level WeakMap storing contour identity metadata keyed by schema object.
|
|
58
|
+
*
|
|
59
|
+
* First-write-wins: when multiple contours share the same underlying schema
|
|
60
|
+
* (e.g. `contour('admin', { id: user.shape.id }, ...)`), the first contour to
|
|
61
|
+
* brand the schema claims it. Subsequent calls skip the write to prevent
|
|
62
|
+
* silent metadata corruption.
|
|
63
|
+
*/
|
|
64
|
+
const contourIdMetadata = new WeakMap<object, ContourIdMetadata>();
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A contour identity schema branded for one contour and tagged with runtime
|
|
68
|
+
* metadata so the topo layer can recognize declared references later on.
|
|
69
|
+
*/
|
|
70
|
+
export type ContourIdSchema<
|
|
71
|
+
TSchema extends z.core.$ZodType = z.core.$ZodType,
|
|
72
|
+
TName extends string = string,
|
|
73
|
+
TIdentity extends string = string,
|
|
74
|
+
> = BrandedSchema<TSchema, ContourIdBrand<TName>> & {
|
|
75
|
+
/** @deprecated Use `getContourIdMetadata()` — metadata lives in a WeakMap, not on the schema. */
|
|
76
|
+
readonly [CONTOUR_ID_METADATA]?: ContourIdMetadata<TName, TIdentity>;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A first-class domain object with schema, identity metadata, and examples.
|
|
81
|
+
*
|
|
82
|
+
* A contour behaves like the `ZodObject` it wraps, so standard Zod composition
|
|
83
|
+
* helpers such as `.pick()`, `.extend()`, and `.array()` continue to work.
|
|
84
|
+
*/
|
|
85
|
+
export type Contour<
|
|
86
|
+
TName extends string = string,
|
|
87
|
+
TShape extends z.ZodRawShape = z.ZodRawShape,
|
|
88
|
+
TIdentity extends keyof TShape & string = keyof TShape & string,
|
|
89
|
+
> = z.ZodObject<TShape> & {
|
|
90
|
+
readonly kind: 'contour';
|
|
91
|
+
readonly name: TName;
|
|
92
|
+
readonly identity: TIdentity;
|
|
93
|
+
readonly identitySchema: TShape[TIdentity];
|
|
94
|
+
readonly id: () => ContourIdSchema<TShape[TIdentity], TName, TIdentity>;
|
|
95
|
+
readonly examples?: readonly z.output<z.ZodObject<TShape>>[] | undefined;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const formatExampleIssues = (issues: readonly z.core.$ZodIssue[]): string =>
|
|
99
|
+
issues
|
|
100
|
+
.map((issue) => {
|
|
101
|
+
const path = issue.path.length > 0 ? issue.path.join('.') : '<root>';
|
|
102
|
+
return `${path}: ${issue.message}`;
|
|
103
|
+
})
|
|
104
|
+
.join('; ');
|
|
105
|
+
|
|
106
|
+
const assertIdentityField = <
|
|
107
|
+
TShape extends z.ZodRawShape,
|
|
108
|
+
TIdentity extends keyof TShape & string,
|
|
109
|
+
>(
|
|
110
|
+
name: string,
|
|
111
|
+
shape: TShape,
|
|
112
|
+
identity: TIdentity
|
|
113
|
+
): void => {
|
|
114
|
+
if (!Object.hasOwn(shape, identity)) {
|
|
115
|
+
throw new TypeError(
|
|
116
|
+
`contour("${name}") identity "${identity}" must match a declared field`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const assertExamples = <TShape extends z.ZodRawShape>(
|
|
122
|
+
name: string,
|
|
123
|
+
schema: z.ZodObject<TShape>,
|
|
124
|
+
examples: readonly z.output<z.ZodObject<TShape>>[]
|
|
125
|
+
): void => {
|
|
126
|
+
for (const [index, example] of examples.entries()) {
|
|
127
|
+
const parsed = schema.safeParse(example);
|
|
128
|
+
if (!parsed.success) {
|
|
129
|
+
throw new TypeError(
|
|
130
|
+
`contour("${name}") example ${index} is invalid: ${formatExampleIssues(parsed.error.issues)}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const validateExamples = <TShape extends z.ZodRawShape>(
|
|
137
|
+
name: string,
|
|
138
|
+
schema: z.ZodObject<TShape>,
|
|
139
|
+
examples?: readonly z.output<z.ZodObject<TShape>>[] | undefined
|
|
140
|
+
): void => {
|
|
141
|
+
if (examples) {
|
|
142
|
+
assertExamples(name, schema, examples);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const brandIdentitySchema = <
|
|
147
|
+
TSchema extends z.core.$ZodType,
|
|
148
|
+
TName extends string,
|
|
149
|
+
TIdentity extends string,
|
|
150
|
+
>(
|
|
151
|
+
contour: TName,
|
|
152
|
+
identity: TIdentity,
|
|
153
|
+
schema: TSchema
|
|
154
|
+
): ContourIdSchema<TSchema, TName, TIdentity> => {
|
|
155
|
+
const branded = (schema as BrandableSchema<TSchema>).brand<
|
|
156
|
+
ContourIdBrand<TName>
|
|
157
|
+
>();
|
|
158
|
+
|
|
159
|
+
// First-write-wins: if another contour already claimed this schema object
|
|
160
|
+
// (possible when Zod v4 brand() returns `this`), preserve the original
|
|
161
|
+
// metadata rather than silently overwriting it.
|
|
162
|
+
if (!contourIdMetadata.has(branded)) {
|
|
163
|
+
contourIdMetadata.set(branded, {
|
|
164
|
+
contour,
|
|
165
|
+
identity,
|
|
166
|
+
} satisfies ContourIdMetadata<TName, TIdentity>);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return branded as ContourIdSchema<TSchema, TName, TIdentity>;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const attachContourMetadata = <
|
|
173
|
+
TName extends string,
|
|
174
|
+
TShape extends z.ZodRawShape,
|
|
175
|
+
TIdentity extends keyof TShape & string,
|
|
176
|
+
>(
|
|
177
|
+
schema: z.ZodObject<TShape>,
|
|
178
|
+
metadata: {
|
|
179
|
+
readonly examples?: readonly z.output<z.ZodObject<TShape>>[] | undefined;
|
|
180
|
+
readonly idSchema: ContourIdSchema<TShape[TIdentity], TName, TIdentity>;
|
|
181
|
+
readonly identity: TIdentity;
|
|
182
|
+
readonly identitySchema: TShape[TIdentity];
|
|
183
|
+
readonly name: TName;
|
|
184
|
+
}
|
|
185
|
+
): void => {
|
|
186
|
+
Object.defineProperties(schema, {
|
|
187
|
+
examples: {
|
|
188
|
+
enumerable: true,
|
|
189
|
+
value: metadata.examples,
|
|
190
|
+
writable: false,
|
|
191
|
+
},
|
|
192
|
+
id: {
|
|
193
|
+
enumerable: true,
|
|
194
|
+
value: () => metadata.idSchema,
|
|
195
|
+
writable: false,
|
|
196
|
+
},
|
|
197
|
+
identity: {
|
|
198
|
+
enumerable: true,
|
|
199
|
+
value: metadata.identity,
|
|
200
|
+
writable: false,
|
|
201
|
+
},
|
|
202
|
+
identitySchema: {
|
|
203
|
+
enumerable: true,
|
|
204
|
+
value: metadata.identitySchema,
|
|
205
|
+
writable: false,
|
|
206
|
+
},
|
|
207
|
+
kind: {
|
|
208
|
+
enumerable: true,
|
|
209
|
+
value: 'contour',
|
|
210
|
+
writable: false,
|
|
211
|
+
},
|
|
212
|
+
name: {
|
|
213
|
+
enumerable: true,
|
|
214
|
+
value: metadata.name,
|
|
215
|
+
writable: false,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/** Read contour identity metadata from the module-level WeakMap, if present. */
|
|
221
|
+
const readMetadata = (schema: unknown): ContourIdMetadata | undefined =>
|
|
222
|
+
typeof schema === 'object' && schema !== null
|
|
223
|
+
? contourIdMetadata.get(schema)
|
|
224
|
+
: undefined;
|
|
225
|
+
|
|
226
|
+
/** Resolve the inner schema from a Zod wrapper (ZodOptional, ZodNullable, etc.). */
|
|
227
|
+
const unwrapInner = (schema: unknown): unknown => {
|
|
228
|
+
const def = (schema as { _def?: Record<string, unknown> })._def;
|
|
229
|
+
return (def?.['innerType'] ?? def?.['schema']) as unknown;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Walk through Zod wrapper layers searching for `CONTOUR_ID_METADATA`.
|
|
234
|
+
*
|
|
235
|
+
* `.nullish()` produces `ZodOptional<ZodNullable<T>>` — two wrapper levels —
|
|
236
|
+
* so a single-step unwrap is insufficient. This iterates until it finds the
|
|
237
|
+
* metadata or exhausts all wrapper layers.
|
|
238
|
+
*/
|
|
239
|
+
const unwrapToMetadata = (schema: unknown): ContourIdMetadata | undefined => {
|
|
240
|
+
let current: unknown = schema;
|
|
241
|
+
while (typeof current === 'object' && current !== null) {
|
|
242
|
+
const inner = unwrapInner(current);
|
|
243
|
+
if (typeof inner !== 'object' || inner === null) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
const metadata = readMetadata(inner);
|
|
247
|
+
if (metadata !== undefined) {
|
|
248
|
+
return metadata;
|
|
249
|
+
}
|
|
250
|
+
current = inner;
|
|
251
|
+
}
|
|
252
|
+
return undefined;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Read contour-reference metadata from a schema returned by `contour.id()`.
|
|
257
|
+
*
|
|
258
|
+
* When the schema is wrapped by Zod combinators (`.optional()`, `.nullable()`,
|
|
259
|
+
* `.default()`, `.nullish()`, etc.) the `CONTOUR_ID_METADATA` symbol lives on
|
|
260
|
+
* the inner schema, not on the wrapper. The unwrap handles arbitrarily nested
|
|
261
|
+
* wrapper levels.
|
|
262
|
+
*/
|
|
263
|
+
export const getContourIdMetadata = (
|
|
264
|
+
schema: unknown
|
|
265
|
+
): ContourIdMetadata | undefined =>
|
|
266
|
+
readMetadata(schema) ?? unwrapToMetadata(schema);
|
|
267
|
+
|
|
268
|
+
/** Inspect a contour schema for fields that reference other contours via `.id()`. */
|
|
269
|
+
export const getContourReferences = (
|
|
270
|
+
contour: AnyContour
|
|
271
|
+
): readonly ContourReference[] =>
|
|
272
|
+
Object.entries(contour.shape)
|
|
273
|
+
.flatMap(([field, schema]) => {
|
|
274
|
+
if (field === contour.identity) {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
const metadata = getContourIdMetadata(schema);
|
|
278
|
+
if (metadata === undefined) {
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return [{ field, ...metadata }];
|
|
283
|
+
})
|
|
284
|
+
.toSorted((left, right) =>
|
|
285
|
+
left.field === right.field
|
|
286
|
+
? left.contour.localeCompare(right.contour)
|
|
287
|
+
: left.field.localeCompare(right.field)
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Create a contour definition from a raw Zod object shape.
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```typescript
|
|
295
|
+
* const user = contour(
|
|
296
|
+
* 'user',
|
|
297
|
+
* {
|
|
298
|
+
* id: z.string().uuid(),
|
|
299
|
+
* email: z.string().email(),
|
|
300
|
+
* name: z.string(),
|
|
301
|
+
* },
|
|
302
|
+
* { identity: 'id' }
|
|
303
|
+
* );
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
306
|
+
export const contour = <
|
|
307
|
+
TName extends string,
|
|
308
|
+
TShape extends z.ZodRawShape,
|
|
309
|
+
TIdentity extends keyof TShape & string,
|
|
310
|
+
>(
|
|
311
|
+
name: TName,
|
|
312
|
+
shape: TShape,
|
|
313
|
+
options: ContourOptions<TShape, TIdentity>
|
|
314
|
+
): Contour<TName, TShape, TIdentity> => {
|
|
315
|
+
assertIdentityField(name, shape, options.identity);
|
|
316
|
+
|
|
317
|
+
const schema = z.object(shape);
|
|
318
|
+
validateExamples(name, schema, options.examples);
|
|
319
|
+
|
|
320
|
+
const identitySchema = shape[options.identity];
|
|
321
|
+
if (!identitySchema) {
|
|
322
|
+
throw new TypeError(
|
|
323
|
+
`contour("${name}") identity "${options.identity}" must resolve to a schema`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const idSchema = brandIdentitySchema(name, options.identity, identitySchema);
|
|
328
|
+
const examples = options.examples
|
|
329
|
+
? Object.freeze([...options.examples])
|
|
330
|
+
: undefined;
|
|
331
|
+
|
|
332
|
+
attachContourMetadata(schema, {
|
|
333
|
+
examples,
|
|
334
|
+
idSchema,
|
|
335
|
+
identity: options.identity,
|
|
336
|
+
identitySchema,
|
|
337
|
+
name,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return schema as Contour<TName, TShape, TIdentity>;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/** Existential type for heterogeneous contour collections. */
|
|
344
|
+
export type AnyContour = Contour<string, z.ZodRawShape, string>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-invocation schema merging for trails with `crossInput`.
|
|
3
|
+
*
|
|
4
|
+
* When a trail declares `crossInput`, callers via `ctx.cross()` pass both
|
|
5
|
+
* public input and composition-only fields. The merged schema validates the
|
|
6
|
+
* combined shape so `executeTrail` doesn't reject the extra fields.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
|
|
11
|
+
import type { AnyTrail } from './trail.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build the validation schema for a cross-invoked trail.
|
|
15
|
+
*
|
|
16
|
+
* When the target trail declares `crossInput`, returns the intersection of
|
|
17
|
+
* `trail.input` and `trail.crossInput`. Returns `undefined` when no
|
|
18
|
+
* `crossInput` is declared, signaling that normal input validation suffices.
|
|
19
|
+
*/
|
|
20
|
+
export const buildCrossValidationSchema = (
|
|
21
|
+
trailDef: AnyTrail
|
|
22
|
+
): z.ZodType | undefined => {
|
|
23
|
+
if (!trailDef.crossInput) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
// Prefer .merge() for ZodObject pairs — produces a proper merged object
|
|
27
|
+
// schema that strips unknown keys and exposes .shape. Fall back to
|
|
28
|
+
// z.intersection for non-object schemas.
|
|
29
|
+
if (
|
|
30
|
+
trailDef.input instanceof z.ZodObject &&
|
|
31
|
+
trailDef.crossInput instanceof z.ZodObject
|
|
32
|
+
) {
|
|
33
|
+
return trailDef.input.merge(trailDef.crossInput);
|
|
34
|
+
}
|
|
35
|
+
return z.intersection(trailDef.input, trailDef.crossInput);
|
|
36
|
+
};
|
package/src/draft.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { AnyContour } from './contour.js';
|
|
2
|
+
import { getContourReferences } from './contour.js';
|
|
1
3
|
import { ValidationError } from './errors.js';
|
|
2
|
-
import type { AnySignal } from './
|
|
3
|
-
import type {
|
|
4
|
+
import type { AnySignal } from './signal.js';
|
|
5
|
+
import type { AnyResource } from './resource.js';
|
|
4
6
|
import { Result } from './result.js';
|
|
5
7
|
import type { Topo } from './topo.js';
|
|
6
8
|
import type { AnyTrail } from './trail.js';
|
|
@@ -9,9 +11,10 @@ export const DRAFT_ID_PREFIX = '_draft.';
|
|
|
9
11
|
|
|
10
12
|
export type DraftDependencyKind =
|
|
11
13
|
| 'cross'
|
|
12
|
-
| '
|
|
13
|
-
| '
|
|
14
|
+
| 'contour'
|
|
15
|
+
| 'resource'
|
|
14
16
|
| 'replaced-by'
|
|
17
|
+
| 'schema-reference'
|
|
15
18
|
| 'signal-from';
|
|
16
19
|
|
|
17
20
|
export interface DraftDependency {
|
|
@@ -22,14 +25,14 @@ export interface DraftDependency {
|
|
|
22
25
|
|
|
23
26
|
export interface DraftFinding {
|
|
24
27
|
readonly id: string;
|
|
25
|
-
readonly kind: '
|
|
28
|
+
readonly kind: 'contour' | 'resource' | 'signal' | 'trail' | 'unknown';
|
|
26
29
|
readonly message: string;
|
|
27
30
|
readonly rule: 'draft-contamination' | 'draft-id';
|
|
28
31
|
readonly via?: DraftDependencyKind | undefined;
|
|
29
32
|
readonly dependsOn?: string | undefined;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
export interface
|
|
35
|
+
export interface DraftReport {
|
|
33
36
|
readonly contaminatedIds: ReadonlySet<string>;
|
|
34
37
|
readonly declaredDraftIds: ReadonlySet<string>;
|
|
35
38
|
readonly dependencies: readonly DraftDependency[];
|
|
@@ -41,7 +44,7 @@ interface DraftReason {
|
|
|
41
44
|
readonly via: DraftDependencyKind;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
type TopoNode =
|
|
47
|
+
type TopoNode = AnyContour | AnyResource | AnySignal | AnyTrail;
|
|
45
48
|
|
|
46
49
|
const replacedByTarget = (value: TopoNode): string | undefined => {
|
|
47
50
|
const raw = value as unknown as { replacedBy?: unknown };
|
|
@@ -77,59 +80,54 @@ const dependencyFromTarget = (
|
|
|
77
80
|
},
|
|
78
81
|
];
|
|
79
82
|
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
83
|
+
const trailDependencies = (trail: AnyTrail): DraftDependency[] => [
|
|
84
|
+
...dependenciesFromIds(
|
|
85
|
+
trail.id,
|
|
86
|
+
(trail.contours ?? []).map((contour) => contour.name),
|
|
87
|
+
'contour'
|
|
88
|
+
),
|
|
89
|
+
...dependenciesFromIds(trail.id, trail.crosses, 'cross'),
|
|
90
|
+
...dependenciesFromIds(
|
|
91
|
+
trail.id,
|
|
92
|
+
trail.resources.map(({ id }) => id),
|
|
93
|
+
'resource'
|
|
94
|
+
),
|
|
95
|
+
...dependencyFromTarget(trail.id, replacedByTarget(trail), 'replaced-by'),
|
|
96
|
+
];
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
),
|
|
100
|
-
...(detours === undefined
|
|
101
|
-
? []
|
|
102
|
-
: dependenciesFromDetours(trail.id, detours)),
|
|
103
|
-
...dependencyFromTarget(trail.id, replacedByTarget(trail), 'replaced-by'),
|
|
104
|
-
];
|
|
105
|
-
};
|
|
98
|
+
const contourDependencies = (contour: AnyContour): DraftDependency[] =>
|
|
99
|
+
dependenciesFromIds(
|
|
100
|
+
contour.name,
|
|
101
|
+
getContourReferences(contour).map((reference) => reference.contour),
|
|
102
|
+
'schema-reference'
|
|
103
|
+
);
|
|
106
104
|
|
|
107
105
|
const signalDependencies = (signal: AnySignal): DraftDependency[] => [
|
|
108
106
|
...dependenciesFromIds(signal.id, signal.from ?? [], 'signal-from'),
|
|
109
107
|
...dependencyFromTarget(signal.id, replacedByTarget(signal), 'replaced-by'),
|
|
110
108
|
];
|
|
111
109
|
|
|
112
|
-
const
|
|
113
|
-
dependencyFromTarget(
|
|
114
|
-
provision.id,
|
|
115
|
-
replacedByTarget(provision),
|
|
116
|
-
'replaced-by'
|
|
117
|
-
);
|
|
110
|
+
const resourceDependencies = (resource: AnyResource): DraftDependency[] =>
|
|
111
|
+
dependencyFromTarget(resource.id, replacedByTarget(resource), 'replaced-by');
|
|
118
112
|
|
|
119
113
|
const nodeKind = (
|
|
120
114
|
id: string,
|
|
115
|
+
contours: ReadonlyMap<string, AnyContour>,
|
|
121
116
|
trails: ReadonlyMap<string, AnyTrail>,
|
|
122
117
|
signals: ReadonlyMap<string, AnySignal>,
|
|
123
|
-
|
|
118
|
+
resources: ReadonlyMap<string, AnyResource>
|
|
124
119
|
): DraftFinding['kind'] => {
|
|
120
|
+
if (contours.has(id)) {
|
|
121
|
+
return 'contour';
|
|
122
|
+
}
|
|
125
123
|
if (trails.has(id)) {
|
|
126
124
|
return 'trail';
|
|
127
125
|
}
|
|
128
126
|
if (signals.has(id)) {
|
|
129
127
|
return 'signal';
|
|
130
128
|
}
|
|
131
|
-
if (
|
|
132
|
-
return '
|
|
129
|
+
if (resources.has(id)) {
|
|
130
|
+
return 'resource';
|
|
133
131
|
}
|
|
134
132
|
return 'unknown';
|
|
135
133
|
};
|
|
@@ -142,9 +140,10 @@ const draftIdsFromKeys = (keys: Iterable<string>): string[] =>
|
|
|
142
140
|
|
|
143
141
|
const collectDeclaredDraftIds = (topo: Topo): ReadonlySet<string> =>
|
|
144
142
|
new Set([
|
|
143
|
+
...draftIdsFromKeys(topo.contours.keys()),
|
|
145
144
|
...draftIdsFromKeys(topo.trails.keys()),
|
|
146
145
|
...draftIdsFromKeys(topo.signals.keys()),
|
|
147
|
-
...draftIdsFromKeys(topo.
|
|
146
|
+
...draftIdsFromKeys(topo.resources.keys()),
|
|
148
147
|
]);
|
|
149
148
|
|
|
150
149
|
const findingForDraftId = (
|
|
@@ -237,9 +236,10 @@ const contaminationFindingForId = (
|
|
|
237
236
|
id: string,
|
|
238
237
|
declaredDraftIds: ReadonlySet<string>,
|
|
239
238
|
reasons: ReadonlyMap<string, DraftReason>,
|
|
239
|
+
contours: ReadonlyMap<string, AnyContour>,
|
|
240
240
|
trails: ReadonlyMap<string, AnyTrail>,
|
|
241
241
|
signals: ReadonlyMap<string, AnySignal>,
|
|
242
|
-
|
|
242
|
+
resources: ReadonlyMap<string, AnyResource>
|
|
243
243
|
): DraftFinding | undefined => {
|
|
244
244
|
if (declaredDraftIds.has(id)) {
|
|
245
245
|
return undefined;
|
|
@@ -252,7 +252,7 @@ const contaminationFindingForId = (
|
|
|
252
252
|
|
|
253
253
|
return findingForContamination(
|
|
254
254
|
id,
|
|
255
|
-
nodeKind(id, trails, signals,
|
|
255
|
+
nodeKind(id, contours, trails, signals, resources),
|
|
256
256
|
reason
|
|
257
257
|
);
|
|
258
258
|
};
|
|
@@ -261,23 +261,25 @@ const collectFindings = (
|
|
|
261
261
|
declaredDraftIds: ReadonlySet<string>,
|
|
262
262
|
contaminatedIds: ReadonlySet<string>,
|
|
263
263
|
reasons: ReadonlyMap<string, DraftReason>,
|
|
264
|
+
contours: ReadonlyMap<string, AnyContour>,
|
|
264
265
|
trails: ReadonlyMap<string, AnyTrail>,
|
|
265
266
|
signals: ReadonlyMap<string, AnySignal>,
|
|
266
|
-
|
|
267
|
+
resources: ReadonlyMap<string, AnyResource>
|
|
267
268
|
): DraftFinding[] => [
|
|
268
269
|
...[...declaredDraftIds]
|
|
269
270
|
.toSorted()
|
|
270
271
|
.map((id) =>
|
|
271
|
-
findingForDraftId(id, nodeKind(id, trails, signals,
|
|
272
|
+
findingForDraftId(id, nodeKind(id, contours, trails, signals, resources))
|
|
272
273
|
),
|
|
273
274
|
...[...contaminatedIds].toSorted().flatMap((id) => {
|
|
274
275
|
const finding = contaminationFindingForId(
|
|
275
276
|
id,
|
|
276
277
|
declaredDraftIds,
|
|
277
278
|
reasons,
|
|
279
|
+
contours,
|
|
278
280
|
trails,
|
|
279
281
|
signals,
|
|
280
|
-
|
|
282
|
+
resources
|
|
281
283
|
);
|
|
282
284
|
|
|
283
285
|
return finding === undefined ? [] : [finding];
|
|
@@ -285,12 +287,13 @@ const collectFindings = (
|
|
|
285
287
|
];
|
|
286
288
|
|
|
287
289
|
const collectDependencies = (topo: Topo): DraftDependency[] => [
|
|
290
|
+
...[...topo.contours.values()].flatMap(contourDependencies),
|
|
288
291
|
...[...topo.trails.values()].flatMap(trailDependencies),
|
|
289
292
|
...[...topo.signals.values()].flatMap(signalDependencies),
|
|
290
|
-
...[...topo.
|
|
293
|
+
...[...topo.resources.values()].flatMap(resourceDependencies),
|
|
291
294
|
];
|
|
292
295
|
|
|
293
|
-
export const
|
|
296
|
+
export const deriveDraftReport = (topo: Topo): DraftReport => {
|
|
294
297
|
const declaredDraftIds = collectDeclaredDraftIds(topo);
|
|
295
298
|
const dependencies = collectDependencies(topo);
|
|
296
299
|
const { contaminatedIds, reasons } = propagateContaminatedIds(
|
|
@@ -301,9 +304,10 @@ export const analyzeDraftState = (topo: Topo): DraftStateAnalysis => {
|
|
|
301
304
|
declaredDraftIds,
|
|
302
305
|
contaminatedIds,
|
|
303
306
|
reasons,
|
|
307
|
+
topo.contours,
|
|
304
308
|
topo.trails,
|
|
305
309
|
topo.signals,
|
|
306
|
-
topo.
|
|
310
|
+
topo.resources
|
|
307
311
|
);
|
|
308
312
|
|
|
309
313
|
return {
|
|
@@ -314,10 +318,10 @@ export const analyzeDraftState = (topo: Topo): DraftStateAnalysis => {
|
|
|
314
318
|
};
|
|
315
319
|
};
|
|
316
320
|
|
|
317
|
-
export const
|
|
321
|
+
export const validateDraftFreeTopo = (
|
|
318
322
|
topo: Topo
|
|
319
323
|
): Result<void, ValidationError> => {
|
|
320
|
-
const analysis =
|
|
324
|
+
const analysis = deriveDraftReport(topo);
|
|
321
325
|
|
|
322
326
|
if (analysis.findings.length === 0) {
|
|
323
327
|
return Result.ok();
|