@o3co/ts.hocon 1.5.0 → 1.6.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/zod.cjs CHANGED
@@ -329,6 +329,252 @@ function mergeUnresolved(receiver, fallback) {
329
329
  return result;
330
330
  }
331
331
 
332
+ // src/internal/resolver/fold-self-ref.ts
333
+ function substFullKey(s) {
334
+ return stringSegmentsToKey(s.segments.map((seg) => seg.text));
335
+ }
336
+ function stringSegmentsToKey(segments) {
337
+ return segments.map((t) => {
338
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
339
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
340
+ }
341
+ return t;
342
+ }).join(".");
343
+ }
344
+ function containsSelfRef(v, fullKey) {
345
+ if (isSubst(v)) return !v.knownAbsent && substFullKey(v) === fullKey;
346
+ if (isConcat(v)) return v.nodes.some((n) => containsSelfRef(n, fullKey));
347
+ if (isAppend(v)) {
348
+ return containsSelfRef(v.existing, fullKey) || containsSelfRef(v.elem, fullKey);
349
+ }
350
+ if (isResObj(v)) {
351
+ for (const f of v.fields.values()) {
352
+ if (containsSelfRef(f, fullKey)) return true;
353
+ }
354
+ return false;
355
+ }
356
+ const hv = v;
357
+ if (hv.kind === "array") {
358
+ return hv.items.some((item) => containsSelfRef(item, fullKey));
359
+ }
360
+ if (hv.kind === "object") {
361
+ for (const f of hv.fields.values()) {
362
+ if (containsSelfRef(f, fullKey)) return true;
363
+ }
364
+ return false;
365
+ }
366
+ return false;
367
+ }
368
+ function foldSelfRef(v, fullKey, replacement) {
369
+ if (isSubst(v)) {
370
+ return substFullKey(v) === fullKey ? replacement : v;
371
+ }
372
+ if (isConcat(v)) {
373
+ return {
374
+ _kind: "concat-placeholder",
375
+ nodes: v.nodes.map((n) => foldSelfRef(n, fullKey, replacement)),
376
+ line: v.line,
377
+ col: v.col
378
+ };
379
+ }
380
+ if (isAppend(v)) {
381
+ return {
382
+ _kind: "append-placeholder",
383
+ existing: foldSelfRef(v.existing, fullKey, replacement),
384
+ elem: foldSelfRef(v.elem, fullKey, replacement)
385
+ };
386
+ }
387
+ if (isResObj(v)) {
388
+ const newFields = /* @__PURE__ */ new Map();
389
+ for (const [k, val] of v.fields) {
390
+ newFields.set(k, foldSelfRef(val, fullKey, replacement));
391
+ }
392
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
393
+ }
394
+ const hv = v;
395
+ if (hv.kind === "array") {
396
+ return {
397
+ kind: "array",
398
+ items: hv.items.map((item) => foldSelfRef(item, fullKey, replacement))
399
+ };
400
+ }
401
+ if (hv.kind === "object") {
402
+ const newFields = /* @__PURE__ */ new Map();
403
+ for (const [k, val] of hv.fields) {
404
+ newFields.set(k, foldSelfRef(val, fullKey, replacement));
405
+ }
406
+ return { kind: "object", fields: newFields };
407
+ }
408
+ return v;
409
+ }
410
+ function foldOrSkipPrior(prior, fullKey, old) {
411
+ if (!containsSelfRef(prior, fullKey)) return cloneResolverValue(prior);
412
+ if (old === void 0) return foldOptionalSelfRefAbsent(prior, fullKey);
413
+ return foldSelfRef(prior, fullKey, old);
414
+ }
415
+ function foldOptionalSelfRefAbsent(v, fullKey) {
416
+ if (isSubst(v) && substFullKey(v) === fullKey) {
417
+ if (!v.optional) return void 0;
418
+ return {
419
+ _kind: "subst-placeholder",
420
+ segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
421
+ optional: v.optional,
422
+ knownAbsent: true,
423
+ listSuffix: v.listSuffix,
424
+ line: v.line,
425
+ col: v.col,
426
+ prefixLen: v.prefixLen
427
+ };
428
+ }
429
+ if (isConcat(v)) {
430
+ const nodes = [];
431
+ for (const node of v.nodes) {
432
+ const folded = foldOptionalSelfRefAbsent(node, fullKey);
433
+ if (folded === void 0) return void 0;
434
+ nodes.push(folded);
435
+ }
436
+ return { _kind: "concat-placeholder", nodes, line: v.line, col: v.col };
437
+ }
438
+ if (isAppend(v)) {
439
+ const existing = foldOptionalSelfRefAbsent(v.existing, fullKey);
440
+ const elem = foldOptionalSelfRefAbsent(v.elem, fullKey);
441
+ if (existing === void 0 || elem === void 0) return void 0;
442
+ return { _kind: "append-placeholder", existing, elem };
443
+ }
444
+ if (isResObj(v)) {
445
+ const fields = /* @__PURE__ */ new Map();
446
+ for (const [key, value] of v.fields) {
447
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
448
+ if (folded === void 0) return void 0;
449
+ fields.set(key, folded);
450
+ }
451
+ return { _kind: "res-obj", fields, priorValues: new Map(v.priorValues) };
452
+ }
453
+ const hv = v;
454
+ if (hv.kind === "array") {
455
+ const items = [];
456
+ for (const item of hv.items) {
457
+ const folded = foldOptionalSelfRefAbsent(item, fullKey);
458
+ if (folded === void 0) return void 0;
459
+ items.push(folded);
460
+ }
461
+ return { kind: "array", items };
462
+ }
463
+ if (hv.kind === "object") {
464
+ const fields = /* @__PURE__ */ new Map();
465
+ for (const [key, value] of hv.fields) {
466
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
467
+ if (folded === void 0) return void 0;
468
+ fields.set(key, folded);
469
+ }
470
+ return { kind: "object", fields };
471
+ }
472
+ return cloneResolverValue(v);
473
+ }
474
+ function cloneResolverValue(v) {
475
+ if (isSubst(v)) {
476
+ return {
477
+ _kind: "subst-placeholder",
478
+ segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
479
+ optional: v.optional,
480
+ knownAbsent: v.knownAbsent,
481
+ listSuffix: v.listSuffix,
482
+ line: v.line,
483
+ col: v.col,
484
+ prefixLen: v.prefixLen
485
+ };
486
+ }
487
+ if (isConcat(v)) {
488
+ return {
489
+ _kind: "concat-placeholder",
490
+ nodes: v.nodes.map((n) => cloneResolverValue(n)),
491
+ line: v.line,
492
+ col: v.col
493
+ };
494
+ }
495
+ if (isResObj(v)) {
496
+ const newFields = /* @__PURE__ */ new Map();
497
+ for (const [k, val] of v.fields) newFields.set(k, cloneResolverValue(val));
498
+ const newPriors = /* @__PURE__ */ new Map();
499
+ for (const [k, val] of v.priorValues) newPriors.set(k, cloneResolverValue(val));
500
+ return { _kind: "res-obj", fields: newFields, priorValues: newPriors };
501
+ }
502
+ if (isAppend(v)) {
503
+ return {
504
+ _kind: "append-placeholder",
505
+ existing: cloneResolverValue(v.existing),
506
+ elem: cloneResolverValue(v.elem)
507
+ };
508
+ }
509
+ const hv = v;
510
+ if (hv.kind === "array") {
511
+ return {
512
+ kind: "array",
513
+ items: hv.items.map((item) => cloneResolverValue(item))
514
+ };
515
+ }
516
+ if (hv.kind === "object") {
517
+ const newFields = /* @__PURE__ */ new Map();
518
+ for (const [k, val] of hv.fields) {
519
+ newFields.set(k, cloneResolverValue(val));
520
+ }
521
+ return { kind: "object", fields: newFields };
522
+ }
523
+ return hv;
524
+ }
525
+ function foldNestedSelfRefs(v, pathPrefix) {
526
+ if (!isResObj(v)) return v;
527
+ const newFields = /* @__PURE__ */ new Map();
528
+ for (const [k, fieldVal] of v.fields) {
529
+ const childPath = [...pathPrefix, k];
530
+ const fullKey = stringSegmentsToKey(childPath);
531
+ const folded = foldNestedSelfRefs(fieldVal, childPath);
532
+ let finalVal = folded;
533
+ if (containsSelfRef(folded, fullKey)) {
534
+ const leafPrior = v.priorValues.get(k);
535
+ if (leafPrior !== void 0) {
536
+ const leafPriorFolded = foldNestedSelfRefs(leafPrior, childPath);
537
+ finalVal = foldSelfRef(folded, fullKey, leafPriorFolded);
538
+ }
539
+ }
540
+ newFields.set(k, finalVal);
541
+ }
542
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
543
+ }
544
+ function containsSubstByPath(v, target) {
545
+ if (isSubst(v)) return !v.knownAbsent && segmentsTextEqual(v.segments, target);
546
+ if (isConcat(v)) return v.nodes.some((n) => containsSubstByPath(n, target));
547
+ if (isAppend(v)) {
548
+ return containsSubstByPath(v.existing, target) || containsSubstByPath(v.elem, target);
549
+ }
550
+ if (isResObj(v)) {
551
+ for (const f of v.fields.values()) {
552
+ if (containsSubstByPath(f, target)) return true;
553
+ }
554
+ return false;
555
+ }
556
+ const hv = v;
557
+ if (hv.kind === "array") {
558
+ return hv.items.some((item) => containsSubstByPath(item, target));
559
+ }
560
+ if (hv.kind === "object") {
561
+ for (const f of hv.fields.values()) {
562
+ if (containsSubstByPath(f, target)) return true;
563
+ }
564
+ return false;
565
+ }
566
+ return false;
567
+ }
568
+ function segmentsTextEqual(a, b) {
569
+ if (a.length !== b.length) return false;
570
+ for (let i = 0; i < a.length; i++) {
571
+ const ai = a[i];
572
+ const bi = b[i];
573
+ if (ai === void 0 || bi === void 0 || ai.text !== bi.text) return false;
574
+ }
575
+ return true;
576
+ }
577
+
332
578
  // src/internal/resolver/utils.ts
