@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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { defaultCustomTags, defaultRegistry, walkCelExpressions, } from "@telorun/templating";
|
|
2
|
+
import { PackageURL } from "packageurl-js";
|
|
3
|
+
import { parseAllDocuments } from "yaml";
|
|
4
|
+
import { selectorFromQualifiers, selectorKey } from "./artifact-selector.js";
|
|
5
|
+
/** `pkg:telo/local/<format>?path=…` — the bundled-controller delivery mode.
|
|
6
|
+
* Anything else (`pkg:npm`, `pkg:cargo`) fetches from its own ecosystem and
|
|
7
|
+
* contributes no layer. */
|
|
8
|
+
const BUNDLED_TYPE = "telo";
|
|
9
|
+
const BUNDLED_NAMESPACE = "local";
|
|
10
|
+
/** Qualifier naming extra files that belong in a controller's layer — what an
|
|
11
|
+
* entry point loads but the manifest cannot otherwise see (a `.wasm` beside its
|
|
12
|
+
* glue, a native library opened at runtime). */
|
|
13
|
+
const SIBLINGS_QUALIFIER = "siblings";
|
|
14
|
+
/** Normalize a `path=` / sibling value to the manifest-relative POSIX form the
|
|
15
|
+
* file selector returns, so membership is a string comparison. */
|
|
16
|
+
function normalizeRelative(value) {
|
|
17
|
+
return value.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
18
|
+
}
|
|
19
|
+
/** Bundled-controller claims from one document's `controllers:` list. */
|
|
20
|
+
function controllerClaims(json) {
|
|
21
|
+
const candidates = json?.controllers;
|
|
22
|
+
if (!Array.isArray(candidates))
|
|
23
|
+
return [];
|
|
24
|
+
const claims = [];
|
|
25
|
+
for (const candidate of candidates) {
|
|
26
|
+
if (typeof candidate !== "string")
|
|
27
|
+
continue;
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = PackageURL.fromString(candidate);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Not a parseable PURL — claim collection is not the place to reject it;
|
|
34
|
+
// the analyzer's own validation and the controller loader both report it
|
|
35
|
+
// with better context.
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (parsed.type !== BUNDLED_TYPE || parsed.namespace !== BUNDLED_NAMESPACE)
|
|
39
|
+
continue;
|
|
40
|
+
const entry = parsed.qualifiers?.path;
|
|
41
|
+
if (typeof entry !== "string" || entry === "")
|
|
42
|
+
continue;
|
|
43
|
+
claims.push({
|
|
44
|
+
role: "controller",
|
|
45
|
+
path: normalizeRelative(entry),
|
|
46
|
+
selector: selectorFromQualifiers(parsed.name, parsed.qualifiers, `controller "${candidate}"`),
|
|
47
|
+
siblings: String(parsed.qualifiers?.[SIBLINGS_QUALIFIER] ?? "")
|
|
48
|
+
.split(",")
|
|
49
|
+
.map((p) => p.trim())
|
|
50
|
+
.filter((p) => p !== ""),
|
|
51
|
+
origin: candidate,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return claims;
|
|
55
|
+
}
|
|
56
|
+
/** Claims contributed by tagged values, asked of the engine that owns each tag.
|
|
57
|
+
* The walk reaches every tagged scalar in the document, so an engine that
|
|
58
|
+
* embeds files is discovered wherever its tag was written.
|
|
59
|
+
*
|
|
60
|
+
* The layer role is assigned HERE, not by the engine: an engine reports what it
|
|
61
|
+
* embeds, and which layer that belongs in is this module's vocabulary. A file a
|
|
62
|
+
* tag embeds is read only when the resource holding it is created, so `assets`
|
|
63
|
+
* — the lazily-fetched layer — is what it is. */
|
|
64
|
+
function taggedClaims(json, registry) {
|
|
65
|
+
const claims = [];
|
|
66
|
+
walkCelExpressions(json, "", (source, path, engineName) => {
|
|
67
|
+
const engine = registry.get(engineName);
|
|
68
|
+
for (const claim of engine?.fileClaims?.(source) ?? []) {
|
|
69
|
+
claims.push({ role: "assets", path: claim.path, origin: `!${engineName} at '${path}'` });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return claims;
|
|
73
|
+
}
|
|
74
|
+
/** Identity of a claim for de-duplication: the same file claimed twice by two
|
|
75
|
+
* resources is one file in one layer. Role and selector are part of it because
|
|
76
|
+
* a file two controller candidates both claim is genuinely copied into each of
|
|
77
|
+
* their layers — dropping one would leave a platform's layer short a file it
|
|
78
|
+
* declared it needs. */
|
|
79
|
+
function claimKey(claim) {
|
|
80
|
+
const selector = claim.role === "controller" ? selectorKey(claim.selector) : "";
|
|
81
|
+
return `${claim.role}\0${selector}\0${claim.path}`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Every module-relative file the manifest names, from every syntax that can name
|
|
85
|
+
* one.
|
|
86
|
+
*
|
|
87
|
+
* `manifestText` is one module's `telo.yaml`. Publish passes the text it is
|
|
88
|
+
* about to ship — i.e. after `include:` partials have been inlined — but the
|
|
89
|
+
* answer does not depend on that: claims are root-relative, so collecting them
|
|
90
|
+
* before or after inlining gives the same set.
|
|
91
|
+
*/
|
|
92
|
+
export function collectModuleFileClaims(manifestText, registry = defaultRegistry()) {
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
const claims = [];
|
|
95
|
+
for (const doc of parseAllDocuments(manifestText, { customTags: defaultCustomTags() })) {
|
|
96
|
+
const json = doc.toJSON();
|
|
97
|
+
for (const claim of [...controllerClaims(json), ...taggedClaims(json, registry)]) {
|
|
98
|
+
const key = claimKey(claim);
|
|
99
|
+
if (seen.has(key))
|
|
100
|
+
continue;
|
|
101
|
+
seen.add(key);
|
|
102
|
+
claims.push(claim);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return claims;
|
|
106
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-compat.d.ts","sourceRoot":"","sources":["../src/schema-compat.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"schema-compat.d.ts","sourceRoot":"","sources":["../src/schema-compat.ts"],"names":[],"mappings":"AAYA,QAAA,MAAM,GAAG,KAA0C,CAAC;AAEpD;;;;;;;mCAOmC;AACnC,wBAAgB,SAAS,IAAI,YAAY,CAAC,OAAO,GAAG,CAAC,CAWpD;AAKD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;oEAEoE;AACpE,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,mBAAmB,CAIrB;AAiDD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAelD;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAGxE;AAuBD,mFAAmF;AACnF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;CACd;AAaD,0GAA0G;AAC1G,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,WAAW,EAAE,CA2B/F;AAED;qFACqF;AACrF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAQ7E;AAED;;;;6DAI6D;AAC7D,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,EAAE,MAAM,GACX,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAsBjC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAGnD,CAAC;AAEF,wEAAwE;AACxE,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAGzF;AAED,8DAA8D;AAC9D,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GAAG,MAAM,CAyBnF;AAED,wFAAwF;AACxF,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAiChG;AAkED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CA0C/E;AAqBD,0EAA0E;AAC1E,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAOtG;AAyDD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAwBlF;AAED;iGACiG;AACjG,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;AAChC;;;;;;;;6DAQ6D;AAC7D,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EACrC,IAAI,SAAK,GACR,OAAO,CA2DT"}
|
package/dist/schema-compat.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import AjvModule from "ajv";
|
|
2
2
|
import addFormats from "ajv-formats";
|
|
3
|
-
import { isRefSentinel, isTaggedSentinel, ManifestRootSchema, normalizeRefSlots } from "@telorun/templating";
|
|
3
|
+
import { INCLUDE_BYTES_ENGINE, INCLUDE_ENGINE_NAMES, isRefSentinel, isTaggedSentinel, ManifestRootSchema, normalizeRefSlots, } from "@telorun/templating";
|
|
4
4
|
import { binaryKeyword, isBinarySlot } from "./binary-slot.js";
|
|
5
5
|
const Ajv = AjvModule.default ?? AjvModule;
|
|
6
6
|
/** Creates a configured AJV instance (allErrors, strict: false, with formats).
|
|
@@ -537,6 +537,17 @@ onSubstitute, path = "") {
|
|
|
537
537
|
if (isRefSentinel(data)) {
|
|
538
538
|
return data;
|
|
539
539
|
}
|
|
540
|
+
// A file embed's type is a CONSTANT of the tag, not a function of the slot:
|
|
541
|
+
// `!include-text` always produces a string and `!include-bytes` always
|
|
542
|
+
// produces bytes. Collapsing them to a slot-shaped placeholder like a CEL
|
|
543
|
+
// expression would make every slot accept both, so a byte embed at a
|
|
544
|
+
// `type: string` field passed `telo check` and failed at resource creation —
|
|
545
|
+
// and the reverse (text at an `x-telo-binary` slot) did too. Substituting the
|
|
546
|
+
// real type lets AJV and the `x-telo-binary` keyword reject both directions
|
|
547
|
+
// statically, with no new diagnostic code.
|
|
548
|
+
if (isTaggedSentinel(data) && INCLUDE_ENGINE_NAMES.has(data.engine)) {
|
|
549
|
+
return data.engine === INCLUDE_BYTES_ENGINE ? new Uint8Array() : "";
|
|
550
|
+
}
|
|
540
551
|
if (isTaggedSentinel(data)) {
|
|
541
552
|
mark();
|
|
542
553
|
return celPlaceholderForSchema(resolved);
|
package/dist/types.d.ts
CHANGED
|
@@ -26,6 +26,37 @@ export interface Range {
|
|
|
26
26
|
export type PositionIndex = Map<string, Range>;
|
|
27
27
|
/** LSP-compatible Diagnostic shape. range is optional because parsed YAML may not carry
|
|
28
28
|
* position info when only the parsed object (not raw text) is available. */
|
|
29
|
+
/** A mechanically applicable repair, carried unchanged from whatever produced
|
|
30
|
+
* it (a templating engine, a kind-name suggestion) to every consumer: CLI
|
|
31
|
+
* JSON, IDE CodeActions, an agent applying it without re-deriving it from
|
|
32
|
+
* prose.
|
|
33
|
+
*
|
|
34
|
+
* `replacement` is the **whole** value at the diagnostic's `path`, corrected —
|
|
35
|
+
* never a fragment — so applying it needs no knowledge of the language inside.
|
|
36
|
+
* There is deliberately no sub-range: carrying one beside a whole-value
|
|
37
|
+
* replacement gives the field two readings, and the minimal-edit reading
|
|
38
|
+
* (splice `replacement` at `range`) produces garbage because the two measure
|
|
39
|
+
* different strings.
|
|
40
|
+
*
|
|
41
|
+
* One shape rather than one per producer: a `fix` field beside a
|
|
42
|
+
* `suggestedKind` field beside a CEL-specific one would leave every host
|
|
43
|
+
* wiring a separate action path for what is the same gesture. */
|
|
44
|
+
export interface DiagnosticFix {
|
|
45
|
+
readonly replacement: string;
|
|
46
|
+
}
|
|
47
|
+
/** The `data` stamp diagnostics carry. Loose by design — passes bolt their own
|
|
48
|
+
* keys on — but the fields every consumer reads are declared. */
|
|
49
|
+
export interface DiagnosticData {
|
|
50
|
+
resource?: {
|
|
51
|
+
kind: string;
|
|
52
|
+
name: string;
|
|
53
|
+
};
|
|
54
|
+
filePath?: string;
|
|
55
|
+
/** Dotted path of the offending value within its resource. */
|
|
56
|
+
path?: string;
|
|
57
|
+
fix?: DiagnosticFix;
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
}
|
|
29
60
|
export interface AnalysisDiagnostic {
|
|
30
61
|
range?: Range;
|
|
31
62
|
severity?: DiagnosticSeverity;
|
|
@@ -36,6 +67,9 @@ export interface AnalysisDiagnostic {
|
|
|
36
67
|
/** Telo-specific extras such as { resource: { kind, name }, path } */
|
|
37
68
|
data?: unknown;
|
|
38
69
|
}
|
|
70
|
+
/** Single reader for a diagnostic's fix, so no consumer re-derives the shape
|
|
71
|
+
* by hand-casting `data`. */
|
|
72
|
+
export declare function diagnosticFix(d: AnalysisDiagnostic): DiagnosticFix | undefined;
|
|
39
73
|
export interface ManifestSource {
|
|
40
74
|
supports(url: string): boolean;
|
|
41
75
|
read(url: string): Promise<{
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtE;qHACqH;AACrH,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAE9F,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,cAAc,CAAC;AAErD,MAAM,WAAW,QAAQ;IACvB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACf;AAED;;oDAEoD;AACpD,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE/C;6EAC6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAExD;;qEAEiE;IACjE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEjE;;qEAEiE;IACjE,cAAc,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B;;;+EAG2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;mEAO+D;IAC/D,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC;6FACyF;IACzF,WAAW,CAAC,EAAE,OAAO,sBAAsB,EAAE,WAAW,CAAC;CAC1D;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;oCAKgC;IAChC,eAAe,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACxC;;;;;;;;;;sDAUkD;IAClD,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;gEAKgE;AAChE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC;IACtD,WAAW,CAAC,EAAE,OAAO,0BAA0B,EAAE,kBAAkB,CAAC;IACpE;;;;+EAI2E;IAC3E,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC,CAAC;CAC5E"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
CHANGED
|
@@ -8,3 +8,9 @@ export const DiagnosticSeverity = {
|
|
|
8
8
|
};
|
|
9
9
|
/** Default entry-point filename when a directory is given instead of a file. */
|
|
10
10
|
export const DEFAULT_MANIFEST_FILENAME = "telo.yaml";
|
|
11
|
+
/** Single reader for a diagnostic's fix, so no consumer re-derives the shape
|
|
12
|
+
* by hand-casting `data`. */
|
|
13
|
+
export function diagnosticFix(d) {
|
|
14
|
+
const fix = d.data?.fix;
|
|
15
|
+
return fix && typeof fix.replacement === "string" ? fix : undefined;
|
|
16
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import { type AnalysisDiagnostic } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* An `!include-text` / `!include-bytes` in a doc that is never instantiated is
|
|
5
|
+
* never read.
|
|
6
|
+
*
|
|
7
|
+
* The two tags resolve when the resource owning them is created — deferred so
|
|
8
|
+
* that loading a manifest does not pull payload layers, which is the property
|
|
9
|
+
* the artifact spec protects by giving `telo.yaml` a layer of its own. The cost
|
|
10
|
+
* of that choice is this dead spot: a doc with no `create()` has no moment at
|
|
11
|
+
* which the file would be read, so the value stays an unresolved marker and
|
|
12
|
+
* whatever reads it sees a sentinel object instead of the file's contents.
|
|
13
|
+
*
|
|
14
|
+
* Nothing else would report it. There is no controller to validate against a
|
|
15
|
+
* schema and no runtime consumer to fail, so the manifest ships looking correct
|
|
16
|
+
* — exactly the silent-no-op failure mode that makes a descriptive field worth
|
|
17
|
+
* checking. Hence an error at the one place that can see it.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately narrow: it reports only where the argument is complete ("this
|
|
20
|
+
* doc is never instantiated"). A JSON-Schema region *inside* a definition —
|
|
21
|
+
* `schema:`, `inputType:` — is equally unreadable, but that is a general
|
|
22
|
+
* question about tags in schema metadata rather than one about these two, and
|
|
23
|
+
* no pass answers it for any tag today.
|
|
24
|
+
*/
|
|
25
|
+
export declare function validateIncludePlacement(manifests: ResourceManifest[]): AnalysisDiagnostic[];
|
|
26
|
+
//# sourceMappingURL=validate-include-placement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-include-placement.d.ts","sourceRoot":"","sources":["../src/validate-include-placement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAmBzE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,gBAAgB,EAAE,GAAG,kBAAkB,EAAE,CA0B5F"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { INCLUDE_ENGINE_NAMES, walkCelExpressions } from "@telorun/templating";
|
|
2
|
+
import { DiagnosticSeverity } from "./types.js";
|
|
3
|
+
const SOURCE = "telo-analyzer";
|
|
4
|
+
/**
|
|
5
|
+
* Docs that are never instantiated as resources.
|
|
6
|
+
*
|
|
7
|
+
* `Telo.Application` / `Telo.Library` are module declarations, and `Telo.Import`
|
|
8
|
+
* is a dependency edge — none of the three reaches a controller's `create()`.
|
|
9
|
+
* `Telo.Definition` and `Telo.Abstract` are deliberately absent: a definition's
|
|
10
|
+
* template body (`resources:` / `invoke:` / `run:` / `provide:`) DOES become
|
|
11
|
+
* resources, so an embed there resolves normally.
|
|
12
|
+
*/
|
|
13
|
+
const NEVER_INSTANTIATED = new Set([
|
|
14
|
+
"Telo.Application",
|
|
15
|
+
"Telo.Library",
|
|
16
|
+
"Telo.Import",
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* An `!include-text` / `!include-bytes` in a doc that is never instantiated is
|
|
20
|
+
* never read.
|
|
21
|
+
*
|
|
22
|
+
* The two tags resolve when the resource owning them is created — deferred so
|
|
23
|
+
* that loading a manifest does not pull payload layers, which is the property
|
|
24
|
+
* the artifact spec protects by giving `telo.yaml` a layer of its own. The cost
|
|
25
|
+
* of that choice is this dead spot: a doc with no `create()` has no moment at
|
|
26
|
+
* which the file would be read, so the value stays an unresolved marker and
|
|
27
|
+
* whatever reads it sees a sentinel object instead of the file's contents.
|
|
28
|
+
*
|
|
29
|
+
* Nothing else would report it. There is no controller to validate against a
|
|
30
|
+
* schema and no runtime consumer to fail, so the manifest ships looking correct
|
|
31
|
+
* — exactly the silent-no-op failure mode that makes a descriptive field worth
|
|
32
|
+
* checking. Hence an error at the one place that can see it.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately narrow: it reports only where the argument is complete ("this
|
|
35
|
+
* doc is never instantiated"). A JSON-Schema region *inside* a definition —
|
|
36
|
+
* `schema:`, `inputType:` — is equally unreadable, but that is a general
|
|
37
|
+
* question about tags in schema metadata rather than one about these two, and
|
|
38
|
+
* no pass answers it for any tag today.
|
|
39
|
+
*/
|
|
40
|
+
export function validateIncludePlacement(manifests) {
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const manifest of manifests) {
|
|
43
|
+
if (!NEVER_INSTANTIATED.has(manifest.kind))
|
|
44
|
+
continue;
|
|
45
|
+
const name = manifest.metadata?.name;
|
|
46
|
+
const filePath = manifest.metadata?.source;
|
|
47
|
+
walkCelExpressions(manifest, "", (source, path, engineName) => {
|
|
48
|
+
if (!INCLUDE_ENGINE_NAMES.has(engineName))
|
|
49
|
+
return;
|
|
50
|
+
out.push({
|
|
51
|
+
severity: DiagnosticSeverity.Error,
|
|
52
|
+
code: "INCLUDE_OUTSIDE_RESOURCE",
|
|
53
|
+
source: SOURCE,
|
|
54
|
+
message: `${manifest.kind}${name ? `/${name}` : ""}: \`!${engineName} ${source}\` at '${path}' is ` +
|
|
55
|
+
`never read — a ${manifest.kind} doc is not instantiated, and a file embed is resolved ` +
|
|
56
|
+
`when the resource holding it is created. Move it onto the resource that needs the ` +
|
|
57
|
+
`file, or read the file at runtime with Fs.File.`,
|
|
58
|
+
data: {
|
|
59
|
+
resource: { kind: manifest.kind, name: name ?? "" },
|
|
60
|
+
filePath,
|
|
61
|
+
path,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-throws-coverage.d.ts","sourceRoot":"","sources":["../src/validate-throws-coverage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAOnE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"validate-throws-coverage.d.ts","sourceRoot":"","sources":["../src/validate-throws-coverage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAOnE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AA+ezE,oDAAoD;AACpD,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,WAAW,EAChB,eAAe,GAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAa,EACvD,WAAW,GAAE,GAAG,CAAC,MAAM,CAAa,GACnC,kBAAkB,EAAE,CAkDtB"}
|
|
@@ -171,7 +171,7 @@ function checkCatchAllPlacement(entries, resource, channel, filePath, arrayPath)
|
|
|
171
171
|
* in coverage-proving `when:` clauses. Phase 2 accepts inherit/passthrough
|
|
172
172
|
* handler unions too — when the resolved union is unbounded, a catch-all is
|
|
173
173
|
* required (rule 4 extension). */
|
|
174
|
-
function checkCatchesCoverage(entries, union, resource, filePath, arrayPath, env) {
|
|
174
|
+
function checkCatchesCoverage(entries, union, resource, filePath, arrayPath, env, handler) {
|
|
175
175
|
const diagnostics = [];
|
|
176
176
|
const declaredCodes = new Set(union.codes.keys());
|
|
177
177
|
const covered = new Set();
|
|
@@ -221,16 +221,19 @@ function checkCatchesCoverage(entries, union, resource, filePath, arrayPath, env
|
|
|
221
221
|
});
|
|
222
222
|
}
|
|
223
223
|
if (!hasCatchAll) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
224
|
+
// One diagnostic per block, not per code: every uncovered code sits at the
|
|
225
|
+
// same `catches:` array, and one catch-all answers all of them at once. A
|
|
226
|
+
// diagnostic each repeated the same location and the same fix N times.
|
|
227
|
+
const uncovered = [...declaredCodes].filter((c) => !covered.has(c)).sort();
|
|
228
|
+
if (uncovered.length > 0) {
|
|
229
|
+
diagnostics.push({
|
|
230
|
+
severity: DiagnosticSeverity.Error,
|
|
231
|
+
code: "UNCOVERED_THROW_CODE",
|
|
232
|
+
source: SOURCE,
|
|
233
|
+
message: `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(", ")}. ` +
|
|
234
|
+
`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.`,
|
|
235
|
+
data: { resource, filePath, path: arrayPath, uncovered },
|
|
236
|
+
});
|
|
234
237
|
}
|
|
235
238
|
}
|
|
236
239
|
return diagnostics;
|
|
@@ -447,7 +450,7 @@ export function validateThrowsCoverage(manifests, defs, aliases, env, aliasesByM
|
|
|
447
450
|
diagnostics.push(...checkCatchAllPlacement(entries, resource, "catches", filePath, arrayPath));
|
|
448
451
|
const handlerRef = resolveHandlerRef(siblingData[catchesFor]);
|
|
449
452
|
const union = handlerRefUnion(handlerRef, manifests, resolveCtx, scopeResolver);
|
|
450
|
-
diagnostics.push(...checkCatchesCoverage(entries, union, resource, filePath, arrayPath, env));
|
|
453
|
+
diagnostics.push(...checkCatchesCoverage(entries, union, resource, filePath, arrayPath, env, handlerRef));
|
|
451
454
|
diagnostics.push(...checkTypedErrorData(entries, union, resource, filePath, arrayPath, env));
|
|
452
455
|
});
|
|
453
456
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -41,14 +41,15 @@
|
|
|
41
41
|
"ajv": "^8.17.1",
|
|
42
42
|
"ajv-formats": "^3.0.1",
|
|
43
43
|
"jsonpath-plus": "^10.3.0",
|
|
44
|
+
"packageurl-js": "^2.0.1",
|
|
44
45
|
"yaml": "^2.8.3",
|
|
45
|
-
"@telorun/templating": "0.
|
|
46
|
+
"@telorun/templating": "0.13.0"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@types/node": "^20.0.0",
|
|
49
50
|
"typescript": "^5.0.0",
|
|
50
51
|
"vitest": "^2.1.8",
|
|
51
|
-
"@telorun/sdk": "0.
|
|
52
|
+
"@telorun/sdk": "0.72.0"
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|
|
54
55
|
"@telorun/sdk": "*"
|