@telorun/analyzer 0.58.0 → 0.60.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 +9 -2
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +38 -10
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +16 -42
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +35 -0
- package/dist/flatten-for-analyzer.d.ts +4 -0
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +8 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/invocation-contract.d.ts +37 -0
- package/dist/invocation-contract.d.ts.map +1 -1
- package/dist/invocation-contract.js +81 -1
- package/dist/kernel-globals.d.ts +1 -1
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +15 -1
- package/dist/manifest-schemas.d.ts +407 -0
- package/dist/manifest-schemas.d.ts.map +1 -0
- package/dist/manifest-schemas.js +396 -0
- package/dist/module-file-claims.d.ts +6 -0
- package/dist/module-file-claims.d.ts.map +1 -1
- package/dist/module-file-claims.js +4 -0
- package/dist/module-metadata-scope.d.ts +44 -0
- package/dist/module-metadata-scope.d.ts.map +1 -0
- package/dist/module-metadata-scope.js +82 -0
- package/dist/parse-loaded-file.d.ts.map +1 -1
- package/dist/parse-loaded-file.js +16 -0
- package/dist/release/bump-level.d.ts +46 -0
- package/dist/release/bump-level.d.ts.map +1 -0
- package/dist/release/bump-level.js +80 -0
- package/dist/release/changelog.d.ts +36 -0
- package/dist/release/changelog.d.ts.map +1 -0
- package/dist/release/changelog.js +53 -0
- package/dist/release/fragment.d.ts +48 -0
- package/dist/release/fragment.d.ts.map +1 -0
- package/dist/release/fragment.js +84 -0
- package/dist/release/index.d.ts +26 -0
- package/dist/release/index.d.ts.map +1 -0
- package/dist/release/index.js +18 -0
- package/dist/release/ledger.d.ts +53 -0
- package/dist/release/ledger.d.ts.map +1 -0
- package/dist/release/ledger.js +109 -0
- package/dist/release/payload-digest.d.ts +63 -0
- package/dist/release/payload-digest.d.ts.map +1 -0
- package/dist/release/payload-digest.js +65 -0
- package/dist/release/release-plan.d.ts +128 -0
- package/dist/release/release-plan.d.ts.map +1 -0
- package/dist/release/release-plan.js +277 -0
- package/dist/release/version-stamp.d.ts +50 -0
- package/dist/release/version-stamp.d.ts.map +1 -0
- package/dist/release/version-stamp.js +119 -0
- package/dist/release/workspace-config.d.ts +41 -0
- package/dist/release/workspace-config.d.ts.map +1 -0
- package/dist/release/workspace-config.js +60 -0
- package/dist/schema-compat.d.ts +18 -0
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +71 -7
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +69 -13
- package/dist/validate-step-inputs.d.ts +1 -1
- package/dist/validate-step-inputs.d.ts.map +1 -1
- package/dist/validate-step-inputs.js +99 -8
- package/package.json +3 -3
- package/src/analyzer.ts +41 -10
- package/src/builtins.ts +16 -42
- package/src/cel-environment.ts +37 -0
- package/src/flatten-for-analyzer.ts +12 -0
- package/src/index.ts +18 -3
- package/src/invocation-contract.ts +92 -1
- package/src/kernel-globals.ts +23 -1
- package/src/manifest-schemas.ts +408 -0
- package/src/module-file-claims.ts +10 -0
- package/src/module-metadata-scope.ts +88 -0
- package/src/parse-loaded-file.ts +16 -0
- package/src/release/bump-level.ts +95 -0
- package/src/release/changelog.ts +62 -0
- package/src/release/fragment.ts +130 -0
- package/src/release/index.ts +66 -0
- package/src/release/ledger.ts +142 -0
- package/src/release/payload-digest.ts +83 -0
- package/src/release/release-plan.ts +392 -0
- package/src/release/version-stamp.ts +142 -0
- package/src/release/workspace-config.ts +81 -0
- package/src/schema-compat.ts +69 -5
- package/src/validate-cel-context.ts +82 -13
- package/src/validate-step-inputs.ts +109 -9
package/src/schema-compat.ts
CHANGED
|
@@ -3,7 +3,6 @@ import addFormats from "ajv-formats";
|
|
|
3
3
|
import {
|
|
4
4
|
isRefSentinel,
|
|
5
5
|
isTaggedSentinel,
|
|
6
|
-
ManifestRootSchema,
|
|
7
6
|
producedTypeOf,
|
|
8
7
|
} from "@telorun/templating";
|
|
9
8
|
import {
|
|
@@ -14,6 +13,7 @@ import {
|
|
|
14
13
|
valueTypeOf,
|
|
15
14
|
valueTypePlaceholder,
|
|
16
15
|
} from "@telorun/sdk";
|
|
16
|
+
import { ManifestRootSchema } from "./manifest-schemas.js";
|
|
17
17
|
import { registerTeloKeywords } from "./value-type-keyword.js";
|
|
18
18
|
|
|
19
19
|
const Ajv = (AjvModule as any).default ?? AjvModule;
|
|
@@ -48,6 +48,16 @@ export interface CompatibilityResult {
|
|
|
48
48
|
issues: string[];
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** The alternatives a union node declares, or undefined when it is not one.
|
|
52
|
+
* `anyOf` and `oneOf` are one question here — which branches could accept this
|
|
53
|
+
* value — and their difference (exactly-one vs at-least-one) is a validation
|
|
54
|
+
* rule, not a compatibility one. */
|
|
55
|
+
function unionBranches(schema: Record<string, any>): Record<string, any>[] | undefined {
|
|
56
|
+
const branches = schema.anyOf ?? schema.oneOf;
|
|
57
|
+
if (!Array.isArray(branches) || branches.length === 0) return undefined;
|
|
58
|
+
return branches.filter((b) => b && typeof b === "object") as Record<string, any>[];
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
/**
|
|
52
62
|
* Conservative structural JSON Schema compatibility check — is a value shaped
|
|
53
63
|
* like `source` acceptable where `target` is declared?
|
|
@@ -115,6 +125,38 @@ function compare(
|
|
|
115
125
|
const source = deref(rawSource, resolveRef);
|
|
116
126
|
const target = deref(rawTarget, resolveRef);
|
|
117
127
|
|
|
128
|
+
// A union is ALTERNATIVES, so it is compared by distributing over branches on
|
|
129
|
+
// both sides: a definite conflict is one where no source-branch/target-branch
|
|
130
|
+
// pair agrees. Returning silently the moment either side was a union — which
|
|
131
|
+
// is what this did — switched the whole comparison off for any slot that
|
|
132
|
+
// accepts more than one shape, and those are exactly the slots where a value
|
|
133
|
+
// type carries the only information distinguishing the branches.
|
|
134
|
+
//
|
|
135
|
+
// `allOf` is a conjunction rather than a choice, so it keeps the old posture:
|
|
136
|
+
// it says too little to judge and stays compatible.
|
|
137
|
+
if (source.allOf || target.allOf) return;
|
|
138
|
+
const sourceBranches = unionBranches(source);
|
|
139
|
+
const targetBranches = unionBranches(target);
|
|
140
|
+
if (sourceBranches || targetBranches) {
|
|
141
|
+
const lefts = sourceBranches ?? [source];
|
|
142
|
+
const rights = targetBranches ?? [target];
|
|
143
|
+
const reasons: string[] = [];
|
|
144
|
+
for (const left of lefts) {
|
|
145
|
+
for (const right of rights) {
|
|
146
|
+
const probe: string[] = [];
|
|
147
|
+
// A fresh `seen` per probe: a pair rejected on one branch must not mark
|
|
148
|
+
// a reference pair visited for the next, which would silently pass it.
|
|
149
|
+
compare(left, right, path, probe, resolveRef, new Set(seen));
|
|
150
|
+
if (probe.length === 0) return;
|
|
151
|
+
reasons.push(...probe);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
issues.push(
|
|
155
|
+
`${path || "/"}: no alternative matches — ${[...new Set(reasons)].join("; ")}`,
|
|
156
|
+
);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
118
160
|
// Value types first: an `instance` representation has no JSON `type` to
|
|
119
161
|
// compare, so its identity IS the comparison — and its arguments are where the
|
|
120
162
|
// real information lives.
|
|
@@ -180,8 +222,6 @@ function compare(
|
|
|
180
222
|
);
|
|
181
223
|
return;
|
|
182
224
|
}
|
|
183
|
-
if (source.anyOf || source.oneOf || source.allOf) return;
|
|
184
|
-
if (target.anyOf || target.oneOf || target.allOf) return;
|
|
185
225
|
|
|
186
226
|
// An array's element, which the old comparison never looked at — so every
|
|
187
227
|
// nested shape passed regardless of what it contained.
|
|
@@ -425,7 +465,14 @@ export function celTypeSatisfiesJsonSchema(celType: string, schema: Record<strin
|
|
|
425
465
|
return true;
|
|
426
466
|
}
|
|
427
467
|
if (!schema.type && !schema.anyOf && !schema.oneOf && !schema.allOf) return true;
|
|
428
|
-
|
|
468
|
+
// `allOf` is a conjunction and says too little to judge from a single CEL
|
|
469
|
+
// type. A union is a CHOICE, so it is satisfied by satisfying any branch —
|
|
470
|
+
// distributed for the same reason `compare` does it: accepting every union
|
|
471
|
+
// outright turns the check off for exactly the slots that admit more than one
|
|
472
|
+
// shape, and those are the ones where the branches carry the information.
|
|
473
|
+
if (schema.allOf) return true;
|
|
474
|
+
const branches = unionBranches(schema);
|
|
475
|
+
if (branches) return branches.some((branch) => celTypeSatisfiesJsonSchema(celType, branch));
|
|
429
476
|
const schemaTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
430
477
|
const accepted: Record<string, string[]> = {
|
|
431
478
|
int: ["integer", "number"],
|
|
@@ -528,6 +575,23 @@ export function celPlaceholderForSchema(rawSchema: Record<string, any>): unknown
|
|
|
528
575
|
// member chosen is irrelevant — only its acceptability to AJV matters, since
|
|
529
576
|
// the real value is checked at runtime once the expression resolves.
|
|
530
577
|
if (Array.isArray(schema.enum) && schema.enum.length > 0) return schema.enum[0];
|
|
578
|
+
// A UNION with no `type` of its own. Without this, a whole-field CEL leaf at
|
|
579
|
+
// such a slot gets `null`, which every branch then rejects — so a field
|
|
580
|
+
// declared `anyOf: [array, boolean]` could not be written as an expression at
|
|
581
|
+
// all, while one whose union happens to contain a `live` branch escaped by
|
|
582
|
+
// accident (nothing validates a live value, so `null` passed). The first
|
|
583
|
+
// branch that yields a placeholder wins: the same conservative posture
|
|
584
|
+
// `selectUnionBranch` takes, and enough for AJV, whose question is only
|
|
585
|
+
// whether SOME branch accepts the stand-in.
|
|
586
|
+
if (schema.type === undefined) {
|
|
587
|
+
const branches = unionBranches(schema);
|
|
588
|
+
if (branches) {
|
|
589
|
+
for (const branch of branches) {
|
|
590
|
+
const candidate = celPlaceholderForSchema(branch);
|
|
591
|
+
if (candidate !== null) return candidate;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
531
595
|
switch (schema.type) {
|
|
532
596
|
case "integer":
|
|
533
597
|
case "number":
|
|
@@ -603,7 +667,7 @@ export function resolveRef(schema: Record<string, any>, root: Record<string, any
|
|
|
603
667
|
* — an ambiguous union is one the analyzer should not resolve on the author's
|
|
604
668
|
* behalf.
|
|
605
669
|
*/
|
|
606
|
-
function selectUnionBranch(
|
|
670
|
+
export function selectUnionBranch(
|
|
607
671
|
schema: Record<string, any>,
|
|
608
672
|
data: unknown,
|
|
609
673
|
root: Record<string, any>,
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export { extractAccessChains, validateChainAgainstSchema } from "@telorun/templating";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
elementSchemaOf,
|
|
4
|
+
isLiveSlot,
|
|
5
|
+
mergeTypeSchemas,
|
|
6
|
+
parseCanonicalTypeSchemaId,
|
|
7
|
+
} from "@telorun/sdk";
|
|
3
8
|
import { KERNEL_BUILTINS } from "./builtins.js";
|
|
4
9
|
|
|
5
10
|
export interface ContextResolveOpts {
|
|
@@ -206,23 +211,73 @@ function schemaAtChain(
|
|
|
206
211
|
return cur && typeof cur === "object" ? cur : undefined;
|
|
207
212
|
}
|
|
208
213
|
|
|
209
|
-
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
|
|
214
|
+
/**
|
|
215
|
+
* The schema of a sibling collection expression, when statically known.
|
|
216
|
+
*
|
|
217
|
+
* Resolves `inputs.*` chains against the resource's DECLARED contract, falling
|
|
218
|
+
* back to the legacy `inputs:` property map some kinds still carry. Reading the
|
|
219
|
+
* contract is what makes this work at all for a kind that declares `inputType:`
|
|
220
|
+
* — resolving only the property map left `item` untyped in every such kind,
|
|
221
|
+
* silently, which reads as "element typing is permissive here" rather than as a
|
|
222
|
+
* gap. Returns undefined for a non-chain or untyped collection, and the caller
|
|
223
|
+
* substitutes `dyn` rather than inventing an element type.
|
|
224
|
+
*/
|
|
225
|
+
function resolveCollectionSchema(
|
|
214
226
|
manifestRoot: Record<string, any>,
|
|
215
227
|
field: string,
|
|
228
|
+
allManifests: Record<string, any>[] | undefined,
|
|
216
229
|
): Record<string, any> | undefined {
|
|
217
230
|
const chain = purePathChain(manifestRoot?.[field]);
|
|
218
231
|
if (!chain || chain[0] !== "inputs") return undefined;
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
232
|
+
const declared = resolveTypeFieldToSchema(manifestRoot.inputType, allManifests ?? []);
|
|
233
|
+
const root =
|
|
234
|
+
declared && typeof declared === "object"
|
|
235
|
+
? declared
|
|
236
|
+
: manifestRoot.inputs && typeof manifestRoot.inputs === "object"
|
|
237
|
+
? { type: "object", properties: manifestRoot.inputs }
|
|
238
|
+
: undefined;
|
|
239
|
+
if (!root) return undefined;
|
|
240
|
+
return schemaAtChain(chain.slice(1), root);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* What ITERATING a collection schema yields.
|
|
245
|
+
*
|
|
246
|
+
* An array answers with `items`; anything else answers through the value-type
|
|
247
|
+
* vocabulary, which is where "what is the element of this" is declared. No type
|
|
248
|
+
* is named here on purpose: a future iterable value type is covered by declaring
|
|
249
|
+
* `element` on one of its parameters, with nothing to change in the analyzer.
|
|
250
|
+
*/
|
|
251
|
+
function elementOfCollection(
|
|
252
|
+
collection: Record<string, any> | undefined,
|
|
253
|
+
): Record<string, any> | undefined {
|
|
254
|
+
if (!collection || typeof collection !== "object") return undefined;
|
|
255
|
+
if (collection.type === "array") {
|
|
256
|
+
return collection.items && typeof collection.items === "object"
|
|
257
|
+
? (collection.items as Record<string, any>)
|
|
258
|
+
: undefined;
|
|
224
259
|
}
|
|
225
|
-
|
|
260
|
+
const element = elementSchemaOf(collection);
|
|
261
|
+
return element && typeof element === "object" ? (element as Record<string, any>) : undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* True when a context binding naming this collection must NOT be bound.
|
|
266
|
+
*
|
|
267
|
+
* A binding that re-exposes the collection can only hand over the value the
|
|
268
|
+
* consumer is already draining, and passing that on is an ordinary
|
|
269
|
+
* pass-through no member-access rule catches — so the drain is silent. `live` is
|
|
270
|
+
* exactly the property that makes a value unsafe to re-expose, and it is already
|
|
271
|
+
* in the vocabulary, so no consumer names a type to decide this.
|
|
272
|
+
*/
|
|
273
|
+
function collectionBindingWithheld(
|
|
274
|
+
schema: Record<string, any>,
|
|
275
|
+
manifestRoot: Record<string, any>,
|
|
276
|
+
allManifests: Record<string, any>[] | undefined,
|
|
277
|
+
): boolean {
|
|
278
|
+
const from = schema?.["x-telo-context-collection-from"] as string | undefined;
|
|
279
|
+
if (!from) return false;
|
|
280
|
+
return isLiveSlot(resolveCollectionSchema(manifestRoot, from, allManifests));
|
|
226
281
|
}
|
|
227
282
|
|
|
228
283
|
/**
|
|
@@ -342,10 +397,20 @@ export function resolveContextAnnotations(
|
|
|
342
397
|
// untyped sources fall back to `dyn` so a wrong element type is never invented.
|
|
343
398
|
const elementFrom = schema["x-telo-context-element-from"] as string | undefined;
|
|
344
399
|
if (elementFrom) {
|
|
345
|
-
const items =
|
|
400
|
+
const items = elementOfCollection(resolveCollectionSchema(manifestRoot, elementFrom, allManifests));
|
|
346
401
|
return items ?? {};
|
|
347
402
|
}
|
|
348
403
|
|
|
404
|
+
// The collection itself, as opposed to its element. Typed from whatever the
|
|
405
|
+
// sibling resolves to, so an array binding keeps the precision it has today
|
|
406
|
+
// instead of degrading to `dyn` now that the slot admits more than one shape.
|
|
407
|
+
// A `live` collection never reaches here — the property is dropped before
|
|
408
|
+
// recursion, so the name is simply not in scope.
|
|
409
|
+
const collectionFrom = schema["x-telo-context-collection-from"] as string | undefined;
|
|
410
|
+
if (collectionFrom) {
|
|
411
|
+
return resolveCollectionSchema(manifestRoot, collectionFrom, allManifests) ?? {};
|
|
412
|
+
}
|
|
413
|
+
|
|
349
414
|
const fromRoot = schema["x-telo-context-from-root"] as string | undefined;
|
|
350
415
|
const fromRefKindRaw = schema["x-telo-context-from-ref-kind"] as
|
|
351
416
|
| string
|
|
@@ -445,6 +510,10 @@ export function resolveContextAnnotations(
|
|
|
445
510
|
if (schema.properties) {
|
|
446
511
|
const props: Record<string, any> = {};
|
|
447
512
|
for (const [k, v] of Object.entries(schema.properties)) {
|
|
513
|
+
// Withholding happens HERE rather than inside the child resolver, because
|
|
514
|
+
// this is the only level that owns the property map — a child can return a
|
|
515
|
+
// schema but cannot remove itself from one.
|
|
516
|
+
if (collectionBindingWithheld(v as Record<string, any>, manifestRoot, allManifests)) continue;
|
|
448
517
|
props[k] = resolveContextAnnotations(v as Record<string, any>, manifestItem, normalizedOpts);
|
|
449
518
|
}
|
|
450
519
|
return { ...schema, properties: props };
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
validateAgainstSchema,
|
|
10
10
|
} from "./schema-compat.js";
|
|
11
11
|
import { plainChainOf } from "@telorun/templating";
|
|
12
|
-
import { valueTypeOf } from "@telorun/sdk";
|
|
12
|
+
import { isLiveSlot, valueTypeOf } from "@telorun/sdk";
|
|
13
|
+
import { manifestFragmentOf } from "./manifest-schemas.js";
|
|
13
14
|
import {
|
|
14
15
|
analyzerContractScope,
|
|
15
16
|
containerOf,
|
|
@@ -25,7 +26,7 @@ export interface StepInputIssue {
|
|
|
25
26
|
message: string;
|
|
26
27
|
/** Set when the issue is a type-argument disagreement rather than a contract
|
|
27
28
|
* shape violation — the two read differently and deserve their own code. */
|
|
28
|
-
code?: "CEL_TYPE_ARGUMENT_MISMATCH";
|
|
29
|
+
code?: "CEL_TYPE_ARGUMENT_MISMATCH" | "LIVE_VALUE_RETRIED";
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
|
|
@@ -118,15 +119,30 @@ export function collectStepInputIssues(
|
|
|
118
119
|
// that silence is exactly where a stream of the wrong element used to
|
|
119
120
|
// flow. The comparison is covariant and gradual: an omitted argument is
|
|
120
121
|
// *any* in both directions, so only a definite conflict is reported.
|
|
121
|
-
|
|
122
|
+
// The roots a plain chain may name here, each paired with the schema it is
|
|
123
|
+
// navigated against. `steps.` is the step map (analyzer state, supplied by
|
|
124
|
+
// the caller). `inputs.` is the ENCLOSING kind's own declared inputType,
|
|
125
|
+
// which is how a value produced OUTSIDE this resource reaches a step at
|
|
126
|
+
// all: an HTTP route maps `request.body` into its handler's inputs, and the
|
|
127
|
+
// handler forwards `inputs.body` onward — the shape a live value most often
|
|
128
|
+
// arrives in, and the one covering only `steps.` missed entirely. A root
|
|
129
|
+
// this cannot resolve contributes nothing rather than guessing at a schema.
|
|
130
|
+
const roots: Array<[string, Record<string, any>]> = [];
|
|
131
|
+
if (stepContext) roots.push(["steps.", stepContext]);
|
|
132
|
+
const ownContract = resolveContract(
|
|
133
|
+
"inputType",
|
|
134
|
+
manifest,
|
|
135
|
+
contractScope.resolveIn(manifest.kind as string, readingModule),
|
|
136
|
+
contractScope,
|
|
137
|
+
);
|
|
138
|
+
if (ownContract) roots.push(["inputs.", ownContract.schema]);
|
|
139
|
+
|
|
140
|
+
if (roots.length > 0) {
|
|
122
141
|
for (const [inputName, inputValue] of Object.entries(values)) {
|
|
123
142
|
const chain = plainChainOf(inputValue);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// guessing at a root this does not hold would compare the wrong schema.
|
|
128
|
-
if (!chain?.startsWith("steps.")) continue;
|
|
129
|
-
const produced = navigateSchemaToExprPath(stepContext, chain.slice("steps.".length));
|
|
143
|
+
const root = chain ? roots.find(([prefix]) => chain.startsWith(prefix)) : undefined;
|
|
144
|
+
if (!chain || !root) continue;
|
|
145
|
+
const produced = navigateSchemaToExprPath(root[1], chain.slice(root[0].length));
|
|
130
146
|
const slotSchema = (contract.schema.properties as Record<string, any> | undefined)?.[
|
|
131
147
|
inputName
|
|
132
148
|
];
|
|
@@ -138,6 +154,28 @@ export function collectStepInputIssues(
|
|
|
138
154
|
// broad new Error-severity check hidden behind an argument-specific
|
|
139
155
|
// name. Both sides must declare a value type for the question to be
|
|
140
156
|
// about arguments at all.
|
|
157
|
+
// A LIVE value is consumed by reading, so it exists exactly once —
|
|
158
|
+
// that is what `live` says in the vocabulary, and re-attempting a
|
|
159
|
+
// dispatch that already read it re-sends nothing. Reported here rather
|
|
160
|
+
// than through a slot-specific annotation because both facts are
|
|
161
|
+
// already declared: the value's liveness by its value type, and the
|
|
162
|
+
// re-attempt by the retry policy. No kind is named.
|
|
163
|
+
if (isLiveSlot(produced)) {
|
|
164
|
+
const retry = declaredRetry(step, stepItemSchema, invokedManifest, invokedDef);
|
|
165
|
+
if (retry !== undefined) {
|
|
166
|
+
out.push({
|
|
167
|
+
path: `${stepPath}.${inputsField}.${inputName}`,
|
|
168
|
+
targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
|
|
169
|
+
message:
|
|
170
|
+
`'${inputName}' is a live value, which is consumed by reading and so exists ` +
|
|
171
|
+
`once — but ${retry} re-attempts the dispatch, and a re-attempt would pass ` +
|
|
172
|
+
`nothing. Collect it to a value first, or chunk the work so each attempt ` +
|
|
173
|
+
`carries its own replayable piece.`,
|
|
174
|
+
code: "LIVE_VALUE_RETRIED",
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
141
179
|
if (!valueTypeOf(produced) || !valueTypeOf(slotSchema)) continue;
|
|
142
180
|
const { compatible, issues } = checkSchemaCompatibility(produced, slotSchema, (ref) =>
|
|
143
181
|
defs.schemaForId(ref),
|
|
@@ -170,3 +208,65 @@ export function collectStepInputIssues(
|
|
|
170
208
|
return out;
|
|
171
209
|
}
|
|
172
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Where a re-attempt is declared for this dispatch, described for a diagnostic,
|
|
213
|
+
* or undefined when none is.
|
|
214
|
+
*
|
|
215
|
+
* A field declares one when its schema was expanded from a shared retry fragment
|
|
216
|
+
* — the shape the author pointed at, rather than a marker they had to remember to
|
|
217
|
+
* write beside it. Which fragment also says WHERE the budget is, so the two
|
|
218
|
+
* spellings a kind may carry (a policy object, or the deprecated bare count) need
|
|
219
|
+
* no guessing between them and no rule about which one wins.
|
|
220
|
+
*
|
|
221
|
+
* Two sites are consulted because there are two real ones: the STEP's own policy
|
|
222
|
+
* — `retry` on the kernel-owned dispatch site — and the TARGET's, a field on an
|
|
223
|
+
* arbitrary kind, because `Http.Request` re-attempts inside its own `invoke()`
|
|
224
|
+
* where only it can tell a 429 from a 500. A live value is equally doomed by
|
|
225
|
+
* either. EVERY retry-bearing field at a site is checked, not the first, since
|
|
226
|
+
* `Http.Request` carries both spellings and property order must not decide which
|
|
227
|
+
* is seen.
|
|
228
|
+
*
|
|
229
|
+
* Only a STATICALLY KNOWN non-zero budget counts. An `attempts` written as CEL
|
|
230
|
+
* says nothing here, and guessing would report a conflict against a manifest that
|
|
231
|
+
* may never retry — the same posture the `use` case-map selector takes.
|
|
232
|
+
*/
|
|
233
|
+
function declaredRetry(
|
|
234
|
+
step: Record<string, any>,
|
|
235
|
+
stepItemSchema: Record<string, any> | undefined,
|
|
236
|
+
invokedManifest: Record<string, any> | undefined,
|
|
237
|
+
invokedDef: Record<string, any> | undefined,
|
|
238
|
+
): string | undefined {
|
|
239
|
+
for (const [field, budget] of retryFields(stepItemSchema)) {
|
|
240
|
+
if (budget(step?.[field]) > 0) return `the step's \`${field}\``;
|
|
241
|
+
}
|
|
242
|
+
for (const [field, budget] of retryFields(invokedDef?.schema as Record<string, any>)) {
|
|
243
|
+
if (budget(invokedManifest?.[field]) > 0) return `the target's \`${field}\``;
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** How each shared retry fragment carries its budget. Keyed on fragment name —
|
|
249
|
+
* the analyzer's own built-ins, never a module's kind — so a kind that adopts a
|
|
250
|
+
* shape is covered without naming it here. */
|
|
251
|
+
const RETRY_BUDGET: Record<string, (value: unknown) => number> = {
|
|
252
|
+
RetryPolicy: (value) => {
|
|
253
|
+
if (!value || typeof value !== "object") return 0;
|
|
254
|
+
const attempts = (value as Record<string, unknown>).attempts;
|
|
255
|
+
return typeof attempts === "number" ? attempts : 0;
|
|
256
|
+
},
|
|
257
|
+
RetryAttempts: (value) => (typeof value === "number" ? value : 0),
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/** Every property of `schema` whose shape came from a retry fragment, paired with
|
|
261
|
+
* the reader for that fragment's budget. */
|
|
262
|
+
function retryFields(
|
|
263
|
+
schema: Record<string, any> | undefined,
|
|
264
|
+
): Array<[string, (value: unknown) => number]> {
|
|
265
|
+
if (!schema) return [];
|
|
266
|
+
const out: Array<[string, (value: unknown) => number]> = [];
|
|
267
|
+
for (const [key, sub] of gatherPropertySchemas(schema)) {
|
|
268
|
+
const budget = RETRY_BUDGET[manifestFragmentOf(sub) ?? ""];
|
|
269
|
+
if (budget) out.push([key, budget]);
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|