333
579
  function segmentsToKey(segments) {
334
580
  return segments.map((s) => {
@@ -370,13 +616,22 @@ function deepMergeHoconValues(base, overlay) {
370
616
  }
371
617
  return { kind: "object", fields: merged };
372
618
  }
373
- function deepMergeResObjInto(dst, src) {
619
+ function deepMergeResObjInto(dst, src, pathPrefix = []) {
374
620
  for (const [k, srcVal] of src.fields) {
621
+ const childPrefix = [...pathPrefix, k];
622
+ const fullKey = stringSegmentsToKey(childPrefix);
375
623
  const dstVal = dst.fields.get(k);
376
624
  if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
377
- deepMergeResObjInto(dstVal, srcVal);
625
+ const priorExisting = dst.priorValues.get(k);
626
+ const prior = foldOrSkipPrior(dstVal, fullKey, priorExisting);
627
+ if (prior !== void 0) dst.priorValues.set(k, prior);
628
+ deepMergeResObjInto(dstVal, srcVal, childPrefix);
378
629
  } else {
379
- if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
630
+ if (dstVal !== void 0) {
631
+ const priorExisting = dst.priorValues.get(k);
632
+ const prior = foldOrSkipPrior(dstVal, fullKey, priorExisting);
633
+ if (prior !== void 0) dst.priorValues.set(k, prior);
634
+ }
380
635
  dst.fields.set(k, srcVal);
381
636
  }
382
637
  }
@@ -454,6 +709,11 @@ var Lexer = class {
454
709
  this.line = 1;
455
710
  this.col = 1;
456
711
  this.hadSpace = false;
712
+ // Accumulates literal whitespace chars consumed between tokens. Reset on every
713
+ // token push. Used so the parser can preserve path-expression whitespace
714
+ // (xx.hocon#42, E13) — e.g. tab vs space adjacent to dots in key paths.
715
+ // Comment text is NOT accumulated; only the actual WS chars.
716
+ this.whitespaceBuffer = "";
457
717
  this.tokens = [];
458
718
  if (input.charCodeAt(0) === 65279) this.input = input.slice(1);
459
719
  }
@@ -469,6 +729,7 @@ var Lexer = class {
469
729
  continue;
470
730
  }
471
731
  if (isHoconWhitespace(ch)) {
732
+ this.whitespaceBuffer += ch;
472
733
  this.advance();
473
734
  this.hadSpace = true;
474
735
  continue;
@@ -560,7 +821,7 @@ var Lexer = class {
560
821
  }
561
822
  throw new ParseError(`unexpected character: ${JSON.stringify(ch)}`, sl, sc);
562
823
  }
563
- this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false });
824
+ this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false, precedingWhitespace: "" });
564
825
  return this.tokens;
