@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/index.cjs CHANGED
@@ -338,6 +338,252 @@ function mergeUnresolved(receiver, fallback) {
338
338
  return result;
339
339
  }
340
340
 
341
+ // src/internal/resolver/fold-self-ref.ts
342
+ function substFullKey(s) {
343
+ return stringSegmentsToKey(s.segments.map((seg) => seg.text));
344
+ }
345
+ function stringSegmentsToKey(segments) {
346
+ return segments.map((t) => {
347
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
348
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
349
+ }
350
+ return t;
351
+ }).join(".");
352
+ }
353
+ function containsSelfRef(v, fullKey) {
354
+ if (isSubst(v)) return !v.knownAbsent && substFullKey(v) === fullKey;
355
+ if (isConcat(v)) return v.nodes.some((n) => containsSelfRef(n, fullKey));
356
+ if (isAppend(v)) {
357
+ return containsSelfRef(v.existing, fullKey) || containsSelfRef(v.elem, fullKey);
358
+ }
359
+ if (isResObj(v)) {
360
+ for (const f of v.fields.values()) {
361
+ if (containsSelfRef(f, fullKey)) return true;
362
+ }
363
+ return false;
364
+ }
365
+ const hv = v;
366
+ if (hv.kind === "array") {
367
+ return hv.items.some((item) => containsSelfRef(item, fullKey));
368
+ }
369
+ if (hv.kind === "object") {
370
+ for (const f of hv.fields.values()) {
371
+ if (containsSelfRef(f, fullKey)) return true;
372
+ }
373
+ return false;
374
+ }
375
+ return false;
376
+ }
377
+ function foldSelfRef(v, fullKey, replacement) {
378
+ if (isSubst(v)) {
379
+ return substFullKey(v) === fullKey ? replacement : v;
380
+ }
381
+ if (isConcat(v)) {
382
+ return {
383
+ _kind: "concat-placeholder",
384
+ nodes: v.nodes.map((n) => foldSelfRef(n, fullKey, replacement)),
385
+ line: v.line,
386
+ col: v.col
387
+ };
388
+ }
389
+ if (isAppend(v)) {
390
+ return {
391
+ _kind: "append-placeholder",
392
+ existing: foldSelfRef(v.existing, fullKey, replacement),
393
+ elem: foldSelfRef(v.elem, fullKey, replacement)
394
+ };
395
+ }
396
+ if (isResObj(v)) {
397
+ const newFields = /* @__PURE__ */ new Map();
398
+ for (const [k, val] of v.fields) {
399
+ newFields.set(k, foldSelfRef(val, fullKey, replacement));
400
+ }
401
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
402
+ }
403
+ const hv = v;
404
+ if (hv.kind === "array") {
405
+ return {
406
+ kind: "array",
407
+ items: hv.items.map((item) => foldSelfRef(item, fullKey, replacement))
408
+ };
409
+ }
410
+ if (hv.kind === "object") {
411
+ const newFields = /* @__PURE__ */ new Map();
412
+ for (const [k, val] of hv.fields) {
413
+ newFields.set(k, foldSelfRef(val, fullKey, replacement));
414
+ }
415
+ return { kind: "object", fields: newFields };
416
+ }
417
+ return v;
418
+ }
419
+ function foldOrSkipPrior(prior, fullKey, old) {
420
+ if (!containsSelfRef(prior, fullKey)) return cloneResolverValue(prior);
421
+ if (old === void 0) return foldOptionalSelfRefAbsent(prior, fullKey);
422
+ return foldSelfRef(prior, fullKey, old);
423
+ }
424
+ function foldOptionalSelfRefAbsent(v, fullKey) {
425
+ if (isSubst(v) && substFullKey(v) === fullKey) {
426
+ if (!v.optional) return void 0;
427
+ return {
428
+ _kind: "subst-placeholder",
429
+ segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
430
+ optional: v.optional,
431
+ knownAbsent: true,
432
+ listSuffix: v.listSuffix,
433
+ line: v.line,
434
+ col: v.col,
435
+ prefixLen: v.prefixLen
436
+ };
437
+ }
438
+ if (isConcat(v)) {
439
+ const nodes = [];
440
+ for (const node of v.nodes) {
441
+ const folded = foldOptionalSelfRefAbsent(node, fullKey);
442
+ if (folded === void 0) return void 0;
443
+ nodes.push(folded);
444
+ }
445
+ return { _kind: "concat-placeholder", nodes, line: v.line, col: v.col };
446
+ }
447
+ if (isAppend(v)) {
448
+ const existing = foldOptionalSelfRefAbsent(v.existing, fullKey);
449
+ const elem = foldOptionalSelfRefAbsent(v.elem, fullKey);
450
+ if (existing === void 0 || elem === void 0) return void 0;
451
+ return { _kind: "append-placeholder", existing, elem };
452
+ }
453
+ if (isResObj(v)) {
454
+ const fields = /* @__PURE__ */ new Map();
455
+ for (const [key, value] of v.fields) {
456
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
457
+ if (folded === void 0) return void 0;
458
+ fields.set(key, folded);
459
+ }
460
+ return { _kind: "res-obj", fields, priorValues: new Map(v.priorValues) };
461
+ }
462
+ const hv = v;
463
+ if (hv.kind === "array") {
464
+ const items = [];
465
+ for (const item of hv.items) {
466
+ const folded = foldOptionalSelfRefAbsent(item, fullKey);
467
+ if (folded === void 0) return void 0;
468
+ items.push(folded);
469
+ }
470
+ return { kind: "array", items };
471
+ }
472
+ if (hv.kind === "object") {
473
+ const fields = /* @__PURE__ */ new Map();
474
+ for (const [key, value] of hv.fields) {
475
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
476
+ if (folded === void 0) return void 0;
477
+ fields.set(key, folded);
478
+ }
479
+ return { kind: "object", fields };
480
+ }
481
+ return cloneResolverValue(v);
482
+ }
483
+ function cloneResolverValue(v) {
484
+ if (isSubst(v)) {
485
+ return {
486
+ _kind: "subst-placeholder",
487
+ segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
488
+ optional: v.optional,
489
+ knownAbsent: v.knownAbsent,
490
+ listSuffix: v.listSuffix,
491
+ line: v.line,
492
+ col: v.col,
493
+ prefixLen: v.prefixLen
494
+ };
495
+ }
496
+ if (isConcat(v)) {
497
+ return {
498
+ _kind: "concat-placeholder",
499
+ nodes: v.nodes.map((n) => cloneResolverValue(n)),
500
+ line: v.line,
501
+ col: v.col
502
+ };
503
+ }
504
+ if (isResObj(v)) {
505
+ const newFields = /* @__PURE__ */ new Map();
506
+ for (const [k, val] of v.fields) newFields.set(k, cloneResolverValue(val));
507
+ const newPriors = /* @__PURE__ */ new Map();
508
+ for (const [k, val] of v.priorValues) newPriors.set(k, cloneResolverValue(val));
509
+ return { _kind: "res-obj", fields: newFields, priorValues: newPriors };
510
+ }
511
+ if (isAppend(v)) {
512
+ return {
513
+ _kind: "append-placeholder",
514
+ existing: cloneResolverValue(v.existing),
515
+ elem: cloneResolverValue(v.elem)
516
+ };
517
+ }
518
+ const hv = v;
519
+ if (hv.kind === "array") {
520
+ return {
521
+ kind: "array",
522
+ items: hv.items.map((item) => cloneResolverValue(item))
523
+ };
524
+ }
525
+ if (hv.kind === "object") {
526
+ const newFields = /* @__PURE__ */ new Map();
527
+ for (const [k, val] of hv.fields) {
528
+ newFields.set(k, cloneResolverValue(val));
529
+ }
530
+ return { kind: "object", fields: newFields };
531
+ }
532
+ return hv;
533
+ }
534
+ function foldNestedSelfRefs(v, pathPrefix) {
535
+ if (!isResObj(v)) return v;
536
+ const newFields = /* @__PURE__ */ new Map();
537
+ for (const [k, fieldVal] of v.fields) {
538
+ const childPath = [...pathPrefix, k];
539
+ const fullKey = stringSegmentsToKey(childPath);
540
+ const folded = foldNestedSelfRefs(fieldVal, childPath);
541
+ let finalVal = folded;
542
+ if (containsSelfRef(folded, fullKey)) {
543
+ const leafPrior = v.priorValues.get(k);
544
+ if (leafPrior !== void 0) {
545
+ const leafPriorFolded = foldNestedSelfRefs(leafPrior, childPath);
546
+ finalVal = foldSelfRef(folded, fullKey, leafPriorFolded);
547
+ }
548
+ }
549
+ newFields.set(k, finalVal);
550
+ }
551
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
552
+ }
553
+ function containsSubstByPath(v, target) {
554
+ if (isSubst(v)) return !v.knownAbsent && segmentsTextEqual(v.segments, target);
555
+ if (isConcat(v)) return v.nodes.some((n) => containsSubstByPath(n, target));
556
+ if (isAppend(v)) {
557
+ return containsSubstByPath(v.existing, target) || containsSubstByPath(v.elem, target);
558
+ }
559
+ if (isResObj(v)) {
560
+ for (const f of v.fields.values()) {
561
+ if (containsSubstByPath(f, target)) return true;
562
+ }
563
+ return false;
564
+ }
565
+ const hv = v;
566
+ if (hv.kind === "array") {
567
+ return hv.items.some((item) => containsSubstByPath(item, target));
568
+ }
569
+ if (hv.kind === "object") {
570
+ for (const f of hv.fields.values()) {
571
+ if (containsSubstByPath(f, target)) return true;
572
+ }
573
+ return false;
574
+ }
575
+ return false;
576
+ }
577
+ function segmentsTextEqual(a, b) {
578
+ if (a.length !== b.length) return false;
579
+ for (let i = 0; i < a.length; i++) {
580
+ const ai = a[i];
581
+ const bi = b[i];
582
+ if (ai === void 0 || bi === void 0 || ai.text !== bi.text) return false;
583
+ }
584
+ return true;
585
+ }
586
+
341
587
  // src/internal/resolver/utils.ts
