@telorun/analyzer 0.56.1 → 0.57.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 +5 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +146 -90
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/manifest-visitor.d.ts +4 -0
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +3 -3
- package/dist/module-file-claims.d.ts +65 -0
- package/dist/module-file-claims.d.ts.map +1 -0
- package/dist/module-file-claims.js +106 -0
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +12 -1
- package/dist/types.d.ts +34 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/validate-include-placement.d.ts +26 -0
- package/dist/validate-include-placement.d.ts.map +1 -0
- package/dist/validate-include-placement.js +67 -0
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +15 -12
- package/package.json +4 -3
- package/src/analyzer.ts +181 -127
- package/src/index.ts +5 -1
- package/src/manifest-visitor.ts +11 -3
- package/src/module-file-claims.ts +168 -0
- package/src/schema-compat.ts +19 -1
- package/src/types.ts +37 -0
- package/src/validate-include-placement.ts +70 -0
- package/src/validate-throws-coverage.ts +16 -11
package/src/schema-compat.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import AjvModule from "ajv";
|
|
2
2
|
import addFormats from "ajv-formats";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
INCLUDE_BYTES_ENGINE,
|
|
5
|
+
INCLUDE_ENGINE_NAMES,
|
|
6
|
+
isRefSentinel,
|
|
7
|
+
isTaggedSentinel,
|
|
8
|
+
ManifestRootSchema,
|
|
9
|
+
normalizeRefSlots,
|
|
10
|
+
} from "@telorun/templating";
|
|
4
11
|
import { binaryKeyword, isBinarySlot } from "./binary-slot.js";
|
|
5
12
|
|
|
6
13
|
const Ajv = (AjvModule as any).default ?? AjvModule;
|
|
@@ -564,6 +571,17 @@ export function substituteCelFields(
|
|
|
564
571
|
if (isRefSentinel(data)) {
|
|
565
572
|
return data;
|
|
566
573
|
}
|
|
574
|
+
// A file embed's type is a CONSTANT of the tag, not a function of the slot:
|
|
575
|
+
// `!include-text` always produces a string and `!include-bytes` always
|
|
576
|
+
// produces bytes. Collapsing them to a slot-shaped placeholder like a CEL
|
|
577
|
+
// expression would make every slot accept both, so a byte embed at a
|
|
578
|
+
// `type: string` field passed `telo check` and failed at resource creation —
|
|
579
|
+
// and the reverse (text at an `x-telo-binary` slot) did too. Substituting the
|
|
580
|
+
// real type lets AJV and the `x-telo-binary` keyword reject both directions
|
|
581
|
+
// statically, with no new diagnostic code.
|
|
582
|
+
if (isTaggedSentinel(data) && INCLUDE_ENGINE_NAMES.has(data.engine)) {
|
|
583
|
+
return data.engine === INCLUDE_BYTES_ENGINE ? new Uint8Array() : "";
|
|
584
|
+
}
|
|
567
585
|
if (isTaggedSentinel(data)) {
|
|
568
586
|
mark();
|
|
569
587
|
return celPlaceholderForSchema(resolved);
|
package/src/types.ts
CHANGED
|
@@ -31,6 +31,36 @@ export type PositionIndex = Map<string, Range>;
|
|
|
31
31
|
|
|
32
32
|
/** LSP-compatible Diagnostic shape. range is optional because parsed YAML may not carry
|
|
33
33
|
* position info when only the parsed object (not raw text) is available. */
|
|
34
|
+
/** A mechanically applicable repair, carried unchanged from whatever produced
|
|
35
|
+
* it (a templating engine, a kind-name suggestion) to every consumer: CLI
|
|
36
|
+
* JSON, IDE CodeActions, an agent applying it without re-deriving it from
|
|
37
|
+
* prose.
|
|
38
|
+
*
|
|
39
|
+
* `replacement` is the **whole** value at the diagnostic's `path`, corrected —
|
|
40
|
+
* never a fragment — so applying it needs no knowledge of the language inside.
|
|
41
|
+
* There is deliberately no sub-range: carrying one beside a whole-value
|
|
42
|
+
* replacement gives the field two readings, and the minimal-edit reading
|
|
43
|
+
* (splice `replacement` at `range`) produces garbage because the two measure
|
|
44
|
+
* different strings.
|
|
45
|
+
*
|
|
46
|
+
* One shape rather than one per producer: a `fix` field beside a
|
|
47
|
+
* `suggestedKind` field beside a CEL-specific one would leave every host
|
|
48
|
+
* wiring a separate action path for what is the same gesture. */
|
|
49
|
+
export interface DiagnosticFix {
|
|
50
|
+
readonly replacement: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The `data` stamp diagnostics carry. Loose by design — passes bolt their own
|
|
54
|
+
* keys on — but the fields every consumer reads are declared. */
|
|
55
|
+
export interface DiagnosticData {
|
|
56
|
+
resource?: { kind: string; name: string };
|
|
57
|
+
filePath?: string;
|
|
58
|
+
/** Dotted path of the offending value within its resource. */
|
|
59
|
+
path?: string;
|
|
60
|
+
fix?: DiagnosticFix;
|
|
61
|
+
[key: string]: unknown;
|
|
62
|
+
}
|
|
63
|
+
|
|
34
64
|
export interface AnalysisDiagnostic {
|
|
35
65
|
range?: Range;
|
|
36
66
|
severity?: DiagnosticSeverity;
|
|
@@ -42,6 +72,13 @@ export interface AnalysisDiagnostic {
|
|
|
42
72
|
data?: unknown;
|
|
43
73
|
}
|
|
44
74
|
|
|
75
|
+
/** Single reader for a diagnostic's fix, so no consumer re-derives the shape
|
|
76
|
+
* by hand-casting `data`. */
|
|
77
|
+
export function diagnosticFix(d: AnalysisDiagnostic): DiagnosticFix | undefined {
|
|
78
|
+
const fix = (d.data as DiagnosticData | undefined)?.fix;
|
|
79
|
+
return fix && typeof fix.replacement === "string" ? fix : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
45
82
|
export interface ManifestSource {
|
|
46
83
|
supports(url: string): boolean;
|
|
47
84
|
read(url: string): Promise<{ text: string; source: string }>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import { INCLUDE_ENGINE_NAMES, walkCelExpressions } from "@telorun/templating";
|
|
3
|
+
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const SOURCE = "telo-analyzer";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Docs that are never instantiated as resources.
|
|
9
|
+
*
|
|
10
|
+
* `Telo.Application` / `Telo.Library` are module declarations, and `Telo.Import`
|
|
11
|
+
* is a dependency edge — none of the three reaches a controller's `create()`.
|
|
12
|
+
* `Telo.Definition` and `Telo.Abstract` are deliberately absent: a definition's
|
|
13
|
+
* template body (`resources:` / `invoke:` / `run:` / `provide:`) DOES become
|
|
14
|
+
* resources, so an embed there resolves normally.
|
|
15
|
+
*/
|
|
16
|
+
const NEVER_INSTANTIATED: ReadonlySet<string> = new Set([
|
|
17
|
+
"Telo.Application",
|
|
18
|
+
"Telo.Library",
|
|
19
|
+
"Telo.Import",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* An `!include-text` / `!include-bytes` in a doc that is never instantiated is
|
|
24
|
+
* never read.
|
|
25
|
+
*
|
|
26
|
+
* The two tags resolve when the resource owning them is created — deferred so
|
|
27
|
+
* that loading a manifest does not pull payload layers, which is the property
|
|
28
|
+
* the artifact spec protects by giving `telo.yaml` a layer of its own. The cost
|
|
29
|
+
* of that choice is this dead spot: a doc with no `create()` has no moment at
|
|
30
|
+
* which the file would be read, so the value stays an unresolved marker and
|
|
31
|
+
* whatever reads it sees a sentinel object instead of the file's contents.
|
|
32
|
+
*
|
|
33
|
+
* Nothing else would report it. There is no controller to validate against a
|
|
34
|
+
* schema and no runtime consumer to fail, so the manifest ships looking correct
|
|
35
|
+
* — exactly the silent-no-op failure mode that makes a descriptive field worth
|
|
36
|
+
* checking. Hence an error at the one place that can see it.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately narrow: it reports only where the argument is complete ("this
|
|
39
|
+
* doc is never instantiated"). A JSON-Schema region *inside* a definition —
|
|
40
|
+
* `schema:`, `inputType:` — is equally unreadable, but that is a general
|
|
41
|
+
* question about tags in schema metadata rather than one about these two, and
|
|
42
|
+
* no pass answers it for any tag today.
|
|
43
|
+
*/
|
|
44
|
+
export function validateIncludePlacement(manifests: ResourceManifest[]): AnalysisDiagnostic[] {
|
|
45
|
+
const out: AnalysisDiagnostic[] = [];
|
|
46
|
+
for (const manifest of manifests) {
|
|
47
|
+
if (!NEVER_INSTANTIATED.has(manifest.kind)) continue;
|
|
48
|
+
const name = (manifest.metadata as { name?: string } | undefined)?.name;
|
|
49
|
+
const filePath = (manifest.metadata as { source?: string } | undefined)?.source;
|
|
50
|
+
walkCelExpressions(manifest, "", (source, path, engineName) => {
|
|
51
|
+
if (!INCLUDE_ENGINE_NAMES.has(engineName)) return;
|
|
52
|
+
out.push({
|
|
53
|
+
severity: DiagnosticSeverity.Error,
|
|
54
|
+
code: "INCLUDE_OUTSIDE_RESOURCE",
|
|
55
|
+
source: SOURCE,
|
|
56
|
+
message:
|
|
57
|
+
`${manifest.kind}${name ? `/${name}` : ""}: \`!${engineName} ${source}\` at '${path}' is ` +
|
|
58
|
+
`never read — a ${manifest.kind} doc is not instantiated, and a file embed is resolved ` +
|
|
59
|
+
`when the resource holding it is created. Move it onto the resource that needs the ` +
|
|
60
|
+
`file, or read the file at runtime with Fs.File.`,
|
|
61
|
+
data: {
|
|
62
|
+
resource: { kind: manifest.kind, name: name ?? "" },
|
|
63
|
+
filePath,
|
|
64
|
+
path,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
@@ -238,6 +238,7 @@ function checkCatchesCoverage(
|
|
|
238
238
|
filePath: string | undefined,
|
|
239
239
|
arrayPath: string,
|
|
240
240
|
env: Environment,
|
|
241
|
+
handler: { kind: string; name?: string } | null,
|
|
241
242
|
): AnalysisDiagnostic[] {
|
|
242
243
|
const diagnostics: AnalysisDiagnostic[] = [];
|
|
243
244
|
const declaredCodes = new Set(union.codes.keys());
|
|
@@ -288,16 +289,20 @@ function checkCatchesCoverage(
|
|
|
288
289
|
}
|
|
289
290
|
|
|
290
291
|
if (!hasCatchAll) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
292
|
+
// One diagnostic per block, not per code: every uncovered code sits at the
|
|
293
|
+
// same `catches:` array, and one catch-all answers all of them at once. A
|
|
294
|
+
// diagnostic each repeated the same location and the same fix N times.
|
|
295
|
+
const uncovered = [...declaredCodes].filter((c) => !covered.has(c)).sort();
|
|
296
|
+
if (uncovered.length > 0) {
|
|
297
|
+
diagnostics.push({
|
|
298
|
+
severity: DiagnosticSeverity.Error,
|
|
299
|
+
code: "UNCOVERED_THROW_CODE",
|
|
300
|
+
source: SOURCE,
|
|
301
|
+
message:
|
|
302
|
+
`handler ${handler?.name ? `\`!ref ${handler.name}\`` : `\`${handler?.kind ?? "?"}\``} can throw ${uncovered.length} code${uncovered.length === 1 ? "" : "s"} that no catches: entry handles: ${uncovered.map((c) => `'${c}'`).join(", ")}. ` +
|
|
303
|
+
`Give each a matching \`when:\` (e.g. \`when: !cel "error.code == '${uncovered[0]}'"\`), or add a catch-all entry — one with no \`when:\`, placed last.`,
|
|
304
|
+
data: { resource, filePath, path: arrayPath, uncovered },
|
|
305
|
+
});
|
|
301
306
|
}
|
|
302
307
|
}
|
|
303
308
|
|
|
@@ -552,7 +557,7 @@ export function validateThrowsCoverage(
|
|
|
552
557
|
const handlerRef = resolveHandlerRef(siblingData[catchesFor]);
|
|
553
558
|
const union = handlerRefUnion(handlerRef, manifests, resolveCtx, scopeResolver);
|
|
554
559
|
diagnostics.push(
|
|
555
|
-
...checkCatchesCoverage(entries, union, resource, filePath, arrayPath, env),
|
|
560
|
+
...checkCatchesCoverage(entries, union, resource, filePath, arrayPath, env, handlerRef),
|
|
556
561
|
);
|
|
557
562
|
diagnostics.push(
|
|
558
563
|
...checkTypedErrorData(entries, union, resource, filePath, arrayPath, env),
|