@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.js CHANGED
@@ -290,6 +290,252 @@ function mergeUnresolved(receiver, fallback) {
290
290
  return result;
291
291
  }
292
292
 
293
+ // src/internal/resolver/fold-self-ref.ts
294
+ function substFullKey(s) {
295
+ return stringSegmentsToKey(s.segments.map((seg) => seg.text));
296
+ }
297
+ function stringSegmentsToKey(segments) {
298
+ return segments.map((t) => {
299
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
300
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
301
+ }
302
+ return t;
303
+ }).join(".");
304
+ }
305
+ function containsSelfRef(v, fullKey) {
306
+ if (isSubst(v)) return !v.knownAbsent && substFullKey(v) === fullKey;
307
+ if (isConcat(v)) return v.nodes.some((n) => containsSelfRef(n, fullKey));
308
+ if (isAppend(v)) {
309
+ return containsSelfRef(v.existing, fullKey) || containsSelfRef(v.elem, fullKey);
310
+ }
311
+ if (isResObj(v)) {
312
+ for (const f of v.fields.values()) {
313
+ if (containsSelfRef(f, fullKey)) return true;
314
+ }
315
+ return false;
316
+ }
317
+ const hv = v;
318
+ if (hv.kind === "array") {
319
+ return hv.items.some((item) => containsSelfRef(item, fullKey));
320
+ }
321
+ if (hv.kind === "object") {
322
+ for (const f of hv.fields.values()) {
323
+ if (containsSelfRef(f, fullKey)) return true;
324
+ }
325
+ return false;
326
+ }
327
+ return false;
328
+ }
329
+ function foldSelfRef(v, fullKey, replacement) {
330
+ if (isSubst(v)) {
331
+ return substFullKey(v) === fullKey ? replacement : v;
332
+ }
333
+ if (isConcat(v)) {
334
+ return {
335
+ _kind: "concat-placeholder",
336
+ nodes: v.nodes.map((n) => foldSelfRef(n, fullKey, replacement)),
337
+ line: v.line,
338
+ col: v.col
339
+ };
340
+ }
341
+ if (isAppend(v)) {
342
+ return {
343
+ _kind: "append-placeholder",
344
+ existing: foldSelfRef(v.existing, fullKey, replacement),
345
+ elem: foldSelfRef(v.elem, fullKey, replacement)
346
+ };
347
+ }
348
+ if (isResObj(v)) {
349
+ const newFields = /* @__PURE__ */ new Map();
350
+ for (const [k, val] of v.fields) {
351
+ newFields.set(k, foldSelfRef(val, fullKey, replacement));
352
+ }
353
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
354
+ }
355
+ const hv = v;
356
+ if (hv.kind === "array") {
357
+ return {
358
+ kind: "array",
359
+ items: hv.items.map((item) => foldSelfRef(item, fullKey, replacement))
360
+ };
361
+ }
362
+ if (hv.kind === "object") {
363
+ const newFields = /* @__PURE__ */ new Map();
364
+ for (const [k, val] of hv.fields) {
365
+ newFields.set(k, foldSelfRef(val, fullKey, replacement));
366
+ }
367
+ return { kind: "object", fields: newFields };
368
+ }
369
+ return v;
370
+ }
371
+ function foldOrSkipPrior(prior, fullKey, old) {
372
+ if (!containsSelfRef(prior, fullKey)) return cloneResolverValue(prior);
373
+ if (old === void 0) return foldOptionalSelfRefAbsent(prior, fullKey);
374
+ return foldSelfRef(prior, fullKey, old);
375
+ }
376
+ function foldOptionalSelfRefAbsent(v, fullKey) {
377
+ if (isSubst(v) && substFullKey(v) === fullKey) {
378
+ if (!v.optional) return void 0;
379
+ return {
380
+ _kind: "subst-placeholder",
381
+ segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
382
+ optional: v.optional,
383
+ knownAbsent: true,
384
+ listSuffix: v.listSuffix,
385
+ line: v.line,
386
+ col: v.col,
387
+ prefixLen: v.prefixLen
388
+ };
389
+ }
390
+ if (isConcat(v)) {
391
+ const nodes = [];
392
+ for (const node of v.nodes) {
393
+ const folded = foldOptionalSelfRefAbsent(node, fullKey);
394
+ if (folded === void 0) return void 0;
395
+ nodes.push(folded);
396
+ }
397
+ return { _kind: "concat-placeholder", nodes, line: v.line, col: v.col };
398
+ }
399
+ if (isAppend(v)) {
400
+ const existing = foldOptionalSelfRefAbsent(v.existing, fullKey);
401
+ const elem = foldOptionalSelfRefAbsent(v.elem, fullKey);
402
+ if (existing === void 0 || elem === void 0) return void 0;
403
+ return { _kind: "append-placeholder", existing, elem };
404
+ }
405
+ if (isResObj(v)) {
406
+ const fields = /* @__PURE__ */ new Map();
407
+ for (const [key, value] of v.fields) {
408
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
409
+ if (folded === void 0) return void 0;
410
+ fields.set(key, folded);
411
+ }
412
+ return { _kind: "res-obj", fields, priorValues: new Map(v.priorValues) };
413
+ }
414
+ const hv = v;
415
+ if (hv.kind === "array") {
416
+ const items = [];
417
+ for (const item of hv.items) {
418
+ const folded = foldOptionalSelfRefAbsent(item, fullKey);
419
+ if (folded === void 0) return void 0;
420
+ items.push(folded);
421
+ }
422
+ return { kind: "array", items };
423
+ }
424
+ if (hv.kind === "object") {
425
+ const fields = /* @__PURE__ */ new Map();
426
+ for (const [key, value] of hv.fields) {
427
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
428
+ if (folded === void 0) return void 0;
429
+ fields.set(key, folded);
430
+ }
431
+ return { kind: "object", fields };
432
+ }
433
+ return cloneResolverValue(v);
434
+ }
435
+ function cloneResolverValue(v) {
436
+ if (isSubst(v)) {
437
+ return {
438
+ _kind: "subst-placeholder",
439
+ segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
440
+ optional: v.optional,
441
+ knownAbsent: v.knownAbsent,
442
+ listSuffix: v.listSuffix,
443
+ line: v.line,
444
+ col: v.col,
445
+ prefixLen: v.prefixLen
446
+ };
447
+ }
448
+ if (isConcat(v)) {
449
+ return {
450
+ _kind: "concat-placeholder",
451
+ nodes: v.nodes.map((n) => cloneResolverValue(n)),
452
+ line: v.line,
453
+ col: v.col
454
+ };
455
+ }
456
+ if (isResObj(v)) {
457
+ const newFields = /* @__PURE__ */ new Map();
458
+ for (const [k, val] of v.fields) newFields.set(k, cloneResolverValue(val));
459
+ const newPriors = /* @__PURE__ */ new Map();
460
+ for (const [k, val] of v.priorValues) newPriors.set(k, cloneResolverValue(val));
461
+ return { _kind: "res-obj", fields: newFields, priorValues: newPriors };
462
+ }
463
+ if (isAppend(v)) {
464
+ return {
465
+ _kind: "append-placeholder",
466
+ existing: cloneResolverValue(v.existing),
467
+ elem: cloneResolverValue(v.elem)
468
+ };
469
+ }
470
+ const hv = v;
471
+ if (hv.kind === "array") {
472
+ return {
473
+ kind: "array",
474
+ items: hv.items.map((item) => cloneResolverValue(item))
475
+ };
476
+ }
477
+ if (hv.kind === "object") {
478
+ const newFields = /* @__PURE__ */ new Map();
479
+ for (const [k, val] of hv.fields) {
480
+ newFields.set(k, cloneResolverValue(val));
481
+ }
482
+ return { kind: "object", fields: newFields };
483
+ }
484
+ return hv;
485
+ }
486
+ function foldNestedSelfRefs(v, pathPrefix) {
487
+ if (!isResObj(v)) return v;
488
+ const newFields = /* @__PURE__ */ new Map();
489
+ for (const [k, fieldVal] of v.fields) {
490
+ const childPath = [...pathPrefix, k];
491
+ const fullKey = stringSegmentsToKey(childPath);
492
+ const folded = foldNestedSelfRefs(fieldVal, childPath);
493
+ let finalVal = folded;
494
+ if (containsSelfRef(folded, fullKey)) {
495
+ const leafPrior = v.priorValues.get(k);
496
+ if (leafPrior !== void 0) {
497
+ const leafPriorFolded = foldNestedSelfRefs(leafPrior, childPath);
498
+ finalVal = foldSelfRef(folded, fullKey, leafPriorFolded);
499
+ }
500
+ }
501
+ newFields.set(k, finalVal);
502
+ }
503
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
504
+ }
505
+ function containsSubstByPath(v, target) {
506
+ if (isSubst(v)) return !v.knownAbsent && segmentsTextEqual(v.segments, target);
507
+ if (isConcat(v)) return v.nodes.some((n) => containsSubstByPath(n, target));
508
+ if (isAppend(v)) {
509
+ return containsSubstByPath(v.existing, target) || containsSubstByPath(v.elem, target);
510
+ }
511
+ if (isResObj(v)) {
512
+ for (const f of v.fields.values()) {
513
+ if (containsSubstByPath(f, target)) return true;
514
+ }
515
+ return false;
516
+ }
517
+ const hv = v;
518
+ if (hv.kind === "array") {
519
+ return hv.items.some((item) => containsSubstByPath(item, target));
520
+ }
521
+ if (hv.kind === "object") {
522
+ for (const f of hv.fields.values()) {
523
+ if (containsSubstByPath(f, target)) return true;
524
+ }
525
+ return false;
526
+ }
527
+ return false;
528
+ }
529
+ function segmentsTextEqual(a, b) {
530
+ if (a.length !== b.length) return false;
531
+ for (let i = 0; i < a.length; i++) {
532
+ const ai = a[i];
533
+ const bi = b[i];
534
+ if (ai === void 0 || bi === void 0 || ai.text !== bi.text) return false;
535
+ }
536
+ return true;
537
+ }
538
+
293
539
  // src/internal/resolver/utils.ts
