typekro 0.29.0 → 0.30.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/.tsbuildinfo +1 -1
- package/dist/core/composition/context.d.ts +4 -0
- package/dist/core/composition/context.d.ts.map +1 -1
- package/dist/core/composition/context.js +1 -0
- package/dist/core/composition/context.js.map +1 -1
- package/dist/core/composition/imperative.d.ts.map +1 -1
- package/dist/core/composition/imperative.js +1 -0
- package/dist/core/composition/imperative.js.map +1 -1
- package/dist/core/deployment/crd-manager.d.ts +4 -1
- package/dist/core/deployment/crd-manager.d.ts.map +1 -1
- package/dist/core/deployment/crd-manager.js +9 -4
- package/dist/core/deployment/crd-manager.js.map +1 -1
- package/dist/core/proxy/create-resource.d.ts.map +1 -1
- package/dist/core/proxy/create-resource.js +3 -1
- package/dist/core/proxy/create-resource.js.map +1 -1
- package/dist/core/references/cel.d.ts +10 -7
- package/dist/core/references/cel.d.ts.map +1 -1
- package/dist/core/references/cel.js +13 -12
- package/dist/core/references/cel.js.map +1 -1
- package/dist/core/serialization/cel-references.d.ts +1 -0
- package/dist/core/serialization/cel-references.d.ts.map +1 -1
- package/dist/core/serialization/cel-references.js +1 -1
- package/dist/core/serialization/cel-references.js.map +1 -1
- package/dist/core/serialization/schema.d.ts.map +1 -1
- package/dist/core/serialization/schema.js +348 -24
- package/dist/core/serialization/schema.js.map +1 -1
- package/dist/factories/apisix/compositions/apisix-bootstrap.d.ts.map +1 -1
- package/dist/factories/apisix/compositions/apisix-bootstrap.js +3 -2
- package/dist/factories/apisix/compositions/apisix-bootstrap.js.map +1 -1
- package/dist/factories/clickstack/utils/helm-values-mapper.js.map +1 -1
- package/dist/factories/ory/utils/helm-values-mapper.d.ts.map +1 -1
- package/dist/factories/ory/utils/helm-values-mapper.js +6 -3
- package/dist/factories/ory/utils/helm-values-mapper.js.map +1 -1
- package/dist/factories/rook/compositions/rook-ceph-platform.d.ts +104 -0
- package/dist/factories/rook/compositions/rook-ceph-platform.d.ts.map +1 -1
- package/dist/factories/rook/types.d.ts +104 -0
- package/dist/factories/rook/types.d.ts.map +1 -1
- package/dist/factories/rook/types.js +9 -0
- package/dist/factories/rook/types.js.map +1 -1
- package/dist/factories/rook/utils/helm-values-mapper.d.ts +1 -1
- package/dist/factories/rook/utils/helm-values-mapper.d.ts.map +1 -1
- package/dist/factories/rook/utils/helm-values-mapper.js +19 -7
- package/dist/factories/rook/utils/helm-values-mapper.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,13 +8,16 @@
|
|
|
8
8
|
import { KUBERNETES_REF_SCHEMA_MARKER_SOURCE } from '../../shared/brands.js';
|
|
9
9
|
import { escapeCelString } from '../../utils/cel-escape.js';
|
|
10
10
|
import { pascalCase } from '../../utils/string.js';
|
|
11
|
+
import { isCelExpression, isKubernetesRef } from '../../utils/type-guards.js';
|
|
12
|
+
import { isValuesMergeExpression } from '../aspects/values-merge.js';
|
|
13
|
+
import { getCompositionAnalysisMetadata } from '../composition/analysis-metadata.js';
|
|
11
14
|
import { createCompositionContext, runWithCompositionContext } from '../composition/context.js';
|
|
15
|
+
import { TypeKroError } from '../errors.js';
|
|
12
16
|
import { getComponentLogger } from '../logging/index.js';
|
|
13
17
|
import { getMetadataField } from '../metadata/index.js';
|
|
14
18
|
import { createSchemaProxy } from '../references/index.js';
|
|
15
|
-
import { getCompositionAnalysisMetadata } from '../composition/analysis-metadata.js';
|
|
16
19
|
import { separateStatusFields } from '../validation/cel-validator.js';
|
|
17
|
-
import { serializeStatusMappingsToCel } from './cel-references.js';
|
|
20
|
+
import { celLiteralForValueTree, serializeStatusMappingsToCel } from './cel-references.js';
|
|
18
21
|
const logger = getComponentLogger('schema-defaults');
|
|
19
22
|
const SCHEMA_MARKER_PATTERN_SOURCE = KUBERNETES_REF_SCHEMA_MARKER_SOURCE;
|
|
20
23
|
function deriveResourceIdAliases(resourceId) {
|
|
@@ -291,7 +294,10 @@ function resolveDefaultsByReExecution(compositionFn, specType, proxyResources) {
|
|
|
291
294
|
// Build the set of optional top-level field names. Only optional fields
|
|
292
295
|
// can be ternary-controlled (required fields always have a value).
|
|
293
296
|
const optionalFieldNames = new Set((specJson.optional ?? []).map((p) => p.key));
|
|
294
|
-
const
|
|
297
|
+
const optionalFieldPaths = collectSchemaFieldPaths(specJson).optional;
|
|
298
|
+
const tempCtx = createCompositionContext('defaults-extraction', {
|
|
299
|
+
suppressResourceDiagnostics: true,
|
|
300
|
+
});
|
|
295
301
|
// The all-undefined defaults run can throw when a composition dereferences an
|
|
296
302
|
// absent optional parent (`spec.settings.tier` with settings undefined). Contain
|
|
297
303
|
// it: literal-default extraction is then simply empty, but cross-field probing
|
|
@@ -331,6 +337,7 @@ function resolveDefaultsByReExecution(compositionFn, specType, proxyResources) {
|
|
|
331
337
|
});
|
|
332
338
|
}
|
|
333
339
|
const crossFieldOverrides = [];
|
|
340
|
+
const ambiguousStructuredFallbacks = [];
|
|
334
341
|
for (const [id, proxyRes] of proxyEntries) {
|
|
335
342
|
const defaultsRes = defaultsMap.get(id);
|
|
336
343
|
// Literal-default + ternary extraction need the defaults-run counterpart;
|
|
@@ -339,14 +346,23 @@ function resolveDefaultsByReExecution(compositionFn, specType, proxyResources) {
|
|
|
339
346
|
extractDefaultsByComparison(proxyRes, defaultsRes, defaults);
|
|
340
347
|
extractTernaryConditionals(proxyRes, defaultsRes, ternaryConditionals, optionalFieldNames, ternaryConditionFieldHints);
|
|
341
348
|
}
|
|
349
|
+
// Ambiguity detection cannot depend on the all-defaults resource existing:
|
|
350
|
+
// an optional guard may remove the entire resource from that execution.
|
|
351
|
+
// Probe each bare structured schema marker with only that marker absent.
|
|
352
|
+
// A concrete structured value then proves only that black-box execution is
|
|
353
|
+
// ambiguous—not that the source was `??`—so compilation fails closed.
|
|
354
|
+
probeAmbiguousStructuredFallbacks(compositionFn, specJson, proxyRes, id, optionalFieldPaths, ambiguousStructuredFallbacks);
|
|
342
355
|
// Cross-field `??` chain reconstruction is independent of the defaults run —
|
|
343
356
|
// it does its own presence-probing — so it always runs.
|
|
344
357
|
reconstructCrossFieldChains(compositionFn, specJson, id, proxyRes, defaults, crossFieldOverrides);
|
|
345
358
|
}
|
|
346
359
|
const hasResults = Object.keys(defaults).length > 0 ||
|
|
347
360
|
ternaryConditionals.length > 0 ||
|
|
348
|
-
crossFieldOverrides.length > 0
|
|
349
|
-
|
|
361
|
+
crossFieldOverrides.length > 0 ||
|
|
362
|
+
ambiguousStructuredFallbacks.length > 0;
|
|
363
|
+
return hasResults
|
|
364
|
+
? { defaults, ternaryConditionals, crossFieldOverrides, ambiguousStructuredFallbacks }
|
|
365
|
+
: undefined;
|
|
350
366
|
}
|
|
351
367
|
catch (err) {
|
|
352
368
|
// Best-effort: composition functions with undefined spec fields may throw
|
|
@@ -387,6 +403,11 @@ function classifyLeaf(value) {
|
|
|
387
403
|
return { kind: 'none' };
|
|
388
404
|
return { kind: 'literal', value };
|
|
389
405
|
}
|
|
406
|
+
if (Array.isArray(value))
|
|
407
|
+
return { kind: 'structured', value };
|
|
408
|
+
if (value !== null && typeof value === 'object') {
|
|
409
|
+
return { kind: 'structured', value: value };
|
|
410
|
+
}
|
|
390
411
|
return { kind: 'none' };
|
|
391
412
|
}
|
|
392
413
|
/** Read the value at a structural path within a (proxy/probe) resource object. */
|
|
@@ -433,6 +454,38 @@ function collectMarkerLeaves(node, path, out) {
|
|
|
433
454
|
}
|
|
434
455
|
}
|
|
435
456
|
}
|
|
457
|
+
function collectAmbiguityCandidateLeaves(node, path, out, mergeShapes = []) {
|
|
458
|
+
if (isValuesMergeExpression(node)) {
|
|
459
|
+
const nestedMergeShapes = [
|
|
460
|
+
...mergeShapes,
|
|
461
|
+
{ path: [...path], overlayCount: node.overlays.length },
|
|
462
|
+
];
|
|
463
|
+
collectAmbiguityCandidateLeaves(node.base, [...path, 'base'], out, nestedMergeShapes);
|
|
464
|
+
node.overlays.forEach((overlay, index) => collectAmbiguityCandidateLeaves(overlay, [...path, 'overlays', String(index)], out, nestedMergeShapes));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const classified = classifyLeaf(node);
|
|
468
|
+
if (classified.kind === 'field') {
|
|
469
|
+
out.push({
|
|
470
|
+
path: [...path],
|
|
471
|
+
field: classified.field,
|
|
472
|
+
mergeShapes: mergeShapes.map((shape) => ({
|
|
473
|
+
path: [...shape.path],
|
|
474
|
+
overlayCount: shape.overlayCount,
|
|
475
|
+
})),
|
|
476
|
+
});
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (Array.isArray(node)) {
|
|
480
|
+
node.forEach((value, index) => collectAmbiguityCandidateLeaves(value, [...path, String(index)], out, mergeShapes));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (node && typeof node === 'object') {
|
|
484
|
+
for (const key of Object.keys(node)) {
|
|
485
|
+
collectAmbiguityCandidateLeaves(node[key], [...path, key], out, mergeShapes);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
436
489
|
/**
|
|
437
490
|
* Build a probe `spec`: the magic schema proxy (so any field access emits its
|
|
438
491
|
* `spec.X` marker) wrapped so the dotted paths in `absent` resolve to `undefined`.
|
|
@@ -449,8 +502,8 @@ function buildProbeSpec(specJson, absent) {
|
|
|
449
502
|
if (absent.has(full))
|
|
450
503
|
return undefined;
|
|
451
504
|
const v = t[prop];
|
|
452
|
-
const
|
|
453
|
-
if (
|
|
505
|
+
const hasDeeperMutation = [...absent].some((a) => a.startsWith(`${full}.`));
|
|
506
|
+
if (hasDeeperMutation && v && (typeof v === 'object' || typeof v === 'function')) {
|
|
454
507
|
return wrap(v, full);
|
|
455
508
|
}
|
|
456
509
|
return v;
|
|
@@ -461,7 +514,9 @@ function buildProbeSpec(specJson, absent) {
|
|
|
461
514
|
/** Run the composition with a probe spec; return the resource matching `resourceId`. */
|
|
462
515
|
function runProbe(compositionFn, specJson, absent, resourceId) {
|
|
463
516
|
try {
|
|
464
|
-
const ctx = createCompositionContext('cross-field-probe'
|
|
517
|
+
const ctx = createCompositionContext('cross-field-probe', {
|
|
518
|
+
suppressResourceDiagnostics: true,
|
|
519
|
+
});
|
|
465
520
|
runWithCompositionContext(ctx, () => {
|
|
466
521
|
compositionFn(buildProbeSpec(specJson, absent));
|
|
467
522
|
});
|
|
@@ -472,6 +527,249 @@ function runProbe(compositionFn, specJson, absent, resourceId) {
|
|
|
472
527
|
}
|
|
473
528
|
}
|
|
474
529
|
const celField = (field) => `schema.spec.${field}`;
|
|
530
|
+
/**
|
|
531
|
+
* Read a probed resource using a path captured from the proxy run. Values-merge
|
|
532
|
+
* operand segments intentionally are not transparent: if removing a candidate
|
|
533
|
+
* causes the wrapper to normalize away, the resulting merged object is not
|
|
534
|
+
* evidence that the original operand contained an implicit fallback.
|
|
535
|
+
*/
|
|
536
|
+
function readRawValueAtPath(obj, path) {
|
|
537
|
+
let current = obj;
|
|
538
|
+
for (const key of path) {
|
|
539
|
+
if (current === null || typeof current !== 'object')
|
|
540
|
+
return undefined;
|
|
541
|
+
current = current[key];
|
|
542
|
+
}
|
|
543
|
+
return current;
|
|
544
|
+
}
|
|
545
|
+
function replaceValueAtPath(root, path, value) {
|
|
546
|
+
const [head, ...tail] = path;
|
|
547
|
+
if (head === undefined)
|
|
548
|
+
return value;
|
|
549
|
+
if (Array.isArray(root)) {
|
|
550
|
+
const index = Number(head);
|
|
551
|
+
if (!Number.isInteger(index) || index < 0 || index >= root.length)
|
|
552
|
+
return root;
|
|
553
|
+
const copy = [...root];
|
|
554
|
+
copy[index] = replaceValueAtPath(copy[index], tail, value);
|
|
555
|
+
return copy;
|
|
556
|
+
}
|
|
557
|
+
if (root === null || typeof root !== 'object')
|
|
558
|
+
return root;
|
|
559
|
+
const record = root;
|
|
560
|
+
return {
|
|
561
|
+
...record,
|
|
562
|
+
[head]: replaceValueAtPath(record[head], tail, value),
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Canonicalize only ordinary value-tree containers. References and CEL nodes
|
|
567
|
+
* are semantic leaves, while merge expressions retain their explicit operand
|
|
568
|
+
* ordering. Kubernetes object maps are unordered, so sorting their keys keeps
|
|
569
|
+
* semantically identical normalization paths from differing solely because a
|
|
570
|
+
* helper inserted properties in a different order.
|
|
571
|
+
*/
|
|
572
|
+
function canonicalizeValueTree(value) {
|
|
573
|
+
if (isKubernetesRef(value) || isCelExpression(value))
|
|
574
|
+
return value;
|
|
575
|
+
if (isValuesMergeExpression(value)) {
|
|
576
|
+
let base = canonicalizeValueTree(value.base);
|
|
577
|
+
let overlays = value.overlays.map(canonicalizeValueTree);
|
|
578
|
+
while (isValuesMergeExpression(base)) {
|
|
579
|
+
overlays = [...base.overlays, ...overlays];
|
|
580
|
+
base = base.base;
|
|
581
|
+
}
|
|
582
|
+
overlays = overlays.filter((overlay) => overlay !== undefined &&
|
|
583
|
+
!(overlay !== null &&
|
|
584
|
+
typeof overlay === 'object' &&
|
|
585
|
+
!Array.isArray(overlay) &&
|
|
586
|
+
Object.getPrototypeOf(overlay) === Object.prototype &&
|
|
587
|
+
Object.keys(overlay).length === 0));
|
|
588
|
+
if (overlays.length === 0)
|
|
589
|
+
return base;
|
|
590
|
+
return {
|
|
591
|
+
__typekroValuesMerge: true,
|
|
592
|
+
base,
|
|
593
|
+
overlays,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
if (Array.isArray(value))
|
|
597
|
+
return value.map(canonicalizeValueTree);
|
|
598
|
+
if (value === null || typeof value !== 'object')
|
|
599
|
+
return value;
|
|
600
|
+
const prototype = Object.getPrototypeOf(value);
|
|
601
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
602
|
+
return value;
|
|
603
|
+
const record = value;
|
|
604
|
+
return Object.fromEntries(Object.keys(record)
|
|
605
|
+
.sort()
|
|
606
|
+
.flatMap((key) => {
|
|
607
|
+
const canonical = canonicalizeValueTree(record[key]);
|
|
608
|
+
return canonical === undefined ? [] : [[key, canonical]];
|
|
609
|
+
}));
|
|
610
|
+
}
|
|
611
|
+
function valueTreeFingerprint(value) {
|
|
612
|
+
try {
|
|
613
|
+
return celLiteralForValueTree(canonicalizeValueTree(value), undefined, 'structured-fallback-normalization');
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
return undefined;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function schemaNodeAllowsNull(node) {
|
|
620
|
+
if (Array.isArray(node))
|
|
621
|
+
return node.some(schemaNodeAllowsNull);
|
|
622
|
+
if (node === null || typeof node !== 'object')
|
|
623
|
+
return false;
|
|
624
|
+
const record = node;
|
|
625
|
+
return Object.hasOwn(record, 'unit') && record.unit === null;
|
|
626
|
+
}
|
|
627
|
+
function collectSchemaFieldPaths(node) {
|
|
628
|
+
const optional = new Set();
|
|
629
|
+
const nullable = new Set();
|
|
630
|
+
const walk = (current, prefix) => {
|
|
631
|
+
if (Array.isArray(current)) {
|
|
632
|
+
for (const branch of current)
|
|
633
|
+
walk(branch, prefix);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (current === null || typeof current !== 'object')
|
|
637
|
+
return;
|
|
638
|
+
const objectNode = current;
|
|
639
|
+
const walkCollectionMember = (member, path) => {
|
|
640
|
+
if (schemaNodeAllowsNull(member))
|
|
641
|
+
nullable.add(path);
|
|
642
|
+
walk(member, path);
|
|
643
|
+
};
|
|
644
|
+
if (objectNode.proto === 'Array' && objectNode.sequence !== undefined) {
|
|
645
|
+
const elementPath = `${prefix}[]`;
|
|
646
|
+
walkCollectionMember(objectNode.sequence, elementPath);
|
|
647
|
+
}
|
|
648
|
+
for (const [index, member] of (objectNode.prefix ?? []).entries()) {
|
|
649
|
+
walkCollectionMember(member, `${prefix}[${index}]`);
|
|
650
|
+
}
|
|
651
|
+
for (const [index, member] of (objectNode.optionals ?? []).entries()) {
|
|
652
|
+
walkCollectionMember(member, `${prefix}[optional:${index}]`);
|
|
653
|
+
}
|
|
654
|
+
for (const [index, entry] of (objectNode.defaultables ?? []).entries()) {
|
|
655
|
+
walkCollectionMember(entry[0], `${prefix}[defaultable:${index}]`);
|
|
656
|
+
}
|
|
657
|
+
if (objectNode.variadic !== undefined) {
|
|
658
|
+
walkCollectionMember(objectNode.variadic, `${prefix}[]`);
|
|
659
|
+
}
|
|
660
|
+
for (const [index, member] of (objectNode.postfix ?? []).entries()) {
|
|
661
|
+
walkCollectionMember(member, `${prefix}[postfix:${index}]`);
|
|
662
|
+
}
|
|
663
|
+
for (const entry of objectNode.index ?? []) {
|
|
664
|
+
const valuePath = prefix ? `${prefix}.*` : '*';
|
|
665
|
+
walkCollectionMember(entry.value, valuePath);
|
|
666
|
+
}
|
|
667
|
+
for (const entry of objectNode.optional ?? []) {
|
|
668
|
+
const path = prefix ? `${prefix}.${entry.key}` : entry.key;
|
|
669
|
+
optional.add(path);
|
|
670
|
+
if (schemaNodeAllowsNull(entry.value))
|
|
671
|
+
nullable.add(path);
|
|
672
|
+
walk(entry.value, path);
|
|
673
|
+
}
|
|
674
|
+
for (const entry of objectNode.required ?? []) {
|
|
675
|
+
const path = prefix ? `${prefix}.${entry.key}` : entry.key;
|
|
676
|
+
if (schemaNodeAllowsNull(entry.value))
|
|
677
|
+
nullable.add(path);
|
|
678
|
+
walk(entry.value, path);
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
walk(node, '');
|
|
682
|
+
return { optional, nullable };
|
|
683
|
+
}
|
|
684
|
+
function matchingAncestorPath(field, paths) {
|
|
685
|
+
let match;
|
|
686
|
+
for (const path of paths) {
|
|
687
|
+
if ((field === path || field.startsWith(`${path}.`)) &&
|
|
688
|
+
(match === undefined || path.length > match.length)) {
|
|
689
|
+
match = path;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return match;
|
|
693
|
+
}
|
|
694
|
+
function readProbeValueAtPath(proxyResource, probedResource, candidate) {
|
|
695
|
+
for (const shape of candidate.mergeShapes) {
|
|
696
|
+
const probedMerge = readRawValueAtPath(probedResource, shape.path);
|
|
697
|
+
if (!isValuesMergeExpression(probedMerge) ||
|
|
698
|
+
probedMerge.overlays.length !== shape.overlayCount) {
|
|
699
|
+
const originalMerge = readRawValueAtPath(proxyResource, shape.path);
|
|
700
|
+
if (!isValuesMergeExpression(originalMerge)) {
|
|
701
|
+
return { value: undefined, ambiguousNormalization: true };
|
|
702
|
+
}
|
|
703
|
+
const expectedWithoutCandidate = replaceValueAtPath(originalMerge, candidate.path.slice(shape.path.length), undefined);
|
|
704
|
+
const actualFingerprint = valueTreeFingerprint(probedMerge);
|
|
705
|
+
const expectedFingerprint = valueTreeFingerprint(expectedWithoutCandidate);
|
|
706
|
+
const ambiguousNormalization = actualFingerprint === undefined ||
|
|
707
|
+
expectedFingerprint === undefined ||
|
|
708
|
+
actualFingerprint !== expectedFingerprint;
|
|
709
|
+
return {
|
|
710
|
+
value: ambiguousNormalization ? probedMerge : undefined,
|
|
711
|
+
ambiguousNormalization,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
value: readRawValueAtPath(probedResource, candidate.path),
|
|
717
|
+
ambiguousNormalization: false,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Detect a bare structured schema marker that becomes a concrete object/array
|
|
722
|
+
* when that exact field is absent. This targeted probe does not infer `??`
|
|
723
|
+
* semantics; it establishes that black-box execution is ambiguous and must
|
|
724
|
+
* fail closed. Explicit `Cel.default()` values carry CEL provenance and do not
|
|
725
|
+
* appear as bare schema-marker leaves.
|
|
726
|
+
*/
|
|
727
|
+
function probeAmbiguousStructuredFallbacks(compositionFn, specJson, proxyResource, resourceId, optionalFieldPaths, out) {
|
|
728
|
+
const markerLeaves = [];
|
|
729
|
+
collectAmbiguityCandidateLeaves(proxyResource, [], markerLeaves);
|
|
730
|
+
const candidatesByField = new Map();
|
|
731
|
+
for (const marker of markerLeaves) {
|
|
732
|
+
const candidates = candidatesByField.get(marker.field) ?? [];
|
|
733
|
+
candidates.push(marker);
|
|
734
|
+
candidatesByField.set(marker.field, candidates);
|
|
735
|
+
}
|
|
736
|
+
for (const [field, candidates] of candidatesByField) {
|
|
737
|
+
// Removing a required leaf is not a realizable KRO instance state. Probe
|
|
738
|
+
// only a field that is optional itself or descends from an optional parent.
|
|
739
|
+
// Nullable fields are rejected at the ArkType → KRO schema boundary because
|
|
740
|
+
// SimpleSchema cannot represent explicit null.
|
|
741
|
+
const optionalPath = matchingAncestorPath(field, optionalFieldPaths);
|
|
742
|
+
if (optionalPath === undefined)
|
|
743
|
+
continue;
|
|
744
|
+
const probedResource = runProbe(compositionFn, specJson, new Set([optionalPath]), resourceId);
|
|
745
|
+
for (const candidate of candidates) {
|
|
746
|
+
const probeRead = readProbeValueAtPath(proxyResource, probedResource, candidate);
|
|
747
|
+
const fallbackValue = probeRead.value;
|
|
748
|
+
if (probeRead.ambiguousNormalization ||
|
|
749
|
+
Array.isArray(fallbackValue) ||
|
|
750
|
+
(fallbackValue !== null && typeof fallbackValue === 'object')) {
|
|
751
|
+
out.push({
|
|
752
|
+
resourceId,
|
|
753
|
+
path: candidate.path,
|
|
754
|
+
field,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function assertNoAmbiguousStructuredFallbacks(result) {
|
|
761
|
+
const [ambiguous] = result?.ambiguousStructuredFallbacks ?? [];
|
|
762
|
+
if (!ambiguous)
|
|
763
|
+
return;
|
|
764
|
+
throw new TypeKroError(`Cannot prove structured fallback semantics for schema.spec.${ambiguous.field} at ` +
|
|
765
|
+
`${ambiguous.resourceId}.${ambiguous.path.join('.')}. ` +
|
|
766
|
+
'Black-box re-execution cannot distinguish nullish coalescing from value-based control flow. ' +
|
|
767
|
+
'Use Cel.default(value, fallback) or Cel.coalesce(value, fallback) to make the graph semantics explicit.', 'AMBIGUOUS_STRUCTURED_FALLBACK', {
|
|
768
|
+
resourceId: ambiguous.resourceId,
|
|
769
|
+
path: ambiguous.path.join('.'),
|
|
770
|
+
field: ambiguous.field,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
475
773
|
/**
|
|
476
774
|
* Presence guard for a (possibly nested) optional field — every ancestor must be
|
|
477
775
|
* present too. `userDeployments.enableSubchart` →
|
|
@@ -484,21 +782,33 @@ function presenceGuard(field) {
|
|
|
484
782
|
return segs.map((_, i) => `has(schema.spec.${segs.slice(0, i + 1).join('.')})`).join(' && ');
|
|
485
783
|
}
|
|
486
784
|
/** Render the chain's CEL: <guard(a)> ? a : (<guard(b)> ? b : <tail>). */
|
|
487
|
-
function buildChainCel(chain, tail) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
?
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
expr =
|
|
499
|
-
nested =
|
|
785
|
+
function buildChainCel(chain, tail, resourceId, path) {
|
|
786
|
+
try {
|
|
787
|
+
const tailCel = tail.kind === 'literal'
|
|
788
|
+
? typeof tail.value === 'string'
|
|
789
|
+
? `"${escapeCelString(tail.value)}"`
|
|
790
|
+
: String(tail.value)
|
|
791
|
+
: tail.kind === 'field'
|
|
792
|
+
? celField(tail.field)
|
|
793
|
+
: tail.kind === 'structured'
|
|
794
|
+
? `(${celLiteralForValueTree(tail.value, undefined, `structured-coalesce.${resourceId}.${path.join('.')}`)})`
|
|
795
|
+
: 'omit()';
|
|
796
|
+
let expr = tailCel;
|
|
797
|
+
let nested = false;
|
|
798
|
+
for (const f of [...chain].reverse()) {
|
|
799
|
+
expr = `${presenceGuard(f)} ? ${celField(f)} : ${nested ? `(${expr})` : expr}`;
|
|
800
|
+
nested = true;
|
|
801
|
+
}
|
|
802
|
+
return `\${${expr}}`;
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
logger.debug('Skipping non-serializable structured coalescing fallback', {
|
|
806
|
+
resourceId,
|
|
807
|
+
path: path.join('.'),
|
|
808
|
+
error: error instanceof Error ? error.message : String(error),
|
|
809
|
+
});
|
|
810
|
+
return undefined;
|
|
500
811
|
}
|
|
501
|
-
return `\${${expr}}`;
|
|
502
812
|
}
|
|
503
813
|
/**
|
|
504
814
|
* For each `spec.X` marker leaf in the proxy-run resource, probe to reconstruct its
|
|
@@ -538,8 +848,11 @@ function reconstructCrossFieldChains(compositionFn, specJson, resourceId, proxyR
|
|
|
538
848
|
// existing default/omit-wrapping mechanisms, so this change can't regress them.
|
|
539
849
|
const [head] = chain;
|
|
540
850
|
if (chain.length >= 2 && head !== undefined) {
|
|
541
|
-
|
|
542
|
-
|
|
851
|
+
const cel = buildChainCel(chain, tail, resourceId, path);
|
|
852
|
+
if (cel !== undefined) {
|
|
853
|
+
delete defaults[head];
|
|
854
|
+
out.push({ resourceId, path, cel });
|
|
855
|
+
}
|
|
543
856
|
}
|
|
544
857
|
}
|
|
545
858
|
}
|
|
@@ -1019,6 +1332,15 @@ function collectOmitFields(specFields, specType) {
|
|
|
1019
1332
|
* instead of auto-generated CEL expressions.
|
|
1020
1333
|
*/
|
|
1021
1334
|
export function arktypeToKroSchema(name, schemaDefinition, resources, statusMappings, nestedStatusCel, schemaFieldValidations) {
|
|
1335
|
+
const nullableField = collectSchemaFieldPaths(schemaDefinition.spec.json).nullable
|
|
1336
|
+
.values()
|
|
1337
|
+
.next().value;
|
|
1338
|
+
if (nullableField !== undefined) {
|
|
1339
|
+
throw new TypeKroError(`KRO SimpleSchema cannot represent nullable field schema.spec.${nullableField}. ` +
|
|
1340
|
+
"TypeKro cannot preserve an ArkType 'T | null' contract by silently converting null " +
|
|
1341
|
+
'to omission or a non-nullable field. Model the KRO input as optional and use ' +
|
|
1342
|
+
'Cel.default(...) for omission, or use direct mode when explicit null is required.', 'KRO_NULLABLE_SCHEMA_UNSUPPORTED', { field: nullableField });
|
|
1343
|
+
}
|
|
1022
1344
|
const specFields = arktypeJsonToKroFields(schemaDefinition.spec.json);
|
|
1023
1345
|
if (!specFields.name) {
|
|
1024
1346
|
specFields.name = `string | default="${name}"`;
|
|
@@ -1093,6 +1415,7 @@ export function arktypeToKroSchema(name, schemaDefinition, resources, statusMapp
|
|
|
1093
1415
|
const reExecutionResult = nestedDefinition && nestedResources
|
|
1094
1416
|
? resolveDefaultsByReExecution(fn, nestedDefinition.spec, nestedResources)
|
|
1095
1417
|
: undefined;
|
|
1418
|
+
assertNoAmbiguousStructuredFallbacks(reExecutionResult);
|
|
1096
1419
|
const reExecutionDefaults = reExecutionResult?.defaults ?? {};
|
|
1097
1420
|
const innerDefaults = remapNullishDefaults({ ...extractedDefaults, ...reExecutionDefaults }, specMappings);
|
|
1098
1421
|
// Non-destructive: don't overwrite existing outer defaults.
|
|
@@ -1112,6 +1435,7 @@ export function arktypeToKroSchema(name, schemaDefinition, resources, statusMapp
|
|
|
1112
1435
|
if (typeof compositionFn === 'function' && resources) {
|
|
1113
1436
|
const reExecutionResult = resolveDefaultsByReExecution(compositionFn, schemaDefinition.spec, resources);
|
|
1114
1437
|
if (reExecutionResult) {
|
|
1438
|
+
assertNoAmbiguousStructuredFallbacks(reExecutionResult);
|
|
1115
1439
|
applyNullishDefaults(specFields, reExecutionResult.defaults, true);
|
|
1116
1440
|
ternaryConditionals.push(...reExecutionResult.ternaryConditionals);
|
|
1117
1441
|
// Cross-field `??` chains reconstructed by probing: write each CEL conditional
|