342
588
  function segmentsToKey(segments) {
343
589
  return segments.map((s) => {
@@ -379,13 +625,22 @@ function deepMergeHoconValues(base, overlay) {
379
625
  }
380
626
  return { kind: "object", fields: merged };
381
627
  }
382
- function deepMergeResObjInto(dst, src) {
628
+ function deepMergeResObjInto(dst, src, pathPrefix = []) {
383
629
  for (const [k, srcVal] of src.fields) {
630
+ const childPrefix = [...pathPrefix, k];
631
+ const fullKey = stringSegmentsToKey(childPrefix);
384
632
  const dstVal = dst.fields.get(k);
385
633
  if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
386
- deepMergeResObjInto(dstVal, srcVal);
634
+ const priorExisting = dst.priorValues.get(k);
635
+ const prior = foldOrSkipPrior(dstVal, fullKey, priorExisting);
636
+ if (prior !== void 0) dst.priorValues.set(k, prior);
637
+ deepMergeResObjInto(dstVal, srcVal, childPrefix);
387
638
  } else {
388
- if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
639
+ if (dstVal !== void 0) {
640
+ const priorExisting = dst.priorValues.get(k);
641
+ const prior = foldOrSkipPrior(dstVal, fullKey, priorExisting);
642
+ if (prior !== void 0) dst.priorValues.set(k, prior);
643
+ }
389
644
  dst.fields.set(k, srcVal);
390
645
  }
391
646
  }
@@ -463,6 +718,11 @@ var Lexer = class {
463
718
  this.line = 1;
464
719
  this.col = 1;
465
720
  this.hadSpace = false;
721
+ // Accumulates literal whitespace chars consumed between tokens. Reset on every
722
+ // token push. Used so the parser can preserve path-expression whitespace
723
+ // (xx.hocon#42, E13) — e.g. tab vs space adjacent to dots in key paths.
724
+ // Comment text is NOT accumulated; only the actual WS chars.
725
+ this.whitespaceBuffer = "";
466
726
  this.tokens = [];
467
727
  if (input.charCodeAt(0) === 65279) this.input = input.slice(1);
468
728
  }
@@ -478,6 +738,7 @@ var Lexer = class {
478
738
  continue;
479
739
  }
480
740
  if (isHoconWhitespace(ch)) {
741
+ this.whitespaceBuffer += ch;
481
742
  this.advance();
482
743
  this.hadSpace = true;
483
744
  continue;
@@ -569,7 +830,7 @@ var Lexer = class {
569
830
  }
570
831
  throw new ParseError(`unexpected character: ${JSON.stringify(ch)}`, sl, sc);
571
832
  }
572
- this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false });
833
+ this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false, precedingWhitespace: "" });
573
834
  return this.tokens;
