@telorun/analyzer 0.66.0 → 0.68.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.map +1 -1
- package/dist/analyzer.js +60 -2
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +67 -16
- package/dist/cel-scope.d.ts +8 -0
- package/dist/cel-scope.d.ts.map +1 -1
- package/dist/cel-scope.js +66 -8
- package/dist/definition-registry.d.ts +17 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +35 -0
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +65 -0
- package/dist/extends-resolution.d.ts +20 -0
- package/dist/extends-resolution.d.ts.map +1 -1
- package/dist/extends-resolution.js +29 -0
- package/dist/flatten-for-analyzer.d.ts +36 -0
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +103 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/inline-imports.d.ts.map +1 -1
- package/dist/inline-imports.js +1 -0
- package/dist/invocation-contract.d.ts +20 -0
- package/dist/invocation-contract.d.ts.map +1 -1
- package/dist/invocation-contract.js +74 -0
- package/dist/manifest-visitor.d.ts +4 -0
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +28 -0
- package/dist/precompile.d.ts.map +1 -1
- package/dist/precompile.js +8 -0
- package/dist/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +14 -1
- package/dist/resource-input.d.ts +75 -0
- package/dist/resource-input.d.ts.map +1 -0
- package/dist/resource-input.js +90 -0
- package/dist/schema-keywords.d.ts +16 -1
- package/dist/schema-keywords.d.ts.map +1 -1
- package/dist/schema-keywords.js +20 -0
- package/dist/schema-projection.d.ts +13 -0
- package/dist/schema-projection.d.ts.map +1 -1
- package/dist/schema-projection.js +7 -0
- package/dist/system-kinds.d.ts +7 -2
- package/dist/system-kinds.d.ts.map +1 -1
- package/dist/system-kinds.js +7 -2
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/template-body.d.ts +50 -0
- package/dist/template-body.d.ts.map +1 -0
- package/dist/template-body.js +58 -0
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +68 -8
- package/dist/validate-extends.d.ts.map +1 -1
- package/dist/validate-extends.js +42 -1
- package/dist/validate-identifier-names.d.ts.map +1 -1
- package/dist/validate-identifier-names.js +17 -2
- package/dist/validate-references.d.ts +17 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +68 -16
- package/dist/validate-resource-inputs.d.ts +35 -0
- package/dist/validate-resource-inputs.d.ts.map +1 -0
- package/dist/validate-resource-inputs.js +319 -0
- package/dist/validate-sensitive-slots.d.ts +28 -0
- package/dist/validate-sensitive-slots.d.ts.map +1 -0
- package/dist/validate-sensitive-slots.js +75 -0
- package/dist/validate-template-dispatch.d.ts +27 -0
- package/dist/validate-template-dispatch.d.ts.map +1 -0
- package/dist/validate-template-dispatch.js +95 -0
- package/dist/value-type-keyword.d.ts +1 -1
- package/dist/value-type-keyword.d.ts.map +1 -1
- package/dist/value-type-keyword.js +1 -0
- package/package.json +3 -3
- package/src/analyzer.ts +71 -2
- package/src/builtins.ts +69 -16
- package/src/cel-scope.ts +90 -14
- package/src/definition-registry.ts +36 -0
- package/src/dependency-graph.ts +66 -0
- package/src/extends-resolution.ts +33 -0
- package/src/flatten-for-analyzer.ts +116 -3
- package/src/index.ts +16 -0
- package/src/inline-imports.ts +1 -0
- package/src/invocation-contract.ts +71 -0
- package/src/manifest-visitor.ts +33 -0
- package/src/precompile.ts +8 -0
- package/src/resolve-ref-sentinels.ts +12 -1
- package/src/resource-input.ts +132 -0
- package/src/schema-keywords.ts +33 -1
- package/src/schema-projection.ts +19 -0
- package/src/system-kinds.ts +7 -2
- package/src/telo-version.ts +1 -1
- package/src/template-body.ts +104 -0
- package/src/validate-cel-context.ts +67 -7
- package/src/validate-extends.ts +51 -1
- package/src/validate-identifier-names.ts +18 -3
- package/src/validate-references.ts +70 -14
- package/src/validate-resource-inputs.ts +367 -0
- package/src/validate-sensitive-slots.ts +109 -0
- package/src/validate-template-dispatch.ts +99 -0
- package/src/value-type-keyword.ts +1 -0
package/dist/validate-extends.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { inheritedCapability } from "./extends-resolution.js";
|
|
1
|
+
import { controllerBearingAncestor, effectiveAuthorSchema, inheritedCapability, isInheritedDelegation, } from "./extends-resolution.js";
|
|
2
2
|
import { DiagnosticSeverity } from "./types.js";
|
|
3
3
|
const SOURCE = "telo-analyzer";
|
|
4
4
|
/** Alias-form pattern for `extends`: "<Alias>.<AbstractName>", two PascalCase segments. */
|
|
@@ -19,6 +19,9 @@ const EXTENDS_ALIAS_RE = /^[A-Z][A-Za-z0-9_]*\.[A-Z][A-Za-z0-9_]*$/;
|
|
|
19
19
|
* - EXTENDS_UNKNOWN_TARGET: alias resolves to a module, but that module has no
|
|
20
20
|
* registered definition with the target name.
|
|
21
21
|
* - EXTENDS_NON_ABSTRACT: target resolves to a Telo.Definition, not a Telo.Abstract.
|
|
22
|
+
* - EXTENDS_CLOSED_PARENT_ADDS_FIELD: a child with no `base:` declares a property its
|
|
23
|
+
* controller-bearing ancestor's closed schema rejects — the runtime counterpart is a
|
|
24
|
+
* create-time failure phrased against the ancestor kind.
|
|
22
25
|
* - CAPABILITY_SHADOWS_EXTENDS (warning): `capability` names a user-declared abstract
|
|
23
26
|
* (metadata.module !== "Telo"). Builtin lifecycle capabilities (Telo.Invocable, etc.)
|
|
24
27
|
* never trigger this — they're lifecycle roles by design.
|
|
@@ -140,6 +143,44 @@ export function validateExtends(manifests, registry, aliases) {
|
|
|
140
143
|
data: { resource, filePath, path: "capability" },
|
|
141
144
|
});
|
|
142
145
|
}
|
|
146
|
+
// A merge-form child (no `base:`) forwards its WHOLE config to the
|
|
147
|
+
// inherited controller as the parent's config, so a field the
|
|
148
|
+
// parent's schema closes out is rejected at the parent's own
|
|
149
|
+
// create-time validation — phrased against the parent kind, at the
|
|
150
|
+
// instance's line, about a resource the author never wrote.
|
|
151
|
+
// Reported here instead, at the property that has to change.
|
|
152
|
+
if (m.base == null &&
|
|
153
|
+
isInheritedDelegation(m, resolveDef)) {
|
|
154
|
+
const ancestor = controllerBearingAncestor(m, resolveDef);
|
|
155
|
+
const ancestorSchema = ancestor
|
|
156
|
+
? effectiveAuthorSchema(ancestor, resolveDef)
|
|
157
|
+
: undefined;
|
|
158
|
+
if (ancestorSchema?.additionalProperties === false) {
|
|
159
|
+
const inherited = new Set(Object.keys(ancestorSchema.properties ?? {}));
|
|
160
|
+
const ownProps = (m
|
|
161
|
+
.schema?.properties ?? {});
|
|
162
|
+
const ancestorKind = `${ancestor?.metadata.module}.${ancestor?.metadata.name}`;
|
|
163
|
+
for (const prop of Object.keys(ownProps)) {
|
|
164
|
+
if (inherited.has(prop))
|
|
165
|
+
continue;
|
|
166
|
+
diagnostics.push({
|
|
167
|
+
severity: DiagnosticSeverity.Error,
|
|
168
|
+
code: "EXTENDS_CLOSED_PARENT_ADDS_FIELD",
|
|
169
|
+
source: SOURCE,
|
|
170
|
+
message: `${label}: declares '${prop}', but '${ancestorKind}' closes its schema ` +
|
|
171
|
+
`(additionalProperties: false) and a child without 'base:' forwards its whole ` +
|
|
172
|
+
`config as that kind's config — so '${prop}' is rejected when the resource is ` +
|
|
173
|
+
`created. Add a 'base:' mapping, which makes this kind's fields construction ` +
|
|
174
|
+
`inputs and builds the parent's config explicitly.`,
|
|
175
|
+
data: {
|
|
176
|
+
resource,
|
|
177
|
+
filePath,
|
|
178
|
+
path: `schema.properties.${prop}`,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
143
184
|
}
|
|
144
185
|
}
|
|
145
186
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-identifier-names.d.ts","sourceRoot":"","sources":["../src/validate-identifier-names.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,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"validate-identifier-names.d.ts","sourceRoot":"","sources":["../src/validate-identifier-names.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,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAQnE,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIrD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,aAAa,EACtB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,KAAK,EAAE,SAAS,GACf,kBAAkB,EAAE,CAwEtB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TYPE_LEVEL_DOC_KINDS, checkName, } from "./identifier-name.js";
|
|
2
|
+
import { isInjectedDeclaration } from "./resource-input.js";
|
|
2
3
|
const SOURCE = "telo-analyzer";
|
|
3
4
|
/**
|
|
4
5
|
* Every author-written name in one pass — resource instances, kinds, modules,
|
|
@@ -36,6 +37,11 @@ export function validateIdentifierNames(manifests, registry, aliases, rootModule
|
|
|
36
37
|
// would blame them for a spelling this pass's own pipeline chose.
|
|
37
38
|
if (metadata?.xTeloOrigin)
|
|
38
39
|
continue;
|
|
40
|
+
// A kind-only stand-in for a `resources:` entry carries the name the author
|
|
41
|
+
// wrote in that block, and the block's own keys are checked below — on the
|
|
42
|
+
// module doc, where the name is written and a squiggle can land.
|
|
43
|
+
if (isInjectedDeclaration(manifest))
|
|
44
|
+
continue;
|
|
39
45
|
const ownModule = metadata?.module;
|
|
40
46
|
if (ownModule && !rootModules.has(ownModule))
|
|
41
47
|
continue;
|
|
@@ -53,7 +59,10 @@ export function validateIdentifierNames(manifests, registry, aliases, rootModule
|
|
|
53
59
|
// keys are the imported library's declarations, so a violation there is
|
|
54
60
|
// the library author's, reported when their module is analyzed as a root.
|
|
55
61
|
if (manifest.kind === "Telo.Application" || manifest.kind === "Telo.Library") {
|
|
56
|
-
|
|
62
|
+
// `resources:` joins them: a library's resource inputs are named in the
|
|
63
|
+
// same identifier space (`!ref connection`, `resources.connection`) and
|
|
64
|
+
// break the same way.
|
|
65
|
+
for (const field of ["variables", "secrets", "ports", "resources"]) {
|
|
57
66
|
const block = manifest[field];
|
|
58
67
|
if (!block || typeof block !== "object" || Array.isArray(block))
|
|
59
68
|
continue;
|
|
@@ -125,7 +134,13 @@ function surfaceFor(kind) {
|
|
|
125
134
|
}
|
|
126
135
|
}
|
|
127
136
|
function singular(field) {
|
|
128
|
-
return field === "variables"
|
|
137
|
+
return field === "variables"
|
|
138
|
+
? "variable"
|
|
139
|
+
: field === "secrets"
|
|
140
|
+
? "secret"
|
|
141
|
+
: field === "ports"
|
|
142
|
+
? "port"
|
|
143
|
+
: "resource input";
|
|
129
144
|
}
|
|
130
145
|
function push(out, violation, at) {
|
|
131
146
|
if (!violation)
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import { type AnalysisDiagnostic, type AnalysisContext } from "./types.js";
|
|
3
|
+
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
|
+
/**
|
|
5
|
+
* Liskov substitutability at a kind constraint: is `resolved` (a canonical
|
|
6
|
+
* `<module>.<Kind>`) accepted where `targetKind` is required?
|
|
7
|
+
*
|
|
8
|
+
* THE one implementation — `checkKind` below builds a MESSAGE from it and adds
|
|
9
|
+
* nothing to the rule, so a resource input's injection site and an ordinary ref
|
|
10
|
+
* slot cannot come to disagree about what satisfies a constraint.
|
|
11
|
+
*
|
|
12
|
+
* A value satisfies the slot when it transitively extends the target kind, or —
|
|
13
|
+
* for a CONCRETE target — IS that kind; `getByExtends` is the same transitive
|
|
14
|
+
* subtype index for both, and an abstract is satisfied only by an implementer,
|
|
15
|
+
* never by the abstract itself (which is non-instantiable). Accepts a constraint
|
|
16
|
+
* that resolves to nothing, and an abstract with no loaded implementations:
|
|
17
|
+
* partial context, where a rejection would be a guess.
|
|
18
|
+
*/
|
|
19
|
+
export declare function kindSatisfies(resolved: string, targetKind: string, registry: DefinitionRegistry): boolean;
|
|
3
20
|
/**
|
|
4
21
|
* Phase 3 — Reference validation.
|
|
5
22
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAarD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAarD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAE/F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAInE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,kBAAkB,GAC3B,OAAO,CAwBT;AA8DD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CA2gBtB"}
|
|
@@ -7,13 +7,67 @@ import { resolveTypeFieldToSchema } from "./validate-cel-context.js";
|
|
|
7
7
|
import { DiagnosticSeverity } from "./types.js";
|
|
8
8
|
const SOURCE = "telo-analyzer";
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Liskov substitutability at a kind constraint: is `resolved` (a canonical
|
|
11
|
+
* `<module>.<Kind>`) accepted where `targetKind` is required?
|
|
12
|
+
*
|
|
13
|
+
* THE one implementation — `checkKind` below builds a MESSAGE from it and adds
|
|
14
|
+
* nothing to the rule, so a resource input's injection site and an ordinary ref
|
|
15
|
+
* slot cannot come to disagree about what satisfies a constraint.
|
|
16
|
+
*
|
|
17
|
+
* A value satisfies the slot when it transitively extends the target kind, or —
|
|
18
|
+
* for a CONCRETE target — IS that kind; `getByExtends` is the same transitive
|
|
19
|
+
* subtype index for both, and an abstract is satisfied only by an implementer,
|
|
20
|
+
* never by the abstract itself (which is non-instantiable). Accepts a constraint
|
|
21
|
+
* that resolves to nothing, and an abstract with no loaded implementations:
|
|
22
|
+
* partial context, where a rejection would be a guess.
|
|
14
23
|
*/
|
|
24
|
+
export function kindSatisfies(resolved, targetKind, registry) {
|
|
25
|
+
const canonical = registry.resolveRef(targetKind) ?? targetKind;
|
|
26
|
+
const targetDef = registry.resolve(canonical);
|
|
27
|
+
if (!targetDef)
|
|
28
|
+
return true;
|
|
29
|
+
if (targetDef.kind !== "Telo.Abstract" && resolved === canonical)
|
|
30
|
+
return true;
|
|
31
|
+
const subtypes = registry.getByExtends(canonical);
|
|
32
|
+
if (subtypes.some((d) => `${d.metadata.module}.${d.metadata.name}` === resolved))
|
|
33
|
+
return true;
|
|
34
|
+
// Leniency is about the CANDIDATE, not about the population, and it is the
|
|
35
|
+
// same question for an abstract target and a concrete one. Accepting whenever
|
|
36
|
+
// no subtype happened to be loaded silenced the check exactly where it was
|
|
37
|
+
// needed: an app whose imports declare no `Telo.Runnable` is an app whose boot
|
|
38
|
+
// targets are all wrong, and every one of them passed.
|
|
39
|
+
//
|
|
40
|
+
// An UNREGISTERED candidate is not partial context, and the distinction is
|
|
41
|
+
// load-bearing: a kind nobody declared is an unknown kind or an unimported
|
|
42
|
+
// alias prefix (`NotAnAlias.Script`), which is precisely what this check
|
|
43
|
+
// exists to report. Only a kind that IS declared, whose ancestry reaches
|
|
44
|
+
// something this analysis never saw, is undecidable — there the missing hop is
|
|
45
|
+
// where the target it appears not to reach could have been declared.
|
|
46
|
+
// A candidate the registry never saw cannot be judged on what it implements —
|
|
47
|
+
// the analysis simply does not hold it. Whether its NAME is resolvable is a
|
|
48
|
+
// different question, asked of the alias scope by the caller.
|
|
49
|
+
if (!registry.resolve(resolved))
|
|
50
|
+
return true;
|
|
51
|
+
return !registry.ancestryResolved(resolved);
|
|
52
|
+
}
|
|
53
|
+
/** {@link kindSatisfies} at every kind a slot accepts, rendered as messages.
|
|
54
|
+
* The acceptance rule is not restated here — only what to say when it fails,
|
|
55
|
+
* which needs the slot's alternatives and the target's flavour. */
|
|
15
56
|
function checkKind(kind, entry, registry, aliases) {
|
|
16
57
|
const resolved = aliases.resolveKind(kind) ?? kind;
|
|
58
|
+
// A qualified kind whose prefix names no import in this scope is a bad NAME
|
|
59
|
+
// rather than a bad type — `{kind: NotAnAlias.Script}` where only `Lib` is
|
|
60
|
+
// imported — and this check is the only place it surfaces, so it is never
|
|
61
|
+
// waved through as partial context. Asked of the resolver, which reports
|
|
62
|
+
// `unknown` for exactly that case: a kind reached through a DECLARED alias
|
|
63
|
+
// resolves (`ok`) even when the target's definitions are not loaded, and a
|
|
64
|
+
// gated one says so separately.
|
|
65
|
+
const unknownAlias = kind.includes(".") &&
|
|
66
|
+
aliases.resolveKindResult(kind).status === "unknown" &&
|
|
67
|
+
// …and the registry does not hold it under the name as written either. A
|
|
68
|
+
// manifest may carry a canonical `<module>.<Kind>`, which no alias resolves
|
|
69
|
+
// and which is nonetheless perfectly identified.
|
|
70
|
+
registry.resolve(resolved) === undefined;
|
|
17
71
|
const errors = [];
|
|
18
72
|
for (const refStr of entry.refs) {
|
|
19
73
|
const targetKind = registry.resolveRef(refStr);
|
|
@@ -22,20 +76,11 @@ function checkKind(kind, entry, registry, aliases) {
|
|
|
22
76
|
const targetDef = registry.resolve(targetKind);
|
|
23
77
|
if (!targetDef)
|
|
24
78
|
return [];
|
|
25
|
-
|
|
26
|
-
// extends the target kind, or — for a CONCRETE target — IS that kind.
|
|
27
|
-
// `getByExtends` is the same transitive subtype index for abstract and
|
|
28
|
-
// concrete targets alike; an abstract is satisfied only by an implementer,
|
|
29
|
-
// never by the abstract kind itself (which is non-instantiable).
|
|
30
|
-
if (targetDef.kind !== "Telo.Abstract" && resolved === targetKind)
|
|
79
|
+
if (!unknownAlias && kindSatisfies(resolved, targetKind, registry))
|
|
31
80
|
return [];
|
|
32
81
|
const subtypes = registry.getByExtends(targetKind);
|
|
33
82
|
const subtypeKinds = new Set(subtypes.map((d) => `${d.metadata.module}.${d.metadata.name}`));
|
|
34
|
-
if (subtypeKinds.has(resolved))
|
|
35
|
-
return [];
|
|
36
83
|
if (targetDef.kind === "Telo.Abstract") {
|
|
37
|
-
if (subtypes.length === 0)
|
|
38
|
-
return []; // partial context — no implementations loaded yet
|
|
39
84
|
// Suggest only what an author can actually wire: with abstract-extends-
|
|
40
85
|
// abstract real (Telo.Executable over Invocable/Runnable), the transitive
|
|
41
86
|
// subtype list contains abstracts, which are non-instantiable and would
|
|
@@ -43,8 +88,15 @@ function checkKind(kind, entry, registry, aliases) {
|
|
|
43
88
|
const concrete = subtypes
|
|
44
89
|
.filter((d) => d.kind !== "Telo.Abstract")
|
|
45
90
|
.map((d) => `${d.metadata.module}.${d.metadata.name}`);
|
|
46
|
-
const options =
|
|
47
|
-
|
|
91
|
+
const options = concrete.length > 0 ? concrete : [...subtypeKinds];
|
|
92
|
+
// With nothing loaded that implements the target there is no list to
|
|
93
|
+
// offer, so the message says what this kind IS instead — which is the
|
|
94
|
+
// half the author can act on ("it declares 'Telo.Invocable'" points
|
|
95
|
+
// straight at a boot target that should have been an invoke step).
|
|
96
|
+
const declared = registry.resolve(resolved)?.capability;
|
|
97
|
+
errors.push(options.length > 0
|
|
98
|
+
? `'${kind}' does not implement '${targetKind}' (known implementations: ${options.join(", ")})`
|
|
99
|
+
: `'${kind}' does not implement '${targetKind}'${declared ? ` — it declares '${declared}'` : ""}`);
|
|
48
100
|
}
|
|
49
101
|
else {
|
|
50
102
|
const options = subtypeKinds.size > 0 ? ` or a subtype (${[...subtypeKinds].join(", ")})` : "";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import type { AliasResolver } from "./alias-resolver.js";
|
|
3
|
+
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
|
+
import { type AnalysisDiagnostic } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* The strict half of {@link readResourceInputs} — the `validate-ref-slots.ts`
|
|
7
|
+
* split applied to the resource-input boundary, and not optional for the same
|
|
8
|
+
* reason: the two sides fail in OPPOSITE directions. An unreadable DECLARATION
|
|
9
|
+
* leaves the library referencing a name nothing stands behind; an unchecked
|
|
10
|
+
* INJECTION hands a library an instance of a kind it never asked for, which
|
|
11
|
+
* surfaces as a method-missing failure inside someone else's module.
|
|
12
|
+
*
|
|
13
|
+
* Two surfaces, scoped the same way every other declaration check is — to the
|
|
14
|
+
* entry's own modules, since a published dependency's block is not the
|
|
15
|
+
* consumer's to fix:
|
|
16
|
+
*
|
|
17
|
+
* - **The library's own declaration.** Each entry's alias-qualified `kind:` must
|
|
18
|
+
* resolve in the DECLARING module's scope (`RESOURCE_INPUT_KIND_UNRESOLVED`).
|
|
19
|
+
* A constraint that resolves to nothing accepts anything, which is the same
|
|
20
|
+
* hole `X_TELO_REF_UNRESOLVED` exists to close one level down.
|
|
21
|
+
* - **The injection site.** Every declared entry must be supplied
|
|
22
|
+
* (`RESOURCE_INPUT_MISSING` — the same failure as a missing required
|
|
23
|
+
* variable), nothing beyond them may be (`RESOURCE_INPUT_UNKNOWN`), the value
|
|
24
|
+
* must name a resource that exists (`RESOURCE_INPUT_UNRESOLVED`), and its
|
|
25
|
+
* kind must satisfy the declared constraint transitively
|
|
26
|
+
* (`RESOURCE_INPUT_KIND_MISMATCH`).
|
|
27
|
+
*
|
|
28
|
+
* Browser-safe.
|
|
29
|
+
*/
|
|
30
|
+
export declare function validateResourceInputs(manifests: ResourceManifest[], registry: DefinitionRegistry, aliases: AliasResolver, rootModules: Set<string>,
|
|
31
|
+
/** Kind acceptance, transitively — the `checkKind` rule `validate-references`
|
|
32
|
+
* applies at an ordinary ref slot, passed in rather than re-derived so the
|
|
33
|
+
* two cannot disagree about what satisfies a constraint. */
|
|
34
|
+
acceptsKind: (suppliedKind: string, requiredKind: string) => boolean): AnalysisDiagnostic[];
|
|
35
|
+
//# sourceMappingURL=validate-resource-inputs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-resource-inputs.d.ts","sourceRoot":"","sources":["../src/validate-resource-inputs.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;AAGnE,OAAO,EAAE,KAAK,kBAAkB,EAAsB,MAAM,YAAY,CAAC;AASzE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,aAAa,EACtB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC;AACxB;;6DAE6D;AAC7D,WAAW,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,GACnE,kBAAkB,EAAE,CA6LtB"}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { isRefSentinel } from "@telorun/templating";
|
|
2
|
+
import { findDynamicLeaf } from "./resource-rule.js";
|
|
3
|
+
import { readResourceInputs, readSuppliedResources } from "./resource-input.js";
|
|
4
|
+
import { DiagnosticSeverity } from "./types.js";
|
|
5
|
+
const SOURCE = "telo-analyzer";
|
|
6
|
+
/**
|
|
7
|
+
* The strict half of {@link readResourceInputs} — the `validate-ref-slots.ts`
|
|
8
|
+
* split applied to the resource-input boundary, and not optional for the same
|
|
9
|
+
* reason: the two sides fail in OPPOSITE directions. An unreadable DECLARATION
|
|
10
|
+
* leaves the library referencing a name nothing stands behind; an unchecked
|
|
11
|
+
* INJECTION hands a library an instance of a kind it never asked for, which
|
|
12
|
+
* surfaces as a method-missing failure inside someone else's module.
|
|
13
|
+
*
|
|
14
|
+
* Two surfaces, scoped the same way every other declaration check is — to the
|
|
15
|
+
* entry's own modules, since a published dependency's block is not the
|
|
16
|
+
* consumer's to fix:
|
|
17
|
+
*
|
|
18
|
+
* - **The library's own declaration.** Each entry's alias-qualified `kind:` must
|
|
19
|
+
* resolve in the DECLARING module's scope (`RESOURCE_INPUT_KIND_UNRESOLVED`).
|
|
20
|
+
* A constraint that resolves to nothing accepts anything, which is the same
|
|
21
|
+
* hole `X_TELO_REF_UNRESOLVED` exists to close one level down.
|
|
22
|
+
* - **The injection site.** Every declared entry must be supplied
|
|
23
|
+
* (`RESOURCE_INPUT_MISSING` — the same failure as a missing required
|
|
24
|
+
* variable), nothing beyond them may be (`RESOURCE_INPUT_UNKNOWN`), the value
|
|
25
|
+
* must name a resource that exists (`RESOURCE_INPUT_UNRESOLVED`), and its
|
|
26
|
+
* kind must satisfy the declared constraint transitively
|
|
27
|
+
* (`RESOURCE_INPUT_KIND_MISMATCH`).
|
|
28
|
+
*
|
|
29
|
+
* Browser-safe.
|
|
30
|
+
*/
|
|
31
|
+
export function validateResourceInputs(manifests, registry, aliases, rootModules,
|
|
32
|
+
/** Kind acceptance, transitively — the `checkKind` rule `validate-references`
|
|
33
|
+
* applies at an ordinary ref slot, passed in rather than re-derived so the
|
|
34
|
+
* two cannot disagree about what satisfies a constraint. */
|
|
35
|
+
acceptsKind) {
|
|
36
|
+
const out = [];
|
|
37
|
+
const isOwn = (module) => !module || rootModules.has(module);
|
|
38
|
+
// --- the library's own declaration -------------------------------------
|
|
39
|
+
for (const m of manifests) {
|
|
40
|
+
if (m.kind !== "Telo.Library")
|
|
41
|
+
continue;
|
|
42
|
+
const moduleName = m.metadata?.name;
|
|
43
|
+
// Only the entry's own modules, so the declaring scope is always the root
|
|
44
|
+
// alias table — a library analyzed here IS a root module.
|
|
45
|
+
if (!isOwn(moduleName))
|
|
46
|
+
continue;
|
|
47
|
+
const scope = aliases;
|
|
48
|
+
const exported = new Set((m.exports?.resources ?? []).filter((e) => typeof e === "string"));
|
|
49
|
+
for (const input of readResourceInputs(m)) {
|
|
50
|
+
// An input is the IMPORTER's instance, borrowed. Exporting it back out
|
|
51
|
+
// would forward a kind-only stand-in into the consumer's flattened set as
|
|
52
|
+
// a resource this library declares — a phantom the consumer can `!ref`
|
|
53
|
+
// and whose kind constraint is all there is behind it. Handing an
|
|
54
|
+
// instance straight back to whoever supplied it is also a relation
|
|
55
|
+
// nothing needs; if it ever does, it should be a decision rather than a
|
|
56
|
+
// name collision nobody noticed.
|
|
57
|
+
if (exported.has(input.name)) {
|
|
58
|
+
out.push({
|
|
59
|
+
severity: DiagnosticSeverity.Error,
|
|
60
|
+
code: "RESOURCE_INPUT_EXPORTED",
|
|
61
|
+
source: SOURCE,
|
|
62
|
+
message: `Resource input '${input.name}' is also listed in 'exports.resources'. An input is ` +
|
|
63
|
+
`an instance the importer supplies, not one this library declares, so there is ` +
|
|
64
|
+
`nothing to export — remove it from 'exports.resources', or rename the input.`,
|
|
65
|
+
data: {
|
|
66
|
+
resource: { kind: m.kind, name: moduleName },
|
|
67
|
+
filePath: m.metadata?.source,
|
|
68
|
+
path: `resources.${input.name}`,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (registry.resolve(input.kind) ?? registry.resolve(scope.resolveKind(input.kind) ?? "")) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
out.push({
|
|
76
|
+
severity: DiagnosticSeverity.Error,
|
|
77
|
+
code: "RESOURCE_INPUT_KIND_UNRESOLVED",
|
|
78
|
+
source: SOURCE,
|
|
79
|
+
message: `Resource input '${input.name}' is constrained to kind '${input.kind}', which does not ` +
|
|
80
|
+
`resolve in this library's scope. Write it alias-qualified — '<Alias>.<Kind>' for an ` +
|
|
81
|
+
`import declared in this file, 'Self.<Kind>' for a kind this library owns, or ` +
|
|
82
|
+
`'Telo.<Kind>' for a built-in. An unresolvable constraint accepts anything.`,
|
|
83
|
+
data: {
|
|
84
|
+
resource: { kind: m.kind, name: moduleName },
|
|
85
|
+
filePath: m.metadata?.source,
|
|
86
|
+
path: `resources.${input.name}.kind`,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// --- the injection site --------------------------------------------------
|
|
92
|
+
for (const m of manifests) {
|
|
93
|
+
if (m.kind !== "Telo.Import")
|
|
94
|
+
continue;
|
|
95
|
+
const meta = m.metadata;
|
|
96
|
+
const alias = meta?.name;
|
|
97
|
+
if (!alias || !isOwn(meta?.module))
|
|
98
|
+
continue;
|
|
99
|
+
// An import whose target identity was never established registers nothing —
|
|
100
|
+
// reporting a missing input against it would blame the author for a
|
|
101
|
+
// dependency the loader has already said it could not obtain.
|
|
102
|
+
if (!meta?.resolvedModuleName)
|
|
103
|
+
continue;
|
|
104
|
+
// A singleton has no room for a per-import override: `logging:` scopes a
|
|
105
|
+
// subtree that is no longer this import's subtree, and `runtime:` selects a
|
|
106
|
+
// controller backend for a library that is instantiated once. Whichever
|
|
107
|
+
// import happened to be created first would decide, so the field is
|
|
108
|
+
// rejected rather than silently applied to everyone or silently dropped.
|
|
109
|
+
if (meta.sharedLibrary === true) {
|
|
110
|
+
for (const field of ["logging", "runtime"]) {
|
|
111
|
+
if (m[field] === undefined)
|
|
112
|
+
continue;
|
|
113
|
+
out.push({
|
|
114
|
+
severity: DiagnosticSeverity.Error,
|
|
115
|
+
code: "SHARED_LIBRARY_OVERRIDE",
|
|
116
|
+
source: SOURCE,
|
|
117
|
+
message: `Import '${alias}' declares '${field}:', but module ` +
|
|
118
|
+
`'${meta.resolvedModuleName}' is 'lifecycle: shared' — one instantiation for the ` +
|
|
119
|
+
`whole application, so a per-import override cannot apply to it. Remove it, or ` +
|
|
120
|
+
`make the library 'lifecycle: isolated'.`,
|
|
121
|
+
data: {
|
|
122
|
+
resource: { kind: m.kind, name: alias },
|
|
123
|
+
filePath: meta.source,
|
|
124
|
+
path: field,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const required = Object.entries(meta.requiredResources ?? {});
|
|
130
|
+
const supplied = readSuppliedResources(m);
|
|
131
|
+
const declared = new Map(required);
|
|
132
|
+
const resource = { kind: m.kind, name: alias };
|
|
133
|
+
const filePath = meta.source;
|
|
134
|
+
for (const [entryName, entryKind] of required) {
|
|
135
|
+
if (entryName in supplied)
|
|
136
|
+
continue;
|
|
137
|
+
out.push({
|
|
138
|
+
severity: DiagnosticSeverity.Error,
|
|
139
|
+
code: "RESOURCE_INPUT_MISSING",
|
|
140
|
+
source: SOURCE,
|
|
141
|
+
message: `Import '${alias}' does not supply the resource input '${entryName}', which module ` +
|
|
142
|
+
`'${meta.resolvedModuleName}' requires (kind '${entryKind}'). Add ` +
|
|
143
|
+
`\`resources: { ${entryName}: !ref <name> }\` to the import.`,
|
|
144
|
+
data: { resource, filePath, path: "resources" },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
for (const [name, value] of Object.entries(supplied)) {
|
|
148
|
+
const path = `resources.${name}`;
|
|
149
|
+
const entry = declared.get(name);
|
|
150
|
+
if (!entry) {
|
|
151
|
+
const known = required.map(([n]) => n).join(", ") || "(none)";
|
|
152
|
+
out.push({
|
|
153
|
+
severity: DiagnosticSeverity.Error,
|
|
154
|
+
code: "RESOURCE_INPUT_UNKNOWN",
|
|
155
|
+
source: SOURCE,
|
|
156
|
+
message: `Import '${alias}' supplies a resource input '${name}', which module ` +
|
|
157
|
+
`'${meta.resolvedModuleName}' does not declare. Declared inputs: ${known}.`,
|
|
158
|
+
data: { resource, filePath, path },
|
|
159
|
+
});
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// Phase 2.5 rewrote a resolvable `!ref` to `{kind, name}`; a sentinel that
|
|
163
|
+
// survived names nothing the importer can reach.
|
|
164
|
+
if (isRefSentinel(value)) {
|
|
165
|
+
out.push({
|
|
166
|
+
severity: DiagnosticSeverity.Error,
|
|
167
|
+
code: "RESOURCE_INPUT_UNRESOLVED",
|
|
168
|
+
source: SOURCE,
|
|
169
|
+
message: `Import '${alias}': resource input '${name}' → resource '${value.source}' not found`,
|
|
170
|
+
data: { resource, filePath, path },
|
|
171
|
+
});
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
175
|
+
out.push({
|
|
176
|
+
severity: DiagnosticSeverity.Error,
|
|
177
|
+
code: "RESOURCE_INPUT_UNRESOLVED",
|
|
178
|
+
source: SOURCE,
|
|
179
|
+
message: `Import '${alias}': resource input '${name}' must be a '!ref' to a resource this ` +
|
|
180
|
+
`module declares.`,
|
|
181
|
+
data: { resource, filePath, path },
|
|
182
|
+
});
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const suppliedKind = value.kind;
|
|
186
|
+
if (typeof suppliedKind !== "string")
|
|
187
|
+
continue;
|
|
188
|
+
const canonical = aliases.resolveKind(suppliedKind) ?? suppliedKind;
|
|
189
|
+
if (acceptsKind(canonical, entry))
|
|
190
|
+
continue;
|
|
191
|
+
out.push({
|
|
192
|
+
severity: DiagnosticSeverity.Error,
|
|
193
|
+
code: "RESOURCE_INPUT_KIND_MISMATCH",
|
|
194
|
+
source: SOURCE,
|
|
195
|
+
message: `Import '${alias}': resource input '${name}' is '${suppliedKind}' (resolved: ` +
|
|
196
|
+
`'${canonical}'), which does not satisfy the declared constraint '${entry}'.`,
|
|
197
|
+
data: { resource, filePath, path },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
out.push(...sharedLibraryConflicts(manifests, isOwn));
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
const SUPPLIED_BLOCKS = ["variables", "secrets", "resources"];
|
|
205
|
+
/**
|
|
206
|
+
* Two imports of ONE `lifecycle: shared` library supplying different values.
|
|
207
|
+
*
|
|
208
|
+
* The runtime half (`ERR_SHARED_LIBRARY_CONFLICT`) is authoritative — it holds
|
|
209
|
+
* resolved values and live instances, and a library reached through a
|
|
210
|
+
* programmatic load never passed `telo check`. But the overwhelmingly common
|
|
211
|
+
* shape is two literal scalars in one flattened manifest set, which is decidable
|
|
212
|
+
* here, and a conflict is a hard BOOT failure: finding it at `telo check` is the
|
|
213
|
+
* difference between a squiggle and a deployment that will not start.
|
|
214
|
+
*
|
|
215
|
+
* Only what is DECIDABLE is compared, and the two ways it is not are skipped
|
|
216
|
+
* rather than guessed:
|
|
217
|
+
*
|
|
218
|
+
* - A value holding a `!cel` (at any depth) is known only at load. Two
|
|
219
|
+
* different expressions may evaluate equal, and identical text in two
|
|
220
|
+
* different modules may not.
|
|
221
|
+
* - A `resources:` entry names a resource by name, which means the same
|
|
222
|
+
* instance only when both imports were DECLARED in the same module — so a
|
|
223
|
+
* cross-module pair is left to the runtime.
|
|
224
|
+
*
|
|
225
|
+
* Grouped by the target's RESOLVED SOURCE, the identity the kernel keys its
|
|
226
|
+
* singleton registry on; `resolvedModuleName` would collapse two versions of one
|
|
227
|
+
* module, which are two libraries.
|
|
228
|
+
*/
|
|
229
|
+
function sharedLibraryConflicts(manifests, isOwn) {
|
|
230
|
+
const out = [];
|
|
231
|
+
const byTarget = new Map();
|
|
232
|
+
for (const m of manifests) {
|
|
233
|
+
if (m.kind !== "Telo.Import")
|
|
234
|
+
continue;
|
|
235
|
+
const meta = m.metadata;
|
|
236
|
+
if (meta?.sharedLibrary !== true || !meta.name || !meta.resolvedSource)
|
|
237
|
+
continue;
|
|
238
|
+
const blocks = new Map();
|
|
239
|
+
for (const block of SUPPLIED_BLOCKS) {
|
|
240
|
+
const value = m[block];
|
|
241
|
+
blocks.set(block, value && typeof value === "object" && !Array.isArray(value)
|
|
242
|
+
? value
|
|
243
|
+
: {});
|
|
244
|
+
}
|
|
245
|
+
const bucket = byTarget.get(meta.resolvedSource);
|
|
246
|
+
const entry = {
|
|
247
|
+
alias: meta.name,
|
|
248
|
+
module: meta.module,
|
|
249
|
+
filePath: meta.source,
|
|
250
|
+
blocks,
|
|
251
|
+
};
|
|
252
|
+
if (bucket)
|
|
253
|
+
bucket.push(entry);
|
|
254
|
+
else
|
|
255
|
+
byTarget.set(meta.resolvedSource, [entry]);
|
|
256
|
+
}
|
|
257
|
+
for (const group of byTarget.values()) {
|
|
258
|
+
if (group.length < 2)
|
|
259
|
+
continue;
|
|
260
|
+
const [first, ...rest] = group;
|
|
261
|
+
for (const later of rest) {
|
|
262
|
+
for (const block of SUPPLIED_BLOCKS) {
|
|
263
|
+
// A reference means the same instance only within one module's scope.
|
|
264
|
+
if (block === "resources" && first.module !== later.module)
|
|
265
|
+
continue;
|
|
266
|
+
const a = first.blocks.get(block);
|
|
267
|
+
const b = later.blocks.get(block);
|
|
268
|
+
for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
269
|
+
if (findDynamicLeaf(a[key]) || findDynamicLeaf(b[key]))
|
|
270
|
+
continue;
|
|
271
|
+
if (sameSuppliedValue(a[key], b[key]))
|
|
272
|
+
continue;
|
|
273
|
+
out.push({
|
|
274
|
+
severity: DiagnosticSeverity.Error,
|
|
275
|
+
code: "SHARED_LIBRARY_CONFLICT",
|
|
276
|
+
source: SOURCE,
|
|
277
|
+
message: `Import '${later.alias}' and import '${first.alias}' both reach a ` +
|
|
278
|
+
`'lifecycle: shared' library — one instantiation for the whole application — but ` +
|
|
279
|
+
`they supply different values for ${block}.${key}. Make the two imports agree, or ` +
|
|
280
|
+
`make the library 'lifecycle: isolated'.`,
|
|
281
|
+
data: isOwn(later.module)
|
|
282
|
+
? {
|
|
283
|
+
resource: { kind: "Telo.Import", name: later.alias },
|
|
284
|
+
filePath: later.filePath,
|
|
285
|
+
path: `${block}.${key}`,
|
|
286
|
+
}
|
|
287
|
+
: {
|
|
288
|
+
resource: { kind: "Telo.Import", name: first.alias },
|
|
289
|
+
filePath: first.filePath,
|
|
290
|
+
path: `${block}.${key}`,
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
/** Structural equality, key-order insensitive — the analyzer's half of the rule
|
|
300
|
+
* the kernel applies to resolved values. A secret's VALUE is compared but never
|
|
301
|
+
* printed: the key is what the author has to look at. */
|
|
302
|
+
function sameSuppliedValue(a, b) {
|
|
303
|
+
if (a === b)
|
|
304
|
+
return true;
|
|
305
|
+
if (a === undefined || b === undefined || a === null || b === null)
|
|
306
|
+
return false;
|
|
307
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
308
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
309
|
+
return false;
|
|
310
|
+
return a.every((item, i) => sameSuppliedValue(item, b[i]));
|
|
311
|
+
}
|
|
312
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
313
|
+
const left = a;
|
|
314
|
+
const right = b;
|
|
315
|
+
const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
316
|
+
return [...keys].every((key) => sameSuppliedValue(left[key], right[key]));
|
|
317
|
+
}
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
/**
|
|
3
|
+
* `x-telo-sensitive` where nothing reads it.
|
|
4
|
+
*
|
|
5
|
+
* The annotation has exactly one consumer: the kernel resolves it from a
|
|
6
|
+
* resource's bound CONTRACT — `inputType` / `outputType` — and carries the
|
|
7
|
+
* marked value as `[redacted]` in trace payloads. Written anywhere else it is an
|
|
8
|
+
* unknown keyword in an open schema, which is to say it validates, ships, and
|
|
9
|
+
* does nothing.
|
|
10
|
+
*
|
|
11
|
+
* For a security control that is the worst available failure: an author marks a
|
|
12
|
+
* token, sees no error, and puts it on the debug wire anyway. So a misplacement
|
|
13
|
+
* is reported rather than ignored — the same posture `X_TELO_REF_UNRESOLVED`
|
|
14
|
+
* takes toward a reference constraint that resolves to nothing, and for the same
|
|
15
|
+
* reason: silence reads as protection.
|
|
16
|
+
*
|
|
17
|
+
* Scoped by the caller to the entry's own modules, since a dependency's schema
|
|
18
|
+
* is not the consumer's to fix.
|
|
19
|
+
*/
|
|
20
|
+
export interface SensitiveSlotIssue {
|
|
21
|
+
code: "SENSITIVE_ANNOTATION_MISPLACED" | "SENSITIVE_ANNOTATION_INVALID";
|
|
22
|
+
manifest: ResourceManifest;
|
|
23
|
+
/** Dotted path to the annotated schema node. */
|
|
24
|
+
path: string;
|
|
25
|
+
message: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function validateSensitiveSlots(manifest: ResourceManifest): SensitiveSlotIssue[];
|
|
28
|
+
//# sourceMappingURL=validate-sensitive-slots.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-sensitive-slots.d.ts","sourceRoot":"","sources":["../src/validate-sensitive-slots.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,gCAAgC,GAAG,8BAA8B,CAAC;IACxE,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAOD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,kBAAkB,EAAE,CAqDvF"}
|