565
826
  }
566
827
  peek(offset = 0) {
@@ -577,12 +838,14 @@ var Lexer = class {
577
838
  return ch;
578
839
  }
579
840
  push(kind, value, l, c, isQuoted = false) {
580
- this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
841
+ this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer });
581
842
  this.hadSpace = false;
843
+ this.whitespaceBuffer = "";
582
844
  }
583
845
  pushSubst(payload, value, l, c) {
584
- this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
846
+ this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer, subst: payload });
585
847
  this.hadSpace = false;
848
+ this.whitespaceBuffer = "";
586
849
  }
587
850
  /**
588
851
  * Read body of a quoted string. Opening `"` must already be consumed.
@@ -828,7 +1091,7 @@ function isUnquotedContinue(ch, nextFn) {
828
1091
  }
829
1092
 
830
1093
  // src/internal/parser/parser.ts
831
- var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
1094
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, precedingWhitespace: "", subst: void 0 };
832
1095
  var Parser = class {
833
1096
  constructor(tokens) {
834
1097
  this.tokens = tokens;
@@ -941,13 +1204,17 @@ var Parser = class {
941
1204
  let trailingDot = false;
942
1205
  let firstWasQuoted = false;
943
1206
  let spaceConcat = false;
1207
+ let postDotPrefix = "";
944
1208
  while (true) {
945
1209
  const t = this.peek();
946
1210
  if (t.kind === "string") {
947
1211
  this.advance();
948
1212
  if (segments.length === 0) firstWasQuoted = true;
949
1213
  if (spaceConcat) {
950
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${t.value}`;
1214
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}${t.value}`;
1215
+ } else if (postDotPrefix !== "") {
1216
+ segments.push(`${postDotPrefix}${t.value}`);
1217
+ postDotPrefix = "";
951
1218
  } else {
952
1219
  segments.push(t.value);
953
1220
  }
@@ -957,24 +1224,23 @@ var Parser = class {
957
1224
  const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
958
1225
  const parts = raw.split(".");
959
1226
  const filtered = parts.filter((s) => s.length > 0);
960
- for (const seg of filtered) {
961
- if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
962
- const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
963
- throw new ParseError(
964
- `unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
965
- t.line,
966
- t.col
967
- );
968
- }
969
- }
970
- if (spaceConcat && filtered.length > 0) {
1227
+ if (spaceConcat) {
1228
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}`;
971
1229
  if (raw.startsWith(".")) {
972
1230
  segments.push(...filtered);
973
- } else {
1231
+ } else if (filtered.length > 0) {
974
1232
  const [head, ...tail] = filtered;
975
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${head}`;
1233
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${head}`;
976
1234
  segments.push(...tail);
977
1235
  }
1236
+ } else if (postDotPrefix !== "" && raw.startsWith(".")) {
1237
+ segments.push(postDotPrefix);
1238
+ postDotPrefix = "";
1239
+ segments.push(...filtered);
1240
+ } else if (postDotPrefix !== "" && filtered.length > 0) {
1241
+ const [head, ...tail] = filtered;
1242
+ segments.push(`${postDotPrefix}${head}`, ...tail);
1243
+ postDotPrefix = "";
978
1244
  } else {
979
1245
  segments.push(...filtered);
980
1246
  }
@@ -984,11 +1250,23 @@ var Parser = class {
984
1250
  break;
985
1251
  }
986
1252
  spaceConcat = false;
987
- if (trailingDot) continue;
1253
+ if (trailingDot) {
1254
+ const next2 = this.peek();
1255
+ if ((next2.kind === "unquoted" || next2.kind === "string") && next2.precedingWhitespace.length > 0) {
1256
+ postDotPrefix = next2.precedingWhitespace;
1257
+ } else {
1258
+ postDotPrefix = "";
1259
+ }
1260
+ continue;
1261
+ }
988
1262
  const next = this.peek();
989
1263
  if (next.kind === "unquoted" && next.value === "." && !next.precedingSpace) {
990
1264
  this.advance();
991
1265
  trailingDot = true;
1266
+ const afterDot = this.peek();
1267
+ if ((afterDot.kind === "unquoted" || afterDot.kind === "string") && afterDot.precedingWhitespace.length > 0) {
1268
+ postDotPrefix = afterDot.precedingWhitespace;
1269
+ }
992
1270
  continue;
993
1271
  }
994
1272
  if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
@@ -1001,6 +1279,14 @@ var Parser = class {
1001
1279
  }
1002
1280
  break;
1003
1281
  }
1282
+ if (trailingDot) {
1283
+ const here = this.peek();
1284
+ throw new ParseError(
1285
+ `path has a trailing period '.' \u2014 empty key segment not allowed (HOCON.md path rules)`,
1286
+ here.line,
1287
+ here.col
1288
+ );
1289
+ }
1004
1290
  return { segments, firstWasQuoted };
1005
1291
  }
1006
1292
  parseInclude() {
@@ -1173,7 +1459,8 @@ var Parser = class {
1173
1459
  break;
1174
1460
  }
1175
1461
  if (hadSpace) {
1176
- parts.push({ kind: "scalar", raw: " ", valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1462
+ const sep = t.precedingWhitespace.length > 0 ? t.precedingWhitespace : " ";
1463
+ parts.push({ kind: "scalar", raw: sep, valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1177
1464
  }
1178
1465
  parts.push(node);
1179
1466
  }
@@ -1387,7 +1674,7 @@ var IncludeLoader = class {
1387
1674
  for (const ext of probeExts) {
1388
1675
  const obj = this.loadSingle(`${absPath}${ext}`);
1389
1676
  if (obj !== void 0) {
1390
- deepMergeResObjInto(merged, obj);
1677
+ deepMergeResObjInto(merged, obj, []);
1391
1678
  foundAny = true;
1392
1679
  }
1393
1680
  }
@@ -1420,7 +1707,7 @@ var IncludeLoader = class {
1420
1707
  for (const ext of probeExts) {
1421
1708
  const obj = await this.loadSingleAsync(`${absPath}${ext}`);
1422
1709
  if (obj !== void 0) {
1423
- deepMergeResObjInto(merged, obj);
1710
+ deepMergeResObjInto(merged, obj, []);
1424
1711
  foundAny = true;
1425
1712
  }
1426
1713
  }
@@ -1592,7 +1879,7 @@ var StructureBuilder = class _StructureBuilder {
1592
1879
  if (pathPrefix.length > 0) {
1593
1880
  this.relativizeResObj(included, pathPrefix);
1594
1881
  }
1595
- deepMergeResObjInto(obj, included);
1882
+ deepMergeResObjInto(obj, included, pathPrefix);
1596
1883
  return;
1597
1884
  }
1598
1885
  const [head, ...tail] = field.key;
@@ -1607,12 +1894,27 @@ var StructureBuilder = class _StructureBuilder {
1607
1894
  return;
1608
1895
  }
1609
1896
  const childPrefix = [...pathPrefix, head];
1897
+ const fullKey = stringSegmentsToKey(childPrefix);
1610
1898
  if (field.append) {
1611
1899
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1612
- obj.priorValues.set(head, existing2);
1900
+ const oldPrior = obj.priorValues.get(head);
1901
+ const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1902
+ if (folded !== void 0) obj.priorValues.set(head, folded);
1613
1903
  obj.fields.set(head, {
1614
1904
  _kind: "append-placeholder",
1615
- existing: existing2,
1905
+ // Use the folded existing (self-ref-free) for the Append. When the
1906
+ // fold returned undefined (self-ref present but no old prior),
1907
+ // fall back to the original existing — resolveSubst will then
1908
+ // raise the "self-ref with no prior value" error at resolve time,
1909
+ // matching the pre-fix behaviour for that edge case.
1910
+ //
1911
+ // Clone `folded` again so the Append.existing and priorValues[head]
1912
+ // do not share the same Subst/Concat references — relativizeResObj
1913
+ // walks BOTH `fields` and `priorValues` and mutates Subst.segments
1914
+ // in place, so a shared reference would get its prefix applied
1915
+ // twice (resulting in e.g. `${outer.outer.base}` instead of
1916
+ // `${outer.base}`). Codex review on PR #131.
1917
+ existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1616
1918
  elem: this.astToResolverValue(field.value, childPrefix)
1617
1919
  });
1618
1920
  return;
@@ -1620,10 +1922,13 @@ var StructureBuilder = class _StructureBuilder {
1620
1922
  const existing = obj.fields.get(head);
1621
1923
  const newVal = this.astToResolverValue(field.value, childPrefix);
1622
1924
  if (existing !== void 0) {
1623
- obj.priorValues.set(head, existing);
1925
+ const oldPrior = obj.priorValues.get(head);
1926
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1927
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1928
+ if (prior !== void 0) obj.priorValues.set(head, prior);
1624
1929
  }
1625
1930
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1626
- deepMergeResObjInto(existing, newVal);
1931
+ deepMergeResObjInto(existing, newVal, childPrefix);
1627
1932
  return;
1628
1933
  }
1629
1934
  obj.fields.set(head, newVal);
@@ -1640,7 +1945,7 @@ var StructureBuilder = class _StructureBuilder {
1640
1945
  if (pathPrefix.length > 0) {
1641
1946
  this.relativizeResObj(included, pathPrefix);
1642
1947
  }
1643
- deepMergeResObjInto(obj, included);
1948
+ deepMergeResObjInto(obj, included, pathPrefix);
1644
1949
  return;
1645
1950
  }
1646
1951
  const [head, ...tail] = field.key;
@@ -1655,12 +1960,15 @@ var StructureBuilder = class _StructureBuilder {
1655
1960
  return;
1656
1961
  }
1657
1962
  const childPrefix = [...pathPrefix, head];
1963
+ const fullKey = stringSegmentsToKey(childPrefix);
1658
1964
  if (field.append) {
1659
1965
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1660
- obj.priorValues.set(head, existing2);
1966
+ const oldPrior = obj.priorValues.get(head);
1967
+ const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1968
+ if (folded !== void 0) obj.priorValues.set(head, folded);
1661
1969
  obj.fields.set(head, {
1662
1970
  _kind: "append-placeholder",
1663
- existing: existing2,
1971
+ existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1664
1972
  elem: await this.astToResolverValueAsync(field.value, childPrefix)
1665
1973
  });
1666
1974
  return;
@@ -1668,10 +1976,13 @@ var StructureBuilder = class _StructureBuilder {
1668
1976
  const existing = obj.fields.get(head);
1669
1977
  const newVal = await this.astToResolverValueAsync(field.value, childPrefix);
1670
1978
  if (existing !== void 0) {
1671
- obj.priorValues.set(head, existing);
1979
+ const oldPrior = obj.priorValues.get(head);
1980
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1981
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1982
+ if (prior !== void 0) obj.priorValues.set(head, prior);
1672
1983
  }
1673
1984
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1674
- deepMergeResObjInto(existing, newVal);
1985
+ deepMergeResObjInto(existing, newVal, childPrefix);
1675
1986
  return;
1676
1987
  }
1677
1988
  obj.fields.set(head, newVal);
@@ -1690,7 +2001,7 @@ var StructureBuilder = class _StructureBuilder {
1690
2001
  return inner;
1691
2002
  }
1692
2003
  case "subst":
1693
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
2004
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, knownAbsent: false, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1694
2005
  case "concat":
1695
2006
  return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
1696
2007
  case "include":
@@ -1714,7 +2025,7 @@ var StructureBuilder = class _StructureBuilder {
1714
2025
  case "object":
1715
2026
  return await this.buildAsync(ast, pathPrefix);
1716
2027
  case "subst":
1717
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
2028
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, knownAbsent: false, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1718
2029
  case "concat": {
1719
2030
  const nodes = [];
1720
2031
  for (const n of ast.nodes) {
@@ -1803,6 +2114,23 @@ var SubstitutionResolver = class {
1803
2114
  // narrower than path-equality. Spec amendment deferred to a follow-up
1804
2115
  // xx.hocon PR (see Phase 6 #3f close-out notes).
1805
2116
  this.resolvingConcats = /* @__PURE__ */ new WeakSet();
