ng-form-foundry-transformers 0.4.1 → 0.5.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.
@@ -1,38 +1,46 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.libconfigTransformer = void 0;
4
- exports.resetLibconfigBetaWarning = resetLibconfigBetaWarning;
5
4
  const thesaurus_1 = require("../../core/thesaurus");
6
5
  const json_schema_1 = require("../../core/json-schema");
6
+ const merge_inferred_1 = require("../../core/merge-inferred");
7
+ const schema_keys_1 = require("../../core/schema-keys");
7
8
  const parser_1 = require("./parser");
8
9
  const schema_1 = require("./schema");
9
10
  const revert_1 = require("./revert");
10
- let warnedBeta = false;
11
- function warnBeta() {
12
- if (warnedBeta)
13
- return;
14
- warnedBeta = true;
15
- console.warn('[ng-form-foundry-transformers] The libconfig transformer is a BETA feature. ' +
16
- 'Verify every write-back (diff toSource output against the original file) before deploying it.');
17
- }
18
- /** Test seam: makes the one-time beta warning observable per test. */
19
- function resetLibconfigBetaWarning() {
20
- warnedBeta = false;
21
- }
22
11
  exports.libconfigTransformer = {
23
12
  id: 'libconfig',
24
13
  toSchema(source, options) {
25
- warnBeta();
26
14
  const root = (0, parser_1.parseLibconfig)(source, { includes: options?.includes });
27
- const schema = options?.schema
15
+ const unknownKeys = options?.unknownKeys ?? 'preserve';
16
+ const fromJsonSchema = options?.schema
28
17
  ? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName, options.schemaOptions)
29
- : (0, schema_1.libconfigToNodeGroup)(root, source, options?.rootName ?? '__root__');
18
+ : undefined;
19
+ const schema = fromJsonSchema && unknownKeys === 'edit'
20
+ ? (0, merge_inferred_1.mergeInferred)(fromJsonSchema, (0, schema_1.libconfigToNodeGroup)(root, source, options?.rootName ?? '__root__'))
21
+ : fromJsonSchema ?? (0, schema_1.libconfigToNodeGroup)(root, source, options?.rootName ?? '__root__');
30
22
  const labeled = options?.thesaurus ? (0, thesaurus_1.applyThesaurus)(schema, options.thesaurus) : schema;
31
23
  const initialValue = (0, schema_1.extractValue)(root, source, options?.schema != null);
32
- return { schema: labeled, binding: { source, root }, initialValue };
24
+ if (fromJsonSchema && unknownKeys === 'edit') {
25
+ // Uncovered empty collections merged as read-only carries: their value
26
+ // must be the verbatim slice, not the typed [] the extraction produced.
27
+ (0, schema_1.carryUncoveredEmpties)(root, source, initialValue, (0, schema_keys_1.childrenOf)(fromJsonSchema));
28
+ }
29
+ return {
30
+ schema: labeled,
31
+ binding: {
32
+ source,
33
+ root,
34
+ // 'preserve' gates the revert on the JSON Schema's NodeGroup; 'edit'
35
+ // on the merged one (schema-born ≈ everything, but settings no form
36
+ // field can carry stay protected); 'drop' and inferred mode leave
37
+ // the value authoritative for the whole document.
38
+ schema: fromJsonSchema && unknownKeys !== 'drop' ? schema : undefined,
39
+ },
40
+ initialValue,
41
+ };
33
42
  },
34
43
  toSource(value, binding) {
35
- warnBeta();
36
- return (0, revert_1.applyValueToSource)(binding.source, binding.root, value);
44
+ return (0, revert_1.applyValueToSource)(binding.source, binding.root, value, binding.schema);
37
45
  },
38
46
  };
@@ -13,6 +13,18 @@
13
13
  * literal re-emits in hex at its original digit width, an int64 keeps its
14
14
  * `L`/`LL` suffix, and values beyond 2^53 travel as exact decimal strings.
15
15
  */
16
+ import type { NodeGroup } from '../../core/schema';
16
17
  import type { CfgGroup } from './parser';
