@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.js CHANGED
@@ -286,6 +286,252 @@ function mergeUnresolved(receiver, fallback) {
286
286
  return result;
287
287
  }
288
288
 
289
+ // src/internal/resolver/fold-self-ref.ts
290
+ function substFullKey(s) {
291
+ return stringSegmentsToKey(s.segments.map((seg) => seg.text));
292
+ }
293
+ function stringSegmentsToKey(segments) {
294
+ return segments.map((t) => {
295
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
296
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
297
+ }
298
+ return t;
299
+ }).join(".");
300
+ }
301
+ function containsSelfRef(v, fullKey) {
302
+ if (isSubst(v)) return !v.knownAbsent && substFullKey(v) === fullKey;
303
+ if (isConcat(v)) return v.nodes.some((n) => containsSelfRef(n, fullKey));
304
+ if (isAppend(v)) {
305
+ return containsSelfRef(v.existing, fullKey) || containsSelfRef(v.elem, fullKey);
306
+ }
307
+ if (isResObj(v)) {
308
+ for (const f of v.fields.values()) {
309
+ if (containsSelfRef(f, fullKey)) return true;
310
+ }
311
+ return false;
312
+ }
313
+ const hv = v;
314
+ if (hv.kind === "array") {
315
+ return hv.items.some((item) => containsSelfRef(item, fullKey));
316
+ }
317
+ if (hv.kind === "object") {
318
+ for (const f of hv.fields.values()) {
319
+ if (containsSelfRef(f, fullKey)) return true;
320
+ }
321
+ return false;
322
+ }
323
+ return false;
324
+ }
325
+ function foldSelfRef(v, fullKey, replacement) {
326
+ if (isSubst(v)) {
327
+ return substFullKey(v) === fullKey ? replacement : v;
328
+ }
329
+ if (isConcat(v)) {
330
+ return {
331
+ _kind: "concat-placeholder",
332
+ nodes: v.nodes.map((n) => foldSelfRef(n, fullKey, replacement)),
333
+ line: v.line,
334
+ col: v.col
335
+ };
336
+ }
337
+ if (isAppend(v)) {
338
+ return {
339
+ _kind: "append-placeholder",
340
+ existing: foldSelfRef(v.existing, fullKey, replacement),
341
+ elem: foldSelfRef(v.elem, fullKey, replacement)
342
+ };
343
+ }
344
+ if (isResObj(v)) {
345
+ const newFields = /* @__PURE__ */ new Map();
346
+ for (const [k, val] of v.fields) {
347
+ newFields.set(k, foldSelfRef(val, fullKey, replacement));
348
+ }
349
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
350
+ }
351
+ const hv = v;
352
+ if (hv.kind === "array") {
353
+ return {
354
+ kind: "array",
355
+ items: hv.items.map((item) => foldSelfRef(item, fullKey, replacement))
356
+ };
357
+ }
358
+ if (hv.kind === "object") {
359
+ const newFields = /* @__PURE__ */ new Map();
360
+ for (const [k, val] of hv.fields) {
361
+ newFields.set(k, foldSelfRef(val, fullKey, replacement));
362
+ }
363
+ return { kind: "object", fields: newFields };
364
+ }
365
+ return v;
366
+ }
367
+ function foldOrSkipPrior(prior, fullKey, old) {
368
+ if (!containsSelfRef(prior, fullKey)) return cloneResolverValue(prior);
369
+ if (old === void 0) return foldOptionalSelfRefAbsent(prior, fullKey);
370
+ return foldSelfRef(prior, fullKey, old);
371
+ }
372
+ function foldOptionalSelfRefAbsent(v, fullKey) {
373
+ if (isSubst(v) && substFullKey(v) === fullKey) {
374
+ if (!v.optional) return void 0;
375
+ return {
376
+ _kind: "subst-placeholder",
377
+ segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
378
+ optional: v.optional,
379
+ knownAbsent: true,
380
+ listSuffix: v.listSuffix,
381
+ line: v.line,
382
+ col: v.col,
383
+ prefixLen: v.prefixLen
384
+ };
385
+ }
386
+ if (isConcat(v)) {
387
+ const nodes = [];
388
+ for (const node of v.nodes) {
389
+ const folded = foldOptionalSelfRefAbsent(node, fullKey);
390
+ if (folded === void 0) return void 0;
391
+ nodes.push(folded);
392
+ }
393
+ return { _kind: "concat-placeholder", nodes, line: v.line, col: v.col };
394
+ }
395
+ if (isAppend(v)) {
396
+ const existing = foldOptionalSelfRefAbsent(v.existing, fullKey);
397
+ const elem = foldOptionalSelfRefAbsent(v.elem, fullKey);
398
+ if (existing === void 0 || elem === void 0) return void 0;
399
+ return { _kind: "append-placeholder", existing, elem };
400
+ }
401
+ if (isResObj(v)) {
402
+ const fields = /* @__PURE__ */ new Map();
403
+ for (const [key, value] of v.fields) {
404
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
405
+ if (folded === void 0) return void 0;
406
+ fields.set(key, folded);
407
+ }
408
+ return { _kind: "res-obj", fields, priorValues: new Map(v.priorValues) };
409
+ }
410
+ const hv = v;
411
+ if (hv.kind === "array") {
412
+ const items = [];
413
+ for (const item of hv.items) {
414
+ const folded = foldOptionalSelfRefAbsent(item, fullKey);
415
+ if (folded === void 0) return void 0;
416
+ items.push(folded);
417
+ }
418
+ return { kind: "array", items };
419
+ }
420
+ if (hv.kind === "object") {
421
+ const fields = /* @__PURE__ */ new Map();
422
+ for (const [key, value] of hv.fields) {
423
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
424
+ if (folded === void 0) return void 0;
425
+ fields.set(key, folded);
426
+ }
427
+ return { kind: "object", fields };
428
+ }
429
+ return cloneResolverValue(v);
430
+ }
431
+ function cloneResolverValue(v) {
432
+ if (isSubst(v)) {
433
+ return {
434
+ _kind: "subst-placeholder",
435
+ segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
436
+ optional: v.optional,
437
+ knownAbsent: v.knownAbsent,
438
+ listSuffix: v.listSuffix,
439
+ line: v.line,
440
+ col: v.col,
441
+ prefixLen: v.prefixLen
442
+ };
443
+ }
444
+ if (isConcat(v)) {
445
+ return {
446
+ _kind: "concat-placeholder",
447
+ nodes: v.nodes.map((n) => cloneResolverValue(n)),
448
+ line: v.line,
449
+ col: v.col
450
+ };
451
+ }
452
+ if (isResObj(v)) {
453
+ const newFields = /* @__PURE__ */ new Map();
454
+ for (const [k, val] of v.fields) newFields.set(k, cloneResolverValue(val));
455
+ const newPriors = /* @__PURE__ */ new Map();
456
+ for (const [k, val] of v.priorValues) newPriors.set(k, cloneResolverValue(val));
457
+ return { _kind: "res-obj", fields: newFields, priorValues: newPriors };
458
+ }
459
+ if (isAppend(v)) {
460
+ return {
461
+ _kind: "append-placeholder",
462
+ existing: cloneResolverValue(v.existing),
463
+ elem: cloneResolverValue(v.elem)
464
+ };
465
+ }
466
+ const hv = v;
467
+ if (hv.kind === "array") {
468
+ return {
469
+ kind: "array",
470
+ items: hv.items.map((item) => cloneResolverValue(item))
471
+ };
472
+ }
473
+ if (hv.kind === "object") {
474
+ const newFields = /* @__PURE__ */ new Map();
475
+ for (const [k, val] of hv.fields) {
476
+ newFields.set(k, cloneResolverValue(val));
477
+ }
478
+ return { kind: "object", fields: newFields };
479
+ }
480
+ return hv;
481
+ }
482
+ function foldNestedSelfRefs(v, pathPrefix) {
483
+ if (!isResObj(v)) return v;
484
+ const newFields = /* @__PURE__ */ new Map();
485
+ for (const [k, fieldVal] of v.fields) {
486
+ const childPath = [...pathPrefix, k];
487
+ const fullKey = stringSegmentsToKey(childPath);
488
+ const folded = foldNestedSelfRefs(fieldVal, childPath);
489
+ let finalVal = folded;
490
+ if (containsSelfRef(folded, fullKey)) {
491
+ const leafPrior = v.priorValues.get(k);
492
+ if (leafPrior !== void 0) {
493
+ const leafPriorFolded = foldNestedSelfRefs(leafPrior, childPath);
494
+ finalVal = foldSelfRef(folded, fullKey, leafPriorFolded);
495
+ }
496
+ }
497
+ newFields.set(k, finalVal);
498
+ }
499
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
500
+ }
501
+ function containsSubstByPath(v, target) {
502
+ if (isSubst(v)) return !v.knownAbsent && segmentsTextEqual(v.segments, target);
503
+ if (isConcat(v)) return v.nodes.some((n) => containsSubstByPath(n, target));
504
+ if (isAppend(v)) {
505
+ return containsSubstByPath(v.existing, target) || containsSubstByPath(v.elem, target);
506
+ }
507
+ if (isResObj(v)) {
508
+ for (const f of v.fields.values()) {
509
+ if (containsSubstByPath(f, target)) return true;
510
+ }
511
+ return false;
512
+ }
513
+ const hv = v;
514
+ if (hv.kind === "array") {
515
+ return hv.items.some((item) => containsSubstByPath(item, target));
516
+ }
517
+ if (hv.kind === "object") {
518
+ for (const f of hv.fields.values()) {
519
+ if (containsSubstByPath(f, target)) return true;
520
+ }
521
+ return false;
522
+ }
523
+ return false;
524
+ }
525
+ function segmentsTextEqual(a, b) {
526
+ if (a.length !== b.length) return false;
527
+ for (let i = 0; i < a.length; i++) {
528
+ const ai = a[i];
529
+ const bi = b[i];
530
+ if (ai === void 0 || bi === void 0 || ai.text !== bi.text) return false;
531
+ }
532
+ return true;
533
+ }
534
+
289
535
  // src/internal/resolver/utils.ts
