@telorun/analyzer 0.62.0 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +6 -0
- package/dist/analyzer.d.ts +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +343 -13
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +2 -7
- package/dist/flatten-for-analyzer.js +1 -1
- package/dist/import-resolution-diagnostics.d.ts +9 -2
- package/dist/import-resolution-diagnostics.d.ts.map +1 -1
- package/dist/import-resolution-diagnostics.js +26 -7
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/invocation-contract.d.ts +6 -1
- package/dist/invocation-contract.d.ts.map +1 -1
- package/dist/invocation-contract.js +27 -4
- package/dist/loaded-types.d.ts +12 -3
- package/dist/loaded-types.d.ts.map +1 -1
- package/dist/manifest-loader.d.ts +0 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +62 -41
- package/dist/manifest-schemas.d.ts +699 -0
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +306 -14
- package/dist/module-compatibility.d.ts +40 -0
- package/dist/module-compatibility.d.ts.map +1 -0
- package/dist/module-compatibility.js +52 -0
- package/dist/ref-slot-reading.d.ts +30 -0
- package/dist/ref-slot-reading.d.ts.map +1 -0
- package/dist/ref-slot-reading.js +139 -0
- package/dist/referrer-rule.d.ts +67 -0
- package/dist/referrer-rule.d.ts.map +1 -0
- package/dist/referrer-rule.js +96 -0
- package/dist/release/index.d.ts +1 -1
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +1 -1
- package/dist/release/payload-digest.d.ts +7 -3
- package/dist/release/payload-digest.d.ts.map +1 -1
- package/dist/release/payload-digest.js +7 -3
- package/dist/release/version-stamp.d.ts +16 -0
- package/dist/release/version-stamp.d.ts.map +1 -1
- package/dist/release/version-stamp.js +52 -0
- package/dist/release/workspace-config.d.ts +10 -6
- package/dist/release/workspace-config.d.ts.map +1 -1
- package/dist/release/workspace-config.js +10 -6
- package/dist/resolve-schema-ref-kinds.d.ts +4 -3
- package/dist/resolve-schema-ref-kinds.d.ts.map +1 -1
- package/dist/resolve-schema-ref-kinds.js +16 -0
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +4 -3
- package/dist/resolve-zone-containment.d.ts +99 -0
- package/dist/resolve-zone-containment.d.ts.map +1 -0
- package/dist/resolve-zone-containment.js +212 -0
- package/dist/resolve-zone-requirements.d.ts +5 -0
- package/dist/resolve-zone-requirements.d.ts.map +1 -1
- package/dist/resolve-zone-requirements.js +48 -7
- package/dist/resource-rule.d.ts +106 -0
- package/dist/resource-rule.d.ts.map +1 -0
- package/dist/resource-rule.js +236 -0
- package/dist/rule-condition.d.ts +50 -0
- package/dist/rule-condition.d.ts.map +1 -0
- package/dist/rule-condition.js +108 -0
- package/dist/schema-keywords.d.ts.map +1 -1
- package/dist/schema-keywords.js +20 -0
- package/dist/schema-projection.d.ts +171 -0
- package/dist/schema-projection.d.ts.map +1 -0
- package/dist/schema-projection.js +317 -0
- package/dist/step-slot.d.ts +39 -0
- package/dist/step-slot.d.ts.map +1 -0
- package/dist/step-slot.js +49 -0
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +17 -3
- package/dist/validate-durable-regions.d.ts +15 -0
- package/dist/validate-durable-regions.d.ts.map +1 -0
- package/dist/validate-durable-regions.js +336 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +44 -2
- package/dist/validate-referrer-rules.d.ts +120 -0
- package/dist/validate-referrer-rules.d.ts.map +1 -0
- package/dist/validate-referrer-rules.js +255 -0
- package/dist/validate-resource-rules.d.ts +114 -0
- package/dist/validate-resource-rules.d.ts.map +1 -0
- package/dist/validate-resource-rules.js +283 -0
- package/dist/validate-schema-projection.d.ts +32 -0
- package/dist/validate-schema-projection.d.ts.map +1 -0
- package/dist/validate-schema-projection.js +110 -0
- package/dist/validate-step-inputs.d.ts +2 -1
- package/dist/validate-step-inputs.d.ts.map +1 -1
- package/dist/validate-step-inputs.js +5 -3
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +11 -5
- package/dist/validate-zone-slots.d.ts +2 -2
- package/dist/validate-zone-slots.d.ts.map +1 -1
- package/dist/validate-zone-slots.js +202 -6
- package/dist/validate-zone-violations.d.ts +12 -0
- package/dist/validate-zone-violations.d.ts.map +1 -0
- package/dist/validate-zone-violations.js +169 -0
- package/dist/value-type-keyword.d.ts +1 -1
- package/dist/value-type-keyword.d.ts.map +1 -1
- package/dist/value-type-keyword.js +4 -0
- package/dist/zone-slot.d.ts +61 -0
- package/dist/zone-slot.d.ts.map +1 -1
- package/dist/zone-slot.js +86 -5
- package/package.json +2 -2
- package/src/analysis-registry.ts +6 -0
- package/src/analyzer.ts +395 -14
- package/src/call-graph.ts +2 -8
- package/src/flatten-for-analyzer.ts +1 -1
- package/src/import-resolution-diagnostics.ts +25 -7
- package/src/index.ts +73 -0
- package/src/invocation-contract.ts +45 -2
- package/src/loaded-types.ts +12 -3
- package/src/manifest-loader.ts +87 -55
- package/src/manifest-schemas.ts +328 -14
- package/src/module-compatibility.ts +72 -0
- package/src/ref-slot-reading.ts +156 -0
- package/src/referrer-rule.ts +116 -0
- package/src/release/index.ts +1 -0
- package/src/release/payload-digest.ts +7 -3
- package/src/release/version-stamp.ts +58 -0
- package/src/release/workspace-config.ts +10 -6
- package/src/resolve-schema-ref-kinds.ts +28 -4
- package/src/resolve-throws-union.ts +4 -3
- package/src/resolve-zone-containment.ts +329 -0
- package/src/resolve-zone-requirements.ts +67 -7
- package/src/resource-rule.ts +252 -0
- package/src/rule-condition.ts +126 -0
- package/src/schema-keywords.ts +24 -0
- package/src/schema-projection.ts +403 -0
- package/src/step-slot.ts +64 -0
- package/src/telo-version.ts +1 -1
- package/src/validate-cel-context.ts +17 -3
- package/src/validate-durable-regions.ts +401 -0
- package/src/validate-references.ts +57 -2
- package/src/validate-referrer-rules.ts +373 -0
- package/src/validate-resource-rules.ts +401 -0
- package/src/validate-schema-projection.ts +184 -0
- package/src/validate-step-inputs.ts +5 -3
- package/src/validate-throws-coverage.ts +12 -5
- package/src/validate-zone-slots.ts +192 -8
- package/src/validate-zone-violations.ts +190 -0
- package/src/value-type-keyword.ts +4 -0
- package/src/zone-slot.ts +114 -5
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schema-projection annotations' single reader — the `ref-slot.ts` /
|
|
3
|
+
* `zone-slot.ts` precedent.
|
|
4
|
+
*
|
|
5
|
+
* A kind whose configuration is a COLLECTION OF TYPED ENTRIES can say what that
|
|
6
|
+
* collection means as a JSON Schema object, so a consumer can type the values
|
|
7
|
+
* it will read without the analyzer learning anything about the domain. A SQL
|
|
8
|
+
* table's columns are the first consumer; nothing in either annotation says
|
|
9
|
+
* SQL, column or table.
|
|
10
|
+
*
|
|
11
|
+
* Two halves, because the two facts have different owners:
|
|
12
|
+
*
|
|
13
|
+
* - `x-telo-schema-map`, on the field a projection keys on, gives the schema
|
|
14
|
+
* node each of its values means (`citext → {type: string}`). It sits with the
|
|
15
|
+
* field because that is where the value vocabulary is declared.
|
|
16
|
+
* - `x-telo-schema-projection`, on the KIND DOCUMENT (a sibling of `schema:`,
|
|
17
|
+
* not a keyword inside it), names the entry collection, the keying field, and
|
|
18
|
+
* the fields that MODIFY the mapped node. It sits on the document because it
|
|
19
|
+
* describes the kind's whole declaration rather than one field of it — but
|
|
20
|
+
* `schema:` is where every other `x-telo-*` keyword lives, so the reader
|
|
21
|
+
* accepts it in both positions and `validate-schema-projection.ts` reports the
|
|
22
|
+
* inner one. Silently ignoring a misplaced annotation is the exact failure the
|
|
23
|
+
* strict half exists to prevent: the projection stops typing its consumers and
|
|
24
|
+
* the diagnostic lands on the CONSUMER, blaming the wrong author.
|
|
25
|
+
*
|
|
26
|
+
* It is a declared LOOKUP, never a computed expression. The analyzer
|
|
27
|
+
* type-checks CEL and substitutes placeholders; it never evaluates, and a
|
|
28
|
+
* `base:`-style mapping is evaluated by the kernel at `create()` — too late for
|
|
29
|
+
* `telo check` to type the rows a consumer reads, which is the projection's
|
|
30
|
+
* whole purpose.
|
|
31
|
+
*
|
|
32
|
+
* Distinct from `x-telo-schema-from`, which derives a field's schema from a
|
|
33
|
+
* referenced KIND's definition schema. A projection is DECLARATION-derived: the
|
|
34
|
+
* row shape lives in one instance's own `columns:`, which no definition-level
|
|
35
|
+
* derivation can reach.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** How a kind's entry collection projects to an object schema. */
|
|
39
|
+
export interface SchemaProjection {
|
|
40
|
+
/** JSON Pointer, from the resource root, to the entries. */
|
|
41
|
+
readonly entries: string;
|
|
42
|
+
/** The entry field whose value keys the `x-telo-schema-map` lookup. */
|
|
43
|
+
readonly key: string;
|
|
44
|
+
/** The entry field naming an entry's identity, when entries are an ARRAY.
|
|
45
|
+
* Absent for a keyed map, where the map key is the identity. */
|
|
46
|
+
readonly nameField?: string;
|
|
47
|
+
/** Entry field that widens the mapped node to admit null. */
|
|
48
|
+
readonly nullable?: string;
|
|
49
|
+
/** Entry field that wraps the mapped node in an array. */
|
|
50
|
+
readonly array?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type SchemaMap = Readonly<Record<string, Record<string, unknown>>>;
|
|
54
|
+
|
|
55
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
56
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The projection a kind declares, or undefined. Invalid shapes read as absent;
|
|
60
|
+
* `validate-schema-projection.ts` is the half that reports them. */
|
|
61
|
+
export function readSchemaProjection(definition: unknown): SchemaProjection | undefined {
|
|
62
|
+
if (!isObject(definition)) return undefined;
|
|
63
|
+
const raw = rawSchemaProjection(definition);
|
|
64
|
+
if (!isObject(raw)) return undefined;
|
|
65
|
+
const entries = raw.entries;
|
|
66
|
+
const key = raw.key;
|
|
67
|
+
if (typeof entries !== "string" || typeof key !== "string") return undefined;
|
|
68
|
+
return {
|
|
69
|
+
entries,
|
|
70
|
+
key,
|
|
71
|
+
nameField: typeof raw.name === "string" ? raw.name : undefined,
|
|
72
|
+
nullable: typeof raw.nullable === "string" ? raw.nullable : undefined,
|
|
73
|
+
array: typeof raw.array === "string" ? raw.array : undefined,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The annotation as written, from either position — the document (canonical)
|
|
78
|
+
* or `schema:` (accepted, and reported by the strict half). The document wins:
|
|
79
|
+
* a kind spelling it in both places is describing its own document. */
|
|
80
|
+
export function rawSchemaProjection(definition: unknown): unknown {
|
|
81
|
+
if (!isObject(definition)) return undefined;
|
|
82
|
+
const own = definition["x-telo-schema-projection"];
|
|
83
|
+
if (own !== undefined) return own;
|
|
84
|
+
const schema = definition.schema;
|
|
85
|
+
return isObject(schema) ? schema["x-telo-schema-projection"] : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** True when the annotation was found inside `schema:` rather than on the
|
|
89
|
+
* document — the misplacement the strict half reports. */
|
|
90
|
+
export function schemaProjectionIsMisplaced(definition: unknown): boolean {
|
|
91
|
+
if (!isObject(definition)) return false;
|
|
92
|
+
if (definition["x-telo-schema-projection"] !== undefined) return false;
|
|
93
|
+
const schema = definition.schema;
|
|
94
|
+
return isObject(schema) && schema["x-telo-schema-projection"] !== undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function readSchemaMap(node: unknown): SchemaMap | undefined {
|
|
98
|
+
if (!isObject(node)) return undefined;
|
|
99
|
+
const raw = node["x-telo-schema-map"];
|
|
100
|
+
if (!isObject(raw)) return undefined;
|
|
101
|
+
const entries = Object.entries(raw).filter(([, value]) => isObject(value));
|
|
102
|
+
if (entries.length === 0) return undefined;
|
|
103
|
+
return Object.fromEntries(entries) as SchemaMap;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The consumer-side annotation: a JSON Pointer to this resource's ref slot
|
|
107
|
+
* whose target declares the projection. */
|
|
108
|
+
export function readProjectionFrom(node: unknown): string | undefined {
|
|
109
|
+
if (!isObject(node)) return undefined;
|
|
110
|
+
const raw = node["x-telo-schema-projection-from"];
|
|
111
|
+
return typeof raw === "string" ? raw : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function navigate(root: unknown, pointer: string): unknown {
|
|
115
|
+
let current: unknown = root;
|
|
116
|
+
for (const segment of pointer.split("/")) {
|
|
117
|
+
if (segment === "") continue;
|
|
118
|
+
if (!isObject(current)) return undefined;
|
|
119
|
+
current = current[segment];
|
|
120
|
+
}
|
|
121
|
+
return current;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Find the `x-telo-schema-map` a projection keys on. The map sits on the entry
|
|
126
|
+
* field's schema, which is reached through the collection's own schema — a
|
|
127
|
+
* keyed map's `additionalProperties`, or an array's `items`.
|
|
128
|
+
*/
|
|
129
|
+
export function projectionKeyMap(
|
|
130
|
+
kindSchema: unknown,
|
|
131
|
+
projection: SchemaProjection,
|
|
132
|
+
): SchemaMap | undefined {
|
|
133
|
+
let node: unknown = kindSchema;
|
|
134
|
+
for (const segment of projection.entries.split("/")) {
|
|
135
|
+
if (segment === "") continue;
|
|
136
|
+
if (!isObject(node) || !isObject(node.properties)) return undefined;
|
|
137
|
+
node = node.properties[segment];
|
|
138
|
+
}
|
|
139
|
+
if (!isObject(node)) return undefined;
|
|
140
|
+
const entry = isObject(node.additionalProperties)
|
|
141
|
+
? node.additionalProperties
|
|
142
|
+
: isObject(node.items)
|
|
143
|
+
? node.items
|
|
144
|
+
: undefined;
|
|
145
|
+
if (!isObject(entry) || !isObject(entry.properties)) return undefined;
|
|
146
|
+
return readSchemaMap(entry.properties[projection.key]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Project one declaration to an object schema.
|
|
151
|
+
*
|
|
152
|
+
* Modifiers are a CLOSED set applied in a FIXED order — `array` wraps, then
|
|
153
|
+
* `nullable` widens. Closed because each changes how the schema is assembled,
|
|
154
|
+
* so a third-party modifier would be a name nothing acts on; ordered because
|
|
155
|
+
* leaving it implicit is how two implementations come to disagree.
|
|
156
|
+
*
|
|
157
|
+
* The projection is deliberately LOSSY. Length, precision, collation and check
|
|
158
|
+
* constraints do not reach it: a consumer needs the type, its nullability and
|
|
159
|
+
* its repetition, and the database enforces the rest. A per-entry schema rich
|
|
160
|
+
* enough to double as a validator would move the domain's semantics into the
|
|
161
|
+
* type layer.
|
|
162
|
+
*/
|
|
163
|
+
export function projectEntries(
|
|
164
|
+
manifest: unknown,
|
|
165
|
+
projection: SchemaProjection,
|
|
166
|
+
map: SchemaMap,
|
|
167
|
+
): Record<string, unknown> | undefined {
|
|
168
|
+
const entries = navigate(manifest, projection.entries);
|
|
169
|
+
if (entries === undefined) return undefined;
|
|
170
|
+
|
|
171
|
+
const pairs: [string, Record<string, unknown>][] = [];
|
|
172
|
+
const consider = (name: string | undefined, entry: unknown): void => {
|
|
173
|
+
if (!isObject(entry) || name === undefined) return;
|
|
174
|
+
const key = entry[projection.key];
|
|
175
|
+
const mapped = typeof key === "string" ? map[key] : undefined;
|
|
176
|
+
// A value with no map entry projects to nothing rather than to `any`: the
|
|
177
|
+
// vocabulary is the kind's own enum, so an unmapped value is a gap in the
|
|
178
|
+
// kind's declaration, not a shape to guess at.
|
|
179
|
+
if (!mapped) return;
|
|
180
|
+
let node: Record<string, unknown> = { ...mapped };
|
|
181
|
+
if (projection.array && entry[projection.array] === true) {
|
|
182
|
+
node = { type: "array", items: node };
|
|
183
|
+
}
|
|
184
|
+
if (projection.nullable && entry[projection.nullable] !== false) {
|
|
185
|
+
node = { anyOf: [node, { type: "null" }] };
|
|
186
|
+
}
|
|
187
|
+
pairs.push([name, node]);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
if (Array.isArray(entries)) {
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
const name = isObject(entry) && projection.nameField
|
|
193
|
+
? (entry[projection.nameField] as string | undefined)
|
|
194
|
+
: undefined;
|
|
195
|
+
consider(name, entry);
|
|
196
|
+
}
|
|
197
|
+
} else if (isObject(entries)) {
|
|
198
|
+
for (const [name, entry] of Object.entries(entries)) consider(name, entry);
|
|
199
|
+
} else {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: Object.fromEntries(pairs),
|
|
206
|
+
additionalProperties: false,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** A reference as the analyzer sees it: the internal `{kind, name, alias?}`
|
|
211
|
+
* shape `resolveRefSentinels` rewrites `!ref` to. */
|
|
212
|
+
export interface ProjectionRef {
|
|
213
|
+
readonly name: string;
|
|
214
|
+
readonly kind?: string;
|
|
215
|
+
readonly alias?: string;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The `{kind, name, alias?}` reference a value holds, or undefined. Exported so
|
|
219
|
+
* a host whose slot may hold EITHER shape can fall back to this reading. */
|
|
220
|
+
export function readProjectionRef(value: unknown): ProjectionRef | undefined {
|
|
221
|
+
if (!isObject(value)) return undefined;
|
|
222
|
+
const name = value.name;
|
|
223
|
+
if (typeof name !== "string") return undefined;
|
|
224
|
+
return {
|
|
225
|
+
name,
|
|
226
|
+
kind: typeof value.kind === "string" ? value.kind : undefined,
|
|
227
|
+
alias: typeof value.alias === "string" ? value.alias : undefined,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** What a reference resolved to. `"ambiguous"` is distinct from `undefined`
|
|
232
|
+
* because the two need different advice: one says disambiguate, the other says
|
|
233
|
+
* the name resolves to nothing. */
|
|
234
|
+
export type ProjectionLookup =
|
|
235
|
+
| { readonly manifest: Record<string, any> }
|
|
236
|
+
| { readonly ambiguous: true }
|
|
237
|
+
| undefined;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* What projecting a consumer's slot needs: resolving a reference to the manifest
|
|
241
|
+
* it names, and the definition that manifest's `kind` names.
|
|
242
|
+
*
|
|
243
|
+
* A RESOLVER rather than a list of manifests, because resolution is scoped and
|
|
244
|
+
* only the host knows the scope: an alias-qualified `!ref Alias.users` names an
|
|
245
|
+
* import's exported instance, and a bare name means the enclosing module's — a
|
|
246
|
+
* distinction a name filter over one flattened list erases, which is how an
|
|
247
|
+
* unambiguous cross-module reference came to read as ambiguous. It is also what
|
|
248
|
+
* lets the kernel supply its own context lookup, so the contract the analyzer
|
|
249
|
+
* types and the contract the kernel enforces are the same schema.
|
|
250
|
+
*/
|
|
251
|
+
export interface ProjectionScope {
|
|
252
|
+
/**
|
|
253
|
+
* The declaration the value at a projected slot names.
|
|
254
|
+
*
|
|
255
|
+
* Takes the RAW slot value rather than a parsed reference, because what sits
|
|
256
|
+
* there depends on the host and only the host can read it: the analyzer sees
|
|
257
|
+
* the `{kind, name, alias?}` reference the loader produced, while the kernel
|
|
258
|
+
* binds contracts AFTER Phase-5 injection has replaced that reference with the
|
|
259
|
+
* live instance. Parsing it here would have hardcoded the analyzer's shape and
|
|
260
|
+
* left the kernel unable to resolve anything — which is a contract enforced
|
|
261
|
+
* statically and not at dispatch.
|
|
262
|
+
*/
|
|
263
|
+
resolveManifest(value: unknown): ProjectionLookup;
|
|
264
|
+
resolveDefinition(kind: string): Record<string, any> | undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The resolver for a FLATTENED manifest list — the analyzer's own shape.
|
|
269
|
+
*
|
|
270
|
+
* An alias narrows to the manifests forwarded from that import (stamped
|
|
271
|
+
* `metadata.alias` by flatten), so two libraries each exporting a `users` table
|
|
272
|
+
* stay distinguishable. Only when nothing carries the alias does it fall back to
|
|
273
|
+
* matching by name alone, which is the pre-flatten shape a standalone module
|
|
274
|
+
* analysis has.
|
|
275
|
+
*/
|
|
276
|
+
export function manifestListScope(
|
|
277
|
+
manifests: readonly Record<string, any>[],
|
|
278
|
+
resolveDefinition: (kind: string) => Record<string, any> | undefined,
|
|
279
|
+
): ProjectionScope {
|
|
280
|
+
return {
|
|
281
|
+
resolveDefinition,
|
|
282
|
+
resolveManifest(value) {
|
|
283
|
+
const ref = readProjectionRef(value);
|
|
284
|
+
if (!ref) return undefined;
|
|
285
|
+
const byName = manifests.filter(
|
|
286
|
+
(candidate) =>
|
|
287
|
+
(candidate?.metadata as { name?: unknown } | undefined)?.name === ref.name &&
|
|
288
|
+
(typeof ref.kind !== "string" || candidate.kind === ref.kind),
|
|
289
|
+
);
|
|
290
|
+
const aliased =
|
|
291
|
+
ref.alias && ref.alias !== "Self"
|
|
292
|
+
? byName.filter(
|
|
293
|
+
(candidate) =>
|
|
294
|
+
(candidate?.metadata as { alias?: unknown } | undefined)?.alias === ref.alias,
|
|
295
|
+
)
|
|
296
|
+
: byName;
|
|
297
|
+
// A name that matches SEVERAL manifests is REFUSED rather than resolved to
|
|
298
|
+
// the first: picking one by flatten order would type the consumer's rows
|
|
299
|
+
// against the wrong declaration — a wrong answer, which is worse than no
|
|
300
|
+
// answer. Reported, so the author is told to disambiguate.
|
|
301
|
+
const matches = aliased.length > 0 ? aliased : byName;
|
|
302
|
+
if (matches.length === 0) return undefined;
|
|
303
|
+
if (matches.length > 1) return { ambiguous: true };
|
|
304
|
+
return { manifest: matches[0]! };
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export type ProjectionFailure =
|
|
310
|
+
| { readonly reason: "no-ref"; readonly pointer: string }
|
|
311
|
+
| { readonly reason: "unresolved"; readonly pointer: string; readonly name: string }
|
|
312
|
+
| { readonly reason: "ambiguous"; readonly pointer: string; readonly name: string }
|
|
313
|
+
| { readonly reason: "no-projection"; readonly pointer: string; readonly kind: string };
|
|
314
|
+
|
|
315
|
+
function refTarget(
|
|
316
|
+
value: unknown,
|
|
317
|
+
scope: ProjectionScope,
|
|
318
|
+
pointer: string,
|
|
319
|
+
):
|
|
320
|
+
| { manifest: Record<string, any>; definition: Record<string, any> }
|
|
321
|
+
| ProjectionFailure {
|
|
322
|
+
if (!isObject(value)) return { reason: "no-ref", pointer };
|
|
323
|
+
const name = typeof value.name === "string" ? value.name : "<unnamed>";
|
|
324
|
+
const found = scope.resolveManifest(value);
|
|
325
|
+
if (!found) return { reason: "unresolved", pointer, name };
|
|
326
|
+
if ("ambiguous" in found) return { reason: "ambiguous", pointer, name };
|
|
327
|
+
const manifest = found.manifest;
|
|
328
|
+
if (typeof manifest.kind !== "string") return { reason: "unresolved", pointer, name };
|
|
329
|
+
const definition = scope.resolveDefinition(manifest.kind);
|
|
330
|
+
if (!definition) return { reason: "no-projection", pointer, kind: manifest.kind };
|
|
331
|
+
return { manifest, definition };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function describeProjectionFailure(failure: ProjectionFailure): string {
|
|
335
|
+
switch (failure.reason) {
|
|
336
|
+
case "no-ref":
|
|
337
|
+
return `'${failure.pointer}' does not hold a reference, so there is no declaration to project.`;
|
|
338
|
+
case "unresolved":
|
|
339
|
+
return `'${failure.pointer}' references '${failure.name}', which resolves to no resource.`;
|
|
340
|
+
case "ambiguous":
|
|
341
|
+
return (
|
|
342
|
+
`'${failure.pointer}' references '${failure.name}', which matches more than one resource ` +
|
|
343
|
+
`in scope. Rename one of them so the reference names exactly one declaration.`
|
|
344
|
+
);
|
|
345
|
+
case "no-projection":
|
|
346
|
+
return (
|
|
347
|
+
`'${failure.pointer}' references a resource of kind '${failure.kind}', which declares no ` +
|
|
348
|
+
`'x-telo-schema-projection' — so there is nothing for this slot to be typed from.`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Replace every `x-telo-schema-projection-from` node with the projection of the
|
|
355
|
+
* declaration it points at.
|
|
356
|
+
*
|
|
357
|
+
* Structural: returns a new schema and never mutates the one handed in. A node
|
|
358
|
+
* that cannot be projected is left exactly as it was — degrading to the slot's
|
|
359
|
+
* own schema rather than to a wrong one — and the reason is pushed to
|
|
360
|
+
* `failures`, because degrading SILENTLY is the failure this whole mechanism
|
|
361
|
+
* exists to move earlier: the consumer's contract quietly reopens and a
|
|
362
|
+
* misspelled field passes `telo check` exactly as it did before.
|
|
363
|
+
*/
|
|
364
|
+
export function resolveSchemaProjections(
|
|
365
|
+
schema: unknown,
|
|
366
|
+
manifest: Record<string, any> | undefined,
|
|
367
|
+
scope: ProjectionScope,
|
|
368
|
+
failures?: ProjectionFailure[],
|
|
369
|
+
): unknown {
|
|
370
|
+
if (Array.isArray(schema)) {
|
|
371
|
+
return schema.map((item) => resolveSchemaProjections(item, manifest, scope, failures));
|
|
372
|
+
}
|
|
373
|
+
if (!isObject(schema)) return schema;
|
|
374
|
+
|
|
375
|
+
const pointer = readProjectionFrom(schema);
|
|
376
|
+
if (pointer && manifest) {
|
|
377
|
+
const target = refTarget(navigate(manifest, pointer), scope, pointer);
|
|
378
|
+
if ("reason" in target) {
|
|
379
|
+
failures?.push(target);
|
|
380
|
+
} else {
|
|
381
|
+
const projection = readSchemaProjection(target.definition);
|
|
382
|
+
const map = projection && projectionKeyMap(target.definition.schema, projection);
|
|
383
|
+
const projected =
|
|
384
|
+
projection && map ? projectEntries(target.manifest, projection, map) : undefined;
|
|
385
|
+
if (projected) {
|
|
386
|
+
const { ["x-telo-schema-projection-from"]: _dropped, ...rest } = schema;
|
|
387
|
+
return { ...rest, ...projected };
|
|
388
|
+
}
|
|
389
|
+
failures?.push({
|
|
390
|
+
reason: "no-projection",
|
|
391
|
+
pointer,
|
|
392
|
+
kind: String(target.manifest.kind ?? "<unknown>"),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return Object.fromEntries(
|
|
398
|
+
Object.entries(schema).map(([key, value]) => [
|
|
399
|
+
key,
|
|
400
|
+
key.startsWith("x-telo-") ? value : resolveSchemaProjections(value, manifest, scope, failures),
|
|
401
|
+
]),
|
|
402
|
+
);
|
|
403
|
+
}
|
package/src/step-slot.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Does this array hold steps, and where does a step keep what the analyzer
|
|
3
|
+
* reads?" — one accessor, the `ref-slot.ts` / `zone-slot.ts` precedent.
|
|
4
|
+
*
|
|
5
|
+
* TWO SPELLINGS, one answer. A kind declares a step body by pointing its items
|
|
6
|
+
* at the shared grammar (`$ref: "telo://manifest#/$defs/Step"`), which fragment
|
|
7
|
+
* expansion stamps `x-telo-fragment: Step`; the field names are then constants
|
|
8
|
+
* of that grammar and nothing states them. Before the fragment existed the shape
|
|
9
|
+
* was each kind's own, so the kind had to say where its dispatch ref, output
|
|
10
|
+
* type and pure-value fields lived — `x-telo-step-context`. That annotation
|
|
11
|
+
* stays READ, permanently: published artifacts carry it and no migration entry
|
|
12
|
+
* can synthesize a `$ref` (the patch vocabulary writes scalars), so it has the
|
|
13
|
+
* standing of the legacy `x-telo-ref` string form.
|
|
14
|
+
*
|
|
15
|
+
* The annotation WINS where both are present. A kind that spells its own shape
|
|
16
|
+
* is describing its own manifest, and a stamp says only which grammar the items
|
|
17
|
+
* point at.
|
|
18
|
+
*
|
|
19
|
+
* Browser-safe: no Node built-ins.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { manifestFragmentOf } from "./manifest-schemas.js";
|
|
23
|
+
|
|
24
|
+
/** The name a step-bearing array's items carry once expanded. */
|
|
25
|
+
export const STEP_FRAGMENT = "Step";
|
|
26
|
+
|
|
27
|
+
/** Where a step keeps what the analyzer reads. */
|
|
28
|
+
export interface StepSlot {
|
|
29
|
+
/** Field on a step naming the resource to dispatch. */
|
|
30
|
+
invoke: string;
|
|
31
|
+
/** Field on the INVOKED resource's manifest that narrows its result type.
|
|
32
|
+
* Absent for a composer that allows no per-instance narrowing. */
|
|
33
|
+
outputType?: string;
|
|
34
|
+
/** Field that produces a result without dispatching. Only a grammar with pure
|
|
35
|
+
* steps has one. */
|
|
36
|
+
value?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The shared grammar's own field names — constants, since the shape is no
|
|
40
|
+
* longer the kind's to choose. */
|
|
41
|
+
const STEP_FRAGMENT_SLOT: StepSlot = { invoke: "invoke", outputType: "outputType", value: "value" };
|
|
42
|
+
|
|
43
|
+
/** The step slot an array property declares, or undefined when it holds no
|
|
44
|
+
* steps. `fieldSchema` is the ARRAY's schema — the stamp sits on its `items`. */
|
|
45
|
+
export function readStepSlot(fieldSchema: unknown): StepSlot | undefined {
|
|
46
|
+
if (!fieldSchema || typeof fieldSchema !== "object") return undefined;
|
|
47
|
+
const declared = (fieldSchema as Record<string, unknown>)["x-telo-step-context"];
|
|
48
|
+
if (declared && typeof declared === "object") {
|
|
49
|
+
const { invoke, outputType, value } = declared as Record<string, unknown>;
|
|
50
|
+
if (typeof invoke !== "string" || invoke.length === 0) return undefined;
|
|
51
|
+
return {
|
|
52
|
+
invoke,
|
|
53
|
+
...(typeof outputType === "string" ? { outputType } : {}),
|
|
54
|
+
...(typeof value === "string" ? { value } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const items = (fieldSchema as Record<string, unknown>).items;
|
|
58
|
+
return manifestFragmentOf(items) === STEP_FRAGMENT ? STEP_FRAGMENT_SLOT : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** True when the property holds a step body. */
|
|
62
|
+
export function isStepSlot(fieldSchema: unknown): boolean {
|
|
63
|
+
return readStepSlot(fieldSchema) !== undefined;
|
|
64
|
+
}
|
package/src/telo-version.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
parseCanonicalTypeSchemaId,
|
|
7
7
|
} from "@telorun/sdk";
|
|
8
8
|
import { KERNEL_BUILTINS } from "./builtins.js";
|
|
9
|
+
import { isStepSlot } from "./step-slot.js";
|
|
10
|
+
import { withRefSlotsAsReadings } from "./ref-slot-reading.js";
|
|
9
11
|
|
|
10
12
|
export interface ContextResolveOpts {
|
|
11
13
|
/** When provided, used to resolve `x-telo-context-from-root` annotations against the
|
|
@@ -432,7 +434,17 @@ export function resolveContextAnnotations(
|
|
|
432
434
|
// so the variable is typed by the CONTRACT rather than by the wrapper
|
|
433
435
|
// around it. A raw JSON Schema resolves to itself, and a plain property
|
|
434
436
|
// map (a transport scope) resolves to nothing and is used verbatim.
|
|
435
|
-
|
|
437
|
+
const resolved = resolveTypeFieldToSchema(navigated, allManifests ?? []) ?? navigated;
|
|
438
|
+
// A ref slot inside it holds a live instance at runtime, which the
|
|
439
|
+
// kernel substitutes with that resource's published reading before
|
|
440
|
+
// evaluating (`celSelfView`). Type it the same way, or `self.<ref>` is
|
|
441
|
+
// the annotation node — a shape nothing can be read off.
|
|
442
|
+
return defs
|
|
443
|
+
? (withRefSlotsAsReadings(resolved, {
|
|
444
|
+
resolve: (kind) => defs.resolve(kind),
|
|
445
|
+
resolveKind: (kind) => aliases?.resolveKind(kind),
|
|
446
|
+
}) as Record<string, any>)
|
|
447
|
+
: resolved;
|
|
436
448
|
}
|
|
437
449
|
}
|
|
438
450
|
if (defs) {
|
|
@@ -568,7 +580,9 @@ export function extractContextsFromSchema(
|
|
|
568
580
|
}
|
|
569
581
|
|
|
570
582
|
/** Schema keys that declare a CEL-bearing region: a field carrying any of these
|
|
571
|
-
* is evaluated at runtime, so a `!cel` inside it (or a descendant) is live.
|
|
583
|
+
* is evaluated at runtime, so a `!cel` inside it (or a descendant) is live. A
|
|
584
|
+
* STEP BODY is one too, and says so through the grammar its items point at
|
|
585
|
+
* rather than through a key — {@link isStepSlot} reads either spelling. */
|
|
572
586
|
const CEL_REGION_KEYS = [
|
|
573
587
|
"x-telo-context",
|
|
574
588
|
"x-telo-step-context",
|
|
@@ -587,7 +601,7 @@ export function extractCelRegionScopes(schema: Record<string, any>, path = "$"):
|
|
|
587
601
|
if (!schema || typeof schema !== "object") return [];
|
|
588
602
|
const out: string[] = [];
|
|
589
603
|
|
|
590
|
-
if (CEL_REGION_KEYS.some((k) => schema[k])) out.push(path);
|
|
604
|
+
if (CEL_REGION_KEYS.some((k) => schema[k]) || isStepSlot(schema)) out.push(path);
|
|
591
605
|
|
|
592
606
|
if (schema.properties) {
|
|
593
607
|
for (const [key, value] of Object.entries(schema.properties as Record<string, any>)) {
|