@o3co/ts.hocon 1.5.2 → 1.6.1

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
@@ -280,55 +280,6 @@ var NotResolvedError = class extends ConfigError {
280
280
  }
281
281
  };
282
282
 
283
- // src/internal/resolver/types.ts
284
- var separatorValues = /* @__PURE__ */ new WeakSet();
285
- function isSubst(v) {
286
- return v._kind === "subst-placeholder";
287
- }
288
- function isConcat(v) {
289
- return v._kind === "concat-placeholder";
290
- }
291
- function isAppend(v) {
292
- return v._kind === "append-placeholder";
293
- }
294
- function isResObj(v) {
295
- return v._kind === "res-obj";
296
- }
297
- function makeResObj() {
298
- return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
299
- }
300
- function mergeUnresolved(receiver, fallback) {
301
- const result = makeResObj();
302
- for (const [k, v] of fallback.fields) {
303
- result.fields.set(k, v);
304
- }
305
- for (const [k, v] of fallback.priorValues) {
306
- result.priorValues.set(k, v);
307
- }
308
- for (const [k, rv] of receiver.fields) {
309
- const existing = result.fields.get(k);
310
- if (existing !== void 0) {
311
- const recPrior = receiver.priorValues.get(k);
312
- if (recPrior !== void 0 && !isResObj(recPrior)) {
313
- result.fields.set(k, rv);
314
- continue;
315
- }
316
- if (isResObj(rv) && isResObj(existing)) {
317
- result.fields.set(k, mergeUnresolved(rv, existing));
318
- continue;
319
- }
320
- result.priorValues.set(k, existing);
321
- }
322
- result.fields.set(k, rv);
323
- }
324
- for (const [k, v] of receiver.priorValues) {
325
- if (receiver.fields.has(k)) {
326
- result.priorValues.set(k, v);
327
- }
328
- }
329
- return result;
330
- }
331
-
332
283
  // src/internal/resolver/fold-self-ref.ts
333
284
  function substFullKey(s) {
334
285
  return stringSegmentsToKey(s.segments.map((seg) => seg.text));
@@ -342,11 +293,8 @@ function stringSegmentsToKey(segments) {
342
293
  }).join(".");
343
294
  }
