@telorun/analyzer 0.62.0 → 0.63.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.map +1 -1
- package/dist/analysis-registry.js +6 -0
- package/dist/analyzer.d.ts +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +343 -13
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +2 -7
- 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 +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/invocation-contract.d.ts +6 -1
- package/dist/invocation-contract.d.ts.map +1 -1
- package/dist/invocation-contract.js +27 -4
- package/dist/loaded-types.d.ts +12 -3
- package/dist/loaded-types.d.ts.map +1 -1
- 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-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/payload-digest.d.ts +7 -3
- package/dist/release/payload-digest.d.ts.map +1 -1
- package/dist/release/payload-digest.js +7 -3
- 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/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 +2 -1
- package/dist/validate-step-inputs.d.ts.map +1 -1
- package/dist/validate-step-inputs.js +5 -3
- 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 +6 -0
- package/src/analyzer.ts +395 -14
- package/src/call-graph.ts +2 -8
- package/src/flatten-for-analyzer.ts +1 -1
- package/src/import-resolution-diagnostics.ts +25 -7
- package/src/index.ts +73 -0
- package/src/invocation-contract.ts +45 -2
- package/src/loaded-types.ts +12 -3
- package/src/manifest-loader.ts +87 -55
- 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/payload-digest.ts +7 -3
- 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/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 +5 -3
- 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
package/src/manifest-schemas.ts
CHANGED
|
@@ -246,6 +246,39 @@ export const InvokeStepSchema = {
|
|
|
246
246
|
retry: {
|
|
247
247
|
title: "Retry",
|
|
248
248
|
$ref: `${MANIFEST_SCHEMA_URI}#/$defs/RetryPolicy`,
|
|
249
|
+
// A sibling of the shared policy rather than a member of it, the way
|
|
250
|
+
// `Http.Request.retry` adds `honorRetryAfter` — fragment expansion merges
|
|
251
|
+
// siblings, so the policy stays closed and each consumer's surface stays
|
|
252
|
+
// honest. `nonRetryable` matches an ERROR CODE, which is what a step's
|
|
253
|
+
// failures carry; an HTTP retry classifies on a response STATUS and has
|
|
254
|
+
// `retryOn` for it, so putting this on the shared shape would advertise a
|
|
255
|
+
// field there that nothing could read.
|
|
256
|
+
properties: {
|
|
257
|
+
nonRetryable: {
|
|
258
|
+
title: "Non-retryable codes",
|
|
259
|
+
description:
|
|
260
|
+
"Error codes that end the loop immediately instead of consuming the budget. " +
|
|
261
|
+
"The built-in exclusions are the ones decidable without judgement — a " +
|
|
262
|
+
"cancellation, and the kernel's verdicts on the shape of the call; whether a " +
|
|
263
|
+
"DOMAIN failure is worth re-attempting only the author knows, and without " +
|
|
264
|
+
"this every terminal one is retried to exhaustion — for a non-idempotent " +
|
|
265
|
+
"target, N extra attempts at a side effect.",
|
|
266
|
+
type: "array",
|
|
267
|
+
items: { type: "string" },
|
|
268
|
+
default: [],
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
timeout: {
|
|
273
|
+
title: "Timeout",
|
|
274
|
+
description:
|
|
275
|
+
"How long ONE attempt may take, in milliseconds; on elapse the dispatch is " +
|
|
276
|
+
"cancelled and the step fails ERR_STEP_TIMEOUT. Per attempt rather than for " +
|
|
277
|
+
"the whole retry loop, so what a single call is allowed to take does not " +
|
|
278
|
+
"depend on how slow the earlier attempts were. It belongs to the step rather " +
|
|
279
|
+
"than the target because the target does not know who is waiting.",
|
|
280
|
+
type: "integer",
|
|
281
|
+
minimum: 1,
|
|
249
282
|
},
|
|
250
283
|
},
|
|
251
284
|
};
|
|
@@ -306,12 +339,241 @@ export const KindSchemaSchema = {
|
|
|
306
339
|
additionalProperties: true,
|
|
307
340
|
};
|
|
308
341
|
|
|
342
|
+
|
|
343
|
+
/** The `error` variable a `catch:` / `finally:` branch binds. One object, used
|
|
344
|
+
* twice with different nullability — `finally` runs on the success path too. */
|
|
345
|
+
const stepErrorProperties = {
|
|
346
|
+
code: {
|
|
347
|
+
description: "Structured error code; INTERNAL_ERROR for plain errors.",
|
|
348
|
+
type: "string",
|
|
349
|
+
},
|
|
350
|
+
message: { description: "Human-readable error message.", type: "string" },
|
|
351
|
+
step: { description: "Name of the step that threw.", type: "string" },
|
|
352
|
+
data: { description: "Structured payload from an InvokeError, else undefined." },
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/** A step list's own items, written as the self-reference the localizer leaves
|
|
356
|
+
* behind. A recursive fragment states its self-references ALREADY localized:
|
|
357
|
+
* `hoistFragmentDef` copies the body verbatim into the consuming schema's
|
|
358
|
+
* `$defs`, so a foreign `telo://manifest#/…` written here would survive into a
|
|
359
|
+
* document the editor's local-only resolver throws on. */
|
|
360
|
+
const stepList = (title: string, description: string) => ({
|
|
361
|
+
title,
|
|
362
|
+
description,
|
|
363
|
+
"x-telo-topology-role": "branch",
|
|
364
|
+
type: "array",
|
|
365
|
+
items: { $ref: `#/$defs/${hoistedDefKey("Step")}` },
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* One step of a step body: a dispatch, a computed value, or a control-flow block.
|
|
370
|
+
*
|
|
371
|
+
* THE GRAMMAR, owned here rather than by `modules/run`. It was declared four
|
|
372
|
+
* times in one file — `$defs` are local to the schema that declares them, so
|
|
373
|
+
* four kinds in one module could not share one, let alone four kinds in four
|
|
374
|
+
* modules — and that is why a kind wanting a body had to take a `!ref` to an
|
|
375
|
+
* executable instead. A fragment is what makes `Sql.Transaction` or a durable
|
|
376
|
+
* `Workflow` able to carry one directly.
|
|
377
|
+
*
|
|
378
|
+
* RECURSIVE (a branch holds steps), so a reference to it is localized and
|
|
379
|
+
* hoisted rather than expanded in place — see {@link localizeRecursiveFragment}.
|
|
380
|
+
*
|
|
381
|
+
* `while/do` is admitted in EVERY body. Three of the four copies dropped it
|
|
382
|
+
* ("the kind is itself the loop"), which is an editorial nudge rather than a
|
|
383
|
+
* soundness rule — a nested `while` inside a for-each body is ordinary control
|
|
384
|
+
* flow the same engine already runs — and a fragment cannot be narrowed by its
|
|
385
|
+
* consumer anyway: draft-07 makes `$ref` exclusive, so a sibling restating a
|
|
386
|
+
* subset would be silently ignored by AJV while still reaching completion. One
|
|
387
|
+
* grammar, or two fragments and the duplication back where it started.
|
|
388
|
+
*/
|
|
389
|
+
export const StepSchema = {
|
|
390
|
+
title: "Step",
|
|
391
|
+
description: "Single executable step or control-flow block.",
|
|
392
|
+
type: "object",
|
|
393
|
+
properties: {
|
|
394
|
+
name: { title: "Name", description: "Unique step name.", type: "string" },
|
|
395
|
+
},
|
|
396
|
+
oneOf: [
|
|
397
|
+
// THE dispatch site, shared with an Application's `targets:` rather than
|
|
398
|
+
// restated. `name` is hoisted to the enclosing step object above, which is
|
|
399
|
+
// why this branch carries only what a dispatch itself needs.
|
|
400
|
+
{
|
|
401
|
+
title: "invoke",
|
|
402
|
+
description: "Invokes an invocable or runnable resource.",
|
|
403
|
+
$ref: `${MANIFEST_SCHEMA_URI}#/$defs/InvokeStep`,
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
title: "if/then/else",
|
|
407
|
+
description: "Conditional branch; executes then or else based on a boolean expression.",
|
|
408
|
+
properties: {
|
|
409
|
+
if: {
|
|
410
|
+
title: "If",
|
|
411
|
+
description: "CEL boolean expression; true executes then, false executes else.",
|
|
412
|
+
"x-telo-topology-role": "predicate",
|
|
413
|
+
type: "boolean",
|
|
414
|
+
},
|
|
415
|
+
elseif: {
|
|
416
|
+
title: "Else If",
|
|
417
|
+
description: "Additional condition-branch pairs evaluated when if is false.",
|
|
418
|
+
"x-telo-topology-role": "branch-list",
|
|
419
|
+
type: "array",
|
|
420
|
+
items: {
|
|
421
|
+
type: "object",
|
|
422
|
+
properties: {
|
|
423
|
+
if: {
|
|
424
|
+
title: "If",
|
|
425
|
+
description: "CEL boolean expression for this else-if branch.",
|
|
426
|
+
"x-telo-topology-role": "predicate",
|
|
427
|
+
type: "boolean",
|
|
428
|
+
},
|
|
429
|
+
then: stepList("Then", "Steps executed when this else-if condition is true."),
|
|
430
|
+
},
|
|
431
|
+
required: ["if", "then"],
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
then: stepList("Then", "Steps executed when if evaluates true."),
|
|
435
|
+
else: stepList(
|
|
436
|
+
"Else",
|
|
437
|
+
"Steps executed when if (and all elseif conditions) evaluate false.",
|
|
438
|
+
),
|
|
439
|
+
},
|
|
440
|
+
required: ["if", "then"],
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
title: "while/do",
|
|
444
|
+
description: "Loop; executes do steps repeatedly while condition is true.",
|
|
445
|
+
properties: {
|
|
446
|
+
while: {
|
|
447
|
+
title: "While",
|
|
448
|
+
description: "CEL boolean expression; evaluated before each iteration.",
|
|
449
|
+
"x-telo-topology-role": "predicate",
|
|
450
|
+
type: "boolean",
|
|
451
|
+
},
|
|
452
|
+
do: stepList("Do", "Steps executed on each iteration."),
|
|
453
|
+
},
|
|
454
|
+
required: ["while", "do"],
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
title: "switch/cases/default",
|
|
458
|
+
description: "Multi-branch dispatch; matches a value expression against case keys.",
|
|
459
|
+
properties: {
|
|
460
|
+
switch: {
|
|
461
|
+
title: "Switch",
|
|
462
|
+
description: "CEL expression; result matched against case keys.",
|
|
463
|
+
"x-telo-topology-role": "discriminator",
|
|
464
|
+
type: "string",
|
|
465
|
+
},
|
|
466
|
+
cases: {
|
|
467
|
+
title: "Cases",
|
|
468
|
+
description: "Map of value to step list; executed when switch matches the key.",
|
|
469
|
+
"x-telo-topology-role": "case-map",
|
|
470
|
+
type: "object",
|
|
471
|
+
additionalProperties: {
|
|
472
|
+
type: "array",
|
|
473
|
+
items: { $ref: `#/$defs/${hoistedDefKey("Step")}` },
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
default: stepList("Default", "Steps executed when no case matches."),
|
|
477
|
+
},
|
|
478
|
+
required: ["switch", "cases"],
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
title: "try/catch/finally",
|
|
482
|
+
description:
|
|
483
|
+
"Error boundary; executes try steps and handles failure via catch and finally.",
|
|
484
|
+
properties: {
|
|
485
|
+
try: stepList("Try", "Steps executed; halts on first failure and jumps to catch."),
|
|
486
|
+
catch: {
|
|
487
|
+
...stepList("Catch", "Steps executed when try fails; receives error context."),
|
|
488
|
+
"x-telo-error-context": {
|
|
489
|
+
type: "object",
|
|
490
|
+
description: "The caught failure (always present inside catch).",
|
|
491
|
+
properties: stepErrorProperties,
|
|
492
|
+
additionalProperties: false,
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
finally: {
|
|
496
|
+
...stepList(
|
|
497
|
+
"Finally",
|
|
498
|
+
"Steps always executed after try/catch regardless of outcome.",
|
|
499
|
+
),
|
|
500
|
+
// error is null on the success path and the caught failure when a
|
|
501
|
+
// try/catch failure propagates. Typed nullable so the analyzer flags
|
|
502
|
+
// unguarded field access (CEL_NULLABLE_ACCESS).
|
|
503
|
+
"x-telo-error-context": {
|
|
504
|
+
type: ["object", "null"],
|
|
505
|
+
description: "The caught failure, or null when try (and catch) succeeded.",
|
|
506
|
+
properties: stepErrorProperties,
|
|
507
|
+
additionalProperties: false,
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
required: ["try"],
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
title: "throw",
|
|
515
|
+
description:
|
|
516
|
+
"Throws an InvokeError unconditionally. Works inside catch blocks via CEL " +
|
|
517
|
+
'(e.g. code set to `!cel "error.code"`).',
|
|
518
|
+
properties: {
|
|
519
|
+
throw: {
|
|
520
|
+
title: "Throw",
|
|
521
|
+
description: "InvokeError descriptor; code is required, message/data optional.",
|
|
522
|
+
type: "object",
|
|
523
|
+
properties: {
|
|
524
|
+
code: {
|
|
525
|
+
title: "Code",
|
|
526
|
+
description: "Structured error code; uppercase SNAKE_CASE by convention.",
|
|
527
|
+
type: "string",
|
|
528
|
+
},
|
|
529
|
+
message: {
|
|
530
|
+
title: "Message",
|
|
531
|
+
description: "Human-readable error message. Defaults to `code` when omitted.",
|
|
532
|
+
type: "string",
|
|
533
|
+
},
|
|
534
|
+
data: {
|
|
535
|
+
title: "Data",
|
|
536
|
+
description: "Optional structured payload attached to the error.",
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
required: ["code"],
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
required: ["throw"],
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
title: "value",
|
|
546
|
+
description:
|
|
547
|
+
"Computes a value and publishes it as `steps.<name>.result`, with no dispatch — " +
|
|
548
|
+
"no resource, no span, no topology node. For an intermediate derived from an " +
|
|
549
|
+
"earlier step: reshape a response, total some rows, build the next call's inputs.",
|
|
550
|
+
properties: {
|
|
551
|
+
value: {
|
|
552
|
+
title: "Value",
|
|
553
|
+
description:
|
|
554
|
+
"A CEL expression — or a structure (map / array) with `!cel` leaves — " +
|
|
555
|
+
"evaluated in the step scope: `inputs`, the results of the steps before it, " +
|
|
556
|
+
"and whatever the enclosing kind binds (`item` / `index` / `items`, " +
|
|
557
|
+
"`iteration` / `previous`).",
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
required: ["value"],
|
|
561
|
+
},
|
|
562
|
+
],
|
|
563
|
+
required: ["name"],
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
/** The fragments that CONTAIN THEMSELVES — a schema's properties hold schemas, a
|
|
567
|
+
* step's branches hold steps. A reference to one is localized and hoisted
|
|
568
|
+
* rather than expanded in place ({@link localizeRecursiveFragment}), because
|
|
569
|
+
* inlining cannot terminate. */
|
|
570
|
+
const RECURSIVE_FRAGMENTS = new Set(["JsonSchema7", "KindSchema", "Step"]);
|
|
571
|
+
|
|
309
572
|
/** The fragments that describe author-written JSON Schema, whichever vocabulary
|
|
310
|
-
* they admit.
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
* ideas split and this set stays the one about schemas. */
|
|
573
|
+
* they admit. Split from {@link RECURSIVE_FRAGMENTS} when `Step` landed: being
|
|
574
|
+
* recursive is a fact about how a reference is RESOLVED, being a schema is a
|
|
575
|
+
* fact about which vocabulary a slot admits, and `Step` is the first fragment
|
|
576
|
+
* that is one without the other. */
|
|
315
577
|
const SCHEMA_FRAGMENTS = new Set(["JsonSchema7", "KindSchema"]);
|
|
316
578
|
|
|
317
579
|
/** True when a slot's `x-telo-fragment` stamp says it holds author-written JSON
|
|
@@ -362,6 +624,7 @@ export const ManifestRootSchema = {
|
|
|
362
624
|
RetryPolicy: RetryPolicySchema,
|
|
363
625
|
RetryAttempts: RetryAttemptsSchema,
|
|
364
626
|
InvokeStep: InvokeStepSchema,
|
|
627
|
+
Step: StepSchema,
|
|
365
628
|
JsonSchema7: JsonSchema7Schema,
|
|
366
629
|
KindSchema: KindSchemaSchema,
|
|
367
630
|
},
|
|
@@ -369,16 +632,62 @@ export const ManifestRootSchema = {
|
|
|
369
632
|
|
|
370
633
|
deepFreeze(ManifestRootSchema);
|
|
371
634
|
|
|
635
|
+
/**
|
|
636
|
+
* One expanded template per fragment, cloned per consumer.
|
|
637
|
+
*
|
|
638
|
+
* Expansion is a full walk of the body and the result is identical every time —
|
|
639
|
+
* `Step` alone is a couple of hundred nodes and `modules/run` hoists it four
|
|
640
|
+
* times, on the kernel's boot path. The copy handed out is still fresh, because
|
|
641
|
+
* downstream passes (`resolveSchemaRefKinds`, migrations) rewrite schemas in
|
|
642
|
+
* place.
|
|
643
|
+
*/
|
|
644
|
+
const expandedFragments = new Map<string, Record<string, unknown>>();
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* A fresh, fully expanded copy of a fragment body.
|
|
648
|
+
*
|
|
649
|
+
* TARGET-INDEPENDENT, which is what makes caching it correct: the walk is given
|
|
650
|
+
* a scratch hoist target and REFUSED if anything lands in it. A localized copy
|
|
651
|
+
* belongs in the schema a validator compiles, never inside another `$defs`
|
|
652
|
+
* entry, so a body that needed one could not have a single cached form — the
|
|
653
|
+
* first consumer would receive the entry and every later one would silently get
|
|
654
|
+
* a pointer to nothing. Nothing exercises that today: a recursive fragment
|
|
655
|
+
* writes its own self-references already localized, and a cross-reference
|
|
656
|
+
* between two recursive fragments is hoisted at the top level by
|
|
657
|
+
* {@link hoistFragmentDef}'s `pending` loop. If one ever does, this throws at
|
|
658
|
+
* the write instead of resolving by accident.
|
|
659
|
+
*/
|
|
660
|
+
function expandedFragment(name: string): Record<string, unknown> {
|
|
661
|
+
let template = expandedFragments.get(name);
|
|
662
|
+
if (!template) {
|
|
663
|
+
const fragment = (ManifestRootSchema.$defs as Record<string, unknown>)[name];
|
|
664
|
+
template = structuredClone(fragment) as Record<string, unknown>;
|
|
665
|
+
const scratch: Record<string, unknown> = {};
|
|
666
|
+
walkFragments(template, new Set(), { hoistTarget: scratch, depth: 1 });
|
|
667
|
+
if (scratch.$defs !== undefined) {
|
|
668
|
+
throw new Error(
|
|
669
|
+
`Manifest fragment '${name}' references a recursive fragment from inside its body. ` +
|
|
670
|
+
`Its expanded form is no longer independent of where it is hoisted — localize the ` +
|
|
671
|
+
`reference in the fragment source (write '#/$defs/telo:<Name>' directly) or teach ` +
|
|
672
|
+
`expandedFragment to take the hoist target.`,
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
expandedFragments.set(name, template);
|
|
676
|
+
}
|
|
677
|
+
return structuredClone(template);
|
|
678
|
+
}
|
|
679
|
+
|
|
372
680
|
/** A private, expanded copy of a fragment, for a consumer that must EMBED one
|
|
373
681
|
* rather than `$ref` it — `builtins.ts` is not a manifest and never passes
|
|
374
682
|
* through the loader, so its dispatch site has to arrive already resolved and
|
|
375
683
|
* already stamped. Cloned for the reason {@link deepFreeze} explains. */
|
|
684
|
+
|
|
376
685
|
export function manifestFragment(name: string): Record<string, unknown> {
|
|
377
686
|
const fragment = (ManifestRootSchema.$defs as Record<string, unknown>)[name];
|
|
378
687
|
if (!fragment || typeof fragment !== "object") {
|
|
379
688
|
throw new Error(`Unknown manifest fragment '${name}'`);
|
|
380
689
|
}
|
|
381
|
-
if (
|
|
690
|
+
if (RECURSIVE_FRAGMENTS.has(name)) {
|
|
382
691
|
// A recursive fragment has no expanded form — that is the whole reason it is
|
|
383
692
|
// localized instead. Embedding one would hand the consumer a body whose
|
|
384
693
|
// `#/$defs` pointers resolve against nothing.
|
|
@@ -388,8 +697,7 @@ export function manifestFragment(name: string): Record<string, unknown> {
|
|
|
388
697
|
`enclosing schema through withSchemaFragments().`,
|
|
389
698
|
);
|
|
390
699
|
}
|
|
391
|
-
const copy =
|
|
392
|
-
expandManifestFragments(copy);
|
|
700
|
+
const copy = expandedFragment(name);
|
|
393
701
|
copy[X_TELO_FRAGMENT] = name;
|
|
394
702
|
return copy;
|
|
395
703
|
}
|
|
@@ -500,7 +808,7 @@ function localizeRecursiveFragment(
|
|
|
500
808
|
const ref = value.$ref;
|
|
501
809
|
if (typeof ref !== "string" || !ref.startsWith(FRAGMENT_PREFIX)) return undefined;
|
|
502
810
|
const name = ref.slice(FRAGMENT_PREFIX.length);
|
|
503
|
-
if (!
|
|
811
|
+
if (!RECURSIVE_FRAGMENTS.has(name)) return undefined;
|
|
504
812
|
|
|
505
813
|
hoistFragmentDef(ctx.hoistTarget, name);
|
|
506
814
|
return { ...value, $ref: `#/$defs/${hoistedDefKey(name)}`, [X_TELO_FRAGMENT]: name };
|
|
@@ -522,8 +830,15 @@ function hoistFragmentDef(target: Record<string, unknown>, name: string): void {
|
|
|
522
830
|
// fragment reached through its own self-reference, with no slot in sight.
|
|
523
831
|
// That node is exactly where an annotation like `x-telo-eval` is written,
|
|
524
832
|
// so it is where the stamp has to be readable.
|
|
525
|
-
|
|
526
|
-
|
|
833
|
+
// A hoisted body may point at a NON-recursive fragment (`Step`'s dispatch
|
|
834
|
+
// branch is `InvokeStep`), and a copy left unexpanded carries a foreign
|
|
835
|
+
// `telo://manifest#/…` reference into a document the editor's resolver
|
|
836
|
+
// throws on — the same failure gating fragment expansion once caused. Where
|
|
837
|
+
// a nested LOCALIZED copy would belong is not this body; see
|
|
838
|
+
// {@link expandedFragment}, which refuses that case rather than guessing.
|
|
839
|
+
const body = expandedFragment(next);
|
|
840
|
+
defs[key] = { ...body, [X_TELO_FRAGMENT]: next };
|
|
841
|
+
for (const nested of RECURSIVE_FRAGMENTS) {
|
|
527
842
|
if (nested !== next && referencesLocalDef(defs[key], nested)) pending.push(nested);
|
|
528
843
|
}
|
|
529
844
|
}
|
|
@@ -589,10 +904,9 @@ function fragmentFor(value: unknown): Record<string, unknown> | undefined {
|
|
|
589
904
|
const fragment = (ManifestRootSchema.$defs as Record<string, unknown>)[name];
|
|
590
905
|
if (!fragment || typeof fragment !== "object") return undefined;
|
|
591
906
|
|
|
592
|
-
const expanded = structuredClone(fragment) as Record<string, unknown>;
|
|
593
907
|
// A fragment may reference another (InvokeStep holds a RetryPolicy); the copy
|
|
594
|
-
//
|
|
595
|
-
|
|
908
|
+
// arrives expanded, so one pass leaves no reference behind.
|
|
909
|
+
const expanded = expandedFragment(name);
|
|
596
910
|
|
|
597
911
|
for (const [key, own] of Object.entries(node)) {
|
|
598
912
|
if (key === "$ref") continue;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Can this runtime host that module version? One question, one answer, one
|
|
3
|
+
* implementation — the CLI's `upgrade` and every IDE's upgrade affordance all
|
|
4
|
+
* ask it of a candidate version's `telo.yaml` text.
|
|
5
|
+
*
|
|
6
|
+
* It lives beside {@link readRequires} because the `requires:` grammar has
|
|
7
|
+
* exactly one reader by rule, and the moment a second host learned to filter
|
|
8
|
+
* candidates by compatibility that rule needed a shared verdict, not a second
|
|
9
|
+
* parser. Browser-safe: text in, verdict out, no transport and no filesystem —
|
|
10
|
+
* fetching the candidate manifest is the caller's job, since only the caller
|
|
11
|
+
* knows which transport (or cache) addresses it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { defaultCustomTags } from "@telorun/templating";
|
|
15
|
+
import { parseAllDocuments } from "yaml";
|
|
16
|
+
import { isModuleKind } from "./module-kinds.js";
|
|
17
|
+
import { evaluateRequires, readRequires, type HostVersions } from "./requires-block.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* How a candidate version answered the compatibility question.
|
|
21
|
+
*
|
|
22
|
+
* `unknown` — the manifest could not be read or names no module document — is
|
|
23
|
+
* never treated as incompatible, since an unreachable registry must not
|
|
24
|
+
* silently freeze a consumer's imports.
|
|
25
|
+
*
|
|
26
|
+
* The two rejecting answers are kept APART because they call for different
|
|
27
|
+
* actions and the user is told which one applies: `too-new` is fixed by
|
|
28
|
+
* upgrading telo, `unreadable` cannot be fixed by the consumer at all.
|
|
29
|
+
* Collapsing them into one "no" and then printing "requires a newer telo" would
|
|
30
|
+
* assert a cause the check never established, and point at a runtime upgrade
|
|
31
|
+
* that will not help.
|
|
32
|
+
*/
|
|
33
|
+
export type ModuleCompatibility = "yes" | "too-new" | "unreadable" | "unknown";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read a module manifest's declared `requires:` and decide whether the runtime
|
|
37
|
+
* described by `teloVersion` / `host` can host it.
|
|
38
|
+
*
|
|
39
|
+
* A module that declares nothing is compatible — the bootstrap rule, permanent
|
|
40
|
+
* for everything published before the mechanism existed. A host that reports no
|
|
41
|
+
* version for an axis skips it rather than guessing, which is exactly the
|
|
42
|
+
* editor case: an IDE is not the machine that will run the manifest, so it
|
|
43
|
+
* speaks for the telo surface and for nothing else.
|
|
44
|
+
*/
|
|
45
|
+
export function manifestCompatibility(
|
|
46
|
+
manifestText: string,
|
|
47
|
+
teloVersion: string | undefined,
|
|
48
|
+
host: HostVersions = {},
|
|
49
|
+
): ModuleCompatibility {
|
|
50
|
+
let doc: Record<string, unknown> | undefined;
|
|
51
|
+
try {
|
|
52
|
+
for (const parsed of parseAllDocuments(manifestText, { customTags: defaultCustomTags() })) {
|
|
53
|
+
const value = parsed.toJS() as Record<string, unknown> | null;
|
|
54
|
+
const kind = value?.kind;
|
|
55
|
+
if (typeof kind === "string" && isModuleKind(kind)) {
|
|
56
|
+
doc = value as Record<string, unknown>;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
return "unknown";
|
|
62
|
+
}
|
|
63
|
+
if (!doc) return "unknown";
|
|
64
|
+
|
|
65
|
+
const { block, issues } = readRequires(doc);
|
|
66
|
+
// A malformed declaration is not a licence to install: the module claims a
|
|
67
|
+
// requirement it failed to state, and guessing which way it pointed is how a
|
|
68
|
+
// consumer ends up on a version that cannot load. The load gate warns about
|
|
69
|
+
// this same manifest, so the two halves agree.
|
|
70
|
+
if (issues.some((i) => !i.unknownAxis)) return "unreadable";
|
|
71
|
+
return evaluateRequires(block, teloVersion, host).satisfied ? "yes" : "too-new";
|
|
72
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a `self.<refSlot>` READS as.
|
|
3
|
+
*
|
|
4
|
+
* A ref slot holds the live instance after Phase-5 injection, and the kernel
|
|
5
|
+
* substitutes each one with its last published reading before evaluating a
|
|
6
|
+
* template body (`celSelfView`) — so `self.table.name` and `resources.users.name`
|
|
7
|
+
* name the same fact. This is the static half of that equivalence: without it a
|
|
8
|
+
* ref slot's schema is whatever the kind wrote at the slot (a title, a
|
|
9
|
+
* description and the annotation), which types nothing at all.
|
|
10
|
+
*
|
|
11
|
+
* A published reading has two halves and they are typed differently, exactly as
|
|
12
|
+
* `resources.<name>` is:
|
|
13
|
+
*
|
|
14
|
+
* - the FLAT half is what `snapshot()` returned, which no manifest declares, so
|
|
15
|
+
* it stays open. Closing it would reject reads that are correct today, and
|
|
16
|
+
* there is nothing to close it against.
|
|
17
|
+
* - `status` is DECLARED (`status:` on the kind, folded along `extends`), so it
|
|
18
|
+
* is typed and a typo below it is `CEL_UNKNOWN_FIELD`.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately no attempt to type the flat half from the kind's own `schema:`:
|
|
21
|
+
* a snapshot is what the controller chose to publish, not the config it was
|
|
22
|
+
* given, and the two coincide only by convention. Typing one as the other would
|
|
23
|
+
* invent errors on a kind that publishes anything else.
|
|
24
|
+
*
|
|
25
|
+
* Browser-safe: no Node built-ins.
|
|
26
|
+
*/
|
|
27
|
+
import { effectiveStatusSchema } from "./extends-resolution.js";
|
|
28
|
+
import { isRefSlot, readRefSlot } from "./ref-slot.js";
|
|
29
|
+
|
|
30
|
+
export interface RefReadingScope {
|
|
31
|
+
/** The definition a canonical kind names. */
|
|
32
|
+
resolve(kind: string): Record<string, any> | undefined;
|
|
33
|
+
/** Canonicalizes an alias-qualified kind, for a slot whose constraint has not
|
|
34
|
+
* been rewritten yet. */
|
|
35
|
+
resolveKind?(kind: string): string | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isObject(value: unknown): value is Record<string, any> {
|
|
39
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The reading schema for one ref slot, or undefined when the slot's constraint
|
|
44
|
+
* resolves to nothing — in which case the slot is left exactly as it was, the
|
|
45
|
+
* degrade-to-today direction every unresolved annotation takes here.
|
|
46
|
+
*
|
|
47
|
+
* A slot constrained to SEVERAL kinds types `status` from the kinds that agree
|
|
48
|
+
* and drops the rest: a read valid against one permitted target must not be
|
|
49
|
+
* rejected because a sibling kind does not declare that field.
|
|
50
|
+
*/
|
|
51
|
+
export function refSlotReadingSchema(
|
|
52
|
+
slot: Record<string, any>,
|
|
53
|
+
scope: RefReadingScope,
|
|
54
|
+
): Record<string, any> | undefined {
|
|
55
|
+
const ref = readRefSlot(slot);
|
|
56
|
+
if (!ref || ref.kinds.length === 0) return undefined;
|
|
57
|
+
|
|
58
|
+
const statuses: Record<string, any>[] = [];
|
|
59
|
+
for (const kind of ref.kinds) {
|
|
60
|
+
const canonical = scope.resolveKind?.(kind) ?? kind;
|
|
61
|
+
const definition = scope.resolve(canonical) ?? scope.resolve(kind);
|
|
62
|
+
if (!definition) continue;
|
|
63
|
+
const status = effectiveStatusSchema(definition as any, (k) => scope.resolve(k) as any);
|
|
64
|
+
if (isObject(status) && isObject(status.properties)) statuses.push(status);
|
|
65
|
+
}
|
|
66
|
+
if (statuses.length === 0) return openReading(slot);
|
|
67
|
+
|
|
68
|
+
const shared = statuses.length === 1 ? statuses[0]!.properties : sharedProperties(statuses);
|
|
69
|
+
return {
|
|
70
|
+
...describedBy(slot),
|
|
71
|
+
type: "object",
|
|
72
|
+
additionalProperties: true,
|
|
73
|
+
properties: {
|
|
74
|
+
status: { type: "object", additionalProperties: false, properties: shared },
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Properties every candidate kind declares. A field only some of them report
|
|
80
|
+
* is not one a read can rely on, and rejecting it would be wrong for the kinds
|
|
81
|
+
* that do — so it degrades to the open half rather than to an error. */
|
|
82
|
+
function sharedProperties(statuses: readonly Record<string, any>[]): Record<string, any> {
|
|
83
|
+
const [first, ...rest] = statuses;
|
|
84
|
+
const out: Record<string, any> = {};
|
|
85
|
+
for (const [name, schema] of Object.entries(first!.properties as Record<string, any>)) {
|
|
86
|
+
if (rest.every((other) => name in (other.properties as Record<string, any>))) {
|
|
87
|
+
out[name] = schema;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A reading whose `status` cannot be typed: open, so nothing new is rejected. */
|
|
94
|
+
function openReading(slot: Record<string, any>): Record<string, any> {
|
|
95
|
+
return { ...describedBy(slot), type: "object", additionalProperties: true };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The slot's own prose, kept so hover and completion still say what it is. */
|
|
99
|
+
function describedBy(slot: Record<string, any>): Record<string, any> {
|
|
100
|
+
const out: Record<string, any> = {};
|
|
101
|
+
if (typeof slot.title === "string") out.title = slot.title;
|
|
102
|
+
if (typeof slot.description === "string") out.description = slot.description;
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Rewrite every ref slot in a kind's own `schema:` to what reading it yields, so
|
|
108
|
+
* the `self` CEL variable answers about published state rather than about the
|
|
109
|
+
* annotation node.
|
|
110
|
+
*
|
|
111
|
+
* A SCHEMA walk, so it recurses through `properties` / `items` /
|
|
112
|
+
* `additionalProperties` and nothing else — there is no inline-declaration
|
|
113
|
+
* boundary to stop at here, because a `kind` key inside a schema is a property
|
|
114
|
+
* NAMED kind, not a nested resource. Untouched subtrees keep their identity, so
|
|
115
|
+
* a schema declaring no ref slot is returned as it was.
|
|
116
|
+
*/
|
|
117
|
+
export function withRefSlotsAsReadings(
|
|
118
|
+
schema: unknown,
|
|
119
|
+
scope: RefReadingScope,
|
|
120
|
+
): unknown {
|
|
121
|
+
if (Array.isArray(schema)) return schema.map((item) => withRefSlotsAsReadings(item, scope));
|
|
122
|
+
if (!isObject(schema)) return schema;
|
|
123
|
+
|
|
124
|
+
if (isRefSlot(schema)) return refSlotReadingSchema(schema, scope) ?? schema;
|
|
125
|
+
|
|
126
|
+
let changed = false;
|
|
127
|
+
const out: Record<string, any> = { ...schema };
|
|
128
|
+
for (const key of ["properties", "items", "additionalProperties"]) {
|
|
129
|
+
const node = schema[key];
|
|
130
|
+
if (node === undefined) continue;
|
|
131
|
+
const next =
|
|
132
|
+
key === "properties"
|
|
133
|
+
? mapValues(node, (value) => withRefSlotsAsReadings(value, scope))
|
|
134
|
+
: withRefSlotsAsReadings(node, scope);
|
|
135
|
+
if (next !== node) {
|
|
136
|
+
out[key] = next;
|
|
137
|
+
changed = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return changed ? out : schema;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function mapValues(
|
|
144
|
+
node: unknown,
|
|
145
|
+
fn: (value: unknown) => unknown,
|
|
146
|
+
): unknown {
|
|
147
|
+
if (!isObject(node)) return node;
|
|
148
|
+
let changed = false;
|
|
149
|
+
const out: Record<string, any> = {};
|
|
150
|
+
for (const [key, value] of Object.entries(node)) {
|
|
151
|
+
const next = fn(value);
|
|
152
|
+
if (next !== value) changed = true;
|
|
153
|
+
out[key] = next;
|
|
154
|
+
}
|
|
155
|
+
return changed ? out : node;
|
|
156
|
+
}
|