@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-schemas.d.ts","sourceRoot":"","sources":["../src/manifest-schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAKH,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD,sEAAsE;AACtE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yEA8ByE;AACzE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoB7B,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwD7B,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB;;;;;;CAM/B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,gBAAgB
|
|
1
|
+
{"version":3,"file":"manifest-schemas.d.ts","sourceRoot":"","sources":["../src/manifest-schemas.ts"],"names":[],"mappings":"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"}
|
package/dist/manifest-schemas.js
CHANGED
|
@@ -235,6 +235,37 @@ export const InvokeStepSchema = {
|
|
|
235
235
|
retry: {
|
|
236
236
|
title: "Retry",
|
|
237
237
|
$ref: `${MANIFEST_SCHEMA_URI}#/$defs/RetryPolicy`,
|
|
238
|
+
// A sibling of the shared policy rather than a member of it, the way
|
|
239
|
+
// `Http.Request.retry` adds `honorRetryAfter` — fragment expansion merges
|
|
240
|
+
// siblings, so the policy stays closed and each consumer's surface stays
|
|
241
|
+
// honest. `nonRetryable` matches an ERROR CODE, which is what a step's
|
|
242
|
+
// failures carry; an HTTP retry classifies on a response STATUS and has
|
|
243
|
+
// `retryOn` for it, so putting this on the shared shape would advertise a
|
|
244
|
+
// field there that nothing could read.
|
|
245
|
+
properties: {
|
|
246
|
+
nonRetryable: {
|
|
247
|
+
title: "Non-retryable codes",
|
|
248
|
+
description: "Error codes that end the loop immediately instead of consuming the budget. " +
|
|
249
|
+
"The built-in exclusions are the ones decidable without judgement — a " +
|
|
250
|
+
"cancellation, and the kernel's verdicts on the shape of the call; whether a " +
|
|
251
|
+
"DOMAIN failure is worth re-attempting only the author knows, and without " +
|
|
252
|
+
"this every terminal one is retried to exhaustion — for a non-idempotent " +
|
|
253
|
+
"target, N extra attempts at a side effect.",
|
|
254
|
+
type: "array",
|
|
255
|
+
items: { type: "string" },
|
|
256
|
+
default: [],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
timeout: {
|
|
261
|
+
title: "Timeout",
|
|
262
|
+
description: "How long ONE attempt may take, in milliseconds; on elapse the dispatch is " +
|
|
263
|
+
"cancelled and the step fails ERR_STEP_TIMEOUT. Per attempt rather than for " +
|
|
264
|
+
"the whole retry loop, so what a single call is allowed to take does not " +
|
|
265
|
+
"depend on how slow the earlier attempts were. It belongs to the step rather " +
|
|
266
|
+
"than the target because the target does not know who is waiting.",
|
|
267
|
+
type: "integer",
|
|
268
|
+
minimum: 1,
|
|
238
269
|
},
|
|
239
270
|
},
|
|
240
271
|
};
|
|
@@ -291,12 +322,226 @@ export const KindSchemaSchema = {
|
|
|
291
322
|
properties: jsonSchemaKeywords(hoistedDefKey("KindSchema")),
|
|
292
323
|
additionalProperties: true,
|
|
293
324
|
};
|
|
325
|
+
/** The `error` variable a `catch:` / `finally:` branch binds. One object, used
|
|
326
|
+
* twice with different nullability — `finally` runs on the success path too. */
|
|
327
|
+
const stepErrorProperties = {
|
|
328
|
+
code: {
|
|
329
|
+
description: "Structured error code; INTERNAL_ERROR for plain errors.",
|
|
330
|
+
type: "string",
|
|
331
|
+
},
|
|
332
|
+
message: { description: "Human-readable error message.", type: "string" },
|
|
333
|
+
step: { description: "Name of the step that threw.", type: "string" },
|
|
334
|
+
data: { description: "Structured payload from an InvokeError, else undefined." },
|
|
335
|
+
};
|
|
336
|
+
/** A step list's own items, written as the self-reference the localizer leaves
|
|
337
|
+
* behind. A recursive fragment states its self-references ALREADY localized:
|
|
338
|
+
* `hoistFragmentDef` copies the body verbatim into the consuming schema's
|
|
339
|
+
* `$defs`, so a foreign `telo://manifest#/…` written here would survive into a
|
|
340
|
+
* document the editor's local-only resolver throws on. */
|
|
341
|
+
const stepList = (title, description) => ({
|
|
342
|
+
title,
|
|
343
|
+
description,
|
|
344
|
+
"x-telo-topology-role": "branch",
|
|
345
|
+
type: "array",
|
|
346
|
+
items: { $ref: `#/$defs/${hoistedDefKey("Step")}` },
|
|
347
|
+
});
|
|
348
|
+
/**
|
|
349
|
+
* One step of a step body: a dispatch, a computed value, or a control-flow block.
|
|
350
|
+
*
|
|
351
|
+
* THE GRAMMAR, owned here rather than by `modules/run`. It was declared four
|
|
352
|
+
* times in one file — `$defs` are local to the schema that declares them, so
|
|
353
|
+
* four kinds in one module could not share one, let alone four kinds in four
|
|
354
|
+
* modules — and that is why a kind wanting a body had to take a `!ref` to an
|
|
355
|
+
* executable instead. A fragment is what makes `Sql.Transaction` or a durable
|
|
356
|
+
* `Workflow` able to carry one directly.
|
|
357
|
+
*
|
|
358
|
+
* RECURSIVE (a branch holds steps), so a reference to it is localized and
|
|
359
|
+
* hoisted rather than expanded in place — see {@link localizeRecursiveFragment}.
|
|
360
|
+
*
|
|
361
|
+
* `while/do` is admitted in EVERY body. Three of the four copies dropped it
|
|
362
|
+
* ("the kind is itself the loop"), which is an editorial nudge rather than a
|
|
363
|
+
* soundness rule — a nested `while` inside a for-each body is ordinary control
|
|
364
|
+
* flow the same engine already runs — and a fragment cannot be narrowed by its
|
|
365
|
+
* consumer anyway: draft-07 makes `$ref` exclusive, so a sibling restating a
|
|
366
|
+
* subset would be silently ignored by AJV while still reaching completion. One
|
|
367
|
+
* grammar, or two fragments and the duplication back where it started.
|
|
368
|
+
*/
|
|
369
|
+
export const StepSchema = {
|
|
370
|
+
title: "Step",
|
|
371
|
+
description: "Single executable step or control-flow block.",
|
|
372
|
+
type: "object",
|
|
373
|
+
properties: {
|
|
374
|
+
name: { title: "Name", description: "Unique step name.", type: "string" },
|
|
375
|
+
},
|
|
376
|
+
oneOf: [
|
|
377
|
+
// THE dispatch site, shared with an Application's `targets:` rather than
|
|
378
|
+
// restated. `name` is hoisted to the enclosing step object above, which is
|
|
379
|
+
// why this branch carries only what a dispatch itself needs.
|
|
380
|
+
{
|
|
381
|
+
title: "invoke",
|
|
382
|
+
description: "Invokes an invocable or runnable resource.",
|
|
383
|
+
$ref: `${MANIFEST_SCHEMA_URI}#/$defs/InvokeStep`,
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
title: "if/then/else",
|
|
387
|
+
description: "Conditional branch; executes then or else based on a boolean expression.",
|
|
388
|
+
properties: {
|
|
389
|
+
if: {
|
|
390
|
+
title: "If",
|
|
391
|
+
description: "CEL boolean expression; true executes then, false executes else.",
|
|
392
|
+
"x-telo-topology-role": "predicate",
|
|
393
|
+
type: "boolean",
|
|
394
|
+
},
|
|
395
|
+
elseif: {
|
|
396
|
+
title: "Else If",
|
|
397
|
+
description: "Additional condition-branch pairs evaluated when if is false.",
|
|
398
|
+
"x-telo-topology-role": "branch-list",
|
|
399
|
+
type: "array",
|
|
400
|
+
items: {
|
|
401
|
+
type: "object",
|
|
402
|
+
properties: {
|
|
403
|
+
if: {
|
|
404
|
+
title: "If",
|
|
405
|
+
description: "CEL boolean expression for this else-if branch.",
|
|
406
|
+
"x-telo-topology-role": "predicate",
|
|
407
|
+
type: "boolean",
|
|
408
|
+
},
|
|
409
|
+
then: stepList("Then", "Steps executed when this else-if condition is true."),
|
|
410
|
+
},
|
|
411
|
+
required: ["if", "then"],
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
then: stepList("Then", "Steps executed when if evaluates true."),
|
|
415
|
+
else: stepList("Else", "Steps executed when if (and all elseif conditions) evaluate false."),
|
|
416
|
+
},
|
|
417
|
+
required: ["if", "then"],
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
title: "while/do",
|
|
421
|
+
description: "Loop; executes do steps repeatedly while condition is true.",
|
|
422
|
+
properties: {
|
|
423
|
+
while: {
|
|
424
|
+
title: "While",
|
|
425
|
+
description: "CEL boolean expression; evaluated before each iteration.",
|
|
426
|
+
"x-telo-topology-role": "predicate",
|
|
427
|
+
type: "boolean",
|
|
428
|
+
},
|
|
429
|
+
do: stepList("Do", "Steps executed on each iteration."),
|
|
430
|
+
},
|
|
431
|
+
required: ["while", "do"],
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
title: "switch/cases/default",
|
|
435
|
+
description: "Multi-branch dispatch; matches a value expression against case keys.",
|
|
436
|
+
properties: {
|
|
437
|
+
switch: {
|
|
438
|
+
title: "Switch",
|
|
439
|
+
description: "CEL expression; result matched against case keys.",
|
|
440
|
+
"x-telo-topology-role": "discriminator",
|
|
441
|
+
type: "string",
|
|
442
|
+
},
|
|
443
|
+
cases: {
|
|
444
|
+
title: "Cases",
|
|
445
|
+
description: "Map of value to step list; executed when switch matches the key.",
|
|
446
|
+
"x-telo-topology-role": "case-map",
|
|
447
|
+
type: "object",
|
|
448
|
+
additionalProperties: {
|
|
449
|
+
type: "array",
|
|
450
|
+
items: { $ref: `#/$defs/${hoistedDefKey("Step")}` },
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
default: stepList("Default", "Steps executed when no case matches."),
|
|
454
|
+
},
|
|
455
|
+
required: ["switch", "cases"],
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
title: "try/catch/finally",
|
|
459
|
+
description: "Error boundary; executes try steps and handles failure via catch and finally.",
|
|
460
|
+
properties: {
|
|
461
|
+
try: stepList("Try", "Steps executed; halts on first failure and jumps to catch."),
|
|
462
|
+
catch: {
|
|
463
|
+
...stepList("Catch", "Steps executed when try fails; receives error context."),
|
|
464
|
+
"x-telo-error-context": {
|
|
465
|
+
type: "object",
|
|
466
|
+
description: "The caught failure (always present inside catch).",
|
|
467
|
+
properties: stepErrorProperties,
|
|
468
|
+
additionalProperties: false,
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
finally: {
|
|
472
|
+
...stepList("Finally", "Steps always executed after try/catch regardless of outcome."),
|
|
473
|
+
// error is null on the success path and the caught failure when a
|
|
474
|
+
// try/catch failure propagates. Typed nullable so the analyzer flags
|
|
475
|
+
// unguarded field access (CEL_NULLABLE_ACCESS).
|
|
476
|
+
"x-telo-error-context": {
|
|
477
|
+
type: ["object", "null"],
|
|
478
|
+
description: "The caught failure, or null when try (and catch) succeeded.",
|
|
479
|
+
properties: stepErrorProperties,
|
|
480
|
+
additionalProperties: false,
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
required: ["try"],
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
title: "throw",
|
|
488
|
+
description: "Throws an InvokeError unconditionally. Works inside catch blocks via CEL " +
|
|
489
|
+
'(e.g. code set to `!cel "error.code"`).',
|
|
490
|
+
properties: {
|
|
491
|
+
throw: {
|
|
492
|
+
title: "Throw",
|
|
493
|
+
description: "InvokeError descriptor; code is required, message/data optional.",
|
|
494
|
+
type: "object",
|
|
495
|
+
properties: {
|
|
496
|
+
code: {
|
|
497
|
+
title: "Code",
|
|
498
|
+
description: "Structured error code; uppercase SNAKE_CASE by convention.",
|
|
499
|
+
type: "string",
|
|
500
|
+
},
|
|
501
|
+
message: {
|
|
502
|
+
title: "Message",
|
|
503
|
+
description: "Human-readable error message. Defaults to `code` when omitted.",
|
|
504
|
+
type: "string",
|
|
505
|
+
},
|
|
506
|
+
data: {
|
|
507
|
+
title: "Data",
|
|
508
|
+
description: "Optional structured payload attached to the error.",
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
required: ["code"],
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
required: ["throw"],
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
title: "value",
|
|
518
|
+
description: "Computes a value and publishes it as `steps.<name>.result`, with no dispatch — " +
|
|
519
|
+
"no resource, no span, no topology node. For an intermediate derived from an " +
|
|
520
|
+
"earlier step: reshape a response, total some rows, build the next call's inputs.",
|
|
521
|
+
properties: {
|
|
522
|
+
value: {
|
|
523
|
+
title: "Value",
|
|
524
|
+
description: "A CEL expression — or a structure (map / array) with `!cel` leaves — " +
|
|
525
|
+
"evaluated in the step scope: `inputs`, the results of the steps before it, " +
|
|
526
|
+
"and whatever the enclosing kind binds (`item` / `index` / `items`, " +
|
|
527
|
+
"`iteration` / `previous`).",
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
required: ["value"],
|
|
531
|
+
},
|
|
532
|
+
],
|
|
533
|
+
required: ["name"],
|
|
534
|
+
};
|
|
535
|
+
/** The fragments that CONTAIN THEMSELVES — a schema's properties hold schemas, a
|
|
536
|
+
* step's branches hold steps. A reference to one is localized and hoisted
|
|
537
|
+
* rather than expanded in place ({@link localizeRecursiveFragment}), because
|
|
538
|
+
* inlining cannot terminate. */
|
|
539
|
+
const RECURSIVE_FRAGMENTS = new Set(["JsonSchema7", "KindSchema", "Step"]);
|
|
294
540
|
/** The fragments that describe author-written JSON Schema, whichever vocabulary
|
|
295
|
-
* they admit.
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
* ideas split and this set stays the one about schemas. */
|
|
541
|
+
* they admit. Split from {@link RECURSIVE_FRAGMENTS} when `Step` landed: being
|
|
542
|
+
* recursive is a fact about how a reference is RESOLVED, being a schema is a
|
|
543
|
+
* fact about which vocabulary a slot admits, and `Step` is the first fragment
|
|
544
|
+
* that is one without the other. */
|
|
300
545
|
const SCHEMA_FRAGMENTS = new Set(["JsonSchema7", "KindSchema"]);
|
|
301
546
|
/** True when a slot's `x-telo-fragment` stamp says it holds author-written JSON
|
|
302
547
|
* Schema — a kind's `schema:`, a `status:` block, a `Telo.JsonSchema`'s own
|
|
@@ -344,11 +589,53 @@ export const ManifestRootSchema = {
|
|
|
344
589
|
RetryPolicy: RetryPolicySchema,
|
|
345
590
|
RetryAttempts: RetryAttemptsSchema,
|
|
346
591
|
InvokeStep: InvokeStepSchema,
|
|
592
|
+
Step: StepSchema,
|
|
347
593
|
JsonSchema7: JsonSchema7Schema,
|
|
348
594
|
KindSchema: KindSchemaSchema,
|
|
349
595
|
},
|
|
350
596
|
};
|
|
351
597
|
deepFreeze(ManifestRootSchema);
|
|
598
|
+
/**
|
|
599
|
+
* One expanded template per fragment, cloned per consumer.
|
|
600
|
+
*
|
|
601
|
+
* Expansion is a full walk of the body and the result is identical every time —
|
|
602
|
+
* `Step` alone is a couple of hundred nodes and `modules/run` hoists it four
|
|
603
|
+
* times, on the kernel's boot path. The copy handed out is still fresh, because
|
|
604
|
+
* downstream passes (`resolveSchemaRefKinds`, migrations) rewrite schemas in
|
|
605
|
+
* place.
|
|
606
|
+
*/
|
|
607
|
+
const expandedFragments = new Map();
|
|
608
|
+
/**
|
|
609
|
+
* A fresh, fully expanded copy of a fragment body.
|
|
610
|
+
*
|
|
611
|
+
* TARGET-INDEPENDENT, which is what makes caching it correct: the walk is given
|
|
612
|
+
* a scratch hoist target and REFUSED if anything lands in it. A localized copy
|
|
613
|
+
* belongs in the schema a validator compiles, never inside another `$defs`
|
|
614
|
+
* entry, so a body that needed one could not have a single cached form — the
|
|
615
|
+
* first consumer would receive the entry and every later one would silently get
|
|
616
|
+
* a pointer to nothing. Nothing exercises that today: a recursive fragment
|
|
617
|
+
* writes its own self-references already localized, and a cross-reference
|
|
618
|
+
* between two recursive fragments is hoisted at the top level by
|
|
619
|
+
* {@link hoistFragmentDef}'s `pending` loop. If one ever does, this throws at
|
|
620
|
+
* the write instead of resolving by accident.
|
|
621
|
+
*/
|
|
622
|
+
function expandedFragment(name) {
|
|
623
|
+
let template = expandedFragments.get(name);
|
|
624
|
+
if (!template) {
|
|
625
|
+
const fragment = ManifestRootSchema.$defs[name];
|
|
626
|
+
template = structuredClone(fragment);
|
|
627
|
+
const scratch = {};
|
|
628
|
+
walkFragments(template, new Set(), { hoistTarget: scratch, depth: 1 });
|
|
629
|
+
if (scratch.$defs !== undefined) {
|
|
630
|
+
throw new Error(`Manifest fragment '${name}' references a recursive fragment from inside its body. ` +
|
|
631
|
+
`Its expanded form is no longer independent of where it is hoisted — localize the ` +
|
|
632
|
+
`reference in the fragment source (write '#/$defs/telo:<Name>' directly) or teach ` +
|
|
633
|
+
`expandedFragment to take the hoist target.`);
|
|
634
|
+
}
|
|
635
|
+
expandedFragments.set(name, template);
|
|
636
|
+
}
|
|
637
|
+
return structuredClone(template);
|
|
638
|
+
}
|
|
352
639
|
/** A private, expanded copy of a fragment, for a consumer that must EMBED one
|
|
353
640
|
* rather than `$ref` it — `builtins.ts` is not a manifest and never passes
|
|
354
641
|
* through the loader, so its dispatch site has to arrive already resolved and
|
|
@@ -358,7 +645,7 @@ export function manifestFragment(name) {
|
|
|
358
645
|
if (!fragment || typeof fragment !== "object") {
|
|
359
646
|
throw new Error(`Unknown manifest fragment '${name}'`);
|
|
360
647
|
}
|
|
361
|
-
if (
|
|
648
|
+
if (RECURSIVE_FRAGMENTS.has(name)) {
|
|
362
649
|
// A recursive fragment has no expanded form — that is the whole reason it is
|
|
363
650
|
// localized instead. Embedding one would hand the consumer a body whose
|
|
364
651
|
// `#/$defs` pointers resolve against nothing.
|
|
@@ -366,8 +653,7 @@ export function manifestFragment(name) {
|
|
|
366
653
|
`point a slot at it with $ref: "${manifestFragmentRef(name)}" and pass the ` +
|
|
367
654
|
`enclosing schema through withSchemaFragments().`);
|
|
368
655
|
}
|
|
369
|
-
const copy =
|
|
370
|
-
expandManifestFragments(copy);
|
|
656
|
+
const copy = expandedFragment(name);
|
|
371
657
|
copy[X_TELO_FRAGMENT] = name;
|
|
372
658
|
return copy;
|
|
373
659
|
}
|
|
@@ -469,7 +755,7 @@ function localizeRecursiveFragment(value, ctx) {
|
|
|
469
755
|
if (typeof ref !== "string" || !ref.startsWith(FRAGMENT_PREFIX))
|
|
470
756
|
return undefined;
|
|
471
757
|
const name = ref.slice(FRAGMENT_PREFIX.length);
|
|
472
|
-
if (!
|
|
758
|
+
if (!RECURSIVE_FRAGMENTS.has(name))
|
|
473
759
|
return undefined;
|
|
474
760
|
hoistFragmentDef(ctx.hoistTarget, name);
|
|
475
761
|
return { ...value, $ref: `#/$defs/${hoistedDefKey(name)}`, [X_TELO_FRAGMENT]: name };
|
|
@@ -492,8 +778,15 @@ function hoistFragmentDef(target, name) {
|
|
|
492
778
|
// fragment reached through its own self-reference, with no slot in sight.
|
|
493
779
|
// That node is exactly where an annotation like `x-telo-eval` is written,
|
|
494
780
|
// so it is where the stamp has to be readable.
|
|
495
|
-
|
|
496
|
-
|
|
781
|
+
// A hoisted body may point at a NON-recursive fragment (`Step`'s dispatch
|
|
782
|
+
// branch is `InvokeStep`), and a copy left unexpanded carries a foreign
|
|
783
|
+
// `telo://manifest#/…` reference into a document the editor's resolver
|
|
784
|
+
// throws on — the same failure gating fragment expansion once caused. Where
|
|
785
|
+
// a nested LOCALIZED copy would belong is not this body; see
|
|
786
|
+
// {@link expandedFragment}, which refuses that case rather than guessing.
|
|
787
|
+
const body = expandedFragment(next);
|
|
788
|
+
defs[key] = { ...body, [X_TELO_FRAGMENT]: next };
|
|
789
|
+
for (const nested of RECURSIVE_FRAGMENTS) {
|
|
497
790
|
if (nested !== next && referencesLocalDef(defs[key], nested))
|
|
498
791
|
pending.push(nested);
|
|
499
792
|
}
|
|
@@ -562,10 +855,9 @@ function fragmentFor(value) {
|
|
|
562
855
|
const fragment = ManifestRootSchema.$defs[name];
|
|
563
856
|
if (!fragment || typeof fragment !== "object")
|
|
564
857
|
return undefined;
|
|
565
|
-
const expanded = structuredClone(fragment);
|
|
566
858
|
// A fragment may reference another (InvokeStep holds a RetryPolicy); the copy
|
|
567
|
-
//
|
|
568
|
-
|
|
859
|
+
// arrives expanded, so one pass leaves no reference behind.
|
|
860
|
+
const expanded = expandedFragment(name);
|
|
569
861
|
for (const [key, own] of Object.entries(node)) {
|
|
570
862
|
if (key === "$ref")
|
|
571
863
|
continue;
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
import { type HostVersions } from "./requires-block.js";
|
|
14
|
+
/**
|
|
15
|
+
* How a candidate version answered the compatibility question.
|
|
16
|
+
*
|
|
17
|
+
* `unknown` — the manifest could not be read or names no module document — is
|
|
18
|
+
* never treated as incompatible, since an unreachable registry must not
|
|
19
|
+
* silently freeze a consumer's imports.
|
|
20
|
+
*
|
|
21
|
+
* The two rejecting answers are kept APART because they call for different
|
|
22
|
+
* actions and the user is told which one applies: `too-new` is fixed by
|
|
23
|
+
* upgrading telo, `unreadable` cannot be fixed by the consumer at all.
|
|
24
|
+
* Collapsing them into one "no" and then printing "requires a newer telo" would
|
|
25
|
+
* assert a cause the check never established, and point at a runtime upgrade
|
|
26
|
+
* that will not help.
|
|
27
|
+
*/
|
|
28
|
+
export type ModuleCompatibility = "yes" | "too-new" | "unreadable" | "unknown";
|
|
29
|
+
/**
|
|
30
|
+
* Read a module manifest's declared `requires:` and decide whether the runtime
|
|
31
|
+
* described by `teloVersion` / `host` can host it.
|
|
32
|
+
*
|
|
33
|
+
* A module that declares nothing is compatible — the bootstrap rule, permanent
|
|
34
|
+
* for everything published before the mechanism existed. A host that reports no
|
|
35
|
+
* version for an axis skips it rather than guessing, which is exactly the
|
|
36
|
+
* editor case: an IDE is not the machine that will run the manifest, so it
|
|
37
|
+
* speaks for the telo surface and for nothing else.
|
|
38
|
+
*/
|
|
39
|
+
export declare function manifestCompatibility(manifestText: string, teloVersion: string | undefined, host?: HostVersions): ModuleCompatibility;
|
|
40
|
+
//# sourceMappingURL=module-compatibility.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-compatibility.d.ts","sourceRoot":"","sources":["../src/module-compatibility.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAAkC,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExF;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,mBAAmB,GAAG,KAAK,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;AAE/E;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,IAAI,GAAE,YAAiB,GACtB,mBAAmB,CAuBrB"}
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
import { defaultCustomTags } from "@telorun/templating";
|
|
14
|
+
import { parseAllDocuments } from "yaml";
|
|
15
|
+
import { isModuleKind } from "./module-kinds.js";
|
|
16
|
+
import { evaluateRequires, readRequires } from "./requires-block.js";
|
|
17
|
+
/**
|
|
18
|
+
* Read a module manifest's declared `requires:` and decide whether the runtime
|
|
19
|
+
* described by `teloVersion` / `host` can host it.
|
|
20
|
+
*
|
|
21
|
+
* A module that declares nothing is compatible — the bootstrap rule, permanent
|
|
22
|
+
* for everything published before the mechanism existed. A host that reports no
|
|
23
|
+
* version for an axis skips it rather than guessing, which is exactly the
|
|
24
|
+
* editor case: an IDE is not the machine that will run the manifest, so it
|
|
25
|
+
* speaks for the telo surface and for nothing else.
|
|
26
|
+
*/
|
|
27
|
+
export function manifestCompatibility(manifestText, teloVersion, host = {}) {
|
|
28
|
+
let doc;
|
|
29
|
+
try {
|
|
30
|
+
for (const parsed of parseAllDocuments(manifestText, { customTags: defaultCustomTags() })) {
|
|
31
|
+
const value = parsed.toJS();
|
|
32
|
+
const kind = value?.kind;
|
|
33
|
+
if (typeof kind === "string" && isModuleKind(kind)) {
|
|
34
|
+
doc = value;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return "unknown";
|
|
41
|
+
}
|
|
42
|
+
if (!doc)
|
|
43
|
+
return "unknown";
|
|
44
|
+
const { block, issues } = readRequires(doc);
|
|
45
|
+
// A malformed declaration is not a licence to install: the module claims a
|
|
46
|
+
// requirement it failed to state, and guessing which way it pointed is how a
|
|
47
|
+
// consumer ends up on a version that cannot load. The load gate warns about
|
|
48
|
+
// this same manifest, so the two halves agree.
|
|
49
|
+
if (issues.some((i) => !i.unknownAxis))
|
|
50
|
+
return "unreadable";
|
|
51
|
+
return evaluateRequires(block, teloVersion, host).satisfied ? "yes" : "too-new";
|
|
52
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface RefReadingScope {
|
|
2
|
+
/** The definition a canonical kind names. */
|
|
3
|
+
resolve(kind: string): Record<string, any> | undefined;
|
|
4
|
+
/** Canonicalizes an alias-qualified kind, for a slot whose constraint has not
|
|
5
|
+
* been rewritten yet. */
|
|
6
|
+
resolveKind?(kind: string): string | undefined;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The reading schema for one ref slot, or undefined when the slot's constraint
|
|
10
|
+
* resolves to nothing — in which case the slot is left exactly as it was, the
|
|
11
|
+
* degrade-to-today direction every unresolved annotation takes here.
|
|
12
|
+
*
|
|
13
|
+
* A slot constrained to SEVERAL kinds types `status` from the kinds that agree
|
|
14
|
+
* and drops the rest: a read valid against one permitted target must not be
|
|
15
|
+
* rejected because a sibling kind does not declare that field.
|
|
16
|
+
*/
|
|
17
|
+
export declare function refSlotReadingSchema(slot: Record<string, any>, scope: RefReadingScope): Record<string, any> | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Rewrite every ref slot in a kind's own `schema:` to what reading it yields, so
|
|
20
|
+
* the `self` CEL variable answers about published state rather than about the
|
|
21
|
+
* annotation node.
|
|
22
|
+
*
|
|
23
|
+
* A SCHEMA walk, so it recurses through `properties` / `items` /
|
|
24
|
+
* `additionalProperties` and nothing else — there is no inline-declaration
|
|
25
|
+
* boundary to stop at here, because a `kind` key inside a schema is a property
|
|
26
|
+
* NAMED kind, not a nested resource. Untouched subtrees keep their identity, so
|
|
27
|
+
* a schema declaring no ref slot is returned as it was.
|
|
28
|
+
*/
|
|
29
|
+
export declare function withRefSlotsAsReadings(schema: unknown, scope: RefReadingScope): unknown;
|
|
30
|
+
//# sourceMappingURL=ref-slot-reading.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ref-slot-reading.d.ts","sourceRoot":"","sources":["../src/ref-slot-reading.ts"],"names":[],"mappings":"AA6BA,MAAM,WAAW,eAAe;IAC9B,6CAA6C;IAC7C,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;IACvD;8BAC0B;IAC1B,WAAW,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAChD;AAMD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzB,KAAK,EAAE,eAAe,GACrB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAuBjC;AA6BD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,OAAO,EACf,KAAK,EAAE,eAAe,GACrB,OAAO,CAqBT"}
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
function isObject(value) {
|
|
30
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The reading schema for one ref slot, or undefined when the slot's constraint
|
|
34
|
+
* resolves to nothing — in which case the slot is left exactly as it was, the
|
|
35
|
+
* degrade-to-today direction every unresolved annotation takes here.
|
|
36
|
+
*
|
|
37
|
+
* A slot constrained to SEVERAL kinds types `status` from the kinds that agree
|
|
38
|
+
* and drops the rest: a read valid against one permitted target must not be
|
|
39
|
+
* rejected because a sibling kind does not declare that field.
|
|
40
|
+
*/
|
|
41
|
+
export function refSlotReadingSchema(slot, scope) {
|
|
42
|
+
const ref = readRefSlot(slot);
|
|
43
|
+
if (!ref || ref.kinds.length === 0)
|
|
44
|
+
return undefined;
|
|
45
|
+
const statuses = [];
|
|
46
|
+
for (const kind of ref.kinds) {
|
|
47
|
+
const canonical = scope.resolveKind?.(kind) ?? kind;
|
|
48
|
+
const definition = scope.resolve(canonical) ?? scope.resolve(kind);
|
|
49
|
+
if (!definition)
|
|
50
|
+
continue;
|
|
51
|
+
const status = effectiveStatusSchema(definition, (k) => scope.resolve(k));
|
|
52
|
+
if (isObject(status) && isObject(status.properties))
|
|
53
|
+
statuses.push(status);
|
|
54
|
+
}
|
|
55
|
+
if (statuses.length === 0)
|
|
56
|
+
return openReading(slot);
|
|
57
|
+
const shared = statuses.length === 1 ? statuses[0].properties : sharedProperties(statuses);
|
|
58
|
+
return {
|
|
59
|
+
...describedBy(slot),
|
|
60
|
+
type: "object",
|
|
61
|
+
additionalProperties: true,
|
|
62
|
+
properties: {
|
|
63
|
+
status: { type: "object", additionalProperties: false, properties: shared },
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Properties every candidate kind declares. A field only some of them report
|
|
68
|
+
* is not one a read can rely on, and rejecting it would be wrong for the kinds
|
|
69
|
+
* that do — so it degrades to the open half rather than to an error. */
|
|
70
|
+
function sharedProperties(statuses) {
|
|
71
|
+
const [first, ...rest] = statuses;
|
|
72
|
+
const out = {};
|
|
73
|
+
for (const [name, schema] of Object.entries(first.properties)) {
|
|
74
|
+
if (rest.every((other) => name in other.properties)) {
|
|
75
|
+
out[name] = schema;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/** A reading whose `status` cannot be typed: open, so nothing new is rejected. */
|
|
81
|
+
function openReading(slot) {
|
|
82
|
+
return { ...describedBy(slot), type: "object", additionalProperties: true };
|
|
83
|
+
}
|
|
84
|
+
/** The slot's own prose, kept so hover and completion still say what it is. */
|
|
85
|
+
function describedBy(slot) {
|
|
86
|
+
const out = {};
|
|
87
|
+
if (typeof slot.title === "string")
|
|
88
|
+
out.title = slot.title;
|
|
89
|
+
if (typeof slot.description === "string")
|
|
90
|
+
out.description = slot.description;
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Rewrite every ref slot in a kind's own `schema:` to what reading it yields, so
|
|
95
|
+
* the `self` CEL variable answers about published state rather than about the
|
|
96
|
+
* annotation node.
|
|
97
|
+
*
|
|
98
|
+
* A SCHEMA walk, so it recurses through `properties` / `items` /
|
|
99
|
+
* `additionalProperties` and nothing else — there is no inline-declaration
|
|
100
|
+
* boundary to stop at here, because a `kind` key inside a schema is a property
|
|
101
|
+
* NAMED kind, not a nested resource. Untouched subtrees keep their identity, so
|
|
102
|
+
* a schema declaring no ref slot is returned as it was.
|
|
103
|
+
*/
|
|
104
|
+
export function withRefSlotsAsReadings(schema, scope) {
|
|
105
|
+
if (Array.isArray(schema))
|
|
106
|
+
return schema.map((item) => withRefSlotsAsReadings(item, scope));
|
|
107
|
+
if (!isObject(schema))
|
|
108
|
+
return schema;
|
|
109
|
+
if (isRefSlot(schema))
|
|
110
|
+
return refSlotReadingSchema(schema, scope) ?? schema;
|
|
111
|
+
let changed = false;
|
|
112
|
+
const out = { ...schema };
|
|
113
|
+
for (const key of ["properties", "items", "additionalProperties"]) {
|
|
114
|
+
const node = schema[key];
|
|
115
|
+
if (node === undefined)
|
|
116
|
+
continue;
|
|
117
|
+
const next = key === "properties"
|
|
118
|
+
? mapValues(node, (value) => withRefSlotsAsReadings(value, scope))
|
|
119
|
+
: withRefSlotsAsReadings(node, scope);
|
|
120
|
+
if (next !== node) {
|
|
121
|
+
out[key] = next;
|
|
122
|
+
changed = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return changed ? out : schema;
|
|
126
|
+
}
|
|
127
|
+
function mapValues(node, fn) {
|
|
128
|
+
if (!isObject(node))
|
|
129
|
+
return node;
|
|
130
|
+
let changed = false;
|
|
131
|
+
const out = {};
|
|
132
|
+
for (const [key, value] of Object.entries(node)) {
|
|
133
|
+
const next = fn(value);
|
|
134
|
+
if (next !== value)
|
|
135
|
+
changed = true;
|
|
136
|
+
out[key] = next;
|
|
137
|
+
}
|
|
138
|
+
return changed ? out : node;
|
|
139
|
+
}
|