@telorun/analyzer 0.73.0 → 0.74.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/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +26 -7
- package/dist/artifact-axes.d.ts +14 -0
- package/dist/artifact-axes.d.ts.map +1 -0
- package/dist/artifact-axes.js +12 -0
- package/dist/artifact-layer-index.d.ts +2 -2
- package/dist/artifact-layer-index.d.ts.map +1 -1
- package/dist/artifact-layer-index.js +9 -4
- package/dist/artifact-selector.d.ts +47 -21
- package/dist/artifact-selector.d.ts.map +1 -1
- package/dist/artifact-selector.js +85 -16
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +73 -16
- package/dist/call-graph.d.ts +12 -1
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +22 -0
- package/dist/catch-scope.js +1 -1
- package/dist/forwarded-declaration.d.ts +26 -0
- package/dist/forwarded-declaration.d.ts.map +1 -0
- package/dist/forwarded-declaration.js +31 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +21 -5
- package/dist/module-alias-scope.d.ts +11 -0
- package/dist/module-alias-scope.d.ts.map +1 -1
- package/dist/module-alias-scope.js +10 -0
- package/dist/module-file-claims.d.ts +17 -1
- package/dist/module-file-claims.d.ts.map +1 -1
- package/dist/module-file-claims.js +38 -3
- package/dist/module-named-files.d.ts +72 -0
- package/dist/module-named-files.d.ts.map +1 -0
- package/dist/module-named-files.js +115 -0
- package/dist/native-entries.d.ts +83 -0
- package/dist/native-entries.d.ts.map +1 -0
- package/dist/native-entries.js +169 -0
- package/dist/normalize-inline-resources.d.ts +41 -13
- package/dist/normalize-inline-resources.d.ts.map +1 -1
- package/dist/normalize-inline-resources.js +236 -54
- package/dist/reference-field-map.d.ts +59 -1
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +200 -34
- package/dist/release/payload-digest.d.ts +2 -1
- package/dist/release/payload-digest.d.ts.map +1 -1
- package/dist/release/payload-digest.js +2 -1
- package/dist/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +8 -5
- package/dist/resolve-throws-union.d.ts +25 -4
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +62 -32
- package/dist/resolve-zone-requirements.d.ts.map +1 -1
- package/dist/resolve-zone-requirements.js +2 -1
- package/dist/rewrite-synthetic-origins.d.ts +6 -1
- package/dist/rewrite-synthetic-origins.d.ts.map +1 -1
- package/dist/rewrite-synthetic-origins.js +107 -13
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +18 -5
- package/dist/schema-walk.d.ts +47 -13
- package/dist/schema-walk.d.ts.map +1 -1
- package/dist/schema-walk.js +150 -30
- package/dist/scope-declarations.d.ts +59 -0
- package/dist/scope-declarations.d.ts.map +1 -0
- package/dist/scope-declarations.js +45 -0
- package/dist/source-entries.d.ts +119 -0
- package/dist/source-entries.d.ts.map +1 -0
- package/dist/source-entries.js +349 -0
- package/dist/step-bodies.d.ts +24 -0
- package/dist/step-bodies.d.ts.map +1 -0
- package/dist/step-bodies.js +28 -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 +3 -2
- package/dist/validate-module-artifact.d.ts +4 -2
- package/dist/validate-module-artifact.d.ts.map +1 -1
- package/dist/validate-module-artifact.js +43 -4
- package/dist/validate-native-entries.d.ts +15 -0
- package/dist/validate-native-entries.d.ts.map +1 -0
- package/dist/validate-native-entries.js +103 -0
- package/dist/validate-observed-state.d.ts +7 -1
- package/dist/validate-observed-state.d.ts.map +1 -1
- package/dist/validate-observed-state.js +24 -6
- package/dist/validate-reference-forms.d.ts.map +1 -1
- package/dist/validate-reference-forms.js +2 -2
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +11 -5
- package/dist/validate-scope-reach.d.ts +31 -0
- package/dist/validate-scope-reach.d.ts.map +1 -0
- package/dist/validate-scope-reach.js +180 -0
- package/dist/validate-source-entries.d.ts +16 -0
- package/dist/validate-source-entries.d.ts.map +1 -0
- package/dist/validate-source-entries.js +90 -0
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +21 -39
- package/dist/value-type-keyword.d.ts +22 -5
- package/dist/value-type-keyword.d.ts.map +1 -1
- package/dist/value-type-keyword.js +16 -7
- package/package.json +3 -3
- package/src/analyzer.ts +33 -9
- package/src/artifact-axes.ts +24 -0
- package/src/artifact-layer-index.ts +9 -6
- package/src/artifact-selector.ts +110 -35
- package/src/builtins.ts +75 -16
- package/src/call-graph.ts +30 -0
- package/src/catch-scope.ts +1 -1
- package/src/forwarded-declaration.ts +32 -0
- package/src/index.ts +36 -1
- package/src/kernel-globals.ts +28 -5
- package/src/migrations/CLAUDE.md +35 -0
- package/src/module-alias-scope.ts +17 -0
- package/src/module-file-claims.ts +63 -4
- package/src/module-named-files.ts +162 -0
- package/src/native-entries.ts +227 -0
- package/src/normalize-inline-resources.ts +326 -70
- package/src/reference-field-map.ts +257 -52
- package/src/release/CLAUDE.md +32 -0
- package/src/release/payload-digest.ts +2 -1
- package/src/resolve-ref-sentinels.ts +7 -6
- package/src/resolve-throws-union.ts +87 -32
- package/src/resolve-zone-requirements.ts +2 -1
- package/src/rewrite-synthetic-origins.ts +107 -16
- package/src/schema-compat.ts +18 -4
- package/src/schema-walk.ts +162 -34
- package/src/scope-declarations.ts +98 -0
- package/src/source-entries.ts +502 -0
- package/src/step-bodies.ts +55 -0
- package/src/telo-version.ts +1 -1
- package/src/validate-cel-context.ts +3 -2
- package/src/validate-module-artifact.ts +46 -3
- package/src/validate-native-entries.ts +147 -0
- package/src/validate-observed-state.ts +36 -7
- package/src/validate-reference-forms.ts +2 -3
- package/src/validate-references.ts +10 -6
- package/src/validate-scope-reach.ts +213 -0
- package/src/validate-source-entries.ts +120 -0
- package/src/validate-throws-coverage.ts +24 -31
- package/src/value-type-keyword.ts +35 -9
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import { isTaggedSentinel } from "@telorun/templating";
|
|
3
3
|
import { scopeResolverForModule, type AliasResolver } from "./alias-resolver.js";
|
|
4
|
-
import { resolveScopedName } from "./call-graph.js";
|
|
4
|
+
import { resolveScopedName, resolveSlotUseAt } from "./call-graph.js";
|
|
5
5
|
import { refSentinelTarget, type RefSentinelTarget } from "./ref-sentinel-target.js";
|
|
6
6
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
hasDeclaredUse,
|
|
9
|
+
possibleUses,
|
|
10
|
+
transfersControl,
|
|
11
|
+
type RefSlot,
|
|
12
|
+
type RefUse,
|
|
13
|
+
} from "./ref-slot.js";
|
|
9
14
|
import { forEachDrivenSlot } from "./schema-walk.js";
|
|
15
|
+
import type { StepSlot } from "./step-slot.js";
|
|
10
16
|
|
|
11
17
|
export interface ThrowsCodeMeta {
|
|
12
18
|
data?: Record<string, any>;
|
|
@@ -207,10 +213,10 @@ function codesFromDefinition(definition: ResourceDefinition): Map<string, Throws
|
|
|
207
213
|
}
|
|
208
214
|
|
|
209
215
|
/** Resolve the effective throw union for a named manifest. The result combines
|
|
210
|
-
* explicit `throws.codes`, `throws.inherit: true` dataflow (step
|
|
211
|
-
*
|
|
212
|
-
* unresolvable passthrough call sites. Cycles
|
|
213
|
-
* result so resolution always terminates. */
|
|
216
|
+
* explicit `throws.codes`, `throws.inherit: true` dataflow (step bodies with
|
|
217
|
+
* try/catch subtraction, plus reference slots that hand a failure back), and
|
|
218
|
+
* unbounded markers for unresolvable passthrough call sites. Cycles
|
|
219
|
+
* short-circuit to an empty result so resolution always terminates. */
|
|
214
220
|
export function resolveThrowsUnion(
|
|
215
221
|
manifest: ResourceManifest,
|
|
216
222
|
ctx: ResolveCtx,
|
|
@@ -266,14 +272,42 @@ export function resolveThrowsUnion(
|
|
|
266
272
|
}
|
|
267
273
|
|
|
268
274
|
/**
|
|
269
|
-
*
|
|
275
|
+
* True for a use through which a target's failure reaches the declaring
|
|
276
|
+
* resource's CALLER: `call` returns into my invocation, and `trigger.consumer`
|
|
277
|
+
* rejects the value my caller drains. `detached` and `trigger.inbound` run where
|
|
278
|
+
* no caller of mine awaits them; `dependency` and `schema` dispatch nothing.
|
|
270
279
|
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
280
|
+
* The reduction `inherit` is read through — by the resolver and by the check
|
|
281
|
+
* that a definition declaring it has something to inherit from.
|
|
282
|
+
*/
|
|
283
|
+
export function handsFailureBack(use: RefUse): boolean {
|
|
284
|
+
return use === "call" || use === "trigger.consumer";
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The uses a throws consumer reads off a slot. A slot that declares no use —
|
|
289
|
+
* the legacy bare-string form — reads as `call`: the throws union must assume
|
|
290
|
+
* `call` to keep an error path, the direction the call graph takes for the same
|
|
291
|
+
* slot. The zone projection reads it the opposite way, deliberately.
|
|
292
|
+
*
|
|
293
|
+
* `uses` defaults to every use the slot can take; a consumer that resolved the
|
|
294
|
+
* slot's case map for one instance passes that answer instead.
|
|
295
|
+
*/
|
|
296
|
+
export function throwsUses(
|
|
297
|
+
slot: RefSlot,
|
|
298
|
+
uses: readonly RefUse[] = possibleUses(slot),
|
|
299
|
+
): readonly RefUse[] {
|
|
300
|
+
return hasDeclaredUse(slot) ? uses : ["call"];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* `throws.inherit: true` — the union of what the resource dispatches: its step
|
|
305
|
+
* bodies (try/catch subtraction included) and every reference slot whose use,
|
|
306
|
+
* resolved for THIS instance, hands a failure back ({@link handsFailureBack}).
|
|
307
|
+
*
|
|
308
|
+
* Only on a kind that DECLARES `inherit`: it is a claim that the kind lets what
|
|
309
|
+
* it dispatches escape, and a kind catching a slot internally does not make it.
|
|
310
|
+
* What a CATCH SCOPE encloses is a different question, answered below.
|
|
277
311
|
*/
|
|
278
312
|
function resolveInherited(
|
|
279
313
|
manifest: ResourceManifest,
|
|
@@ -282,16 +316,23 @@ function resolveInherited(
|
|
|
282
316
|
ownerModule: string | undefined,
|
|
283
317
|
): ThrowsUnion {
|
|
284
318
|
const result: ThrowsUnion = { codes: new Map(), unbounded: false };
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
319
|
+
const schema = definition.schema as Record<string, any> | undefined;
|
|
320
|
+
|
|
321
|
+
forEachDrivenSlot(schema, manifest, (driven) => {
|
|
322
|
+
if (driven.kind === "step") {
|
|
323
|
+
unionInto(result, stepSiteThrows(driven.data, driven.slots, ctx, ownerModule));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
// Several slots at one site are the branches declaring it; any one that
|
|
327
|
+
// hands a failure back counts, since a branch not applying can only add codes.
|
|
328
|
+
const handsBack = driven.slots.some(({ slot, fieldPath }) =>
|
|
329
|
+
throwsUses(
|
|
330
|
+
slot,
|
|
331
|
+
resolveSlotUseAt(slot, manifest, schema, driven.path, fieldPath).use,
|
|
332
|
+
).some(handsFailureBack),
|
|
333
|
+
);
|
|
334
|
+
if (handsBack) unionInto(result, resolveRefTargetThrows(driven.data, ctx, ownerModule));
|
|
335
|
+
});
|
|
295
336
|
|
|
296
337
|
return result;
|
|
297
338
|
}
|
|
@@ -327,13 +368,15 @@ export function resolveScopeUnion(
|
|
|
327
368
|
|
|
328
369
|
forEachDrivenSlot(definition.schema, manifest, (driven) => {
|
|
329
370
|
if (driven.kind === "step") {
|
|
330
|
-
unionInto(
|
|
331
|
-
result,
|
|
332
|
-
collectStepArrayThrows(driven.data, driven.slot.invoke, undefined, ctx, ownerModule),
|
|
333
|
-
);
|
|
371
|
+
unionInto(result, stepSiteThrows(driven.data, driven.slots, ctx, ownerModule));
|
|
334
372
|
return;
|
|
335
373
|
}
|
|
336
|
-
if (
|
|
374
|
+
if (
|
|
375
|
+
driven.slots.some(({ slot }) => !slot.throwsThrough && throwsUses(slot).some(transfersControl))
|
|
376
|
+
) {
|
|
377
|
+
unionInto(result, resolveRefTargetThrows(driven.data, ctx, ownerModule));
|
|
378
|
+
}
|
|
379
|
+
if (driven.slots.some(({ slot }) => slot.throwsThrough)) {
|
|
337
380
|
const target = resolveRefManifest(driven.data, ctx, ownerModule);
|
|
338
381
|
const targetDef = target
|
|
339
382
|
? definitionFor(
|
|
@@ -347,15 +390,27 @@ export function resolveScopeUnion(
|
|
|
347
390
|
// A target that cannot be resolved says nothing about what it throws, so
|
|
348
391
|
// the scope's union is no longer enumerable.
|
|
349
392
|
else result.unbounded = true;
|
|
350
|
-
return;
|
|
351
393
|
}
|
|
352
|
-
if (!possibleUses(driven.slot).some(transfersControl)) return;
|
|
353
|
-
unionInto(result, resolveRefTargetThrows(driven.data, ctx, ownerModule));
|
|
354
394
|
});
|
|
355
395
|
|
|
356
396
|
return result;
|
|
357
397
|
}
|
|
358
398
|
|
|
399
|
+
/** The union a step-body site reaches — once per distinct invoke field, since
|
|
400
|
+
* several branches may declare a body at one site. */
|
|
401
|
+
function stepSiteThrows(
|
|
402
|
+
steps: unknown[],
|
|
403
|
+
slots: StepSlot[],
|
|
404
|
+
ctx: ResolveCtx,
|
|
405
|
+
ownerModule: string | undefined,
|
|
406
|
+
): ThrowsUnion {
|
|
407
|
+
const result = emptyUnion();
|
|
408
|
+
for (const invoke of new Set(slots.map((slot) => slot.invoke))) {
|
|
409
|
+
unionInto(result, collectStepArrayThrows(steps, invoke, undefined, ctx, ownerModule));
|
|
410
|
+
}
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
|
|
359
414
|
function collectStepArrayThrows(
|
|
360
415
|
steps: unknown[],
|
|
361
416
|
invokeField: string,
|
|
@@ -32,6 +32,7 @@ import type { ModuleDocuments } from "./module-documents.js";
|
|
|
32
32
|
import { readProvidesZone, readRequiresZone } from "./zone-slot.js";
|
|
33
33
|
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
34
34
|
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
35
|
+
import { isForwardedDeclaration } from "./forwarded-declaration.js";
|
|
35
36
|
|
|
36
37
|
const SOURCE = "telo-analyzer";
|
|
37
38
|
|
|
@@ -616,7 +617,7 @@ export function projectZoneRequirements(args: ProjectionArgs): ProjectionResult
|
|
|
616
617
|
// (with the internal graph in hand) and seeded below — deriving them here
|
|
617
618
|
// against the flattened view would resolve correlation against a graph
|
|
618
619
|
// that no longer holds the library's internals.
|
|
619
|
-
if (
|
|
620
|
+
if (isForwardedDeclaration(node.manifest)) continue;
|
|
620
621
|
const def = resolveDef(node.kind, meta?.module);
|
|
621
622
|
const schema = def?.schema as Record<string, any> | undefined;
|
|
622
623
|
if (!def || !schema) continue;
|
|
@@ -7,9 +7,9 @@ interface XTeloOrigin {
|
|
|
7
7
|
pathFromParent: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
function readOrigin(manifest:
|
|
11
|
-
if (!manifest) return undefined;
|
|
12
|
-
const origin = (manifest
|
|
10
|
+
function readOrigin(manifest: unknown): XTeloOrigin | undefined {
|
|
11
|
+
if (!manifest || typeof manifest !== "object") return undefined;
|
|
12
|
+
const origin = (manifest as { metadata?: { xTeloOrigin?: XTeloOrigin } }).metadata?.xTeloOrigin;
|
|
13
13
|
if (
|
|
14
14
|
!origin ||
|
|
15
15
|
typeof origin.parentKind !== "string" ||
|
|
@@ -26,7 +26,12 @@ function readOrigin(manifest: ResourceManifest | undefined): XTeloOrigin | undef
|
|
|
26
26
|
* `data.resource`, which has no YAML source. Rewrite each such diagnostic
|
|
27
27
|
* back to the chain root: walk up `metadata.xTeloOrigin` until a manifest
|
|
28
28
|
* with no origin is reached, and prepend each hop's `pathFromParent` to
|
|
29
|
-
* `data.path` so position-index lookups against the root doc resolve.
|
|
29
|
+
* `data.path` so position-index lookups against the root doc resolve.
|
|
30
|
+
*
|
|
31
|
+
* An extraction from a scope member lives INSIDE the scope array rather than at
|
|
32
|
+
* the top level, so a diagnostic about it names the top-level owner and a path
|
|
33
|
+
* through an array index the author never wrote (`with[3]`). That prefix is
|
|
34
|
+
* unfolded the same way, into the position the declaration was written at. */
|
|
30
35
|
export function rewriteSyntheticOrigins(
|
|
31
36
|
diagnostics: AnalysisDiagnostic[],
|
|
32
37
|
manifests: ResourceManifest[],
|
|
@@ -45,31 +50,117 @@ export function rewriteSyntheticOrigins(
|
|
|
45
50
|
|
|
46
51
|
let current = byName.get(data.resource.name);
|
|
47
52
|
let origin = readOrigin(current);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let
|
|
51
|
-
let rootKind: string = origin.parentKind;
|
|
52
|
-
let rootName: string = origin.parentName;
|
|
53
|
+
const path = typeof data.path === "string" ? data.path : "";
|
|
54
|
+
let accumPath = path;
|
|
55
|
+
let root: { kind?: string; name?: string } = data.resource;
|
|
53
56
|
|
|
54
57
|
while (origin) {
|
|
55
58
|
accumPath = accumPath ? `${origin.pathFromParent}.${accumPath}` : origin.pathFromParent;
|
|
56
|
-
|
|
57
|
-
rootName = origin.parentName;
|
|
59
|
+
root = { kind: origin.parentKind, name: origin.parentName };
|
|
58
60
|
current = byName.get(origin.parentName);
|
|
59
61
|
origin = readOrigin(current);
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
+
const rerouted = root !== data.resource;
|
|
65
|
+
const unfolded = current ? unfoldScopedOrigins(current, accumPath) : accumPath;
|
|
66
|
+
if (!rerouted && unfolded === path) return d;
|
|
64
67
|
|
|
65
68
|
return {
|
|
66
69
|
...d,
|
|
67
70
|
data: {
|
|
68
71
|
...data,
|
|
69
|
-
resource:
|
|
70
|
-
filePath:
|
|
71
|
-
|
|
72
|
+
resource: root,
|
|
73
|
+
filePath: rerouted
|
|
74
|
+
? ((current?.metadata as { source?: string } | undefined)?.source ?? data.filePath)
|
|
75
|
+
: data.filePath,
|
|
76
|
+
path: unfolded,
|
|
72
77
|
},
|
|
73
78
|
};
|
|
74
79
|
});
|
|
75
80
|
}
|
|
81
|
+
|
|
82
|
+
type Segment = string | number;
|
|
83
|
+
|
|
84
|
+
/** Replace every scope-array index that holds an extraction with the position
|
|
85
|
+
* the extraction was written at, until none is left. */
|
|
86
|
+
function unfoldScopedOrigins(root: ResourceManifest, path: string): string {
|
|
87
|
+
let segments = parseSegments(path);
|
|
88
|
+
let unfoldedAny = false;
|
|
89
|
+
// Each unfolding replaces one extraction with its parent, which is strictly
|
|
90
|
+
// closer to what the author wrote, so the chain is as long as the nesting.
|
|
91
|
+
for (let hops = 0; hops <= segments.length + 16; hops++) {
|
|
92
|
+
const next = unfoldOnce(root, segments);
|
|
93
|
+
if (!next) break;
|
|
94
|
+
segments = next;
|
|
95
|
+
unfoldedAny = true;
|
|
96
|
+
}
|
|
97
|
+
// Re-formatting is lossy (an empty segment in `entries../x` is dropped), so a
|
|
98
|
+
// path nothing unfolded is returned as written.
|
|
99
|
+
return unfoldedAny ? formatSegments(segments) : path;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function unfoldOnce(root: ResourceManifest, segments: Segment[]): Segment[] | undefined {
|
|
103
|
+
let node: unknown = root;
|
|
104
|
+
for (let i = 0; i < segments.length; i++) {
|
|
105
|
+
const segment = segments[i];
|
|
106
|
+
const container = node;
|
|
107
|
+
node =
|
|
108
|
+
typeof segment === "number"
|
|
109
|
+
? Array.isArray(container)
|
|
110
|
+
? container[segment]
|
|
111
|
+
: undefined
|
|
112
|
+
: container && typeof container === "object" && !Array.isArray(container)
|
|
113
|
+
? (container as Record<string, unknown>)[segment]
|
|
114
|
+
: undefined;
|
|
115
|
+
if (node === undefined || node === null) return undefined;
|
|
116
|
+
if (typeof segment !== "number" || i === 0) continue;
|
|
117
|
+
const origin = readOrigin(node);
|
|
118
|
+
if (!origin) continue;
|
|
119
|
+
|
|
120
|
+
// Its parent is either the resource holding this scope array (a slot of
|
|
121
|
+
// the resource that declares the scope) or a sibling member of the array.
|
|
122
|
+
const holderPath = segments.slice(0, i - 1);
|
|
123
|
+
let parentPath: Segment[] | undefined;
|
|
124
|
+
if (nameOf(at(root, holderPath)) === origin.parentName) {
|
|
125
|
+
parentPath = holderPath;
|
|
126
|
+
} else {
|
|
127
|
+
const sibling = (container as unknown[]).findIndex((m) => nameOf(m) === origin.parentName);
|
|
128
|
+
if (sibling >= 0) parentPath = [...segments.slice(0, i), sibling];
|
|
129
|
+
}
|
|
130
|
+
if (!parentPath) return undefined;
|
|
131
|
+
return [...parentPath, ...parseSegments(origin.pathFromParent), ...segments.slice(i + 1)];
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function at(root: unknown, segments: Segment[]): unknown {
|
|
137
|
+
let node = root;
|
|
138
|
+
for (const segment of segments) {
|
|
139
|
+
if (!node || typeof node !== "object") return undefined;
|
|
140
|
+
node = (node as Record<string | number, unknown>)[segment];
|
|
141
|
+
}
|
|
142
|
+
return node;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function nameOf(value: unknown): string | undefined {
|
|
146
|
+
const name = (value as { metadata?: { name?: unknown } } | undefined)?.metadata?.name;
|
|
147
|
+
return typeof name === "string" ? name : undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** `with[3].steps[0].invoke` → `["with", 3, "steps", 0, "invoke"]`. */
|
|
151
|
+
function parseSegments(path: string): Segment[] {
|
|
152
|
+
const out: Segment[] = [];
|
|
153
|
+
for (const match of path.matchAll(/\[(\d+)\]|([^.[\]]+)/g)) {
|
|
154
|
+
out.push(match[1] !== undefined ? Number(match[1]) : match[2]!);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function formatSegments(segments: Segment[]): string {
|
|
160
|
+
let out = "";
|
|
161
|
+
for (const segment of segments) {
|
|
162
|
+
if (typeof segment === "number") out += `[${segment}]`;
|
|
163
|
+
else out += out ? `.${segment}` : segment;
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
package/src/schema-compat.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
celTypeOfValueType,
|
|
11
11
|
isCompiledValue,
|
|
12
12
|
readValueTypeSlot,
|
|
13
|
+
VALUE_TYPE_BINDINGS,
|
|
13
14
|
valueBrandBases,
|
|
14
15
|
valueTypeOf,
|
|
15
16
|
valueTypePlaceholder,
|
|
@@ -36,8 +37,9 @@ export function createAjv(): InstanceType<typeof Ajv> {
|
|
|
36
37
|
// One registration site for every Telo keyword — the annotations as no-ops and
|
|
37
38
|
// `x-telo-type` as the one that checks. Registered here and in the kernel's
|
|
38
39
|
// validators from one definition, so a literal at an instance-typed slot is
|
|
39
|
-
// rejected statically and at dispatch by the identical rule.
|
|
40
|
-
|
|
40
|
+
// rejected statically and at dispatch by the identical rule. This instance is
|
|
41
|
+
// static analysis alone, so it also asserts a `live` type: no literal can be one.
|
|
42
|
+
registerTeloKeywords(instance, { assertLive: true });
|
|
41
43
|
instance.addSchema(ManifestRootSchema);
|
|
42
44
|
return instance;
|
|
43
45
|
}
|
|
@@ -508,6 +510,18 @@ function foldedConstraints(schema: Record<string, any>): Record<string, any> {
|
|
|
508
510
|
return out;
|
|
509
511
|
}
|
|
510
512
|
|
|
513
|
+
/** The stand-in for a CEL leaf at a `live` slot. Static analysis asserts a live
|
|
514
|
+
* type like any instance type (a literal can never be one), so the leaf's
|
|
515
|
+
* stand-in has to BE one. A live binding declares no factory, and building an
|
|
516
|
+
* instance would mean knowing its constructor's signature; an object on the
|
|
517
|
+
* constructor's prototype satisfies `instanceof` and is never used as one. */
|
|
518
|
+
function liveValuePlaceholder(schema: Record<string, any>): unknown | undefined {
|
|
519
|
+
const entry = readValueTypeSlot(schema)?.entry;
|
|
520
|
+
if (!entry?.live || entry.representation !== "instance") return undefined;
|
|
521
|
+
const binding = VALUE_TYPE_BINDINGS[entry.binding!];
|
|
522
|
+
return binding ? Object.create(binding.constructor.prototype) : undefined;
|
|
523
|
+
}
|
|
524
|
+
|
|
511
525
|
export function celPlaceholderForSchema(rawSchema: Record<string, any>): unknown {
|
|
512
526
|
const schema = foldedConstraints(rawSchema);
|
|
513
527
|
// An instance-typed slot's placeholder must BE an instance: the same keyword
|
|
@@ -516,8 +530,8 @@ export function celPlaceholderForSchema(rawSchema: Record<string, any>): unknown
|
|
|
516
530
|
// rejected because no YAML literal is a byte buffer, while a value arriving by
|
|
517
531
|
// reference passes. The stand-in comes from the binding table, so a new
|
|
518
532
|
// instance type brings its own rather than adding a branch here; a `live` type
|
|
519
|
-
// declares none, because nothing validates it.
|
|
520
|
-
const placeholder = valueTypePlaceholder(schema);
|
|
533
|
+
// declares none, because nothing validates it at dispatch.
|
|
534
|
+
const placeholder = valueTypePlaceholder(schema) ?? liveValuePlaceholder(schema);
|
|
521
535
|
if (placeholder !== undefined) return placeholder;
|
|
522
536
|
if (schema.default !== undefined) return schema.default;
|
|
523
537
|
// An enum-constrained field needs a placeholder drawn from the enum: the
|
package/src/schema-walk.ts
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* scope rule is consumed by the IDE, which must not pull the pass in behind it.
|
|
10
10
|
*/
|
|
11
11
|
import { MANIFEST_SCHEMA_URI, ManifestRootSchema } from "./manifest-schemas.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import type { RefSlot } from "./ref-slot.js";
|
|
13
|
+
import type { StepSlot } from "./step-slot.js";
|
|
14
|
+
import { buildDrivenSlotMap, refSlotOfEntry, type DrivenSlots } from "./reference-field-map.js";
|
|
14
15
|
|
|
15
16
|
/** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
|
|
16
17
|
* Non-refs and unresolved refs pass through unchanged. */
|
|
@@ -144,57 +145,184 @@ export function walkStepArray(
|
|
|
144
145
|
}
|
|
145
146
|
});
|
|
146
147
|
}
|
|
147
|
-
/** A slot through which
|
|
148
|
+
/** A slot a kind's schema declares through which its resources drive another.
|
|
149
|
+
* `path` is the field-map form (`routes[].handler`, `content.{}.encoder`);
|
|
150
|
+
* `slots` holds every slot any branch declares there. */
|
|
151
|
+
export type DeclaredSlot =
|
|
152
|
+
| { kind: "step"; slots: StepSlot[]; path: string }
|
|
153
|
+
| { kind: "ref"; slots: RefSlot[]; path: string };
|
|
154
|
+
|
|
155
|
+
/** A reference slot at a site, with the declaration it came from — which is
|
|
156
|
+
* what a case-map selector's schema default is read against. */
|
|
157
|
+
export interface DrivenRef {
|
|
158
|
+
slot: RefSlot;
|
|
159
|
+
fieldPath: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The slots at one concrete site of one resource (`routes[0].handler`): every
|
|
163
|
+
* slot any branch or recursion route declares there. A consumer reads them as
|
|
164
|
+
* a union — a branch that does not apply to this resource can only add what
|
|
165
|
+
* counts, never remove it. */
|
|
148
166
|
export type DrivenSlot =
|
|
149
|
-
| { kind: "step";
|
|
150
|
-
| { kind: "ref";
|
|
167
|
+
| { kind: "step"; slots: StepSlot[]; data: unknown[]; path: string }
|
|
168
|
+
| { kind: "ref"; slots: DrivenRef[]; data: unknown; path: string };
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Every slot a kind's schema declares through which its resources drive
|
|
172
|
+
* another — the schema-only mode of {@link forEachDrivenSlot}, read off the
|
|
173
|
+
* same {@link buildDrivenSlotMap}.
|
|
174
|
+
*/
|
|
175
|
+
export function forEachDeclaredSlot(schema: unknown, visit: (slot: DeclaredSlot) => void): void {
|
|
176
|
+
if (!schema || typeof schema !== "object") return;
|
|
177
|
+
for (const [path, at] of buildDrivenSlotMap(schema as Record<string, any>).paths) {
|
|
178
|
+
if (at.steps.length > 0) visit({ kind: "step", slots: at.steps, path });
|
|
179
|
+
if (at.refs.length > 0) visit({ kind: "ref", slots: at.refs.map(refSlotOfEntry), path });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
151
182
|
|
|
152
183
|
/**
|
|
153
184
|
* Every slot of one resource through which it drives another, schema and data in
|
|
154
185
|
* tandem.
|
|
155
186
|
*
|
|
156
|
-
* One traversal
|
|
157
|
-
* scope enclosure
|
|
158
|
-
* structural question and two copies would eventually
|
|
159
|
-
* walk stops.
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
187
|
+
* One traversal for every throws question — a kind's `inherit` union, a scope
|
|
188
|
+
* list's denominator, catch-scope enclosure, and whether `inherit` is legal —
|
|
189
|
+
* because they ask the same structural question and two copies would eventually
|
|
190
|
+
* disagree about where the walk stops. The slots are the driven-slot map's, so
|
|
191
|
+
* the reach is the reference field map's plus local `$ref`; a step slot is a
|
|
192
|
+
* stop (that traversal owns everything below it, `try`/`catch` subtraction
|
|
193
|
+
* included) and so is a reference slot (a resolved ref is a leaf).
|
|
194
|
+
*
|
|
195
|
+
* Each concrete site is visited once, with every slot that reaches it. A
|
|
196
|
+
* map-value (`additionalProperties`) slot applies only to keys its schema does
|
|
197
|
+
* not declare, and never to the resource envelope at the root, as JSON Schema
|
|
198
|
+
* applies it. A recursive schema's back-edge is followed as deep as the data
|
|
199
|
+
* goes, guarded against data that aliases one of its own ancestors. A schema
|
|
200
|
+
* whose map holds no step or reference slot is not walked at all.
|
|
163
201
|
*/
|
|
164
202
|
export function forEachDrivenSlot(
|
|
165
203
|
schema: unknown,
|
|
166
204
|
data: unknown,
|
|
167
205
|
visit: (slot: DrivenSlot) => void,
|
|
168
|
-
path = "",
|
|
169
206
|
): void {
|
|
170
207
|
if (!schema || typeof schema !== "object" || data === undefined || data === null) return;
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
208
|
+
const driven = buildDrivenSlotMap(schema as Record<string, any>);
|
|
209
|
+
if (!driven.drives) return;
|
|
210
|
+
const sites = new Map<
|
|
211
|
+
string,
|
|
212
|
+
{ data: unknown; steps: StepSlot[]; refs: DrivenRef[]; from: Set<object> }
|
|
213
|
+
>();
|
|
214
|
+
const onData = new Set<unknown>([data]);
|
|
178
215
|
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
216
|
+
const walk = (prefix: string, value: unknown, base: string): void => {
|
|
217
|
+
for (const [fieldPath, at] of driven.paths) {
|
|
218
|
+
const rel = relativeFieldPath(fieldPath, prefix);
|
|
219
|
+
if (rel === undefined) continue;
|
|
220
|
+
for (const found of resolveSites(value, rel, prefix, driven)) {
|
|
221
|
+
const path = joinConcrete(base, found.path);
|
|
222
|
+
if (at.steps.length > 0 || at.refs.length > 0) {
|
|
223
|
+
let site = sites.get(path);
|
|
224
|
+
if (!site) {
|
|
225
|
+
site = { data: found.value, steps: [], refs: [], from: new Set() };
|
|
226
|
+
sites.set(path, site);
|
|
227
|
+
}
|
|
228
|
+
for (const step of at.steps) {
|
|
229
|
+
if (site.from.has(step)) continue;
|
|
230
|
+
site.from.add(step);
|
|
231
|
+
site.steps.push(step);
|
|
232
|
+
}
|
|
233
|
+
for (const entry of at.refs) {
|
|
234
|
+
if (site.from.has(entry)) continue;
|
|
235
|
+
site.from.add(entry);
|
|
236
|
+
site.refs.push({ slot: refSlotOfEntry(entry), fieldPath });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (at.recurse.length === 0) continue;
|
|
240
|
+
if (!found.value || typeof found.value !== "object" || onData.has(found.value)) continue;
|
|
241
|
+
onData.add(found.value);
|
|
242
|
+
for (const to of at.recurse) walk(to, found.value, path);
|
|
243
|
+
onData.delete(found.value);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
walk("", data, "");
|
|
184
248
|
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
forEachDrivenSlot(propSchema, obj[key], visit, path ? `${path}.${key}` : key);
|
|
249
|
+
for (const [path, site] of sites) {
|
|
250
|
+
if (site.steps.length > 0 && Array.isArray(site.data)) {
|
|
251
|
+
visit({ kind: "step", slots: site.steps, data: site.data, path });
|
|
252
|
+
} else if (site.refs.length > 0) {
|
|
253
|
+
visit({ kind: "ref", slots: site.refs, data: site.data, path });
|
|
191
254
|
}
|
|
192
255
|
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
interface Site {
|
|
259
|
+
value: unknown;
|
|
260
|
+
path: string;
|
|
261
|
+
}
|
|
193
262
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
263
|
+
/** The values at `rel` below `value`, where `rel` is relative to the field path
|
|
264
|
+
* `prefix`. A `{}` segment skips the keys the driven map records as declared
|
|
265
|
+
* for that map path; each `[]` iterates one array level. */
|
|
266
|
+
function resolveSites(value: unknown, rel: string, prefix: string, driven: DrivenSlots): Site[] {
|
|
267
|
+
let sites: Site[] = [{ value, path: "" }];
|
|
268
|
+
let field = prefix;
|
|
269
|
+
for (const part of rel.split(".")) {
|
|
270
|
+
const next: Site[] = [];
|
|
271
|
+
if (part === "{}") {
|
|
272
|
+
field = joinKey(field, "{}");
|
|
273
|
+
const declared = driven.declaredKeys.get(field);
|
|
274
|
+
for (const site of sites) {
|
|
275
|
+
if (!site.value || typeof site.value !== "object" || Array.isArray(site.value)) continue;
|
|
276
|
+
for (const [key, entry] of Object.entries(site.value as Record<string, unknown>)) {
|
|
277
|
+
if (entry == null || declared?.has(key)) continue;
|
|
278
|
+
next.push({ value: entry, path: joinKey(site.path, key) });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
const key = part.replace(/(\[\])+$/, "");
|
|
283
|
+
const depth = (part.length - key.length) / 2;
|
|
284
|
+
field = key ? joinKey(field, part) : `${field}${part}`;
|
|
285
|
+
for (const site of sites) {
|
|
286
|
+
let level: Site[];
|
|
287
|
+
if (key) {
|
|
288
|
+
if (!site.value || typeof site.value !== "object") continue;
|
|
289
|
+
const entry = (site.value as Record<string, unknown>)[key];
|
|
290
|
+
if (entry == null) continue;
|
|
291
|
+
level = [{ value: entry, path: joinKey(site.path, key) }];
|
|
292
|
+
} else {
|
|
293
|
+
level = [site];
|
|
294
|
+
}
|
|
295
|
+
for (let d = 0; d < depth; d++) {
|
|
296
|
+
const items: Site[] = [];
|
|
297
|
+
for (const holder of level) {
|
|
298
|
+
if (!Array.isArray(holder.value)) continue;
|
|
299
|
+
holder.value.forEach((item, i) => {
|
|
300
|
+
if (item != null) items.push({ value: item, path: `${holder.path}[${i}]` });
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
level = items;
|
|
304
|
+
}
|
|
305
|
+
next.push(...level);
|
|
306
|
+
}
|
|
197
307
|
}
|
|
308
|
+
sites = next;
|
|
198
309
|
}
|
|
310
|
+
return sites;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const joinKey = (path: string, key: string): string => (path ? `${path}.${key}` : key);
|
|
314
|
+
|
|
315
|
+
/** `fieldPath` relative to `prefix`, or undefined when it is not strictly
|
|
316
|
+
* below it. A result starting `[]` iterates the value at `prefix` itself. */
|
|
317
|
+
function relativeFieldPath(fieldPath: string, prefix: string): string | undefined {
|
|
318
|
+
if (prefix === "") return fieldPath;
|
|
319
|
+
if (fieldPath.startsWith(`${prefix}.`)) return fieldPath.slice(prefix.length + 1);
|
|
320
|
+
if (fieldPath.startsWith(`${prefix}[]`)) return fieldPath.slice(prefix.length);
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function joinConcrete(base: string, rel: string): string {
|
|
325
|
+
if (!base) return rel;
|
|
326
|
+
return rel.startsWith("[") ? `${base}${rel}` : `${base}.${rel}`;
|
|
199
327
|
}
|
|
200
328
|
|