2117
+ // Path stack tracking the full dotted path of the field currently being
2118
+ // assigned in `resolveResObj`. Leaf keys are pushed on entry to each
2119
+ // `(key, val)` iteration and popped on exit, so nested objects build up
2120
+ // the full path (e.g. `["o", "history"]` while resolving `o.history`).
2121
+ //
2122
+ // Used to tighten self-ref detection at the L192 found-lookup site:
2123
+ // a substitution `${X}` whose looked-up value contains a Subst pointing
2124
+ // at `X` is only a true self-reference when the field currently being
2125
+ // assigned is `X` (or a descendant of `X`). Without this guard, an
2126
+ // external lookup like `b = ${o}` (where o has a self-referential
2127
+ // field) would mis-fire and short-circuit to o's prior, returning the
2128
+ // wrong value for b. The is_owner gate (rfp prefix-matches s.segments)
2129
+ // narrows the check to the owner case only.
2130
+ //
2131
+ // Cross-impl with rs.hocon v1.5.1's resolving_field_path (same purpose,
2132
+ // same prefix-match semantics).
2133
+ this.resolvingFieldPath = [];
1806
2134
  }
1807
2135
  /** Returns a prefix for error messages, including origin info when set. */
1808
2136
  originPrefix() {
@@ -1814,35 +2142,57 @@ var SubstitutionResolver = class {
1814
2142
  resolveResObj(obj) {
1815
2143
  const result = /* @__PURE__ */ new Map();
1816
2144
  for (const [key, val] of obj.fields) {
1817
- const resolved = this.resolveVal(val, obj);
2145
+ this.resolvingFieldPath.push(key);
2146
+ const fullCacheKey = stringSegmentsToKey(this.resolvingFieldPath);
2147
+ this.cache.delete(fullCacheKey);
2148
+ let resolved;
2149
+ try {
2150
+ resolved = this.resolveVal(val, obj);
2151
+ } finally {
2152
+ this.resolvingFieldPath.pop();
2153
+ }
1818
2154
  if (resolved !== void 0) {
1819
2155
  if (resolved.kind === "object") {
1820
2156
  const prior = obj.priorValues.get(key);
1821
2157
  if (prior !== void 0) {
1822
2158
  const priorResolved = this.resolveVal(prior, obj);
1823
2159
  if (priorResolved !== void 0 && priorResolved.kind === "object") {
1824
- result.set(
1825
- key,
1826
- deepMergeHoconValues(
1827
- priorResolved,
1828
- resolved
1829
- )
2160
+ const finalValue = deepMergeHoconValues(
2161
+ priorResolved,
2162
+ resolved
1830
2163
  );
2164
+ result.set(key, finalValue);
2165
+ this.cache.set(fullCacheKey, finalValue);
2166
+ this.cacheDescendants(fullCacheKey, finalValue);
1831
2167
  continue;
1832
2168
  }
1833
2169
  }
1834
2170
  }
1835
2171
  result.set(key, resolved);
2172
+ this.cache.set(fullCacheKey, resolved);
2173
+ this.cacheDescendants(fullCacheKey, resolved);
1836
2174
  } else {
1837
2175
  const prior = obj.priorValues.get(key);
1838
2176
  if (prior !== void 0) {
1839
2177
  const priorResolved = this.resolveVal(prior, obj);
1840
- if (priorResolved !== void 0) result.set(key, priorResolved);
2178
+ if (priorResolved !== void 0) {
2179
+ result.set(key, priorResolved);
2180
+ this.cache.set(fullCacheKey, priorResolved);
2181
+ this.cacheDescendants(fullCacheKey, priorResolved);
2182
+ }
1841
2183
  }
1842
2184
  }
1843
2185
  }
1844
2186
  return { kind: "object", fields: result };
1845
2187
  }
2188
+ cacheDescendants(prefix, value) {
2189
+ if (value.kind !== "object") return;
2190
+ for (const [key, child] of value.fields) {
2191
+ const childKey = `${prefix}.${stringSegmentsToKey([key])}`;
2192
+ this.cache.set(childKey, child);
2193
+ this.cacheDescendants(childKey, child);
2194
+ }
2195
+ }
1846
2196
  resolveVal(v, scope) {
1847
2197
  if (isSubst(v)) return this.resolveSubst(v, scope);
1848
2198
  if (isConcat(v)) {
@@ -1868,6 +2218,7 @@ var SubstitutionResolver = class {
1868
2218
  return hv;
1869
2219
  }
1870
2220
  resolveSubst(s, scope) {
2221
+ if (s.knownAbsent) return void 0;
1871
2222
  const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
1872
2223
  if (this.cache.has(key)) return this.cache.get(key);
1873
2224
  if (this.resolving.has(key)) {
@@ -1904,7 +2255,10 @@ var SubstitutionResolver = class {
1904
2255
  try {
1905
2256
  const found = lookupPath(this.root, s.segments);
1906
2257
  if (found !== void 0) {
1907
- if (isConcat(found) && this.resolvingConcats.has(found)) {
2258
+ const rfp = this.resolvingFieldPath;
2259
+ const isOwner = rfp.length >= s.segments.length && s.segments.every((seg, i) => rfp[i] === seg.text);
2260
+ const isSelfRef = isConcat(found) && this.resolvingConcats.has(found) || isOwner && containsSubstByPath(found, s.segments);
2261
+ if (isSelfRef) {
1908
2262
  let prior;
1909
2263
  if (s.segments.length > 1) {
1910
2264
  const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
@@ -1957,6 +2311,28 @@ var SubstitutionResolver = class {
1957
2311
  if (result !== void 0) this.cache.set(key, result);
1958
2312
  return result;
1959
2313
  }
2314
+ if (s.prefixLen > 0 && s.segments.length > s.prefixLen) {
2315
+ const originalSegments = s.segments.slice(s.prefixLen);
2316
+ const fallbackFound = lookupPath(this.root, originalSegments);
2317
+ if (fallbackFound !== void 0) {
2318
+ let result = this.resolveVal(fallbackFound, scope);
2319
+ if (originalSegments.length === 1 && result !== void 0 && result.kind === "object") {
2320
+ const rootSeg = originalSegments[0]?.text ?? "";
2321
+ const prior = this.root.priorValues.get(rootSeg);
2322
+ if (prior !== void 0) {
2323
+ const priorResolved = this.resolveVal(prior, scope);
2324
+ if (priorResolved !== void 0 && priorResolved.kind === "object") {
2325
+ result = deepMergeHoconValues(
2326
+ priorResolved,
2327
+ result
2328
+ );
2329
+ }
2330
+ }
2331
+ }
2332
+ if (result !== void 0) this.cache.set(key, result);
2333
+ return result;
2334
+ }
2335
+ }
1960
2336
  if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
1961
2337
  const result = this.resolveEnvList(s);
1962
2338
  if (result !== void 0) {