294
540
  function segmentsToKey(segments) {
295
541
  return segments.map((s) => {
@@ -331,13 +577,22 @@ function deepMergeHoconValues(base, overlay) {
331
577
  }
332
578
  return { kind: "object", fields: merged };
333
579
  }
334
- function deepMergeResObjInto(dst, src) {
580
+ function deepMergeResObjInto(dst, src, pathPrefix = []) {
335
581
  for (const [k, srcVal] of src.fields) {
582
+ const childPrefix = [...pathPrefix, k];
583
+ const fullKey = stringSegmentsToKey(childPrefix);
336
584
  const dstVal = dst.fields.get(k);
337
585
  if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
338
- deepMergeResObjInto(dstVal, srcVal);
586
+ const priorExisting = dst.priorValues.get(k);
587
+ const prior = foldOrSkipPrior(dstVal, fullKey, priorExisting);
588
+ if (prior !== void 0) dst.priorValues.set(k, prior);
589
+ deepMergeResObjInto(dstVal, srcVal, childPrefix);
339
590
  } else {
340
- if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
591
+ if (dstVal !== void 0) {
592
+ const priorExisting = dst.priorValues.get(k);
593
+ const prior = foldOrSkipPrior(dstVal, fullKey, priorExisting);
594
+ if (prior !== void 0) dst.priorValues.set(k, prior);
595
+ }
341
596
  dst.fields.set(k, srcVal);
342
597
  }
343
598
  }
@@ -415,6 +670,11 @@ var Lexer = class {
415
670
  this.line = 1;
416
671
  this.col = 1;
417
672
  this.hadSpace = false;
673
+ // Accumulates literal whitespace chars consumed between tokens. Reset on every
674
+ // token push. Used so the parser can preserve path-expression whitespace
675
+ // (xx.hocon#42, E13) — e.g. tab vs space adjacent to dots in key paths.
676
+ // Comment text is NOT accumulated; only the actual WS chars.
677
+ this.whitespaceBuffer = "";
418
678
  this.tokens = [];
419
679
  if (input.charCodeAt(0) === 65279) this.input = input.slice(1);
420
680
  }
@@ -430,6 +690,7 @@ var Lexer = class {
430
690
  continue;
431
691
  }
432
692
  if (isHoconWhitespace(ch)) {
693
+ this.whitespaceBuffer += ch;
433
694
  this.advance();
434
695
  this.hadSpace = true;
435
696
  continue;
@@ -521,7 +782,7 @@ var Lexer = class {
521
782
  }
522
783
  throw new ParseError(`unexpected character: ${JSON.stringify(ch)}`, sl, sc);
523
784
  }
524
- this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false });
785
+ this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false, precedingWhitespace: "" });
525
786
  return this.tokens;
