@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/index.js CHANGED
@@ -237,55 +237,6 @@ var NotResolvedError = class extends ConfigError {
237
237
  }
238
238
  };
239
239
 
240
- // src/internal/resolver/types.ts
241
- var separatorValues = /* @__PURE__ */ new WeakSet();
242
- function isSubst(v) {
243
- return v._kind === "subst-placeholder";
244
- }
245
- function isConcat(v) {
246
- return v._kind === "concat-placeholder";
247
- }
248
- function isAppend(v) {
249
- return v._kind === "append-placeholder";
250
- }
251
- function isResObj(v) {
252
- return v._kind === "res-obj";
253
- }
254
- function makeResObj() {
255
- return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
256
- }
257
- function mergeUnresolved(receiver, fallback) {
258
- const result = makeResObj();
259
- for (const [k, v] of fallback.fields) {
260
- result.fields.set(k, v);
261
- }
262
- for (const [k, v] of fallback.priorValues) {
263
- result.priorValues.set(k, v);
264
- }
265
- for (const [k, rv] of receiver.fields) {
266
- const existing = result.fields.get(k);
267
- if (existing !== void 0) {
268
- const recPrior = receiver.priorValues.get(k);
269
- if (recPrior !== void 0 && !isResObj(recPrior)) {
270
- result.fields.set(k, rv);
271
- continue;
272
- }
273
- if (isResObj(rv) && isResObj(existing)) {
274
- result.fields.set(k, mergeUnresolved(rv, existing));
275
- continue;
276
- }
277
- result.priorValues.set(k, existing);
278
- }
279
- result.fields.set(k, rv);
280
- }
281
- for (const [k, v] of receiver.priorValues) {
282
- if (receiver.fields.has(k)) {
283
- result.priorValues.set(k, v);
284
- }
285
- }
286
- return result;
287
- }
288
-
289
240
  // src/internal/resolver/fold-self-ref.ts
290
241
  function substFullKey(s) {
291
242
  return stringSegmentsToKey(s.segments.map((seg) => seg.text));
@@ -299,11 +250,8 @@ function stringSegmentsToKey(segments) {
299
250
  }).join(".");
300
251
  }