344
295
  function containsSelfRef(v, fullKey) {
345
- if (isSubst(v)) return substFullKey(v) === fullKey;
296
+ if (isSubst(v)) return !v.knownAbsent && substFullKey(v) === fullKey;
346
297
  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
298
  if (isResObj(v)) {
351
299
  for (const f of v.fields.values()) {
352
300
  if (containsSelfRef(f, fullKey)) return true;
@@ -377,19 +325,12 @@ function foldSelfRef(v, fullKey, replacement) {
377
325
  col: v.col
378
326
  };
379
327
  }
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
328
  if (isResObj(v)) {
388
329
  const newFields = /* @__PURE__ */ new Map();
389
330
  for (const [k, val] of v.fields) {
390
331
  newFields.set(k, foldSelfRef(val, fullKey, replacement));
391
332
  }
392
- return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
333
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
393
334
  }
394
335
  const hv = v;
395
336
  if (hv.kind === "array") {
@@ -407,17 +348,106 @@ function foldSelfRef(v, fullKey, replacement) {
407
348
  }
408
349
  return v;
409
350
  }
351
+ function foldKnownAbsentSelfRef(v, fullKey, replacement) {
352
+ if (isSubst(v)) {
353
+ return v.knownAbsent && substFullKey(v) === fullKey ? replacement : v;
354
+ }
355
+ if (isConcat(v)) {
356
+ return {
357
+ _kind: "concat-placeholder",
358
+ nodes: v.nodes.map((n) => foldKnownAbsentSelfRef(n, fullKey, replacement)),
359
+ line: v.line,
360
+ col: v.col
361
+ };
362
+ }
363
+ if (isResObj(v)) {
364
+ const newFields = /* @__PURE__ */ new Map();
365
+ for (const [k, val] of v.fields) {
366
+ newFields.set(k, foldKnownAbsentSelfRef(val, fullKey, replacement));
367
+ }
368
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
369
+ }
370
+ const hv = v;
371
+ if (hv.kind === "array") {
372
+ return {
373
+ kind: "array",
374
+ items: hv.items.map((item) => foldKnownAbsentSelfRef(item, fullKey, replacement))
375
+ };
376
+ }
377
+ if (hv.kind === "object") {
378
+ const newFields = /* @__PURE__ */ new Map();
379
+ for (const [k, val] of hv.fields) {
380
+ newFields.set(k, foldKnownAbsentSelfRef(val, fullKey, replacement));
381
+ }
382
+ return { kind: "object", fields: newFields };
383
+ }
384
+ return v;
385
+ }
410
386
  function foldOrSkipPrior(prior, fullKey, old) {
411
387
  if (!containsSelfRef(prior, fullKey)) return cloneResolverValue(prior);
412
- if (old === void 0) return void 0;
388
+ if (old === void 0) return foldOptionalSelfRefAbsent(prior, fullKey);
413
389
  return foldSelfRef(prior, fullKey, old);
414
390
  }
391
+ function foldOptionalSelfRefAbsent(v, fullKey) {
392
+ if (isSubst(v) && substFullKey(v) === fullKey) {
393
+ if (!v.optional) return void 0;
394
+ return {
395
+ _kind: "subst-placeholder",
396
+ segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
397
+ optional: v.optional,
398
+ knownAbsent: true,
399
+ listSuffix: v.listSuffix,
400
+ line: v.line,
401
+ col: v.col,
402
+ prefixLen: v.prefixLen
403
+ };
404
+ }
405
+ if (isConcat(v)) {
406
+ const nodes = [];
407
+ for (const node of v.nodes) {
408
+ const folded = foldOptionalSelfRefAbsent(node, fullKey);
409
+ if (folded === void 0) return void 0;
410
+ nodes.push(folded);
411
+ }
412
+ return { _kind: "concat-placeholder", nodes, line: v.line, col: v.col };
413
+ }
414
+ if (isResObj(v)) {
415
+ const fields = /* @__PURE__ */ new Map();
416
+ for (const [key, value] of v.fields) {
417
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
418
+ if (folded === void 0) return void 0;
419
+ fields.set(key, folded);
420
+ }
421
+ return { _kind: "res-obj", fields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
422
+ }
423
+ const hv = v;
424
+ if (hv.kind === "array") {
425
+ const items = [];
426
+ for (const item of hv.items) {
427
+ const folded = foldOptionalSelfRefAbsent(item, fullKey);
428
+ if (folded === void 0) return void 0;
429
+ items.push(folded);
430
+ }
431
+ return { kind: "array", items };
432
+ }
433
+ if (hv.kind === "object") {
434
+ const fields = /* @__PURE__ */ new Map();
435
+ for (const [key, value] of hv.fields) {
436
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
437
+ if (folded === void 0) return void 0;
438
+ fields.set(key, folded);
439
+ }
440
+ return { kind: "object", fields };
441
+ }
442
+ return cloneResolverValue(v);
443
+ }
415
444
  function cloneResolverValue(v) {
416
445
  if (isSubst(v)) {
417
446
  return {
418
447
  _kind: "subst-placeholder",
419
448
  segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
420
449
  optional: v.optional,
450
+ knownAbsent: v.knownAbsent,
421
451
  listSuffix: v.listSuffix,
422
452
  line: v.line,
423
453
  col: v.col,
@@ -437,14 +467,7 @@ function cloneResolverValue(v) {
437
467
  for (const [k, val] of v.fields) newFields.set(k, cloneResolverValue(val));
438
468
  const newPriors = /* @__PURE__ */ new Map();
439
469
  for (const [k, val] of v.priorValues) newPriors.set(k, cloneResolverValue(val));
440
- return { _kind: "res-obj", fields: newFields, priorValues: newPriors };
441
- }
442
- if (isAppend(v)) {
443
- return {
444
- _kind: "append-placeholder",
445
- existing: cloneResolverValue(v.existing),
446
- elem: cloneResolverValue(v.elem)
447
- };
470
+ return { _kind: "res-obj", fields: newFields, priorValues: newPriors, resetKeys: new Set(v.resetKeys) };
448
471
  }
449
472
  const hv = v;
450
473
  if (hv.kind === "array") {
@@ -479,14 +502,11 @@ function foldNestedSelfRefs(v, pathPrefix) {
479
502
  }
480
503
  newFields.set(k, finalVal);
481
504
  }
482
- return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
505
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
483
506
  }
484
507
  function containsSubstByPath(v, target) {
485
- if (isSubst(v)) return segmentsTextEqual(v.segments, target);
508
+ if (isSubst(v)) return !v.knownAbsent && segmentsTextEqual(v.segments, target);
486
509
  if (isConcat(v)) return v.nodes.some((n) => containsSubstByPath(n, target));
487
- if (isAppend(v)) {
488
- return containsSubstByPath(v.existing, target) || containsSubstByPath(v.elem, target);
489
- }
490
510
  if (isResObj(v)) {
491
511
  for (const f of v.fields.values()) {
492
512
  if (containsSubstByPath(f, target)) return true;
@@ -515,6 +535,53 @@ function segmentsTextEqual(a, b) {
515
535
  return true;
516
536
  }
517
537
 
538
+ // src/internal/resolver/types.ts
539
+ var separatorValues = /* @__PURE__ */ new WeakSet();
540
+ function isSubst(v) {
541
+ return v._kind === "subst-placeholder";
542
+ }
543
+ function isConcat(v) {
544
+ return v._kind === "concat-placeholder";
545
+ }
546
+ function isResObj(v) {
547
+ return v._kind === "res-obj";
548
+ }
549
+ function makeResObj() {
550
+ return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map(), resetKeys: /* @__PURE__ */ new Set() };
551
+ }
552
+ function mergeUnresolved(receiver, fallback) {
553
+ const result = makeResObj();
554
+ for (const [k, v] of fallback.fields) {
555
+ result.fields.set(k, v);
556
+ }
557
+ for (const [k, v] of fallback.priorValues) {
558
+ result.priorValues.set(k, v);
559
+ }
560
+ for (const [k, rv] of receiver.fields) {
561
+ const existing = result.fields.get(k);
562
+ if (existing !== void 0) {
563
+ const recPrior = receiver.priorValues.get(k);
564
+ if (recPrior !== void 0 && !isResObj(recPrior)) {
565
+ result.fields.set(k, rv);
566
+ continue;
567
+ }
568
+ if (isResObj(rv) && isResObj(existing)) {
569
+ result.fields.set(k, mergeUnresolved(rv, existing));
570
+ continue;
571
+ }
572
+ const foldedPrior = foldOrSkipPrior(existing, k, void 0);
573
+ if (foldedPrior !== void 0) result.priorValues.set(k, foldedPrior);
574
+ }
575
+ result.fields.set(k, rv);
576
+ }
577
+ for (const [k, v] of receiver.priorValues) {
578
+ if (receiver.fields.has(k)) {
579
+ result.priorValues.set(k, v);
580
+ }
581
+ }
582
+ return result;
583
+ }
584
+
518
585
  // src/internal/resolver/utils.ts
519
586
  function segmentsToKey(segments) {
520
587
  return segments.map((s) => {
@@ -568,9 +635,19 @@ function deepMergeResObjInto(dst, src, pathPrefix = []) {
568
635
  deepMergeResObjInto(dstVal, srcVal, childPrefix);
569
636
  } else {
570
637
  if (dstVal !== void 0) {
571
- const priorExisting = dst.priorValues.get(k);
572
- const prior = foldOrSkipPrior(dstVal, fullKey, priorExisting);
573
- if (prior !== void 0) dst.priorValues.set(k, prior);
638
+ if (src.resetKeys.has(k)) {
639
+ dst.priorValues.delete(k);
640
+ } else {
641
+ const dstFolded = foldOrSkipPrior(dstVal, fullKey, dst.priorValues.get(k));
642
+ if (dstFolded !== void 0) {
643
+ const srcPrior = src.priorValues.get(k);
644
+ if (srcPrior !== void 0) {
645
+ dst.priorValues.set(k, foldKnownAbsentSelfRef(srcPrior, fullKey, dstFolded));
646
+ } else {
647
+ dst.priorValues.set(k, dstFolded);
648
+ }
649
+ }
650
+ }
574
651
  }
575
652
  dst.fields.set(k, srcVal);
576
653
  }
@@ -580,6 +657,7 @@ function deepMergeResObjInto(dst, src, pathPrefix = []) {
580
657
  dst.priorValues.set(k, srcPrior);
581
658
  }
582
659
  }
660
+ for (const k of src.resetKeys) dst.resetKeys.add(k);
583
661
  }
584
662
  function hoconValueToResObj(hv) {
585
663
  const obj = makeResObj();
@@ -649,6 +727,11 @@ var Lexer = class {
649
727
  this.line = 1;
650
728
  this.col = 1;
651
729
  this.hadSpace = false;
730
+ // Accumulates literal whitespace chars consumed between tokens. Reset on every
731
+ // token push. Used so the parser can preserve path-expression whitespace
732
+ // (xx.hocon#42, E13) — e.g. tab vs space adjacent to dots in key paths.
733
+ // Comment text is NOT accumulated; only the actual WS chars.
734
+ this.whitespaceBuffer = "";
652
735
  this.tokens = [];
653
736
  if (input.charCodeAt(0) === 65279) this.input = input.slice(1);
654
737
  }
@@ -664,6 +747,7 @@ var Lexer = class {
664
747
  continue;
665
748
  }
666
749
  if (isHoconWhitespace(ch)) {
750
+ this.whitespaceBuffer += ch;
667
751
  this.advance();
668
752
  this.hadSpace = true;
669
753
  continue;
@@ -755,7 +839,7 @@ var Lexer = class {
755
839
  }
756
840
  throw new ParseError(`unexpected character: ${JSON.stringify(ch)}`, sl, sc);
757
841
  }
758
- this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false });
842
+ this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false, precedingWhitespace: "" });
759
843
  return this.tokens;
760
844
  }
761
845
  peek(offset = 0) {
@@ -772,12 +856,14 @@ var Lexer = class {
772
856
  return ch;
773
857
  }
774
858
  push(kind, value, l, c, isQuoted = false) {
775
- this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
859
+ this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer });
776
860
  this.hadSpace = false;
861
+ this.whitespaceBuffer = "";
777
862
  }
778
863
  pushSubst(payload, value, l, c) {
779
- this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
864
+ this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer, subst: payload });
780
865
  this.hadSpace = false;
866
+ this.whitespaceBuffer = "";
781
867
  }
782
868
  /**
783
869
  * Read body of a quoted string. Opening `"` must already be consumed.
@@ -1023,7 +1109,7 @@ function isUnquotedContinue(ch, nextFn) {
1023
1109
  }
1024
1110
 
1025
1111
  // src/internal/parser/parser.ts
1026
- var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
1112
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, precedingWhitespace: "", subst: void 0 };
1027
1113
  var Parser = class {
1028
1114
  constructor(tokens) {
1029
1115
  this.tokens = tokens;
@@ -1136,13 +1222,17 @@ var Parser = class {
1136
1222
  let trailingDot = false;
1137
1223
  let firstWasQuoted = false;
1138
1224
  let spaceConcat = false;
1225
+ let postDotPrefix = "";
1139
1226
  while (true) {
1140
1227
  const t = this.peek();
1141
1228
  if (t.kind === "string") {
1142
1229
  this.advance();
1143
1230
  if (segments.length === 0) firstWasQuoted = true;
1144
1231
  if (spaceConcat) {
1145
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${t.value}`;
1232
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}${t.value}`;
1233
+ } else if (postDotPrefix !== "") {
1234
+ segments.push(`${postDotPrefix}${t.value}`);
1235
+ postDotPrefix = "";
1146
1236
  } else {
1147
1237
  segments.push(t.value);
1148
1238
  }
@@ -1152,24 +1242,23 @@ var Parser = class {
1152
1242
  const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
1153
1243
  const parts = raw.split(".");
1154
1244
  const filtered = parts.filter((s) => s.length > 0);
1155
- for (const seg of filtered) {
1156
- if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
1157
- const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
1158
- throw new ParseError(
1159
- `unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
1160
- t.line,
1161
- t.col
1162
- );
1163
- }
1164
- }
1165
- if (spaceConcat && filtered.length > 0) {
1245
+ if (spaceConcat) {
1246
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}`;
1166
1247
  if (raw.startsWith(".")) {
1167
1248
  segments.push(...filtered);
1168
- } else {
1249
+ } else if (filtered.length > 0) {
1169
1250
  const [head, ...tail] = filtered;
1170
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${head}`;
1251
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${head}`;
1171
1252
  segments.push(...tail);
1172
1253
  }
1254
+ } else if (postDotPrefix !== "" && raw.startsWith(".")) {
1255
+ segments.push(postDotPrefix);
1256
+ postDotPrefix = "";
1257
+ segments.push(...filtered);
1258
+ } else if (postDotPrefix !== "" && filtered.length > 0) {
1259
+ const [head, ...tail] = filtered;
1260
+ segments.push(`${postDotPrefix}${head}`, ...tail);
1261
+ postDotPrefix = "";
1173
1262
  } else {
1174
1263
  segments.push(...filtered);
1175
1264
  }
@@ -1179,11 +1268,23 @@ var Parser = class {
1179
1268
  break;
1180
1269
  }
1181
1270
  spaceConcat = false;
1182
- if (trailingDot) continue;
1271
+ if (trailingDot) {
1272
+ const next2 = this.peek();
1273
+ if ((next2.kind === "unquoted" || next2.kind === "string") && next2.precedingWhitespace.length > 0) {
1274
+ postDotPrefix = next2.precedingWhitespace;
1275
+ } else {
1276
+ postDotPrefix = "";
1277
+ }
1278
+ continue;
1279
+ }
1183
1280
  const next = this.peek();
1184
1281
  if (next.kind === "unquoted" && next.value === "." && !next.precedingSpace) {
1185
1282
  this.advance();
1186
1283
  trailingDot = true;
1284
+ const afterDot = this.peek();
1285
+ if ((afterDot.kind === "unquoted" || afterDot.kind === "string") && afterDot.precedingWhitespace.length > 0) {
1286
+ postDotPrefix = afterDot.precedingWhitespace;
1287
+ }
1187
1288
  continue;
1188
1289
  }
1189
1290
  if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
@@ -1196,6 +1297,14 @@ var Parser = class {
1196
1297
  }
1197
1298
  break;
1198
1299
  }
1300
+ if (trailingDot) {
1301
+ const here = this.peek();
1302
+ throw new ParseError(
1303
+ `path has a trailing period '.' \u2014 empty key segment not allowed (HOCON.md path rules)`,
1304
+ here.line,
1305
+ here.col
1306
+ );
1307
+ }
1199
1308
  return { segments, firstWasQuoted };
1200
1309
  }
1201
1310
  parseInclude() {
@@ -1368,7 +1477,8 @@ var Parser = class {
1368
1477
  break;
1369
1478
  }
1370
1479
  if (hadSpace) {
1371
- parts.push({ kind: "scalar", raw: " ", valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1480
+ const sep = t.precedingWhitespace.length > 0 ? t.precedingWhitespace : " ";
1481
+ parts.push({ kind: "scalar", raw: sep, valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1372
1482
  }
1373
1483
  parts.push(node);
1374
1484
  }
@@ -1804,35 +1914,19 @@ var StructureBuilder = class _StructureBuilder {
1804
1914
  const childPrefix = [...pathPrefix, head];
1805
1915
  const fullKey = stringSegmentsToKey(childPrefix);
1806
1916
  if (field.append) {
1807
- const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1808
- const oldPrior = obj.priorValues.get(head);
1809
- const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1810
- if (folded !== void 0) obj.priorValues.set(head, folded);
1811
- obj.fields.set(head, {
1812
- _kind: "append-placeholder",
1813
- // Use the folded existing (self-ref-free) for the Append. When the
1814
- // fold returned undefined (self-ref present but no old prior),
1815
- // fall back to the original existing — resolveSubst will then
1816
- // raise the "self-ref with no prior value" error at resolve time,
1817
- // matching the pre-fix behaviour for that edge case.
1818
- //
1819
- // Clone `folded` again so the Append.existing and priorValues[head]
1820
- // do not share the same Subst/Concat references — relativizeResObj
1821
- // walks BOTH `fields` and `priorValues` and mutates Subst.segments
1822
- // in place, so a shared reference would get its prefix applied
1823
- // twice (resulting in e.g. `${outer.outer.base}` instead of
1824
- // `${outer.base}`). Codex review on PR #131.
1825
- existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1826
- elem: this.astToResolverValue(field.value, childPrefix)
1827
- });
1917
+ const synthetic = this.desugarAppend(field, childPrefix);
1918
+ this.applyField(obj, synthetic, pathPrefix);
1828
1919
  return;
1829
1920
  }
1830
1921
  const existing = obj.fields.get(head);
1831
1922
  const newVal = this.astToResolverValue(field.value, childPrefix);
1923
+ if (!containsSelfRef(newVal, fullKey)) {
1924
+ obj.resetKeys.add(head);
1925
+ }
1832
1926
  if (existing !== void 0) {
1833
- const exFolded = foldNestedSelfRefs(existing, childPrefix);
1834
1927
  const oldPrior = obj.priorValues.get(head);
1835
- const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1928
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1929
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1836
1930
  if (prior !== void 0) obj.priorValues.set(head, prior);
1837
1931
  }
1838
1932
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
@@ -1870,23 +1964,19 @@ var StructureBuilder = class _StructureBuilder {
1870
1964
  const childPrefix = [...pathPrefix, head];
1871
1965
  const fullKey = stringSegmentsToKey(childPrefix);
1872
1966
  if (field.append) {
1873
- const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1874
- const oldPrior = obj.priorValues.get(head);
1875
- const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1876
- if (folded !== void 0) obj.priorValues.set(head, folded);
1877
- obj.fields.set(head, {
1878
- _kind: "append-placeholder",
1879
- existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1880
- elem: await this.astToResolverValueAsync(field.value, childPrefix)
1881
- });
1967
+ const synthetic = this.desugarAppend(field, childPrefix);
1968
+ await this.applyFieldAsync(obj, synthetic, pathPrefix);
1882
1969
  return;
1883
1970
  }
1884
1971
  const existing = obj.fields.get(head);
1885
1972
  const newVal = await this.astToResolverValueAsync(field.value, childPrefix);
1973
+ if (!containsSelfRef(newVal, fullKey)) {
1974
+ obj.resetKeys.add(head);
1975
+ }
1886
1976
  if (existing !== void 0) {
1887
- const exFolded = foldNestedSelfRefs(existing, childPrefix);
1888
1977
  const oldPrior = obj.priorValues.get(head);
1889
- const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1978
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1979
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1890
1980
  if (prior !== void 0) obj.priorValues.set(head, prior);
1891
1981
  }
1892
1982
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
@@ -1895,6 +1985,21 @@ var StructureBuilder = class _StructureBuilder {
1895
1985
  }
1896
1986
  obj.fields.set(head, newVal);
1897
1987
  }
1988
+ /** Builds the `key = ${?fullkey} [value]` field that `key += value` desugars
1989
+ * to (S13b.2, HOCON.md L732). `childPrefix` is the field's fully-qualified
1990
+ * path (pathPrefix + key), so a nested `srv { items += x }` references
1991
+ * `${?srv.items}`; include relativization rewrites it under a mount prefix.
1992
+ * `field.key` is already single-segment here (multi-segment keys are split
1993
+ * into nested objects upstream). Returns a NEW AstField (append=false);
1994
+ * positions inherit from the original field so resolve-time errors (the
1995
+ * S13b.2 non-array check, now via the scalar+array concat) keep a location. */
1996
+ desugarAppend(field, childPrefix) {
1997
+ const segments = childPrefix.map((text) => ({ text, line: field.pos.line, col: field.pos.col }));
1998
+ const subst = { kind: "subst", segments, optional: true, listSuffix: false, pos: field.pos };
1999
+ const elemArray = { kind: "array", items: [field.value], pos: field.pos };
2000
+ const synthetic = { kind: "concat", nodes: [subst, elemArray], pos: field.pos };
2001
+ return { key: field.key, value: synthetic, append: false, pos: field.pos };
2002
+ }
1898
2003
  astToResolverValue(ast, pathPrefix) {
1899
2004
  switch (ast.kind) {
1900
2005
  case "scalar": {
@@ -1909,7 +2014,7 @@ var StructureBuilder = class _StructureBuilder {
1909
2014
  return inner;
1910
2015
  }
1911
2016
  case "subst":
1912
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
2017
+ 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 };
1913
2018
  case "concat":
1914
2019
  return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
1915
2020
  case "include":
@@ -1933,7 +2038,7 @@ var StructureBuilder = class _StructureBuilder {
1933
2038
  case "object":
1934
2039
  return await this.buildAsync(ast, pathPrefix);
1935
2040
  case "subst":
1936
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
2041
+ 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 };
1937
2042
  case "concat": {
1938
2043
  const nodes = [];
1939
2044
  for (const n of ast.nodes) {
@@ -1959,11 +2064,6 @@ var StructureBuilder = class _StructureBuilder {
1959
2064
  }
1960
2065
  return;
1961
2066
  }
1962
- if (isAppend(val)) {
1963
- this.relativizeSubstPaths(val.existing, prefixSegments);
1964
- this.relativizeSubstPaths(val.elem, prefixSegments);
1965
- return;
1966
- }
1967
2067
  if (isResObj(val)) {
1968
2068
  this.relativizeResObj(val, prefixSegments);
1969
2069
  return;
@@ -2051,6 +2151,8 @@ var SubstitutionResolver = class {
2051
2151
  const result = /* @__PURE__ */ new Map();
2052
2152
  for (const [key, val] of obj.fields) {
2053
2153
  this.resolvingFieldPath.push(key);
2154
+ const fullCacheKey = stringSegmentsToKey(this.resolvingFieldPath);
2155
+ this.cache.delete(fullCacheKey);
2054
2156
  let resolved;
2055
2157
  try {
2056
2158
  resolved = this.resolveVal(val, obj);
@@ -2063,28 +2165,42 @@ var SubstitutionResolver = class {
2063
2165
  if (prior !== void 0) {
2064
2166
  const priorResolved = this.resolveVal(prior, obj);
2065
2167
  if (priorResolved !== void 0 && priorResolved.kind === "object") {
2066
- result.set(
2067
- key,
2068
- deepMergeHoconValues(
2069
- priorResolved,
2070
- resolved
2071
- )
2168
+ const finalValue = deepMergeHoconValues(
2169
+ priorResolved,
2170
+ resolved
2072
2171
  );
2172
+ result.set(key, finalValue);
2173
+ this.cache.set(fullCacheKey, finalValue);
2174
+ this.cacheDescendants(fullCacheKey, finalValue);
2073
2175
  continue;
2074
2176
  }
2075
2177
  }
2076
2178
  }
2077
2179
  result.set(key, resolved);
2180
+ this.cache.set(fullCacheKey, resolved);
2181
+ this.cacheDescendants(fullCacheKey, resolved);
2078
2182
  } else {
2079
2183
  const prior = obj.priorValues.get(key);
2080
2184
  if (prior !== void 0) {
2081
2185
  const priorResolved = this.resolveVal(prior, obj);
2082
- if (priorResolved !== void 0) result.set(key, priorResolved);
2186
+ if (priorResolved !== void 0) {
2187
+ result.set(key, priorResolved);
2188
+ this.cache.set(fullCacheKey, priorResolved);
2189
+ this.cacheDescendants(fullCacheKey, priorResolved);
2190
+ }
2083
2191
  }
2084
2192
  }
2085
2193
  }
2086
2194
  return { kind: "object", fields: result };
2087
2195
  }
2196
+ cacheDescendants(prefix, value) {
2197
+ if (value.kind !== "object") return;
2198
+ for (const [key, child] of value.fields) {
2199
+ const childKey = `${prefix}.${stringSegmentsToKey([key])}`;
2200
+ this.cache.set(childKey, child);
2201
+ this.cacheDescendants(childKey, child);
2202
+ }
2203
+ }
2088
2204
  resolveVal(v, scope) {
2089
2205
  if (isSubst(v)) return this.resolveSubst(v, scope);
2090
2206
  if (isConcat(v)) {
@@ -2096,7 +2212,6 @@ var SubstitutionResolver = class {
2096
2212
  this.resolvingConcats.delete(v);
2097
2213
  }
2098
2214
  }
2099
- if (isAppend(v)) return this.resolveAppend(v, scope);
2100
2215
  if (isResObj(v)) return this.resolveResObj(v);
2101
2216
  const hv = v;
2102
2217
  if (hv.kind === "array") {
@@ -2110,6 +2225,7 @@ var SubstitutionResolver = class {
2110
2225
  return hv;
2111
2226
  }
2112
2227
  resolveSubst(s, scope) {
2228
+ if (s.knownAbsent) return void 0;
2113
2229
  const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
2114
2230
  if (this.cache.has(key)) return this.cache.get(key);
2115
2231
  if (this.resolving.has(key)) {
@@ -2202,6 +2318,28 @@ var SubstitutionResolver = class {
2202
2318
  if (result !== void 0) this.cache.set(key, result);
2203
2319
  return result;
2204
2320
  }
2321
+ if (s.prefixLen > 0 && s.segments.length > s.prefixLen) {
2322
+ const originalSegments = s.segments.slice(s.prefixLen);
2323
+ const fallbackFound = lookupPath(this.root, originalSegments);
2324
+ if (fallbackFound !== void 0) {
2325
+ let result = this.resolveVal(fallbackFound, scope);
2326
+ if (originalSegments.length === 1 && result !== void 0 && result.kind === "object") {
2327
+ const rootSeg = originalSegments[0]?.text ?? "";
2328
+ const prior = this.root.priorValues.get(rootSeg);
2329
+ if (prior !== void 0) {
2330
+ const priorResolved = this.resolveVal(prior, scope);
2331
+ if (priorResolved !== void 0 && priorResolved.kind === "object") {
2332
+ result = deepMergeHoconValues(
2333
+ priorResolved,
2334
+ result
2335
+ );
2336
+ }
2337
+ }
2338
+ }
2339
+ if (result !== void 0) this.cache.set(key, result);
2340
+ return result;
2341
+ }
2342
+ }
2205
2343
  if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
2206
2344
  const result = this.resolveEnvList(s);
2207
2345
  if (result !== void 0) {
@@ -2358,38 +2496,6 @@ var SubstitutionResolver = class {
2358
2496
  }
2359
2497
  return folded;
2360
2498
  }
2361
- resolveAppend(a, scope) {
2362
- const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };
2363
- if (this.opts.allowUnresolved && (isSubst(existing) || isConcat(existing) || isAppend(existing))) {
2364
- return a;
2365
- }
2366
- const elem = this.resolveVal(a.elem, scope);
2367
- let priorItems;
2368
- if (existing.kind === "array") {
2369
- priorItems = existing.items;
2370
- } else if (existing.kind === "object") {
2371
- const converted = numericObjectToArray(existing);
2372
- if (converted === null) {
2373
- throw new ResolveError(
2374
- `'+=' on non-array value: prior value is object with non-numeric keys (spec L732)`,
2375
- "",
2376
- 0,
2377
- 0
2378
- );
2379
- }
2380
- priorItems = converted;
2381
- } else {
2382
- throw new ResolveError(
2383
- `'+=' on non-array value: prior value is ${existing.kind} (spec L732)`,
2384
- "",
2385
- 0,
2386
- 0
2387
- );
2388
- }
2389
- const items = [...priorItems];
2390
- if (elem !== void 0) items.push(elem);
2391
- return { kind: "array", items };
2392
- }
2393
2499
  };
2394
2500
 
2395
2501
  // src/internal/resolver/resolver.ts
@@ -2407,7 +2513,7 @@ function containsPlaceholders(tree) {
2407
2513
  }
2408
2514
  function valContainsPlaceholders(v) {
2409
2515
  const rv = v;
2410
- if (isSubst(rv) || isConcat(rv) || isAppend(rv)) return true;
2516
+ if (isSubst(rv) || isConcat(rv)) return true;
2411
2517
  if (isResObj(rv)) return containsPlaceholders(rv);
2412
2518
  const hv = v;
2413
2519
  if (hv?.kind === "array") {
@@ -2418,7 +2524,7 @@ function valContainsPlaceholders(v) {
2418
2524
  function buildPartialHoconFromResObj(tree) {
2419
2525
  const fields = /* @__PURE__ */ new Map();
2420
2526
  for (const [k, v] of tree.fields) {
2421
- if (!isSubst(v) && !isConcat(v) && !isAppend(v)) {
2527
+ if (!isSubst(v) && !isConcat(v)) {
2422
2528
  if (isResObj(v)) {
2423
2529
  fields.set(k, buildPartialHoconFromResObj(v));
2424
2530
  } else {
@@ -2807,7 +2913,7 @@ function hoconToJs(v) {
2807
2913
  function resObjToKeyShape(tree) {
2808
2914
  const fields = /* @__PURE__ */ new Map();
2809
2915
  for (const [k, v] of tree.fields) {
2810
- if (isSubst(v) || isConcat(v) || isAppend(v)) {
2916
+ if (isSubst(v) || isConcat(v)) {
2811
2917
  fields.set(k, { kind: "scalar", raw: "", valueType: "null" });
2812
2918
  } else if (isResObj(v)) {
2813
2919
  fields.set(k, resObjToKeyShape(v));
@@ -2821,7 +2927,7 @@ function stripPlaceholderFields(v) {
2821
2927
  const fields = /* @__PURE__ */ new Map();
2822
2928
  let hadPlaceholders = false;
2823
2929
  for (const [k, val] of v.fields) {
2824
- if (val._kind === "subst-placeholder" || val._kind === "concat-placeholder" || val._kind === "append-placeholder") {
2930
+ if (val._kind === "subst-placeholder" || val._kind === "concat-placeholder") {
2825
2931
  hadPlaceholders = true;
2826
2932
  } else if (val.kind === "object") {
2827
2933
  const inner = stripPlaceholderFields(val);