@telorun/analyzer 0.42.0 → 0.44.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/analysis-registry.d.ts +4 -4
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +3 -3
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +92 -3
- package/dist/builtins.js +7 -7
- package/dist/definition-registry.d.ts +49 -22
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +67 -61
- package/dist/loaded-types.d.ts +13 -6
- package/dist/loaded-types.d.ts.map +1 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +1 -0
- package/dist/reconcile-module-versions.d.ts +2 -2
- package/dist/reconcile-module-versions.d.ts.map +1 -1
- package/dist/reconcile-module-versions.js +69 -17
- package/dist/reference-field-map.d.ts +2 -1
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +1 -1
- package/dist/resolve-schema-ref-kinds.d.ts +62 -0
- package/dist/resolve-schema-ref-kinds.d.ts.map +1 -0
- package/dist/resolve-schema-ref-kinds.js +81 -0
- package/dist/sources/manifest-cache.d.ts +14 -4
- package/dist/sources/manifest-cache.d.ts.map +1 -1
- package/dist/sources/manifest-cache.js +15 -6
- package/dist/validate-nested-inline.d.ts +3 -1
- package/dist/validate-nested-inline.d.ts.map +1 -1
- package/dist/validate-nested-inline.js +14 -2
- package/dist/validate-value-schema.d.ts +13 -0
- package/dist/validate-value-schema.d.ts.map +1 -0
- package/dist/validate-value-schema.js +114 -0
- package/package.json +2 -2
- package/src/analysis-registry.ts +4 -4
- package/src/analyzer.ts +97 -3
- package/src/builtins.ts +7 -7
- package/src/definition-registry.ts +67 -66
- package/src/loaded-types.ts +13 -6
- package/src/manifest-loader.ts +1 -0
- package/src/reconcile-module-versions.ts +69 -17
- package/src/reference-field-map.ts +3 -2
- package/src/resolve-schema-ref-kinds.ts +118 -0
- package/src/sources/manifest-cache.ts +26 -8
- package/src/validate-nested-inline.ts +13 -1
- package/src/validate-value-schema.ts +126 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAGV,UAAU,EACV,WAAW,EACX,YAAY,EACb,MAAM,mBAAmB,CAAC;AAK3B,OAAO,EAIL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACpB,MAAM,YAAY,CAAC;AAwCpB,qBAAa,MAAM;IACjB;;;yEAGqE;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiC;IAE3D;;;;;8BAK0B;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IAEzD,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IAErC;;;;mBAIe;gBACH,OAAO,GAAE,cAAc,EAAO,EAAE,OAAO,GAAE,iBAAsB;IAK3E,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAKtC,OAAO,CAAC,IAAI;IAMN,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAUrD;;;;2CAIuC;IACvC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAM7C;;;+BAG2B;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAyCvE;;;;gEAI4D;IAC5D,OAAO,CAAC,oBAAoB;IAa5B;gFAC4E;IAC5E,OAAO,CAAC,cAAc;IAQtB;;wEAEoE;IAC9D,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAsB3E;;;qCAGiC;IAC3B,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAGV,UAAU,EACV,WAAW,EACX,YAAY,EACb,MAAM,mBAAmB,CAAC;AAK3B,OAAO,EAIL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACpB,MAAM,YAAY,CAAC;AAwCpB,qBAAa,MAAM;IACjB;;;yEAGqE;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiC;IAE3D;;;;;8BAK0B;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IAEzD,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IAErC;;;;mBAIe;gBACH,OAAO,GAAE,cAAc,EAAO,EAAE,OAAO,GAAE,iBAAsB;IAK3E,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAKtC,OAAO,CAAC,IAAI;IAMN,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAUrD;;;;2CAIuC;IACvC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAM7C;;;+BAG2B;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAyCvE;;;;gEAI4D;IAC5D,OAAO,CAAC,oBAAoB;IAa5B;gFAC4E;IAC5E,OAAO,CAAC,cAAc;IAQtB;;wEAEoE;IAC9D,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAsB3E;;;qCAGiC;IAC3B,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAiI9E;;;;;;;;sBAQkB;IAClB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAOlE,OAAO,CAAC,6BAA6B;IAarC,OAAO,CAAC,mCAAmC;IAc3C,OAAO,CAAC,2BAA2B;YAkCrB,eAAe;IAmB7B;;;0CAGsC;IAChC,gBAAgB,CACpB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CA0B5D"}
|
package/dist/manifest-loader.js
CHANGED
|
@@ -13,8 +13,8 @@ export interface VersionReconciliation {
|
|
|
13
13
|
diagnostics: AnalysisDiagnostic[];
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* Reconcile a loaded import graph so each module
|
|
17
|
-
* resolves to a single version. Within a shared major the highest version wins
|
|
16
|
+
* Reconcile a loaded import graph so each module location (an import ref minus
|
|
17
|
+
* its version) resolves to a single version. Within a shared major the highest version wins
|
|
18
18
|
* (a non-lossy hoist, given Telo's additive-only pre-1.0 policy); a major
|
|
19
19
|
* mismatch is a hard conflict. Mutates `importEdges` in place — every edge that
|
|
20
20
|
* pointed at a losing source is repointed at the winner — so `flattenForAnalyzer`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile-module-versions.d.ts","sourceRoot":"","sources":["../src/reconcile-module-versions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAElE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIzE;;;;0CAI0C;AAC1C,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B;2EACuE;IACvE,WAAW,EAAE,kBAAkB,EAAE,CAAC;CACnC;
|
|
1
|
+
{"version":3,"file":"reconcile-module-versions.d.ts","sourceRoot":"","sources":["../src/reconcile-module-versions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAElE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIzE;;;;0CAI0C;AAC1C,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B;2EACuE;IACvE,WAAW,EAAE,kBAAkB,EAAE,CAAC;CACnC;AAuMD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAClC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,GAChD,qBAAqB,CAsEvB"}
|
|
@@ -64,27 +64,56 @@ function compareVersions(a, b) {
|
|
|
64
64
|
}
|
|
65
65
|
return 0;
|
|
66
66
|
}
|
|
67
|
-
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
|
|
67
|
+
/** The location identity of an import ref: the ref with its version stripped.
|
|
68
|
+
* Two refs share an identity when they address the same module at different
|
|
69
|
+
* versions, whatever transport owns them:
|
|
70
|
+
*
|
|
71
|
+
* "std/kv-store@0.3.0" → "std/kv-store"
|
|
72
|
+
* "oci://ghcr.io/acme/s3@1.2.0" → "oci://ghcr.io/acme/s3"
|
|
73
|
+
* "https://x.com/lib/telo.yaml" → itself (a URL carries no version)
|
|
74
|
+
*
|
|
75
|
+
* Returns `null` for a relative path, which addresses one file on the
|
|
76
|
+
* publisher's disk and is therefore not a cross-import key: two local libraries
|
|
77
|
+
* that merely agree on `metadata.name` are distinct modules, and reconciling
|
|
78
|
+
* them would drop one and break its kinds. The same local file reached via two
|
|
79
|
+
* paths is already collapsed by canonical-source dedup.
|
|
80
|
+
*
|
|
81
|
+
* **What this key cannot relate.** It compares ref *spellings*, so it groups by
|
|
82
|
+
* origin exactly and nothing else. Two consequences, both accepted:
|
|
83
|
+
*
|
|
84
|
+
* - A module imported once by a registry ref and once by a relative path is two
|
|
85
|
+
* groups, so a version skew between them is not hoisted. Keying on what the
|
|
86
|
+
* module declares about itself would catch that case, but only by trusting a
|
|
87
|
+
* self-declared identity — which is what this change removes, and which
|
|
88
|
+
* cannot tell two same-named modules from different origins apart.
|
|
89
|
+
* - A bare `std/kv-store@0.4.0` and the equivalent direct
|
|
90
|
+
* `https://<registry>/std/kv-store/0.4.0/telo.yaml` are two groups. Relating
|
|
91
|
+
* them needs the configured registry base, which this pure, browser-safe
|
|
92
|
+
* function does not have. */
|
|
93
|
+
function refIdentity(ref) {
|
|
94
|
+
const base = ref.split("#")[0];
|
|
95
|
+
if (!base || base.startsWith(".") || base.startsWith("/") || base.startsWith("file:")) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const lastSlash = base.lastIndexOf("/");
|
|
99
|
+
const at = base.lastIndexOf("@");
|
|
100
|
+
return at > lastSlash && at > 0 ? base.slice(0, at) : base;
|
|
101
|
+
}
|
|
102
|
+
/** Read a loaded module's version and raw owner text under the location
|
|
103
|
+
* identity the import edge reached it by. The identity comes from the ref, not
|
|
104
|
+
* from anything the module declares about itself — a module's own metadata
|
|
105
|
+
* cannot distinguish two same-named modules published to different origins. */
|
|
106
|
+
function moduleIdentityOf(mod, identity) {
|
|
75
107
|
const doc = mod.owner.manifests.find((m) => m && isModuleKind(m.kind));
|
|
76
108
|
if (!doc)
|
|
77
109
|
return null;
|
|
78
110
|
const meta = doc.metadata;
|
|
79
|
-
|
|
80
|
-
if (typeof name !== "string" || name.length === 0)
|
|
81
|
-
return null;
|
|
82
|
-
if (typeof meta.namespace !== "string" || meta.namespace.length === 0)
|
|
111
|
+
if (typeof meta?.name !== "string" || meta.name.length === 0)
|
|
83
112
|
return null;
|
|
84
113
|
const version = typeof meta.version === "string" ? meta.version : "";
|
|
85
114
|
return {
|
|
86
115
|
source: mod.owner.source,
|
|
87
|
-
identity
|
|
116
|
+
identity,
|
|
88
117
|
version,
|
|
89
118
|
parsed: parseVersion(version),
|
|
90
119
|
text: mod.owner.text,
|
|
@@ -154,8 +183,8 @@ function hoistDiagnostic(identity, importerSource, alias, loser, winner, conflic
|
|
|
154
183
|
return null;
|
|
155
184
|
}
|
|
156
185
|
/**
|
|
157
|
-
* Reconcile a loaded import graph so each module
|
|
158
|
-
* resolves to a single version. Within a shared major the highest version wins
|
|
186
|
+
* Reconcile a loaded import graph so each module location (an import ref minus
|
|
187
|
+
* its version) resolves to a single version. Within a shared major the highest version wins
|
|
159
188
|
* (a non-lossy hoist, given Telo's additive-only pre-1.0 policy); a major
|
|
160
189
|
* mismatch is a hard conflict. Mutates `importEdges` in place — every edge that
|
|
161
190
|
* pointed at a losing source is repointed at the winner — so `flattenForAnalyzer`
|
|
@@ -165,10 +194,33 @@ function hoistDiagnostic(identity, importerSource, alias, loser, winner, conflic
|
|
|
165
194
|
export function reconcileModuleVersions(modules, importEdges) {
|
|
166
195
|
const overrides = new Map();
|
|
167
196
|
const diagnostics = [];
|
|
197
|
+
// Location identity per resolved module, taken from the ref that reached it.
|
|
198
|
+
// Two refs at different versions resolve to different canonical sources, so a
|
|
199
|
+
// source normally maps to exactly one identity; the entry module has no
|
|
200
|
+
// inbound edge and needs none (it is never reconciled against itself).
|
|
201
|
+
//
|
|
202
|
+
// One source CAN be reached by two spellings of the same location (a bare
|
|
203
|
+
// registry ref and the direct URL it resolves to). Both name the same module
|
|
204
|
+
// at the same version, so either identity groups it correctly — but the choice
|
|
205
|
+
// must not depend on edge iteration order, or the same graph could reconcile
|
|
206
|
+
// differently across runs. First edge wins.
|
|
207
|
+
const identityBySource = new Map();
|
|
208
|
+
for (const aliasMap of importEdges.values()) {
|
|
209
|
+
for (const edge of aliasMap.values()) {
|
|
210
|
+
if (identityBySource.has(edge.targetSource))
|
|
211
|
+
continue;
|
|
212
|
+
const identity = refIdentity(edge.targetRef);
|
|
213
|
+
if (identity)
|
|
214
|
+
identityBySource.set(edge.targetSource, identity);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
168
217
|
const groups = new Map();
|
|
169
218
|
const infoBySource = new Map();
|
|
170
|
-
for (const mod of modules
|
|
171
|
-
const
|
|
219
|
+
for (const [source, mod] of modules) {
|
|
220
|
+
const identity = identityBySource.get(source);
|
|
221
|
+
if (!identity)
|
|
222
|
+
continue;
|
|
223
|
+
const info = moduleIdentityOf(mod, identity);
|
|
172
224
|
if (!info)
|
|
173
225
|
continue;
|
|
174
226
|
infoBySource.set(info.source, info);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** An entry for a field that carries one or more x-telo-ref constraints. */
|
|
2
2
|
export interface RefFieldEntry {
|
|
3
|
-
/** One or more canonical
|
|
3
|
+
/** One or more canonical kind keys ("<module>.<Kind>"), or the deprecated
|
|
4
|
+
* identity form ("<namespace>/<module>#<Kind>") for a legacy published module.
|
|
4
5
|
* Multiple entries arise from anyOf branches. */
|
|
5
6
|
refs: string[];
|
|
6
7
|
/** True when the field path traversed through at least one array (path contains "[]"). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reference-field-map.d.ts","sourceRoot":"","sources":["../src/reference-field-map.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B;
|
|
1
|
+
{"version":3,"file":"reference-field-map.d.ts","sourceRoot":"","sources":["../src/reference-field-map.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B;;sDAEkD;IAClD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,0FAA0F;IAC1F,OAAO,EAAE,OAAO,CAAC;IACjB;8DAC0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC9B;;;;;;;;;;kCAU8B;IAC9B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B;2CACuC;IACvC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC1B;AAED;8CAC8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC;;qFAEiF;IACjF,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,eAAe,GAAG,oBAAoB,CAAC;AAEnF;0FAC0F;AAC1F,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAE3D,wBAAgB,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,aAAa,CAEvE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,eAAe,CAE3E;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,oBAAoB,CAErF;AAED,oGAAoG;AACpG,eAAO,MAAM,cAAc,aAAwC,CAAC;AAEpE;;;;;;;;;2EAS2E;AAC3E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAItE;AAED;;;;sEAIsE;AACtE,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;wDAUwD;AACxD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAoCpF;AAED;+DAC+D;AAC/D,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAExE;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,iBAAiB,CAQrF;AASD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE,CAa/D;AAED;;;8CAG8C;AAC9C,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,UAAU,EAAE,MAAM,GACjB,iBAAiB,CAInB"}
|
|
@@ -142,7 +142,7 @@ function traverseNode(node, path, map, root, visitedRefs = new Set()) {
|
|
|
142
142
|
// the kernel's `runInvoke` emit-Invoked path. The walker fix and the
|
|
143
143
|
// dispatcher fix need to land together — see the follow-up in
|
|
144
144
|
// [kernel/nodejs/plans/reference-syntax-unification.md] and the
|
|
145
|
-
// stopgap in `resource-context.ts:
|
|
145
|
+
// stopgap in `resource-context.ts:ensureKindRef`. `visitedRefs`
|
|
146
146
|
// stays as a parameter so the recursive calls below thread the right
|
|
147
147
|
// signature; turning the descent back on is a single-branch change.
|
|
148
148
|
if (typeof node?.$ref === "string")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import type { AliasResolver } from "./alias-resolver.js";
|
|
3
|
+
/** Why an `x-telo-ref` constraint did not canonicalize.
|
|
4
|
+
*
|
|
5
|
+
* - `legacy` — the deprecated `<namespace>/<module>#<Kind>` identity form,
|
|
6
|
+
* still resolved through the identity table.
|
|
7
|
+
* - `unknown` — the prefix is not an alias in the declaring module's scope: a
|
|
8
|
+
* typo, a missing `imports:` entry — or a value that was already
|
|
9
|
+
* canonical, which the caller separates by asking the registry.
|
|
10
|
+
* - `gated` — the alias is known and the target owns the kind, but its
|
|
11
|
+
* `exports.kinds` does not list it. */
|
|
12
|
+
export type RefConstraintReason = "legacy" | "unknown" | "gated";
|
|
13
|
+
/** An `x-telo-ref` constraint that did not canonicalize. */
|
|
14
|
+
export interface RefConstraintIssue {
|
|
15
|
+
/** The constraint string exactly as authored. */
|
|
16
|
+
ref: string;
|
|
17
|
+
/** Dotted path to the annotated schema node within the doc, e.g.
|
|
18
|
+
* `schema.properties.store`. Points the author at the slot, not just the doc. */
|
|
19
|
+
path: string;
|
|
20
|
+
/** The `Telo.Definition` / `Telo.Abstract` doc that declares the slot. */
|
|
21
|
+
manifest: ResourceManifest;
|
|
22
|
+
reason: RefConstraintReason;
|
|
23
|
+
/** For `gated`: the target module and the kinds it does export. */
|
|
24
|
+
gate?: {
|
|
25
|
+
module: string;
|
|
26
|
+
exported: string[];
|
|
27
|
+
};
|
|
28
|
+
/** Aliases the declaring scope does know — the "did you mean" material for
|
|
29
|
+
* an `unknown` prefix. */
|
|
30
|
+
knownAliases?: string[];
|
|
31
|
+
}
|
|
32
|
+
/** True for the legacy identity form, which is split on `#` by the identity table. */
|
|
33
|
+
export declare function isLegacyRefIdentity(ref: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Rewrites the `x-telo-ref` constraints on one definition doc from the alias
|
|
36
|
+
* form (`KvStore.Store`, `Self.Store`, `Telo.Invocable`) to the canonical
|
|
37
|
+
* `<module>.<Kind>` key the definition registry is keyed by.
|
|
38
|
+
*
|
|
39
|
+
* `resolver` must be the scope of the module that DECLARES the definition, not
|
|
40
|
+
* the consumer's: an imported library names its dependencies by its own aliases.
|
|
41
|
+
* This mirrors how `extends:` and `capability:` are pre-resolved before
|
|
42
|
+
* registration, so the registry never needs module context to answer a ref query.
|
|
43
|
+
*
|
|
44
|
+
* Every constraint that does not canonicalize is returned, tagged with why. That
|
|
45
|
+
* matters more than it looks: an unresolved constraint leaves a string naming no
|
|
46
|
+
* registered kind, and the reference check treats an unknown target as partial
|
|
47
|
+
* context and skips it — so an unreported one would silently let the slot accept
|
|
48
|
+
* anything. The authored value is left in place either way, so a diagnostic
|
|
49
|
+
* quotes what the author actually wrote.
|
|
50
|
+
*
|
|
51
|
+
* An `unknown` result also covers an already-canonical value (`kv-store.Store`
|
|
52
|
+
* names a module, not an alias), which is what keeps the rewrite idempotent —
|
|
53
|
+
* the caller drops those by checking the definition registry once every kind is
|
|
54
|
+
* registered.
|
|
55
|
+
*
|
|
56
|
+
* The walk covers the whole doc rather than a fixed field list: a constraint can
|
|
57
|
+
* sit in `schema`, `inputType`, `outputType`, or a `$defs` entry nested in any of
|
|
58
|
+
* them, and rewriting one that appears in a template body's inline schema is
|
|
59
|
+
* correct too.
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolveSchemaRefKinds(definition: ResourceManifest, resolver: Pick<AliasResolver, "resolveKindResult" | "knownAliases">): RefConstraintIssue[];
|
|
62
|
+
//# sourceMappingURL=resolve-schema-ref-kinds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-schema-ref-kinds.d.ts","sourceRoot":"","sources":["../src/resolve-schema-ref-kinds.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD;;;;;;;;sDAQsD;AACtD,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;AAEjE,4DAA4D;AAC5D,MAAM,WAAW,kBAAkB;IACjC,iDAAiD;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ;sFACkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,MAAM,EAAE,mBAAmB,CAAC;IAC5B,mEAAmE;IACnE,IAAI,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAC9C;+BAC2B;IAC3B,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,sFAAsF;AACtF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,gBAAgB,EAC5B,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,mBAAmB,GAAG,cAAc,CAAC,GAClE,kBAAkB,EAAE,CAiDtB"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const REF_ANNOTATION = "x-telo-ref";
|
|
2
|
+
/** True for the legacy identity form, which is split on `#` by the identity table. */
|
|
3
|
+
export function isLegacyRefIdentity(ref) {
|
|
4
|
+
return ref.includes("#");
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Rewrites the `x-telo-ref` constraints on one definition doc from the alias
|
|
8
|
+
* form (`KvStore.Store`, `Self.Store`, `Telo.Invocable`) to the canonical
|
|
9
|
+
* `<module>.<Kind>` key the definition registry is keyed by.
|
|
10
|
+
*
|
|
11
|
+
* `resolver` must be the scope of the module that DECLARES the definition, not
|
|
12
|
+
* the consumer's: an imported library names its dependencies by its own aliases.
|
|
13
|
+
* This mirrors how `extends:` and `capability:` are pre-resolved before
|
|
14
|
+
* registration, so the registry never needs module context to answer a ref query.
|
|
15
|
+
*
|
|
16
|
+
* Every constraint that does not canonicalize is returned, tagged with why. That
|
|
17
|
+
* matters more than it looks: an unresolved constraint leaves a string naming no
|
|
18
|
+
* registered kind, and the reference check treats an unknown target as partial
|
|
19
|
+
* context and skips it — so an unreported one would silently let the slot accept
|
|
20
|
+
* anything. The authored value is left in place either way, so a diagnostic
|
|
21
|
+
* quotes what the author actually wrote.
|
|
22
|
+
*
|
|
23
|
+
* An `unknown` result also covers an already-canonical value (`kv-store.Store`
|
|
24
|
+
* names a module, not an alias), which is what keeps the rewrite idempotent —
|
|
25
|
+
* the caller drops those by checking the definition registry once every kind is
|
|
26
|
+
* registered.
|
|
27
|
+
*
|
|
28
|
+
* The walk covers the whole doc rather than a fixed field list: a constraint can
|
|
29
|
+
* sit in `schema`, `inputType`, `outputType`, or a `$defs` entry nested in any of
|
|
30
|
+
* them, and rewriting one that appears in a template body's inline schema is
|
|
31
|
+
* correct too.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveSchemaRefKinds(definition, resolver) {
|
|
34
|
+
const issues = [];
|
|
35
|
+
const record = (ref, path) => {
|
|
36
|
+
if (isLegacyRefIdentity(ref)) {
|
|
37
|
+
issues.push({ ref, path, manifest: definition, reason: "legacy" });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const result = resolver.resolveKindResult(ref);
|
|
41
|
+
if (result.status === "ok")
|
|
42
|
+
return;
|
|
43
|
+
issues.push(result.status === "gated"
|
|
44
|
+
? {
|
|
45
|
+
ref,
|
|
46
|
+
path,
|
|
47
|
+
manifest: definition,
|
|
48
|
+
reason: "gated",
|
|
49
|
+
gate: { module: result.module, exported: result.exported },
|
|
50
|
+
}
|
|
51
|
+
: {
|
|
52
|
+
ref,
|
|
53
|
+
path,
|
|
54
|
+
manifest: definition,
|
|
55
|
+
reason: "unknown",
|
|
56
|
+
knownAliases: resolver.knownAliases(),
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
const walk = (value, path) => {
|
|
60
|
+
if (value === null || typeof value !== "object")
|
|
61
|
+
return;
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
value.forEach((item, i) => walk(item, `${path}[${i}]`));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const obj = value;
|
|
67
|
+
const ref = obj[REF_ANNOTATION];
|
|
68
|
+
if (typeof ref === "string" && ref) {
|
|
69
|
+
const result = isLegacyRefIdentity(ref) ? null : resolver.resolveKindResult(ref);
|
|
70
|
+
if (result?.status === "ok")
|
|
71
|
+
obj[REF_ANNOTATION] = result.kind;
|
|
72
|
+
else
|
|
73
|
+
record(ref, path);
|
|
74
|
+
}
|
|
75
|
+
for (const key of Object.keys(obj)) {
|
|
76
|
+
walk(obj[key], path ? `${path}.${key}` : key);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
walk(definition, "");
|
|
80
|
+
return issues;
|
|
81
|
+
}
|
|
@@ -7,16 +7,26 @@ export declare const MANIFEST_CACHE_BASE_URL = "https://manifests.telo.sh";
|
|
|
7
7
|
* stores per version — `{ transport, host, path, version }` — so the tracker's
|
|
8
8
|
* write key and the editor's read key come from the same function and never
|
|
9
9
|
* drift. `path` is the slash-separated repo/module path (multi-segment OCI
|
|
10
|
-
* repos nest as prefixes). */
|
|
10
|
+
* repos nest as prefixes), empty for a module addressed at a host's root. */
|
|
11
11
|
export interface ManifestCacheCoords {
|
|
12
12
|
transport: string;
|
|
13
13
|
host: string;
|
|
14
14
|
path: string;
|
|
15
|
-
version
|
|
15
|
+
/** Omit only when the ref carries no version to key by — a direct `https://`
|
|
16
|
+
* URL addresses exactly one file, so its path alone is already unambiguous.
|
|
17
|
+
* Every ref whose grammar has a version segment must supply it, or two
|
|
18
|
+
* versions of one module would collide on a single key. */
|
|
19
|
+
version?: string;
|
|
20
|
+
/** File within the version directory. Defaults to the module manifest, which
|
|
21
|
+
* is the only file the hub's bucket stores; the local install cache also
|
|
22
|
+
* holds each `include:` partial, which is named here. */
|
|
23
|
+
file?: string;
|
|
16
24
|
}
|
|
17
25
|
/** Deterministic cache key for one module version:
|
|
18
|
-
* `<transport>/<host>/<path…>/<version
|
|
19
|
-
*
|
|
26
|
+
* `<transport>/<host>/<path…>/<version>/<file>`, where `version` is omitted
|
|
27
|
+
* when the coordinates carry none and `file` defaults to the module manifest.
|
|
28
|
+
* Returns `null` when any coordinate is empty or would traverse out of the key
|
|
29
|
+
* space. */
|
|
20
30
|
export declare function manifestCacheKey(coords: ManifestCacheCoords): string | null;
|
|
21
31
|
/** Cache coordinates for an `oci://host/repo@tag` ref. Returns `null` when the
|
|
22
32
|
* ref carries no explicit tag (a defaulted `latest` or a `sha256:` digest is
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-cache.d.ts","sourceRoot":"","sources":["../../src/sources/manifest-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAKlD;;iFAEiF;AACjF,eAAO,MAAM,uBAAuB,8BAA8B,CAAC;AAEnE
|
|
1
|
+
{"version":3,"file":"manifest-cache.d.ts","sourceRoot":"","sources":["../../src/sources/manifest-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAKlD;;iFAEiF;AACjF,eAAO,MAAM,uBAAuB,8BAA8B,CAAC;AAEnE;;;;8EAI8E;AAC9E,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb;;;gEAG4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;8DAE0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAOD;;;;aAIa;AACb,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,GAAG,IAAI,CAe3E;AAED;;;6EAG6E;AAC7E,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI,CAY9E;AAID;;2DAE2D;AAC3D,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;;;;;;;;oEASoE;AACpE,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI,CAuB/F;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,GAAG,mBAAmB,EACzC,OAAO,GAAE,MAAgC,GACxC,MAAM,GAAG,IAAI,CAKf;AAED;;;;;+DAK+D;AAC/D,qBAAa,mBAAoB,YAAW,cAAc;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,GAAE,MAAgC;IAEtE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAQxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAalE;;;iEAG6D;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;CAOxD"}
|
|
@@ -10,15 +10,24 @@ function invalidSegment(segment) {
|
|
|
10
10
|
return segment === "" || segment === "." || segment === ".." || segment.includes("\\");
|
|
11
11
|
}
|
|
12
12
|
/** Deterministic cache key for one module version:
|
|
13
|
-
* `<transport>/<host>/<path…>/<version
|
|
14
|
-
*
|
|
13
|
+
* `<transport>/<host>/<path…>/<version>/<file>`, where `version` is omitted
|
|
14
|
+
* when the coordinates carry none and `file` defaults to the module manifest.
|
|
15
|
+
* Returns `null` when any coordinate is empty or would traverse out of the key
|
|
16
|
+
* space. */
|
|
15
17
|
export function manifestCacheKey(coords) {
|
|
16
|
-
const { transport, host, path, version } = coords;
|
|
17
|
-
const segments = [transport, host, ...path.split("/")
|
|
18
|
-
if (
|
|
18
|
+
const { transport, host, path, version, file } = coords;
|
|
19
|
+
const segments = [transport, host, ...(path ? path.split("/") : [])];
|
|
20
|
+
if (version !== undefined)
|
|
21
|
+
segments.push(version);
|
|
22
|
+
segments.push(file ?? DEFAULT_MANIFEST_FILENAME);
|
|
23
|
+
if (segments.some(invalidSegment) ||
|
|
24
|
+
transport.includes("/") ||
|
|
25
|
+
host.includes("/") ||
|
|
26
|
+
(version !== undefined && version.includes("/")) ||
|
|
27
|
+
(file !== undefined && file.includes("/"))) {
|
|
19
28
|
return null;
|
|
20
29
|
}
|
|
21
|
-
return
|
|
30
|
+
return segments.join("/");
|
|
22
31
|
}
|
|
23
32
|
/** Cache coordinates for an `oci://host/repo@tag` ref. Returns `null` when the
|
|
24
33
|
* ref carries no explicit tag (a defaulted `latest` or a `sha256:` digest is
|
|
@@ -26,5 +26,7 @@ export interface InlineDefinitionLookup {
|
|
|
26
26
|
* `rewriteSyntheticOrigins` reroutes those on inline-extracted (synthetic)
|
|
27
27
|
* manifests back to the root doc.
|
|
28
28
|
*/
|
|
29
|
-
export declare function validateNestedInlineResources(manifest: ResourceManifest, rootSchema: Record<string, any>, lookupDefinition: InlineDefinitionLookup
|
|
29
|
+
export declare function validateNestedInlineResources(manifest: ResourceManifest, rootSchema: Record<string, any>, lookupDefinition: InlineDefinitionLookup,
|
|
30
|
+
/** Needed to resolve a `telo#Type` field a value slot is validated against. */
|
|
31
|
+
allManifests?: Record<string, any>[]): AnalysisDiagnostic[];
|
|
30
32
|
//# sourceMappingURL=validate-nested-inline.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-nested-inline.d.ts","sourceRoot":"","sources":["../src/validate-nested-inline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"validate-nested-inline.d.ts","sourceRoot":"","sources":["../src/validate-nested-inline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAKzE,mFAAmF;AACnF,MAAM,WAAW,sBAAsB;IACrC,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KAAE,GAAG,SAAS,CAAC;CAC9D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,gBAAgB,EAC1B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC/B,gBAAgB,EAAE,sBAAsB;AACxC,+EAA+E;AAC/E,YAAY,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAO,GACvC,kBAAkB,EAAE,CA6HtB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { collectRefs, isInlineResource } from "./reference-field-map.js";
|
|
2
2
|
import { collectProperties, resolveRef, substituteCelFields, validateAgainstSchema, } from "./schema-compat.js";
|
|
3
3
|
import { DiagnosticSeverity } from "./types.js";
|
|
4
|
+
import { collectValueSchemaIssues } from "./validate-value-schema.js";
|
|
4
5
|
const SOURCE = "telo-analyzer";
|
|
5
6
|
/**
|
|
6
7
|
* Validates inline resources nested inside a resource body against their kind's
|
|
@@ -22,7 +23,9 @@ const SOURCE = "telo-analyzer";
|
|
|
22
23
|
* `rewriteSyntheticOrigins` reroutes those on inline-extracted (synthetic)
|
|
23
24
|
* manifests back to the root doc.
|
|
24
25
|
*/
|
|
25
|
-
export function validateNestedInlineResources(manifest, rootSchema, lookupDefinition
|
|
26
|
+
export function validateNestedInlineResources(manifest, rootSchema, lookupDefinition,
|
|
27
|
+
/** Needed to resolve a `telo#Type` field a value slot is validated against. */
|
|
28
|
+
allManifests = []) {
|
|
26
29
|
const diagnostics = [];
|
|
27
30
|
const resource = { kind: manifest.kind, name: manifest.metadata?.name };
|
|
28
31
|
const filePath = manifest.metadata?.source;
|
|
@@ -68,7 +71,16 @@ export function validateNestedInlineResources(manifest, rootSchema, lookupDefini
|
|
|
68
71
|
: {};
|
|
69
72
|
const data = { ...inline, metadata: { name: "__inline__", ...existingMeta } };
|
|
70
73
|
const substituted = substituteCelFields(data, effectiveSchema, effectiveSchema);
|
|
71
|
-
|
|
74
|
+
// The same two passes the top-level resource loop runs, so a kind's
|
|
75
|
+
// guarantees don't depend on whether the author wrote it standalone or
|
|
76
|
+
// inline (under a step's `invoke:`, or in a `with:` scope). `data` carries
|
|
77
|
+
// the synthesized metadata; `x-telo-value-schema-from` reads sibling fields
|
|
78
|
+
// off the resource, which are present either way.
|
|
79
|
+
const inlineIssues = [
|
|
80
|
+
...validateAgainstSchema(substituted, effectiveSchema),
|
|
81
|
+
...collectValueSchemaIssues(data, schema, allManifests),
|
|
82
|
+
];
|
|
83
|
+
for (const issue of inlineIssues) {
|
|
72
84
|
diagnostics.push({
|
|
73
85
|
severity: DiagnosticSeverity.Error,
|
|
74
86
|
code: "SCHEMA_VIOLATION",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type SchemaIssue } from "./schema-compat.js";
|
|
2
|
+
/**
|
|
3
|
+
* Validate every `x-telo-value-schema-from` slot in one resource.
|
|
4
|
+
*
|
|
5
|
+
* CEL leaves are replaced with schema-shaped placeholders before AJV runs
|
|
6
|
+
* (`substituteCelFields`), so an expression is accepted wherever its slot's
|
|
7
|
+
* declared type would be — a static pass cannot know what it evaluates to. What
|
|
8
|
+
* this DOES catch is structural disagreement that no runtime value can fix: a
|
|
9
|
+
* missing required property, an unknown property under
|
|
10
|
+
* `additionalProperties: false`, or a literal of the wrong type.
|
|
11
|
+
*/
|
|
12
|
+
export declare function collectValueSchemaIssues(manifest: Record<string, any>, defSchema: Record<string, any> | undefined, allManifests: Record<string, any>[]): SchemaIssue[];
|
|
13
|
+
//# sourceMappingURL=validate-value-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-value-schema.d.ts","sourceRoot":"","sources":["../src/validate-value-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8C,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AA2FlG;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC7B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,EAC1C,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAClC,WAAW,EAAE,CAoBf"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { substituteCelFields, validateAgainstSchema } from "./schema-compat.js";
|
|
2
|
+
import { resolveTypeFieldToSchema } from "./validate-cel-context.js";
|
|
3
|
+
/**
|
|
4
|
+
* `x-telo-value-schema-from: "<field>"` — the value written at the annotated
|
|
5
|
+
* node must satisfy the type declared at the resource's `<field>`.
|
|
6
|
+
*
|
|
7
|
+
* The motivating shape is a kind with ONE declared output contract and SEVERAL
|
|
8
|
+
* places that must each produce it — a decision table's rows, a switch's arms.
|
|
9
|
+
* Only the branch that wins at runtime gets checked there, so a mistyped branch
|
|
10
|
+
* ships and fails on the one input that selects it. This annotation checks every
|
|
11
|
+
* branch statically instead.
|
|
12
|
+
*
|
|
13
|
+
* Generic and topology-driven: the analyzer hardcodes no kind. Any definition
|
|
14
|
+
* with a `telo#Type` field and value-producing slots opts in by annotating those
|
|
15
|
+
* slots. `<field>` is resolved with the same `telo#Type` semantics as
|
|
16
|
+
* `inputType` / `outputType` everywhere else, so an inline
|
|
17
|
+
* `{ kind: Type.JsonSchema, schema: … }` and a named type reference both work.
|
|
18
|
+
*
|
|
19
|
+
* A field that resolves to no schema — the common case of an optional
|
|
20
|
+
* `outputType` left undeclared — is skipped, not reported: declaring the
|
|
21
|
+
* contract is what opts into the check.
|
|
22
|
+
*/
|
|
23
|
+
const ANNOTATION = "x-telo-value-schema-from";
|
|
24
|
+
/** Walk a definition schema collecting every `x-telo-value-schema-from`, in the
|
|
25
|
+
* same scope form the CEL-context walker produces. */
|
|
26
|
+
function collectAnnotations(schema, path) {
|
|
27
|
+
if (!schema || typeof schema !== "object")
|
|
28
|
+
return [];
|
|
29
|
+
const out = [];
|
|
30
|
+
const from = schema[ANNOTATION];
|
|
31
|
+
if (typeof from === "string" && from.length > 0)
|
|
32
|
+
out.push({ scope: path, from });
|
|
33
|
+
if (schema.properties) {
|
|
34
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
35
|
+
out.push(...collectAnnotations(value, `${path}.${key}`));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (schema.items && typeof schema.items === "object") {
|
|
39
|
+
out.push(...collectAnnotations(schema.items, `${path}[*]`));
|
|
40
|
+
}
|
|
41
|
+
for (const key of ["oneOf", "anyOf", "allOf"]) {
|
|
42
|
+
if (Array.isArray(schema[key])) {
|
|
43
|
+
for (const sub of schema[key])
|
|
44
|
+
out.push(...collectAnnotations(sub, path));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/** Expand a scope into the concrete values present in this manifest, carrying
|
|
50
|
+
* each one's real path so a diagnostic points at the row the author wrote. */
|
|
51
|
+
function resolveScopeValues(manifest, scope) {
|
|
52
|
+
const stripped = scope.startsWith("$.") ? scope.slice(2) : scope;
|
|
53
|
+
if (!stripped)
|
|
54
|
+
return [];
|
|
55
|
+
let frontier = [{ path: "", value: manifest }];
|
|
56
|
+
// Segments look like `choices[*]` or `default` — a name plus optional wildcard.
|
|
57
|
+
for (const segment of stripped.split(".")) {
|
|
58
|
+
const wildcard = segment.endsWith("[*]");
|
|
59
|
+
const name = wildcard ? segment.slice(0, -3) : segment;
|
|
60
|
+
const next = [];
|
|
61
|
+
for (const entry of frontier) {
|
|
62
|
+
const container = entry.value;
|
|
63
|
+
if (!container || typeof container !== "object")
|
|
64
|
+
continue;
|
|
65
|
+
const child = container[name];
|
|
66
|
+
if (child === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
const childPath = entry.path ? `${entry.path}.${name}` : name;
|
|
69
|
+
if (!wildcard) {
|
|
70
|
+
next.push({ path: childPath, value: child });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (!Array.isArray(child))
|
|
74
|
+
continue;
|
|
75
|
+
child.forEach((item, i) => next.push({ path: `${childPath}[${i}]`, value: item }));
|
|
76
|
+
}
|
|
77
|
+
frontier = next;
|
|
78
|
+
if (frontier.length === 0)
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
return frontier;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Validate every `x-telo-value-schema-from` slot in one resource.
|
|
85
|
+
*
|
|
86
|
+
* CEL leaves are replaced with schema-shaped placeholders before AJV runs
|
|
87
|
+
* (`substituteCelFields`), so an expression is accepted wherever its slot's
|
|
88
|
+
* declared type would be — a static pass cannot know what it evaluates to. What
|
|
89
|
+
* this DOES catch is structural disagreement that no runtime value can fix: a
|
|
90
|
+
* missing required property, an unknown property under
|
|
91
|
+
* `additionalProperties: false`, or a literal of the wrong type.
|
|
92
|
+
*/
|
|
93
|
+
export function collectValueSchemaIssues(manifest, defSchema, allManifests) {
|
|
94
|
+
if (!defSchema)
|
|
95
|
+
return [];
|
|
96
|
+
const annotations = collectAnnotations(defSchema, "$");
|
|
97
|
+
if (annotations.length === 0)
|
|
98
|
+
return [];
|
|
99
|
+
const issues = [];
|
|
100
|
+
for (const { scope, from } of annotations) {
|
|
101
|
+
const target = resolveTypeFieldToSchema(manifest[from], allManifests);
|
|
102
|
+
if (!target || typeof target !== "object")
|
|
103
|
+
continue;
|
|
104
|
+
for (const { path, value } of resolveScopeValues(manifest, scope)) {
|
|
105
|
+
for (const issue of validateAgainstSchema(substituteCelFields(value, target), target)) {
|
|
106
|
+
issues.push({
|
|
107
|
+
message: `\`${path}\` does not satisfy the type declared at \`${from}\`: ${issue.message}`,
|
|
108
|
+
path: issue.path ? `${path}.${issue.path}` : path,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return issues;
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@types/node": "^20.0.0",
|
|
49
49
|
"typescript": "^5.0.0",
|
|
50
50
|
"vitest": "^2.1.8",
|
|
51
|
-
"@telorun/sdk": "0.
|
|
51
|
+
"@telorun/sdk": "0.56.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@telorun/sdk": "*"
|