@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.
@@ -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":"AAKA,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,CAgDT"}
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"}
@@ -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<{
@@ -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;AA0ezE,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"}
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
- for (const code of declaredCodes) {
225
- if (!covered.has(code)) {
226
- diagnostics.push({
227
- severity: DiagnosticSeverity.Error,
228
- code: "UNCOVERED_THROW_CODE",
229
- source: SOURCE,
230
- message: `Code '${code}' is declared by the handler but not covered by any catches: entry (no matching \`when:\` and no catch-all).`,
231
- data: { resource, filePath, path: arrayPath },
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.56.1",
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.12.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.70.0"
52
+ "@telorun/sdk": "0.72.0"
52
53
  },
53
54
  "peerDependencies": {
54
55
  "@telorun/sdk": "*"