290
536
  function segmentsToKey(segments) {
291
537
  return segments.map((s) => {
@@ -327,13 +573,22 @@ function deepMergeHoconValues(base, overlay) {
327
573
  }
328
574
  return { kind: "object", fields: merged };
329
575
  }
330
- function deepMergeResObjInto(dst, src) {
576
+ function deepMergeResObjInto(dst, src, pathPrefix = []) {
331
577
  for (const [k, srcVal] of src.fields) {
578
+ const childPrefix = [...pathPrefix, k];
579
+ const fullKey = stringSegmentsToKey(childPrefix);
332
580
  const dstVal = dst.fields.get(k);
333
581
  if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
334
- deepMergeResObjInto(dstVal, srcVal);
582
+ const priorExisting = dst.priorValues.get(k);
583
+ const prior = foldOrSkipPrior(dstVal, fullKey, priorExisting);
584
+ if (prior !== void 0) dst.priorValues.set(k, prior);
585
+ deepMergeResObjInto(dstVal, srcVal, childPrefix);
335
586
  } else {
336
- if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
587
+ if (dstVal !== void 0) {
588
+ const priorExisting = dst.priorValues.get(k);
589
+ const prior = foldOrSkipPrior(dstVal, fullKey, priorExisting);
590
+ if (prior !== void 0) dst.priorValues.set(k, prior);
591
+ }
337
592
  dst.fields.set(k, srcVal);
338
593
  }
339
594
  }
@@ -411,6 +666,11 @@ var Lexer = class {
411
666
  this.line = 1;
412
667
  this.col = 1;
413
668
  this.hadSpace = false;
669
+ // Accumulates literal whitespace chars consumed between tokens. Reset on every
670
+ // token push. Used so the parser can preserve path-expression whitespace
671
+ // (xx.hocon#42, E13) — e.g. tab vs space adjacent to dots in key paths.
672
+ // Comment text is NOT accumulated; only the actual WS chars.
673
+ this.whitespaceBuffer = "";
414
674
  this.tokens = [];
415
675
  if (input.charCodeAt(0) === 65279) this.input = input.slice(1);
416
676
  }
@@ -426,6 +686,7 @@ var Lexer = class {
426
686
  continue;
427
687
  }
428
688
  if (isHoconWhitespace(ch)) {
689
+ this.whitespaceBuffer += ch;
429
690
  this.advance();
430
691
  this.hadSpace = true;
431
692
  continue;
@@ -517,7 +778,7 @@ var Lexer = class {
517
778
  }
518
779
  throw new ParseError(`unexpected character: ${JSON.stringify(ch)}`, sl, sc);
519
780
  }
520
- this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false });
781
+ this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false, precedingWhitespace: "" });
521
782
  return this.tokens;
