@telorun/analyzer 0.19.1 → 0.21.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/README.md CHANGED
@@ -55,8 +55,8 @@ metadata:
55
55
  A complete feedback collection REST API — no code, pure YAML.
56
56
  Persists entries to SQLite and serves them over HTTP.
57
57
  imports:
58
- Http: std/http-server@0.8.0
59
- Sql: std/sql@0.5.1
58
+ Http: std/http-server@0.9.0
59
+ Sql: std/sql@0.8.0
60
60
  targets:
61
61
  - Migrations
62
62
  - Server
@@ -91,6 +91,10 @@ export declare class AnalysisRegistry {
91
91
  */
92
92
  builtinDefinitions(): ResourceDefinition[];
93
93
  resolveDefinition(kind: string): ResourceDefinition | undefined;
94
+ /** Canonical kinds (`module.Name`) of every definition that extends the given
95
+ * abstract kind — the concrete implementations a caller may instantiate in
96
+ * its place. Empty when `kind` is not an abstract or has no implementations. */
97
+ implementationsOf(kind: string): string[];
94
98
  allKinds(): string[];
95
99
  /** Returns every import alias that points at `moduleName` (the canonical, kebab-case
96
100
  * module name). Empty when no import declares that target. */
@@ -1 +1 @@
1
- {"version":3,"file":"analysis-registry.d.ts","sourceRoot":"","sources":["../src/analysis-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAKzE,OAAO,EAAqC,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAEhG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGlD;;wDAEwD;AACxD,MAAM,WAAW,YAAY;IAC3B;0CACsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,OAAO,EAAE,OAAO,CAAC;IACjB,uEAAuE;IACvE,IAAI,EAAE,MAAM,EAAE,CAAC;IACf;;;8EAG0E;IAC1E,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;GAIG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA4B;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAC/C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAoC;IAEpE,kBAAkB,CAAC,GAAG,EAAE,kBAAkB,GAAG,IAAI;IAIjD,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAIpE,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI;IAIpE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI7C;;;;;;;OAOG;IACH,mBAAmB,CACjB,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,EAClC,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,GACnC,IAAI;IAkBP;;;;;;OAMG;IACH,oBAAoB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,YAAY,EAAE;IAoBhE;;;;;;wBAMoB;IACpB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAQtD;;;;;;sFAMkF;IAClF,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAenE;;;;;qDAKiD;IACjD,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAepE,OAAO,CAAC,mBAAmB;IAS3B;;;;;;OAMG;IACH,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,EACxB,IAAI,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;KAAE,GACzF,IAAI;IAQP;;;;OAIG;IACH,kBAAkB,IAAI,kBAAkB,EAAE;IAI1C,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAM/D,QAAQ,IAAI,MAAM,EAAE;IAIpB;mEAC+D;IAC/D,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE;IAIxC;;;gCAG4B;IAC5B,oBAAoB,IAAI,MAAM,EAAE;IAIhC;wEACoE;IACpE,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIhD;;;;;;;;iDAQ6C;IAC7C,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,SAAS;IAoB9D;;;;;gEAK4D;IAC5D,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS;IAiB7D,qFAAqF;IACrF,QAAQ,IAAI,eAAe;CAG5B"}
1
+ {"version":3,"file":"analysis-registry.d.ts","sourceRoot":"","sources":["../src/analysis-registry.ts"],"names":[],"mappings":"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"}
@@ -171,6 +171,19 @@ export class AnalysisRegistry {
171
171
  const resolved = ctx.aliases?.resolveKind(kind);
172
172
  return ctx.definitions?.resolve(kind) ?? (resolved ? ctx.definitions?.resolve(resolved) : undefined);
173
173
  }
174
+ /** Canonical kinds (`module.Name`) of every definition that extends the given
175
+ * abstract kind — the concrete implementations a caller may instantiate in
176
+ * its place. Empty when `kind` is not an abstract or has no implementations. */
177
+ implementationsOf(kind) {
178
+ const defs = this._context().definitions;
179
+ if (!defs)
180
+ return [];
181
+ return defs.getByExtends(kind).flatMap((def) => {
182
+ const module = def.metadata?.module;
183
+ const name = def.metadata?.name;
184
+ return module && name ? [`${module}.${name}`] : [];
185
+ });
186
+ }
174
187
  allKinds() {
175
188
  return this._context().definitions?.kinds() ?? [];
176
189
  }
@@ -175,6 +175,8 @@ function traverseNode(node, path, map, root, visitedRefs = new Set()) {
175
175
  }
176
176
  return;
177
177
  }
178
+ if (typeof node?.$ref === "string")
179
+ return;
178
180
  // Array — recurse into items
179
181
  if (node.type === "array" && node.items) {
180
182
  traverseNode(node.items, path + "[]", map, root, visitedRefs);
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-ref-sentinels.d.ts","sourceRoot":"","sources":["../src/resolve-ref-sentinels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAQnE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE,aAAa,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,EAK5C,kBAAkB,GAAE,gBAAgB,EAAO,GAC1C,IAAI,CAwEN"}
1
+ {"version":3,"file":"resolve-ref-sentinels.d.ts","sourceRoot":"","sources":["../src/resolve-ref-sentinels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAQnE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE,aAAa,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,EAK5C,kBAAkB,GAAE,gBAAgB,EAAO,GAC1C,IAAI,CAmFN"}
@@ -1,5 +1,5 @@
1
1
  import { isRefSentinel } from "@telorun/templating";
2
- import { isRefEntry } from "./reference-field-map.js";
2
+ import { isRefEntry, isScopeEntry } from "./reference-field-map.js";
3
3
  import { REF_RESOLUTION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
4
4
  /**
5
5
  * Walks every `x-telo-ref` slot in every non-system resource and rewrites
@@ -81,22 +81,80 @@ crossModuleTargets = []) {
81
81
  }
82
82
  return undefined;
83
83
  };
84
- for (const r of resources) {
84
+ const processResource = (r) => {
85
85
  if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind))
86
- continue;
87
- if (isForeign(r))
88
- continue;
86
+ return;
89
87
  const fieldMap = aliases && aliasesByModule
90
88
  ? registry.expandedFieldMapForResource(r, aliases, aliasesByModule)
91
89
  : registry.getFieldMapForKind(r.kind, aliases);
92
90
  if (!fieldMap)
93
- continue;
91
+ return;
94
92
  for (const [fieldPath, entry] of fieldMap) {
95
- if (!isRefEntry(entry))
93
+ const parts = fieldPath.split(".");
94
+ if (isRefEntry(entry)) {
95
+ descend(r, parts, resolveTarget);
96
+ }
97
+ else if (isScopeEntry(entry)) {
98
+ // x-telo-scope resources (e.g. a Run.Sequence `with` server) carry their own ref
99
+ // slots. The top-level walk skips scope contents, so recurse so a scoped resource's
100
+ // `!ref` (e.g. an Http.Server mount) is canonicalized to {kind, name} like any other.
101
+ forEachScopeResource(r, parts, processResource);
102
+ }
103
+ }
104
+ };
105
+ for (const r of resources) {
106
+ if (isForeign(r))
107
+ continue;
108
+ processResource(r);
109
+ }
110
+ }
111
+ /**
112
+ * Navigates `obj` along the scope field path (dot notation, `[]` = array items) and
113
+ * invokes `cb` on every resource-like object found — any value carrying a `kind` string.
114
+ *
115
+ * Two-phase design:
116
+ *
117
+ * Phase 1 — path-walk: steps through each `parts` segment. `[]`-suffixed parts spread
118
+ * the array into individual elements so `current` always ends up holding scalars or
119
+ * plain objects, never intermediate arrays. Non-`[]` parts push the value as-is.
120
+ *
121
+ * Phase 2 — terminal visit: after the walk, `current` contains the values at the end
122
+ * of the path. These are always scalars or plain objects because of the `[]` spreading
123
+ * above, EXCEPT when a scope field is typed as an array in the schema but the path
124
+ * was authored WITHOUT a `[]` suffix. The `visit` function handles that case by
125
+ * recursing one level into arrays so `cb` is always called on resource objects, not
126
+ * on their container.
127
+ */
128
+ function forEachScopeResource(obj, parts, cb) {
129
+ let current = [obj];
130
+ for (const part of parts) {
131
+ const isArr = part.endsWith("[]");
132
+ const key = isArr ? part.slice(0, -2) : part;
133
+ const next = [];
134
+ for (const node of current) {
135
+ if (!node || typeof node !== "object")
136
+ continue;
137
+ const val = node[key];
138
+ if (val == null)
96
139
  continue;
97
- descend(r, fieldPath.split("."), resolveTarget);
140
+ if (isArr && Array.isArray(val))
141
+ next.push(...val);
142
+ else if (!isArr)
143
+ next.push(val);
98
144
  }
145
+ current = next;
99
146
  }
147
+ const visit = (node) => {
148
+ if (Array.isArray(node)) {
149
+ for (const elem of node)
150
+ visit(elem);
151
+ }
152
+ else if (node && typeof node === "object" && typeof node.kind === "string") {
153
+ cb(node);
154
+ }
155
+ };
156
+ for (const node of current)
157
+ visit(node);
100
158
  }
101
159
  /** Walks `obj` along `fieldPath` parts (dot notation with `[]` for arrays and `{}` for
102
160
  * additionalProperties-typed maps) and replaces any `!ref` sentinel at the terminal slot
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.19.1",
3
+ "version": "0.21.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -37,18 +37,18 @@
37
37
  "src/**"
38
38
  ],
39
39
  "dependencies": {
40
- "@marcbachmann/cel-js": "^7.5.3",
40
+ "@marcbachmann/cel-js": "^7.6.1",
41
41
  "ajv": "^8.17.1",
42
42
  "ajv-formats": "^3.0.1",
43
43
  "jsonpath-plus": "^10.3.0",
44
44
  "yaml": "^2.8.3",
45
- "@telorun/templating": "0.5.0"
45
+ "@telorun/templating": "0.7.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^20.0.0",
49
49
  "typescript": "^5.0.0",
50
50
  "vitest": "^2.1.8",
51
- "@telorun/sdk": "0.21.0"
51
+ "@telorun/sdk": "0.23.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@telorun/sdk": "*"
@@ -210,6 +210,19 @@ export class AnalysisRegistry {
210
210
  return ctx.definitions?.resolve(kind) ?? (resolved ? ctx.definitions?.resolve(resolved) : undefined);
211
211
  }
212
212
 
213
+ /** Canonical kinds (`module.Name`) of every definition that extends the given
214
+ * abstract kind — the concrete implementations a caller may instantiate in
215
+ * its place. Empty when `kind` is not an abstract or has no implementations. */
216
+ implementationsOf(kind: string): string[] {
217
+ const defs = this._context().definitions;
218
+ if (!defs) return [];
219
+ return defs.getByExtends(kind).flatMap((def) => {
220
+ const module = (def.metadata as { module?: string } | undefined)?.module;
221
+ const name = def.metadata?.name as string | undefined;
222
+ return module && name ? [`${module}.${name}`] : [];
223
+ });
224
+ }
225
+
213
226
  allKinds(): string[] {
214
227
  return this._context().definitions?.kinds() ?? [];
215
228
  }
@@ -229,6 +229,7 @@ function traverseNode(
229
229
  }
230
230
  return;
231
231
  }
232
+ if (typeof node?.$ref === "string") return;
232
233
 
233
234
  // Array — recurse into items
234
235
  if (node.type === "array" && node.items) {
@@ -2,7 +2,7 @@ import type { ResourceManifest } from "@telorun/sdk";
2
2
  import { isRefSentinel } from "@telorun/templating";
3
3
  import type { AliasResolver } from "./alias-resolver.js";
4
4
  import type { DefinitionRegistry } from "./definition-registry.js";
5
- import { isRefEntry } from "./reference-field-map.js";
5
+ import { isRefEntry, isScopeEntry } from "./reference-field-map.js";
6
6
  import { REF_RESOLUTION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
7
7
 
8
8
  /** Resolved ref shape written in place of a `!ref` sentinel. `alias` is set only for
@@ -96,21 +96,78 @@ export function resolveRefSentinels(
96
96
  return undefined;
97
97
  };
98
98
 
99
- for (const r of resources) {
100
- if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind)) continue;
101
- if (isForeign(r)) continue;
99
+ const processResource = (r: ResourceManifest): void => {
100
+ if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind)) return;
102
101
 
103
102
  const fieldMap =
104
103
  aliases && aliasesByModule
105
104
  ? registry.expandedFieldMapForResource(r, aliases, aliasesByModule)
106
105
  : registry.getFieldMapForKind(r.kind, aliases);
107
- if (!fieldMap) continue;
106
+ if (!fieldMap) return;
108
107
 
109
108
  for (const [fieldPath, entry] of fieldMap) {
110
- if (!isRefEntry(entry)) continue;
111
- descend(r as Record<string, unknown>, fieldPath.split("."), resolveTarget);
109
+ const parts = fieldPath.split(".");
110
+ if (isRefEntry(entry)) {
111
+ descend(r as Record<string, unknown>, parts, resolveTarget);
112
+ } else if (isScopeEntry(entry)) {
113
+ // x-telo-scope resources (e.g. a Run.Sequence `with` server) carry their own ref
114
+ // slots. The top-level walk skips scope contents, so recurse so a scoped resource's
115
+ // `!ref` (e.g. an Http.Server mount) is canonicalized to {kind, name} like any other.
116
+ forEachScopeResource(r as Record<string, unknown>, parts, processResource);
117
+ }
118
+ }
119
+ };
120
+
121
+ for (const r of resources) {
122
+ if (isForeign(r)) continue;
123
+ processResource(r);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Navigates `obj` along the scope field path (dot notation, `[]` = array items) and
129
+ * invokes `cb` on every resource-like object found — any value carrying a `kind` string.
130
+ *
131
+ * Two-phase design:
132
+ *
133
+ * Phase 1 — path-walk: steps through each `parts` segment. `[]`-suffixed parts spread
134
+ * the array into individual elements so `current` always ends up holding scalars or
135
+ * plain objects, never intermediate arrays. Non-`[]` parts push the value as-is.
136
+ *
137
+ * Phase 2 — terminal visit: after the walk, `current` contains the values at the end
138
+ * of the path. These are always scalars or plain objects because of the `[]` spreading
139
+ * above, EXCEPT when a scope field is typed as an array in the schema but the path
140
+ * was authored WITHOUT a `[]` suffix. The `visit` function handles that case by
141
+ * recursing one level into arrays so `cb` is always called on resource objects, not
142
+ * on their container.
143
+ */
144
+ function forEachScopeResource(
145
+ obj: Record<string, unknown>,
146
+ parts: string[],
147
+ cb: (resource: ResourceManifest) => void,
148
+ ): void {
149
+ let current: unknown[] = [obj];
150
+ for (const part of parts) {
151
+ const isArr = part.endsWith("[]");
152
+ const key = isArr ? part.slice(0, -2) : part;
153
+ const next: unknown[] = [];
154
+ for (const node of current) {
155
+ if (!node || typeof node !== "object") continue;
156
+ const val = (node as Record<string, unknown>)[key];
157
+ if (val == null) continue;
158
+ if (isArr && Array.isArray(val)) next.push(...val);
159
+ else if (!isArr) next.push(val);
112
160
  }
161
+ current = next;
113
162
  }
163
+ const visit = (node: unknown): void => {
164
+ if (Array.isArray(node)) {
165
+ for (const elem of node) visit(elem);
166
+ } else if (node && typeof node === "object" && typeof (node as { kind?: unknown }).kind === "string") {
167
+ cb(node as ResourceManifest);
168
+ }
169
+ };
170
+ for (const node of current) visit(node);
114
171
  }
115
172
 
116
173
  /** Walks `obj` along `fieldPath` parts (dot notation with `[]` for arrays and `{}` for