574
835
  }
575
836
  peek(offset = 0) {
@@ -586,12 +847,14 @@ var Lexer = class {
586
847
  return ch;
587
848
  }
588
849
  push(kind, value, l, c, isQuoted = false) {
589
- this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
850
+ this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer });
590
851
  this.hadSpace = false;
852
+ this.whitespaceBuffer = "";
591
853
  }
592
854
  pushSubst(payload, value, l, c) {
593
- this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
855
+ this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer, subst: payload });
594
856
  this.hadSpace = false;
857
+ this.whitespaceBuffer = "";
595
858
  }
596
859
  /**
597
860
  * Read body of a quoted string. Opening `"` must already be consumed.
@@ -837,7 +1100,7 @@ function isUnquotedContinue(ch, nextFn) {
837
1100
  }
838
1101
 
839
1102
  // src/internal/parser/parser.ts
840
- var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
1103
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, precedingWhitespace: "", subst: void 0 };
841
1104
  var Parser = class {
842
1105
  constructor(tokens) {
843
1106
  this.tokens = tokens;
@@ -950,13 +1213,17 @@ var Parser = class {
950
1213
  let trailingDot = false;
951
1214
  let firstWasQuoted = false;
952
1215
  let spaceConcat = false;
1216
+ let postDotPrefix = "";
953
1217
  while (true) {
954
1218
  const t = this.peek();
955
1219
  if (t.kind === "string") {
956
1220
  this.advance();
957
1221
  if (segments.length === 0) firstWasQuoted = true;
958
1222
  if (spaceConcat) {
959
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${t.value}`;
1223
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}${t.value}`;
1224
+ } else if (postDotPrefix !== "") {
1225
+ segments.push(`${postDotPrefix}${t.value}`);
1226
+ postDotPrefix = "";
960
1227
  } else {
961
1228
  segments.push(t.value);
962
1229
  }
@@ -966,24 +1233,23 @@ var Parser = class {
966
1233
  const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
967
1234
  const parts = raw.split(".");
968
1235
  const filtered = parts.filter((s) => s.length > 0);
969
- for (const seg of filtered) {
970
- if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
971
- const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
972
- throw new ParseError(
973
- `unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
974
- t.line,
975
- t.col
976
- );
977
- }
978
- }
979
- if (spaceConcat && filtered.length > 0) {
1236
+ if (spaceConcat) {
1237
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}`;
980
1238
  if (raw.startsWith(".")) {
981
1239
  segments.push(...filtered);
982
- } else {
1240
+ } else if (filtered.length > 0) {
983
1241
  const [head, ...tail] = filtered;
984
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${head}`;
1242
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${head}`;
985
1243
  segments.push(...tail);
986
1244
  }
1245
+ } else if (postDotPrefix !== "" && raw.startsWith(".")) {
1246
+ segments.push(postDotPrefix);
1247
+ postDotPrefix = "";
1248
+ segments.push(...filtered);
1249
+ } else if (postDotPrefix !== "" && filtered.length > 0) {
1250
+ const [head, ...tail] = filtered;
1251
+ segments.push(`${postDotPrefix}${head}`, ...tail);
1252
+ postDotPrefix = "";
987
1253
  } else {
988
1254
  segments.push(...filtered);
989
1255
  }
@@ -993,11 +1259,23 @@ var Parser = class {
993
1259
  break;
994
1260
  }
995
1261
  spaceConcat = false;
996
- if (trailingDot) continue;
1262
+ if (trailingDot) {
1263
+ const next2 = this.peek();
1264
+ if ((next2.kind === "unquoted" || next2.kind === "string") && next2.precedingWhitespace.length > 0) {
1265
+ postDotPrefix = next2.precedingWhitespace;
1266
+ } else {
1267
+ postDotPrefix = "";
1268
+ }
1269
+ continue;
1270
+ }
997
1271
  const next = this.peek();
998
1272
  if (next.kind === "unquoted" && next.value === "." && !next.precedingSpace) {
999
1273
  this.advance();
1000
1274
  trailingDot = true;
1275
+ const afterDot = this.peek();
1276
+ if ((afterDot.kind === "unquoted" || afterDot.kind === "string") && afterDot.precedingWhitespace.length > 0) {
1277
+ postDotPrefix = afterDot.precedingWhitespace;
1278
+ }
1001
1279
  continue;
1002
1280
  }
1003
1281
  if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
@@ -1010,6 +1288,14 @@ var Parser = class {
1010
1288
  }
1011
1289
  break;
1012
1290
  }
1291
+ if (trailingDot) {
1292
+ const here = this.peek();
1293
+ throw new ParseError(
1294
+ `path has a trailing period '.' \u2014 empty key segment not allowed (HOCON.md path rules)`,
1295
+ here.line,
1296
+ here.col
1297
+ );
1298
+ }
1013
1299
  return { segments, firstWasQuoted };
1014
1300
  }
1015
1301
  parseInclude() {
@@ -1182,7 +1468,8 @@ var Parser = class {
1182
1468
  break;
1183
1469
  }
1184
1470
  if (hadSpace) {
1185
- parts.push({ kind: "scalar", raw: " ", valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1471
+ const sep = t.precedingWhitespace.length > 0 ? t.precedingWhitespace : " ";
1472
+ parts.push({ kind: "scalar", raw: sep, valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1186
1473
  }
1187
1474
  parts.push(node);
1188
1475
  }
@@ -1396,7 +1683,7 @@ var IncludeLoader = class {
1396
1683
  for (const ext of probeExts) {
1397
1684
  const obj = this.loadSingle(`${absPath}${ext}`);
1398
1685
  if (obj !== void 0) {
1399
- deepMergeResObjInto(merged, obj);
1686
+ deepMergeResObjInto(merged, obj, []);
1400
1687
  foundAny = true;
1401
1688
  }
1402
1689
  }
@@ -1429,7 +1716,7 @@ var IncludeLoader = class {
1429
1716
  for (const ext of probeExts) {
1430
1717
  const obj = await this.loadSingleAsync(`${absPath}${ext}`);
1431
1718
  if (obj !== void 0) {
1432
- deepMergeResObjInto(merged, obj);
1719
+ deepMergeResObjInto(merged, obj, []);
1433
1720
  foundAny = true;
1434
1721
  }
1435
1722
  }
@@ -1601,7 +1888,7 @@ var StructureBuilder = class _StructureBuilder {
1601
1888
  if (pathPrefix.length > 0) {
1602
1889
  this.relativizeResObj(included, pathPrefix);
1603
1890
  }
1604
- deepMergeResObjInto(obj, included);
1891
+ deepMergeResObjInto(obj, included, pathPrefix);
1605
1892
  return;
1606
1893
  }
1607
1894
  const [head, ...tail] = field.key;
@@ -1616,12 +1903,27 @@ var StructureBuilder = class _StructureBuilder {
1616
1903
  return;
1617
1904
  }
1618
1905
  const childPrefix = [...pathPrefix, head];
1906
+ const fullKey = stringSegmentsToKey(childPrefix);
1619
1907
  if (field.append) {
1620
1908
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1621
- obj.priorValues.set(head, existing2);
1909
+ const oldPrior = obj.priorValues.get(head);
1910
+ const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1911
+ if (folded !== void 0) obj.priorValues.set(head, folded);
1622
1912
  obj.fields.set(head, {
1623
1913
  _kind: "append-placeholder",
1624
- existing: existing2,
1914
+ // Use the folded existing (self-ref-free) for the Append. When the
1915
+ // fold returned undefined (self-ref present but no old prior),
1916
+ // fall back to the original existing — resolveSubst will then
1917
+ // raise the "self-ref with no prior value" error at resolve time,
1918
+ // matching the pre-fix behaviour for that edge case.
1919
+ //
1920
+ // Clone `folded` again so the Append.existing and priorValues[head]
1921
+ // do not share the same Subst/Concat references — relativizeResObj
1922
+ // walks BOTH `fields` and `priorValues` and mutates Subst.segments
1923
+ // in place, so a shared reference would get its prefix applied
1924
+ // twice (resulting in e.g. `${outer.outer.base}` instead of
1925
+ // `${outer.base}`). Codex review on PR #131.
1926
+ existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1625
1927
  elem: this.astToResolverValue(field.value, childPrefix)
1626
1928
  });
1627
1929
  return;
@@ -1629,10 +1931,13 @@ var StructureBuilder = class _StructureBuilder {
1629
1931
  const existing = obj.fields.get(head);
1630
1932
  const newVal = this.astToResolverValue(field.value, childPrefix);
1631
1933
  if (existing !== void 0) {
1632
- obj.priorValues.set(head, existing);
1934
+ const oldPrior = obj.priorValues.get(head);
1935
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1936
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1937
+ if (prior !== void 0) obj.priorValues.set(head, prior);
1633
1938
  }
1634
1939
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1635
- deepMergeResObjInto(existing, newVal);
1940
+ deepMergeResObjInto(existing, newVal, childPrefix);
1636
1941
  return;
1637
1942
  }
1638
1943
  obj.fields.set(head, newVal);
@@ -1649,7 +1954,7 @@ var StructureBuilder = class _StructureBuilder {
1649
1954
  if (pathPrefix.length > 0) {
1650
1955
  this.relativizeResObj(included, pathPrefix);
1651
1956
  }
1652
- deepMergeResObjInto(obj, included);
1957
+ deepMergeResObjInto(obj, included, pathPrefix);
1653
1958
  return;
1654
1959
  }
1655
1960
  const [head, ...tail] = field.key;
@@ -1664,12 +1969,15 @@ var StructureBuilder = class _StructureBuilder {
1664
1969
  return;
1665
1970
  }
1666
1971
  const childPrefix = [...pathPrefix, head];
1972
+ const fullKey = stringSegmentsToKey(childPrefix);
1667
1973
  if (field.append) {
1668
1974
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1669
- obj.priorValues.set(head, existing2);
1975
+ const oldPrior = obj.priorValues.get(head);
1976
+ const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1977
+ if (folded !== void 0) obj.priorValues.set(head, folded);
1670
1978
  obj.fields.set(head, {
1671
1979
  _kind: "append-placeholder",
1672
- existing: existing2,
1980
+ existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1673
1981
  elem: await this.astToResolverValueAsync(field.value, childPrefix)
1674
1982
  });
1675
1983
  return;
@@ -1677,10 +1985,13 @@ var StructureBuilder = class _StructureBuilder {
1677
1985
  const existing = obj.fields.get(head);
1678
1986
  const newVal = await this.astToResolverValueAsync(field.value, childPrefix);
1679
1987
  if (existing !== void 0) {
1680
- obj.priorValues.set(head, existing);
1988
+ const oldPrior = obj.priorValues.get(head);
1989
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1990
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1991
+ if (prior !== void 0) obj.priorValues.set(head, prior);
1681
1992
  }
1682
1993
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1683
- deepMergeResObjInto(existing, newVal);
1994
+ deepMergeResObjInto(existing, newVal, childPrefix);
1684
1995
  return;
1685
1996
  }
1686
1997
  obj.fields.set(head, newVal);
@@ -1699,7 +2010,7 @@ var StructureBuilder = class _StructureBuilder {
1699
2010
  return inner;
1700
2011
  }
1701
2012
  case "subst":
1702
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
2013
+ 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 };
1703
2014
  case "concat":
1704
2015
  return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
1705
2016
  case "include":
@@ -1723,7 +2034,7 @@ var StructureBuilder = class _StructureBuilder {
1723
2034
  case "object":
1724
2035
  return await this.buildAsync(ast, pathPrefix);
1725
2036
  case "subst":
1726
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
2037
+ 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 };
1727
2038
  case "concat": {
1728
2039
  const nodes = [];
1729
2040
  for (const n of ast.nodes) {
@@ -1812,6 +2123,23 @@ var SubstitutionResolver = class {
1812
2123
  // narrower than path-equality. Spec amendment deferred to a follow-up
1813
2124
  // xx.hocon PR (see Phase 6 #3f close-out notes).
1814
2125
  this.resolvingConcats = /* @__PURE__ */ new WeakSet();