522
783
  }
523
784
  peek(offset = 0) {
@@ -534,12 +795,14 @@ var Lexer = class {
534
795
  return ch;
535
796
  }
536
797
  push(kind, value, l, c, isQuoted = false) {
537
- this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
798
+ this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer });
538
799
  this.hadSpace = false;
800
+ this.whitespaceBuffer = "";
539
801
  }
540
802
  pushSubst(payload, value, l, c) {
541
- this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
803
+ this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer, subst: payload });
542
804
  this.hadSpace = false;
805
+ this.whitespaceBuffer = "";
543
806
  }
544
807
  /**
545
808
  * Read body of a quoted string. Opening `"` must already be consumed.
@@ -785,7 +1048,7 @@ function isUnquotedContinue(ch, nextFn) {
785
1048
  }
786
1049
 
787
1050
  // src/internal/parser/parser.ts
788
- var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
1051
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, precedingWhitespace: "", subst: void 0 };
789
1052
  var Parser = class {
790
1053
  constructor(tokens) {
791
1054
  this.tokens = tokens;
@@ -898,13 +1161,17 @@ var Parser = class {
898
1161
  let trailingDot = false;
899
1162
  let firstWasQuoted = false;
900
1163
  let spaceConcat = false;
1164
+ let postDotPrefix = "";
901
1165
  while (true) {
902
1166
  const t = this.peek();
903
1167
  if (t.kind === "string") {
904
1168
  this.advance();
905
1169
  if (segments.length === 0) firstWasQuoted = true;
906
1170
  if (spaceConcat) {
907
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${t.value}`;
1171
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}${t.value}`;
1172
+ } else if (postDotPrefix !== "") {
1173
+ segments.push(`${postDotPrefix}${t.value}`);
1174
+ postDotPrefix = "";
908
1175
  } else {
909
1176
  segments.push(t.value);
910
1177
  }
@@ -914,24 +1181,23 @@ var Parser = class {
914
1181
  const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
915
1182
  const parts = raw.split(".");
916
1183
  const filtered = parts.filter((s) => s.length > 0);
917
- for (const seg of filtered) {
918
- if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
919
- const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
920
- throw new ParseError(
921
- `unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
922
- t.line,
923
- t.col
924
- );
925
- }
926
- }
927
- if (spaceConcat && filtered.length > 0) {
1184
+ if (spaceConcat) {
1185
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}`;
928
1186
  if (raw.startsWith(".")) {
929
1187
  segments.push(...filtered);
930
- } else {
1188
+ } else if (filtered.length > 0) {
931
1189
  const [head, ...tail] = filtered;
932
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${head}`;
1190
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${head}`;
933
1191
  segments.push(...tail);
934
1192
  }
1193
+ } else if (postDotPrefix !== "" && raw.startsWith(".")) {
1194
+ segments.push(postDotPrefix);
1195
+ postDotPrefix = "";
1196
+ segments.push(...filtered);
1197
+ } else if (postDotPrefix !== "" && filtered.length > 0) {
1198
+ const [head, ...tail] = filtered;
1199
+ segments.push(`${postDotPrefix}${head}`, ...tail);
1200
+ postDotPrefix = "";
935
1201
  } else {
936
1202
  segments.push(...filtered);
937
1203
  }
@@ -941,11 +1207,23 @@ var Parser = class {
941
1207
  break;
942
1208
  }
943
1209
  spaceConcat = false;
944
- if (trailingDot) continue;
1210
+ if (trailingDot) {
1211
+ const next2 = this.peek();
1212
+ if ((next2.kind === "unquoted" || next2.kind === "string") && next2.precedingWhitespace.length > 0) {
1213
+ postDotPrefix = next2.precedingWhitespace;
1214
+ } else {
1215
+ postDotPrefix = "";
1216
+ }
1217
+ continue;
1218
+ }
945
1219
  const next = this.peek();
946
1220
  if (next.kind === "unquoted" && next.value === "." && !next.precedingSpace) {
947
1221
  this.advance();
948
1222
  trailingDot = true;
1223
+ const afterDot = this.peek();
1224
+ if ((afterDot.kind === "unquoted" || afterDot.kind === "string") && afterDot.precedingWhitespace.length > 0) {
1225
+ postDotPrefix = afterDot.precedingWhitespace;
1226
+ }
949
1227
  continue;
950
1228
  }
951
1229
  if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
@@ -958,6 +1236,14 @@ var Parser = class {
958
1236
  }
959
1237
  break;
960
1238
  }
1239
+ if (trailingDot) {
1240
+ const here = this.peek();
1241
+ throw new ParseError(
1242
+ `path has a trailing period '.' \u2014 empty key segment not allowed (HOCON.md path rules)`,
1243
+ here.line,
1244
+ here.col
1245
+ );
1246
+ }
961
1247
  return { segments, firstWasQuoted };
962
1248
  }
963
1249
  parseInclude() {
@@ -1130,7 +1416,8 @@ var Parser = class {
1130
1416
  break;
1131
1417
  }
1132
1418
  if (hadSpace) {
1133
- parts.push({ kind: "scalar", raw: " ", valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1419
+ const sep = t.precedingWhitespace.length > 0 ? t.precedingWhitespace : " ";
1420
+ parts.push({ kind: "scalar", raw: sep, valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1134
1421
  }
1135
1422
  parts.push(node);
1136
1423
  }
@@ -1343,7 +1630,7 @@ var IncludeLoader = class {
1343
1630
  for (const ext of probeExts) {
1344
1631
  const obj = this.loadSingle(`${absPath}${ext}`);
1345
1632
  if (obj !== void 0) {
1346
- deepMergeResObjInto(merged, obj);
1633
+ deepMergeResObjInto(merged, obj, []);
1347
1634
  foundAny = true;
1348
1635
  }
1349
1636
  }
@@ -1376,7 +1663,7 @@ var IncludeLoader = class {
1376
1663
  for (const ext of probeExts) {
1377
1664
  const obj = await this.loadSingleAsync(`${absPath}${ext}`);
1378
1665
  if (obj !== void 0) {
1379
- deepMergeResObjInto(merged, obj);
1666
+ deepMergeResObjInto(merged, obj, []);
1380
1667
  foundAny = true;
1381
1668
  }
1382
1669
  }
@@ -1548,7 +1835,7 @@ var StructureBuilder = class _StructureBuilder {
1548
1835
  if (pathPrefix.length > 0) {
1549
1836
  this.relativizeResObj(included, pathPrefix);
1550
1837
  }
1551
- deepMergeResObjInto(obj, included);
1838
+ deepMergeResObjInto(obj, included, pathPrefix);
1552
1839
  return;
1553
1840
  }
1554
1841
  const [head, ...tail] = field.key;
@@ -1563,12 +1850,27 @@ var StructureBuilder = class _StructureBuilder {
1563
1850
  return;
1564
1851
  }
1565
1852
  const childPrefix = [...pathPrefix, head];
1853
+ const fullKey = stringSegmentsToKey(childPrefix);
1566
1854
  if (field.append) {
1567
1855
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1568
- obj.priorValues.set(head, existing2);
1856
+ const oldPrior = obj.priorValues.get(head);
1857
+ const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1858
+ if (folded !== void 0) obj.priorValues.set(head, folded);
1569
1859
  obj.fields.set(head, {
1570
1860
  _kind: "append-placeholder",
1571
- existing: existing2,
1861
+ // Use the folded existing (self-ref-free) for the Append. When the
1862
+ // fold returned undefined (self-ref present but no old prior),
1863
+ // fall back to the original existing — resolveSubst will then
1864
+ // raise the "self-ref with no prior value" error at resolve time,
1865
+ // matching the pre-fix behaviour for that edge case.
1866
+ //
1867
+ // Clone `folded` again so the Append.existing and priorValues[head]
1868
+ // do not share the same Subst/Concat references — relativizeResObj
1869
+ // walks BOTH `fields` and `priorValues` and mutates Subst.segments
1870
+ // in place, so a shared reference would get its prefix applied
1871
+ // twice (resulting in e.g. `${outer.outer.base}` instead of
1872
+ // `${outer.base}`). Codex review on PR #131.
1873
+ existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1572
1874
  elem: this.astToResolverValue(field.value, childPrefix)
1573
1875
  });
1574
1876
  return;
@@ -1576,10 +1878,13 @@ var StructureBuilder = class _StructureBuilder {
1576
1878
  const existing = obj.fields.get(head);
1577
1879
  const newVal = this.astToResolverValue(field.value, childPrefix);
1578
1880
  if (existing !== void 0) {
1579
- obj.priorValues.set(head, existing);
1881
+ const oldPrior = obj.priorValues.get(head);
1882
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1883
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1884
+ if (prior !== void 0) obj.priorValues.set(head, prior);
1580
1885
  }
1581
1886
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1582
- deepMergeResObjInto(existing, newVal);
1887
+ deepMergeResObjInto(existing, newVal, childPrefix);
1583
1888
  return;
1584
1889
  }
1585
1890
  obj.fields.set(head, newVal);
@@ -1596,7 +1901,7 @@ var StructureBuilder = class _StructureBuilder {
1596
1901
  if (pathPrefix.length > 0) {
1597
1902
  this.relativizeResObj(included, pathPrefix);
1598
1903
  }
1599
- deepMergeResObjInto(obj, included);
1904
+ deepMergeResObjInto(obj, included, pathPrefix);
1600
1905
  return;
1601
1906
  }
1602
1907
  const [head, ...tail] = field.key;
@@ -1611,12 +1916,15 @@ var StructureBuilder = class _StructureBuilder {
1611
1916
  return;
1612
1917
  }
1613
1918
  const childPrefix = [...pathPrefix, head];
1919
+ const fullKey = stringSegmentsToKey(childPrefix);
1614
1920
  if (field.append) {
1615
1921
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1616
- obj.priorValues.set(head, existing2);
1922
+ const oldPrior = obj.priorValues.get(head);
1923
+ const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1924
+ if (folded !== void 0) obj.priorValues.set(head, folded);
1617
1925
  obj.fields.set(head, {
1618
1926
  _kind: "append-placeholder",
1619
- existing: existing2,
1927
+ existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1620
1928
  elem: await this.astToResolverValueAsync(field.value, childPrefix)
1621
1929
  });
1622
1930
  return;
@@ -1624,10 +1932,13 @@ var StructureBuilder = class _StructureBuilder {
1624
1932
  const existing = obj.fields.get(head);
1625
1933
  const newVal = await this.astToResolverValueAsync(field.value, childPrefix);
1626
1934
  if (existing !== void 0) {
1627
- obj.priorValues.set(head, existing);
1935
+ const oldPrior = obj.priorValues.get(head);
1936
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1937
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1938
+ if (prior !== void 0) obj.priorValues.set(head, prior);
1628
1939
  }
1629
1940
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1630
- deepMergeResObjInto(existing, newVal);
1941
+ deepMergeResObjInto(existing, newVal, childPrefix);
1631
1942
  return;
1632
1943
  }
1633
1944
  obj.fields.set(head, newVal);
@@ -1646,7 +1957,7 @@ var StructureBuilder = class _StructureBuilder {
1646
1957
  return inner;
1647
1958
  }
1648
1959
  case "subst":
1649
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1960
+ 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 };
1650
1961
  case "concat":
1651
1962
  return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
1652
1963
  case "include":
@@ -1670,7 +1981,7 @@ var StructureBuilder = class _StructureBuilder {
1670
1981
  case "object":
1671
1982
  return await this.buildAsync(ast, pathPrefix);
1672
1983
  case "subst":
1673
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1984
+ 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 };
1674
1985
  case "concat": {
1675
1986
  const nodes = [];
1676
1987
  for (const n of ast.nodes) {
@@ -1759,6 +2070,23 @@ var SubstitutionResolver = class {
1759
2070
  // narrower than path-equality. Spec amendment deferred to a follow-up
1760
2071
  // xx.hocon PR (see Phase 6 #3f close-out notes).
1761
2072
  this.resolvingConcats = /* @__PURE__ */ new WeakSet();
