@telorun/analyzer 0.71.0 → 0.73.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 +92 -15
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +71 -97
- package/dist/catch-scope.d.ts +72 -0
- package/dist/catch-scope.d.ts.map +1 -0
- package/dist/catch-scope.js +102 -0
- package/dist/cel-scope-query.d.ts.map +1 -1
- package/dist/cel-scope-query.js +32 -11
- package/dist/cel-scope.d.ts.map +1 -1
- package/dist/cel-scope.js +1 -0
- package/dist/deprecation.d.ts +21 -0
- package/dist/deprecation.d.ts.map +1 -0
- package/dist/deprecation.js +26 -0
- package/dist/eval-paths.d.ts +36 -5
- package/dist/eval-paths.d.ts.map +1 -1
- package/dist/eval-paths.js +47 -6
- package/dist/extends-resolution.d.ts +12 -0
- package/dist/extends-resolution.d.ts.map +1 -1
- package/dist/extends-resolution.js +23 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/manifest-visitor.d.ts +17 -1
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +5 -1
- package/dist/migrations/report.d.ts +1 -1
- package/dist/migrations/report.d.ts.map +1 -1
- package/dist/migrations/report.js +5 -0
- package/dist/nearest-name.d.ts +22 -0
- package/dist/nearest-name.d.ts.map +1 -0
- package/dist/nearest-name.js +43 -0
- package/dist/ref-slot.d.ts +15 -0
- package/dist/ref-slot.d.ts.map +1 -1
- package/dist/ref-slot.js +7 -0
- package/dist/release/destinations.d.ts +59 -0
- package/dist/release/destinations.d.ts.map +1 -0
- package/dist/release/destinations.js +75 -0
- package/dist/release/index.d.ts +6 -2
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +3 -1
- package/dist/release/ledger.d.ts +20 -11
- package/dist/release/ledger.d.ts.map +1 -1
- package/dist/release/ledger.js +43 -14
- package/dist/release/release-plan.d.ts +3 -2
- package/dist/release/release-plan.d.ts.map +1 -1
- package/dist/release/release-plan.js +12 -19
- package/dist/release/workspace-config.d.ts +114 -34
- package/dist/release/workspace-config.d.ts.map +1 -1
- package/dist/release/workspace-config.js +322 -38
- package/dist/release/workspace-schema.d.ts +41 -0
- package/dist/release/workspace-schema.d.ts.map +1 -0
- package/dist/release/workspace-schema.js +77 -0
- package/dist/resolve-throws-union.d.ts +29 -1
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +111 -16
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +13 -1
- package/dist/schema-error-report.d.ts +7 -0
- package/dist/schema-error-report.d.ts.map +1 -1
- package/dist/schema-error-report.js +57 -8
- package/dist/schema-keywords.d.ts.map +1 -1
- package/dist/schema-keywords.js +3 -1
- package/dist/schema-walk.d.ts +27 -0
- package/dist/schema-walk.d.ts.map +1 -1
- package/dist/schema-walk.js +44 -0
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/template-body.d.ts +10 -0
- package/dist/template-body.d.ts.map +1 -1
- package/dist/template-body.js +25 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +11 -0
- package/dist/validate-cel-context.d.ts +23 -8
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +48 -16
- package/dist/validate-exports.d.ts +28 -0
- package/dist/validate-exports.d.ts.map +1 -0
- package/dist/validate-exports.js +146 -0
- package/dist/validate-extends.d.ts.map +1 -1
- package/dist/validate-extends.js +37 -1
- package/dist/validate-identifier-names.d.ts +2 -2
- package/dist/validate-identifier-names.d.ts.map +1 -1
- package/dist/validate-identifier-names.js +22 -7
- package/dist/validate-provider-coherence.d.ts +4 -8
- package/dist/validate-provider-coherence.d.ts.map +1 -1
- package/dist/validate-provider-coherence.js +12 -123
- package/dist/validate-ref-slots.d.ts +1 -1
- package/dist/validate-ref-slots.d.ts.map +1 -1
- package/dist/validate-ref-slots.js +34 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +168 -63
- package/dist/validate-template-body.d.ts +36 -0
- package/dist/validate-template-body.d.ts.map +1 -0
- package/dist/validate-template-body.js +246 -0
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +236 -85
- package/package.json +3 -2
- package/src/analyzer.ts +118 -12
- package/src/builtins.ts +85 -97
- package/src/catch-scope.ts +157 -0
- package/src/cel-scope-query.ts +28 -9
- package/src/cel-scope.ts +1 -0
- package/src/deprecation.ts +36 -0
- package/src/eval-paths.ts +53 -6
- package/src/extends-resolution.ts +24 -0
- package/src/index.ts +9 -1
- package/src/manifest-visitor.ts +19 -2
- package/src/migrations/report.ts +5 -1
- package/src/nearest-name.ts +47 -0
- package/src/ref-slot.ts +19 -0
- package/src/release/destinations.ts +105 -0
- package/src/release/index.ts +33 -2
- package/src/release/ledger.ts +51 -21
- package/src/release/release-plan.ts +16 -26
- package/src/release/workspace-config.ts +483 -47
- package/src/release/workspace-schema.ts +99 -0
- package/src/resolve-throws-union.ts +139 -21
- package/src/schema-compat.ts +13 -0
- package/src/schema-error-report.ts +66 -10
- package/src/schema-keywords.ts +4 -1
- package/src/schema-walk.ts +56 -0
- package/src/telo-version.ts +1 -1
- package/src/template-body.ts +25 -0
- package/src/types.ts +18 -0
- package/src/validate-cel-context.ts +57 -16
- package/src/validate-exports.ts +185 -0
- package/src/validate-extends.ts +43 -0
- package/src/validate-identifier-names.ts +28 -9
- package/src/validate-provider-coherence.ts +17 -127
- package/src/validate-ref-slots.ts +41 -1
- package/src/validate-references.ts +174 -63
- package/src/validate-template-body.ts +274 -0
- package/src/validate-throws-coverage.ts +333 -92
- package/dist/validate-template-dispatch.d.ts +0 -27
- package/dist/validate-template-dispatch.d.ts.map +0 -1
- package/dist/validate-template-dispatch.js +0 -95
- package/src/validate-template-dispatch.ts +0 -99
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape of `telo-workspace.yaml`, declared as data.
|
|
3
|
+
*
|
|
4
|
+
* One declaration read by two consumers — the strict half that reports what a
|
|
5
|
+
* marker got wrong, and the editor's completion list. Two hand-maintained key
|
|
6
|
+
* lists drift, and the drift is silent in the worst direction: an editor
|
|
7
|
+
* offering a key the checker rejects.
|
|
8
|
+
*
|
|
9
|
+
* Written as JSON Schema because that is what the shape *is*, and because the
|
|
10
|
+
* `description` a completion item shows and the type a diagnostic names are the
|
|
11
|
+
* same two facts a schema already carries. It is not compiled by AJV — the
|
|
12
|
+
* strict half walks it, which is what lets a wrong value say `'registry' must be
|
|
13
|
+
* the publish destination base, as a string` instead of a keyword trace.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface WorkspaceKeySchema {
|
|
17
|
+
readonly type: "string" | "string[]" | "entry[]";
|
|
18
|
+
readonly description: string;
|
|
19
|
+
/** Values worth offering at this key when nothing better is known. */
|
|
20
|
+
readonly examples?: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface WorkspaceBlockSchema {
|
|
24
|
+
readonly description: string;
|
|
25
|
+
readonly properties: Readonly<Record<string, WorkspaceKeySchema>>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Every block, and every key in it, is optional — a marker whose whole content
|
|
30
|
+
* is comments is valid, and is what a runner seeds for the cache anchor alone.
|
|
31
|
+
*/
|
|
32
|
+
export const WORKSPACE_SCHEMA: Readonly<Record<string, WorkspaceBlockSchema>> = {
|
|
33
|
+
release: {
|
|
34
|
+
description: "How `telo release` behaves: where modules are, where they publish, and which of their paths are not release-relevant.",
|
|
35
|
+
properties: {
|
|
36
|
+
registry: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description:
|
|
39
|
+
"Publish destination base. A module's ref is `<registry>/<its own directory name>`. Omitted: --registry, then TELO_OCI_REGISTRY, then the base each module's own ledger entry recorded.",
|
|
40
|
+
examples: ["oci://ghcr.io/telorun"],
|
|
41
|
+
},
|
|
42
|
+
ignore: {
|
|
43
|
+
type: "string[]",
|
|
44
|
+
description:
|
|
45
|
+
"Paths under a module whose changes are not release-relevant, so no changelog fragment is asked for. Module-relative, gitignore-style. Declaring it replaces the built-in default; [] ignores nothing.",
|
|
46
|
+
examples: ["**/tests/**", "**/docs/**", "**/plans/**", "**/README.md", "**/CHANGELOG.md"],
|
|
47
|
+
},
|
|
48
|
+
modules: {
|
|
49
|
+
type: "entry[]",
|
|
50
|
+
description:
|
|
51
|
+
"The subtrees that may hold modules — a place to look, never a module: what makes a directory a module is its telo.yaml. Workspace-relative, gitignore-style, last match wins. A bare string is an entry with no overrides.",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
env: {
|
|
56
|
+
description: "How `telo run` resolves a manifest's environment.",
|
|
57
|
+
properties: {
|
|
58
|
+
roots: {
|
|
59
|
+
type: "string[]",
|
|
60
|
+
description:
|
|
61
|
+
"How far up the .env walk may climb. Workspace-relative, gitignore-style, matched against each ancestor directory; the nearest match stops the walk. Omitted: the walk stops at this file.",
|
|
62
|
+
},
|
|
63
|
+
files: {
|
|
64
|
+
type: "string[]",
|
|
65
|
+
description:
|
|
66
|
+
"Which files are collected in each directory, later winning within one directory. Filenames only — a / or a glob is an error. Declaring it replaces the default; [] collects none.",
|
|
67
|
+
examples: [".env", ".env.local"],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Keys a `release.modules` entry may carry in its object form. `path` is the
|
|
74
|
+
* pattern; the rest override the block's own keys, key-wise. */
|
|
75
|
+
export const MODULE_ENTRY_KEYS: Readonly<Record<string, WorkspaceKeySchema>> = {
|
|
76
|
+
path: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description: "The gitignore-style pattern this entry matches modules with.",
|
|
79
|
+
},
|
|
80
|
+
registry: WORKSPACE_SCHEMA.release.properties.registry,
|
|
81
|
+
ignore: WORKSPACE_SCHEMA.release.properties.ignore,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const WORKSPACE_BLOCKS: readonly string[] = Object.keys(WORKSPACE_SCHEMA);
|
|
85
|
+
|
|
86
|
+
/** Built-in when `release.ignore` is absent. Every pattern carries `**\/` — the
|
|
87
|
+
* anchoring at a module root is the defect this replaced, so anchoring some and
|
|
88
|
+
* not others would reproduce it partially. */
|
|
89
|
+
export const DEFAULT_RELEASE_IGNORE: readonly string[] = [
|
|
90
|
+
"**/tests/**",
|
|
91
|
+
"**/docs/**",
|
|
92
|
+
"**/plans/**",
|
|
93
|
+
"**/README.md",
|
|
94
|
+
"**/CHANGELOG.md",
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
/** Built-in when `env.files` is absent — the pair the walk hardcoded before it
|
|
98
|
+
* was authorable, in the order it applied them. */
|
|
99
|
+
export const DEFAULT_ENV_FILES: readonly string[] = [".env", ".env.local"];
|
|
@@ -4,7 +4,9 @@ import { scopeResolverForModule, type AliasResolver } from "./alias-resolver.js"
|
|
|
4
4
|
import { resolveScopedName } 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 { possibleUses, readRefSlot, transfersControl, type RefSlot } from "./ref-slot.js";
|
|
7
8
|
import { readStepSlot } from "./step-slot.js";
|
|
9
|
+
import { forEachDrivenSlot } from "./schema-walk.js";
|
|
8
10
|
|
|
9
11
|
export interface ThrowsCodeMeta {
|
|
10
12
|
data?: Record<string, any>;
|
|
@@ -263,6 +265,16 @@ export function resolveThrowsUnion(
|
|
|
263
265
|
}
|
|
264
266
|
}
|
|
265
267
|
|
|
268
|
+
/**
|
|
269
|
+
* `throws.inherit: true` — the union a composer's own STEP BODIES reach.
|
|
270
|
+
*
|
|
271
|
+
* Deliberately steps only, and not every slot the resource drives: `inherit` is
|
|
272
|
+
* a DECLARATION that a kind's union is the union of what it dispatches, and a
|
|
273
|
+
* kind that does not make that claim must not have it inferred — a kind holding
|
|
274
|
+
* a `call` ref it catches internally would silently gain codes it never lets
|
|
275
|
+
* escape. What a CATCH SCOPE needs is a different question with a different
|
|
276
|
+
* answer, and it has its own resolver below.
|
|
277
|
+
*/
|
|
266
278
|
function resolveInherited(
|
|
267
279
|
manifest: ResourceManifest,
|
|
268
280
|
definition: ResourceDefinition,
|
|
@@ -284,6 +296,66 @@ function resolveInherited(
|
|
|
284
296
|
return result;
|
|
285
297
|
}
|
|
286
298
|
|
|
299
|
+
/**
|
|
300
|
+
* The union a SCOPE-LEVEL `catches:` list can be asked to render — everything
|
|
301
|
+
* the resource it is written on drives, transitively.
|
|
302
|
+
*
|
|
303
|
+
* `x-telo-catches-for: ""` is itself the claim that this is the denominator, so
|
|
304
|
+
* nothing is inferred from a kind that did not opt in, and no definition has to
|
|
305
|
+
* declare a `throws:` block to carry a catch scope (the kernel forbids one on a
|
|
306
|
+
* `Telo.Service` and a `Telo.Mount`, and rightly: what a router renders is not
|
|
307
|
+
* what a router THROWS).
|
|
308
|
+
*
|
|
309
|
+
* Three edges, three answers. A **step body** contributes its own traversal,
|
|
310
|
+
* subtraction included. A **control-transferring ref** contributes the target's
|
|
311
|
+
* own declared union — a route handler is a leaf here, and asking what IT drives
|
|
312
|
+
* would credit this scope with codes the handler catches internally. A
|
|
313
|
+
* **`throwsThrough` ref** recurses, because the target is another scope on the
|
|
314
|
+
* same ladder: a server renders what its mounts' routes throw, not what the
|
|
315
|
+
* mounts themselves declare.
|
|
316
|
+
*/
|
|
317
|
+
export function resolveScopeUnion(
|
|
318
|
+
manifest: ResourceManifest,
|
|
319
|
+
definition: ResourceDefinition,
|
|
320
|
+
ctx: ResolveCtx,
|
|
321
|
+
seen: Set<ResourceManifest> = new Set(),
|
|
322
|
+
): ThrowsUnion {
|
|
323
|
+
const result: ThrowsUnion = { codes: new Map(), unbounded: false };
|
|
324
|
+
if (seen.has(manifest)) return result;
|
|
325
|
+
seen.add(manifest);
|
|
326
|
+
const ownerModule = (manifest.metadata as { module?: string } | undefined)?.module;
|
|
327
|
+
|
|
328
|
+
forEachDrivenSlot(definition.schema, manifest, (driven) => {
|
|
329
|
+
if (driven.kind === "step") {
|
|
330
|
+
unionInto(
|
|
331
|
+
result,
|
|
332
|
+
collectStepArrayThrows(driven.data, driven.slot.invoke, undefined, ctx, ownerModule),
|
|
333
|
+
);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (driven.slot.throwsThrough) {
|
|
337
|
+
const target = resolveRefManifest(driven.data, ctx, ownerModule);
|
|
338
|
+
const targetDef = target
|
|
339
|
+
? definitionFor(
|
|
340
|
+
target.kind,
|
|
341
|
+
ctx.defs,
|
|
342
|
+
ctx.aliases,
|
|
343
|
+
scopeResolverFor(ctx, (target.metadata as { module?: string } | undefined)?.module),
|
|
344
|
+
)
|
|
345
|
+
: undefined;
|
|
346
|
+
if (target && targetDef) unionInto(result, resolveScopeUnion(target, targetDef, ctx, seen));
|
|
347
|
+
// A target that cannot be resolved says nothing about what it throws, so
|
|
348
|
+
// the scope's union is no longer enumerable.
|
|
349
|
+
else result.unbounded = true;
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (!possibleUses(driven.slot).some(transfersControl)) return;
|
|
353
|
+
unionInto(result, resolveRefTargetThrows(driven.data, ctx, ownerModule));
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
|
|
287
359
|
function collectStepArrayThrows(
|
|
288
360
|
steps: unknown[],
|
|
289
361
|
invokeField: string,
|
|
@@ -410,9 +482,32 @@ function resolveStepInvokeThrows(
|
|
|
410
482
|
ctx: ResolveCtx,
|
|
411
483
|
ownerModule: string | undefined,
|
|
412
484
|
): ThrowsUnion {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
485
|
+
return resolveRefTargetThrows(step[invokeField], ctx, ownerModule, () =>
|
|
486
|
+
resolvePassthroughAtCallSite(step, enclosingTryCodes),
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* The effective throw union behind a resolved reference value.
|
|
492
|
+
*
|
|
493
|
+
* Shared by both ways a resource drives another: a step's `invoke:` and a
|
|
494
|
+
* reference slot that carries throws. The two used to differ only in where the
|
|
495
|
+
* ref value was read from, and keeping one copy is what stops a router's
|
|
496
|
+
* denominator and a sequence's from disagreeing about what a name resolves to.
|
|
497
|
+
*
|
|
498
|
+
* `onPassthrough` is the one genuine difference: a passthrough kind's union is a
|
|
499
|
+
* property of the CALL SITE (`inputs.code`), which only a step has. A reference
|
|
500
|
+
* slot has no such site, so the union is unbounded there rather than guessed.
|
|
501
|
+
*/
|
|
502
|
+
function resolveRefTargetThrows(
|
|
503
|
+
refValue: unknown,
|
|
504
|
+
ctx: ResolveCtx,
|
|
505
|
+
ownerModule: string | undefined,
|
|
506
|
+
onPassthrough?: () => ThrowsUnion,
|
|
507
|
+
): ThrowsUnion {
|
|
508
|
+
if (!refValue || typeof refValue !== "object" || Array.isArray(refValue)) return emptyUnion();
|
|
509
|
+
const ref = refValue as Record<string, any>;
|
|
510
|
+
const invokedKind = ref.kind as string | undefined;
|
|
416
511
|
// A reference that still carries its parse-time sentinel — a library-internal
|
|
417
512
|
// `!ref` inside a manifest forwarded into a consumer's flat set, where Phase
|
|
418
513
|
// 2.5 had nothing to resolve it against. The target is in the declaring
|
|
@@ -422,7 +517,7 @@ function resolveStepInvokeThrows(
|
|
|
422
517
|
// its consumer and get the consumer's `catches:` rejected for the code the
|
|
423
518
|
// entry point documents.
|
|
424
519
|
if (!invokedKind) {
|
|
425
|
-
const sentinel = refSentinelTarget(
|
|
520
|
+
const sentinel = refSentinelTarget(ref);
|
|
426
521
|
if (!sentinel) return emptyUnion();
|
|
427
522
|
const target = findSentinelTarget(ctx, sentinel, ownerModule);
|
|
428
523
|
if (target) return resolveThrowsUnion(target, ctx);
|
|
@@ -430,32 +525,19 @@ function resolveStepInvokeThrows(
|
|
|
430
525
|
}
|
|
431
526
|
|
|
432
527
|
// The invoked kind's alias resolves in the OWNER manifest's lexical scope (the
|
|
433
|
-
//
|
|
528
|
+
// resource that declares the slot), so a library's step referencing its own
|
|
434
529
|
// import resolves against that library, not the consumer.
|
|
435
530
|
const scopeResolver = scopeResolverFor(ctx, ownerModule);
|
|
436
531
|
const definition = definitionFor(invokedKind, ctx.defs, ctx.aliases, scopeResolver);
|
|
437
532
|
if (!definition) return { codes: new Map(), unbounded: true };
|
|
438
533
|
|
|
439
534
|
if (definition.throws?.passthrough) {
|
|
440
|
-
return
|
|
535
|
+
return onPassthrough ? onPassthrough() : { codes: new Map(), unbounded: true };
|
|
441
536
|
}
|
|
442
537
|
|
|
443
538
|
// Named manifest: resolve the full chain (covers transitive inherit).
|
|
444
|
-
const
|
|
445
|
-
if (
|
|
446
|
-
const scopedInvokedKind = scopeResolver?.resolveKind(invokedKind);
|
|
447
|
-
const target = findTarget(
|
|
448
|
-
ctx,
|
|
449
|
-
invokeName,
|
|
450
|
-
ownerModule,
|
|
451
|
-
(m) =>
|
|
452
|
-
m.kind === invokedKind ||
|
|
453
|
-
ctx.aliases.resolveKind(m.kind) === invokedKind ||
|
|
454
|
-
m.kind === ctx.aliases.resolveKind(invokedKind) ||
|
|
455
|
-
(scopedInvokedKind !== undefined && m.kind === scopedInvokedKind),
|
|
456
|
-
);
|
|
457
|
-
if (target) return resolveThrowsUnion(target, ctx);
|
|
458
|
-
}
|
|
539
|
+
const target = resolveRefManifest(ref, ctx, ownerModule);
|
|
540
|
+
if (target) return resolveThrowsUnion(target, ctx);
|
|
459
541
|
|
|
460
542
|
// Fall back to the definition's own explicit codes. Mark unbounded when the
|
|
461
543
|
// definition depends on call-site or transitive resolution we couldn't
|
|
@@ -466,6 +548,42 @@ function resolveStepInvokeThrows(
|
|
|
466
548
|
return { codes, unbounded };
|
|
467
549
|
}
|
|
468
550
|
|
|
551
|
+
/**
|
|
552
|
+
* The manifest a resolved reference value names, in either shape it arrives in —
|
|
553
|
+
* a `{kind, name}` pair, or a `!ref` still carrying its parse-time sentinel.
|
|
554
|
+
*
|
|
555
|
+
* Exported because the catch-scope enclosure walk asks the same question about
|
|
556
|
+
* the same values; resolving a name twice by two rules is how two passes end up
|
|
557
|
+
* disagreeing about which resource a slot points at.
|
|
558
|
+
*/
|
|
559
|
+
export function resolveRefManifest(
|
|
560
|
+
refValue: unknown,
|
|
561
|
+
ctx: ResolveCtx,
|
|
562
|
+
ownerModule: string | undefined,
|
|
563
|
+
): ResourceManifest | undefined {
|
|
564
|
+
if (!refValue || typeof refValue !== "object" || Array.isArray(refValue)) return undefined;
|
|
565
|
+
const ref = refValue as Record<string, any>;
|
|
566
|
+
const kind = ref.kind as string | undefined;
|
|
567
|
+
if (!kind) {
|
|
568
|
+
const sentinel = refSentinelTarget(ref);
|
|
569
|
+
return sentinel ? findSentinelTarget(ctx, sentinel, ownerModule) : undefined;
|
|
570
|
+
}
|
|
571
|
+
const name = ref.name as string | undefined;
|
|
572
|
+
if (!name) return undefined;
|
|
573
|
+
const scopeResolver = scopeResolverFor(ctx, ownerModule);
|
|
574
|
+
const scopedKind = scopeResolver?.resolveKind(kind);
|
|
575
|
+
return findTarget(
|
|
576
|
+
ctx,
|
|
577
|
+
name,
|
|
578
|
+
ownerModule,
|
|
579
|
+
(m) =>
|
|
580
|
+
m.kind === kind ||
|
|
581
|
+
ctx.aliases.resolveKind(m.kind) === kind ||
|
|
582
|
+
m.kind === ctx.aliases.resolveKind(kind) ||
|
|
583
|
+
(scopedKind !== undefined && m.kind === scopedKind),
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
469
587
|
/** Resolve a passthrough-style invocable at a specific call site. Recognised forms
|
|
470
588
|
* (see "passthrough: true" in the plan):
|
|
471
589
|
* - constant literal (no template) → `{ <literal> }`
|
package/src/schema-compat.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
celBaseOfValueType,
|
|
10
10
|
celTypeOfValueType,
|
|
11
|
+
isCompiledValue,
|
|
11
12
|
readValueTypeSlot,
|
|
12
13
|
valueBrandBases,
|
|
13
14
|
valueTypeOf,
|
|
@@ -799,6 +800,18 @@ export function substituteCelFields(
|
|
|
799
800
|
mark();
|
|
800
801
|
return celPlaceholderForSchema(resolved);
|
|
801
802
|
}
|
|
803
|
+
// The same fact in its third spelling. An expression reaches this walk as a
|
|
804
|
+
// `${{ … }}` string or a `!cel` sentinel BEFORE `precompileDoc`, and as a
|
|
805
|
+
// CompiledValue after — so a caller running under `compile: true` (every
|
|
806
|
+
// `telo run`, unlike `telo check`) handed one to AJV as a plain object, and a
|
|
807
|
+
// slot typed `boolean` rejected a `when:` the author wrote correctly. The
|
|
808
|
+
// kernel's `stripCompiledValues` has always substituted here; missing it on
|
|
809
|
+
// this side made one manifest mean two things depending on which command read
|
|
810
|
+
// it.
|
|
811
|
+
if (isCompiledValue(data)) {
|
|
812
|
+
mark();
|
|
813
|
+
return celPlaceholderForSchema(resolved);
|
|
814
|
+
}
|
|
802
815
|
if (Array.isArray(data)) {
|
|
803
816
|
const item = resolveRefIn((resolved.items ?? {}) as Record<string, any>, root, external);
|
|
804
817
|
return data.map((element, i) =>
|
|
@@ -44,6 +44,13 @@ export interface SchemaIssue {
|
|
|
44
44
|
message: string;
|
|
45
45
|
/** Dotted path to the field (e.g. "config.handler"). Empty string means root. */
|
|
46
46
|
path: string;
|
|
47
|
+
/** The AJV keyword that produced it, so a consumer can key on WHAT failed
|
|
48
|
+
* rather than on how the sentence reads. Prose is the renderer's to change;
|
|
49
|
+
* a caller matching on it breaks silently when it does, and mis-fires on any
|
|
50
|
+
* other issue whose text happens to quote the same name. */
|
|
51
|
+
keyword?: string;
|
|
52
|
+
/** For `required`, the property that is missing. */
|
|
53
|
+
missingProperty?: string;
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
const UNION_KEYWORDS = new Set(["anyOf", "oneOf"]);
|
|
@@ -53,10 +60,42 @@ const UNION_KEYWORDS = new Set(["anyOf", "oneOf"]);
|
|
|
53
60
|
* further in. These are what make a branch implausible. */
|
|
54
61
|
const SHAPE_KEYWORDS = new Set(["required", "type", "additionalProperties", "enum", "const"]);
|
|
55
62
|
|
|
63
|
+
/** Keywords that exist to DISCRIMINATE, so a mismatch is positive evidence that
|
|
64
|
+
* the value is not this branch — at any depth, not only at the union node.
|
|
65
|
+
*
|
|
66
|
+
* Depth is otherwise the tiebreak, and it inverts exactly here: a branch that
|
|
67
|
+
* agreed on the discriminator and failed one constraint reported at the union
|
|
68
|
+
* node loses to every branch that disagreed about the discriminator one level
|
|
69
|
+
* in. That is how a `capability: Telo.Service` document declaring a forbidden
|
|
70
|
+
* key was reported as `/capability must be equal to constant` — naming neither
|
|
71
|
+
* the key at fault nor a branch the value could ever have been. */
|
|
72
|
+
const DISCRIMINATOR_KEYWORDS = new Set(["const", "enum"]);
|
|
73
|
+
|
|
74
|
+
/** RFC 6901 escapes: `~1` is a literal `/` in the key, `~0` a literal `~`.
|
|
75
|
+
* Decoded wherever a segment is shown or matched, because encoded it is not the
|
|
76
|
+
* key the manifest holds — a content map (`application/json`) anchors nowhere
|
|
77
|
+
* and reads wrong in the sentence. `~0` is expanded LAST, or `~01` would decode
|
|
78
|
+
* to `/` instead of the literal `~1` it encodes. */
|
|
79
|
+
function unescapeSegment(part: string): string {
|
|
80
|
+
return part.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The whole pointer, segment by segment, for prose that quotes it verbatim. */
|
|
84
|
+
function unescapePointer(pointer: string): string {
|
|
85
|
+
return pointer
|
|
86
|
+
.split("/")
|
|
87
|
+
.map((segment, i) => (i === 0 ? segment : unescapeSegment(segment)))
|
|
88
|
+
.join("/");
|
|
89
|
+
}
|
|
90
|
+
|
|
56
91
|
/* ------------------------------------------------------------------ prose */
|
|
57
92
|
|
|
58
93
|
export function formatSingleError(err: AjvErrorLike): string {
|
|
59
|
-
|
|
94
|
+
// Unescaped for the same reason the path is: a reader acting on this sentence
|
|
95
|
+
// needs the key the manifest holds (`application/json`), not its RFC 6901
|
|
96
|
+
// encoding. Reporting one form in `path` and the other in `message` made one
|
|
97
|
+
// diagnostic disagree with itself.
|
|
98
|
+
const p = unescapePointer(err.instancePath || "") || "/";
|
|
60
99
|
const params = err.params ?? {};
|
|
61
100
|
switch (err.keyword) {
|
|
62
101
|
case "additionalProperties":
|
|
@@ -67,6 +106,16 @@ export function formatSingleError(err: AjvErrorLike): string {
|
|
|
67
106
|
return `${p} ${err.message ?? "is invalid"} (${(params.allowedValues as unknown[])?.join(" | ")})`;
|
|
68
107
|
case "type":
|
|
69
108
|
return `${p} must be ${params.type}${describeActual(err)}`;
|
|
109
|
+
// A `false` schema at a property is how a branch forbids a key it otherwise
|
|
110
|
+
// declares. AJV's own text ("boolean schema is false") describes the schema
|
|
111
|
+
// rather than the value, and the path is the only part a reader can act on.
|
|
112
|
+
case "false schema":
|
|
113
|
+
return `${p} is not allowed here`;
|
|
114
|
+
// `not:` says a forbidden shape matched, and AJV reports nothing about the
|
|
115
|
+
// inner schema — so the honest message says where, and no more. A branch
|
|
116
|
+
// that wants to name the key writes `properties: { <key>: false }` instead.
|
|
117
|
+
case "not":
|
|
118
|
+
return `${p} matches a shape that is not allowed here`;
|
|
70
119
|
default:
|
|
71
120
|
return `${p} ${err.message ?? "is invalid"}`;
|
|
72
121
|
}
|
|
@@ -130,11 +179,13 @@ function describeAlternatives(errors: AjvErrorLike[], unionInstancePath: string)
|
|
|
130
179
|
}
|
|
131
180
|
|
|
132
181
|
/** Is this branch a plausible reading of the value — does it accept the value's
|
|
133
|
-
* shape at the union node itself, and
|
|
182
|
+
* shape at the union node itself, and agree with every discriminator it pins? */
|
|
134
183
|
function isPlausible(errors: AjvErrorLike[], unionInstancePath: string): boolean {
|
|
135
|
-
return !errors.some(
|
|
136
|
-
|
|
137
|
-
|
|
184
|
+
return !errors.some((e) => {
|
|
185
|
+
const keyword = e.keyword ?? "";
|
|
186
|
+
if (DISCRIMINATOR_KEYWORDS.has(keyword)) return true;
|
|
187
|
+
return (e.instancePath || "") === unionInstancePath && SHAPE_KEYWORDS.has(keyword);
|
|
188
|
+
});
|
|
138
189
|
}
|
|
139
190
|
|
|
140
191
|
/**
|
|
@@ -392,7 +443,7 @@ export function ajvErrorToPath(err: AjvErrorLike): string {
|
|
|
392
443
|
let result = "";
|
|
393
444
|
for (const part of parts) {
|
|
394
445
|
if (/^\d+$/.test(part)) result += `[${part}]`;
|
|
395
|
-
else result += result ? `.${part}` : part;
|
|
446
|
+
else result += result ? `.${unescapeSegment(part)}` : unescapeSegment(part);
|
|
396
447
|
}
|
|
397
448
|
if (err.keyword === "required" && err.params?.missingProperty) {
|
|
398
449
|
const missing = err.params.missingProperty as string;
|
|
@@ -403,10 +454,15 @@ export function ajvErrorToPath(err: AjvErrorLike): string {
|
|
|
403
454
|
|
|
404
455
|
/** Reduced, path-anchored issues — what a diagnostic list is built from. */
|
|
405
456
|
export function schemaIssues(errors: AjvErrorLike[] | null | undefined): SchemaIssue[] {
|
|
406
|
-
return reduceSchemaErrors(errors).map((err) =>
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
457
|
+
return reduceSchemaErrors(errors).map((err) => {
|
|
458
|
+
const missing = (err.params ?? {}).missingProperty;
|
|
459
|
+
return {
|
|
460
|
+
message: formatSingleError(err),
|
|
461
|
+
path: ajvErrorToPath(err),
|
|
462
|
+
...(err.keyword ? { keyword: err.keyword } : {}),
|
|
463
|
+
...(typeof missing === "string" ? { missingProperty: missing } : {}),
|
|
464
|
+
};
|
|
465
|
+
});
|
|
410
466
|
}
|
|
411
467
|
|
|
412
468
|
/** Reduced, rendered as one sentence — what a thrown runtime error carries. */
|
package/src/schema-keywords.ts
CHANGED
|
@@ -386,7 +386,10 @@ export const TELO_SCHEMA_ANNOTATIONS: Record<
|
|
|
386
386
|
},
|
|
387
387
|
"x-telo-catches-for": {
|
|
388
388
|
title: "Catches for",
|
|
389
|
-
description:
|
|
389
|
+
description:
|
|
390
|
+
"Whose failures this catch list renders: a sibling field naming the handler, " +
|
|
391
|
+
"or the EMPTY string for everything this resource drives — a scope-level list, " +
|
|
392
|
+
"which owes coverage of nothing itself and answers for every site it encloses.",
|
|
390
393
|
type: "string",
|
|
391
394
|
},
|
|
392
395
|
};
|
package/src/schema-walk.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
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 { readRefSlot, type RefSlot } from "./ref-slot.js";
|
|
13
|
+
import { readStepSlot, type StepSlot } from "./step-slot.js";
|
|
12
14
|
|
|
13
15
|
/** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
|
|
14
16
|
* Non-refs and unresolved refs pass through unchanged. */
|
|
@@ -142,3 +144,57 @@ export function walkStepArray(
|
|
|
142
144
|
}
|
|
143
145
|
});
|
|
144
146
|
}
|
|
147
|
+
/** A slot through which a resource drives another. */
|
|
148
|
+
export type DrivenSlot =
|
|
149
|
+
| { kind: "step"; slot: StepSlot; data: unknown[]; path: string }
|
|
150
|
+
| { kind: "ref"; slot: RefSlot; data: unknown; path: string };
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Every slot of one resource through which it drives another, schema and data in
|
|
154
|
+
* tandem.
|
|
155
|
+
*
|
|
156
|
+
* One traversal with two consumers — the inherited union here, and the catch
|
|
157
|
+
* scope enclosure in `validate-throws-coverage.ts` — because both ask the same
|
|
158
|
+
* structural question and two copies would eventually disagree about where the
|
|
159
|
+
* walk stops. It terminates on the manifest's own depth, and it stops AT a step
|
|
160
|
+
* slot (that traversal owns everything below it, `try`/`catch` subtraction
|
|
161
|
+
* included) and AT a reference slot (a resolved ref is a leaf, `{kind, name}`,
|
|
162
|
+
* with nothing beneath it to visit).
|
|
163
|
+
*/
|
|
164
|
+
export function forEachDrivenSlot(
|
|
165
|
+
schema: unknown,
|
|
166
|
+
data: unknown,
|
|
167
|
+
visit: (slot: DrivenSlot) => void,
|
|
168
|
+
path = "",
|
|
169
|
+
): void {
|
|
170
|
+
if (!schema || typeof schema !== "object" || data === undefined || data === null) return;
|
|
171
|
+
const node = schema as Record<string, any>;
|
|
172
|
+
|
|
173
|
+
const stepSlot = readStepSlot(node);
|
|
174
|
+
if (stepSlot) {
|
|
175
|
+
if (Array.isArray(data)) visit({ kind: "step", slot: stepSlot, data, path });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const refSlot = readRefSlot(node);
|
|
180
|
+
if (refSlot) {
|
|
181
|
+
visit({ kind: "ref", slot: refSlot, data, path });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const props = node.properties as Record<string, any> | undefined;
|
|
186
|
+
if (props && typeof data === "object" && !Array.isArray(data)) {
|
|
187
|
+
const obj = data as Record<string, unknown>;
|
|
188
|
+
for (const [key, propSchema] of Object.entries(props)) {
|
|
189
|
+
if (obj[key] === undefined) continue;
|
|
190
|
+
forEachDrivenSlot(propSchema, obj[key], visit, path ? `${path}.${key}` : key);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (node.items && Array.isArray(data)) {
|
|
195
|
+
for (const [i, item] of data.entries()) {
|
|
196
|
+
forEachDrivenSlot(node.items, item, visit, `${path}[${i}]`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
package/src/telo-version.ts
CHANGED
package/src/template-body.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import { isRefSentinel } from "@telorun/templating";
|
|
2
3
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
3
4
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
5
|
import type { ModuleScopes } from "./alias-resolver.js";
|
|
@@ -68,6 +69,30 @@ export function templateBodies(
|
|
|
68
69
|
return out;
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/** What a definition's dispatch slot names: the `resources:` entry a `!ref` at
|
|
73
|
+
* `slot` (`invoke` / `provide` / `run` / `mount`) resolves to, with its kind.
|
|
74
|
+
* The entry is matched by its LITERAL `metadata.name`; a slot holding anything
|
|
75
|
+
* but a `!ref`, or naming no entry, resolves to nothing — `validate-template-body`
|
|
76
|
+
* is what reports either. `Self.<entry>` is the explicit self-qualifier. */
|
|
77
|
+
export function dispatchTargetOf(
|
|
78
|
+
definition: Record<string, any>,
|
|
79
|
+
slot: string,
|
|
80
|
+
): { entry: Record<string, any> | undefined; kind: string; name: string } | undefined {
|
|
81
|
+
const value = definition[slot];
|
|
82
|
+
if (!isRefSentinel(value)) return undefined;
|
|
83
|
+
const source = value.source;
|
|
84
|
+
const name = source.startsWith("Self.") ? source.slice("Self.".length) : source;
|
|
85
|
+
const bodies = definition.resources;
|
|
86
|
+
if (!Array.isArray(bodies)) return undefined;
|
|
87
|
+
for (const body of bodies) {
|
|
88
|
+
if (!body || typeof body !== "object") continue;
|
|
89
|
+
const entry = body as Record<string, any>;
|
|
90
|
+
if (entry.metadata?.name !== name || typeof entry.kind !== "string") continue;
|
|
91
|
+
return { entry, kind: entry.kind, name };
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
71
96
|
/** True when a CEL path lies at or inside a body's own subtree. */
|
|
72
97
|
export function pathInBody(path: string, prefix: string): boolean {
|
|
73
98
|
return (
|
package/src/types.ts
CHANGED
|
@@ -11,6 +11,19 @@ export const DiagnosticSeverity = {
|
|
|
11
11
|
} as const;
|
|
12
12
|
export type DiagnosticSeverity = (typeof DiagnosticSeverity)[keyof typeof DiagnosticSeverity];
|
|
13
13
|
|
|
14
|
+
/** Matches LSP DiagnosticTag values exactly.
|
|
15
|
+
* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticTag
|
|
16
|
+
*
|
|
17
|
+
* Declared whole rather than trimmed to what Telo emits today, for the reason
|
|
18
|
+
* the severity ladder above is: it is someone else's closed vocabulary, and a
|
|
19
|
+
* partial copy of one is what drifts. `Unnecessary` (rendered faded) has an
|
|
20
|
+
* obvious future consumer in the unused-declaration checks. */
|
|
21
|
+
export const DiagnosticTag = {
|
|
22
|
+
Unnecessary: 1,
|
|
23
|
+
Deprecated: 2,
|
|
24
|
+
} as const;
|
|
25
|
+
export type DiagnosticTag = (typeof DiagnosticTag)[keyof typeof DiagnosticTag];
|
|
26
|
+
|
|
14
27
|
/** Default entry-point filename when a directory is given instead of a file. */
|
|
15
28
|
export const DEFAULT_MANIFEST_FILENAME = "telo.yaml";
|
|
16
29
|
|
|
@@ -70,6 +83,11 @@ export interface AnalysisDiagnostic {
|
|
|
70
83
|
/** e.g. "telo-analyzer" */
|
|
71
84
|
source?: string;
|
|
72
85
|
message: string;
|
|
86
|
+
/** What KIND of thing this is, orthogonal to how loudly it asks to be dealt
|
|
87
|
+
* with. A deprecation is warning-grade *and* a deprecation; severity alone
|
|
88
|
+
* can only say the first, which is why an editor renders a deprecated symbol
|
|
89
|
+
* struck through rather than merely yellow. */
|
|
90
|
+
tags?: DiagnosticTag[];
|
|
73
91
|
/** Telo-specific extras such as { resource: { kind, name }, path } */
|
|
74
92
|
data?: unknown;
|
|
75
93
|
}
|