@limaformat/lima 0.4.0 → 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.
package/CHANGELOG.md CHANGED
@@ -12,6 +12,48 @@ are both frozen regardless of what this file shows.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.5.1] — 2026-10-28
16
+
17
+ Core errata `1.0.10`: aligned top-level values are trimmed, as Core 1.0
18
+ always required.
19
+
20
+ ### Fixed
21
+
22
+ - A top-level inline value kept every space after `: ` but the first:
23
+ `title: Hello` read `' Hello'`, and `n: 42` the string `' 42'`
24
+ instead of the integer 42 — against §4 step 3 and the §4 and §5.1
25
+ examples. Nested keys were not affected. The top-level entry now skips the
26
+ whitespace after the separator before reading the value, as the block
27
+ mapping does; the References 2.0 columns it reports were already physical
28
+ and are unchanged.
29
+
30
+ The shared conformance corpus grows to 218 Core cases (7 new, `since:
31
+ "1.0.10"`); TypeScript, Rust, and Go pass all of them.
32
+
33
+ A unit test in `src/block.test.ts` had asserted the defect (`key: value`
34
+ → `' value'`); it now asserts the specified `'value'`.
35
+
36
+ ## [0.5.0] — 2026-09-09
37
+
38
+ Scope cleanup. The frozen **References 1.0** resolver is no longer bundled in
39
+ the published package — it was never a public entry point (`parseReferences`
40
+ and `parse` are References 2.0) and only existed to run the frozen 1.0
41
+ conformance corpus, which now does so against the repository source. The
42
+ 1.0 specification and its 101-case corpus stay in the repository as a
43
+ frozen regression target.
44
+
45
+ No changes to Core 1.0 or References 2.0 syntax, semantics, or public API.
46
+ The version advances to `0.5.0` in step with the Rust crate and Go module.
47
+
48
+ ### Changed
49
+
50
+ - The published tarball drops from ~59.3 KB to ~53.4 KB — the internal
51
+ References 1.0 resolver (`references.ts`) is excluded from the build.
52
+ - Documentation de-emphasises References 1.0 throughout: it is presented as
53
+ a briefly-published, immediately-superseded early version, not a current
54
+ target. The "Migrating References 1.0 to 2.0" guide is removed (see git
55
+ history if you need it).
56
+
15
57
  ## [0.4.0] — 2026-09-08
16
58
 
17
59
  First independent whole-repo review of Lima, then a second review of the
package/dist/block.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * never resolves a reference in the first place).
10
10
  */
11
11
  import { checkKeyLength, checkDuplicateKey, NESTING_DEPTH_LIMIT } from './normalize.js';
12
- import { stripKeyQuotes, stripComment, stripCommentKeepEscapes, parseQuotedOrTyped, closingQuoteIndex, isValidKey, positionedBuilder, } from './scalars.js';
12
+ import { stripKeyQuotes, stripComment, stripCommentKeepEscapes, parseQuotedOrTyped, closingQuoteIndex, isValidKey, } from './scalars.js';
13
13
  import { parseFlowMapping, parseFlowOrScalarValue } from './flow.js';
14
14
  import { buildBlockScalar } from './block-scalar.js';
15
15
  import { LimaError } from './errors.js';
@@ -49,7 +49,7 @@ const inlineSource = (wantSource, ctx, keyLine, valueCol, rawUndecoded) => wantS
49
49
  tabAdjust: ctx.tabAdjust ? [ctx.tabAdjust[keyLine - 1] ?? 0] : undefined,
50
50
  }) : undefined;
