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.
Files changed (44) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/core/composition/context.d.ts +4 -0
  3. package/dist/core/composition/context.d.ts.map +1 -1
  4. package/dist/core/composition/context.js +1 -0
  5. package/dist/core/composition/context.js.map +1 -1
  6. package/dist/core/composition/imperative.d.ts.map +1 -1
  7. package/dist/core/composition/imperative.js +1 -0
  8. package/dist/core/composition/imperative.js.map +1 -1
  9. package/dist/core/deployment/crd-manager.d.ts +4 -1
  10. package/dist/core/deployment/crd-manager.d.ts.map +1 -1
  11. package/dist/core/deployment/crd-manager.js +9 -4
  12. package/dist/core/deployment/crd-manager.js.map +1 -1
  13. package/dist/core/proxy/create-resource.d.ts.map +1 -1
  14. package/dist/core/proxy/create-resource.js +3 -1
  15. package/dist/core/proxy/create-resource.js.map +1 -1
  16. package/dist/core/references/cel.d.ts +10 -7
  17. package/dist/core/references/cel.d.ts.map +1 -1
  18. package/dist/core/references/cel.js +13 -12
  19. package/dist/core/references/cel.js.map +1 -1
  20. package/dist/core/serialization/cel-references.d.ts +1 -0
  21. package/dist/core/serialization/cel-references.d.ts.map +1 -1
  22. package/dist/core/serialization/cel-references.js +1 -1
  23. package/dist/core/serialization/cel-references.js.map +1 -1
  24. package/dist/core/serialization/schema.d.ts.map +1 -1
  25. package/dist/core/serialization/schema.js +348 -24
  26. package/dist/core/serialization/schema.js.map +1 -1
  27. package/dist/factories/apisix/compositions/apisix-bootstrap.d.ts.map +1 -1
  28. package/dist/factories/apisix/compositions/apisix-bootstrap.js +3 -2
  29. package/dist/factories/apisix/compositions/apisix-bootstrap.js.map +1 -1
  30. package/dist/factories/clickstack/utils/helm-values-mapper.js.map +1 -1
  31. package/dist/factories/ory/utils/helm-values-mapper.d.ts.map +1 -1
  32. package/dist/factories/ory/utils/helm-values-mapper.js +6 -3
  33. package/dist/factories/ory/utils/helm-values-mapper.js.map +1 -1
  34. package/dist/factories/rook/compositions/rook-ceph-platform.d.ts +104 -0
  35. package/dist/factories/rook/compositions/rook-ceph-platform.d.ts.map +1 -1
  36. package/dist/factories/rook/types.d.ts +104 -0
  37. package/dist/factories/rook/types.d.ts.map +1 -1
  38. package/dist/factories/rook/types.js +9 -0
  39. package/dist/factories/rook/types.js.map +1 -1
  40. package/dist/factories/rook/utils/helm-values-mapper.d.ts +1 -1
  41. package/dist/factories/rook/utils/helm-values-mapper.d.ts.map +1 -1
  42. package/dist/factories/rook/utils/helm-values-mapper.js +19 -7
  43. package/dist/factories/rook/utils/helm-values-mapper.js.map +1 -1
  44. 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 tempCtx = createCompositionContext('defaults-extraction');
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
- return hasResults ? { defaults, ternaryConditionals, crossFieldOverrides } : undefined;
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 hasDeeperAbsent = [...absent].some((a) => a.startsWith(`${full}.`));
453
- if (hasDeeperAbsent && v && (typeof v === 'object' || typeof v === 'function')) {
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
- const tailCel = tail.kind === 'literal'
489
- ? typeof tail.value === 'string'
490
- ? `"${escapeCelString(tail.value)}"`
491
- : String(tail.value)
492
- : tail.kind === 'field'
493
- ? celField(tail.field)
494
- : 'omit()';
495
- let expr = tailCel;
496
- let nested = false;
497
- for (const f of [...chain].reverse()) {
498
- expr = `${presenceGuard(f)} ? ${celField(f)} : ${nested ? `(${expr})` : expr}`;
499
- nested = true;
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
- delete defaults[head];
542
- out.push({ resourceId, path, cel: buildChainCel(chain, tail) });
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