@pieceful/ravel-projection 0.2.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.
package/src/builder.js ADDED
@@ -0,0 +1,533 @@
1
+ import { createLineIndex } from "./line-index.js";
2
+ import { buildProjectionIndexes, coalesceProjectionSegments, validateProjectionSegments } from "./mapping.js";
3
+ import {
4
+ clone,
5
+ deepFreeze,
6
+ defaultCapabilities,
7
+ encodeUriPart,
8
+ encodeVirtualPath,
9
+ isInteger,
10
+ projectionStages,
11
+ sourceLocation,
12
+ sourceRangeOffsets,
13
+ stableHash,
14
+ stableStringify,
15
+ throwIfAborted
16
+ } from "./internal.js";
17
+
18
+ const defaultPosition = () => ({ line: 0, column: 0, offset: 0 });
19
+ const defaultSource = (uri) => ({
20
+ uri,
21
+ range: { start: defaultPosition(), end: defaultPosition() }
22
+ });
23
+
24
+ const languageForName = (name) => {
25
+ const extension = String(name).split(".").at(-1)?.toLowerCase();
26
+ return ({
27
+ ts: "typescript",
28
+ tsx: "typescriptreact",
29
+ js: "javascript",
30
+ jsx: "javascriptreact",
31
+ mjs: "javascript",
32
+ cjs: "javascript",
33
+ py: "python",
34
+ rs: "rust",
35
+ go: "go",
36
+ java: "java",
37
+ css: "css",
38
+ html: "html",
39
+ json: "json",
40
+ md: "markdown"
41
+ })[extension] ?? extension ?? "plaintext";
42
+ };
43
+
44
+ export const createProjectionId = ({
45
+ workspaceId = "workspace",
46
+ targetId = "default",
47
+ artifactId,
48
+ stage = "assembled",
49
+ languageId = "plaintext"
50
+ } = {}) => [
51
+ "ravel-projection",
52
+ workspaceId,
53
+ targetId,
54
+ artifactId ?? "artifact",
55
+ stage,
56
+ languageId
57
+ ].map((part) => encodeUriPart(part)).join(":");
58
+
59
+ export const createVirtualUri = ({
60
+ workspaceId = "workspace",
61
+ targetId = "default",
62
+ artifactId,
63
+ stage = "assembled",
64
+ path
65
+ } = {}) => {
66
+ const safeStage = projectionStages.has(stage) ? stage : "assembled";
67
+ const virtualPath = path ?? artifactId ?? "artifact.txt";
68
+ return `pieceful-virtual://${encodeUriPart(workspaceId)}/` +
69
+ `${encodeUriPart(targetId)}/${encodeUriPart(artifactId ?? virtualPath)}/` +
70
+ `${encodeUriPart(safeStage)}/${encodeVirtualPath(virtualPath)}`;
71
+ };
72
+
73
+ const referenceKey = (reference) => {
74
+ const range = sourceRangeOffsets(reference?.source);
75
+ return stableStringify({
76
+ from: reference?.from,
77
+ to: reference?.to ?? reference?.chunk,
78
+ uri: reference?.source?.uri,
79
+ start: range?.start,
80
+ end: range?.end
81
+ });
82
+ };
83
+
84
+ const normalizeReference = (reference, fallbackFrom) => ({
85
+ kind: "reference",
86
+ from: reference?.from ?? fallbackFrom,
87
+ to: reference?.to ?? reference?.chunk,
88
+ source: sourceLocation(reference?.source)
89
+ });
90
+
91
+ const referencesForSegment = (segment) => (segment?.via ?? [])
92
+ .filter((step) => step?.kind === "reference" && typeof step.to === "string")
93
+ .map((step) => normalizeReference(step))
94
+ .reverse();
95
+
96
+ const transformsForSegment = (segment) => (segment?.via ?? [])
97
+ .filter((step) => step?.kind === "transform")
98
+ .map((step) => ({
99
+ kind: "transform",
100
+ name: step.name,
101
+ phase: step.phase,
102
+ source: sourceLocation(step.source)
103
+ }));
104
+
105
+ const classifyCoreSegment = (segment) => {
106
+ if (!segment?.source) return "synthetic";
107
+ const virtualLength = segment.generated?.end - segment.generated?.start;
108
+ const sourceRange = sourceRangeOffsets(segment.source);
109
+ const exact = segment.precision === "exact" && sourceRange &&
110
+ sourceRange.end - sourceRange.start === virtualLength;
111
+ if (exact) return transformsForSegment(segment).length ? "transformed" : "exact";
112
+ if (segment.kind === "transform" || segment.kind === "delay-fulfillment" ||
113
+ (segment.origins?.length && transformsForSegment(segment).length)) return "opaque";
114
+ return "anchored";
115
+ };
116
+
117
+ const directReferences = (program, pieceId) => (program.chunks?.[pieceId]?.references ?? [])
118
+ .map((reference) => normalizeReference(reference, pieceId))
119
+ .filter((reference) => typeof reference.to === "string");
120
+
121
+ const occurrenceBuilder = (program, projectionId, rootPieceId, bodyRange, rawSegments, shift, signal) => {
122
+ const occurrences = [];
123
+ const byId = new Map();
124
+ const childByKey = new Map();
125
+
126
+ const add = ({ pieceId, parent, reference, implicit = false }) => {
127
+ const key = parent
128
+ ? `${parent.id}\u0000${referenceKey(reference)}\u0000${pieceId}`
129
+ : `${projectionId}\u0000root\u0000${pieceId}`;
130
+ const id = `${projectionId}:occ:${stableHash(key)}`;
131
+ const existing = byId.get(id);
132
+ if (existing) return existing;
133
+ const occurrence = {
134
+ id,
135
+ pieceId,
136
+ projectionId,
137
+ virtual: parent ? null : clone(bodyRange),
138
+ invocationSource: sourceLocation(reference?.source),
139
+ definitionSource: sourceLocation(program.chunks?.[pieceId]?.source),
140
+ expansionPath: parent ? [...parent.expansionPath, pieceId] : [pieceId],
141
+ parentOccurrenceId: parent?.id,
142
+ childOccurrenceIds: [],
143
+ implicit,
144
+ _ranges: []
145
+ };
146
+ occurrences.push(occurrence);
147
+ byId.set(id, occurrence);
148
+ if (parent) {
149
+ parent.childOccurrenceIds.push(id);
150
+ childByKey.set(`${parent.id}\u0000${referenceKey(reference)}`, occurrence);
151
+ }
152
+ return occurrence;
153
+ };
154
+
155
+ const root = add({ pieceId: rootPieceId });
156
+
157
+ const ensureChain = (chain, leafPieceId) => {
158
+ let current = root;
159
+ for (const reference of chain) {
160
+ throwIfAborted(signal);
161
+ if (reference.from && reference.from !== current.pieceId) continue;
162
+ const key = `${current.id}\u0000${referenceKey(reference)}`;
163
+ current = childByKey.get(key) ?? add({ pieceId: reference.to, parent: current, reference });
164
+ }
165
+ if (leafPieceId && leafPieceId !== current.pieceId && leafPieceId !== rootPieceId) {
166
+ const implicitReference = {
167
+ from: current.pieceId,
168
+ to: leafPieceId,
169
+ source: program.chunks?.[leafPieceId]?.source
170
+ };
171
+ const key = `${current.id}\u0000${referenceKey(implicitReference)}`;
172
+ current = childByKey.get(key) ?? add({
173
+ pieceId: leafPieceId,
174
+ parent: current,
175
+ reference: implicitReference,
176
+ implicit: true
177
+ });
178
+ }
179
+ return current;
180
+ };
181
+
182
+ const assignments = [];
183
+ for (const segment of rawSegments) {
184
+ throwIfAborted(signal);
185
+ const leaf = ensureChain(referencesForSegment(segment), segment.chunk);
186
+ const range = {
187
+ start: shift + segment.generated.start,
188
+ end: shift + segment.generated.end
189
+ };
190
+ assignments.push({ segment, occurrence: leaf, range });
191
+ let cursor = leaf;
192
+ while (cursor) {
193
+ cursor._ranges.push(range);
194
+ cursor = cursor.parentOccurrenceId ? byId.get(cursor.parentOccurrenceId) : undefined;
195
+ }
196
+ }
197
+
198
+ const inferAnchor = (parent, reference) => {
199
+ const invocation = sourceRangeOffsets(reference.source);
200
+ if (!invocation) return parent.virtual?.start ?? bodyRange.start;
201
+ let before;
202
+ let after;
203
+ for (const assignment of assignments) {
204
+ if (assignment.occurrence.id !== parent.id || assignment.segment.source?.uri !== reference.source?.uri) continue;
205
+ const sourceRange = sourceRangeOffsets(assignment.segment.source);
206
+ if (!sourceRange) continue;
207
+ if (sourceRange.end <= invocation.start && (!before || sourceRange.end > before.sourceEnd)) {
208
+ before = { sourceEnd: sourceRange.end, virtual: assignment.range.end };
209
+ }
210
+ if (sourceRange.start >= invocation.end && (!after || sourceRange.start < after.sourceStart)) {
211
+ after = { sourceStart: sourceRange.start, virtual: assignment.range.start };
212
+ }
213
+ }
214
+ for (const siblingId of parent.childOccurrenceIds) {
215
+ const sibling = byId.get(siblingId);
216
+ const siblingSource = sourceRangeOffsets(sibling?.invocationSource);
217
+ const siblingVirtual = sibling?.virtual ?? (sibling?._ranges?.length ? {
218
+ start: Math.min(...sibling._ranges.map((range) => range.start)),
219
+ end: Math.max(...sibling._ranges.map((range) => range.end))
220
+ } : undefined);
221
+ if (!siblingSource || sibling?.invocationSource?.uri !== reference.source?.uri || !siblingVirtual) continue;
222
+ if (siblingSource.end <= invocation.start && (!before || siblingSource.end > before.sourceEnd)) {
223
+ before = { sourceEnd: siblingSource.end, virtual: siblingVirtual.end };
224
+ }
225
+ if (siblingSource.start >= invocation.end && (!after || siblingSource.start < after.sourceStart)) {
226
+ after = { sourceStart: siblingSource.start, virtual: siblingVirtual.start };
227
+ }
228
+ }
229
+ return before?.virtual ?? after?.virtual ?? parent.virtual?.start ?? bodyRange.start;
230
+ };
231
+
232
+ // Core provenance cannot emit a text segment for an empty reference. Walk
233
+ // each concrete parent occurrence to retain those semantic occurrences as
234
+ // zero-width anchors.
235
+ for (let index = 0; index < occurrences.length; index += 1) {
236
+ throwIfAborted(signal);
237
+ const parent = occurrences[index];
238
+ if (parent.expansionPath.slice(0, -1).includes(parent.pieceId)) continue;
239
+ for (const reference of directReferences(program, parent.pieceId)) {
240
+ const key = `${parent.id}\u0000${referenceKey(reference)}`;
241
+ if (childByKey.has(key)) continue;
242
+ if (parent.expansionPath.includes(reference.to)) continue;
243
+ const child = add({ pieceId: reference.to, parent, reference });
244
+ const anchor = inferAnchor(parent, reference);
245
+ child.virtual = { start: anchor, end: anchor };
246
+ }
247
+ }
248
+
249
+ for (const occurrence of occurrences) {
250
+ if (!occurrence.virtual) {
251
+ if (occurrence._ranges.length) occurrence.virtual = {
252
+ start: Math.min(...occurrence._ranges.map((range) => range.start)),
253
+ end: Math.max(...occurrence._ranges.map((range) => range.end))
254
+ };
255
+ else {
256
+ const parent = byId.get(occurrence.parentOccurrenceId);
257
+ occurrence.virtual = { start: parent?.virtual?.start ?? bodyRange.start, end: parent?.virtual?.start ?? bodyRange.start };
258
+ }
259
+ }
260
+ delete occurrence._ranges;
261
+ }
262
+
263
+ return { occurrences, assignments, root, byId };
264
+ };
265
+
266
+ const projectionSegmentFor = (assignment, shift = 0) => {
267
+ const { segment, occurrence, range } = assignment;
268
+ return {
269
+ virtual: clone(range ?? {
270
+ start: shift + segment.generated.start,
271
+ end: shift + segment.generated.end
272
+ }),
273
+ source: sourceLocation(segment.source),
274
+ pieceId: segment.chunk ?? occurrence?.pieceId,
275
+ occurrenceId: occurrence?.id,
276
+ expansionPath: clone(occurrence?.expansionPath ?? [segment.chunk].filter(Boolean)),
277
+ kind: classifyCoreSegment(segment),
278
+ role: segment.kind === "literal" ? "content" : segment.kind ?? "content",
279
+ startAffinity: "right",
280
+ endAffinity: "left",
281
+ transformChain: transformsForSegment(segment)
282
+ };
283
+ };
284
+
285
+ const sourceTextsRecord = (sourceTexts) => {
286
+ if (sourceTexts instanceof Map) return Object.fromEntries([...sourceTexts.entries()]);
287
+ return sourceTexts && typeof sourceTexts === "object" ? sourceTexts : {};
288
+ };
289
+
290
+ const reachableProjectionChunks = (program, rootPieceId) => {
291
+ const result = [];
292
+ const seen = new Set();
293
+ const pending = [rootPieceId];
294
+ while (pending.length) {
295
+ const id = pending.shift();
296
+ if (typeof id !== "string" || seen.has(id)) continue;
297
+ seen.add(id);
298
+ const chunk = program?.chunks?.[id];
299
+ if (!chunk) continue;
300
+ result.push([id, chunk]);
301
+ const dependencies = new Set([
302
+ ...(chunk.dependencies ?? []),
303
+ ...(chunk.references ?? []).map((reference) => reference.chunk)
304
+ ]);
305
+ pending.push(...[...dependencies].filter((entry) => typeof entry === "string").sort());
306
+ }
307
+ return result.sort(([left], [right]) => left.localeCompare(right));
308
+ };
309
+
310
+ const occurrenceForOrigin = (occurrenceData, segment, origin) => {
311
+ const chain = [
312
+ ...(origin?.via ?? []),
313
+ ...(segment?.via ?? [])
314
+ ].filter((step) => step?.kind === "reference").map(normalizeReference).reverse();
315
+ let current = occurrenceData.root;
316
+ for (const reference of chain) {
317
+ const key = `${current.id}\u0000${referenceKey(reference)}`;
318
+ const child = occurrenceData.occurrences.find((entry) =>
319
+ entry.parentOccurrenceId === current.id &&
320
+ referenceKey({ from: current.pieceId, to: entry.pieceId, source: entry.invocationSource }) === referenceKey(reference));
321
+ if (!child) break;
322
+ current = child;
323
+ }
324
+ return origin?.chunk
325
+ ? occurrenceData.occurrences.find((entry) => entry.pieceId === origin.chunk &&
326
+ entry.expansionPath.every((piece, index) => current.expansionPath[index] === piece)) ?? current
327
+ : current;
328
+ };
329
+
330
+ export const buildVirtualDocument = (program, options = {}) => {
331
+ const signal = options.signal;
332
+ throwIfAborted(signal);
333
+ const deliverableNames = Object.keys(program?.deliverables ?? {}).sort();
334
+ const artifactId = options.artifactId ?? deliverableNames[0] ?? "missing-artifact";
335
+ const deliverable = program?.deliverables?.[artifactId];
336
+ const targetId = options.targetId ?? "default";
337
+ const workspaceId = options.workspaceId ?? "workspace";
338
+ const stage = projectionStages.has(options.stage) ? options.stage : "assembled";
339
+ const rootPieceId = deliverable?.from ?? options.rootPieceId ?? "<missing>";
340
+ const languageId = options.languageId ??
341
+ program?.chunks?.[rootPieceId]?.metadata?.language ?? languageForName(artifactId);
342
+ const id = options.projectionId ?? createProjectionId({
343
+ workspaceId, targetId, artifactId, stage, languageId
344
+ });
345
+ const uri = options.uri ?? createVirtualUri({
346
+ workspaceId,
347
+ targetId,
348
+ artifactId,
349
+ stage,
350
+ path: options.path ?? artifactId
351
+ });
352
+ const prefix = typeof options.prefix === "string" ? options.prefix : "";
353
+ const suffix = typeof options.suffix === "string" ? options.suffix : "";
354
+ const body = deliverable?.value ?? "";
355
+ const text = prefix + body + suffix;
356
+ const bodyRange = { start: prefix.length, end: prefix.length + body.length };
357
+ const rawSegments = Array.isArray(deliverable?.segments) ? deliverable.segments : [];
358
+ const occurrenceData = occurrenceBuilder(
359
+ program ?? {}, id, rootPieceId, bodyRange, rawSegments, prefix.length, signal
360
+ );
361
+ const mappings = occurrenceData.assignments.map((assignment) => projectionSegmentFor(assignment));
362
+
363
+ // A coarse transform retains all known input origins for navigation. Each
364
+ // origin is a separate candidate over the same virtual range, intentionally
365
+ // preserving one-to-many provenance.
366
+ for (const assignment of occurrenceData.assignments) {
367
+ for (const origin of assignment.segment.origins ?? []) {
368
+ if (!origin?.source) continue;
369
+ const occurrence = occurrenceForOrigin(occurrenceData, assignment.segment, origin);
370
+ mappings.push({
371
+ virtual: clone(assignment.range),
372
+ source: sourceLocation(origin.source),
373
+ pieceId: origin.chunk ?? occurrence.pieceId,
374
+ occurrenceId: occurrence.id,
375
+ expansionPath: clone(occurrence.expansionPath),
376
+ kind: "opaque",
377
+ role: "transform-origin",
378
+ startAffinity: "right",
379
+ endAffinity: "left",
380
+ transformChain: transformsForSegment(assignment.segment)
381
+ });
382
+ }
383
+ }
384
+
385
+ if (prefix.length) mappings.push({
386
+ virtual: { start: 0, end: prefix.length },
387
+ pieceId: rootPieceId,
388
+ occurrenceId: occurrenceData.root.id,
389
+ expansionPath: [rootPieceId],
390
+ kind: "synthetic",
391
+ role: "prefix",
392
+ startAffinity: "right",
393
+ endAffinity: "left",
394
+ transformChain: []
395
+ });
396
+ if (suffix.length) mappings.push({
397
+ virtual: { start: bodyRange.end, end: text.length },
398
+ pieceId: rootPieceId,
399
+ occurrenceId: occurrenceData.root.id,
400
+ expansionPath: [rootPieceId],
401
+ kind: "synthetic",
402
+ role: "suffix",
403
+ startAffinity: "right",
404
+ endAffinity: "left",
405
+ transformChain: []
406
+ });
407
+
408
+ const contentMappings = coalesceProjectionSegments(mappings);
409
+ const invocationMappings = occurrenceData.occurrences
410
+ .filter((occurrence) => occurrence.parentOccurrenceId && occurrence.invocationSource)
411
+ .map((occurrence) => ({
412
+ virtual: { start: occurrence.virtual.start, end: occurrence.virtual.start },
413
+ source: clone(occurrence.invocationSource),
414
+ pieceId: occurrence.pieceId,
415
+ occurrenceId: occurrence.id,
416
+ expansionPath: clone(occurrence.expansionPath),
417
+ kind: "anchored",
418
+ role: "invocation",
419
+ startAffinity: "right",
420
+ endAffinity: "left",
421
+ transformChain: []
422
+ }));
423
+ const finalMappings = coalesceProjectionSegments([...contentMappings, ...invocationMappings]);
424
+ const occurrences = occurrenceData.occurrences.map((occurrence) => ({
425
+ ...occurrence,
426
+ invocationSource: sourceLocation(occurrence.invocationSource),
427
+ definitionSource: sourceLocation(occurrence.definitionSource)
428
+ }));
429
+ const lineIndex = createLineIndex(text);
430
+ const sourceLineIndexes = Object.fromEntries(Object.entries(sourceTextsRecord(options.sourceTexts))
431
+ .filter(([, sourceText]) => typeof sourceText === "string")
432
+ .sort(([left], [right]) => left.localeCompare(right))
433
+ .map(([sourceUri, sourceText]) => [sourceUri, createLineIndex(sourceText)]));
434
+ const indexes = buildProjectionIndexes(finalMappings, occurrences);
435
+ const projectionDiagnostics = [
436
+ ...(program?.diagnostics ?? []).map(clone),
437
+ ...(!deliverable ? [{
438
+ code: "RVP001",
439
+ severity: "error",
440
+ message: `Unknown projection artifact: ${artifactId}`,
441
+ source: defaultSource(artifactId)
442
+ }] : []),
443
+ ...validateProjectionSegments(text, finalMappings).map((issue) => ({
444
+ ...issue,
445
+ severity: "error",
446
+ source: sourceLocation(deliverable?.source) ?? defaultSource(artifactId)
447
+ }))
448
+ ];
449
+ const capabilities = deepFreeze({
450
+ ...defaultCapabilities(stage),
451
+ ...(options.capabilities ?? {})
452
+ });
453
+ const snapshotId = String(options.snapshotId ?? `snapshot:${stableHash({
454
+ documents: program?.documents,
455
+ artifactId,
456
+ text,
457
+ mappings: finalMappings
458
+ })}`);
459
+ const inputHash = stableHash({
460
+ artifactId,
461
+ workspaceId,
462
+ targetId,
463
+ stage,
464
+ languageId,
465
+ rootPieceId,
466
+ uri,
467
+ prefix,
468
+ suffix,
469
+ capabilities,
470
+ deliverable,
471
+ chunks: reachableProjectionChunks(program, rootPieceId)
472
+ });
473
+ const document = {
474
+ id,
475
+ uri,
476
+ snapshotId,
477
+ sourceVersions: clone(options.sourceVersions ?? {}),
478
+ version: isInteger(options.version) && options.version > 0 ? options.version : 1,
479
+ workspaceId,
480
+ artifactId,
481
+ targetId,
482
+ stage,
483
+ languageId,
484
+ text,
485
+ mappings: finalMappings,
486
+ occurrences,
487
+ lineIndex,
488
+ sourceLineIndexes,
489
+ indexes,
490
+ contentHash: stableHash(text),
491
+ inputHash,
492
+ artifactSource: sourceLocation(deliverable?.source),
493
+ projectionDiagnostics,
494
+ capabilities
495
+ };
496
+ return deepFreeze(document);
497
+ };
498
+
499
+ export const projectionInputHash = (program, options = {}) => {
500
+ const artifactId = options.artifactId ?? Object.keys(program?.deliverables ?? {}).sort()[0];
501
+ const deliverable = program?.deliverables?.[artifactId];
502
+ const workspaceId = options.workspaceId ?? "workspace";
503
+ const targetId = options.targetId ?? "default";
504
+ const stage = projectionStages.has(options.stage) ? options.stage : "assembled";
505
+ const rootPieceId = deliverable?.from ?? options.rootPieceId ?? "<missing>";
506
+ const languageId = options.languageId ??
507
+ program?.chunks?.[rootPieceId]?.metadata?.language ?? languageForName(artifactId);
508
+ const uri = options.uri ?? createVirtualUri({
509
+ workspaceId,
510
+ targetId,
511
+ artifactId,
512
+ stage,
513
+ path: options.path ?? artifactId
514
+ });
515
+ const capabilities = {
516
+ ...defaultCapabilities(stage),
517
+ ...(options.capabilities ?? {})
518
+ };
519
+ return stableHash({
520
+ artifactId,
521
+ workspaceId,
522
+ targetId,
523
+ stage,
524
+ languageId,
525
+ rootPieceId,
526
+ uri,
527
+ prefix: typeof options.prefix === "string" ? options.prefix : "",
528
+ suffix: typeof options.suffix === "string" ? options.suffix : "",
529
+ capabilities,
530
+ deliverable,
531
+ chunks: reachableProjectionChunks(program, rootPieceId)
532
+ });
533
+ };
package/src/context.js ADDED
@@ -0,0 +1,108 @@
1
+ import { lineWindow } from "./line-index.js";
2
+ import { mapVirtualRange } from "./mapping.js";
3
+ import { clone, deepFreeze, rangesOverlap, sourceRangeOffsets, validOffsetRange } from "./internal.js";
4
+
5
+ const occurrenceMap = (document) => new Map(
6
+ (document?.occurrences ?? []).map((occurrence) => [occurrence.id, occurrence])
7
+ );
8
+
9
+ const isDescendant = (candidate, selected, byId) => {
10
+ let current = candidate;
11
+ while (current?.parentOccurrenceId) {
12
+ if (current.parentOccurrenceId === selected.id) return true;
13
+ current = byId.get(current.parentOccurrenceId);
14
+ }
15
+ return false;
16
+ };
17
+
18
+ const sourceSelectionMatches = (segment, selection) => {
19
+ if (!selection || segment.source?.uri !== selection.uri) return false;
20
+ const segmentRange = sourceRangeOffsets(segment.source);
21
+ const selectionRange = sourceRangeOffsets(selection);
22
+ return Boolean(segmentRange && selectionRange && rangesOverlap(segmentRange, selectionRange, {
23
+ includeBoundary: segmentRange.start === segmentRange.end || selectionRange.start === selectionRange.end
24
+ }));
25
+ };
26
+
27
+ const highlightCategories = (segment, occurrence, selected, byId, sourceSelection) => {
28
+ const categories = [];
29
+ if (sourceSelectionMatches(segment, sourceSelection)) categories.push("selected-fragment");
30
+ if (occurrence?.id === selected.id) categories.push("selected-piece");
31
+ else if (occurrence && isDescendant(occurrence, selected, byId)) categories.push("descendant");
32
+ else categories.push("surrounding-context");
33
+ if (segment.kind === "transformed" || segment.transformChain?.length) categories.push("transformed");
34
+ if (segment.kind === "synthetic") categories.push("synthetic");
35
+ return categories;
36
+ };
37
+
38
+ const concisePath = (occurrence) => occurrence.expansionPath.join(" › ");
39
+
40
+ export const generatedContext = (document, occurrenceId, options = {}) => {
41
+ if (!document || typeof occurrenceId !== "string") {
42
+ return deepFreeze({ ok: false, reason: "invalid-occurrence" });
43
+ }
44
+ if (options.projectionVersion !== undefined && options.projectionVersion !== document.version) {
45
+ return deepFreeze({ ok: false, reason: "stale-projection" });
46
+ }
47
+ const byId = occurrenceMap(document);
48
+ const selected = byId.get(occurrenceId);
49
+ if (!selected) return deepFreeze({ ok: false, reason: "unknown-occurrence" });
50
+ const window = lineWindow(document.lineIndex, selected.virtual, options.surroundingLines ?? 3);
51
+ const visibleRange = window.ok ? window.range : clone(selected.virtual);
52
+ const highlights = [];
53
+ for (const segment of document.mappings) {
54
+ if (segment.virtual.start === segment.virtual.end ||
55
+ !rangesOverlap(segment.virtual, visibleRange, { includeBoundary: true })) continue;
56
+ const occurrence = byId.get(segment.occurrenceId);
57
+ const categories = highlightCategories(segment, occurrence, selected, byId, options.sourceSelection);
58
+ highlights.push({
59
+ range: {
60
+ start: Math.max(segment.virtual.start, visibleRange.start),
61
+ end: Math.min(segment.virtual.end, visibleRange.end)
62
+ },
63
+ kind: categories[0],
64
+ categories,
65
+ mappingKind: segment.kind,
66
+ pieceId: segment.pieceId,
67
+ occurrenceId: segment.occurrenceId
68
+ });
69
+ }
70
+ const breadcrumb = [];
71
+ let cursor = selected;
72
+ while (cursor) {
73
+ breadcrumb.unshift({
74
+ occurrenceId: cursor.id,
75
+ pieceId: cursor.pieceId,
76
+ label: cursor.pieceId,
77
+ virtual: clone(cursor.virtual),
78
+ invocationSource: clone(cursor.invocationSource)
79
+ });
80
+ cursor = cursor.parentOccurrenceId ? byId.get(cursor.parentOccurrenceId) : undefined;
81
+ }
82
+ const siblings = document.occurrences
83
+ .filter((occurrence) => occurrence.pieceId === selected.pieceId && occurrence.id !== selected.id)
84
+ .map((occurrence) => ({
85
+ occurrenceId: occurrence.id,
86
+ pieceId: occurrence.pieceId,
87
+ targetId: document.targetId,
88
+ artifactId: document.artifactId,
89
+ stage: document.stage,
90
+ virtual: clone(occurrence.virtual),
91
+ pathLabel: concisePath(occurrence)
92
+ }));
93
+ return deepFreeze({
94
+ ok: true,
95
+ projection: document,
96
+ projectionVersion: document.version,
97
+ selectedOccurrenceId: selected.id,
98
+ visibleRange,
99
+ highlights,
100
+ breadcrumb,
101
+ siblings
102
+ });
103
+ };
104
+
105
+ export const navigateGeneratedSelection = (document, range, options = {}) => {
106
+ if (!validOffsetRange(range)) return deepFreeze({ ok: false, reason: "invalid-range", matches: [] });
107
+ return mapVirtualRange(document, range, options);
108
+ };