51
51
  const inlineOrBlockScalar = (raw, keyIndent, keyLine, valueCol, rawUndecoded, cursor, ctx, builder) => {
52
- const wantSource = builder === positionedBuilder;
52
+ const wantSource = builder.positions === true;
53
53
  if (raw !== '|') {
54
54
  const value = parseFlowOrScalarValue(raw, ctx, keyLine, builder, inlineSource(wantSource, ctx, keyLine, valueCol, rawUndecoded));
55
55
  cursor.next();
@@ -129,7 +129,7 @@ const cursorAfterDash = (cursor) => {
129
129
  };
130
130
  /** Shared block grammar consuming one mutable physical-line cursor. */
131
131
  const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
132
- const wantSource = builder === positionedBuilder;
132
+ const wantSource = builder.positions === true;
133
133
  let items = null;
134
134
  let entries = null;
135
135
  let pendingItem = null;
package/dist/builder.d.ts CHANGED
@@ -26,6 +26,8 @@
26
26
  */
27
27
  import type { ReferenceSource } from './reference-tokens2.js';
28
28
  export interface ValueBuilder<V, M = Map<string, V>> {
29
+ /** Whether scalar builders need physical source anchors for editor positions. */
30
+ readonly positions?: boolean;
29
31
  null(line: number): V;
30
32
  bool(value: boolean, line: number): V;
31
33
  int(value: number, line: number): V;
package/dist/core.d.ts CHANGED
@@ -22,6 +22,7 @@ import { type LimaValue } from './value.js';
22
22
  import { type ParseContext, type Diagnostic } from './normalize.js';
23
23
  import { type PositionedValue } from './scalars.js';
24
24
  import type { ValueBuilder } from './builder.js';
25
+ import type { ReferenceToken2 } from './reference-tokens2.js';
25
26
  export type { Diagnostic, ParseContext } from './normalize.js';
26
27
  export { NESTING_DEPTH_LIMIT, SCALAR_LENGTH_LIMIT } from './normalize.js';
27
28
  export type { PositionedValue, InsertedAt } from './scalars.js';
@@ -40,6 +41,16 @@ export type CoreOptions = {
40
41
  * primitive the References layer (`references.ts`) builds on.
41
42
  */
42
43
  export declare const parseCoreWithPositions: (frontMatter: string, ctx: ParseContext) => Map<string, PositionedValue>;
44
+ export type PositionedReferenceParse = {
45
+ document: Map<string, PositionedValue>;
46
+ references: ReferenceToken2[];
47
+ };
48
+ /**
49
+ * Position-aware Core parse plus every active References 2.0 token seen while
50
+ * values are built. Recording at construction time preserves tokens from
51
+ * earlier duplicate-key values even though the result map is last-value-wins.
52
+ */
53
+ export declare const parseCoreWithPositionedReferences: (frontMatter: string, ctx: ParseContext) => PositionedReferenceParse;
43
54
  /** The public result shape (Core §11.1): every value `toNative*` can produce. */
44
55
  export type NativeValue = null | boolean | number | string | Date | NativeValue[] | {
45
56
  [key: string]: NativeValue;
package/dist/core.js CHANGED
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import { checkKeyLength, byteLength, DOCUMENT_SIZE_LIMIT, TOP_LEVEL_KEY_LIMIT, NESTING_DEPTH_LIMIT, } from './normalize.js';
22
22
  import { unescapeDQ, stripComment, stripCommentKeepEscapes, positionedBuilder, NO_SPAN_VALUE, parseSimpleScalarSpan, } from './scalars.js';
23
+ import { isTrimWhitespace } from './chars.js';
23
24
  import { parseFlowOrScalarValue } from './flow.js';
24
25
  import { parseBlockRange } from './block.js';
25
26
  import { buildBlockScalar } from './block-scalar.js';
@@ -156,8 +157,21 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
156
157
  searchFrom = lineEnd + 1;
157
158
  }
158
159
  }
159
- const parseEntry = (key, line, rawStart, isBlock, inlineEnd, nextStart) => {
160
+ const parseEntry = (key, line, separatorEnd, isBlock, inlineEnd, nextStart) => {
160
161
  checkKeyLength(key, line);
162
+ // §4 step 3: an inline value begins after the spaces that follow `: `, however many there are — as in a
163
+ // block mapping (block.ts trimSlice), so that the scalar paths below and the source column see the value
164
+ // itself. The newline that ends the line is not skipped.
165
+ let rawStart = separatorEnd;
166
+ if (!isBlock) {
167
+ while (rawStart < nextStart) {
168
+ const c = frontMatter.charCodeAt(rawStart);
169
+ // printable ASCII other than SPACE is never whitespace: the common case ends here at once
170
+ if ((c > 0x20 && c < 0x7f) || c === 10 || !isTrimWhitespace(c))
171
+ break;
172
+ rawStart++;
173
+ }
174
+ }
161
175
  if ((strict || ctx.onWarning !== undefined) && builder.hasMappingKey(root, key)) {
162
176
  const diagnostic = {
163
177
  code: 'DUPLICATE_KEY', line, key,
@@ -176,7 +190,7 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
176
190
  // source line — the anchor for References 2.0 token positions (§2.4).
177
191
  // References 2.0 token positions (§2.4): only build the source anchor
178
192
  // for the annotated builder; `parseCore` (native) discards it.
179
- const wantSource = builder === positionedBuilder;
193
+ const wantSource = builder.positions === true;
180
194
  const valueLineStart = wantSource ? frontMatter.lastIndexOf('\n', rawStart - 1) + 1 : 0;
181
195
  const valueCol = wantSource ? [...frontMatter.slice(valueLineStart, rawStart)].length : 0;
182
196
  const lineTabAdjust = wantSource && ctx.tabAdjust ? [ctx.tabAdjust[line - 1] ?? 0] : undefined;
@@ -315,6 +329,28 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
315
329
  * primitive the References layer (`references.ts`) builds on.
316
330
  */
317
331
  export const parseCoreWithPositions = (frontMatter, ctx) => parseCoreGeneric(frontMatter, ctx, positionedBuilder, depthOfPositioned);
332
+ /**
333
+ * Position-aware Core parse plus every active References 2.0 token seen while
334
+ * values are built. Recording at construction time preserves tokens from
335
+ * earlier duplicate-key values even though the result map is last-value-wins.
336
+ */
337
+ export const parseCoreWithPositionedReferences = (frontMatter, ctx) => {
338
+ const references = [];
339
+ const recordingBuilder = {
340
+ ...positionedBuilder,
341
+ string: (value, line, quoted, source) => {
342
+ const positioned = positionedBuilder.string(value, line, quoted, source);
343
+ if (positioned.kind === 'string' && positioned.references2) {
344
+ references.push(...positioned.references2);
345
+ }
346
+ return positioned;
347
+ },
348
+ };
349
+ return {
350
+ document: parseCoreGeneric(frontMatter, ctx, recordingBuilder, depthOfPositioned),
351
+ references,
352
+ };
353
+ };
318
354
  /**
319
355
  * The `ValueBuilder<NativeValue>` — `parseCore`'s fast path. Every scalar
320
356
  * builder is the identity function: unlike `positionedBuilder`, there is no
@@ -39,5 +39,13 @@ export type ReferenceSource = {
39
39
  col: number;
40
40
  tabAdjust?: number[];
41
41
  };
42
+ export declare const SEGMENT = "[a-zA-Z0-9_][a-zA-Z0-9_:-]*";
43
+ export declare const DOC_PATH = "[a-zA-Z0-9_][a-zA-Z0-9_:-]*(?:\\.[a-zA-Z0-9_][a-zA-Z0-9_:-]*)*";
44
+ export declare const PARTIAL_NAME = "[a-zA-Z0-9_][a-zA-Z0-9_:/-]*";
45
+ export declare const PARTIAL_PATH = "[a-zA-Z0-9_][a-zA-Z0-9_:/-]*(?:\\.[a-zA-Z0-9_][a-zA-Z0-9_:-]*)*";
42
46
  export declare const PURE_REFERENCE_2: RegExp;
47
+ /** Converts a 0-based codepoint offset to JavaScript/LSP UTF-16 units. */
48
+ export declare const codepointOffsetToUtf16: (text: string, codepointOffset: number) => number;
49
+ /** Converts a 0-based JavaScript/LSP UTF-16 offset to a codepoint offset. */
50
+ export declare const utf16OffsetToCodepoint: (text: string, utf16Offset: number) => number;
43
51
  export declare const scanReferenceTokens2: (value: string, source: ReferenceSource | undefined) => ReferenceToken2[];
@@ -1,20 +1,42 @@
1
1
  /** Reference tokens recorded while the annotated Core tree is being built. */
2
- const SEGMENT = '[a-zA-Z0-9_][a-zA-Z0-9_:-]*';
3
- const DOC_PATH = `${SEGMENT}(?:\\.${SEGMENT})*`;
4
- const PARTIAL_NAME = '[a-zA-Z0-9_][a-zA-Z0-9_:/-]*';
5
- const PARTIAL_PATH = `${PARTIAL_NAME}(?:\\.${SEGMENT})*`;
2
+ export const SEGMENT = '[a-zA-Z0-9_][a-zA-Z0-9_:-]*';
3
+ export const DOC_PATH = `${SEGMENT}(?:\\.${SEGMENT})*`;
4
+ export const PARTIAL_NAME = '[a-zA-Z0-9_][a-zA-Z0-9_:/-]*';
5
+ export const PARTIAL_PATH = `${PARTIAL_NAME}(?:\\.${SEGMENT})*`;
6
6
  export const PURE_REFERENCE_2 = new RegExp(`^(?:\\$\\{(${DOC_PATH})\\}|\\$\\((${PARTIAL_PATH})\\))$`);
7
7
  const REFERENCE_2 = new RegExp(`\\$\\{(${DOC_PATH})\\}|\\$\\((${PARTIAL_PATH})\\)`, 'g');
8
8
  const NO_TOKENS = [];
9
+ const utf16WidthAt = (text, offset) => text.codePointAt(offset) > 0xffff ? 2 : 1;
9
10
  /** Codepoint length of the UTF-16 slice `[a, b)` of `s`. */
10
11
  const codepointLen = (s, a, b) => {
11
12
  let n = 0;
12
13
  for (let i = a; i < b;) {
13
- i += s.codePointAt(i) > 0xffff ? 2 : 1;
14
+ i += utf16WidthAt(s, i);
14
15
  n++;
15
16
  }
16
17
  return n;
17
18
  };
19
+ /** Converts a 0-based codepoint offset to JavaScript/LSP UTF-16 units. */
20
+ export const codepointOffsetToUtf16 = (text, codepointOffset) => {
21
+ let utf16Offset = 0;
22
+ let codepoints = 0;
23
+ while (utf16Offset < text.length && codepoints < codepointOffset) {
24
+ utf16Offset += utf16WidthAt(text, utf16Offset);
25
+ codepoints++;
26
+ }
27
+ return utf16Offset;
28
+ };
29
+ /** Converts a 0-based JavaScript/LSP UTF-16 offset to a codepoint offset. */
30
+ export const utf16OffsetToCodepoint = (text, utf16Offset) => {
31
+ let currentUtf16 = 0;
32
+ let codepoints = 0;
33
+ const limit = Math.min(Math.max(utf16Offset, 0), text.length);
34
+ while (currentUtf16 < limit) {
35
+ currentUtf16 += utf16WidthAt(text, currentUtf16);
36
+ codepoints++;
37
+ }
38
+ return codepoints;
39
+ };
18
40
  /** Splice points (decoded value order): token text, UTF-16 index, path. */
19
41
  const scanDecoded = (value) => {
20
42
  REFERENCE_2.lastIndex = 0;
@@ -1,5 +1,19 @@
1
- import { type Diagnostic } from './core.js';
1
+ import { type Diagnostic, type PositionedReferenceParse, type PositionedValue } from './core.js';
2
2
  type Meta = Record<string, unknown>;
3
+ /**
4
+ * Resolve one document-reference path with the same traversal, cycle checks,
5
+ * and edge budget used by the public References 2.0 parser. The returned
6
+ * positioned value retains the definition site's source line.
7
+ */
8
+ export type DocumentReferenceTargetResolution = {
9
+ status: 'resolved';
10
+ target: PositionedValue;
11
+ } | {
12
+ status: 'unverifiable-partials';
13
+ } | {
14
+ status: 'unresolved';
15
+ };
16
+ export declare const resolveDocumentReferenceTarget: (parsed: PositionedReferenceParse, path: string) => DocumentReferenceTargetResolution;
3
17
  export type ParseMode = 'references' | 'core';
4
18
  export type ParseOptions = {
5
19
  mode?: ParseMode;
@@ -7,6 +7,10 @@ import { collectAllParticipants, deepCopyPositioned, earliestParticipant, emptyM
7
7
  const PARTIAL_NAME = '[a-zA-Z0-9_][a-zA-Z0-9_:/-]*';
8
8
  const PARTIAL_NAME_RE = new RegExp(`^${PARTIAL_NAME}$`);
9
9
  const MAX_EDGES = 3;
10
+ // Looking up the root path is itself the first reference edge; recursion into
11
+ // that root therefore gets the public resolver's remaining two-edge budget.
12
+ const TARGET_EDGE_BUDGET = MAX_EDGES - 1;
13
+ const referenceResourceStatuses = new WeakMap();
10
14
  /** Copies a pure-reference target and stamps the new insertion without
11
15
  * discarding provenance already attached to the copied root (R-112). */
12
16
  const copyWithInsertion = (target, insertedAt) => {
@@ -44,6 +48,91 @@ const documentTarget = (root, path) => {
44
48
  }
45
49
  return { root: value, tail: segments.slice(index) };
46
50
  };
51
+ export const resolveDocumentReferenceTarget = (parsed, path) => {
52
+ const { document } = parsed;
53
+ const target = documentTarget(document, path);
54
+ if (target === undefined)
55
+ return { status: 'unresolved' };
56
+ const resourceStatus = documentReferenceResourceStatus(parsed);
57
+ if (resourceStatus === 'unverifiable-partials')
58
+ return { status: resourceStatus };
59
+ if (resourceStatus === 'invalid')
60
+ return { status: 'unresolved' };
61
+ const ctx = { diagnostics: [], cache: new WeakMap() };
62
+ const stack = new Set([target.root]);
63
+ const resolved = resolveNode(target.root, document, new Map(), ctx, TARGET_EDGE_BUDGET, stack);
64
+ if (!resolved.complete || ctx.diagnostics.length > 0)
65
+ return { status: 'unresolved' };
66
+ const selected = lookupPath(resolved.value, target.tail);
67
+ return selected === undefined
68
+ ? { status: 'unresolved' }
69
+ : { status: 'resolved', target: selected };
70
+ };
71
+ const resolveDocument = (document, partials, ctx) => {
72
+ const resolved = new Map();
73
+ for (const [key, value] of document) {
74
+ const stack = new Set([value]);
75
+ resolved.set(key, resolveNode(value, document, partials, ctx, MAX_EDGES, stack).value);
76
+ }
77
+ return resolved;
78
+ };
79
+ const assertResolvedDocumentResourceLimits = (resolved) => {
80
+ const finalized = [...resolved].map(([key, value]) => [key, finalizePositioned(value)]);
81
+ const depth = finalized.length === 0 ? 0 : Math.max(...finalized.map(([, value]) => value.depth));
82
+ if (depth > NESTING_DEPTH_LIMIT) {
83
+ const participants = finalized
84
+ .filter(([, value]) => value.depth === depth)
85
+ .flatMap(([, value]) => value.deepestParticipants);
86
+ const winner = earliestParticipant(participants);
87
+ throw new LimaError({
88
+ code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
89
+ ...(winner?.offset !== undefined ? { column: winner.offset + 1 } : {}),
90
+ message: `Lima: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line ${winner?.line ?? 1}`,
91
+ });
92
+ }
93
+ let nodeCount = 1;
94
+ for (const [, value] of finalized)
95
+ nodeCount += value.nodeCount;
96
+ if (nodeCount > RESULT_NODE_LIMIT) {
97
+ const participants = [];
98
+ for (const value of resolved.values())
99
+ collectAllParticipants(value, participants);
100
+ const winner = earliestParticipant(participants);
101
+ throw new LimaError({
102
+ code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
103
+ ...(winner?.offset !== undefined ? { column: winner.offset + 1 } : {}),
104
+ message: `Lima: result exceeds maximum size of ${RESULT_NODE_LIMIT} total nodes at line ${winner?.line ?? 1}`,
105
+ });
106
+ }
107
+ return finalized;
108
+ };
109
+ const documentReferenceResourceStatus = (parsed) => {
110
+ // Partial values are supplied by caller code and can change whole-document
111
+ // depth/node limits. With no file convention for them, editor tooling cannot
112
+ // prove that even an unrelated document reference is safe to resolve.
113
+ if (parsed.references.some((reference) => reference.partialPath !== undefined))
114
+ return 'unverifiable-partials';
115
+ const { document } = parsed;
116
+ const cached = referenceResourceStatuses.get(document);
117
+ if (cached !== undefined)
118
+ return cached;
119
+ const ctx = { diagnostics: [], cache: new WeakMap() };
120
+ const resolved = resolveDocument(document, new Map(), ctx);
121
+ let valid = !ctx.diagnostics.some((diagnostic) => diagnostic.code === 'RESOURCE_LIMIT');
122
+ if (valid) {
123
+ try {
124
+ assertResolvedDocumentResourceLimits(resolved);
125
+ }
126
+ catch (error) {
127
+ if (!(error instanceof LimaError) || error.code !== 'RESOURCE_LIMIT')
128
+ throw error;
129
+ valid = false;
130
+ }
131
+ }
132
+ const status = valid ? 'valid' : 'invalid';
133
+ referenceResourceStatuses.set(document, status);
134
+ return status;
135
+ };
47
136
  const lookupPartial = (partials, path) => {
48
137
  const dot = path.indexOf('.');
49
138
  if (dot < 0)
@@ -284,11 +373,7 @@ const parseInternal = (frontMatter, options, useResolveCache) => {
284
373
  }
285
374
  const document = parseCoreWithPositions(frontMatter, { strict: options?.strict ?? false, onWarning: options?.onWarning });
286
375
  const ctx = { diagnostics: [], ...(useResolveCache ? { cache: new WeakMap() } : {}) };
287
- const resolved = new Map();
288
- for (const [key, value] of document) {
289
- const stack = new Set([value]);
290
- resolved.set(key, resolveNode(value, document, partials, ctx, MAX_EDGES, stack).value);
291
- }
376
+ const resolved = resolveDocument(document, partials, ctx);
292
377
  if (options?.strict)
293
378
  for (const value of resolved.values())
294
379
  scanUnresolved(value, ctx);
@@ -299,31 +384,7 @@ const parseInternal = (frontMatter, options, useResolveCache) => {
299
384
  // already do; `offset` was internal-only here.
300
385
  throw new LimaError({ ...winner, column: winner.offset + 1 });
301
386
  }
302
- const finalized = [...resolved].map(([key, value]) => [key, finalizePositioned(value)]);
303
- const depth = finalized.length === 0 ? 0 : Math.max(...finalized.map(([, value]) => value.depth));
304
- if (depth > NESTING_DEPTH_LIMIT) {
305
- const participants = finalized.filter(([, value]) => value.depth === depth).flatMap(([, value]) => value.deepestParticipants);
306
- const winner = earliestParticipant(participants);
307
- throw new LimaError({
308
- code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
309
- ...(winner?.offset !== undefined ? { column: winner.offset + 1 } : {}),
310
- message: `Lima: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line ${winner?.line ?? 1}`,
311
- });
312
- }
313
- let nodeCount = 1;
314
- for (const [, value] of finalized)
315
- nodeCount += value.nodeCount;
316
- if (nodeCount > RESULT_NODE_LIMIT) {
317
- const participants = [];
318
- for (const value of resolved.values())
319
- collectAllParticipants(value, participants);
320
- const winner = earliestParticipant(participants);
321
- throw new LimaError({
322
- code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
323
- ...(winner?.offset !== undefined ? { column: winner.offset + 1 } : {}),
324
- message: `Lima: result exceeds maximum size of ${RESULT_NODE_LIMIT} total nodes at line ${winner?.line ?? 1}`,
325
- });
326
- }
387
+ const finalized = assertResolvedDocumentResourceLimits(resolved);
327
388
  const out = emptyMapping();
328
389
  for (const [key, value] of finalized)
329
390
  out[key] = value.native;
package/dist/scalars.js CHANGED
@@ -13,6 +13,7 @@ export const hasActiveReferences2 = (value) => value.kind === 'string' ? (value.
13
13
  : false;
14
14
  /** The `ValueBuilder<PositionedValue>` — reconstructs today's annotated tree exactly, for References. */
15
15
  export const positionedBuilder = {
16
+ positions: true,
16
17
  null: (line) => ({ kind: 'null', line }),
17
18
  bool: (value, line) => ({ kind: 'bool', value, line }),
18
19
  int: (value, line) => ({ kind: 'int', value, line }),
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@limaformat/lima",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Lima frontmatter parser — LIMA Is Metadata Annotation",
5
5
  "keywords": ["frontmatter", "parser", "yaml", "markdown", "metadata"],
6
6
  "homepage": "https://limaformat.dev",
7
7
  "author": "Michael Kortstiege",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/limaformat/lima",
10
+ "url": "git+https://github.com/limaformat/lima.git",
11
11
  "directory": "js"
12
12
  },
13
13
  "bugs": "https://github.com/limaformat/lima/issues",
@@ -23,8 +23,10 @@
23
23
  "engines": { "node": ">=18" },
24
24
  "scripts": {
25
25
  "build": "tsc -p tsconfig.build.json",
26
+ "prepack": "bun run build",
26
27
  "pretest": "bun run build",
27
28
  "test": "bun test",
29
+ "pretypecheck": "bun run build",
28
30
  "typecheck": "tsc --noEmit",
29
31
  "prepublishOnly": "bun run typecheck && bun test",
30
32
  "bench": "bun run bench/index.ts",
@@ -1,38 +0,0 @@
1
- /**
2
- * Lima References 1.0 — layered strictly on top of `core.ts` (Appendix B:
3
- * reference resolution is exclusively this extension's concern; Core never
4
- * sees it). Reads the annotated `PositionedValue` tree Core produces and
5
- * performs the two-phase resolution the spec describes (§4), keeping every
6
- * intermediate result in the SAME annotated representation instead of a
7
- * separate raw-value tree with a bolted-on marker for "inert":
8
- *
9
- * - A string's `quoted` flag (set once, by Core, from actual quote syntax)
10
- * is the single source of truth for "never treat this as a reference
11
- * site" — §2.3 (quoted tokens) and §3.8 (partial content) are the same
12
- * rule applied to two different origins, so partial values are wrapped
13
- * into this same representation with every string leaf pre-marked
14
- * `quoted: true` before insertion (`partialToPositioned`), rather than
15
- * needing a second, unrelated mechanism.
16
- * - Every node keeps the source `line` it was parsed with, even after
17
- * being deep-copied into a new position — diagnostics read it directly
18
- * off the node instead of falling back to a coarser key-level lookup.
19
- *
20
- * There is no module-level mutable state: diagnostics are collected in a
21
- * `ResolutionContext` created fresh per call and threaded explicitly.
22
- */
23
- import { type Diagnostic } from './core.js';
24
- type Meta = Record<string, unknown>;
25
- export type ReferencesOptions = {
26
- /** Named values available via `(%key)` references. */
27
- partials?: Meta;
28
- strict?: boolean;
29
- /** Core §11.2 (inherited): callback for non-strict warnings (e.g. duplicate keys). Discarded if omitted. */
30
- onWarning?: (diagnostic: Diagnostic) => void;
31
- };
32
- export declare const parseReferences: <T extends Record<string, unknown> = Meta>(frontMatter: string, options?: ReferencesOptions) => T;
33
- /** Backward-compatible primary entry point — References layered on Core. */
34
- export declare const parse: typeof parseReferences;
35
- export type ParseOptions = ReferencesOptions;
36
- /** Internal conformance entry point for the frozen References 1.0 suite. */
37
- export declare const parseReferencesV1: typeof parseReferences;
38
- export {};
@@ -1,413 +0,0 @@
1
- /**
2
- * Lima References 1.0 — layered strictly on top of `core.ts` (Appendix B:
3
- * reference resolution is exclusively this extension's concern; Core never
4
- * sees it). Reads the annotated `PositionedValue` tree Core produces and
5
- * performs the two-phase resolution the spec describes (§4), keeping every
6
- * intermediate result in the SAME annotated representation instead of a
7
- * separate raw-value tree with a bolted-on marker for "inert":
8
- *
9
- * - A string's `quoted` flag (set once, by Core, from actual quote syntax)
10
- * is the single source of truth for "never treat this as a reference
11
- * site" — §2.3 (quoted tokens) and §3.8 (partial content) are the same
12
- * rule applied to two different origins, so partial values are wrapped
13
- * into this same representation with every string leaf pre-marked
14
- * `quoted: true` before insertion (`partialToPositioned`), rather than
15
- * needing a second, unrelated mechanism.
16
- * - Every node keeps the source `line` it was parsed with, even after
17
- * being deep-copied into a new position — diagnostics read it directly
18
- * off the node instead of falling back to a coarser key-level lookup.
19
- *
20
- * There is no module-level mutable state: diagnostics are collected in a
21
- * `ResolutionContext` created fresh per call and threaded explicitly.
22
- */
23
- import { countNodes, canonicalString, codepointLength, ingestPartialValue, PARTIAL_COUNT_LIMIT, PARTIAL_NAME_LENGTH_LIMIT, PARTIAL_NODE_LIMIT, RESULT_NODE_LIMIT, SCALAR_LENGTH_LIMIT, } from './value.js';
24
- import { parseCoreWithPositions, toPlainValue, NESTING_DEPTH_LIMIT, } from './core.js';
25
- import { LimaError } from './errors.js';
26
- import { collectAllParticipants, deepCopyPositioned, earliestParticipant, emptyMapping, finalizePositioned, partialToPositioned, } from './positioned-tree.js';
27
- // References §2.1/§2.2/Appendix B grammar, per sigil.
28
- const DOC_SEGMENT = '[a-zA-Z0-9_][a-zA-Z0-9_:-]*';
29
- const DOC_PATH = `${DOC_SEGMENT}(?:\\.${DOC_SEGMENT})*`;
30
- const PARTIAL_KEY = '[a-zA-Z0-9_][a-zA-Z0-9_:/-]*';
31
- const PURE_REF_RE = new RegExp(`^\\((?:\\$(${DOC_PATH})|%(${PARTIAL_KEY}))\\)$`);
32
- const INTERP_RE = new RegExp(`\\((?:\\$(${DOC_PATH})|%(${PARTIAL_KEY}))\\)`, 'g');
33
- /**
34
- * §5: of every reference-resolution error encountered during a single
35
- * resolveTree pass, only the one at the lowest source line is ultimately
36
- * thrown. Tracking a running minimum instead of collecting every diagnostic
37
- * into an array and sorting it at the end avoids an O(e log e) sort (and
38
- * its allocations) for documents with many reference errors — `<` (not
39
- * `<=`) preserves the original stable-sort-then-take-first tie-break: the
40
- * first diagnostic reported at any given minimum line keeps winning over a
41
- * later one at the same line, exactly as it did when every one of them was
42
- * pushed into an array and stably sorted by line.
43
- */
44
- const reportDiagnostic = (ctx, d) => {
45
- if (ctx.best === null || d.line < ctx.best.line)
46
- ctx.best = d;
47
- };
48
- // ─── PositionedValue-level helpers ─────────────────────────────────────────
49
- const getNestedValueP = (root, path) => {
50
- if (!path.includes('.'))
51
- return root.get(path);
52
- const parts = path.split('.');
53
- let cur = root.get(parts[0]);
54
- for (let i = 1; i < parts.length; i++) {
55
- if (cur === undefined || cur.kind !== 'mapping')
56
- return undefined;
57
- cur = cur.entries.get(parts[i]);
58
- }
59
- return cur;
60
- };
61
- /**
62
- * `resolveTree` re-checks reference-freedom repeatedly on the SAME target
63
- * subtree — once per reference pointing at it (the fast-path guard at the
64
- * top of `resolveTree`, once more before copying a pure-reference target,
65
- * once more per interpolation match) — so a document with many references
66
- * into one large shared mapping would otherwise re-traverse that whole
67
- * subtree for every single reference. `PositionedValue` nodes are only ever
68
- * produced fresh (by Core, or by `deepCopyPositioned` below) and never
69
- * mutated in place, so object identity is a stable cache key for the
70
- * lifetime of a single `parseReferences` call — a module-level `WeakMap`
71
- * is safe (and self-cleaning: entries vanish once a parse's tree is
72
- * garbage-collected) rather than needing a per-call cache threaded through
73
- * every function.
74
- */
75
- const referenceFreeCache = new WeakMap();
76
- /**
77
- * A reference is only resolved from a target that is itself reference-free
78
- * (§4). Quoted strings are always free (§2.3); partial-derived values are
79
- * always free too, but never reach this function — see `partialToPositioned`.
80
- */
81
- const isReferenceFreeP = (v) => {
82
- if (v.kind !== 'string' && v.kind !== 'array' && v.kind !== 'mapping')
83
- return true;
84
- const cached = referenceFreeCache.get(v);
85
- if (cached !== undefined)
86
- return cached;
87
- let result;
88
- if (v.kind === 'string') {
89
- result = v.quoted || (!v.value.includes('($') && !v.value.includes('(%'));
90
- }
91
- else if (v.kind === 'array') {
92
- result = v.items.every(isReferenceFreeP);
93
- }
94
- else {
95
- result = true;
96
- for (const c of v.entries.values())
97
- if (!isReferenceFreeP(c)) {
98
- result = false;
99
- break;
100
- }
101
- }
102
- referenceFreeCache.set(v, result);
103
- return result;
104
- };
105
- /**
106
- * Recursively resolves reference-shaped string leaves in `node` against
107
- * `lookup` — the single function both resolution phases share (§4.1's live
108
- * backward-reference pass and §4.2's snapshot-based forward pass differ
109
- * only in which `lookup` map and which source tree they're applied to, not
110
- * in the resolution logic itself). Errors are collected into `ctx`, never
111
- * thrown directly — see the module doc comment on error ordering.
112
- */
113
- const resolveTree = (node, lookup, partials, ctx) => {
114
- // Fast path: a subtree with no active reference-shaped string anywhere
115
- // inside it has nothing this function could change — skip rebuilding
116
- // it. Matters most for phase 2's redundant re-walk (see the module doc
117
- // comment) of a large already-resolved value, e.g. a big partial copied
118
- // in by several separate references: without this, every element gets
119
- // a fresh resolveTree call and every array/mapping level gets
120
- // reconstructed via .map()/a new Map on a pass that can only ever
121
- // return its input unchanged.
122
- if (node.kind !== 'string' && isReferenceFreeP(node))
123
- return node;
124
- if (node.kind === 'string') {
125
- if (node.quoted)
126
- return node;
127
- const val = node.value;
128
- // Pure reference: entire value is exactly one ($path) or (%key).
129
- if (val.charCodeAt(0) === 40 && val.charCodeAt(val.length - 1) === 41) {
130
- const m = val.match(PURE_REF_RE);
131
- if (m) {
132
- const isPartial = m[2] !== undefined;
133
- const key = (isPartial ? m[2] : m[1]);
134
- // R-112: stamped on the copy's root only — descendants keep
135
- // whatever insertedAt they already carried from an earlier,
136
- // more deeply nested resolution (see deepCopyPositioned).
137
- const insertedAt = { line: node.line, token: val };
138
- if (isPartial) {
139
- const target = partials.get(key);
140
- // §3.1 deep-copy requirement: `target` is the SAME cached tree
141
- // built once per partial name at ingestion (see
142
- // `partialToPositioned`'s call site) — every pure reference to
143
- // this partial anywhere in the document retrieves that one
144
- // tree. A shallow spread of its root would leave descendants
145
- // (including a mutable `Date` in an `instant` node) aliased
146
- // across every such reference; deep-copying here, exactly like
147
- // the document-reference branch below, is what actually
148
- // satisfies the guarantee.
149
- if (target !== undefined)
150
- return { ...deepCopyPositioned(target), insertedAt };
151
- }
152
- else {
153
- const target = getNestedValueP(lookup, key);
154
- if (target !== undefined && isReferenceFreeP(target)) {
155
- return { ...deepCopyPositioned(target), insertedAt };
156
- }
157
- }
158
- // Unresolved (or target not yet reference-free) — leave unchanged.
159
- }
160
- }
161
- // String interpolation: replace all ($path) / (%key) occurrences.
162
- if (val.includes('($') || val.includes('(%')) {
163
- const replaced = val.replace(INTERP_RE, (match, docPath, partialKey) => {
164
- const isPartial = partialKey !== undefined;
165
- const key = isPartial ? partialKey : docPath;
166
- const target = isPartial ? partials.get(key) : getNestedValueP(lookup, key);
167
- if (target === undefined || (!isPartial && !isReferenceFreeP(target)))
168
- return match;
169
- if (target.kind === 'mapping') {
170
- reportDiagnostic(ctx, {
171
- code: 'INVALID_INTERPOLATION', line: node.line, token: match,
172
- message: `Lima: invalid interpolation of "${match}" at line ${node.line}: mapping cannot be interpolated into a string`,
173
- });
174
- return match;
175
- }
176
- if (target.kind === 'array') {
177
- if (target.items.some((item) => item.kind === 'array' || item.kind === 'mapping')) {
178
- reportDiagnostic(ctx, {
179
- code: 'INVALID_INTERPOLATION', line: node.line, token: match,
180
- message: `Lima: invalid interpolation of "${match}" at line ${node.line}: array contains a nested array or mapping`,
181
- });
182
- return match;
183
- }
184
- return target.items.map((item) => canonicalString(toPlainValue(item))).join(', ');
185
- }
186
- return canonicalString(toPlainValue(target));
187
- });
188
- // §6.2 final scalar-length limit: interpolation can grow a string
189
- // past the limit even when neither the raw document text nor the
190
- // interpolated target individually violated it — a hard error in
191
- // both modes, thrown immediately like the other resource limits
192
- // (never part of the ordered diagnostics set below).
193
- if (codepointLength(replaced) > SCALAR_LENGTH_LIMIT) {
194
- throw new LimaError({
195
- code: 'RESOURCE_LIMIT', line: node.line,
196
- message: `Lima: scalar exceeds maximum length of ${SCALAR_LENGTH_LIMIT} code points at line ${node.line}`,
197
- });
198
- }
199
- return { kind: 'string', value: replaced, line: node.line, quoted: false };
200
- }
201
- return node;
202
- }
203
- if (node.kind === 'array') {
204
- return {
205
- kind: 'array',
206
- line: node.line,
207
- // Phase 2 redundantly re-walks values phase 1 already resolved
208
- // (harmless — see the module doc comment) — including, sometimes,
209
- // a node that phase 1 already stamped with insertedAt. Must be
210
- // carried over here, not just at the point of insertion, or a
211
- // reference's provenance silently disappears on the second pass.
212
- insertedAt: node.insertedAt,
213
- items: node.items.map((item) => {
214
- const resolved = resolveTree(item, lookup, partials, ctx);
215
- if (item.kind === 'string' && resolved.kind === 'array') {
216
- // References Appendix: array spreading was removed, and a
217
- // nested array produced by reference insertion violates
218
- // Core §7.2 (sequences contain scalars or mappings only) —
219
- // throws in BOTH modes (R-036/R-143).
220
- reportDiagnostic(ctx, {
221
- code: 'INVALID_REFERENCE_SHAPE', line: item.line, token: item.value,
222
- message: `Lima: reference "${item.value}" resolves to an array, which cannot be inserted as a sequence item at line ${item.line}`,
223
- });
224
- return item;
225
- }
226
- return resolved;
227
- }),
228
- };
229
- }
230
- if (node.kind === 'mapping') {
231
- const entries = new Map();
232
- for (const [k, c] of node.entries)
233
- entries.set(k, resolveTree(c, lookup, partials, ctx));
234
- return { kind: 'mapping', entries, line: node.line, insertedAt: node.insertedAt };
235
- }
236
- return node; // null/bool/int/float/instant — nothing to resolve
237
- };
238
- export const parseReferences = (frontMatter, options) => {
239
- const strict = options?.strict ?? false;
240
- const rawPartials = options?.partials ?? {};
241
- // §6.2 / R-114: partials are validated and deep-copied before document
242
- // parsing begins — this must run even for an empty document. Partial
243
- // errors carry a value path, never a document line (parsing hasn't
244
- // started), so they throw directly rather than joining the ordered
245
- // diagnostics collected below.
246
- const partialNames = Object.keys(rawPartials);
247
- if (partialNames.length > PARTIAL_COUNT_LIMIT) {
248
- throw new LimaError({ code: 'INVALID_PARTIAL', message: `Lima: too many partials (max ${PARTIAL_COUNT_LIMIT})` });
249
- }
250
- for (const name of partialNames) {
251
- if (codepointLength(name) > PARTIAL_NAME_LENGTH_LIMIT) {
252
- throw new LimaError({
253
- code: 'INVALID_PARTIAL', partial: name, path: name,
254
- message: `Lima: invalid partial "${name}" at path "${name}": name exceeds maximum length of ${PARTIAL_NAME_LENGTH_LIMIT} code points`,
255
- });
256
- }
257
- }
258
- const partials = new Map();
259
- for (const [name, value] of Object.entries(rawPartials)) {
260
- partials.set(name, ingestPartialValue(value, name, name));
261
- }
262
- let totalPartialNodes = 0;
263
- for (const v of partials.values())
264
- totalPartialNodes += countNodes(v);
265
- if (totalPartialNodes > PARTIAL_NODE_LIMIT) {
266
- throw new LimaError({
267
- code: 'INVALID_PARTIAL',
268
- message: `Lima: partials exceed the combined maximum of ${PARTIAL_NODE_LIMIT} value nodes`,
269
- });
270
- }
271
- // Converted to the annotated representation once per partial, not once
272
- // per reference: nothing downstream ever mutates a PositionedValue tree
273
- // in place (resolveTree's fast path returns inert subtrees unchanged;
274
- // toNativeFromPositioned always allocates fresh native containers for
275
- // the public result), so every reference to the same partial can safely
276
- // reuse the same body and only needs its own `insertedAt` stamped onto
277
- // a shallow copy of the root — no per-reference deep copy required. The
278
- // internal `line` on partial-derived nodes is never read downstream
279
- // (every diagnostic that could involve one uses the referencing site's
280
- // own line instead), so the placeholder `0` here is fine.
281
- const partialsPositioned = new Map();
282
- for (const [name, value] of partials)
283
- partialsPositioned.set(name, partialToPositioned(value, 0));
284
- const root = parseCoreWithPositions(frontMatter, { strict, onWarning: options?.onWarning });
285
- const ctx = { best: null };
286
- const hasRefs = frontMatter.includes('($') || frontMatter.includes('(%');
287
- let finalMap;
288
- if (!hasRefs) {
289
- finalMap = root;
290
- }
291
- else {
292
- // Phase 1 (§4.1): resolve each top-level key's value in document
293
- // order against a live, growing snapshot of already-processed keys
294
- // — reproduces backward-reference resolution without needing a
295
- // second pass for the common case.
296
- const phase1Live = new Map();
297
- // §3.7/§4 one-hop limit: a top-level key whose OWN inline value was
298
- // itself a pure reference token must never be usable as another
299
- // key's hop target via its (possibly already-resolved) live value —
300
- // only its original token text is a valid phase-2 lookup target, so
301
- // a chain a→b→c can never fully resolve regardless of where c
302
- // happens to be written relative to b (§4: independent of mapping
303
- // enumeration order).
304
- const originalPureRefText = new Map();
305
- for (const [key, node] of root) {
306
- if (node.kind === 'string' && !node.quoted && PURE_REF_RE.test(node.value)) {
307
- originalPureRefText.set(key, node.value);
308
- }
309
- phase1Live.set(key, resolveTree(node, phase1Live, partialsPositioned, ctx));
310
- }
311
- // Phase 2 (§4.2): re-resolve every key's phase-1 value — this is
312
- // what catches forward references — against a snapshot that is
313
- // immutable for the whole phase (a plain reference to a Map built
314
- // once above, no cloning needed: nothing here mutates it).
315
- const phase1Snapshot = new Map();
316
- for (const [key, value] of phase1Live) {
317
- phase1Snapshot.set(key, originalPureRefText.has(key)
318
- ? { kind: 'string', value: originalPureRefText.get(key), line: root.get(key).line, quoted: false }
319
- : value);
320
- }
321
- finalMap = new Map();
322
- for (const [key] of root) {
323
- finalMap.set(key, resolveTree(phase1Live.get(key), phase1Snapshot, partialsPositioned, ctx));
324
- }
325
- }
326
- // Strict mode: collect every reference still unresolved after both
327
- // phases. Each leaf reports its OWN line — every node in this tree
328
- // carries one from the moment Core parsed it, so no separate
329
- // provenance-tracking pass is needed to attribute this precisely.
330
- if (strict) {
331
- const scanUnresolved = (v) => {
332
- if (v.kind === 'string') {
333
- if (!v.quoted && (v.value.includes('($') || v.value.includes('(%'))) {
334
- const m = v.value.match(/\(([%$])([^)]+)\)/);
335
- if (m) {
336
- reportDiagnostic(ctx, {
337
- code: 'UNRESOLVED_REFERENCE', line: v.line, token: `(${m[1]}${m[2]})`,
338
- message: `Lima: unresolved reference "(${m[1]}${m[2]})" at line ${v.line}`,
339
- });
340
- }
341
- }
342
- return;
343
- }
344
- if (v.kind === 'array') {
345
- for (const item of v.items)
346
- scanUnresolved(item);
347
- return;
348
- }
349
- if (v.kind === 'mapping') {
350
- for (const c of v.entries.values())
351
- scanUnresolved(c);
352
- return;
353
- }
354
- };
355
- for (const v of finalMap.values())
356
- scanUnresolved(v);
357
- }
358
- // §5: of every reference-resolution error collected above, the one at
359
- // the lowest source line is thrown; the rest are discarded.
360
- if (ctx.best !== null) {
361
- throw new LimaError(ctx.best);
362
- }
363
- // Depth (Core §9, re-checked on the final POST-substitution tree —
364
- // inserted values can add depth Core's own pre-resolution check inside
365
- // parseCoreWithPositions could not see yet), node count (§6.2), and the
366
- // native result itself all come out of one pass over `finalMap` — see
367
- // finalizePositioned's doc comment for why this replaces three
368
- // independent full-tree walks.
369
- const finalResults = [];
370
- for (const [k, v] of finalMap)
371
- finalResults.push([k, finalizePositioned(v)]);
372
- // References §5/R-112: attributed to the lowest-line reference token
373
- // among the insertions on the actual deepest path, or line 1 (R-113)
374
- // when the excess depth came entirely from literal, non-reference
375
- // content.
376
- const depth = finalResults.length === 0 ? 0 : Math.max(...finalResults.map(([, r]) => r.depth));
377
- if (depth > NESTING_DEPTH_LIMIT) {
378
- const participants = finalResults.filter(([, r]) => r.depth === depth).flatMap(([, r]) => r.deepestParticipants);
379
- const winner = earliestParticipant(participants);
380
- throw new LimaError({
381
- code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
382
- message: winner
383
- ? `Lima: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line ${winner.line}: "${winner.token}"`
384
- : `Lima: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line 1`,
385
- });
386
- }
387
- // §6.2: total node count of the final result tree, both modes. Same
388
- // R-112 attribution — every reference insertion anywhere in the tree
389
- // contributes to the total, so the lowest-line one is reported.
390
- let totalResultNodes = 1; // the root mapping itself counts as one node
391
- for (const [, r] of finalResults)
392
- totalResultNodes += r.nodeCount;
393
- if (totalResultNodes > RESULT_NODE_LIMIT) {
394
- const participants = [];
395
- for (const v of finalMap.values())
396
- collectAllParticipants(v, participants);
397
- const winner = earliestParticipant(participants);
398
- throw new LimaError({
399
- code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
400
- message: winner
401
- ? `Lima: result exceeds maximum size of ${RESULT_NODE_LIMIT} total nodes at line ${winner.line}: "${winner.token}"`
402
- : `Lima: result exceeds maximum size of ${RESULT_NODE_LIMIT} total nodes at line 1`,
403
- });
404
- }
405
- const out = emptyMapping();
406
- for (const [k, r] of finalResults)
407
- out[k] = r.native;
408
- return out;
409
- };
410
- /** Backward-compatible primary entry point — References layered on Core. */
411
- export const parse = parseReferences;
412
- /** Internal conformance entry point for the frozen References 1.0 suite. */
413
- export const parseReferencesV1 = parseReferences;