semantifold 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +71 -0
  3. package/build/index.d.ts +54 -0
  4. package/build/index.d.ts.map +1 -0
  5. package/build/index.js +45 -0
  6. package/build/src/backends/identifiers.d.ts +19 -0
  7. package/build/src/backends/identifiers.d.ts.map +1 -0
  8. package/build/src/backends/identifiers.js +93 -0
  9. package/build/src/backends/index.d.ts +35 -0
  10. package/build/src/backends/index.d.ts.map +1 -0
  11. package/build/src/backends/index.js +149 -0
  12. package/build/src/backends/java.d.ts +8 -0
  13. package/build/src/backends/java.d.ts.map +1 -0
  14. package/build/src/backends/java.js +172 -0
  15. package/build/src/backends/javascript.d.ts +8 -0
  16. package/build/src/backends/javascript.d.ts.map +1 -0
  17. package/build/src/backends/javascript.js +173 -0
  18. package/build/src/backends/php.d.ts +8 -0
  19. package/build/src/backends/php.d.ts.map +1 -0
  20. package/build/src/backends/php.js +175 -0
  21. package/build/src/backends/ruby.d.ts +8 -0
  22. package/build/src/backends/ruby.d.ts.map +1 -0
  23. package/build/src/backends/ruby.js +155 -0
  24. package/build/src/backends/scalars.d.ts +15 -0
  25. package/build/src/backends/scalars.d.ts.map +1 -0
  26. package/build/src/backends/scalars.js +138 -0
  27. package/build/src/backends/shared.d.ts +26 -0
  28. package/build/src/backends/shared.d.ts.map +1 -0
  29. package/build/src/backends/shared.js +424 -0
  30. package/build/src/backends/typescript.d.ts +8 -0
  31. package/build/src/backends/typescript.d.ts.map +1 -0
  32. package/build/src/backends/typescript.js +160 -0
  33. package/build/src/backends/writer.d.ts +87 -0
  34. package/build/src/backends/writer.d.ts.map +1 -0
  35. package/build/src/backends/writer.js +207 -0
  36. package/build/src/diagnostic.d.ts +67 -0
  37. package/build/src/diagnostic.d.ts.map +1 -0
  38. package/build/src/diagnostic.js +96 -0
  39. package/build/src/frontends/index.d.ts +14 -0
  40. package/build/src/frontends/index.d.ts.map +1 -0
  41. package/build/src/frontends/index.js +36 -0
  42. package/build/src/frontends/java.d.ts +12 -0
  43. package/build/src/frontends/java.d.ts.map +1 -0
  44. package/build/src/frontends/java.js +630 -0
  45. package/build/src/frontends/javascript-typescript.d.ts +19 -0
  46. package/build/src/frontends/javascript-typescript.d.ts.map +1 -0
  47. package/build/src/frontends/javascript-typescript.js +633 -0
  48. package/build/src/frontends/php.d.ts +12 -0
  49. package/build/src/frontends/php.d.ts.map +1 -0
  50. package/build/src/frontends/php.js +523 -0
  51. package/build/src/frontends/ruby.d.ts +12 -0
  52. package/build/src/frontends/ruby.d.ts.map +1 -0
  53. package/build/src/frontends/ruby.js +515 -0
  54. package/build/src/frontends/scalars.d.ts +19 -0
  55. package/build/src/frontends/scalars.d.ts.map +1 -0
  56. package/build/src/frontends/scalars.js +71 -0
  57. package/build/src/mapping.d.ts +160 -0
  58. package/build/src/mapping.d.ts.map +1 -0
  59. package/build/src/mapping.js +1120 -0
  60. package/build/src/semantic/location.d.ts +61 -0
  61. package/build/src/semantic/location.d.ts.map +1 -0
  62. package/build/src/semantic/location.js +142 -0
  63. package/build/src/semantic/operators.d.ts +16 -0
  64. package/build/src/semantic/operators.d.ts.map +1 -0
  65. package/build/src/semantic/operators.js +24 -0
  66. package/build/src/semantic/provenance.d.ts +74 -0
  67. package/build/src/semantic/provenance.d.ts.map +1 -0
  68. package/build/src/semantic/provenance.js +577 -0
  69. package/build/src/semantic/scalars.d.ts +19 -0
  70. package/build/src/semantic/scalars.d.ts.map +1 -0
  71. package/build/src/semantic/scalars.js +40 -0
  72. package/build/src/semantic/types.d.ts +1003 -0
  73. package/build/src/semantic/types.d.ts.map +1 -0
  74. package/build/src/semantic/types.js +279 -0
  75. package/build/src/semantic/validate.d.ts +60 -0
  76. package/build/src/semantic/validate.d.ts.map +1 -0
  77. package/build/src/semantic/validate.js +472 -0
  78. package/docs/architecture.md +39 -0
  79. package/docs/coding-standards.md +7 -0
  80. package/docs/goals.md +7 -0
  81. package/docs/language-support.md +69 -0
  82. package/docs/plans/2026-09-02-initial-toolchain.md +54 -0
  83. package/docs/source-maps.md +134 -0
  84. package/docs/testing.md +25 -0
  85. package/index.js +63 -0
  86. package/package.json +69 -0
  87. package/src/backends/identifiers.js +101 -0
  88. package/src/backends/index.js +159 -0
  89. package/src/backends/java.js +182 -0
  90. package/src/backends/javascript.js +184 -0
  91. package/src/backends/php.js +184 -0
  92. package/src/backends/ruby.js +163 -0
  93. package/src/backends/scalars.js +120 -0
  94. package/src/backends/shared.js +456 -0
  95. package/src/backends/typescript.js +168 -0
  96. package/src/backends/writer.js +224 -0
  97. package/src/diagnostic.js +103 -0
  98. package/src/frontends/index.js +36 -0
  99. package/src/frontends/java.js +735 -0
  100. package/src/frontends/javascript-typescript.js +718 -0
  101. package/src/frontends/php.js +593 -0
  102. package/src/frontends/ruby.js +622 -0
  103. package/src/frontends/scalars.js +77 -0
  104. package/src/mapping.js +1260 -0
  105. package/src/semantic/location.js +154 -0
  106. package/src/semantic/operators.js +28 -0
  107. package/src/semantic/provenance.js +629 -0
  108. package/src/semantic/scalars.js +44 -0
  109. package/src/semantic/types.js +319 -0
  110. package/src/semantic/validate.js +529 -0
  111. package/tsconfig.json +16 -0