526
787
  }
527
788
  peek(offset = 0) {
@@ -538,12 +799,14 @@ var Lexer = class {
538
799
  return ch;
539
800
  }
540
801
  push(kind, value, l, c, isQuoted = false) {
541
- this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
802
+ this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer });
542
803
  this.hadSpace = false;
804
+ this.whitespaceBuffer = "";
543
805
  }
544
806
  pushSubst(payload, value, l, c) {
545
- this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
807
+ this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer, subst: payload });
546
808
  this.hadSpace = false;
809
+ this.whitespaceBuffer = "";
547
810
  }
548
811
  /**
549
812
  * Read body of a quoted string. Opening `"` must already be consumed.
@@ -789,7 +1052,7 @@ function isUnquotedContinue(ch, nextFn) {
789
1052
  }
790
1053
 
791
1054
  // src/internal/parser/parser.ts
792
- var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
1055
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, precedingWhitespace: "", subst: void 0 };
793
1056
  var Parser = class {
794
1057
  constructor(tokens) {
795
1058
  this.tokens = tokens;
@@ -902,13 +1165,17 @@ var Parser = class {
902
1165
  let trailingDot = false;
903
1166
  let firstWasQuoted = false;
904
1167
  let spaceConcat = false;
1168
+ let postDotPrefix = "";
905
1169
  while (true) {
906
1170
  const t = this.peek();
907
1171
  if (t.kind === "string") {
908
1172
  this.advance();
909
1173
  if (segments.length === 0) firstWasQuoted = true;
910
1174
  if (spaceConcat) {
911
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${t.value}`;
1175
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}${t.value}`;
1176
+ } else if (postDotPrefix !== "") {
1177
+ segments.push(`${postDotPrefix}${t.value}`);
1178
+ postDotPrefix = "";
912
1179
  } else {
913
1180
  segments.push(t.value);
914
1181
  }
@@ -918,24 +1185,23 @@ var Parser = class {
918
1185
  const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
919
1186
  const parts = raw.split(".");
920
1187
  const filtered = parts.filter((s) => s.length > 0);
921
- for (const seg of filtered) {
922
- if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
923
- const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
924
- throw new ParseError(
925
- `unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
926
- t.line,
927
- t.col
928
- );
929
- }
930
- }
931
- if (spaceConcat && filtered.length > 0) {
1188
+ if (spaceConcat) {
1189
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}`;
932
1190
  if (raw.startsWith(".")) {
933
1191
  segments.push(...filtered);
934
- } else {
1192
+ } else if (filtered.length > 0) {
935
1193
  const [head, ...tail] = filtered;
936
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${head}`;
1194
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${head}`;
937
1195
  segments.push(...tail);
938
1196
  }
1197
+ } else if (postDotPrefix !== "" && raw.startsWith(".")) {
1198
+ segments.push(postDotPrefix);
1199
+ postDotPrefix = "";
1200
+ segments.push(...filtered);
1201
+ } else if (postDotPrefix !== "" && filtered.length > 0) {
1202
+ const [head, ...tail] = filtered;
1203
+ segments.push(`${postDotPrefix}${head}`, ...tail);
1204
+ postDotPrefix = "";
939
1205
  } else {
940
1206
  segments.push(...filtered);
941
1207
  }
@@ -945,11 +1211,23 @@ var Parser = class {
945
1211
  break;
946
1212
  }
947
1213
  spaceConcat = false;
948
- if (trailingDot) continue;
1214
+ if (trailingDot) {
1215
+ const next2 = this.peek();
1216
+ if ((next2.kind === "unquoted" || next2.kind === "string") && next2.precedingWhitespace.length > 0) {
1217
+ postDotPrefix = next2.precedingWhitespace;
1218
+ } else {
1219
+ postDotPrefix = "";
1220
+ }
1221
+ continue;
1222
+ }
949
1223
  const next = this.peek();
950
1224
  if (next.kind === "unquoted" && next.value === "." && !next.precedingSpace) {
951
1225
  this.advance();
952
1226
  trailingDot = true;
1227
+ const afterDot = this.peek();
1228
+ if ((afterDot.kind === "unquoted" || afterDot.kind === "string") && afterDot.precedingWhitespace.length > 0) {
1229
+ postDotPrefix = afterDot.precedingWhitespace;
1230
+ }
953
1231
  continue;
954
1232
  }
955
1233
  if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
@@ -962,6 +1240,14 @@ var Parser = class {
962
1240
  }
963
1241
  break;
964
1242
  }
1243
+ if (trailingDot) {
1244
+ const here = this.peek();
1245
+ throw new ParseError(
1246
+ `path has a trailing period '.' \u2014 empty key segment not allowed (HOCON.md path rules)`,
1247
+ here.line,
1248
+ here.col
1249
+ );
1250
+ }
965
1251
  return { segments, firstWasQuoted };
966
1252
  }
967
1253
  parseInclude() {
@@ -1134,7 +1420,8 @@ var Parser = class {
1134
1420
  break;
1135
1421
  }
1136
1422
  if (hadSpace) {
1137
- parts.push({ kind: "scalar", raw: " ", valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1423
+ const sep = t.precedingWhitespace.length > 0 ? t.precedingWhitespace : " ";
1424
+ parts.push({ kind: "scalar", raw: sep, valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1138
1425
  }
1139
1426
  parts.push(node);
1140
1427
  }
@@ -1347,7 +1634,7 @@ var IncludeLoader = class {
1347
1634
  for (const ext of probeExts) {
1348
1635
  const obj = this.loadSingle(`${absPath}${ext}`);
1349
1636
  if (obj !== void 0) {
1350
- deepMergeResObjInto(merged, obj);
1637
+ deepMergeResObjInto(merged, obj, []);
1351
1638
  foundAny = true;
1352
1639
  }
1353
1640
  }
@@ -1380,7 +1667,7 @@ var IncludeLoader = class {
1380
1667
  for (const ext of probeExts) {
1381
1668
  const obj = await this.loadSingleAsync(`${absPath}${ext}`);
1382
1669
  if (obj !== void 0) {
1383
- deepMergeResObjInto(merged, obj);
1670
+ deepMergeResObjInto(merged, obj, []);
1384
1671
  foundAny = true;
1385
1672
  }
1386
1673
  }
@@ -1552,7 +1839,7 @@ var StructureBuilder = class _StructureBuilder {
1552
1839
  if (pathPrefix.length > 0) {
1553
1840
  this.relativizeResObj(included, pathPrefix);
1554
1841
  }
1555
- deepMergeResObjInto(obj, included);
1842
+ deepMergeResObjInto(obj, included, pathPrefix);
1556
1843
  return;
1557
1844
  }
1558
1845
  const [head, ...tail] = field.key;
@@ -1567,12 +1854,27 @@ var StructureBuilder = class _StructureBuilder {
1567
1854
  return;
1568
1855
  }
1569
1856
  const childPrefix = [...pathPrefix, head];
1857
+ const fullKey = stringSegmentsToKey(childPrefix);
1570
1858
  if (field.append) {
1571
1859
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1572
- obj.priorValues.set(head, existing2);
1860
+ const oldPrior = obj.priorValues.get(head);
1861
+ const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1862
+ if (folded !== void 0) obj.priorValues.set(head, folded);
1573
1863
  obj.fields.set(head, {
1574
1864
  _kind: "append-placeholder",
1575
- existing: existing2,
1865
+ // Use the folded existing (self-ref-free) for the Append. When the
1866
+ // fold returned undefined (self-ref present but no old prior),
1867
+ // fall back to the original existing — resolveSubst will then
1868
+ // raise the "self-ref with no prior value" error at resolve time,
1869
+ // matching the pre-fix behaviour for that edge case.
1870
+ //
1871
+ // Clone `folded` again so the Append.existing and priorValues[head]
1872
+ // do not share the same Subst/Concat references — relativizeResObj
1873
+ // walks BOTH `fields` and `priorValues` and mutates Subst.segments
1874
+ // in place, so a shared reference would get its prefix applied
1875
+ // twice (resulting in e.g. `${outer.outer.base}` instead of
1876
+ // `${outer.base}`). Codex review on PR #131.
1877
+ existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1576
1878
  elem: this.astToResolverValue(field.value, childPrefix)
1577
1879
  });
1578
1880
  return;
@@ -1580,10 +1882,13 @@ var StructureBuilder = class _StructureBuilder {
1580
1882
  const existing = obj.fields.get(head);
1581
1883
  const newVal = this.astToResolverValue(field.value, childPrefix);
1582
1884
  if (existing !== void 0) {
1583
- obj.priorValues.set(head, existing);
1885
+ const oldPrior = obj.priorValues.get(head);
1886
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1887
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1888
+ if (prior !== void 0) obj.priorValues.set(head, prior);
1584
1889
  }
1585
1890
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1586
- deepMergeResObjInto(existing, newVal);
1891
+ deepMergeResObjInto(existing, newVal, childPrefix);
1587
1892
  return;
1588
1893
  }
1589
1894
  obj.fields.set(head, newVal);
@@ -1600,7 +1905,7 @@ var StructureBuilder = class _StructureBuilder {
1600
1905
  if (pathPrefix.length > 0) {
1601
1906
  this.relativizeResObj(included, pathPrefix);
1602
1907
  }
1603
- deepMergeResObjInto(obj, included);
1908
+ deepMergeResObjInto(obj, included, pathPrefix);
1604
1909
  return;
1605
1910
  }
1606
1911
  const [head, ...tail] = field.key;
@@ -1615,12 +1920,15 @@ var StructureBuilder = class _StructureBuilder {
1615
1920
  return;
1616
1921
  }
1617
1922
  const childPrefix = [...pathPrefix, head];
1923
+ const fullKey = stringSegmentsToKey(childPrefix);
1618
1924
  if (field.append) {
1619
1925
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1620
- obj.priorValues.set(head, existing2);
1926
+ const oldPrior = obj.priorValues.get(head);
1927
+ const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1928
+ if (folded !== void 0) obj.priorValues.set(head, folded);
1621
1929
  obj.fields.set(head, {
1622
1930
  _kind: "append-placeholder",
1623
- existing: existing2,
1931
+ existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1624
1932
  elem: await this.astToResolverValueAsync(field.value, childPrefix)
1625
1933
  });
1626
1934
  return;
@@ -1628,10 +1936,13 @@ var StructureBuilder = class _StructureBuilder {
1628
1936
  const existing = obj.fields.get(head);
1629
1937
  const newVal = await this.astToResolverValueAsync(field.value, childPrefix);
1630
1938
  if (existing !== void 0) {
1631
- obj.priorValues.set(head, existing);
1939
+ const oldPrior = obj.priorValues.get(head);
1940
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1941
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1942
+ if (prior !== void 0) obj.priorValues.set(head, prior);
1632
1943
  }
1633
1944
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1634
- deepMergeResObjInto(existing, newVal);
1945
+ deepMergeResObjInto(existing, newVal, childPrefix);
1635
1946
  return;
1636
1947
  }
1637
1948
  obj.fields.set(head, newVal);
@@ -1650,7 +1961,7 @@ var StructureBuilder = class _StructureBuilder {
1650
1961
  return inner;
1651
1962
  }
1652
1963
  case "subst":
1653
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1964
+ 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 };
1654
1965
  case "concat":
1655
1966
  return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
1656
1967
  case "include":
@@ -1674,7 +1985,7 @@ var StructureBuilder = class _StructureBuilder {
1674
1985
  case "object":
1675
1986
  return await this.buildAsync(ast, pathPrefix);
1676
1987
  case "subst":
1677
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1988
+ 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 };
1678
1989
  case "concat": {
1679
1990
  const nodes = [];
1680
1991
  for (const n of ast.nodes) {
@@ -1763,6 +2074,23 @@ var SubstitutionResolver = class {
1763
2074
  // narrower than path-equality. Spec amendment deferred to a follow-up
1764
2075
  // xx.hocon PR (see Phase 6 #3f close-out notes).
1765
2076
  this.resolvingConcats = /* @__PURE__ */ new WeakSet();