2126
+ // Path stack tracking the full dotted path of the field currently being
2127
+ // assigned in `resolveResObj`. Leaf keys are pushed on entry to each
2128
+ // `(key, val)` iteration and popped on exit, so nested objects build up
2129
+ // the full path (e.g. `["o", "history"]` while resolving `o.history`).
2130
+ //
2131
+ // Used to tighten self-ref detection at the L192 found-lookup site:
2132
+ // a substitution `${X}` whose looked-up value contains a Subst pointing
2133
+ // at `X` is only a true self-reference when the field currently being
2134
+ // assigned is `X` (or a descendant of `X`). Without this guard, an
2135
+ // external lookup like `b = ${o}` (where o has a self-referential
2136
+ // field) would mis-fire and short-circuit to o's prior, returning the
2137
+ // wrong value for b. The is_owner gate (rfp prefix-matches s.segments)
2138
+ // narrows the check to the owner case only.
2139
+ //
2140
+ // Cross-impl with rs.hocon v1.5.1's resolving_field_path (same purpose,
2141
+ // same prefix-match semantics).
2142
+ this.resolvingFieldPath = [];
1815
2143
  }
1816
2144
  /** Returns a prefix for error messages, including origin info when set. */
1817
2145
  originPrefix() {
@@ -1823,35 +2151,57 @@ var SubstitutionResolver = class {
1823
2151
  resolveResObj(obj) {
1824
2152
  const result = /* @__PURE__ */ new Map();
1825
2153
  for (const [key, val] of obj.fields) {
1826
- const resolved = this.resolveVal(val, obj);
2154
+ this.resolvingFieldPath.push(key);
2155
+ const fullCacheKey = stringSegmentsToKey(this.resolvingFieldPath);
2156
+ this.cache.delete(fullCacheKey);
2157
+ let resolved;
2158
+ try {
2159
+ resolved = this.resolveVal(val, obj);
2160
+ } finally {
2161
+ this.resolvingFieldPath.pop();
2162
+ }
1827
2163
  if (resolved !== void 0) {
1828
2164
  if (resolved.kind === "object") {
1829
2165
  const prior = obj.priorValues.get(key);
1830
2166
  if (prior !== void 0) {
1831
2167
  const priorResolved = this.resolveVal(prior, obj);
1832
2168
  if (priorResolved !== void 0 && priorResolved.kind === "object") {
1833
- result.set(
1834
- key,
1835
- deepMergeHoconValues(
1836
- priorResolved,
1837
- resolved
1838
- )
2169
+ const finalValue = deepMergeHoconValues(
2170
+ priorResolved,
2171
+ resolved
1839
2172
  );
2173
+ result.set(key, finalValue);
2174
+ this.cache.set(fullCacheKey, finalValue);
2175
+ this.cacheDescendants(fullCacheKey, finalValue);
1840
2176
  continue;
1841
2177
  }
1842
2178
  }
1843
2179
  }
1844
2180
  result.set(key, resolved);
2181
+ this.cache.set(fullCacheKey, resolved);
2182
+ this.cacheDescendants(fullCacheKey, resolved);
1845
2183
  } else {
1846
2184
  const prior = obj.priorValues.get(key);
1847
2185
  if (prior !== void 0) {
1848
2186
  const priorResolved = this.resolveVal(prior, obj);
1849
- if (priorResolved !== void 0) result.set(key, priorResolved);
2187
+ if (priorResolved !== void 0) {
2188
+ result.set(key, priorResolved);
2189
+ this.cache.set(fullCacheKey, priorResolved);
2190
+ this.cacheDescendants(fullCacheKey, priorResolved);
2191
+ }
1850
2192
  }
1851
2193
  }
1852
2194
  }
1853
2195
  return { kind: "object", fields: result };
1854
2196
  }
2197
+ cacheDescendants(prefix, value) {
2198
+ if (value.kind !== "object") return;
2199
+ for (const [key, child] of value.fields) {
2200
+ const childKey = `${prefix}.${stringSegmentsToKey([key])}`;
2201
+ this.cache.set(childKey, child);
2202
+ this.cacheDescendants(childKey, child);
2203
+ }
2204
+ }
1855
2205
  resolveVal(v, scope) {
1856
2206
  if (isSubst(v)) return this.resolveSubst(v, scope);
1857
2207
  if (isConcat(v)) {
@@ -1877,6 +2227,7 @@ var SubstitutionResolver = class {
1877
2227
  return hv;
1878
2228
  }
1879
2229
  resolveSubst(s, scope) {
2230
+ if (s.knownAbsent) return void 0;
1880
2231
  const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
1881
2232
  if (this.cache.has(key)) return this.cache.get(key);
1882
2233
  if (this.resolving.has(key)) {
@@ -1913,7 +2264,10 @@ var SubstitutionResolver = class {
1913
2264
  try {
1914
2265
  const found = lookupPath(this.root, s.segments);
1915
2266
  if (found !== void 0) {
1916
- if (isConcat(found) && this.resolvingConcats.has(found)) {
2267
+ const rfp = this.resolvingFieldPath;
2268
+ const isOwner = rfp.length >= s.segments.length && s.segments.every((seg, i) => rfp[i] === seg.text);
2269
+ const isSelfRef = isConcat(found) && this.resolvingConcats.has(found) || isOwner && containsSubstByPath(found, s.segments);
2270
+ if (isSelfRef) {
1917
2271
  let prior;
1918
2272
  if (s.segments.length > 1) {
1919
2273
  const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
@@ -1966,6 +2320,28 @@ var SubstitutionResolver = class {
1966
2320
  if (result !== void 0) this.cache.set(key, result);
1967
2321
  return result;
1968
2322
  }
2323
+ if (s.prefixLen > 0 && s.segments.length > s.prefixLen) {
2324
+ const originalSegments = s.segments.slice(s.prefixLen);
2325
+ const fallbackFound = lookupPath(this.root, originalSegments);
2326
+ if (fallbackFound !== void 0) {
2327
+ let result = this.resolveVal(fallbackFound, scope);
2328
+ if (originalSegments.length === 1 && result !== void 0 && result.kind === "object") {
2329
+ const rootSeg = originalSegments[0]?.text ?? "";
2330
+ const prior = this.root.priorValues.get(rootSeg);
2331
+ if (prior !== void 0) {
2332
+ const priorResolved = this.resolveVal(prior, scope);
2333
+ if (priorResolved !== void 0 && priorResolved.kind === "object") {
2334
+ result = deepMergeHoconValues(
2335
+ priorResolved,
2336
+ result
2337
+ );
2338
+ }
2339
+ }
2340
+ }
2341
+ if (result !== void 0) this.cache.set(key, result);
2342
+ return result;
2343
+ }
2344
+ }
1969
2345
  if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
1970
2346
  const result = this.resolveEnvList(s);
1971
2347
  if (result !== void 0) {