301
252
  function containsSelfRef(v, fullKey) {
302
- if (isSubst(v)) return substFullKey(v) === fullKey;
253
+ if (isSubst(v)) return !v.knownAbsent && substFullKey(v) === fullKey;
303
254
  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
255
  if (isResObj(v)) {
308
256
  for (const f of v.fields.values()) {
309
257
  if (containsSelfRef(f, fullKey)) return true;
@@ -334,19 +282,12 @@ function foldSelfRef(v, fullKey, replacement) {
334
282
  col: v.col
335
283
  };
336
284
  }
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
285
  if (isResObj(v)) {
345
286
  const newFields = /* @__PURE__ */ new Map();
346
287
  for (const [k, val] of v.fields) {
347
288
  newFields.set(k, foldSelfRef(val, fullKey, replacement));
348
289
  }
349
- return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
290
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
350
291
  }
351
292
  const hv = v;
352
293
  if (hv.kind === "array") {
@@ -364,17 +305,106 @@ function foldSelfRef(v, fullKey, replacement) {
364
305
  }
365
306
  return v;
366
307
  }
308
+ function foldKnownAbsentSelfRef(v, fullKey, replacement) {
309
+ if (isSubst(v)) {
310
+ return v.knownAbsent && substFullKey(v) === fullKey ? replacement : v;
311
+ }
312
+ if (isConcat(v)) {
313
+ return {
314
+ _kind: "concat-placeholder",
315
+ nodes: v.nodes.map((n) => foldKnownAbsentSelfRef(n, fullKey, replacement)),
316
+ line: v.line,
317
+ col: v.col
318
+ };
319
+ }
320
+ if (isResObj(v)) {
321
+ const newFields = /* @__PURE__ */ new Map();
322
+ for (const [k, val] of v.fields) {
323
+ newFields.set(k, foldKnownAbsentSelfRef(val, fullKey, replacement));
324
+ }
325
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
326
+ }
327
+ const hv = v;
328
+ if (hv.kind === "array") {
329
+ return {
330
+ kind: "array",
331
+ items: hv.items.map((item) => foldKnownAbsentSelfRef(item, fullKey, replacement))
332
+ };
333
+ }
334
+ if (hv.kind === "object") {
335
+ const newFields = /* @__PURE__ */ new Map();
336
+ for (const [k, val] of hv.fields) {
337
+ newFields.set(k, foldKnownAbsentSelfRef(val, fullKey, replacement));
338
+ }
339
+ return { kind: "object", fields: newFields };
340
+ }
341
+ return v;
342
+ }
367
343
  function foldOrSkipPrior(prior, fullKey, old) {
368
344
  if (!containsSelfRef(prior, fullKey)) return cloneResolverValue(prior);
369
- if (old === void 0) return void 0;
345
+ if (old === void 0) return foldOptionalSelfRefAbsent(prior, fullKey);
370
346
  return foldSelfRef(prior, fullKey, old);
371
347
  }
348
+ function foldOptionalSelfRefAbsent(v, fullKey) {
349
+ if (isSubst(v) && substFullKey(v) === fullKey) {
350
+ if (!v.optional) return void 0;
351
+ return {
352
+ _kind: "subst-placeholder",
353
+ segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
354
+ optional: v.optional,
355
+ knownAbsent: true,
356
+ listSuffix: v.listSuffix,
357
+ line: v.line,
358
+ col: v.col,
359
+ prefixLen: v.prefixLen
360
+ };
361
+ }
362
+ if (isConcat(v)) {
363
+ const nodes = [];
364
+ for (const node of v.nodes) {
365
+ const folded = foldOptionalSelfRefAbsent(node, fullKey);
366
+ if (folded === void 0) return void 0;
367
+ nodes.push(folded);
368
+ }
369
+ return { _kind: "concat-placeholder", nodes, line: v.line, col: v.col };
370
+ }
371
+ if (isResObj(v)) {
372
+ const fields = /* @__PURE__ */ new Map();
373
+ for (const [key, value] of v.fields) {
374
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
375
+ if (folded === void 0) return void 0;
376
+ fields.set(key, folded);
377
+ }
378
+ return { _kind: "res-obj", fields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
379
+ }
380
+ const hv = v;
381
+ if (hv.kind === "array") {
382
+ const items = [];
383
+ for (const item of hv.items) {
384
+ const folded = foldOptionalSelfRefAbsent(item, fullKey);
385
+ if (folded === void 0) return void 0;
386
+ items.push(folded);
387
+ }
388
+ return { kind: "array", items };
389
+ }
390
+ if (hv.kind === "object") {
391
+ const fields = /* @__PURE__ */ new Map();
392
+ for (const [key, value] of hv.fields) {
393
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
394
+ if (folded === void 0) return void 0;
395
+ fields.set(key, folded);
396
+ }
397
+ return { kind: "object", fields };
398
+ }
399
+ return cloneResolverValue(v);
400
+ }
372
401
  function cloneResolverValue(v) {
373
402
  if (isSubst(v)) {
374
403
  return {
375
404
  _kind: "subst-placeholder",
376
405
  segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
377
406
  optional: v.optional,
407
+ knownAbsent: v.knownAbsent,
378
408
  listSuffix: v.listSuffix,
379
409
  line: v.line,
380
410
  col: v.col,
@@ -394,14 +424,7 @@ function cloneResolverValue(v) {
394
424
  for (const [k, val] of v.fields) newFields.set(k, cloneResolverValue(val));
395
425
  const newPriors = /* @__PURE__ */ new Map();
396
426
  for (const [k, val] of v.priorValues) newPriors.set(k, cloneResolverValue(val));
397
- return { _kind: "res-obj", fields: newFields, priorValues: newPriors };
398
- }
399
- if (isAppend(v)) {
400
- return {
401
- _kind: "append-placeholder",
402
- existing: cloneResolverValue(v.existing),
403
- elem: cloneResolverValue(v.elem)
404
- };
427
+ return { _kind: "res-obj", fields: newFields, priorValues: newPriors, resetKeys: new Set(v.resetKeys) };
405
428
  }
406
429
  const hv = v;
407
430
  if (hv.kind === "array") {
@@ -436,14 +459,11 @@ function foldNestedSelfRefs(v, pathPrefix) {
436
459
  }
437
460
  newFields.set(k, finalVal);
438
461
  }
439
- return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
462
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
440
463
  }
441
464
  function containsSubstByPath(v, target) {
442
- if (isSubst(v)) return segmentsTextEqual(v.segments, target);
465
+ if (isSubst(v)) return !v.knownAbsent && segmentsTextEqual(v.segments, target);
443
466
  if (isConcat(v)) return v.nodes.some((n) => containsSubstByPath(n, target));
444
- if (isAppend(v)) {
445
- return containsSubstByPath(v.existing, target) || containsSubstByPath(v.elem, target);
446
- }
447
467
  if (isResObj(v)) {
448
468
  for (const f of v.fields.values()) {
449
469
  if (containsSubstByPath(f, target)) return true;
@@ -472,6 +492,53 @@ function segmentsTextEqual(a, b) {
472
492
  return true;
473
493
  }
474
494
 
495
+ // src/internal/resolver/types.ts
496
+ var separatorValues = /* @__PURE__ */ new WeakSet();
497
+ function isSubst(v) {
498
+ return v._kind === "subst-placeholder";
499
+ }
500
+ function isConcat(v) {
501
+ return v._kind === "concat-placeholder";
502
+ }
503
+ function isResObj(v) {
504
+ return v._kind === "res-obj";
505
+ }
506
+ function makeResObj() {
507
+ return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map(), resetKeys: /* @__PURE__ */ new Set() };
508
+ }
509
+ function mergeUnresolved(receiver, fallback) {
510
+ const result = makeResObj();
511
+ for (const [k, v] of fallback.fields) {
512
+ result.fields.set(k, v);
513
+ }
514
+ for (const [k, v] of fallback.priorValues) {
515
+ result.priorValues.set(k, v);
516
+ }
517
+ for (const [k, rv] of receiver.fields) {
518
+ const existing = result.fields.get(k);
519
+ if (existing !== void 0) {
520
+ const recPrior = receiver.priorValues.get(k);
521
+ if (recPrior !== void 0 && !isResObj(recPrior)) {
522
+ result.fields.set(k, rv);
523
+ continue;
524
+ }
525
+ if (isResObj(rv) && isResObj(existing)) {
526
+ result.fields.set(k, mergeUnresolved(rv, existing));
527
+ continue;
528
+ }
529
+ const foldedPrior = foldOrSkipPrior(existing, k, void 0);
530
+ if (foldedPrior !== void 0) result.priorValues.set(k, foldedPrior);
531
+ }
532
+ result.fields.set(k, rv);
533
+ }
534
+ for (const [k, v] of receiver.priorValues) {
535
+ if (receiver.fields.has(k)) {
536
+ result.priorValues.set(k, v);
537
+ }
538
+ }
539
+ return result;
540
+ }
541
+
475
542
  // src/internal/resolver/utils.ts
476
543
  function segmentsToKey(segments) {
477
544
  return segments.map((s) => {
@@ -525,9 +592,19 @@ function deepMergeResObjInto(dst, src, pathPrefix = []) {
525
592
  deepMergeResObjInto(dstVal, srcVal, childPrefix);
526
593
  } else {
527
594
  if (dstVal !== void 0) {
528
- const priorExisting = dst.priorValues.get(k);
529
- const prior = foldOrSkipPrior(dstVal, fullKey, priorExisting);
530
- if (prior !== void 0) dst.priorValues.set(k, prior);
595
+ if (src.resetKeys.has(k)) {
596
+ dst.priorValues.delete(k);
597
+ } else {
598
+ const dstFolded = foldOrSkipPrior(dstVal, fullKey, dst.priorValues.get(k));
599
+ if (dstFolded !== void 0) {
600
+ const srcPrior = src.priorValues.get(k);
601
+ if (srcPrior !== void 0) {
602
+ dst.priorValues.set(k, foldKnownAbsentSelfRef(srcPrior, fullKey, dstFolded));
603
+ } else {
604
+ dst.priorValues.set(k, dstFolded);
605
+ }
606
+ }
607
+ }
531
608
  }
532
609
  dst.fields.set(k, srcVal);
533
610
  }
@@ -537,6 +614,7 @@ function deepMergeResObjInto(dst, src, pathPrefix = []) {
537
614
  dst.priorValues.set(k, srcPrior);
538
615
  }
539
616
  }
617
+ for (const k of src.resetKeys) dst.resetKeys.add(k);
540
618
  }
541
619
  function hoconValueToResObj(hv) {
542
620
  const obj = makeResObj();
@@ -606,6 +684,11 @@ var Lexer = class {
606
684
  this.line = 1;
607
685
  this.col = 1;
608
686
  this.hadSpace = false;
687
+ // Accumulates literal whitespace chars consumed between tokens. Reset on every
688
+ // token push. Used so the parser can preserve path-expression whitespace
689
+ // (xx.hocon#42, E13) — e.g. tab vs space adjacent to dots in key paths.
690
+ // Comment text is NOT accumulated; only the actual WS chars.
691
+ this.whitespaceBuffer = "";
609
692
  this.tokens = [];
610
693
  if (input.charCodeAt(0) === 65279) this.input = input.slice(1);
611
694
  }
@@ -621,6 +704,7 @@ var Lexer = class {
621
704
  continue;
622
705
  }
623
706
  if (isHoconWhitespace(ch)) {
707
+ this.whitespaceBuffer += ch;
624
708
  this.advance();
625
709
  this.hadSpace = true;
626
710
  continue;
@@ -712,7 +796,7 @@ var Lexer = class {
712
796
  }
713
797
  throw new ParseError(`unexpected character: ${JSON.stringify(ch)}`, sl, sc);
714
798
  }
715
- this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false });
799
+ this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false, precedingWhitespace: "" });
716
800
  return this.tokens;
717
801
  }
718
802
  peek(offset = 0) {
@@ -729,12 +813,14 @@ var Lexer = class {
729
813
  return ch;
730
814
  }
731
815
  push(kind, value, l, c, isQuoted = false) {
732
- this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
816
+ this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer });
733
817
  this.hadSpace = false;
818
+ this.whitespaceBuffer = "";
734
819
  }
735
820
  pushSubst(payload, value, l, c) {
736
- this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
821
+ this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer, subst: payload });
737
822
  this.hadSpace = false;
823
+ this.whitespaceBuffer = "";
738
824
  }
739
825
  /**
740
826
  * Read body of a quoted string. Opening `"` must already be consumed.
@@ -980,7 +1066,7 @@ function isUnquotedContinue(ch, nextFn) {
980
1066
  }
981
1067
 
982
1068
  // src/internal/parser/parser.ts
983
- var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
1069
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, precedingWhitespace: "", subst: void 0 };
984
1070
  var Parser = class {
985
1071
  constructor(tokens) {
986
1072
  this.tokens = tokens;
@@ -1093,13 +1179,17 @@ var Parser = class {
1093
1179
  let trailingDot = false;
1094
1180
  let firstWasQuoted = false;
1095
1181
  let spaceConcat = false;
1182
+ let postDotPrefix = "";
1096
1183
  while (true) {
1097
1184
  const t = this.peek();
1098
1185
  if (t.kind === "string") {
1099
1186
  this.advance();
1100
1187
  if (segments.length === 0) firstWasQuoted = true;
1101
1188
  if (spaceConcat) {
1102
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${t.value}`;
1189
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}${t.value}`;
1190
+ } else if (postDotPrefix !== "") {
1191
+ segments.push(`${postDotPrefix}${t.value}`);
1192
+ postDotPrefix = "";
1103
1193
  } else {
1104
1194
  segments.push(t.value);
1105
1195
  }
@@ -1109,24 +1199,23 @@ var Parser = class {
1109
1199
  const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
1110
1200
  const parts = raw.split(".");
1111
1201
  const filtered = parts.filter((s) => s.length > 0);
1112
- for (const seg of filtered) {
1113
- if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
1114
- const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
1115
- throw new ParseError(
1116
- `unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
1117
- t.line,
1118
- t.col
1119
- );
1120
- }
1121
- }
1122
- if (spaceConcat && filtered.length > 0) {
1202
+ if (spaceConcat) {
1203
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}`;
1123
1204
  if (raw.startsWith(".")) {
1124
1205
  segments.push(...filtered);
1125
- } else {
1206
+ } else if (filtered.length > 0) {
1126
1207
  const [head, ...tail] = filtered;
1127
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${head}`;
1208
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${head}`;
1128
1209
  segments.push(...tail);
1129
1210
  }
1211
+ } else if (postDotPrefix !== "" && raw.startsWith(".")) {
1212
+ segments.push(postDotPrefix);
1213
+ postDotPrefix = "";
1214
+ segments.push(...filtered);
1215
+ } else if (postDotPrefix !== "" && filtered.length > 0) {
1216
+ const [head, ...tail] = filtered;
1217
+ segments.push(`${postDotPrefix}${head}`, ...tail);
1218
+ postDotPrefix = "";
1130
1219
  } else {
1131
1220
  segments.push(...filtered);
1132
1221
  }
@@ -1136,11 +1225,23 @@ var Parser = class {
1136
1225
  break;
1137
1226
  }
1138
1227
  spaceConcat = false;
1139
- if (trailingDot) continue;
1228
+ if (trailingDot) {
1229
+ const next2 = this.peek();
1230
+ if ((next2.kind === "unquoted" || next2.kind === "string") && next2.precedingWhitespace.length > 0) {
1231
+ postDotPrefix = next2.precedingWhitespace;
1232
+ } else {
1233
+ postDotPrefix = "";
1234
+ }
1235
+ continue;
1236
+ }
1140
1237
  const next = this.peek();
1141
1238
  if (next.kind === "unquoted" && next.value === "." && !next.precedingSpace) {
1142
1239
  this.advance();
1143
1240
  trailingDot = true;
1241
+ const afterDot = this.peek();
1242
+ if ((afterDot.kind === "unquoted" || afterDot.kind === "string") && afterDot.precedingWhitespace.length > 0) {
1243
+ postDotPrefix = afterDot.precedingWhitespace;
1244
+ }
1144
1245
  continue;
1145
1246
  }
1146
1247
  if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
@@ -1153,6 +1254,14 @@ var Parser = class {
1153
1254
  }
1154
1255
  break;
1155
1256
  }
1257
+ if (trailingDot) {
1258
+ const here = this.peek();
1259
+ throw new ParseError(
1260
+ `path has a trailing period '.' \u2014 empty key segment not allowed (HOCON.md path rules)`,
1261
+ here.line,
1262
+ here.col
1263
+ );
1264
+ }
1156
1265
  return { segments, firstWasQuoted };
1157
1266
  }
1158
1267
  parseInclude() {
@@ -1325,7 +1434,8 @@ var Parser = class {
1325
1434
  break;
1326
1435
  }
1327
1436
  if (hadSpace) {
1328
- parts.push({ kind: "scalar", raw: " ", valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1437
+ const sep = t.precedingWhitespace.length > 0 ? t.precedingWhitespace : " ";
1438
+ parts.push({ kind: "scalar", raw: sep, valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1329
1439
  }
1330
1440
  parts.push(node);
1331
1441
  }
@@ -1760,35 +1870,19 @@ var StructureBuilder = class _StructureBuilder {
1760
1870
  const childPrefix = [...pathPrefix, head];
1761
1871
  const fullKey = stringSegmentsToKey(childPrefix);
1762
1872
  if (field.append) {
1763
- const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1764
- const oldPrior = obj.priorValues.get(head);
1765
- const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1766
- if (folded !== void 0) obj.priorValues.set(head, folded);
1767
- obj.fields.set(head, {
1768
- _kind: "append-placeholder",
1769
- // Use the folded existing (self-ref-free) for the Append. When the
1770
- // fold returned undefined (self-ref present but no old prior),
1771
- // fall back to the original existing — resolveSubst will then
1772
- // raise the "self-ref with no prior value" error at resolve time,
1773
- // matching the pre-fix behaviour for that edge case.
1774
- //
1775
- // Clone `folded` again so the Append.existing and priorValues[head]
1776
- // do not share the same Subst/Concat references — relativizeResObj
1777
- // walks BOTH `fields` and `priorValues` and mutates Subst.segments
1778
- // in place, so a shared reference would get its prefix applied
1779
- // twice (resulting in e.g. `${outer.outer.base}` instead of
1780
- // `${outer.base}`). Codex review on PR #131.
1781
- existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1782
- elem: this.astToResolverValue(field.value, childPrefix)
1783
- });
1873
+ const synthetic = this.desugarAppend(field, childPrefix);
1874
+ this.applyField(obj, synthetic, pathPrefix);
1784
1875
  return;
1785
1876
  }
1786
1877
  const existing = obj.fields.get(head);
1787
1878
  const newVal = this.astToResolverValue(field.value, childPrefix);
1879
+ if (!containsSelfRef(newVal, fullKey)) {
1880
+ obj.resetKeys.add(head);
1881
+ }
1788
1882
  if (existing !== void 0) {
1789
- const exFolded = foldNestedSelfRefs(existing, childPrefix);
1790
1883
  const oldPrior = obj.priorValues.get(head);
1791
- const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1884
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1885
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1792
1886
  if (prior !== void 0) obj.priorValues.set(head, prior);
1793
1887
  }
1794
1888
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
@@ -1826,23 +1920,19 @@ var StructureBuilder = class _StructureBuilder {
1826
1920
  const childPrefix = [...pathPrefix, head];
1827
1921
  const fullKey = stringSegmentsToKey(childPrefix);
1828
1922
  if (field.append) {
1829
- const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1830
- const oldPrior = obj.priorValues.get(head);
1831
- const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1832
- if (folded !== void 0) obj.priorValues.set(head, folded);
1833
- obj.fields.set(head, {
1834
- _kind: "append-placeholder",
1835
- existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1836
- elem: await this.astToResolverValueAsync(field.value, childPrefix)
1837
- });
1923
+ const synthetic = this.desugarAppend(field, childPrefix);
1924
+ await this.applyFieldAsync(obj, synthetic, pathPrefix);
1838
1925
  return;
1839
1926
  }
1840
1927
  const existing = obj.fields.get(head);
1841
1928
  const newVal = await this.astToResolverValueAsync(field.value, childPrefix);
1929
+ if (!containsSelfRef(newVal, fullKey)) {
1930
+ obj.resetKeys.add(head);
1931
+ }
1842
1932
  if (existing !== void 0) {
1843
- const exFolded = foldNestedSelfRefs(existing, childPrefix);
1844
1933
  const oldPrior = obj.priorValues.get(head);
1845
- const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1934
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1935
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1846
1936
  if (prior !== void 0) obj.priorValues.set(head, prior);
1847
1937
  }
1848
1938
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
@@ -1851,6 +1941,21 @@ var StructureBuilder = class _StructureBuilder {
1851
1941
  }
1852
1942
  obj.fields.set(head, newVal);
1853
1943
  }
1944
+ /** Builds the `key = ${?fullkey} [value]` field that `key += value` desugars
1945
+ * to (S13b.2, HOCON.md L732). `childPrefix` is the field's fully-qualified
1946
+ * path (pathPrefix + key), so a nested `srv { items += x }` references
1947
+ * `${?srv.items}`; include relativization rewrites it under a mount prefix.
1948
+ * `field.key` is already single-segment here (multi-segment keys are split
1949
+ * into nested objects upstream). Returns a NEW AstField (append=false);
1950
+ * positions inherit from the original field so resolve-time errors (the
1951
+ * S13b.2 non-array check, now via the scalar+array concat) keep a location. */
1952
+ desugarAppend(field, childPrefix) {
1953
+ const segments = childPrefix.map((text) => ({ text, line: field.pos.line, col: field.pos.col }));
1954
+ const subst = { kind: "subst", segments, optional: true, listSuffix: false, pos: field.pos };
1955
+ const elemArray = { kind: "array", items: [field.value], pos: field.pos };
1956
+ const synthetic = { kind: "concat", nodes: [subst, elemArray], pos: field.pos };
1957
+ return { key: field.key, value: synthetic, append: false, pos: field.pos };
1958
+ }
1854
1959
  astToResolverValue(ast, pathPrefix) {
1855
1960
  switch (ast.kind) {
1856
1961
  case "scalar": {
@@ -1865,7 +1970,7 @@ var StructureBuilder = class _StructureBuilder {
1865
1970
  return inner;
1866
1971
  }
1867
1972
  case "subst":
1868
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1973
+ 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 };
1869
1974
  case "concat":
1870
1975
  return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
1871
1976
  case "include":
@@ -1889,7 +1994,7 @@ var StructureBuilder = class _StructureBuilder {
1889
1994
  case "object":
1890
1995
  return await this.buildAsync(ast, pathPrefix);
1891
1996
  case "subst":
1892
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1997
+ 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 };
1893
1998
  case "concat": {
1894
1999
  const nodes = [];
1895
2000
  for (const n of ast.nodes) {
@@ -1915,11 +2020,6 @@ var StructureBuilder = class _StructureBuilder {
1915
2020
  }
1916
2021
  return;
1917
2022
  }
1918
- if (isAppend(val)) {
1919
- this.relativizeSubstPaths(val.existing, prefixSegments);
1920
- this.relativizeSubstPaths(val.elem, prefixSegments);
1921
- return;
1922
- }
1923
2023
  if (isResObj(val)) {
1924
2024
  this.relativizeResObj(val, prefixSegments);
1925
2025
  return;
@@ -2007,6 +2107,8 @@ var SubstitutionResolver = class {
2007
2107
  const result = /* @__PURE__ */ new Map();
2008
2108
  for (const [key, val] of obj.fields) {
2009
2109
  this.resolvingFieldPath.push(key);
2110
+ const fullCacheKey = stringSegmentsToKey(this.resolvingFieldPath);
2111
+ this.cache.delete(fullCacheKey);
2010
2112
  let resolved;
2011
2113
  try {
2012
2114
  resolved = this.resolveVal(val, obj);
@@ -2019,28 +2121,42 @@ var SubstitutionResolver = class {
2019
2121
  if (prior !== void 0) {
2020
2122
  const priorResolved = this.resolveVal(prior, obj);
2021
2123
  if (priorResolved !== void 0 && priorResolved.kind === "object") {
2022
- result.set(
2023
- key,
2024
- deepMergeHoconValues(
2025
- priorResolved,
2026
- resolved
2027
- )
2124
+ const finalValue = deepMergeHoconValues(
2125
+ priorResolved,
2126
+ resolved
2028
2127
  );
2128
+ result.set(key, finalValue);
2129
+ this.cache.set(fullCacheKey, finalValue);
2130
+ this.cacheDescendants(fullCacheKey, finalValue);
2029
2131
  continue;
2030
2132
  }
2031
2133
  }
2032
2134
  }
2033
2135
  result.set(key, resolved);
2136
+ this.cache.set(fullCacheKey, resolved);
2137
+ this.cacheDescendants(fullCacheKey, resolved);
2034
2138
  } else {
2035
2139
  const prior = obj.priorValues.get(key);
2036
2140
  if (prior !== void 0) {
2037
2141
  const priorResolved = this.resolveVal(prior, obj);
2038
- if (priorResolved !== void 0) result.set(key, priorResolved);
2142
+ if (priorResolved !== void 0) {
2143
+ result.set(key, priorResolved);
2144
+ this.cache.set(fullCacheKey, priorResolved);
2145
+ this.cacheDescendants(fullCacheKey, priorResolved);
2146
+ }
2039
2147
  }
2040
2148
  }
2041
2149
  }
2042
2150
  return { kind: "object", fields: result };
2043
2151
  }
2152
+ cacheDescendants(prefix, value) {
2153
+ if (value.kind !== "object") return;
2154
+ for (const [key, child] of value.fields) {
2155
+ const childKey = `${prefix}.${stringSegmentsToKey([key])}`;
2156
+ this.cache.set(childKey, child);
2157
+ this.cacheDescendants(childKey, child);
2158
+ }
2159
+ }
2044
2160
  resolveVal(v, scope) {
2045
2161
  if (isSubst(v)) return this.resolveSubst(v, scope);
2046
2162
  if (isConcat(v)) {
@@ -2052,7 +2168,6 @@ var SubstitutionResolver = class {
2052
2168
  this.resolvingConcats.delete(v);
2053
2169
  }
2054
2170
  }
2055
- if (isAppend(v)) return this.resolveAppend(v, scope);
2056
2171
  if (isResObj(v)) return this.resolveResObj(v);
2057
2172
  const hv = v;
2058
2173
  if (hv.kind === "array") {
@@ -2066,6 +2181,7 @@ var SubstitutionResolver = class {
2066
2181
  return hv;
2067
2182
  }
2068
2183
  resolveSubst(s, scope) {
2184
+ if (s.knownAbsent) return void 0;
2069
2185
  const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
2070
2186
  if (this.cache.has(key)) return this.cache.get(key);
2071
2187
  if (this.resolving.has(key)) {
@@ -2158,6 +2274,28 @@ var SubstitutionResolver = class {
2158
2274
  if (result !== void 0) this.cache.set(key, result);
2159
2275
  return result;
2160
2276
  }
2277
+ if (s.prefixLen > 0 && s.segments.length > s.prefixLen) {
2278
+ const originalSegments = s.segments.slice(s.prefixLen);
2279
+ const fallbackFound = lookupPath(this.root, originalSegments);
2280
+ if (fallbackFound !== void 0) {
2281
+ let result = this.resolveVal(fallbackFound, scope);
2282
+ if (originalSegments.length === 1 && result !== void 0 && result.kind === "object") {
2283
+ const rootSeg = originalSegments[0]?.text ?? "";
2284
+ const prior = this.root.priorValues.get(rootSeg);
2285
+ if (prior !== void 0) {
2286
+ const priorResolved = this.resolveVal(prior, scope);
2287
+ if (priorResolved !== void 0 && priorResolved.kind === "object") {
2288
+ result = deepMergeHoconValues(
2289
+ priorResolved,
2290
+ result
2291
+ );
2292
+ }
2293
+ }
2294
+ }
2295
+ if (result !== void 0) this.cache.set(key, result);
2296
+ return result;
2297
+ }
2298
+ }
2161
2299
  if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
2162
2300
  const result = this.resolveEnvList(s);
2163
2301
  if (result !== void 0) {
@@ -2314,38 +2452,6 @@ var SubstitutionResolver = class {
2314
2452
  }
2315
2453
  return folded;
2316
2454
  }
2317
- resolveAppend(a, scope) {
2318
- const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };
2319
- if (this.opts.allowUnresolved && (isSubst(existing) || isConcat(existing) || isAppend(existing))) {
2320
- return a;
2321
- }
2322
- const elem = this.resolveVal(a.elem, scope);
2323
- let priorItems;
2324
- if (existing.kind === "array") {
2325
- priorItems = existing.items;
2326
- } else if (existing.kind === "object") {
2327
- const converted = numericObjectToArray(existing);
2328
- if (converted === null) {
2329
- throw new ResolveError(
2330
- `'+=' on non-array value: prior value is object with non-numeric keys (spec L732)`,
2331
- "",
2332
- 0,
2333
- 0
2334
- );
2335
- }
2336
- priorItems = converted;
2337
- } else {
2338
- throw new ResolveError(
2339
- `'+=' on non-array value: prior value is ${existing.kind} (spec L732)`,
2340
- "",
2341
- 0,
2342
- 0
2343
- );
2344
- }
2345
- const items = [...priorItems];
2346
- if (elem !== void 0) items.push(elem);
2347
- return { kind: "array", items };
2348
- }
2349
2455
  };
2350
2456
 
2351
2457
  // src/internal/resolver/resolver.ts
@@ -2363,7 +2469,7 @@ function containsPlaceholders(tree) {
2363
2469
  }
2364
2470
  function valContainsPlaceholders(v) {
2365
2471
  const rv = v;
2366
- if (isSubst(rv) || isConcat(rv) || isAppend(rv)) return true;
2472
+ if (isSubst(rv) || isConcat(rv)) return true;
2367
2473
  if (isResObj(rv)) return containsPlaceholders(rv);
2368
2474
  const hv = v;
2369
2475
  if (hv?.kind === "array") {
@@ -2374,7 +2480,7 @@ function valContainsPlaceholders(v) {
2374
2480
  function buildPartialHoconFromResObj(tree) {
2375
2481
  const fields = /* @__PURE__ */ new Map();
2376
2482
  for (const [k, v] of tree.fields) {
2377
- if (!isSubst(v) && !isConcat(v) && !isAppend(v)) {
2483
+ if (!isSubst(v) && !isConcat(v)) {
2378
2484
  if (isResObj(v)) {
2379
2485
  fields.set(k, buildPartialHoconFromResObj(v));
2380
2486
  } else {
@@ -2767,7 +2873,7 @@ function hoconToJs(v) {
2767
2873
  function resObjToKeyShape(tree) {
2768
2874
  const fields = /* @__PURE__ */ new Map();
2769
2875
  for (const [k, v] of tree.fields) {
2770
- if (isSubst(v) || isConcat(v) || isAppend(v)) {
2876
+ if (isSubst(v) || isConcat(v)) {
2771
2877
  fields.set(k, { kind: "scalar", raw: "", valueType: "null" });
2772
2878
  } else if (isResObj(v)) {
2773
2879
  fields.set(k, resObjToKeyShape(v));
@@ -2781,7 +2887,7 @@ function stripPlaceholderFields(v) {
2781
2887
  const fields = /* @__PURE__ */ new Map();
2782
2888
  let hadPlaceholders = false;
2783
2889
  for (const [k, val] of v.fields) {
2784
- if (val._kind === "subst-placeholder" || val._kind === "concat-placeholder" || val._kind === "append-placeholder") {
2890
+ if (val._kind === "subst-placeholder" || val._kind === "concat-placeholder") {
2785
2891
  hadPlaceholders = true;
2786
2892
  } else if (val.kind === "object") {
2787
2893
  const inner = stripPlaceholderFields(val);