package/src/mapping.js ADDED
@@ -0,0 +1,1260 @@
1
+ // @ts-check
2
+
3
+ import {addSegment, GenMapping, maybeAddSegment, setSourceContent, toEncodedMap} from "@jridgewell/gen-mapping"
4
+ import {
5
+ eachMapping,
6
+ originalPositionFor as traceOriginalPositionFor,
7
+ sourceContentFor,
8
+ TraceMap
9
+ } from "@jridgewell/trace-mapping"
10
+ import {SemantifoldDiagnostic} from "./diagnostic.js"
11
+ import {createCoordinateIndex, indexedOffsetAt, indexedPointAt, locationFromOffsets} from "./semantic/location.js"
12
+ import {primaryLocation} from "./semantic/provenance.js"
13
+
14
+ /** @typedef {ReturnType<typeof buildMappingIndex>} MappingIndex */
15
+ /** @type {WeakMap<import("./semantic/types.js").SemantifoldMapping, MappingIndex>} */
16
+ const mappingIndexes = new WeakMap()
17
+
18
+ /**
19
+ * Projects an authoritative Semantifold mapping to Source Map v3.
20
+ * @param {import("./semantic/types.js").SemantifoldMapping} mapping - Rich mapping.
21
+ * @returns {import("@jridgewell/gen-mapping").EncodedSourceMap} Source Map v3.
22
+ */
23
+ export function toSourceMapV3(mapping) {
24
+ const index = mappingIndex(mapping)
25
+
26
+ const generated = new GenMapping({file: mapping.generated.filename})
27
+
28
+ for (const source of mapping.sources) setSourceContent(generated, source.filename, source.content)
29
+
30
+ for (const span of mapping.spans) {
31
+ const location = primaryLocation(span.origin)
32
+ const line = span.generated.start.line - 1
33
+ const column = span.generated.start.column - 1
34
+
35
+ if (!location || span.mappingKind == "synthetic") {
36
+ maybeAddSegment(generated, line, column)
37
+ continue
38
+ }
39
+
40
+ const registered = sourceForOrigin(mapping, span.origin, location, index.sourcesById)
41
+
42
+ if (span.name) {
43
+ addSegment(generated, line, column, registered.filename, location.start.line - 1, location.start.column - 1, span.name, registered.content)
44
+ } else {
45
+ addSegment(generated, line, column, registered.filename, location.start.line - 1, location.start.column - 1, null, registered.content)
46
+ }
47
+ }
48
+
49
+ return toEncodedMap(generated)
50
+ }
51
+
52
+ /**
53
+ * Looks up original provenance for one generated UTF-16 position.
54
+ * @param {import("./semantic/types.js").SemantifoldMapping} mapping - Rich mapping.
55
+ * @param {{offset: number} | {line: number, column: number}} position - Generated position.
56
+ * @returns {{generatedLocation: import("./semantic/types.js").SourceLocation, location: import("./semantic/types.js").SourceLocation | undefined, mappingKind: import("./semantic/types.js").MappingKind, name: string | undefined, nodeId: string | undefined, role: string | undefined, symbolId: string | undefined}} Lookup result.
57
+ */
58
+ export function originalPositionFor(mapping, position) {
59
+ const index = mappingIndex(mapping)
60
+ const offset = positionOffset(index.generatedCoordinates, position)
61
+ const span = spanForGenerated(mapping.spans, offset)
62
+
63
+ if (!span) throw new RangeError(`No generated mapping at offset ${offset}.`)
64
+
65
+ return mappingResult(span)
66
+ }
67
+
68
+ /**
69
+ * Looks up every generated span containing one original UTF-16 position.
70
+ * @param {import("./semantic/types.js").SemantifoldMapping} mapping - Rich mapping.
71
+ * @param {({filename: string} | {sourceId: string}) & ({offset: number} | {line: number, column: number})} position - Original position.
72
+ * @returns {{generatedLocation: import("./semantic/types.js").SourceLocation, location: import("./semantic/types.js").SourceLocation | undefined, mappingKind: import("./semantic/types.js").MappingKind, name: string | undefined, nodeId: string | undefined, role: string | undefined, symbolId: string | undefined}[]} Generated results.
73
+ */
74
+ export function generatedPositionFor(mapping, position) {
75
+ const index = mappingIndex(mapping)
76
+ const source = "sourceId" in position
77
+ ? index.sourcesById.get(position.sourceId)
78
+ : mapping.sources.find((candidate) => candidate.filename == position.filename)
79
+
80
+ if (!source) return []
81
+ if (source.content == null && !("offset" in position)) {
82
+ throw new TypeError(`Source content is required for line/column lookup of ${source.filename}.`)
83
+ }
84
+
85
+ let offset
86
+
87
+ if ("offset" in position) {
88
+ if (source.content == null) {
89
+ if (!Number.isInteger(position.offset) || position.offset < 0) throw new RangeError(`Invalid source offset: ${position.offset}`)
90
+ } else indexedPointAt(/** @type {ReturnType<typeof createCoordinateIndex>} */ (index.sourceCoordinates.get(source.id)), position.offset)
91
+ offset = position.offset
92
+ } else {
93
+ offset = indexedOffsetAt(/** @type {ReturnType<typeof createCoordinateIndex>} */ (index.sourceCoordinates.get(source.id)),
94
+ position.line, position.column)
95
+ }
96
+ const entries = index.originalBySource.get(source.id) ?? []
97
+ let low = 0
98
+ let high = entries.length
99
+
100
+ while (low < high) {
101
+ const middle = Math.floor((low + high) / 2)
102
+
103
+ if (entries[middle].start <= offset) low = middle + 1
104
+ else high = middle
105
+ }
106
+
107
+ /** @type {Set<import("./semantic/types.js").SemantifoldMappingSpan>} */
108
+ const matches = new Set()
109
+
110
+ for (const span of index.originalPointsBySource.get(source.id)?.get(offset) ?? []) matches.add(span)
111
+
112
+ for (let candidate = low - 1; candidate >= 0 && entries[candidate].prefixEnd > offset; candidate--) {
113
+ if (entries[candidate].end > offset) matches.add(entries[candidate].span)
114
+ }
115
+
116
+ return [...matches].sort((left, right) => left.generated.start.offset - right.generated.start.offset).map(mappingResult)
117
+ }
118
+
119
+ /**
120
+ * Returns every generated span associated with one canonical semantic node.
121
+ * @param {import("./semantic/types.js").SemantifoldMapping} mapping - Rich mapping.
122
+ * @param {string} nodeId - Canonical node identity.
123
+ * @returns {import("./semantic/types.js").SemantifoldMappingSpan[]} Matching spans.
124
+ */
125
+ export function spansForNode(mapping, nodeId) {
126
+ const index = mappingIndex(mapping)
127
+ if (!index.nodeSpans.has(nodeId)) throw new RangeError(`Unknown semantic node: ${nodeId}`)
128
+
129
+ return [...(index.nodeSpans.get(nodeId) ?? [])]
130
+ }
131
+
132
+ /**
133
+ * Returns every generated span associated with one canonical semantic symbol.
134
+ * @param {import("./semantic/types.js").SemantifoldMapping} mapping - Rich mapping.
135
+ * @param {string} symbolId - Canonical symbol identity.
136
+ * @returns {import("./semantic/types.js").SemantifoldMappingSpan[]} Matching spans.
137
+ */
138
+ export function spansForSymbol(mapping, symbolId) {
139
+ const index = mappingIndex(mapping)
140
+ if (!index.symbolSpans.has(symbolId)) throw new RangeError(`Unknown semantic symbol: ${symbolId}`)
141
+
142
+ return [...(index.symbolSpans.get(symbolId) ?? [])]
143
+ }
144
+
145
+ /**
146
+ * Serializes a validated rich mapping deterministically.
147
+ * @param {import("./semantic/types.js").SemantifoldMapping} mapping - Rich mapping.
148
+ * @returns {string} Canonical pretty JSON with one trailing LF.
149
+ */
150
+ export function stringifyMapping(mapping) {
151
+ mappingIndex(mapping)
152
+
153
+ return `${JSON.stringify(sortJson(mapping), null, 2)}\n`
154
+ }
155
+
156
+ /**
157
+ * Parses and validates a rich mapping.
158
+ * @param {string} serialized - Mapping JSON.
159
+ * @returns {import("./semantic/types.js").SemantifoldMapping} Parsed mapping.
160
+ */
161
+ export function parseMapping(serialized) {
162
+ const mapping = /** @type {unknown} */ (JSON.parse(serialized))
163
+
164
+ return finalizeMapping(mapping)
165
+ }
166
+
167
+ /**
168
+ * Composes an outer rich map through an inner rich map.
169
+ * @param {import("./semantic/types.js").SemantifoldMapping} outer - Final output to intermediate map.
170
+ * @param {import("./semantic/types.js").SemantifoldMapping} inner - Intermediate to original map.
171
+ * @returns {import("./semantic/types.js").SemantifoldMapping} Composed rich map.
172
+ */
173
+ export function composeMappings(outer, inner) {
174
+ const outerIndex = mappingIndex(outer)
175
+
176
+ mappingIndex(inner)
177
+
178
+ /** @type {import("./semantic/types.js").RegisteredSource[]} */
179
+ const sources = inner.sources.map((source) => ({...source}))
180
+ const sourceIds = new Set(sources.map((source) => source.id))
181
+ /** @type {Map<string, string>} */
182
+ const outerSourceIds = new Map()
183
+ let nextSourceIndex = 0
184
+
185
+ for (const source of outer.sources) {
186
+ if (sourceRepresentsGenerated(source, inner.generated)) continue
187
+ retainOuterSource(source.id)
188
+ }
189
+
190
+ const spans = outer.spans.flatMap((span) => {
191
+ /** @type {ReturnType<typeof traceRelatedOrigin> | undefined} */
192
+ let trace
193
+
194
+ for (const related of relatedOriginsForOrigin(span.origin)) {
195
+ trace = traceRelatedOrigin(related)
196
+ if (trace?.traced) break
197
+ }
198
+ const location = trace?.related.location
199
+ const overlapping = trace?.overlapping ?? []
200
+ const traced = trace?.traced
201
+
202
+ if (!traced) {
203
+ for (const related of relatedOriginsForOrigin(span.origin)) {
204
+ const relatedTrace = traceRelatedOrigin(related)
205
+
206
+ if (relatedTrace && isGeneratedEofPoint(related)) retainOuterSource(related.sourceId)
207
+ }
208
+ const unidentified = {...span}
209
+
210
+ delete unidentified.nodeId
211
+ delete unidentified.symbolId
212
+
213
+ return [{...unidentified, origin: remapOriginSourceIds(span.origin, outerSourceIds)}]
214
+ }
215
+ const selectedRelated = trace?.related
216
+
217
+ if (!selectedRelated) throw new RangeError("Intermediate trace selection is missing its related origin.")
218
+
219
+ if (location && overlapping.length > 1 && span.mappingKind == "exact" &&
220
+ span.generated.end.offset - span.generated.start.offset == location.end.offset - location.start.offset) {
221
+ return overlapping.map((candidate) => {
222
+ const overlapStart = Math.max(location.start.offset, candidate.generated.start.offset)
223
+ const overlapEnd = Math.min(location.end.offset, candidate.generated.end.offset)
224
+ const generatedStart = span.generated.start.offset + overlapStart - location.start.offset
225
+ const generatedEnd = span.generated.start.offset + overlapEnd - location.start.offset
226
+ const preservesWholeOrigin = overlapStart == candidate.generated.start.offset && overlapEnd == candidate.generated.end.offset
227
+
228
+ return composedSpan(span, candidate, locationFromOffsets(
229
+ outer.generated.filename,
230
+ outer.generated.content,
231
+ generatedStart,
232
+ generatedEnd
233
+ ), preservesWholeOrigin, selectedRelated, true)
234
+ })
235
+ }
236
+
237
+ const preservesWholeOrigin = Boolean(location && location.start.offset == traced.generated.start.offset &&
238
+ location.end.offset == traced.generated.end.offset)
239
+ const composed = composedSpan(span, traced, span.generated, preservesWholeOrigin, selectedRelated)
240
+
241
+ if (span.origin.kind == "source" && overlapping.length > 1) {
242
+ const origins = composeRelatedOrigins([selectedRelated], selectedRelated, traced, false)
243
+
244
+ if (origins.length > 0) {
245
+ return [{...composed, mappingKind: /** @type {const} */ ("anchor"), origin: {
246
+ kind: /** @type {const} */ ("derived"),
247
+ origins
248
+ }}]
249
+ }
250
+ }
251
+
252
+ return [composed]
253
+ })
254
+
255
+ return finalizeMapping({
256
+ coordinateSystem: "utf16",
257
+ generated: outer.generated,
258
+ nodes: inner.nodes,
259
+ schema: "SemantifoldMapping",
260
+ sources,
261
+ spans,
262
+ symbols: inner.symbols,
263
+ version: 1
264
+ })
265
+
266
+ /**
267
+ * Composes one outer generated range with an inner range.
268
+ * @param {import("./semantic/types.js").SemantifoldMappingSpan} outerSpan - Outer span.
269
+ * @param {import("./semantic/types.js").SemantifoldMappingSpan} innerSpan - Inner span.
270
+ * @param {import("./semantic/types.js").SourceLocation} generated - Final generated subrange.
271
+ * @param {boolean} preservesWholeOrigin - Whether adopting the inner origin retains its complete range.
272
+ * @param {import("./semantic/types.js").RelatedOrigin} selectedRelated - Outer origin traced through this inner span.
273
+ * @param {boolean} [restrictSelectedTrace] - Whether an exact split selects only this inner span.
274
+ * @returns {import("./semantic/types.js").SemantifoldMappingSpan} Composed span.
275
+ */
276
+ function composedSpan(outerSpan, innerSpan, generated, preservesWholeOrigin, selectedRelated, restrictSelectedTrace = false) {
277
+ if (outerSpan.mappingKind == "synthetic" && outerSpan.origin.kind == "synthetic") {
278
+ return definedProperties({
279
+ generated,
280
+ mappingKind: /** @type {const} */ ("synthetic"),
281
+ name: outerSpan.name,
282
+ origin: {
283
+ kind: /** @type {const} */ ("synthetic"),
284
+ reason: outerSpan.origin.reason,
285
+ relatedOrigins: composeRelatedOrigins(outerSpan.origin.relatedOrigins, selectedRelated, innerSpan, restrictSelectedTrace)
286
+ },
287
+ role: outerSpan.role
288
+ })
289
+ }
290
+
291
+ const composedDerivedOrigins = outerSpan.origin.kind == "derived"
292
+ ? composeRelatedOrigins(outerSpan.origin.origins, selectedRelated, innerSpan, restrictSelectedTrace)
293
+ : undefined
294
+ const origin = outerSpan.origin.kind == "source" ? innerSpan.origin : outerSpan.origin.kind == "derived"
295
+ ? composedDerivedOrigins?.length == 0 && innerSpan.origin.kind == "synthetic"
296
+ ? innerSpan.origin
297
+ : {kind: /** @type {const} */ ("derived"), origins: /** @type {import("./semantic/types.js").RelatedOrigin[]} */ (composedDerivedOrigins)}
298
+ : {
299
+ kind: /** @type {const} */ ("synthetic"),
300
+ reason: outerSpan.origin.reason,
301
+ relatedOrigins: composeRelatedOrigins(outerSpan.origin.relatedOrigins, selectedRelated, innerSpan, restrictSelectedTrace)
302
+ }
303
+
304
+ return definedProperties({
305
+ ...outerSpan,
306
+ generated,
307
+ mappingKind: innerSpan.mappingKind == "synthetic" ? /** @type {const} */ ("synthetic") :
308
+ outerSpan.mappingKind == "exact" && innerSpan.mappingKind == "exact" && preservesWholeOrigin
309
+ ? /** @type {const} */ ("exact")
310
+ : /** @type {const} */ ("anchor"),
311
+ name: outerSpan.name ?? innerSpan.name,
312
+ nodeId: innerSpan.nodeId,
313
+ origin,
314
+ role: innerSpan.role ?? outerSpan.role,
315
+ symbolId: innerSpan.symbolId
316
+ })
317
+ }
318
+
319
+ /**
320
+ * Finds the inner span selected by one related intermediate origin.
321
+ * @param {import("./semantic/types.js").RelatedOrigin} related - Outer related origin.
322
+ * @returns {{
323
+ * overlapping: import("./semantic/types.js").SemantifoldMappingSpan[],
324
+ * related: import("./semantic/types.js").RelatedOrigin,
325
+ * traced: import("./semantic/types.js").SemantifoldMappingSpan | undefined
326
+ * } | undefined} Trace selection when the related source is the intermediate artifact.
327
+ */
328
+ function traceRelatedOrigin(related) {
329
+ const source = outerIndex.sourcesById.get(related.sourceId)
330
+
331
+ if (!source || !sourceRepresentsGenerated(source, inner.generated)) return undefined
332
+ const overlapping = spansOverlappingGenerated(inner.spans, related.location.start.offset, related.location.end.offset)
333
+
334
+ return {
335
+ overlapping,
336
+ related,
337
+ traced: overlapping[0] ?? spanForGenerated(inner.spans, related.location.start.offset)
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Traces intermediate related origins and remaps unrelated source identities in order.
343
+ * @param {import("./semantic/types.js").RelatedOrigin[]} relatedOrigins - Outer related origins.
344
+ * @param {import("./semantic/types.js").RelatedOrigin} selectedRelated - Origin using the selected inner span.
345
+ * @param {import("./semantic/types.js").SemantifoldMappingSpan} selectedSpan - Selected inner span.
346
+ * @param {boolean} restrictSelectedTrace - Whether an exact split selects only the supplied span.
347
+ * @returns {import("./semantic/types.js").RelatedOrigin[]} Composed related origins.
348
+ */
349
+ function composeRelatedOrigins(relatedOrigins, selectedRelated, selectedSpan, restrictSelectedTrace) {
350
+ return relatedOrigins.flatMap((related) => {
351
+ const source = outerIndex.sourcesById.get(related.sourceId)
352
+
353
+ if (!source || !sourceRepresentsGenerated(source, inner.generated)) {
354
+ return [remapRelatedOriginSourceId(related, outerSourceIds)]
355
+ }
356
+ const trace = traceRelatedOrigin(related)
357
+ const tracedSpans = restrictSelectedTrace && related === selectedRelated
358
+ ? [selectedSpan]
359
+ : trace?.overlapping.length
360
+ ? trace.overlapping
361
+ : trace?.traced ? [trace.traced] : []
362
+
363
+ if (tracedSpans.length == 0) {
364
+ if (!isGeneratedEofPoint(related)) throw new RangeError(`Unable to trace intermediate origin '${related.location.filename}'.`)
365
+ retainOuterSource(related.sourceId)
366
+
367
+ return [remapRelatedOriginSourceId(related, outerSourceIds)]
368
+ }
369
+
370
+ return tracedSpans.flatMap((tracedSpan) => relatedOriginsForSpan(tracedSpan).map((tracedRelated) => definedProperties({
371
+ ...tracedRelated,
372
+ nodeId: tracedRelated.nodeId ?? tracedSpan.nodeId,
373
+ role: related.role ?? tracedRelated.role,
374
+ symbolId: tracedRelated.symbolId ?? tracedSpan.symbolId
375
+ })))
376
+ })
377
+ }
378
+
379
+ /**
380
+ * Retains one outer source that survives composition and assigns its composed identity.
381
+ * @param {string} sourceId - Outer source registry identity.
382
+ * @returns {void}
383
+ */
384
+ function retainOuterSource(sourceId) {
385
+ if (outerSourceIds.has(sourceId)) return
386
+ const source = outerIndex.sourcesById.get(sourceId)
387
+
388
+ if (!source) throw new RangeError(`Unknown outer mapping source '${sourceId}'.`)
389
+ let composedSource = sources.find((candidate) => sameRegisteredSource(candidate, source))
390
+
391
+ if (!composedSource) {
392
+ while (sourceIds.has(`source:${nextSourceIndex}`)) nextSourceIndex++
393
+ composedSource = {...source, id: `source:${nextSourceIndex}`}
394
+ sourceIds.add(composedSource.id)
395
+ sources.push(composedSource)
396
+ }
397
+ outerSourceIds.set(source.id, composedSource.id)
398
+ }
399
+
400
+ /**
401
+ * Tests whether one intermediate origin is the exact point after all generated code units.
402
+ * @param {import("./semantic/types.js").RelatedOrigin} related - Candidate intermediate origin.
403
+ * @returns {boolean} Whether this is an EOF point rather than a generated half-open range.
404
+ */
405
+ function isGeneratedEofPoint(related) {
406
+ return related.location.start.offset == inner.generated.content.length &&
407
+ related.location.end.offset == inner.generated.content.length
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Converts one mapped span's closed origin into related provenance.
413
+ * @param {import("./semantic/types.js").SemantifoldMappingSpan} span - Traced inner span.
414
+ * @returns {import("./semantic/types.js").RelatedOrigin[]} Related original ranges.
415
+ */
416
+ function relatedOriginsForSpan(span) {
417
+ return relatedOriginsForOrigin(span.origin)
418
+ }
419
+
420
+ /**
421
+ * Returns every related range in one closed origin.
422
+ * @param {import("./semantic/types.js").SemanticOrigin} origin - Closed origin.
423
+ * @returns {import("./semantic/types.js").RelatedOrigin[]} Related ranges in semantic order.
424
+ */
425
+ function relatedOriginsForOrigin(origin) {
426
+ if (origin.kind == "source") return [{location: origin.location, sourceId: origin.sourceId}]
427
+ if (origin.kind == "derived") return origin.origins
428
+
429
+ return origin.relatedOrigins
430
+ }
431
+
432
+ /**
433
+ * Determines whether a registered source is the generated artifact consumed by an inner map.
434
+ * @param {import("./semantic/types.js").RegisteredSource} source - Outer source registry entry.
435
+ * @param {import("./semantic/types.js").GeneratedSource} generated - Inner generated artifact.
436
+ * @returns {boolean} Whether identity-selected provenance may be traced through the inner map.
437
+ */
438
+ function sourceRepresentsGenerated(source, generated) {
439
+ return source.filename == generated.filename && source.content == generated.content &&
440
+ (source.language == null || source.language == generated.language)
441
+ }
442
+
443
+ /**
444
+ * Compares complete registered-source identity fields other than registry-local ID.
445
+ * @param {import("./semantic/types.js").RegisteredSource} left - First registered source.
446
+ * @param {import("./semantic/types.js").RegisteredSource} right - Second registered source.
447
+ * @returns {boolean} Whether both entries represent the same source artifact.
448
+ */
449
+ function sameRegisteredSource(left, right) {
450
+ return left.filename == right.filename && left.content == right.content && left.language == right.language
451
+ }
452
+
453
+ /**
454
+ * Composes Source Map v3 maps with maintained tracing machinery.
455
+ * @param {import("@jridgewell/trace-mapping").SourceMapInput} outer - Final output to intermediate map.
456
+ * @param {import("@jridgewell/trace-mapping").SourceMapInput} inner - Intermediate to original map.
457
+ * @returns {import("@jridgewell/gen-mapping").EncodedSourceMap} Composed v3 map.
458
+ */
459
+ export function composeSourceMaps(outer, inner) {
460
+ const outerMap = new TraceMap(outer)
461
+ const innerMap = new TraceMap(inner)
462
+ const composed = new GenMapping({file: outerMap.file})
463
+ const intermediateIndexes = typeof innerMap.file == "string"
464
+ ? outerMap.resolvedSources.flatMap((source, index) => source == innerMap.file ? [index] : [])
465
+ : []
466
+ const intermediateIndex = intermediateIndexes.length == 1 ? intermediateIndexes[0] : undefined
467
+ const intermediateSource = intermediateIndex === undefined ? undefined : outerMap.resolvedSources[intermediateIndex]
468
+
469
+ innerMap.sources.forEach((rawSource, index) => {
470
+ const source = innerMap.resolvedSources[index] ?? rawSource
471
+
472
+ if (source != null) setSourceContent(composed, source, innerMap.sourcesContent?.[index] ?? null)
473
+ })
474
+ outerMap.sources.forEach((rawSource, index) => {
475
+ const source = outerMap.resolvedSources[index] ?? rawSource
476
+
477
+ if (source != null && index != intermediateIndex) {
478
+ setSourceContent(composed, source, outerMap.sourcesContent?.[index] ?? null)
479
+ }
480
+ })
481
+
482
+ eachMapping(outerMap, (mapping) => {
483
+ if (mapping.source == null || mapping.originalLine == null || mapping.originalColumn == null) {
484
+ maybeAddSegment(composed, mapping.generatedLine - 1, mapping.generatedColumn)
485
+ return
486
+ }
487
+
488
+ const isIntermediate = intermediateSource !== undefined && mapping.source == intermediateSource
489
+ const traced = isIntermediate
490
+ ? traceOriginalPositionFor(innerMap, {column: mapping.originalColumn, line: mapping.originalLine})
491
+ : {column: mapping.originalColumn, line: mapping.originalLine, name: mapping.name, source: mapping.source}
492
+
493
+ if (traced.source == null || traced.line == null || traced.column == null) {
494
+ maybeAddSegment(composed, mapping.generatedLine - 1, mapping.generatedColumn)
495
+ return
496
+ }
497
+
498
+ const content = isIntermediate ? sourceContentFor(innerMap, traced.source) : sourceContentFor(outerMap, traced.source)
499
+ const name = traced.name ?? mapping.name
500
+
501
+ if (name == null) {
502
+ addSegment(composed, mapping.generatedLine - 1, mapping.generatedColumn, traced.source, traced.line - 1, traced.column, null, content)
503
+ } else {
504
+ addSegment(composed, mapping.generatedLine - 1, mapping.generatedColumn, traced.source, traced.line - 1, traced.column, name, content)
505
+ }
506
+ })
507
+
508
+ return toEncodedMap(composed)
509
+ }
510
+
511
+ /**
512
+ * Imports a point-based Source Map v3 as a range-based Semantifold map.
513
+ * @param {import("@jridgewell/trace-mapping").SourceMapInput} sourceMap - Source Map v3.
514
+ * @param {object} generated - Generated program.
515
+ * @param {string} generated.content - Exact generated content.
516
+ * @param {string} generated.filename - Generated filename.
517
+ * @param {import("./semantic/types.js").SemanticLanguage} generated.language - Generated language.
518
+ * @param {{filename: string, content: string, language?: import("./semantic/types.js").SemanticLanguage}[]} [generated.sources] - Exact source content overrides.
519
+ * @returns {import("./semantic/types.js").SemantifoldMapping} Imported rich map.
520
+ */
521
+ export function mappingFromSourceMap(sourceMap, {content, filename, language, sources: provided = []}) {
522
+ const trace = new TraceMap(sourceMap)
523
+ const generatedCoordinates = createCoordinateIndex(content)
524
+ /** @type {import("./semantic/types.js").RegisteredSource[]} */
525
+ const sources = []
526
+ /** @type {Map<number, {filename: string, content: string, language?: import("./semantic/types.js").SemanticLanguage}>} */
527
+ const overridesBySourceIndex = new Map()
528
+
529
+ for (const override of provided) {
530
+ const exact = trace.sources.flatMap((source, index) => {
531
+ const resolved = trace.resolvedSources[index]
532
+
533
+ return source == override.filename || resolved == override.filename ? [index] : []
534
+ })
535
+
536
+ if (exact.length > 0) {
537
+ for (const index of exact) overridesBySourceIndex.set(index, override)
538
+ continue
539
+ }
540
+ const suffix = trace.sources.flatMap((source, index) => {
541
+ const resolved = trace.resolvedSources[index]
542
+
543
+ return source?.endsWith(`/${override.filename}`) || resolved?.endsWith(`/${override.filename}`) ? [index] : []
544
+ })
545
+
546
+ if (suffix.length > 1) {
547
+ throw new RangeError(`Ambiguous Source Map source content override '${override.filename}' matches multiple sources.`)
548
+ }
549
+ if (suffix.length == 1 && !overridesBySourceIndex.has(suffix[0])) overridesBySourceIndex.set(suffix[0], override)
550
+ }
551
+ /** @type {Map<string, import("./semantic/types.js").RegisteredSource>} */
552
+ const sourcesByMapName = new Map()
553
+
554
+ trace.sources.forEach((source, index) => {
555
+ if (source == null) return
556
+
557
+ const resolvedSource = trace.resolvedSources[index] ?? source
558
+ const providedSource = overridesBySourceIndex.get(index)
559
+ const registered = {
560
+ content: providedSource?.content ?? trace.sourcesContent?.[index] ?? null,
561
+ filename: providedSource?.filename ?? resolvedSource,
562
+ id: `source:${sources.length}`,
563
+ language: providedSource?.language ?? null
564
+ }
565
+
566
+ sources.push(registered)
567
+ if (!sourcesByMapName.has(resolvedSource)) sourcesByMapName.set(resolvedSource, registered)
568
+ })
569
+ const sourceCoordinates = new Map(sources.filter((source) => source.content != null)
570
+ .map((source) => [source.id, createCoordinateIndex(/** @type {string} */ (source.content))]))
571
+
572
+ /** @type {{generatedOffset: number, mapping: import("@jridgewell/trace-mapping").EachMapping}[]} */
573
+ const points = []
574
+
575
+ eachMapping(trace, (mapping) => {
576
+ points.push({generatedOffset: indexedOffsetAt(generatedCoordinates, mapping.generatedLine, mapping.generatedColumn + 1), mapping})
577
+ })
578
+
579
+ /** @type {import("./semantic/types.js").SemantifoldMappingSpan[]} */
580
+ const mappedSpans = []
581
+
582
+ points.forEach(({generatedOffset, mapping}, index) => {
583
+ const next = points[index + 1]
584
+ const end = next?.mapping.generatedLine == mapping.generatedLine
585
+ ? next.generatedOffset
586
+ : lineEndOffset(content, generatedOffset)
587
+
588
+ if (end <= generatedOffset) return
589
+
590
+ if (mapping.source == null || mapping.originalLine == null || mapping.originalColumn == null) {
591
+ mappedSpans.push({
592
+ generated: locationFromOffsets(filename, content, generatedOffset, end),
593
+ mappingKind: "synthetic",
594
+ origin: {kind: "synthetic", reason: "unmapped Source Map segment", relatedOrigins: []}
595
+ })
596
+ return
597
+ }
598
+
599
+ const source = sourcesByMapName.get(mapping.source)
600
+
601
+ if (!source) throw new RangeError(`Source Map references unregistered source '${mapping.source}'.`)
602
+ if (source.content == null) {
603
+ throw new RangeError(`Source content is required to import mapped positions for '${source.filename}'.`)
604
+ }
605
+
606
+ const originalOffset = indexedOffsetAt(
607
+ /** @type {ReturnType<typeof createCoordinateIndex>} */ (sourceCoordinates.get(source.id)),
608
+ mapping.originalLine,
609
+ mapping.originalColumn + 1
610
+ )
611
+ const originalEnd = Math.min(source.content.length, originalOffset + 1)
612
+ const originalLocation = locationFromOffsets(source.filename, source.content, originalOffset, originalEnd)
613
+
614
+ mappedSpans.push(definedProperties({
615
+ generated: locationFromOffsets(filename, content, generatedOffset, end),
616
+ mappingKind: "anchor",
617
+ name: mapping.name ?? undefined,
618
+ origin: {kind: "source", location: originalLocation, sourceId: source.id}
619
+ }))
620
+ })
621
+
622
+ /** @type {import("./semantic/types.js").SemantifoldMappingSpan[]} */
623
+ const spans = []
624
+ let cursor = 0
625
+
626
+ for (const span of mappedSpans) {
627
+ if (span.generated.start.offset > cursor) spans.push(unmappedSpan(cursor, span.generated.start.offset))
628
+ if (span.generated.end.offset > cursor) {
629
+ spans.push(span.generated.start.offset < cursor ? {
630
+ ...span,
631
+ generated: locationFromOffsets(filename, content, cursor, span.generated.end.offset)
632
+ } : span)
633
+ cursor = span.generated.end.offset
634
+ }
635
+ }
636
+ if (cursor < content.length) spans.push(unmappedSpan(cursor, content.length))
637
+
638
+ return finalizeMapping({
639
+ coordinateSystem: "utf16",
640
+ generated: {content, filename, language},
641
+ nodes: [],
642
+ schema: "SemantifoldMapping",
643
+ sources,
644
+ spans,
645
+ symbols: [],
646
+ version: 1
647
+ })
648
+
649
+ /**
650
+ * Creates one explicit unmapped range.
651
+ * @param {number} start - Generated start offset.
652
+ * @param {number} end - Generated end offset.
653
+ * @returns {import("./semantic/types.js").SemantifoldMappingSpan} Synthetic span.
654
+ */
655
+ function unmappedSpan(start, end) {
656
+ return {
657
+ generated: locationFromOffsets(filename, content, start, end),
658
+ mappingKind: "synthetic",
659
+ origin: {kind: "synthetic", reason: "unmapped Source Map range", relatedOrigins: []}
660
+ }
661
+ }
662
+ }
663
+
664
+ /**
665
+ * Remaps a generated location while retaining the generated range in the result.
666
+ * @param {import("./semantic/types.js").SourceLocation} location - Generated location.
667
+ * @param {import("./semantic/types.js").SemantifoldMapping} mapping - Rich mapping.
668
+ * @returns {ReturnType<typeof originalPositionFor>} Remapped lookup.
669
+ */
670
+ export function remapLocation(location, mapping) {
671
+ if (location.filename != mapping.generated.filename) throw new RangeError(`Location is not in ${mapping.generated.filename}.`)
672
+
673
+ return originalPositionFor(mapping, {offset: location.start.offset})
674
+ }
675
+
676
+ /**
677
+ * Remaps a Semantifold diagnostic and preserves its generated location and cause.
678
+ * @param {SemantifoldDiagnostic} diagnostic - Generated-program diagnostic.
679
+ * @param {import("./semantic/types.js").SemantifoldMapping} mapping - Rich mapping.
680
+ * @returns {SemantifoldDiagnostic} Remapped diagnostic.
681
+ */
682
+ export function remapDiagnostic(diagnostic, mapping) {
683
+ if (!diagnostic.location) return diagnostic
684
+
685
+ const remapped = remapLocation(diagnostic.location, mapping)
686
+
687
+ if (!remapped.location) return diagnostic
688
+
689
+ return new SemantifoldDiagnostic({
690
+ cause: diagnostic,
691
+ code: diagnostic.code,
692
+ generatedLocation: diagnostic.location,
693
+ language: diagnostic.language,
694
+ location: remapped.location,
695
+ message: diagnostic.detail
696
+ })
697
+ }
698
+
699
+ /**
700
+ * Validates the public mapping envelope and ordered half-open generated ranges.
701
+ * @param {unknown} value - Candidate mapping.
702
+ * @returns {asserts value is import("./semantic/types.js").SemantifoldMapping} Nothing when valid.
703
+ */
704
+ export function validateMapping(value) {
705
+ if (!value || typeof value != "object" || Reflect.get(value, "schema") != "SemantifoldMapping" ||
706
+ Reflect.get(value, "version") != 1 || Reflect.get(value, "coordinateSystem") != "utf16") {
707
+ throw new TypeError("Expected a SemantifoldMapping v1 value.")
708
+ }
709
+
710
+ const mapping = /** @type {Record<string, unknown>} */ (value)
711
+ const generated = /** @type {Record<string, unknown>} */ (mapping.generated)
712
+ const spans = mapping.spans
713
+
714
+ if (!generated || typeof generated != "object" || typeof Reflect.get(generated, "filename") != "string" ||
715
+ typeof Reflect.get(generated, "content") != "string" || !isLanguage(Reflect.get(generated, "language")) ||
716
+ !Array.isArray(mapping.sources) ||
717
+ !Array.isArray(mapping.nodes) || !Array.isArray(mapping.symbols) || !Array.isArray(spans)) {
718
+ throw new TypeError("Malformed SemantifoldMapping v1 envelope.")
719
+ }
720
+
721
+ const content = /** @type {string} */ (generated.content)
722
+ const filename = /** @type {string} */ (generated.filename)
723
+
724
+ if (content.includes("\r")) throw new TypeError("SemantifoldMapping generated content must use LF line endings.")
725
+
726
+ const sourceIds = new Set()
727
+
728
+ for (const source of mapping.sources) {
729
+ if (!source || typeof source != "object" || typeof source.id != "string" || source.id.length == 0 || sourceIds.has(source.id) ||
730
+ typeof source.filename != "string" || source.filename.length == 0 || source.content !== null && typeof source.content != "string" ||
731
+ source.language !== null && !isLanguage(source.language)) {
732
+ throw new TypeError("Malformed or duplicate SemantifoldMapping source.")
733
+ }
734
+ sourceIds.add(source.id)
735
+ }
736
+ const sourceCoordinates = new Map(mapping.sources.filter((source) => source.content != null)
737
+ .map((source) => [source.id, createCoordinateIndex(/** @type {string} */ (source.content))]))
738
+ const sourcesById = new Map(mapping.sources.map((source) => [source.id, source]))
739
+ /** @type {Map<string, import("./semantic/types.js").RegisteredSource[]>} */
740
+ const sourcesByFilename = new Map()
741
+
742
+ for (const source of mapping.sources) {
743
+ const sameName = sourcesByFilename.get(source.filename) ?? []
744
+
745
+ sameName.push(source)
746
+ sourcesByFilename.set(source.filename, sameName)
747
+ }
748
+ const generatedCoordinates = createCoordinateIndex(content)
749
+
750
+ const nodeIds = new Set()
751
+ const nodePaths = new Set()
752
+
753
+ for (const node of mapping.nodes) {
754
+ if (!node || typeof node != "object" || typeof node.id != "string" || node.id.length == 0 || nodeIds.has(node.id) ||
755
+ typeof node.path != "string" || nodePaths.has(node.path) ||
756
+ typeof node.kind != "string" || node.kind.length == 0 || !node.origin || typeof node.ranges != "object" ||
757
+ node.ranges === null || Array.isArray(node.ranges)) throw new TypeError("Malformed or duplicate semantic node identity.")
758
+ nodeIds.add(node.id)
759
+ nodePaths.add(node.path)
760
+ }
761
+
762
+ const symbolIds = new Set()
763
+
764
+ for (const symbol of mapping.symbols) {
765
+ if (!symbol || typeof symbol != "object" || typeof symbol.id != "string" || symbol.id.length == 0 || symbolIds.has(symbol.id) ||
766
+ typeof symbol.name != "string" || symbol.name.length == 0 || !["function", "parameter", "local"].includes(symbol.kind) ||
767
+ !nodeIds.has(symbol.declarationNodeId) || !Array.isArray(symbol.references)) {
768
+ throw new TypeError("Malformed or duplicate semantic symbol identity.")
769
+ }
770
+ symbolIds.add(symbol.id)
771
+ }
772
+
773
+ for (const node of mapping.nodes) {
774
+ validateOrigin(node.origin, sourcesById, sourceCoordinates, nodeIds, symbolIds)
775
+ for (const [role, location] of Object.entries(node.ranges)) {
776
+ if (role.length == 0) throw new TypeError("Semantic node range roles must be non-empty strings.")
777
+ validateLocationForSources(location, sourcesByFilename, sourceCoordinates)
778
+ }
779
+ if (node.symbolId !== undefined && (typeof node.symbolId != "string" || !symbolIds.has(node.symbolId))) {
780
+ throw new TypeError("Semantic node references an unknown symbol.")
781
+ }
782
+ }
783
+
784
+ for (const symbol of mapping.symbols) {
785
+ validateLocationForSources(symbol.location, sourcesByFilename, sourceCoordinates)
786
+ for (const reference of symbol.references) {
787
+ if (!reference || typeof reference != "object" || !nodeIds.has(reference.nodeId) ||
788
+ !["declaration", "read", "write", "call"].includes(reference.role)) {
789
+ throw new TypeError("Malformed semantic symbol reference.")
790
+ }
791
+ validateLocationForSources(reference.location, sourcesByFilename, sourceCoordinates)
792
+ }
793
+ }
794
+
795
+ let previousEnd = 0
796
+
797
+ for (const span of spans) {
798
+ if (!span || typeof span != "object" || !["exact", "anchor", "synthetic"].includes(Reflect.get(span, "mappingKind")) ||
799
+ !Reflect.get(span, "generated") || !Reflect.get(span, "origin")) {
800
+ throw new TypeError("Malformed SemantifoldMapping span.")
801
+ }
802
+
803
+ const location = /** @type {import("./semantic/types.js").SourceLocation} */ (Reflect.get(span, "generated"))
804
+
805
+ if (!location.start || !location.end || location.filename != filename || location.start.offset != previousEnd ||
806
+ location.end.offset <= location.start.offset || location.end.offset > content.length ||
807
+ !samePoint(indexedPointAt(generatedCoordinates, location.start.offset), location.start) ||
808
+ !samePoint(indexedPointAt(generatedCoordinates, location.end.offset), location.end)) {
809
+ throw new TypeError("SemantifoldMapping spans must exactly cover ordered normalized generated ranges.")
810
+ }
811
+ validateOrigin(
812
+ /** @type {import("./semantic/types.js").SemanticOrigin} */ (Reflect.get(span, "origin")),
813
+ sourcesById,
814
+ sourceCoordinates,
815
+ nodeIds,
816
+ symbolIds
817
+ )
818
+ const nodeId = Reflect.get(span, "nodeId")
819
+ const symbolId = Reflect.get(span, "symbolId")
820
+
821
+ if (nodeId !== undefined && (typeof nodeId != "string" || !nodeIds.has(nodeId))) throw new TypeError("Mapping span references an unknown node.")
822
+ if (symbolId !== undefined && (typeof symbolId != "string" || !symbolIds.has(symbolId))) throw new TypeError("Mapping span references an unknown symbol.")
823
+ if (Reflect.get(span, "role") !== undefined && typeof Reflect.get(span, "role") != "string" ||
824
+ Reflect.get(span, "name") !== undefined && typeof Reflect.get(span, "name") != "string") {
825
+ throw new TypeError("Mapping span roles and names must be strings.")
826
+ }
827
+ previousEnd = location.end.offset
828
+ }
829
+
830
+ if (previousEnd != content.length) throw new TypeError("SemantifoldMapping spans must cover all generated content.")
831
+ }
832
+
833
+ /**
834
+ * Validates, freezes, and indexes one mapping produced at a trust boundary.
835
+ * @param {unknown} value - Candidate mapping.
836
+ * @returns {import("./semantic/types.js").SemantifoldMapping} Immutable indexed mapping.
837
+ */
838
+ export function finalizeMapping(value) {
839
+ // Producers may retain references to mutable semantic locations; mappings own their immutable copy.
840
+ const mapping = structuredClone(value)
841
+
842
+ validateMapping(mapping)
843
+ freezeJson(mapping)
844
+ mappingIndexes.set(mapping, buildMappingIndex(mapping))
845
+
846
+ return mapping
847
+ }
848
+
849
+ /**
850
+ * Gets an index, validating mutable caller mappings on every trust-boundary use.
851
+ * @param {import("./semantic/types.js").SemantifoldMapping} mapping - Mapping.
852
+ * @returns {MappingIndex} Lookup index.
853
+ */
854
+ function mappingIndex(mapping) {
855
+ const cached = mappingIndexes.get(mapping)
856
+
857
+ if (cached) return cached
858
+
859
+ validateMapping(mapping)
860
+
861
+ return buildMappingIndex(mapping)
862
+ }
863
+
864
+ /**
865
+ * Builds generated, original, node, and symbol indexes in one pass.
866
+ * @param {import("./semantic/types.js").SemantifoldMapping} mapping - Validated mapping.
867
+ * @returns {{generatedCoordinates: ReturnType<typeof createCoordinateIndex>, nodeSpans: Map<string, import("./semantic/types.js").SemantifoldMappingSpan[]>, originalBySource: Map<string, {end: number, prefixEnd: number, span: import("./semantic/types.js").SemantifoldMappingSpan, start: number}[]>, originalPointsBySource: Map<string, Map<number, import("./semantic/types.js").SemantifoldMappingSpan[]>>, sourceCoordinates: Map<string, ReturnType<typeof createCoordinateIndex>>, sourcesById: Map<string, import("./semantic/types.js").RegisteredSource>, symbolSpans: Map<string, import("./semantic/types.js").SemantifoldMappingSpan[]>}} Mapping index.
868
+ */
869
+ function buildMappingIndex(mapping) {
870
+ const generatedCoordinates = createCoordinateIndex(mapping.generated.content)
871
+ const sourcesById = new Map(mapping.sources.map((source) => [source.id, source]))
872
+ const sourceCoordinates = new Map(mapping.sources.filter((source) => source.content != null)
873
+ .map((source) => [source.id, createCoordinateIndex(/** @type {string} */ (source.content))]))
874
+ const nodeSpans = new Map(mapping.nodes.map((node) => [node.id, /** @type {import("./semantic/types.js").SemantifoldMappingSpan[]} */ ([])]))
875
+ const symbolSpans = new Map(mapping.symbols.map((symbol) => [symbol.id, /** @type {import("./semantic/types.js").SemantifoldMappingSpan[]} */ ([])]))
876
+ /** @type {Map<string, {end: number, prefixEnd: number, span: import("./semantic/types.js").SemantifoldMappingSpan, start: number}[]>} */
877
+ const originalBySource = new Map(mapping.sources.map((source) => [source.id, []]))
878
+ /** @type {Map<string, Map<number, import("./semantic/types.js").SemantifoldMappingSpan[]>>} */
879
+ const originalPointsBySource = new Map(mapping.sources.map((source) => [source.id, new Map()]))
880
+
881
+ for (const span of mapping.spans) {
882
+ const spanNodeIds = new Set(span.nodeId ? [span.nodeId] : [])
883
+ const spanSymbolIds = new Set(span.symbolId ? [span.symbolId] : [])
884
+
885
+ for (const related of relatedOriginsForOrigin(span.origin)) {
886
+ if (related.nodeId) spanNodeIds.add(related.nodeId)
887
+ if (related.symbolId) spanSymbolIds.add(related.symbolId)
888
+ if (related.location.end.offset == related.location.start.offset) {
889
+ const points = originalPointsBySource.get(related.sourceId)
890
+ const pointSpans = points?.get(related.location.start.offset) ?? []
891
+
892
+ pointSpans.push(span)
893
+ points?.set(related.location.start.offset, pointSpans)
894
+ continue
895
+ }
896
+ originalBySource.get(related.sourceId)?.push({
897
+ end: related.location.end.offset,
898
+ prefixEnd: related.location.end.offset,
899
+ span,
900
+ start: related.location.start.offset
901
+ })
902
+ }
903
+ for (const nodeId of spanNodeIds) nodeSpans.get(nodeId)?.push(span)
904
+ for (const symbolId of spanSymbolIds) symbolSpans.get(symbolId)?.push(span)
905
+ }
906
+
907
+ for (const entries of originalBySource.values()) {
908
+ entries.sort((left, right) => left.start - right.start || left.span.generated.start.offset - right.span.generated.start.offset)
909
+ let prefixEnd = 0
910
+
911
+ for (const entry of entries) {
912
+ prefixEnd = Math.max(prefixEnd, entry.end)
913
+ entry.prefixEnd = prefixEnd
914
+ }
915
+ }
916
+
917
+ return {generatedCoordinates, nodeSpans, originalBySource, originalPointsBySource, sourceCoordinates, sourcesById, symbolSpans}
918
+ }
919
+
920
+ /**
921
+ * Finds the generated span containing an offset by binary search.
922
+ * @param {import("./semantic/types.js").SemantifoldMappingSpan[]} spans - Ordered spans.
923
+ * @param {number} offset - Generated offset.
924
+ * @returns {import("./semantic/types.js").SemantifoldMappingSpan | undefined} Containing span.
925
+ */
926
+ function spanForGenerated(spans, offset) {
927
+ let low = 0
928
+ let high = spans.length
929
+
930
+ while (low < high) {
931
+ const middle = Math.floor((low + high) / 2)
932
+
933
+ if (spans[middle].generated.start.offset <= offset) low = middle + 1
934
+ else high = middle
935
+ }
936
+ const candidate = spans[low - 1]
937
+
938
+ return candidate && offset < candidate.generated.end.offset ? candidate : undefined
939
+ }
940
+
941
+ /**
942
+ * Finds ordered generated spans intersecting a half-open range.
943
+ * @param {import("./semantic/types.js").SemantifoldMappingSpan[]} spans - Ordered spans.
944
+ * @param {number} start - Inclusive generated offset.
945
+ * @param {number} end - Exclusive generated offset.
946
+ * @returns {import("./semantic/types.js").SemantifoldMappingSpan[]} Overlapping spans.
947
+ */
948
+ function spansOverlappingGenerated(spans, start, end) {
949
+ if (end <= start) {
950
+ const containing = spanForGenerated(spans, start)
951
+
952
+ return containing ? [containing] : []
953
+ }
954
+
955
+ let low = 0
956
+ let high = spans.length
957
+
958
+ while (low < high) {
959
+ const middle = Math.floor((low + high) / 2)
960
+
961
+ if (spans[middle].generated.end.offset <= start) low = middle + 1
962
+ else high = middle
963
+ }
964
+ const overlapping = []
965
+
966
+ for (let index = low; index < spans.length && spans[index].generated.start.offset < end; index++) {
967
+ overlapping.push(spans[index])
968
+ }
969
+
970
+ return overlapping
971
+ }
972
+
973
+ /**
974
+ * Deep-freezes a JSON tree.
975
+ * @param {unknown} value - JSON value.
976
+ * @param {WeakSet<object>} [visited] - Cycle protection for rejected caller extras.
977
+ * @returns {void}
978
+ */
979
+ function freezeJson(value, visited = new WeakSet()) {
980
+ if (!value || typeof value != "object" || visited.has(value)) return
981
+
982
+ visited.add(value)
983
+ for (const child of Object.values(value)) freezeJson(child, visited)
984
+ if (!Object.isFrozen(value)) Object.freeze(value)
985
+ }
986
+
987
+ /**
988
+ * Validates one closed mapping origin.
989
+ * @param {import("./semantic/types.js").SemanticOrigin} origin - Origin.
990
+ * @param {Map<string, import("./semantic/types.js").RegisteredSource>} sourcesById - Sources by identity.
991
+ * @param {Map<string, ReturnType<typeof createCoordinateIndex>>} coordinates - Source coordinate indexes.
992
+ * @param {Set<unknown>} [nodeIds] - Known semantic nodes.
993
+ * @param {Set<unknown>} [symbolIds] - Known semantic symbols.
994
+ * @returns {void}
995
+ */
996
+ function validateOrigin(origin, sourcesById, coordinates, nodeIds, symbolIds) {
997
+ if (!origin || typeof origin != "object") throw new TypeError("Mapping origin must be a closed provenance value.")
998
+
999
+ if (origin.kind == "source") {
1000
+ validateRelated({location: origin.location, sourceId: origin.sourceId}, sourcesById, coordinates, nodeIds, symbolIds)
1001
+ return
1002
+ }
1003
+ if (origin.kind == "derived") {
1004
+ if (!Array.isArray(origin.origins) || origin.origins.length == 0) throw new TypeError("Derived provenance requires origins.")
1005
+ origin.origins.forEach((related) => validateRelated(related, sourcesById, coordinates, nodeIds, symbolIds))
1006
+ return
1007
+ }
1008
+ if (origin.kind == "synthetic") {
1009
+ if (typeof origin.reason != "string" || !Array.isArray(origin.relatedOrigins)) throw new TypeError("Malformed synthetic provenance.")
1010
+ origin.relatedOrigins.forEach((related) => validateRelated(related, sourcesById, coordinates, nodeIds, symbolIds))
1011
+ return
1012
+ }
1013
+
1014
+ throw new TypeError("Unknown provenance kind.")
1015
+ }
1016
+
1017
+ /**
1018
+ * Validates one source-related origin.
1019
+ * @param {import("./semantic/types.js").RelatedOrigin} related - Related origin.
1020
+ * @param {Map<string, import("./semantic/types.js").RegisteredSource>} sourcesById - Sources by identity.
1021
+ * @param {Map<string, ReturnType<typeof createCoordinateIndex>>} coordinates - Source coordinate indexes.
1022
+ * @param {Set<unknown> | undefined} nodeIds - Known semantic nodes.
1023
+ * @param {Set<unknown> | undefined} symbolIds - Known semantic symbols.
1024
+ * @returns {void}
1025
+ */
1026
+ function validateRelated(related, sourcesById, coordinates, nodeIds, symbolIds) {
1027
+ const source = sourcesById.get(related.sourceId)
1028
+
1029
+ if (!source || !related.location || related.location.filename != source.filename ||
1030
+ related.nodeId !== undefined && typeof related.nodeId != "string" ||
1031
+ related.symbolId !== undefined && typeof related.symbolId != "string" ||
1032
+ related.role !== undefined && typeof related.role != "string") throw new TypeError("Origin references an unknown source.")
1033
+ if (nodeIds && related.nodeId !== undefined && !nodeIds.has(related.nodeId)) throw new TypeError("Origin references an unknown node.")
1034
+ if (symbolIds && related.symbolId !== undefined && !symbolIds.has(related.symbolId)) {
1035
+ throw new TypeError("Origin references an unknown symbol.")
1036
+ }
1037
+ validateLocationForSource(related.location, source, coordinates.get(source.id))
1038
+ }
1039
+
1040
+ /**
1041
+ * Validates a location against at least one same-filename source.
1042
+ * @param {unknown} value - Candidate location.
1043
+ * @param {Map<string, import("./semantic/types.js").RegisteredSource[]>} sourcesByFilename - Sources by filename.
1044
+ * @param {Map<string, ReturnType<typeof createCoordinateIndex>>} coordinates - Source coordinate indexes.
1045
+ * @returns {void}
1046
+ */
1047
+ function validateLocationForSources(value, sourcesByFilename, coordinates) {
1048
+ if (!value || typeof value != "object") throw new TypeError("Malformed original location.")
1049
+ const location = /** @type {import("./semantic/types.js").SourceLocation} */ (value)
1050
+ const candidates = sourcesByFilename.get(location.filename) ?? []
1051
+
1052
+ if (candidates.length == 0 || !candidates.some((source) => locationMatchesSource(location, source, coordinates.get(source.id)))) {
1053
+ throw new TypeError("Stale original location.")
1054
+ }
1055
+ }
1056
+
1057
+ /**
1058
+ * Validates a location against its identified source.
1059
+ * @param {unknown} value - Candidate location.
1060
+ * @param {import("./semantic/types.js").RegisteredSource} source - Source.
1061
+ * @param {ReturnType<typeof createCoordinateIndex> | undefined} coordinates - Source coordinates when content exists.
1062
+ * @returns {void}
1063
+ */
1064
+ function validateLocationForSource(value, source, coordinates) {
1065
+ if (!value || typeof value != "object" || !locationMatchesSource(
1066
+ /** @type {import("./semantic/types.js").SourceLocation} */ (value), source, coordinates
1067
+ )) throw new TypeError("Stale original location.")
1068
+ }
1069
+
1070
+ /**
1071
+ * Tests a location against one source entry.
1072
+ * @param {import("./semantic/types.js").SourceLocation} location - Location.
1073
+ * @param {import("./semantic/types.js").RegisteredSource} source - Source.
1074
+ * @param {ReturnType<typeof createCoordinateIndex> | undefined} coordinates - Source coordinates when content exists.
1075
+ * @returns {boolean} Whether the range is normalized for the source.
1076
+ */
1077
+ function locationMatchesSource(location, source, coordinates) {
1078
+ if (!location || location.filename != source.filename || !location.start || !location.end ||
1079
+ !validPoint(location.start) || !validPoint(location.end) || location.end.offset < location.start.offset) return false
1080
+
1081
+ if (source.content == null) return true
1082
+
1083
+ return location.end.offset <= source.content.length && coordinates !== undefined &&
1084
+ samePoint(indexedPointAt(coordinates, location.start.offset), location.start) &&
1085
+ samePoint(indexedPointAt(coordinates, location.end.offset), location.end)
1086
+ }
1087
+
1088
+ /**
1089
+ * Tests the structural validity of one source point.
1090
+ * @param {unknown} value - Candidate point.
1091
+ * @returns {value is import("./semantic/types.js").SourcePoint} Whether it is a point.
1092
+ */
1093
+ function validPoint(value) {
1094
+ if (!value || typeof value != "object") return false
1095
+ const point = /** @type {Record<string, unknown>} */ (value)
1096
+
1097
+ return Number.isInteger(point.offset) && Number.isInteger(point.line) && Number.isInteger(point.column) &&
1098
+ /** @type {number} */ (point.offset) >= 0 && /** @type {number} */ (point.line) >= 1 && /** @type {number} */ (point.column) >= 1
1099
+ }
1100
+
1101
+ /**
1102
+ * Tests a supported semantic language value.
1103
+ * @param {unknown} value - Candidate language.
1104
+ * @returns {value is import("./semantic/types.js").SemanticLanguage} Whether supported.
1105
+ */
1106
+ function isLanguage(value) {
1107
+ return value == "php" || value == "ruby" || value == "javascript" || value == "typescript" || value == "java"
1108
+ }
1109
+
1110
+ /**
1111
+ * Builds one stable lookup result.
1112
+ * @param {import("./semantic/types.js").SemantifoldMappingSpan} span - Mapping span.
1113
+ * @returns {{generatedLocation: import("./semantic/types.js").SourceLocation, location: import("./semantic/types.js").SourceLocation | undefined, mappingKind: import("./semantic/types.js").MappingKind, name: string | undefined, nodeId: string | undefined, role: string | undefined, symbolId: string | undefined}} Lookup result.
1114
+ */
1115
+ function mappingResult(span) {
1116
+ return {
1117
+ generatedLocation: span.generated,
1118
+ location: primaryLocation(span.origin),
1119
+ mappingKind: span.mappingKind,
1120
+ name: span.name,
1121
+ nodeId: span.nodeId,
1122
+ role: span.role,
1123
+ symbolId: span.symbolId
1124
+ }
1125
+ }
1126
+
1127
+ /**
1128
+ * Resolves an offset-form or line/column-form position.
1129
+ * @param {ReturnType<typeof createCoordinateIndex>} coordinates - Source coordinate index.
1130
+ * @param {{offset: number} | {line: number, column: number}} position - Position.
1131
+ * @returns {number} UTF-16 offset.
1132
+ */
1133
+ function positionOffset(coordinates, position) {
1134
+ if ("offset" in position) {
1135
+ indexedPointAt(coordinates, position.offset)
1136
+
1137
+ return position.offset
1138
+ }
1139
+
1140
+ return indexedOffsetAt(coordinates, position.line, position.column)
1141
+ }
1142
+
1143
+ /**
1144
+ * Finds the exclusive content offset before the next generated line break.
1145
+ * @param {string} content - Generated content.
1146
+ * @param {number} offset - Offset on the current line.
1147
+ * @returns {number} End of the current line or content.
1148
+ */
1149
+ function lineEndOffset(content, offset) {
1150
+ const newline = content.indexOf("\n", offset)
1151
+
1152
+ return newline == -1 ? content.length : newline
1153
+ }
1154
+
1155
+ /**
1156
+ * Compares complete normalized points.
1157
+ * @param {import("./semantic/types.js").SourcePoint} left - Left point.
1158
+ * @param {import("./semantic/types.js").SourcePoint} right - Right point.
1159
+ * @returns {boolean} Whether coordinates match.
1160
+ */
1161
+ function samePoint(left, right) {
1162
+ return left.offset == right.offset && left.line == right.line && left.column == right.column
1163
+ }
1164
+
1165
+ /**
1166
+ * Resolves a source registry entry for an origin.
1167
+ * @param {import("./semantic/types.js").SemantifoldMapping} mapping - Mapping.
1168
+ * @param {import("./semantic/types.js").SemanticOrigin} origin - Origin.
1169
+ * @param {import("./semantic/types.js").SourceLocation} location - Primary location.
1170
+ * @param {Map<string, import("./semantic/types.js").RegisteredSource>} sourcesById - Indexed sources.
1171
+ * @returns {import("./semantic/types.js").RegisteredSource} Registered source.
1172
+ */
1173
+ function sourceForOrigin(mapping, origin, location, sourcesById) {
1174
+ const sourceId = sourceIdForOrigin(origin)
1175
+ const source = sourcesById.get(sourceId ?? "") ??
1176
+ mapping.sources.find((candidate) => candidate.filename == location.filename)
1177
+
1178
+ if (!source) throw new RangeError(`Unknown mapping source for ${location.filename}.`)
1179
+
1180
+ return source
1181
+ }
1182
+
1183
+ /**
1184
+ * Resolves the registry identity of an origin's primary location.
1185
+ * @param {import("./semantic/types.js").SemanticOrigin} origin - Origin.
1186
+ * @returns {string | undefined} Source identity.
1187
+ */
1188
+ function sourceIdForOrigin(origin) {
1189
+ if (origin.kind == "source") return origin.sourceId
1190
+ if (origin.kind == "derived") return origin.origins[0]?.sourceId
1191
+
1192
+ return origin.relatedOrigins[0]?.sourceId
1193
+ }
1194
+
1195
+ /**
1196
+ * Rewrites closed-origin source identities against a composed registry.
1197
+ * @param {import("./semantic/types.js").SemanticOrigin} origin - Origin.
1198
+ * @param {Map<string, string>} sourceIds - Original-to-composed source identities.
1199
+ * @returns {import("./semantic/types.js").SemanticOrigin} Rewritten origin.
1200
+ */
1201
+ function remapOriginSourceIds(origin, sourceIds) {
1202
+ if (origin.kind == "source") {
1203
+ const sourceId = sourceIds.get(origin.sourceId)
1204
+
1205
+ if (!sourceId) throw new RangeError(`Composed mapping omitted source '${origin.location.filename}'.`)
1206
+
1207
+ return {kind: "source", location: origin.location, sourceId}
1208
+ }
1209
+ if (origin.kind == "derived") return {kind: "derived", origins: origin.origins.map((related) => remapRelatedOriginSourceId(related, sourceIds))}
1210
+
1211
+ return {
1212
+ kind: "synthetic",
1213
+ reason: origin.reason,
1214
+ relatedOrigins: origin.relatedOrigins.map((related) => remapRelatedOriginSourceId(related, sourceIds))
1215
+ }
1216
+ }
1217
+
1218
+ /**
1219
+ * Rewrites one related source identity while dropping identities from a discarded node registry.
1220
+ * @param {import("./semantic/types.js").RelatedOrigin} related - Related origin.
1221
+ * @param {Map<string, string>} sourceIds - Original-to-composed source identities.
1222
+ * @returns {import("./semantic/types.js").RelatedOrigin} Rewritten related origin.
1223
+ */
1224
+ function remapRelatedOriginSourceId(related, sourceIds) {
1225
+ const sourceId = sourceIds.get(related.sourceId)
1226
+
1227
+ if (!sourceId) throw new RangeError(`Composed mapping omitted source '${related.location.filename}'.`)
1228
+
1229
+ const unidentified = {...related}
1230
+
1231
+ delete unidentified.nodeId
1232
+ delete unidentified.symbolId
1233
+
1234
+ return {...unidentified, sourceId}
1235
+ }
1236
+
1237
+ /**
1238
+ * Sorts object keys recursively while preserving semantic array order.
1239
+ * @param {unknown} value - JSON value.
1240
+ * @returns {unknown} Deterministically keyed value.
1241
+ */
1242
+ function sortJson(value) {
1243
+ if (Array.isArray(value)) return value.map(sortJson)
1244
+ if (value && typeof value == "object") {
1245
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
1246
+ .map(([key, child]) => [key, sortJson(child)]))
1247
+ }
1248
+
1249
+ return value
1250
+ }
1251
+
1252
+ /**
1253
+ * Removes undefined optional properties from a public JSON value.
1254
+ * @template {Record<string, unknown>} Value
1255
+ * @param {Value} value - Candidate object.
1256
+ * @returns {Value} Compact JSON-safe object.
1257
+ */
1258
+ function definedProperties(value) {
1259
+ return /** @type {Value} */ (Object.fromEntries(Object.entries(value).filter(([, child]) => child !== undefined)))
1260
+ }