17
- /** Apply `value` onto the parsed `root` of `source`, returning the new text. */
18
- export declare function applyValueToSource(source: string, root: CfgGroup, value: Record<string, unknown>): string;
18
+ /**
19
+ * Apply `value` onto the parsed `root` of `source`, returning the new text.
20
+ *
21
+ * Without `schema` (inferred mode) the value is authoritative for the whole
22
+ * document: it was extracted from every setting, so a key it lacks is a
23
+ * deletion. With `schema` (the NodeGroup the form was built from) the value
24
+ * is authoritative for **schema-born paths only** — the form never carried
25
+ * the other settings, so they survive byte-verbatim in their original
26
+ * positions. A partial schema thus edits its slice of an OAI/srsRAN config
27
+ * without erasing keys the schema does not enumerate; absence of a
28
+ * schema-born key still deletes (a presence toggle turned off).
29
+ */
30
+ export declare function applyValueToSource(source: string, root: CfgGroup, value: Record<string, unknown>, schema?: NodeGroup): string;
@@ -1,33 +1,50 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.applyValueToSource = applyValueToSource;
4
+ const schema_keys_1 = require("../../core/schema-keys");
4
5
  const schema_1 = require("./schema");
5
- /** Apply `value` onto the parsed `root` of `source`, returning the new text. */
6
- function applyValueToSource(source, root, value) {
6
+ /**
7
+ * Apply `value` onto the parsed `root` of `source`, returning the new text.
8
+ *
9
+ * Without `schema` (inferred mode) the value is authoritative for the whole
10
+ * document: it was extracted from every setting, so a key it lacks is a
11
+ * deletion. With `schema` (the NodeGroup the form was built from) the value
12
+ * is authoritative for **schema-born paths only** — the form never carried
13
+ * the other settings, so they survive byte-verbatim in their original
14
+ * positions. A partial schema thus edits its slice of an OAI/srsRAN config
15
+ * without erasing keys the schema does not enumerate; absence of a
16
+ * schema-born key still deletes (a presence toggle turned off).
17
+ */
18
+ function applyValueToSource(source, root, value, schema) {
7
19
  const edits = [];
8
- patchGroup(source, root, value, edits);
20
+ patchGroup(source, root, value, edits, schema && (0, schema_keys_1.childrenOf)(schema));
9
21
  edits.sort((a, b) => b.start - a.start);
10
22
  let out = source;
11
23
  for (const e of edits)
12
24
  out = out.slice(0, e.start) + e.text + out.slice(e.end);
13
25
  return out;
14
26
  }
15
- function patchGroup(src, group, value, edits) {
27
+ function patchGroup(src, group, value, edits, schema) {
16
28
  for (const setting of group.settings) {
17
- if (!value || !(setting.name in value)) {
29
+ if (schema && !schema.has(setting.name))
30
+ continue; // not schema-born: verbatim
31
+ // Own-key check: `in` would resolve setting names like `toString` through
32
+ // the prototype chain, making them undeletable (and their "value" a function).
33
+ if (!value || !Object.prototype.hasOwnProperty.call(value, setting.name)) {
18
34
  edits.push(deleteSetting(src, setting));
19
35
  continue;
20
36
  }
21
- patchValue(src, setting.value, value[setting.name], edits);
37
+ patchValue(src, setting.value, value[setting.name], edits, schema?.get(setting.name));
22
38
  }
23
39
  const known = new Set(group.settings.map((s) => s.name));
24
- // null/undefined means "absent", never a value: such keys are not added.
25
- const added = Object.keys(value ?? {}).filter((k) => !known.has(k) && value[k] != null);
40
+ // null/undefined means "absent", never a value: such keys are not added
41
+ // and under a schema, only schema-born keys may be added at all.
42
+ const added = Object.keys(value ?? {}).filter((k) => !known.has(k) && value[k] != null && (!schema || schema.has(k)));
26
43
  if (added.length) {
27
44
  edits.push(insertionEdit(src, group, added.map((k) => `${k} = ${serialize(value[k], '')};`)));
28
45
  }
29
46
  }
30
- function patchValue(src, node, value, edits) {
47
+ function patchValue(src, node, value, edits, schema) {
31
48
  switch (node.kind) {
32
49
  case 'scalar': {
33
50
  if (node.value === value)
@@ -41,7 +58,7 @@ function patchValue(src, node, value, edits) {
41
58
  }
42
59
  case 'group': {
43
60
  if (isRecord(value))
44
- return patchGroup(src, node, value, edits);
61
+ return patchGroup(src, node, value, edits, schema && (0, schema_keys_1.childrenOf)(schema));
45
62
  edits.push({ ...node.span, text: serialize(value, '') }); // shape change: regenerate
46
63
  return;
47
64
  }
@@ -61,7 +78,7 @@ function patchValue(src, node, value, edits) {
61
78
  case 'list': {
62
79
  const shape = (0, schema_1.listShape)(node);
63
80
  if (shape === 'groups' && Array.isArray(value)) {
64
- return patchElements(src, node.elements, node, value, edits);
81
+ return patchElements(src, node.elements, node, value, edits, (0, schema_keys_1.itemSchemaOf)(schema));
65
82
  }
66
83
  if (shape === 'scalars' && Array.isArray(value)) {
67
84
  return patchElements(src, node.elements, node, value, edits);
@@ -89,10 +106,10 @@ function patchValue(src, node, value, edits) {
89
106
  }
90
107
  }
91
108
  /** Element-wise patch of an array/list: edit in place, then grow or shrink. */
92
- function patchElements(src, elements, collection, value, edits) {
109
+ function patchElements(src, elements, collection, value, edits, itemSchema) {
93
110
  const shared = Math.min(elements.length, value.length);
94
111
  for (let i = 0; i < shared; i++)
95
- patchValue(src, elements[i], value[i], edits);
112
+ patchValue(src, elements[i], value[i], edits, itemSchema);
96
113
  if (value.length > elements.length) {
97
114
  const items = value.slice(elements.length).map((v) => serialize(v, ''));
98
115
  const at = elements.length ? elements[elements.length - 1].span.end : collection.innerSpan.start;
@@ -19,6 +19,7 @@
19
19
  * empty collections editable.
20
20
  */
21
21
  import type { NodeGroup } from '../../core/schema';
22
+ import { type SchemaKeys } from '../../core/schema-keys';
22
23
  import { type CfgGroup, type CfgList, type CfgValue } from './parser';
23
24
  /** Infer the root NodeGroup for a parsed document. */
24
25
  export declare function libconfigToNodeGroup(root: CfgGroup, source: string, name: string): NodeGroup;
@@ -36,4 +37,14 @@ type ListShape = 'groups' | 'scalars' | 'empty' | 'heterogeneous';
36
37
  export declare function listShape(list: CfgList): ListShape;
37
38
  /** The verbatim source slice of a node. */
38
39
  export declare function raw(value: CfgValue, source: string): string;
40
+ /**
41
+ * `unknownKeys: 'edit'` extraction fixup. The whole-document extraction runs
42
+ * with `emptyAsArrays` (schema-covered empty collections are typed and
43
+ * editable), but collections the JSON Schema does **not** cover merge as
44
+ * read-only raw-carry leaves — their value must be the verbatim source
45
+ * slice, not `[]`. Walks the document alongside the original (pre-merge)
46
+ * schema coverage and restores the carry on every uncovered empty
47
+ * collection, at any depth.
48
+ */
49
+ export declare function carryUncoveredEmpties(group: CfgGroup, source: string, value: unknown, keys: SchemaKeys): void;
39
50
  export {};
@@ -4,6 +4,8 @@ exports.libconfigToNodeGroup = libconfigToNodeGroup;
4
4
  exports.extractValue = extractValue;
5
5
  exports.listShape = listShape;
6
6
  exports.raw = raw;
7
+ exports.carryUncoveredEmpties = carryUncoveredEmpties;
8
+ const schema_keys_1 = require("../../core/schema-keys");
7
9
  const parser_1 = require("./parser");
8
10
  /** Digits only — the constraint for int64 values carried as strings. */
9
11
  const INTEGER_STRING_PATTERN = '^[-+]?[0-9]+$';
@@ -80,14 +82,21 @@ function scalarLeaf(name, scalar) {
80
82
  case 'float':
81
83
  return { kind: 'leaf', name, type: 'number' };
82
84
  case 'int':
83
- return { kind: 'leaf', name, type: 'number', integer: true };
85
+ return withRadix({ kind: 'leaf', name, type: 'number', integer: true }, scalar);
84
86
  case 'int64':
85
87
  // Beyond 2^53 the value rides as an exact decimal string.
86
88
  return typeof scalar.value === 'string'
87
- ? { kind: 'leaf', name, type: 'string', pattern: INTEGER_STRING_PATTERN }
88
- : { kind: 'leaf', name, type: 'number', integer: true };
89
+ ? withRadix({ kind: 'leaf', name, type: 'string', pattern: INTEGER_STRING_PATTERN }, scalar)
90
+ : withRadix({ kind: 'leaf', name, type: 'number', integer: true }, scalar);
89
91
  }
90
92
  }
93
+ /** Carry a non-decimal literal's base onto the leaf as its display radix. */
94
+ function withRadix(leaf, scalar) {
95
+ const radix = scalar.int?.radix;
96
+ if (radix && radix !== 10)
97
+ leaf.radix = radix;
98
+ return leaf;
99
+ }
91
100
  /**
92
101
  * A homogeneous scalar collection → leafList. One int64 element beyond 2^53
93
102
  * degrades the whole list to string carry: a leafList holds one scalar type,
@@ -97,10 +106,23 @@ function listLeaf(name, elements) {
97
106
  if (elements.length === 0)
98
107
  return rawLeaf(name, 'empty collection');
99
108
  const f = (0, parser_1.family)(elements[0].type);
109
+ const radix = f === 'integer' ? sharedRadix(elements) : undefined;
100
110
  if (f === 'integer' && elements.some((e) => typeof e.value === 'string')) {
101
- return { kind: 'leafList', name, type: 'string' };
111
+ return { kind: 'leafList', name, type: 'string', ...(radix && { radix }) };
102
112
  }
103
- return { kind: 'leafList', name, type: f === 'integer' || f === 'float' ? 'number' : f === 'bool' ? 'boolean' : 'string' };
113
+ return {
114
+ kind: 'leafList',
115
+ name,
116
+ type: f === 'integer' || f === 'float' ? 'number' : f === 'bool' ? 'boolean' : 'string',
117
+ ...(radix && { radix }),
118
+ };
119
+ }
120
+ /** The display radix shared by every element, when uniform and non-decimal. */
121
+ function sharedRadix(elements) {
122
+ const first = elements[0]?.int?.radix ?? 10;
123
+ if (first === 10)
124
+ return undefined;
125
+ return elements.every((e) => (e.int?.radix ?? 10) === first) ? first : undefined;
104
126
  }
105
127
  function arrayValue(elements) {
106
128
  const stringCarry = elements.length > 0 &&
@@ -164,3 +186,61 @@ function rawLeaf(name, why) {
164
186
  function raw(value, source) {
165
187
  return source.slice(value.span.start, value.span.end);
166
188
  }
189
+ /**
190
+ * `unknownKeys: 'edit'` extraction fixup. The whole-document extraction runs
191
+ * with `emptyAsArrays` (schema-covered empty collections are typed and
192
+ * editable), but collections the JSON Schema does **not** cover merge as
193
+ * read-only raw-carry leaves — their value must be the verbatim source
194
+ * slice, not `[]`. Walks the document alongside the original (pre-merge)
195
+ * schema coverage and restores the carry on every uncovered empty
196
+ * collection, at any depth.
197
+ */
198
+ function carryUncoveredEmpties(group, source, value, keys) {
199
+ if (!isRecord(value))
200
+ return;
201
+ for (const setting of group.settings) {
202
+ if (keys && !keys.has(setting.name)) {
203
+ restoreEmptyCarries(setting.value, source, value, setting.name);
204
+ continue;
205
+ }
206
+ const child = keys?.get(setting.name);
207
+ const v = value[setting.name];
208
+ if (setting.value.kind === 'group' && child) {
209
+ carryUncoveredEmpties(setting.value, source, v, (0, schema_keys_1.childrenOf)(child));
210
+ }
211
+ else if (setting.value.kind === 'list' && Array.isArray(v)) {
212
+ const itemSchema = (0, schema_keys_1.itemSchemaOf)(child);
213
+ if (!itemSchema)
214
+ continue;
215
+ const itemKeys = (0, schema_keys_1.childrenOf)(itemSchema);
216
+ setting.value.elements.forEach((el, i) => {
217
+ if (el.kind === 'group')
218
+ carryUncoveredEmpties(el, source, v[i], itemKeys);
219
+ });
220
+ }
221
+ }
222
+ }
223
+ /** In a fully uncovered subtree, every empty collection reverts to its carry. */
224
+ function restoreEmptyCarries(node, source, parent, key) {
225
+ if ((node.kind === 'array' || node.kind === 'list') && node.elements.length === 0) {
226
+ parent[key] = raw(node, source);
227
+ return;
228
+ }
229
+ if (node.kind === 'group') {
230
+ const v = parent[key];
231
+ if (!isRecord(v))
232
+ return;
233
+ for (const s of node.settings)
234
+ restoreEmptyCarries(s.value, source, v, s.name);
235
+ return;
236
+ }
237
+ if (node.kind === 'list') {
238
+ const v = parent[key];
239
+ if (!Array.isArray(v))
240
+ return;
241
+ node.elements.forEach((el, i) => restoreEmptyCarries(el, source, v, i));
242
+ }
243
+ }
244
+ function isRecord(v) {
245
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
246
+ }
@@ -7,5 +7,5 @@
7
7
  * `core` — they are format-agnostic and shared with the JSON transformer.
8
8
  */
9
9
  export { yamlTransformer } from './yaml-transformer';
10
- export type { YamlOptions } from './yaml-transformer';
10
+ export type { YamlOptions, YamlBinding } from './yaml-transformer';
11
11
  export { applyValueToDocument } from './revert';
@@ -1,4 +1,5 @@
1
1
  import { type Document } from 'yaml';
2
+ import type { NodeGroup } from '../../core/schema';
2
3
  /**
3
4
  * Apply an edited form value onto a parsed YAML {@link Document} in place,
4
5
  * preserving comments and formatting on every node that survives the edit.
@@ -10,9 +11,15 @@ import { type Document } from 'yaml';
10
11
  * {@link import('./json-schema')}. Callers should clone the document first if the
11
12
  * original must be preserved.
12
13
  *
14
+ * With `schema` (the NodeGroup the form was built from) the
15
+ * value is authoritative for **schema-born paths only**: keys the schema does
16
+ * not cover — at any depth — are preserved verbatim, their comments included.
17
+ * Without it the value covers the whole document and a key it lacks is a
18
+ * deletion.
19
+ *
13
20
  * Map keys are matched by the *same string form* `doc.toJS()` produced when the
14
21
  * form value was built, so non-string keys (a `80:` port map, a `true:` flag)
15
22
  * reconcile against their original typed key nodes instead of being appended as
16
23
  * duplicate string-keyed pairs.
17
24
  */
18
- export declare function applyValueToDocument(doc: Document, value: unknown): void;
25
+ export declare function applyValueToDocument(doc: Document, value: unknown, schema?: NodeGroup): void;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.applyValueToDocument = applyValueToDocument;
4
4
  const yaml_1 = require("yaml");
5
5
  const bigint_1 = require("../../core/bigint");
6
+ const schema_keys_1 = require("../../core/schema-keys");
6
7
  /**
7
8
  * Apply an edited form value onto a parsed YAML {@link Document} in place,
8
9
  * preserving comments and formatting on every node that survives the edit.
@@ -14,17 +15,23 @@ const bigint_1 = require("../../core/bigint");
14
15
  * {@link import('./json-schema')}. Callers should clone the document first if the
15
16
  * original must be preserved.
16
17
  *
18
+ * With `schema` (the NodeGroup the form was built from) the
19
+ * value is authoritative for **schema-born paths only**: keys the schema does
20
+ * not cover — at any depth — are preserved verbatim, their comments included.
21
+ * Without it the value covers the whole document and a key it lacks is a
22
+ * deletion.
23
+ *
17
24
  * Map keys are matched by the *same string form* `doc.toJS()` produced when the
18
25
  * form value was built, so non-string keys (a `80:` port map, a `true:` flag)
19
26
  * reconcile against their original typed key nodes instead of being appended as
20
27
  * duplicate string-keyed pairs.
21
28
  */
22
- function applyValueToDocument(doc, value) {
29
+ function applyValueToDocument(doc, value, schema) {
23
30
  if (doc.contents == null) {
24
31
  doc.contents = doc.createNode(value);
25
32
  return;
26
33
  }
27
- doc.contents = applyToNode(doc, doc.contents, value);
34
+ doc.contents = applyToNode(doc, doc.contents, value, schema);
28
35
  }
29
36
  /**
30
37
  * Reconcile the parsed `node` at one position with its edited `value`, returning
@@ -32,22 +39,27 @@ function applyValueToDocument(doc, value) {
32
39
  * a map/sequence is reconciled child-by-child; a shape change (or a brand-new
33
40
  * position) allocates a fresh node.
34
41
  */
35
- function applyToNode(doc, node, value) {
42
+ function applyToNode(doc, node, value, schema) {
36
43
  if (isPlainObject(value)) {
37
44
  if (!(0, yaml_1.isMap)(node))
38
45
  return doc.createNode(value);
39
- reconcileMap(doc, node, value);
46
+ reconcileMap(doc, node, value, schema && (0, schema_keys_1.childrenOf)(schema));
40
47
  return node;
41
48
  }
42
49
  if (Array.isArray(value)) {
43
50
  if (!(0, yaml_1.isSeq)(node))
44
51
  return doc.createNode(value);
45
- reconcileSeq(doc, node, value);
52
+ reconcileSeq(doc, node, value, (0, schema_keys_1.itemSchemaOf)(schema));
46
53
  return node;
47
54
  }
48
55
  // scalar (string / number / boolean / null)
49
56
  if ((0, yaml_1.isScalar)(node)) {
50
- node.value = coerceScalarValue(value, node.value);
57
+ const next = coerceScalarValue(value, node.value);
58
+ // An unchanged value keeps the node untouched — the parsed BigInt and the
59
+ // form's number compare by numeric value, so an identity pass never
60
+ // downgrades a BigInt-held integer to a lossy JS number.
61
+ if (!sameScalar(node.value, next))
62
+ node.value = next;
51
63
  return node;
52
64
  }
53
65
  return doc.createNode(value);
@@ -56,13 +68,24 @@ function applyToNode(doc, node, value) {
56
68
  * Update a map node against the value object: drop pairs whose key is gone,
57
69
  * recurse into the ones that remain (matched by their `toJS` key string, so
58
70
  * typed keys line up), and append a fresh pair for each genuinely new key.
71
+ * Under `schema`, keys that are not schema-born are exempt from all three —
72
+ * never dropped, never recursed into, never addable.
59
73
  */
60
- function reconcileMap(doc, node, value) {
61
- node.items = node.items.filter((pair) => keyString(pair.key) in value);
74
+ function reconcileMap(doc, node, value, schema) {
75
+ node.items = node.items.filter((pair) => {
76
+ const key = keyString(pair.key);
77
+ if (schema && !schema.has(key))
78
+ return true; // not schema-born: verbatim
79
+ // Own-key check: `in` would resolve doc keys like `toString` through the
80
+ // prototype chain and make them undeletable.
81
+ return Object.prototype.hasOwnProperty.call(value, key);
82
+ });
62
83
  for (const key of Object.keys(value)) {
84
+ if (schema && !schema.has(key))
85
+ continue; // not schema-born: never written
63
86
  const pair = node.items.find((p) => keyString(p.key) === key);
64
87
  if (pair) {
65
- pair.value = applyToNode(doc, pair.value, value[key]);
88
+ pair.value = applyToNode(doc, pair.value, value[key], schema?.get(key));
66
89
  }
67
90
  else {
68
91
  node.items.push(doc.createPair(key, value[key]));
@@ -70,12 +93,12 @@ function reconcileMap(doc, node, value) {
70
93
  }
71
94
  }
72
95
  /** Update a sequence node: shrink/grow to the value's length, recurse per item. */
73
- function reconcileSeq(doc, node, value) {
96
+ function reconcileSeq(doc, node, value, itemSchema) {
74
97
  while (node.items.length > value.length)
75
98
  node.items.pop();
76
99
  for (let i = 0; i < value.length; i++) {
77
100
  if (i < node.items.length) {
78
- node.items[i] = applyToNode(doc, node.items[i], value[i]);
101
+ node.items[i] = applyToNode(doc, node.items[i], value[i], itemSchema);
79
102
  }
80
103
  else {
81
104
  node.items.push(doc.createNode(value[i]));
@@ -96,6 +119,15 @@ function coerceScalarValue(value, current) {
96
119
  }
97
120
  return value;
98
121
  }
122
+ /** Numeric equality across the BigInt/number divide, `===` otherwise. */
123
+ function sameScalar(current, next) {
124
+ if (current === next)
125
+ return true;
126
+ if (typeof current === 'bigint' && typeof next === 'number' && Number.isInteger(next)) {
127
+ return current === BigInt(next);
128
+ }
129
+ return false;
130
+ }
99
131
  /**
100
132
  * The string a key node takes as a JS object key, matching `doc.toJS()`: the
101
133
  * scalar value stringified, with `null`/`undefined` collapsing to `''` (the
@@ -1,5 +1,5 @@
1
1
  import { type Document } from 'yaml';
2
- import type { FormValue, Thesaurus } from '../../core/schema';
2
+ import type { FormValue, NodeGroup, Thesaurus } from '../../core/schema';
3
3
  import type { TransformResult } from '../../core/transformer';
4
4
  import { type JsonSchema, type JsonSchemaOptions } from '../../core/json-schema';
5
5
  /** Options for {@link yamlTransformer}'s `toSchema`. */
@@ -21,6 +21,32 @@ export interface YamlOptions {
21
21
  * case-insensitively; never paths.
22
22
  */
23
23
  thesaurus?: Thesaurus;
24
+ /**
25
+ * Schema-driven mode only: what happens to keys the JSON Schema does not
26
+ * cover. `'preserve'` (default) keeps them verbatim — the form never
27
+ * carried them, so a partial schema edits its slice without erasing the
28
+ * rest. `'drop'` makes the edited value authoritative for the whole
29
+ * document, deleting uncovered keys — for consumers whose schema is
30
+ * intentionally complete. `'edit'` surfaces them instead: uncovered keys
31
+ * render as editable fields typed by the data (the inferred schema merged
32
+ * under the JSON Schema), so nothing is invisible and the value covers the
33
+ * whole document. Ignored without a `schema`.
34
+ */
35
+ unknownKeys?: 'preserve' | 'drop' | 'edit';
36
+ }
37
+ /**
38
+ * The revert context: the parsed {@link Document} and — in schema-driven
39
+ * mode with `unknownKeys: 'preserve'` or `'edit'` — the NodeGroup the form
40
+ * was built from (the JSON Schema's own under `'preserve'`, the
41
+ * inferred-merged one under `'edit'`), so `toSource` treats the value as
42
+ * authoritative for schema-born paths only. Under `'edit'` that is almost
43
+ * the whole document; the gate still protects the keys no form field can
44
+ * carry (e.g. a key inside a covered choice's object that no case names).
45
+ * Treat it as opaque: build it with `toSchema`, hand it back to `toSource`.
46
+ */
47
+ export interface YamlBinding {
48
+ doc: Document;
49
+ schema?: NodeGroup;
24
50
  }
25
51
  /**
26
52
  * A YAML {@link Transformer}: turn a YAML config document into a form and write
@@ -28,14 +54,15 @@ export interface YamlOptions {
28
54
  * preserves comments, key order, and formatting by applying edits onto the parsed
29
55
  * document (see {@link applyValueToDocument}).
30
56
  *
31
- * The `binding` it round-trips is the parsed {@link Document}; `toSource` clones
32
- * it before applying, so a single `toSchema` result can serve many edits.
57
+ * The `binding` it round-trips is a {@link YamlBinding}; `toSource` clones the
58
+ * document inside it before applying, so a single `toSchema` result can serve
59
+ * many edits.
33
60
  *
34
61
  * With a JSON Schema in {@link YamlOptions}, the form is schema-driven; without
35
62
  * one it is inferred from the data.
36
63
  */
37
64
  export declare const yamlTransformer: {
38
65
  id: string;
39
- toSchema(source: string, options?: YamlOptions): TransformResult<Document>;
40
- toSource(value: FormValue, binding: Document): string;
66
+ toSchema(source: string, options?: YamlOptions): TransformResult<YamlBinding>;
67
+ toSource(value: FormValue, binding: YamlBinding): string;
41
68
  };