2077
+ // Path stack tracking the full dotted path of the field currently being
2078
+ // assigned in `resolveResObj`. Leaf keys are pushed on entry to each
2079
+ // `(key, val)` iteration and popped on exit, so nested objects build up
2080
+ // the full path (e.g. `["o", "history"]` while resolving `o.history`).
2081
+ //
2082
+ // Used to tighten self-ref detection at the L192 found-lookup site:
2083
+ // a substitution `${X}` whose looked-up value contains a Subst pointing
2084
+ // at `X` is only a true self-reference when the field currently being
2085
+ // assigned is `X` (or a descendant of `X`). Without this guard, an
2086
+ // external lookup like `b = ${o}` (where o has a self-referential
2087
+ // field) would mis-fire and short-circuit to o's prior, returning the
2088
+ // wrong value for b. The is_owner gate (rfp prefix-matches s.segments)
2089
+ // narrows the check to the owner case only.
2090
+ //
2091
+ // Cross-impl with rs.hocon v1.5.1's resolving_field_path (same purpose,
2092
+ // same prefix-match semantics).
2093
+ this.resolvingFieldPath = [];
1766
2094
  }
1767
2095
  /** Returns a prefix for error messages, including origin info when set. */
1768
2096
  originPrefix() {
@@ -1774,35 +2102,57 @@ var SubstitutionResolver = class {
1774
2102
  resolveResObj(obj) {
1775
2103
  const result = /* @__PURE__ */ new Map();
1776
2104
  for (const [key, val] of obj.fields) {
1777
- const resolved = this.resolveVal(val, obj);
2105
+ this.resolvingFieldPath.push(key);
2106
+ const fullCacheKey = stringSegmentsToKey(this.resolvingFieldPath);
2107
+ this.cache.delete(fullCacheKey);
2108
+ let resolved;
2109
+ try {
2110
+ resolved = this.resolveVal(val, obj);
2111
+ } finally {
2112
+ this.resolvingFieldPath.pop();
2113
+ }
1778
2114
  if (resolved !== void 0) {
1779
2115
  if (resolved.kind === "object") {
1780
2116
  const prior = obj.priorValues.get(key);
1781
2117
  if (prior !== void 0) {
1782
2118
  const priorResolved = this.resolveVal(prior, obj);
1783
2119
  if (priorResolved !== void 0 && priorResolved.kind === "object") {
1784
- result.set(
1785
- key,
1786
- deepMergeHoconValues(
1787
- priorResolved,
1788
- resolved
1789
- )
2120
+ const finalValue = deepMergeHoconValues(
2121
+ priorResolved,
2122
+ resolved
1790
2123
  );
2124
+ result.set(key, finalValue);
2125
+ this.cache.set(fullCacheKey, finalValue);
2126
+ this.cacheDescendants(fullCacheKey, finalValue);
1791
2127
  continue;
1792
2128
  }
1793
2129
  }
1794
2130
  }
1795
2131
  result.set(key, resolved);
2132
+ this.cache.set(fullCacheKey, resolved);
2133
+ this.cacheDescendants(fullCacheKey, resolved);
1796
2134
  } else {
1797
2135
  const prior = obj.priorValues.get(key);
1798
2136
  if (prior !== void 0) {
1799
2137
  const priorResolved = this.resolveVal(prior, obj);
1800
- if (priorResolved !== void 0) result.set(key, priorResolved);
2138
+ if (priorResolved !== void 0) {
2139
+ result.set(key, priorResolved);
2140
+ this.cache.set(fullCacheKey, priorResolved);
2141
+ this.cacheDescendants(fullCacheKey, priorResolved);
2142
+ }
1801
2143
  }
1802
2144
  }
1803
2145
  }
1804
2146
  return { kind: "object", fields: result };
1805
2147
  }
2148
+ cacheDescendants(prefix, value) {
2149
+ if (value.kind !== "object") return;
2150
+ for (const [key, child] of value.fields) {
2151
+ const childKey = `${prefix}.${stringSegmentsToKey([key])}`;
2152
+ this.cache.set(childKey, child);
2153
+ this.cacheDescendants(childKey, child);
2154
+ }
2155
+ }
1806
2156
  resolveVal(v, scope) {
1807
2157
  if (isSubst(v)) return this.resolveSubst(v, scope);
1808
2158
  if (isConcat(v)) {
@@ -1828,6 +2178,7 @@ var SubstitutionResolver = class {
1828
2178
  return hv;
1829
2179
  }
1830
2180
  resolveSubst(s, scope) {
2181
+ if (s.knownAbsent) return void 0;
1831
2182
  const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
1832
2183
  if (this.cache.has(key)) return this.cache.get(key);
1833
2184
  if (this.resolving.has(key)) {
@@ -1864,7 +2215,10 @@ var SubstitutionResolver = class {
1864
2215
  try {
1865
2216
  const found = lookupPath(this.root, s.segments);
1866
2217
  if (found !== void 0) {
1867
- if (isConcat(found) && this.resolvingConcats.has(found)) {
2218
+ const rfp = this.resolvingFieldPath;
2219
+ const isOwner = rfp.length >= s.segments.length && s.segments.every((seg, i) => rfp[i] === seg.text);
2220
+ const isSelfRef = isConcat(found) && this.resolvingConcats.has(found) || isOwner && containsSubstByPath(found, s.segments);
2221
+ if (isSelfRef) {
1868
2222
  let prior;
1869
2223
  if (s.segments.length > 1) {
1870
2224
  const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
@@ -1917,6 +2271,28 @@ var SubstitutionResolver = class {
1917
2271
  if (result !== void 0) this.cache.set(key, result);
1918
2272
  return result;
1919
2273
  }
2274
+ if (s.prefixLen > 0 && s.segments.length > s.prefixLen) {
2275
+ const originalSegments = s.segments.slice(s.prefixLen);
2276
+ const fallbackFound = lookupPath(this.root, originalSegments);
2277
+ if (fallbackFound !== void 0) {
2278
+ let result = this.resolveVal(fallbackFound, scope);
2279
+ if (originalSegments.length === 1 && result !== void 0 && result.kind === "object") {
2280
+ const rootSeg = originalSegments[0]?.text ?? "";
2281
+ const prior = this.root.priorValues.get(rootSeg);
2282
+ if (prior !== void 0) {
2283
+ const priorResolved = this.resolveVal(prior, scope);
2284
+ if (priorResolved !== void 0 && priorResolved.kind === "object") {
2285
+ result = deepMergeHoconValues(
2286
+ priorResolved,
2287
+ result
2288
+ );
2289
+ }
2290
+ }
2291
+ }
2292
+ if (result !== void 0) this.cache.set(key, result);
2293
+ return result;
2294
+ }
2295
+ }
1920
2296
  if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
1921
2297
  const result = this.resolveEnvList(s);
1922
2298
  if (result !== void 0) {