@telorun/analyzer 0.62.1 → 0.64.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 +24 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +41 -0
- package/dist/analyzer.d.ts +3 -37
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +396 -476
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +2 -7
- package/dist/cel-scope-query.d.ts +109 -0
- package/dist/cel-scope-query.d.ts.map +1 -0
- package/dist/cel-scope-query.js +270 -0
- package/dist/cel-scope.d.ts +166 -0
- package/dist/cel-scope.d.ts.map +1 -0
- package/dist/cel-scope.js +377 -0
- package/dist/definition-registry.d.ts +15 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +25 -9
- package/dist/find-manifest.d.ts +10 -0
- package/dist/find-manifest.d.ts.map +1 -0
- package/dist/find-manifest.js +12 -0
- package/dist/flatten-for-analyzer.js +1 -1
- package/dist/import-resolution-diagnostics.d.ts +9 -2
- package/dist/import-resolution-diagnostics.d.ts.map +1 -1
- package/dist/import-resolution-diagnostics.js +26 -7
- package/dist/index.d.ts +22 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -1
- package/dist/invocation-contract.d.ts +17 -1
- package/dist/invocation-contract.d.ts.map +1 -1
- package/dist/invocation-contract.js +42 -4
- package/dist/loaded-types.d.ts +12 -3
- package/dist/loaded-types.d.ts.map +1 -1
- package/dist/manifest-analysis.d.ts +73 -0
- package/dist/manifest-analysis.d.ts.map +1 -0
- package/dist/manifest-analysis.js +78 -0
- package/dist/manifest-loader.d.ts +0 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +62 -41
- package/dist/manifest-path.d.ts +18 -0
- package/dist/manifest-path.d.ts.map +1 -0
- package/dist/manifest-path.js +37 -0
- package/dist/manifest-schemas.d.ts +699 -0
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +306 -14
- package/dist/module-compatibility.d.ts +40 -0
- package/dist/module-compatibility.d.ts.map +1 -0
- package/dist/module-compatibility.js +52 -0
- package/dist/ref-slot-reading.d.ts +30 -0
- package/dist/ref-slot-reading.d.ts.map +1 -0
- package/dist/ref-slot-reading.js +139 -0
- package/dist/referrer-rule.d.ts +67 -0
- package/dist/referrer-rule.d.ts.map +1 -0
- package/dist/referrer-rule.js +96 -0
- package/dist/release/index.d.ts +1 -1
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +1 -1
- package/dist/release/version-stamp.d.ts +16 -0
- package/dist/release/version-stamp.d.ts.map +1 -1
- package/dist/release/version-stamp.js +52 -0
- package/dist/release/workspace-config.d.ts +10 -6
- package/dist/release/workspace-config.d.ts.map +1 -1
- package/dist/release/workspace-config.js +10 -6
- package/dist/resolve-schema-ref-kinds.d.ts +4 -3
- package/dist/resolve-schema-ref-kinds.d.ts.map +1 -1
- package/dist/resolve-schema-ref-kinds.js +16 -0
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +4 -3
- package/dist/resolve-zone-containment.d.ts +99 -0
- package/dist/resolve-zone-containment.d.ts.map +1 -0
- package/dist/resolve-zone-containment.js +212 -0
- package/dist/resolve-zone-requirements.d.ts +5 -0
- package/dist/resolve-zone-requirements.d.ts.map +1 -1
- package/dist/resolve-zone-requirements.js +48 -7
- package/dist/resource-rule.d.ts +106 -0
- package/dist/resource-rule.d.ts.map +1 -0
- package/dist/resource-rule.js +236 -0
- package/dist/rule-condition.d.ts +50 -0
- package/dist/rule-condition.d.ts.map +1 -0
- package/dist/rule-condition.js +108 -0
- package/dist/schema-keywords.d.ts.map +1 -1
- package/dist/schema-keywords.js +20 -0
- package/dist/schema-projection.d.ts +171 -0
- package/dist/schema-projection.d.ts.map +1 -0
- package/dist/schema-projection.js +317 -0
- package/dist/schema-walk.d.ts +25 -0
- package/dist/schema-walk.d.ts.map +1 -0
- package/dist/schema-walk.js +126 -0
- package/dist/step-slot.d.ts +39 -0
- package/dist/step-slot.d.ts.map +1 -0
- package/dist/step-slot.js +49 -0
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +17 -3
- package/dist/validate-durable-regions.d.ts +15 -0
- package/dist/validate-durable-regions.d.ts.map +1 -0
- package/dist/validate-durable-regions.js +336 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +44 -2
- package/dist/validate-referrer-rules.d.ts +120 -0
- package/dist/validate-referrer-rules.d.ts.map +1 -0
- package/dist/validate-referrer-rules.js +255 -0
- package/dist/validate-resource-rules.d.ts +114 -0
- package/dist/validate-resource-rules.d.ts.map +1 -0
- package/dist/validate-resource-rules.js +283 -0
- package/dist/validate-schema-projection.d.ts +32 -0
- package/dist/validate-schema-projection.d.ts.map +1 -0
- package/dist/validate-schema-projection.js +110 -0
- package/dist/validate-step-inputs.d.ts +19 -1
- package/dist/validate-step-inputs.d.ts.map +1 -1
- package/dist/validate-step-inputs.js +102 -9
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +11 -5
- package/dist/validate-zone-slots.d.ts +2 -2
- package/dist/validate-zone-slots.d.ts.map +1 -1
- package/dist/validate-zone-slots.js +202 -6
- package/dist/validate-zone-violations.d.ts +12 -0
- package/dist/validate-zone-violations.d.ts.map +1 -0
- package/dist/validate-zone-violations.js +169 -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 +4 -0
- package/dist/zone-slot.d.ts +61 -0
- package/dist/zone-slot.d.ts.map +1 -1
- package/dist/zone-slot.js +86 -5
- package/package.json +2 -2
- package/src/analysis-registry.ts +43 -0
- package/src/analyzer.ts +452 -595
- package/src/call-graph.ts +2 -8
- package/src/cel-scope-query.ts +337 -0
- package/src/cel-scope.ts +570 -0
- package/src/definition-registry.ts +31 -13
- package/src/find-manifest.ts +19 -0
- package/src/flatten-for-analyzer.ts +1 -1
- package/src/import-resolution-diagnostics.ts +25 -7
- package/src/index.ts +86 -0
- package/src/invocation-contract.ts +67 -2
- package/src/loaded-types.ts +12 -3
- package/src/manifest-analysis.ts +132 -0
- package/src/manifest-loader.ts +87 -55
- package/src/manifest-path.ts +34 -0
- package/src/manifest-schemas.ts +328 -14
- package/src/module-compatibility.ts +72 -0
- package/src/ref-slot-reading.ts +156 -0
- package/src/referrer-rule.ts +116 -0
- package/src/release/index.ts +1 -0
- package/src/release/version-stamp.ts +58 -0
- package/src/release/workspace-config.ts +10 -6
- package/src/resolve-schema-ref-kinds.ts +28 -4
- package/src/resolve-throws-union.ts +4 -3
- package/src/resolve-zone-containment.ts +329 -0
- package/src/resolve-zone-requirements.ts +67 -7
- package/src/resource-rule.ts +252 -0
- package/src/rule-condition.ts +126 -0
- package/src/schema-keywords.ts +24 -0
- package/src/schema-projection.ts +403 -0
- package/src/schema-walk.ts +144 -0
- package/src/step-slot.ts +64 -0
- package/src/telo-version.ts +1 -1
- package/src/validate-cel-context.ts +17 -3
- package/src/validate-durable-regions.ts +401 -0
- package/src/validate-references.ts +57 -2
- package/src/validate-referrer-rules.ts +373 -0
- package/src/validate-resource-rules.ts +401 -0
- package/src/validate-schema-projection.ts +184 -0
- package/src/validate-step-inputs.ts +148 -10
- package/src/validate-throws-coverage.ts +12 -5
- package/src/validate-zone-slots.ts +192 -8
- package/src/validate-zone-violations.ts +190 -0
- package/src/value-type-keyword.ts +4 -0
- package/src/zone-slot.ts +114 -5
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
validateAgainstSchema,
|
|
10
10
|
} from "./schema-compat.js";
|
|
11
11
|
import { plainChainOf } from "@telorun/templating";
|
|
12
|
-
import { isLiveSlot, valueTypeOf } from "@telorun/sdk";
|
|
12
|
+
import { isLiveSlot, valueTypeOf, type ResourceDefinition } from "@telorun/sdk";
|
|
13
13
|
import { manifestFragmentOf } from "./manifest-schemas.js";
|
|
14
14
|
import {
|
|
15
15
|
analyzerContractScope,
|
|
@@ -19,6 +19,13 @@ import {
|
|
|
19
19
|
resolveLocalRef,
|
|
20
20
|
walkStepArray,
|
|
21
21
|
} from "./analyzer.js";
|
|
22
|
+
import { readStepSlot } from "./step-slot.js";
|
|
23
|
+
import { navigateConcretePath } from "./manifest-path.js";
|
|
24
|
+
import {
|
|
25
|
+
isRefEntry,
|
|
26
|
+
resolveFieldEntries,
|
|
27
|
+
type ReferenceFieldMap,
|
|
28
|
+
} from "./reference-field-map.js";
|
|
22
29
|
|
|
23
30
|
export interface StepInputIssue {
|
|
24
31
|
path: string;
|
|
@@ -42,7 +49,8 @@ export interface StepInputIssue {
|
|
|
42
49
|
* CEL leaves are replaced by schema-shaped placeholders first (`substituteCelFields`),
|
|
43
50
|
* so an expression is never a false positive — only structural disagreement is
|
|
44
51
|
* reported. Nothing is hardcoded about `Run.Sequence`: the invoke field comes
|
|
45
|
-
* from
|
|
52
|
+
* from the step slot (the shared grammar, or the legacy `x-telo-step-context`),
|
|
53
|
+
* and the paired inputs field from whichever sibling
|
|
46
54
|
* property carries `x-telo-topology-role: inputs`.
|
|
47
55
|
*/
|
|
48
56
|
export function collectStepInputIssues(
|
|
@@ -64,9 +72,18 @@ export function collectStepInputIssues(
|
|
|
64
72
|
const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
|
|
65
73
|
const readingModule = (manifest.metadata as { module?: string } | undefined)?.module;
|
|
66
74
|
|
|
75
|
+
const ctx: CallCheckContext = {
|
|
76
|
+
manifest,
|
|
77
|
+
allManifests,
|
|
78
|
+
defs,
|
|
79
|
+
contractScope,
|
|
80
|
+
readingModule,
|
|
81
|
+
stepContext,
|
|
82
|
+
};
|
|
83
|
+
|
|
67
84
|
for (const [fieldName, fieldSchema] of Object.entries(props)) {
|
|
68
|
-
const stepCtx = fieldSchema
|
|
69
|
-
if (!stepCtx
|
|
85
|
+
const stepCtx = readStepSlot(fieldSchema);
|
|
86
|
+
if (!stepCtx) continue;
|
|
70
87
|
const steps = manifest[fieldName];
|
|
71
88
|
if (!Array.isArray(steps)) continue;
|
|
72
89
|
|
|
@@ -90,6 +107,127 @@ export function collectStepInputIssues(
|
|
|
90
107
|
if (!invoke || typeof invoke !== "object") return;
|
|
91
108
|
if (!values || typeof values !== "object" || Array.isArray(values)) return;
|
|
92
109
|
|
|
110
|
+
out.push(
|
|
111
|
+
...checkCallSite(
|
|
112
|
+
{
|
|
113
|
+
inputsPath: `${stepPath}.${inputsField}`,
|
|
114
|
+
values: values as Record<string, any>,
|
|
115
|
+
invoke,
|
|
116
|
+
// Only a step declares a re-attempt policy, so only a step can carry
|
|
117
|
+
// the live-value-retried finding.
|
|
118
|
+
declaredRetryFor: (invokedManifest, invokedDef) =>
|
|
119
|
+
declaredRetry(step, stepItemSchema, invokedManifest, invokedDef),
|
|
120
|
+
},
|
|
121
|
+
ctx,
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Validate the argument map of every call this resource makes through a
|
|
131
|
+
* REFERENCE SLOT, as opposed to a step.
|
|
132
|
+
*
|
|
133
|
+
* A slot that transfers control names its argument slot on its own `x-telo-ref`
|
|
134
|
+
* (`inputs:`, a JSON Pointer relative to the object enclosing the slot). That
|
|
135
|
+
* annotation is the only thing tying an otherwise-open `inputs:` map to the
|
|
136
|
+
* resource it holds arguments for — an HTTP route's `handler:` + `inputs:` pair
|
|
137
|
+
* is exactly this shape, and nothing about it is a step.
|
|
138
|
+
*
|
|
139
|
+
* Discovery is driven by the annotation rather than by any kind's topology, so
|
|
140
|
+
* a composer that names its argument slot gets its call sites checked without
|
|
141
|
+
* the analyzer learning what a route is. It is the same check the step driver
|
|
142
|
+
* runs, because it is the same question.
|
|
143
|
+
*/
|
|
144
|
+
export function collectRefInputIssues(
|
|
145
|
+
manifest: Record<string, any>,
|
|
146
|
+
fieldMap: ReferenceFieldMap | undefined,
|
|
147
|
+
allManifests: Record<string, any>[],
|
|
148
|
+
defs: DefinitionRegistry,
|
|
149
|
+
aliases: AliasResolver,
|
|
150
|
+
scopes: ModuleScopes,
|
|
151
|
+
): StepInputIssue[] {
|
|
152
|
+
const out: StepInputIssue[] = [];
|
|
153
|
+
if (!fieldMap) return out;
|
|
154
|
+
|
|
155
|
+
const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
|
|
156
|
+
const ctx: CallCheckContext = {
|
|
157
|
+
manifest,
|
|
158
|
+
allManifests,
|
|
159
|
+
defs,
|
|
160
|
+
contractScope,
|
|
161
|
+
readingModule: (manifest.metadata as { module?: string } | undefined)?.module,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
for (const [fieldPath, entry] of fieldMap) {
|
|
165
|
+
if (!isRefEntry(entry) || !entry.inputs) continue;
|
|
166
|
+
const pointer = pointerSegments(entry.inputs);
|
|
167
|
+
if (!pointer) continue;
|
|
168
|
+
|
|
169
|
+
for (const { value: invoke, path: slotPath } of resolveFieldEntries(manifest, fieldPath)) {
|
|
170
|
+
if (!invoke || typeof invoke !== "object" || Array.isArray(invoke)) continue;
|
|
171
|
+
// Relative to the object ENCLOSING the slot, which is the annotation's
|
|
172
|
+
// documented anchor.
|
|
173
|
+
const enclosing = slotPath.slice(0, Math.max(0, slotPath.lastIndexOf(".")));
|
|
174
|
+
const inputsPath = [enclosing, ...pointer].filter(Boolean).join(".");
|
|
175
|
+
const values = navigateConcretePath(manifest, inputsPath);
|
|
176
|
+
if (!values || typeof values !== "object" || Array.isArray(values)) continue;
|
|
177
|
+
|
|
178
|
+
out.push(
|
|
179
|
+
...checkCallSite(
|
|
180
|
+
{ inputsPath, values: values as Record<string, any>, invoke: invoke as Record<string, any> },
|
|
181
|
+
ctx,
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** A JSON Pointer naming a sibling FIELD path. An array index is not a field,
|
|
190
|
+
* so a pointer carrying one names nothing this can resolve. */
|
|
191
|
+
function pointerSegments(pointer: string): string[] | undefined {
|
|
192
|
+
if (!pointer.startsWith("/")) return undefined;
|
|
193
|
+
const segments = pointer
|
|
194
|
+
.slice(1)
|
|
195
|
+
.split("/")
|
|
196
|
+
.map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
197
|
+
return segments.every((s) => s.length > 0 && !/^\d+$/.test(s)) ? segments : undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** One call site: the arguments written, and the reference they are for. */
|
|
201
|
+
interface CallSite {
|
|
202
|
+
/** Concrete path of the argument map, for anchoring a diagnostic. */
|
|
203
|
+
inputsPath: string;
|
|
204
|
+
values: Record<string, any>;
|
|
205
|
+
invoke: Record<string, any>;
|
|
206
|
+
declaredRetryFor?(
|
|
207
|
+
invokedManifest: Record<string, any> | undefined,
|
|
208
|
+
invokedDef: ResourceDefinition | undefined,
|
|
209
|
+
): string | undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
interface CallCheckContext {
|
|
213
|
+
manifest: Record<string, any>;
|
|
214
|
+
allManifests: Record<string, any>[];
|
|
215
|
+
defs: DefinitionRegistry;
|
|
216
|
+
contractScope: ReturnType<typeof analyzerContractScope>;
|
|
217
|
+
readingModule: string | undefined;
|
|
218
|
+
stepContext?: Record<string, any>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The check itself, shared by both drivers: the arguments written at a call site
|
|
223
|
+
* against the contract the target declares.
|
|
224
|
+
*/
|
|
225
|
+
function checkCallSite(site: CallSite, ctx: CallCheckContext): StepInputIssue[] {
|
|
226
|
+
const out: StepInputIssue[] = [];
|
|
227
|
+
const { manifest, allManifests, defs, contractScope, readingModule, stepContext } = ctx;
|
|
228
|
+
const { invoke, values } = site;
|
|
229
|
+
{
|
|
230
|
+
{
|
|
93
231
|
const invokedKind = invoke.kind as string | undefined;
|
|
94
232
|
const invokedName = invoke.name as string | undefined;
|
|
95
233
|
const invokedManifest = invokedName
|
|
@@ -102,7 +240,7 @@ export function collectStepInputIssues(
|
|
|
102
240
|
? contractScope.resolveIn(invokedKind, readingModule)
|
|
103
241
|
: undefined;
|
|
104
242
|
const contract = resolveContract("inputType", invokedManifest, invokedDef, contractScope);
|
|
105
|
-
if (!contract) return;
|
|
243
|
+
if (!contract) return out;
|
|
106
244
|
|
|
107
245
|
// Findings AT a substituted path are about a placeholder, not about
|
|
108
246
|
// anything the author wrote — a `pattern`-constrained string or a `oneOf`
|
|
@@ -161,10 +299,10 @@ export function collectStepInputIssues(
|
|
|
161
299
|
// already declared: the value's liveness by its value type, and the
|
|
162
300
|
// re-attempt by the retry policy. No kind is named.
|
|
163
301
|
if (isLiveSlot(produced)) {
|
|
164
|
-
const retry =
|
|
302
|
+
const retry = site.declaredRetryFor?.(invokedManifest, invokedDef);
|
|
165
303
|
if (retry !== undefined) {
|
|
166
304
|
out.push({
|
|
167
|
-
path: `${
|
|
305
|
+
path: `${site.inputsPath}.${inputName}`,
|
|
168
306
|
targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
|
|
169
307
|
message:
|
|
170
308
|
`'${inputName}' is a live value, which is consumed by reading and so exists ` +
|
|
@@ -182,7 +320,7 @@ export function collectStepInputIssues(
|
|
|
182
320
|
);
|
|
183
321
|
if (compatible) continue;
|
|
184
322
|
out.push({
|
|
185
|
-
path: `${
|
|
323
|
+
path: `${site.inputsPath}.${inputName}`,
|
|
186
324
|
targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
|
|
187
325
|
message: issues.join("; "),
|
|
188
326
|
code: "CEL_TYPE_ARGUMENT_MISMATCH",
|
|
@@ -198,12 +336,12 @@ export function collectStepInputIssues(
|
|
|
198
336
|
// container that should have held it, which does exist.
|
|
199
337
|
const anchor = missingRequired(issue) ? containerOf(issue.path) : issue.path;
|
|
200
338
|
out.push({
|
|
201
|
-
path: anchor ? `${
|
|
339
|
+
path: anchor ? `${site.inputsPath}.${anchor}` : site.inputsPath,
|
|
202
340
|
targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
|
|
203
341
|
message: issue.message,
|
|
204
342
|
});
|
|
205
343
|
}
|
|
206
|
-
}
|
|
344
|
+
}
|
|
207
345
|
}
|
|
208
346
|
return out;
|
|
209
347
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "./resolve-throws-union.js";
|
|
16
16
|
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
17
17
|
import { extractAccessChains, validateChainAgainstSchema } from "./validate-cel-context.js";
|
|
18
|
+
import { isStepSlot } from "./step-slot.js";
|
|
18
19
|
|
|
19
20
|
const SOURCE = "telo-analyzer";
|
|
20
21
|
const TEMPLATE_REGEX = /\$\{\{\s*([^}]+?)\s*\}\}/g;
|
|
@@ -457,9 +458,11 @@ function checkCelChainAgainstDataSchema(
|
|
|
457
458
|
}
|
|
458
459
|
|
|
459
460
|
/** Rule 8 extension: `inherit: true` only makes sense on a definition whose
|
|
460
|
-
* schema
|
|
461
|
-
*
|
|
462
|
-
*
|
|
461
|
+
* schema declares at least one STEP BODY — an array whose items point at the
|
|
462
|
+
* shared grammar, or, for a module published before that fragment existed, one
|
|
463
|
+
* carrying the legacy `x-telo-step-context` annotation. That is what drives the
|
|
464
|
+
* resolver's generic step traversal; a definition with `inherit: true` and no
|
|
465
|
+
* such array has no invocables to inherit from. */
|
|
463
466
|
function validateThrowsDeclarations(manifests: ResourceManifest[]): AnalysisDiagnostic[] {
|
|
464
467
|
const diagnostics: AnalysisDiagnostic[] = [];
|
|
465
468
|
for (const m of manifests) {
|
|
@@ -475,7 +478,11 @@ function validateThrowsDeclarations(manifests: ResourceManifest[]): AnalysisDiag
|
|
|
475
478
|
severity: DiagnosticSeverity.Error,
|
|
476
479
|
code: "INHERIT_WITHOUT_STEP_CONTEXT",
|
|
477
480
|
source: SOURCE,
|
|
478
|
-
message:
|
|
481
|
+
message:
|
|
482
|
+
`Telo.Definition '${name}' declares throws.inherit: true but its schema declares no step ` +
|
|
483
|
+
`body. inherit is only meaningful on a definition that drives invocables through steps — ` +
|
|
484
|
+
`give an array field 'items: { $ref: "telo://manifest#/$defs/Step" }' (the legacy ` +
|
|
485
|
+
`x-telo-step-context annotation is also recognised).`,
|
|
479
486
|
data: { resource: { kind: m.kind, name }, filePath, path: "throws.inherit" },
|
|
480
487
|
});
|
|
481
488
|
}
|
|
@@ -486,7 +493,7 @@ function validateThrowsDeclarations(manifests: ResourceManifest[]): AnalysisDiag
|
|
|
486
493
|
|
|
487
494
|
function schemaHasStepContext(schema: Record<string, any> | undefined): boolean {
|
|
488
495
|
if (!schema || typeof schema !== "object") return false;
|
|
489
|
-
if (
|
|
496
|
+
if (isStepSlot(schema)) return true;
|
|
490
497
|
const props = schema.properties;
|
|
491
498
|
if (props && typeof props === "object") {
|
|
492
499
|
for (const v of Object.values(props as Record<string, any>)) {
|
|
@@ -26,10 +26,11 @@
|
|
|
26
26
|
*
|
|
27
27
|
* Browser-safe: no Node built-ins.
|
|
28
28
|
*/
|
|
29
|
-
import type
|
|
29
|
+
import { ZONE_ATTRIBUTES, zoneAttributeNames, type ResourceManifest } from "@telorun/sdk";
|
|
30
|
+
import { distance } from "./levenshtein.js";
|
|
30
31
|
|
|
31
32
|
export interface ZoneSlotIssue {
|
|
32
|
-
code: "ZONE_ANNOTATION_INVALID";
|
|
33
|
+
code: "ZONE_ANNOTATION_INVALID" | "ZONE_ATTRIBUTE_UNKNOWN" | "ZONE_ATTRIBUTE_INCOMPLETE";
|
|
33
34
|
manifest: ResourceManifest;
|
|
34
35
|
/** Schema path of the annotated slot. */
|
|
35
36
|
path: string;
|
|
@@ -38,6 +39,7 @@ export interface ZoneSlotIssue {
|
|
|
38
39
|
|
|
39
40
|
const PROVIDES = "x-telo-provides-zone";
|
|
40
41
|
const REQUIRES = "x-telo-requires-zone";
|
|
42
|
+
const VIOLATES = "x-telo-violates-zone";
|
|
41
43
|
|
|
42
44
|
/** A self-relative JSON Pointer, the only correlation-key spelling both halves
|
|
43
45
|
* read identically. `""` (whole document) is meaningless as a key, so a
|
|
@@ -53,6 +55,107 @@ function describe(value: unknown): string {
|
|
|
53
55
|
return typeof value;
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
/** The closest declared attribute name within an edit-distance threshold, or
|
|
59
|
+
* undefined. Mirrors `suggestValueType`: case-sensitive, and silent on a tie,
|
|
60
|
+
* because a coin-flip suggestion is worse than none. */
|
|
61
|
+
function suggestAttribute(name: string): string | undefined {
|
|
62
|
+
if (!name) return undefined;
|
|
63
|
+
const threshold = Math.min(3, Math.floor(name.length / 3));
|
|
64
|
+
if (threshold < 1) return undefined;
|
|
65
|
+
let best: string | undefined;
|
|
66
|
+
let bestDist = threshold + 1;
|
|
67
|
+
let tied = false;
|
|
68
|
+
for (const candidate of ZONE_ATTRIBUTES.keys()) {
|
|
69
|
+
const d = distance(name, candidate);
|
|
70
|
+
if (d < bestDist) {
|
|
71
|
+
best = candidate;
|
|
72
|
+
bestDist = d;
|
|
73
|
+
tied = false;
|
|
74
|
+
} else if (d === bestDist) {
|
|
75
|
+
tied = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return !best || bestDist > threshold || tied ? undefined : best;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The attributes half of the object form.
|
|
83
|
+
*
|
|
84
|
+
* The composed `additionalProperties: false` schema would already reject an
|
|
85
|
+
* unknown name; the dedicated code earns its place by NAMING the valid ones and
|
|
86
|
+
* suggesting a spelling, instead of reporting a schema violation on a key the
|
|
87
|
+
* author believed was real — the same standing `X_TELO_TYPE_UNKNOWN` has over
|
|
88
|
+
* the value-type vocabulary it is modelled on.
|
|
89
|
+
*/
|
|
90
|
+
function checkAttributes(
|
|
91
|
+
obj: Record<string, unknown>,
|
|
92
|
+
definition: ResourceManifest,
|
|
93
|
+
path: string,
|
|
94
|
+
issues: ZoneSlotIssue[],
|
|
95
|
+
annotation: string = PROVIDES,
|
|
96
|
+
): void {
|
|
97
|
+
const declared = new Set<string>();
|
|
98
|
+
|
|
99
|
+
for (const [name, value] of Object.entries(obj)) {
|
|
100
|
+
if (name === "key") continue;
|
|
101
|
+
const entry = ZONE_ATTRIBUTES.get(name);
|
|
102
|
+
if (!entry) {
|
|
103
|
+
const suggestion = suggestAttribute(name);
|
|
104
|
+
issues.push({
|
|
105
|
+
code: "ZONE_ATTRIBUTE_UNKNOWN",
|
|
106
|
+
manifest: definition,
|
|
107
|
+
path,
|
|
108
|
+
message:
|
|
109
|
+
`${annotation} at '${path}' declares '${name}', which is not a zone attribute. ` +
|
|
110
|
+
(suggestion ? `Did you mean '${suggestion}'? ` : "") +
|
|
111
|
+
`The vocabulary is closed: ${zoneAttributeNames().join(", ")}. An attribute ` +
|
|
112
|
+
`states a property of everything executed inside this zone, and every reader ` +
|
|
113
|
+
`of one is core — a name outside the set would be read by nothing.`,
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Each value is the REASON, and it is required by being the value itself
|
|
118
|
+
// rather than a sibling of a boolean. That is also what makes a type check
|
|
119
|
+
// possible at all: there is no `true` to accept, so `atomic: true` is caught
|
|
120
|
+
// here rather than reading as a valid declaration with nothing to say.
|
|
121
|
+
if (typeof value !== "string" || !value) {
|
|
122
|
+
issues.push({
|
|
123
|
+
code: "ZONE_ANNOTATION_INVALID",
|
|
124
|
+
manifest: definition,
|
|
125
|
+
path,
|
|
126
|
+
message:
|
|
127
|
+
`${annotation} at '${path}' declares '${name}' as ${describe(value)}. Every zone ` +
|
|
128
|
+
`attribute's value is the author's REASON — a non-empty sentence, quoted verbatim ` +
|
|
129
|
+
`by the diagnostics that enforce it (${entry.description}).`,
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
declared.add(name);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The completeness rule is about what a region PROMISES: declaring `atomic`
|
|
137
|
+
// without `noSuspend` is an incomplete promise. A VIOLATION is not a promise —
|
|
138
|
+
// breaking one guarantee implies nothing about breaking another — so the rule
|
|
139
|
+
// does not apply there.
|
|
140
|
+
if (annotation !== PROVIDES) return;
|
|
141
|
+
// `requires:` lives in the vocabulary entry rather than as a hardcoded pair of
|
|
142
|
+
// names here, so the completeness rule sits beside the thing it constrains.
|
|
143
|
+
for (const name of declared) {
|
|
144
|
+
for (const dependency of ZONE_ATTRIBUTES.get(name)!.requires) {
|
|
145
|
+
if (declared.has(dependency)) continue;
|
|
146
|
+
issues.push({
|
|
147
|
+
code: "ZONE_ATTRIBUTE_INCOMPLETE",
|
|
148
|
+
manifest: definition,
|
|
149
|
+
path,
|
|
150
|
+
message:
|
|
151
|
+
`${PROVIDES} at '${path}' declares '${name}' without '${dependency}', which it ` +
|
|
152
|
+
`requires. ${ZONE_ATTRIBUTES.get(dependency)!.description} Declare it with its own ` +
|
|
153
|
+
`reason — a generic message is exactly what the required reason exists to prevent.`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
56
159
|
function checkProvides(
|
|
57
160
|
raw: unknown,
|
|
58
161
|
definition: ResourceManifest,
|
|
@@ -61,16 +164,38 @@ function checkProvides(
|
|
|
61
164
|
): void {
|
|
62
165
|
if (raw === true) return;
|
|
63
166
|
if (isPointer(raw)) return;
|
|
167
|
+
|
|
168
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
169
|
+
const obj = raw as Record<string, unknown>;
|
|
170
|
+
// `key` keeps the meaning it has in the scalar spelling; only its absence is
|
|
171
|
+
// legitimate here (an uncorrelated zone that still declares attributes).
|
|
172
|
+
if (obj.key !== undefined && !isPointer(obj.key)) {
|
|
173
|
+
issues.push({
|
|
174
|
+
code: "ZONE_ANNOTATION_INVALID",
|
|
175
|
+
manifest: definition,
|
|
176
|
+
path,
|
|
177
|
+
message:
|
|
178
|
+
`${PROVIDES} at '${path}' declares the correlation key ${describe(obj.key)}, which ` +
|
|
179
|
+
`is not a self-relative JSON Pointer. Write '/connection'. A bare field name is ` +
|
|
180
|
+
`read as a pointer by the runtime but skipped by the checker, so the two halves ` +
|
|
181
|
+
`would disagree about what this manifest means.`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
checkAttributes(obj, definition, path, issues);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
64
188
|
issues.push({
|
|
65
189
|
code: "ZONE_ANNOTATION_INVALID",
|
|
66
190
|
manifest: definition,
|
|
67
191
|
path,
|
|
68
192
|
message:
|
|
69
193
|
`${PROVIDES} at '${path}' is ${describe(raw)}. It takes 'true' (the zone is ` +
|
|
70
|
-
`uncorrelated)
|
|
194
|
+
`uncorrelated), a self-relative JSON Pointer naming this kind's own field ` +
|
|
71
195
|
`whose resolved reference the zone carries as its correlation payload ` +
|
|
72
|
-
`(e.g. '/connection')
|
|
73
|
-
`
|
|
196
|
+
`(e.g. '/connection'), or an object carrying that pointer as 'key' beside the ` +
|
|
197
|
+
`zone attributes this region declares (${zoneAttributeNames().join(", ")}). It ` +
|
|
198
|
+
`never names the zone — the zone a slot provides is always the declaring kind.`,
|
|
74
199
|
});
|
|
75
200
|
}
|
|
76
201
|
|
|
@@ -129,17 +254,74 @@ function checkRequires(
|
|
|
129
254
|
fail(`${REQUIRES} at '${path}' declares a non-string 'reason'.`);
|
|
130
255
|
}
|
|
131
256
|
|
|
257
|
+
// `attributes` is the GUARANTEE half — what the satisfying zone must declare,
|
|
258
|
+
// as opposed to which kind it is. It is a list of NAMES with no reasons: the
|
|
259
|
+
// reason belongs to whoever makes the promise, and a requirer restating it
|
|
260
|
+
// would be a second sentence to keep in step with the one that is enforced.
|
|
261
|
+
if (obj.attributes !== undefined) {
|
|
262
|
+
if (!Array.isArray(obj.attributes) || obj.attributes.length === 0) {
|
|
263
|
+
fail(
|
|
264
|
+
`${REQUIRES} at '${path}' declares 'attributes' as ${describe(obj.attributes)}. It ` +
|
|
265
|
+
`takes a non-empty list of zone attribute names; omit it to require the kind alone.`,
|
|
266
|
+
);
|
|
267
|
+
} else {
|
|
268
|
+
for (const name of obj.attributes) {
|
|
269
|
+
if (typeof name === "string" && ZONE_ATTRIBUTES.has(name)) continue;
|
|
270
|
+
const suggestion = typeof name === "string" ? suggestAttribute(name) : undefined;
|
|
271
|
+
issues.push({
|
|
272
|
+
code: "ZONE_ATTRIBUTE_UNKNOWN",
|
|
273
|
+
manifest: definition,
|
|
274
|
+
path,
|
|
275
|
+
message:
|
|
276
|
+
`${REQUIRES} at '${path}' requires '${String(name)}', which is not a zone ` +
|
|
277
|
+
`attribute. ` +
|
|
278
|
+
(suggestion ? `Did you mean '${suggestion}'? ` : "") +
|
|
279
|
+
`The vocabulary is closed: ${zoneAttributeNames().join(", ")}.`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
132
285
|
for (const key of Object.keys(obj)) {
|
|
133
|
-
if (key === "zone" || key === "key" || key === "reason") {
|
|
286
|
+
if (key === "zone" || key === "key" || key === "reason" || key === "attributes") {
|
|
134
287
|
continue;
|
|
135
288
|
}
|
|
136
289
|
fail(
|
|
137
290
|
`${REQUIRES} at '${path}' declares an unknown property '${key}'. The object form takes ` +
|
|
138
|
-
`'zone', 'key' and '
|
|
291
|
+
`'zone', 'key', 'reason' and 'attributes'.`,
|
|
139
292
|
);
|
|
140
293
|
}
|
|
141
294
|
}
|
|
142
295
|
|
|
296
|
+
/**
|
|
297
|
+
* The third annotation: what this kind cannot honour.
|
|
298
|
+
*
|
|
299
|
+
* Only the object form, because a violation without a reason is exactly the
|
|
300
|
+
* shape the vocabulary refuses everywhere else — the diagnostic that fires on it
|
|
301
|
+
* prints the region's promise and this resource's rebuttal side by side, and one
|
|
302
|
+
* of the two would otherwise be a generated sentence.
|
|
303
|
+
*/
|
|
304
|
+
function checkViolates(
|
|
305
|
+
raw: unknown,
|
|
306
|
+
definition: ResourceManifest,
|
|
307
|
+
path: string,
|
|
308
|
+
issues: ZoneSlotIssue[],
|
|
309
|
+
): void {
|
|
310
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
311
|
+
issues.push({
|
|
312
|
+
code: "ZONE_ANNOTATION_INVALID",
|
|
313
|
+
manifest: definition,
|
|
314
|
+
path,
|
|
315
|
+
message:
|
|
316
|
+
`${VIOLATES} at '${path}' is ${describe(raw)}. It takes a map of zone attribute ` +
|
|
317
|
+
`names to the reason this kind cannot honour each — ` +
|
|
318
|
+
`\`noSuspend: this waits for a delivery that may be days away\`.`,
|
|
319
|
+
});
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
checkAttributes(raw as Record<string, unknown>, definition, path, issues, VIOLATES);
|
|
323
|
+
}
|
|
324
|
+
|
|
143
325
|
/** Walk a definition schema, reporting every zone annotation it cannot read.
|
|
144
326
|
* Pure-schema walk, so it needs a visited guard for cyclic `$defs`. */
|
|
145
327
|
function walkSchema(
|
|
@@ -159,8 +341,10 @@ function walkSchema(
|
|
|
159
341
|
const obj = node as Record<string, unknown>;
|
|
160
342
|
if (obj[PROVIDES] !== undefined) checkProvides(obj[PROVIDES], definition, path, issues);
|
|
161
343
|
if (obj[REQUIRES] !== undefined) checkRequires(obj[REQUIRES], definition, path, issues);
|
|
344
|
+
if (obj[VIOLATES] !== undefined) checkViolates(obj[VIOLATES], definition, path, issues);
|
|
162
345
|
for (const [key, value] of Object.entries(obj)) {
|
|
163
|
-
if (key === PROVIDES || key === REQUIRES || key ===
|
|
346
|
+
if (key === PROVIDES || key === REQUIRES || key === VIOLATES) continue;
|
|
347
|
+
if (key === "examples" || key === "default") continue;
|
|
164
348
|
walkSchema(value, path ? `${path}.${key}` : key, visited, definition, issues);
|
|
165
349
|
}
|
|
166
350
|
}
|