2073
+ // Path stack tracking the full dotted path of the field currently being
2074
+ // assigned in `resolveResObj`. Leaf keys are pushed on entry to each
2075
+ // `(key, val)` iteration and popped on exit, so nested objects build up
2076
+ // the full path (e.g. `["o", "history"]` while resolving `o.history`).
2077
+ //
2078
+ // Used to tighten self-ref detection at the L192 found-lookup site:
2079
+ // a substitution `${X}` whose looked-up value contains a Subst pointing
2080
+ // at `X` is only a true self-reference when the field currently being
2081
+ // assigned is `X` (or a descendant of `X`). Without this guard, an
2082
+ // external lookup like `b = ${o}` (where o has a self-referential
2083
+ // field) would mis-fire and short-circuit to o's prior, returning the
2084
+ // wrong value for b. The is_owner gate (rfp prefix-matches s.segments)
2085
+ // narrows the check to the owner case only.
2086
+ //
2087
+ // Cross-impl with rs.hocon v1.5.1's resolving_field_path (same purpose,
2088
+ // same prefix-match semantics).
2089
+ this.resolvingFieldPath = [];
1762
2090
  }
1763
2091
  /** Returns a prefix for error messages, including origin info when set. */
1764
2092
  originPrefix() {
@@ -1770,35 +2098,57 @@ var SubstitutionResolver = class {
1770
2098
  resolveResObj(obj) {
1771
2099
  const result = /* @__PURE__ */ new Map();
1772
2100
  for (const [key, val] of obj.fields) {
1773
- const resolved = this.resolveVal(val, obj);
2101
+ this.resolvingFieldPath.push(key);
2102
+ const fullCacheKey = stringSegmentsToKey(this.resolvingFieldPath);
2103
+ this.cache.delete(fullCacheKey);
2104
+ let resolved;
2105
+ try {
2106
+ resolved = this.resolveVal(val, obj);
2107
+ } finally {
2108
+ this.resolvingFieldPath.pop();
2109
+ }
1774
2110
  if (resolved !== void 0) {
1775
2111
  if (resolved.kind === "object") {
1776
2112
  const prior = obj.priorValues.get(key);
1777
2113
  if (prior !== void 0) {
1778
2114
  const priorResolved = this.resolveVal(prior, obj);
1779
2115
  if (priorResolved !== void 0 && priorResolved.kind === "object") {
1780
- result.set(
1781
- key,
1782
- deepMergeHoconValues(
1783
- priorResolved,
1784
- resolved
1785
- )
2116
+ const finalValue = deepMergeHoconValues(
2117
+ priorResolved,
2118
+ resolved
1786
2119
  );
2120
+ result.set(key, finalValue);
2121
+ this.cache.set(fullCacheKey, finalValue);
2122
+ this.cacheDescendants(fullCacheKey, finalValue);
1787
2123
  continue;
1788
2124
  }
1789
2125
  }
1790
2126
  }
1791
2127
  result.set(key, resolved);
2128
+ this.cache.set(fullCacheKey, resolved);
2129
+ this.cacheDescendants(fullCacheKey, resolved);
1792
2130
  } else {
1793
2131
  const prior = obj.priorValues.get(key);
1794
2132
  if (prior !== void 0) {
1795
2133
  const priorResolved = this.resolveVal(prior, obj);
1796
- if (priorResolved !== void 0) result.set(key, priorResolved);
2134
+ if (priorResolved !== void 0) {
2135
+ result.set(key, priorResolved);
2136
+ this.cache.set(fullCacheKey, priorResolved);
2137
+ this.cacheDescendants(fullCacheKey, priorResolved);
2138
+ }
1797
2139
  }
1798
2140
  }
1799
2141
  }
1800
2142
  return { kind: "object", fields: result };
1801
2143
  }
2144
+ cacheDescendants(prefix, value) {
2145
+ if (value.kind !== "object") return;
2146
+ for (const [key, child] of value.fields) {
2147
+ const childKey = `${prefix}.${stringSegmentsToKey([key])}`;
2148
+ this.cache.set(childKey, child);
2149
+ this.cacheDescendants(childKey, child);
2150
+ }
2151
+ }
1802
2152
  resolveVal(v, scope) {
1803
2153
  if (isSubst(v)) return this.resolveSubst(v, scope);
1804
2154
  if (isConcat(v)) {
@@ -1824,6 +2174,7 @@ var SubstitutionResolver = class {
1824
2174
  return hv;
1825
2175
  }
1826
2176
  resolveSubst(s, scope) {
2177
+ if (s.knownAbsent) return void 0;
1827
2178
  const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
1828
2179
  if (this.cache.has(key)) return this.cache.get(key);
1829
2180
  if (this.resolving.has(key)) {
@@ -1860,7 +2211,10 @@ var SubstitutionResolver = class {
1860
2211
  try {
1861
2212
  const found = lookupPath(this.root, s.segments);
1862
2213
  if (found !== void 0) {
1863
- if (isConcat(found) && this.resolvingConcats.has(found)) {
2214
+ const rfp = this.resolvingFieldPath;
2215
+ const isOwner = rfp.length >= s.segments.length && s.segments.every((seg, i) => rfp[i] === seg.text);
2216
+ const isSelfRef = isConcat(found) && this.resolvingConcats.has(found) || isOwner && containsSubstByPath(found, s.segments);
2217
+ if (isSelfRef) {
1864
2218
  let prior;
1865
2219
  if (s.segments.length > 1) {
1866
2220
  const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
@@ -1913,6 +2267,28 @@ var SubstitutionResolver = class {
1913
2267
  if (result !== void 0) this.cache.set(key, result);
1914
2268
  return result;
1915
2269
  }
2270
+ if (s.prefixLen > 0 && s.segments.length > s.prefixLen) {
2271
+ const originalSegments = s.segments.slice(s.prefixLen);
2272
+ const fallbackFound = lookupPath(this.root, originalSegments);
2273
+ if (fallbackFound !== void 0) {
2274
+ let result = this.resolveVal(fallbackFound, scope);
2275
+ if (originalSegments.length === 1 && result !== void 0 && result.kind === "object") {
2276
+ const rootSeg = originalSegments[0]?.text ?? "";
2277
+ const prior = this.root.priorValues.get(rootSeg);
2278
+ if (prior !== void 0) {
2279
+ const priorResolved = this.resolveVal(prior, scope);
2280
+ if (priorResolved !== void 0 && priorResolved.kind === "object") {
2281
+ result = deepMergeHoconValues(
2282
+ priorResolved,
2283
+ result
2284
+ );
2285
+ }
2286
+ }
2287
+ }
2288
+ if (result !== void 0) this.cache.set(key, result);
2289
+ return result;
2290
+ }
2291
+ }
1916
2292
  if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
1917
2293
  const result = this.resolveEnvList(s);
1918
2294
  if (result !== void 0) {