wabachi 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 (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -0
  3. package/dist/architecture/canon/authority.d.ts +35 -0
  4. package/dist/architecture/canon/authority.js +76 -0
  5. package/dist/architecture/canon/authority.js.map +1 -0
  6. package/dist/architecture/canon/boundaries.d.ts +31 -0
  7. package/dist/architecture/canon/boundaries.js +73 -0
  8. package/dist/architecture/canon/boundaries.js.map +1 -0
  9. package/dist/architecture/canon/codec.d.ts +10 -0
  10. package/dist/architecture/canon/codec.js +516 -0
  11. package/dist/architecture/canon/codec.js.map +1 -0
  12. package/dist/architecture/canon/constraints.d.ts +60 -0
  13. package/dist/architecture/canon/constraints.js +116 -0
  14. package/dist/architecture/canon/constraints.js.map +1 -0
  15. package/dist/architecture/canon/decisions.d.ts +62 -0
  16. package/dist/architecture/canon/decisions.js +192 -0
  17. package/dist/architecture/canon/decisions.js.map +1 -0
  18. package/dist/architecture/canon/deployment.d.ts +89 -0
  19. package/dist/architecture/canon/deployment.js +156 -0
  20. package/dist/architecture/canon/deployment.js.map +1 -0
  21. package/dist/architecture/canon/document.d.ts +60 -0
  22. package/dist/architecture/canon/document.js +106 -0
  23. package/dist/architecture/canon/document.js.map +1 -0
  24. package/dist/architecture/canon/elements.d.ts +21 -0
  25. package/dist/architecture/canon/elements.js +87 -0
  26. package/dist/architecture/canon/elements.js.map +1 -0
  27. package/dist/architecture/canon/flows.d.ts +37 -0
  28. package/dist/architecture/canon/flows.js +82 -0
  29. package/dist/architecture/canon/flows.js.map +1 -0
  30. package/dist/architecture/canon/identity.d.ts +45 -0
  31. package/dist/architecture/canon/identity.js +83 -0
  32. package/dist/architecture/canon/identity.js.map +1 -0
  33. package/dist/architecture/canon/relationships.d.ts +48 -0
  34. package/dist/architecture/canon/relationships.js +181 -0
  35. package/dist/architecture/canon/relationships.js.map +1 -0
  36. package/dist/architecture/canon/repository-mappings.d.ts +51 -0
  37. package/dist/architecture/canon/repository-mappings.js +134 -0
  38. package/dist/architecture/canon/repository-mappings.js.map +1 -0
  39. package/dist/architecture/canon/responsibilities.d.ts +40 -0
  40. package/dist/architecture/canon/responsibilities.js +89 -0
  41. package/dist/architecture/canon/responsibilities.js.map +1 -0
  42. package/dist/architecture/canon/validate.d.ts +14 -0
  43. package/dist/architecture/canon/validate.js +389 -0
  44. package/dist/architecture/canon/validate.js.map +1 -0
  45. package/dist/architecture/canon/views.d.ts +101 -0
  46. package/dist/architecture/canon/views.js +182 -0
  47. package/dist/architecture/canon/views.js.map +1 -0
  48. package/dist/architecture/cli.d.ts +2 -0
  49. package/dist/architecture/cli.js +158 -0
  50. package/dist/architecture/cli.js.map +1 -0
  51. package/dist/architecture/documentation/html.d.ts +3 -0
  52. package/dist/architecture/documentation/html.js +143 -0
  53. package/dist/architecture/documentation/html.js.map +1 -0
  54. package/dist/architecture/documentation/model.d.ts +46 -0
  55. package/dist/architecture/documentation/model.js +13 -0
  56. package/dist/architecture/documentation/model.js.map +1 -0
  57. package/dist/architecture/documentation/project.d.ts +4 -0
  58. package/dist/architecture/documentation/project.js +166 -0
  59. package/dist/architecture/documentation/project.js.map +1 -0
  60. package/dist/architecture/documentation/site.d.ts +43 -0
  61. package/dist/architecture/documentation/site.js +147 -0
  62. package/dist/architecture/documentation/site.js.map +1 -0
  63. package/dist/architecture/projection/structurizr-export.d.ts +65 -0
  64. package/dist/architecture/projection/structurizr-export.js +136 -0
  65. package/dist/architecture/projection/structurizr-export.js.map +1 -0
  66. package/dist/architecture/projection/structurizr.d.ts +34 -0
  67. package/dist/architecture/projection/structurizr.js +667 -0
  68. package/dist/architecture/projection/structurizr.js.map +1 -0
  69. package/dist/cli.d.ts +1 -0
  70. package/dist/cli.js +143 -0
  71. package/dist/cli.js.map +1 -0
  72. package/dist/index.d.ts +2 -0
  73. package/dist/index.js +6 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/runtime/correlation.d.ts +143 -0
  76. package/dist/runtime/correlation.js +919 -0
  77. package/dist/runtime/correlation.js.map +1 -0
  78. package/dist/runtime/facts.d.ts +138 -0
  79. package/dist/runtime/facts.js +614 -0
  80. package/dist/runtime/facts.js.map +1 -0
  81. package/dist/runtime/fixtureProvider.d.ts +10 -0
  82. package/dist/runtime/fixtureProvider.js +30 -0
  83. package/dist/runtime/fixtureProvider.js.map +1 -0
  84. package/dist/runtime/graftProvider.d.ts +8 -0
  85. package/dist/runtime/graftProvider.js +107 -0
  86. package/dist/runtime/graftProvider.js.map +1 -0
  87. package/dist/runtime/graftWiring.d.ts +30 -0
  88. package/dist/runtime/graftWiring.js +3 -0
  89. package/dist/runtime/graftWiring.js.map +1 -0
  90. package/dist/runtime/manifest.d.ts +17 -0
  91. package/dist/runtime/manifest.js +39 -0
  92. package/dist/runtime/manifest.js.map +1 -0
  93. package/dist/runtime/matrix.d.ts +337 -0
  94. package/dist/runtime/matrix.js +1485 -0
  95. package/dist/runtime/matrix.js.map +1 -0
  96. package/dist/runtime/observation.d.ts +36 -0
  97. package/dist/runtime/observation.js +3 -0
  98. package/dist/runtime/observation.js.map +1 -0
  99. package/dist/runtime/provider.d.ts +34 -0
  100. package/dist/runtime/provider.js +2 -0
  101. package/dist/runtime/provider.js.map +1 -0
  102. package/dist/runtime/repository.d.ts +20 -0
  103. package/dist/runtime/repository.js +55 -0
  104. package/dist/runtime/repository.js.map +1 -0
  105. package/dist/runtime/run.d.ts +21 -0
  106. package/dist/runtime/run.js +80 -0
  107. package/dist/runtime/run.js.map +1 -0
  108. package/dist/runtime/scipProvider.d.ts +11 -0
  109. package/dist/runtime/scipProvider.js +126 -0
  110. package/dist/runtime/scipProvider.js.map +1 -0
  111. package/dist/runtime/typescriptProvider.d.ts +10 -0
  112. package/dist/runtime/typescriptProvider.js +289 -0
  113. package/dist/runtime/typescriptProvider.js.map +1 -0
  114. package/dist/runtime/workflow.d.ts +30 -0
  115. package/dist/runtime/workflow.js +153 -0
  116. package/dist/runtime/workflow.js.map +1 -0
  117. package/package.json +72 -0
@@ -0,0 +1,919 @@
1
+ import { createHash } from "node:crypto";
2
+ const MAX_CANDIDATE_PAIRS_PER_KEY = 100_000;
3
+ const RULE_ORDER = [
4
+ "same-repository-revision",
5
+ "same-path",
6
+ "same-range",
7
+ "same-kind",
8
+ "same-qualified-name",
9
+ "same-alias",
10
+ "same-name",
11
+ "same-signature",
12
+ ];
13
+ /**
14
+ * Converts provider paths to a repository-relative, slash-separated form.
15
+ * This intentionally does not consult the host filesystem.
16
+ */
17
+ export function normalizeRepositoryPath(value) {
18
+ const parts = [];
19
+ for (const part of value.trim().replaceAll("\\", "/").split("/")) {
20
+ if (part.length === 0 || part === ".")
21
+ continue;
22
+ if (part === "..") {
23
+ if (parts.length > 0 && parts[parts.length - 1] !== "..")
24
+ parts.pop();
25
+ continue;
26
+ }
27
+ parts.push(part);
28
+ }
29
+ return parts.join("/") || ".";
30
+ }
31
+ /**
32
+ * Converts common TypeScript/Graft (`L1C1-L1C4`), SCIP (`0:0-0:3`), and
33
+ * packed SCIP ranges to the same one-based representation.
34
+ */
35
+ export function normalizeSourceRange(value, providerId = "", rangeBase) {
36
+ if (value === undefined)
37
+ return undefined;
38
+ if (isNumberArray(value)) {
39
+ if (value.length !== 3 && value.length !== 4)
40
+ return undefined;
41
+ const numbers = value.map((part) => Number(part));
42
+ if (numbers.some((part) => !Number.isInteger(part) || part < 0))
43
+ return undefined;
44
+ const zeroBased = (rangeBase ?? defaultRangeBase(providerId)) === "zero-based";
45
+ const [startLine, startColumn, endLine, endColumn] = numbers.length === 3 ? [numbers[0], numbers[1], numbers[0], numbers[2]] : numbers;
46
+ return makeRange(startLine, startColumn, endLine, endColumn, zeroBased);
47
+ }
48
+ if (typeof value === "object") {
49
+ if (!Number.isInteger(value.startLine) ||
50
+ value.startLine < 1 ||
51
+ !Number.isInteger(value.endLine) ||
52
+ value.endLine < 1 ||
53
+ (value.startColumn !== undefined && (!Number.isInteger(value.startColumn) || value.startColumn < 1)) ||
54
+ (value.endColumn !== undefined && (!Number.isInteger(value.endColumn) || value.endColumn < 1))) {
55
+ return undefined;
56
+ }
57
+ return { ...value };
58
+ }
59
+ const text = value.trim();
60
+ const labelled = /^L(\d+)(?:C(\d+))?(?:-L?(\d+)(?:C(\d+))?)?$/iu.exec(text);
61
+ if (labelled) {
62
+ const startLine = Number(labelled[1]);
63
+ const startColumn = labelled[2] === undefined ? undefined : Number(labelled[2]);
64
+ const endLine = labelled[3] === undefined ? startLine : Number(labelled[3]);
65
+ const endColumn = labelled[4] === undefined ? undefined : Number(labelled[4]);
66
+ return makeRange(startLine, startColumn, endLine, endColumn, false);
67
+ }
68
+ const numeric = /^(\d+):(\d+)-(\d+):(\d+)$/u.exec(text);
69
+ if (numeric) {
70
+ const zeroBased = (rangeBase ?? defaultRangeBase(providerId)) === "zero-based";
71
+ return makeRange(Number(numeric[1]), Number(numeric[2]), Number(numeric[3]), Number(numeric[4]), zeroBased);
72
+ }
73
+ return undefined;
74
+ }
75
+ function defaultRangeBase(providerId) {
76
+ return providerId === "scip-typescript" || providerId === "scip" ? "zero-based" : "one-based";
77
+ }
78
+ function makeRange(startLine, startColumn, endLine, endColumn, zeroBased) {
79
+ const lineOffset = zeroBased ? 1 : 0;
80
+ const columnOffset = zeroBased ? 1 : 0;
81
+ const normalized = {
82
+ startLine: startLine + lineOffset,
83
+ endLine: endLine + lineOffset,
84
+ ...(startColumn === undefined ? {} : { startColumn: startColumn + columnOffset }),
85
+ ...(endColumn === undefined ? {} : { endColumn: endColumn + columnOffset }),
86
+ };
87
+ if (normalized.startLine > normalized.endLine)
88
+ return undefined;
89
+ if (normalized.startLine === normalized.endLine &&
90
+ normalized.startColumn !== undefined &&
91
+ normalized.endColumn !== undefined &&
92
+ normalized.startColumn > normalized.endColumn) {
93
+ return undefined;
94
+ }
95
+ return normalized;
96
+ }
97
+ /** Correlates provider-native entities using only deterministic local rules. */
98
+ export function correlateProviderEntities(inputs) {
99
+ const entities = normalizeEntities(inputs);
100
+ const candidateBuild = buildCandidateEdges(entities);
101
+ const edges = candidateBuild.edges;
102
+ const edgeIndex = indexEdges(edges, entities);
103
+ const groups = buildGroups(entities, edges);
104
+ const entityGroup = new Map();
105
+ for (const group of groups) {
106
+ for (const index of group.indices)
107
+ entityGroup.set(index, group);
108
+ }
109
+ const canonicalIds = new Map();
110
+ for (const group of groups) {
111
+ canonicalIds.set(group, makeCanonicalId(group, entities));
112
+ }
113
+ const links = edges.map((edge) => toCorrelationLink(edge, entities, edgeIndex)).sort(compareLinks);
114
+ const canonicalEntities = groups
115
+ .map((group) => {
116
+ const canonicalId = canonicalIds.get(group);
117
+ if (canonicalId === undefined)
118
+ throw new Error("internal correlation group has no identity");
119
+ return toCanonicalEntity(group, canonicalId, entities, edgeIndex, entityGroup, canonicalIds);
120
+ })
121
+ .sort((left, right) => left.canonicalId.localeCompare(right.canonicalId));
122
+ return {
123
+ canonicalEntities,
124
+ links,
125
+ metrics: buildMetrics(entities, edgeIndex, groups, entityGroup, canonicalIds),
126
+ diagnostics: candidateBuild.diagnostics,
127
+ };
128
+ }
129
+ /** Short alias for callers that already have provider entities. */
130
+ export const correlateEntities = correlateProviderEntities;
131
+ /** Extracts definition/structural entities from the existing Observation envelope. */
132
+ export function providerEntitiesFromObservations(observations) {
133
+ return extractProviderEntities(observations, true);
134
+ }
135
+ /**
136
+ * Extracts every endpoint represented by observations. Definitions are still
137
+ * preferred by the fact normalizer, but relation-only endpoints (for example
138
+ * call sites) must remain available when no definition observation exists.
139
+ */
140
+ export function providerEntitiesFromAllObservations(observations) {
141
+ return extractProviderEntities(observations, false);
142
+ }
143
+ function extractProviderEntities(observations, preferDefinitions) {
144
+ const byProvider = new Map();
145
+ for (const observation of observations) {
146
+ const key = providerKey(observation.provider);
147
+ const list = byProvider.get(key) ?? [];
148
+ list.push(observation);
149
+ byProvider.set(key, list);
150
+ }
151
+ const entities = [];
152
+ for (const providerObservations of byProvider.values()) {
153
+ const hasDefinitions = providerObservations.some((observation) => observation.predicate === "defines");
154
+ for (const observation of providerObservations) {
155
+ if (preferDefinitions && hasDefinitions && observation.predicate !== "defines")
156
+ continue;
157
+ const endpoints = [];
158
+ if (observation.predicate === "defines") {
159
+ if (isObservationEntity(observation.object)) {
160
+ endpoints.push(observation.object);
161
+ }
162
+ else {
163
+ endpoints.push(observation.subject);
164
+ }
165
+ }
166
+ else {
167
+ endpoints.push(observation.subject);
168
+ if (isObservationEntity(observation.object))
169
+ endpoints.push(observation.object);
170
+ }
171
+ for (const endpoint of endpoints) {
172
+ entities.push(toProviderEntityInput(observation, endpoint));
173
+ }
174
+ }
175
+ }
176
+ return entities;
177
+ }
178
+ /** Correlates the provider-native entities represented by observations. */
179
+ export function correlateObservations(observations) {
180
+ return correlateProviderEntities(providerEntitiesFromObservations(observations));
181
+ }
182
+ function toProviderEntityInput(observation, endpoint) {
183
+ const native = asRecord(observation.providerNative);
184
+ const nativeNode = asRecord(native?.node);
185
+ const sourceNode = asRecord(native?.sourceNode);
186
+ const targetNode = asRecord(native?.targetNode);
187
+ const endpointNode = [sourceNode, targetNode, nativeNode].find((candidate) => stringValue(candidate?.id) === endpoint.id);
188
+ const nativeRecord = endpointNode ?? nativeNode ?? native;
189
+ const nativeId = stringValue(nativeRecord?.id);
190
+ const endpointIsNode = nativeId === endpoint.id;
191
+ const pathValue = (endpointIsNode ? stringValue(nativeRecord?.path) : undefined) ?? observation.source.path;
192
+ const spanValue = (endpointIsNode ? stringValue(nativeRecord?.span) : undefined) ?? observation.source.span;
193
+ const nativeName = endpointIsNode ? stringValue(nativeRecord?.name) : undefined;
194
+ const nativeQualifiedName = endpointIsNode ? stringValue(nativeRecord?.qualifiedName) : undefined;
195
+ const nativeSignature = endpointIsNode
196
+ ? (stringValue(nativeRecord?.signature) ?? stringValue(nativeRecord?.type))
197
+ : undefined;
198
+ const nativeAliases = endpointIsNode ? stringArray(nativeRecord?.aliases) : [];
199
+ const inferredName = nativeName ?? lastSymbolPart(nativeQualifiedName ?? endpoint.id);
200
+ return {
201
+ provider: observation.provider,
202
+ repository: observation.repository,
203
+ id: endpoint.id,
204
+ kind: endpoint.kind,
205
+ path: pathValue,
206
+ span: spanValue,
207
+ name: inferredName,
208
+ qualifiedName: nativeQualifiedName ?? endpoint.id,
209
+ signature: nativeSignature,
210
+ aliases: nativeAliases,
211
+ providerNative: observation.providerNative,
212
+ };
213
+ }
214
+ function isObservationEntity(value) {
215
+ const record = asRecord(value);
216
+ return record !== undefined && typeof record.id === "string" && typeof record.kind === "string";
217
+ }
218
+ function normalizeEntities(inputs) {
219
+ const normalized = inputs.map((input) => {
220
+ const pathValue = input.path ?? input.source?.path;
221
+ const spanValue = input.range ?? input.span ?? input.source?.span;
222
+ const pathNormalized = pathValue === undefined ? undefined : normalizeRepositoryPath(pathValue);
223
+ const rangeNormalized = normalizeSourceRange(spanValue, input.provider.id, input.rangeBase);
224
+ const aliases = uniqueSorted((input.aliases ?? []).map((alias) => alias.trim()).filter((alias) => alias.length > 0));
225
+ const qualifiedName = cleanOptional(input.qualifiedName);
226
+ const name = cleanOptional(input.name);
227
+ const signature = cleanOptional(input.signature);
228
+ const qualifiedKeys = uniqueSorted(qualifiedName === undefined ? [] : symbolKeys(qualifiedName));
229
+ const aliasKeys = uniqueSorted(aliases.flatMap((alias) => symbolKeys(alias)));
230
+ const nameKeys = uniqueSorted([
231
+ ...(name === undefined ? [] : symbolKeys(name)),
232
+ ...(qualifiedName === undefined ? [] : symbolKeys(qualifiedName)),
233
+ ...aliasKeys,
234
+ ]);
235
+ const signatureKey = signature === undefined ? undefined : normalizeSignature(signature);
236
+ const kind = cleanOptional(input.kind) ?? "unknown";
237
+ const kindFamily = kindFamilyOf(kind);
238
+ const providerKeyValue = providerKey(input.provider);
239
+ const repositoryKeyValue = repositoryKey(input.repository);
240
+ const id = input.id.trim();
241
+ const key = stableSerialize({
242
+ provider: providerKeyValue,
243
+ repository: repositoryKeyValue,
244
+ id,
245
+ kind: kindFamily,
246
+ path: pathNormalized,
247
+ range: rangeNormalized,
248
+ name: nameKeys,
249
+ qualifiedName: qualifiedKeys,
250
+ aliases: aliasKeys,
251
+ signature: signatureKey,
252
+ });
253
+ return {
254
+ input,
255
+ providerKey: providerKeyValue,
256
+ repositoryKey: repositoryKeyValue,
257
+ id,
258
+ kind,
259
+ kindFamily,
260
+ path: pathNormalized,
261
+ range: rangeNormalized,
262
+ name,
263
+ qualifiedName,
264
+ signature,
265
+ aliases,
266
+ qualifiedKeys,
267
+ aliasKeys,
268
+ nameKeys,
269
+ signatureKey,
270
+ key,
271
+ };
272
+ });
273
+ normalized.sort(compareEntities);
274
+ const deduplicated = [];
275
+ for (const entity of normalized) {
276
+ const previous = deduplicated[deduplicated.length - 1];
277
+ if (previous?.key !== entity.key) {
278
+ deduplicated.push(entity);
279
+ continue;
280
+ }
281
+ // The provider-native payload is not part of identity. Choosing the
282
+ // lexicographically first payload keeps duplicate observations stable.
283
+ if (stableSerialize(entity.input.providerNative) < stableSerialize(previous.input.providerNative)) {
284
+ deduplicated[deduplicated.length - 1] = entity;
285
+ }
286
+ }
287
+ return deduplicated;
288
+ }
289
+ function compareEntities(left, right) {
290
+ const keyCompare = left.key.localeCompare(right.key);
291
+ if (keyCompare !== 0)
292
+ return keyCompare;
293
+ return stableSerialize(left.input.providerNative).localeCompare(stableSerialize(right.input.providerNative));
294
+ }
295
+ function buildCandidateEdges(entities) {
296
+ const buckets = new Map();
297
+ for (let index = 0; index < entities.length; index += 1) {
298
+ for (const key of candidateKeys(entities[index])) {
299
+ const bucket = buckets.get(key) ?? [];
300
+ bucket.push(index);
301
+ buckets.set(key, bucket);
302
+ }
303
+ }
304
+ const pairKeys = new Set();
305
+ const skippedByClass = new Map();
306
+ let indexedKeyCount = 0;
307
+ let globalPairCount = 0;
308
+ let globalCapExceeded = false;
309
+ for (const [key, bucket] of buckets.entries()) {
310
+ const potentialPairCount = (bucket.length * (bucket.length - 1)) / 2;
311
+ if (potentialPairCount > MAX_CANDIDATE_PAIRS_PER_KEY) {
312
+ const keyClass = key.split(":", 1)[0] ?? "unknown";
313
+ const current = skippedByClass.get(keyClass) ?? { keyCount: 0, potentialPairCount: 0 };
314
+ current.keyCount += 1;
315
+ current.potentialPairCount += potentialPairCount;
316
+ skippedByClass.set(keyClass, current);
317
+ continue;
318
+ }
319
+ indexedKeyCount += 1;
320
+ for (let left = 0; left < bucket.length; left += 1) {
321
+ for (let right = left + 1; right < bucket.length; right += 1) {
322
+ if (globalCapExceeded)
323
+ continue;
324
+ const first = bucket[left];
325
+ const second = bucket[right];
326
+ const low = Math.min(first, second);
327
+ const high = Math.max(first, second);
328
+ const pairKey = `${low}\u0000${high}`;
329
+ if (!pairKeys.has(pairKey)) {
330
+ if (globalPairCount >= MAX_CANDIDATE_PAIRS_PER_KEY) {
331
+ globalCapExceeded = true;
332
+ const keyClass = key.split(":", 1)[0] ?? "unknown";
333
+ const current = skippedByClass.get(keyClass) ?? { keyCount: 0, potentialPairCount: 0 };
334
+ current.keyCount += 1;
335
+ current.potentialPairCount += potentialPairCount;
336
+ skippedByClass.set(keyClass, current);
337
+ continue;
338
+ }
339
+ pairKeys.add(pairKey);
340
+ globalPairCount += 1;
341
+ }
342
+ }
343
+ }
344
+ }
345
+ const edges = [];
346
+ for (const pairKey of pairKeys) {
347
+ const separator = pairKey.indexOf("\u0000");
348
+ const left = Number(pairKey.slice(0, separator));
349
+ const right = Number(pairKey.slice(separator + 1));
350
+ const edge = compareEntityPair(left, right, entities[left], entities[right]);
351
+ if (edge !== undefined)
352
+ edges.push(edge);
353
+ }
354
+ const skippedKeys = [...skippedByClass.entries()]
355
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
356
+ .map(([keyClass, value]) => ({ keyClass, ...value }));
357
+ return {
358
+ edges: edges.sort(compareEdges),
359
+ diagnostics: {
360
+ maxCandidatePairsPerKey: MAX_CANDIDATE_PAIRS_PER_KEY,
361
+ indexedKeyCount,
362
+ skippedKeyCount: skippedKeys.reduce((count, item) => count + item.keyCount, 0),
363
+ skippedPotentialPairCount: skippedKeys.reduce((count, item) => count + item.potentialPairCount, 0),
364
+ skippedKeys,
365
+ },
366
+ };
367
+ }
368
+ /**
369
+ * Candidate generation is indexed by every rule that can make
370
+ * `compareEntityPair` return a score. The final pair comparison remains the
371
+ * authority, so these keys only avoid evaluating impossible pairs.
372
+ */
373
+ function candidateKeys(entity) {
374
+ const keys = new Set();
375
+ if (entity.path !== undefined && entity.range !== undefined) {
376
+ keys.add(`path-range:${entity.path}:${stableSerialize(entity.range)}`);
377
+ }
378
+ if (entity.path !== undefined && entity.signatureKey !== undefined) {
379
+ keys.add(`path-signature:${entity.path}:${entity.signatureKey}`);
380
+ }
381
+ if (entity.signatureKey !== undefined)
382
+ keys.add(`signature:${entity.signatureKey}`);
383
+ if (entity.qualifiedName !== undefined) {
384
+ keys.add(`qualified-exact:${entity.qualifiedName}`);
385
+ keys.add(`alias-match-exact:${entity.qualifiedName}`);
386
+ }
387
+ for (const qualifiedKey of entity.qualifiedKeys) {
388
+ keys.add(`qualified-normalized:${qualifiedKey}`);
389
+ }
390
+ for (const alias of entity.aliases)
391
+ keys.add(`alias-match-exact:${alias}`);
392
+ for (const aliasKey of entity.aliasKeys) {
393
+ keys.add(`alias-normalized:${aliasKey}`);
394
+ }
395
+ for (const nameKey of entity.nameKeys) {
396
+ if (entity.path !== undefined) {
397
+ keys.add(`path-name-normalized:${entity.path}:${nameKey}`);
398
+ }
399
+ }
400
+ // A bare token such as "type" or "0" is not a useful cross-file
401
+ // candidate key. Exact names remain useful when the source path also
402
+ // agrees, while the final pair comparison still computes all rules.
403
+ if (entity.path !== undefined && entity.name !== undefined) {
404
+ keys.add(`path-name-exact:${entity.path}:${entity.name}`);
405
+ }
406
+ if (entity.path !== undefined &&
407
+ entity.name === undefined &&
408
+ entity.qualifiedName === undefined &&
409
+ entity.aliases.length === 0) {
410
+ keys.add(`path-kind:${entity.path}:${entity.kindFamily}`);
411
+ }
412
+ return [...keys].sort();
413
+ }
414
+ function indexEdges(edges, entities) {
415
+ const incident = new Map();
416
+ const targets = new Map();
417
+ const addIncident = (index, edge) => {
418
+ const values = incident.get(index) ?? [];
419
+ values.push(edge);
420
+ incident.set(index, values);
421
+ };
422
+ const addTarget = (index, provider, target) => {
423
+ const byProvider = targets.get(index) ?? new Map();
424
+ const values = byProvider.get(provider) ?? new Set();
425
+ values.add(target);
426
+ byProvider.set(provider, values);
427
+ targets.set(index, byProvider);
428
+ };
429
+ for (const edge of edges) {
430
+ addIncident(edge.left, edge);
431
+ addIncident(edge.right, edge);
432
+ addTarget(edge.left, entities[edge.right].providerKey, edge.right);
433
+ addTarget(edge.right, entities[edge.left].providerKey, edge.left);
434
+ }
435
+ for (const values of incident.values())
436
+ values.sort(compareEdges);
437
+ return { incident, targets };
438
+ }
439
+ function uniqueEdges(edges) {
440
+ const byKey = new Map();
441
+ for (const edge of edges)
442
+ byKey.set(`${edge.left}\u0000${edge.right}`, edge);
443
+ return [...byKey.values()].sort(compareEdges);
444
+ }
445
+ function compareEntityPair(left, right, first, second) {
446
+ if (first.providerKey === second.providerKey || first.repositoryKey !== second.repositoryKey)
447
+ return undefined;
448
+ if (!kindCompatible(first.kindFamily, second.kindFamily))
449
+ return undefined;
450
+ const samePath = first.path !== undefined && first.path === second.path;
451
+ const sameRange = rangesEqual(first.range, second.range);
452
+ const sameKind = first.kindFamily === second.kindFamily;
453
+ const sameQualifiedName = intersects(first.qualifiedKeys, second.qualifiedKeys);
454
+ const sameAlias = intersects(first.aliasKeys, second.aliasKeys) ||
455
+ intersects(first.aliasKeys, second.qualifiedKeys) ||
456
+ intersects(first.qualifiedKeys, second.aliasKeys);
457
+ const sameName = intersects(first.nameKeys, second.nameKeys);
458
+ const sameSignature = first.signatureKey !== undefined && second.signatureKey !== undefined && first.signatureKey === second.signatureKey;
459
+ const rules = ["same-repository-revision"];
460
+ if (samePath)
461
+ rules.push("same-path");
462
+ if (sameRange)
463
+ rules.push("same-range");
464
+ if (sameKind)
465
+ rules.push("same-kind");
466
+ if (sameQualifiedName)
467
+ rules.push("same-qualified-name");
468
+ if (sameAlias)
469
+ rules.push("same-alias");
470
+ if (sameName)
471
+ rules.push("same-name");
472
+ if (sameSignature)
473
+ rules.push("same-signature");
474
+ let score;
475
+ if (samePath && sameRange) {
476
+ score = 100;
477
+ }
478
+ else if ((sameQualifiedName || sameAlias) && kindCompatible(first.kindFamily, second.kindFamily)) {
479
+ score = 86 + (samePath ? 4 : 0) + (sameSignature ? 3 : 0);
480
+ }
481
+ else if (samePath && sameSignature && kindCompatible(first.kindFamily, second.kindFamily)) {
482
+ score = 82;
483
+ }
484
+ else if (samePath && sameName && kindCompatible(first.kindFamily, second.kindFamily)) {
485
+ score = 68;
486
+ }
487
+ else if (sameSignature && kindCompatible(first.kindFamily, second.kindFamily)) {
488
+ score = 58;
489
+ }
490
+ else if (samePath && sameKind && first.nameKeys.length === 0 && second.nameKeys.length === 0) {
491
+ score = 45;
492
+ }
493
+ else if (sameName && kindCompatible(first.kindFamily, second.kindFamily)) {
494
+ score = 48;
495
+ }
496
+ if (score === undefined)
497
+ return undefined;
498
+ return {
499
+ left,
500
+ right,
501
+ score,
502
+ strength: score >= 80 ? "matched" : "probable",
503
+ rules: sortRules(rules),
504
+ };
505
+ }
506
+ function buildGroups(entities, edges) {
507
+ const assigned = new Set();
508
+ const blocked = new Set();
509
+ const groups = [];
510
+ // Consume evidence from strongest to weakest. This preserves overloads:
511
+ // exact declaration locations are grouped before a shared qualified name
512
+ // can connect every overload to every other overload.
513
+ const scores = [...new Set(edges.map((edge) => edge.score))].sort((left, right) => right - left);
514
+ for (const score of scores) {
515
+ const eligible = new Set();
516
+ for (let index = 0; index < entities.length; index += 1) {
517
+ if (!assigned.has(index) && !blocked.has(index))
518
+ eligible.add(index);
519
+ }
520
+ const scoreEdges = edges.filter((edge) => edge.score === score && eligible.has(edge.left) && eligible.has(edge.right));
521
+ for (const component of connectedComponents(entities.length, scoreEdges, eligible)) {
522
+ if (component.length < 2)
523
+ continue;
524
+ if (hasProviderCollision(component, entities)) {
525
+ for (const index of component)
526
+ blocked.add(index);
527
+ continue;
528
+ }
529
+ const group = { indices: component, strength: score >= 80 ? "matched" : "probable" };
530
+ groups.push(group);
531
+ for (const index of component)
532
+ assigned.add(index);
533
+ }
534
+ }
535
+ for (let index = 0; index < entities.length; index += 1) {
536
+ if (!assigned.has(index)) {
537
+ groups.push({ indices: [index], strength: "probable" });
538
+ }
539
+ }
540
+ return groups.sort((left, right) => entities[left.indices[0]].key.localeCompare(entities[right.indices[0]].key));
541
+ }
542
+ function connectedComponents(entityCount, edges, eligible) {
543
+ const adjacency = Array.from({ length: entityCount }, () => []);
544
+ for (const edge of edges) {
545
+ adjacency[edge.left].push(edge.right);
546
+ adjacency[edge.right].push(edge.left);
547
+ }
548
+ const visited = new Set();
549
+ const components = [];
550
+ for (const start of [...eligible].sort((left, right) => left - right)) {
551
+ if (visited.has(start) || adjacency[start].length === 0)
552
+ continue;
553
+ const stack = [start];
554
+ const component = [];
555
+ while (stack.length > 0) {
556
+ const current = stack.pop();
557
+ if (current === undefined || visited.has(current))
558
+ continue;
559
+ visited.add(current);
560
+ component.push(current);
561
+ for (const next of adjacency[current].slice().sort((left, right) => right - left)) {
562
+ if (!visited.has(next))
563
+ stack.push(next);
564
+ }
565
+ }
566
+ components.push(component.sort((left, right) => left - right));
567
+ }
568
+ return components.sort((left, right) => left[0] - right[0]);
569
+ }
570
+ function hasProviderCollision(indices, entities) {
571
+ const providers = new Set(indices.map((index) => entities[index].providerKey));
572
+ return providers.size !== indices.length;
573
+ }
574
+ function makeCanonicalId(group, entities) {
575
+ const repository = entities[group.indices[0]].repositoryKey;
576
+ const members = group.indices.map((index) => entities[index].key).sort();
577
+ const digest = createHash("sha256").update(stableSerialize({ repository, members })).digest("hex");
578
+ return `ce_${digest}`;
579
+ }
580
+ function toCanonicalEntity(group, canonicalId, entities, edgeIndex, entityGroup, canonicalIds) {
581
+ const members = group.indices.map((index) => toCanonicalMember(entities[index])).sort(compareMembers);
582
+ const groupIndices = new Set(group.indices);
583
+ const incidentEdges = uniqueEdges(group.indices.flatMap((index) => edgeIndex.incident.get(index) ?? []));
584
+ const internalEdges = incidentEdges.filter((edge) => groupIndices.has(edge.left) && groupIndices.has(edge.right));
585
+ const candidateCanonicalIds = uniqueSorted(incidentEdges
586
+ .flatMap((edge) => {
587
+ const other = groupIndices.has(edge.left) ? edge.right : edge.left;
588
+ const otherGroup = entityGroup.get(other);
589
+ const otherId = otherGroup === undefined ? undefined : canonicalIds.get(otherGroup);
590
+ return otherId === undefined || otherId === canonicalId ? [] : [otherId];
591
+ })
592
+ .filter((id) => id !== undefined));
593
+ const status = group.indices.length > 1
594
+ ? group.strength === "matched"
595
+ ? "matched"
596
+ : "probable"
597
+ : incidentEdges.length > 0
598
+ ? "ambiguous"
599
+ : "unmatched";
600
+ const cardinality = groupCardinality(group, incidentEdges, edgeIndex, entities, groupIndices);
601
+ const reason = status === "unmatched"
602
+ ? "no-cross-provider-evidence"
603
+ : status === "ambiguous"
604
+ ? cardinality === "one-to-many" || cardinality === "many-to-one" || cardinality === "many-to-many"
605
+ ? "provider-cardinality-conflict"
606
+ : "competing-evidence"
607
+ : status === "probable"
608
+ ? "probable-evidence"
609
+ : "deterministic-evidence";
610
+ const evidence = internalEdges.length > 0 ? internalEdges : incidentEdges;
611
+ const rules = sortRules(uniqueRules(evidence.flatMap((edge) => edge.rules)));
612
+ const repository = entities[group.indices[0]].input.repository;
613
+ return {
614
+ canonicalId,
615
+ identity: canonicalId,
616
+ repository,
617
+ kind: canonicalKind(group.indices.map((index) => entities[index].kindFamily)),
618
+ status,
619
+ members,
620
+ candidateCanonicalIds,
621
+ rationale: {
622
+ method: "deterministic-rules",
623
+ reason,
624
+ cardinality,
625
+ rules,
626
+ evidence: evidence.map((edge) => toEvidence(edge, entities)).sort(compareEvidence),
627
+ },
628
+ };
629
+ }
630
+ function toCanonicalMember(entity) {
631
+ return {
632
+ provider: entity.input.provider,
633
+ nativeId: entity.id,
634
+ kind: entity.kind,
635
+ canonicalKind: entity.kindFamily,
636
+ repository: entity.input.repository,
637
+ ...(entity.path === undefined ? {} : { path: entity.path }),
638
+ ...(entity.range === undefined ? {} : { range: entity.range }),
639
+ ...(entity.name === undefined ? {} : { name: entity.name }),
640
+ ...(entity.qualifiedName === undefined ? {} : { qualifiedName: entity.qualifiedName }),
641
+ ...(entity.signature === undefined ? {} : { signature: entity.signature }),
642
+ aliases: entity.aliases,
643
+ providerNative: entity.input.providerNative,
644
+ };
645
+ }
646
+ function toEvidence(edge, entities) {
647
+ return {
648
+ left: toReference(entities[edge.left]),
649
+ right: toReference(entities[edge.right]),
650
+ score: edge.score,
651
+ rules: edge.rules,
652
+ };
653
+ }
654
+ function toCorrelationLink(edge, entities, edgeIndex) {
655
+ return {
656
+ left: toReference(entities[edge.left]),
657
+ right: toReference(entities[edge.right]),
658
+ score: edge.score,
659
+ strength: edge.strength,
660
+ cardinality: edgeCardinality(edge, edgeIndex, entities),
661
+ rules: edge.rules,
662
+ };
663
+ }
664
+ function toReference(entity) {
665
+ return {
666
+ provider: entity.input.provider,
667
+ nativeId: entity.id,
668
+ kind: entity.kind,
669
+ ...(entity.path === undefined ? {} : { path: entity.path }),
670
+ ...(entity.range === undefined ? {} : { range: entity.range }),
671
+ };
672
+ }
673
+ function edgeCardinality(edge, edgeIndex, entities) {
674
+ const leftTargets = edgeIndex.targets.get(edge.left)?.get(entities[edge.right].providerKey) ?? new Set();
675
+ const rightTargets = edgeIndex.targets.get(edge.right)?.get(entities[edge.left].providerKey) ?? new Set();
676
+ if (leftTargets.size > 1 && rightTargets.size > 1)
677
+ return "many-to-many";
678
+ if (leftTargets.size > 1)
679
+ return "one-to-many";
680
+ if (rightTargets.size > 1)
681
+ return "many-to-one";
682
+ return "one-to-one";
683
+ }
684
+ function groupCardinality(group, incidentEdges, edgeIndex, entities, groupIndices) {
685
+ if (incidentEdges.length === 0)
686
+ return "unmatched";
687
+ let oneToMany = false;
688
+ let manyToOne = false;
689
+ let manyToMany = false;
690
+ for (const edge of incidentEdges) {
691
+ const cardinality = edgeCardinality(edge, edgeIndex, entities);
692
+ if (cardinality === "many-to-many")
693
+ manyToMany = true;
694
+ if (cardinality === "one-to-many") {
695
+ if (groupIndices.has(edge.left))
696
+ oneToMany = true;
697
+ if (groupIndices.has(edge.right))
698
+ manyToOne = true;
699
+ }
700
+ if (cardinality === "many-to-one") {
701
+ if (groupIndices.has(edge.left))
702
+ oneToMany = true;
703
+ if (groupIndices.has(edge.right))
704
+ manyToOne = true;
705
+ }
706
+ }
707
+ if (manyToMany || (oneToMany && manyToOne))
708
+ return "many-to-many";
709
+ if (oneToMany)
710
+ return "one-to-many";
711
+ if (manyToOne)
712
+ return "many-to-one";
713
+ return "one-to-one";
714
+ }
715
+ function buildMetrics(entities, edgeIndex, groups, entityGroup, canonicalIds) {
716
+ const providerEntries = new Map();
717
+ for (let index = 0; index < entities.length; index += 1) {
718
+ const entity = entities[index];
719
+ const key = entity.input.provider.id;
720
+ const entry = providerEntries.get(key) ?? { provider: entity.input.provider, indices: [] };
721
+ entry.indices.push(index);
722
+ providerEntries.set(key, entry);
723
+ }
724
+ const result = {};
725
+ for (const key of [...providerEntries.keys()].sort()) {
726
+ const entry = providerEntries.get(key);
727
+ if (entry === undefined)
728
+ continue;
729
+ const statusCounts = {
730
+ matched: 0,
731
+ probable: 0,
732
+ ambiguous: 0,
733
+ unmatched: 0,
734
+ };
735
+ const canonicalEntityIds = new Set();
736
+ let candidateCount = 0;
737
+ for (const index of entry.indices) {
738
+ const group = entityGroup.get(index);
739
+ if (group !== undefined) {
740
+ const canonicalId = canonicalIds.get(group);
741
+ if (canonicalId !== undefined)
742
+ canonicalEntityIds.add(canonicalId);
743
+ }
744
+ const incident = edgeIndex.incident.get(index) ?? [];
745
+ candidateCount += incident.length;
746
+ const status = group === undefined
747
+ ? "unmatched"
748
+ : group.indices.length > 1
749
+ ? group.strength
750
+ : incident.length > 0
751
+ ? "ambiguous"
752
+ : "unmatched";
753
+ statusCounts[status] += 1;
754
+ }
755
+ result[key] = {
756
+ provider: entry.provider,
757
+ total: entry.indices.length,
758
+ canonicalEntityCount: canonicalEntityIds.size,
759
+ candidateCount,
760
+ matched: statusCounts.matched,
761
+ probable: statusCounts.probable,
762
+ ambiguous: statusCounts.ambiguous,
763
+ unmatched: statusCounts.unmatched,
764
+ };
765
+ }
766
+ return result;
767
+ }
768
+ function compareLinks(left, right) {
769
+ const providerCompare = providerReferenceKey(left.left).localeCompare(providerReferenceKey(right.left));
770
+ if (providerCompare !== 0)
771
+ return providerCompare;
772
+ return providerReferenceKey(left.right).localeCompare(providerReferenceKey(right.right));
773
+ }
774
+ function compareEdges(left, right) {
775
+ if (left.left !== right.left)
776
+ return left.left - right.left;
777
+ if (left.right !== right.right)
778
+ return left.right - right.right;
779
+ return right.score - left.score;
780
+ }
781
+ function compareEvidence(left, right) {
782
+ const leftCompare = providerReferenceKey(left.left).localeCompare(providerReferenceKey(right.left));
783
+ if (leftCompare !== 0)
784
+ return leftCompare;
785
+ return providerReferenceKey(left.right).localeCompare(providerReferenceKey(right.right));
786
+ }
787
+ function compareMembers(left, right) {
788
+ return providerReferenceKey(left).localeCompare(providerReferenceKey(right));
789
+ }
790
+ function providerReferenceKey(reference) {
791
+ return stableSerialize({
792
+ provider: providerKey(reference.provider),
793
+ nativeId: reference.nativeId,
794
+ kind: reference.kind,
795
+ path: reference.path,
796
+ range: reference.range,
797
+ });
798
+ }
799
+ function providerKey(provider) {
800
+ return `${provider.id}\u0000${provider.version}\u0000${provider.determinism}`;
801
+ }
802
+ function repositoryKey(repository) {
803
+ return stableSerialize({ source: repository.source, commitSha: repository.commitSha });
804
+ }
805
+ function kindFamilyOf(kind) {
806
+ const normalized = kind.toLowerCase().replace(/[\s_-]/gu, "");
807
+ if (/(function|method|callable)/u.test(normalized))
808
+ return "callable";
809
+ if (/(class|constructor)/u.test(normalized))
810
+ return "class";
811
+ if (/(interface)/u.test(normalized))
812
+ return "interface";
813
+ if (/(typealias|typedef|type)/u.test(normalized))
814
+ return "type";
815
+ if (/(enum)/u.test(normalized))
816
+ return "enum";
817
+ if (/(variable|constant|const)/u.test(normalized))
818
+ return "variable";
819
+ if (/(property|field|member)/u.test(normalized))
820
+ return "property";
821
+ if (/(module|file|sourcefile)/u.test(normalized))
822
+ return "module";
823
+ if (/(symbol|declaration|unknown)/u.test(normalized))
824
+ return "symbol";
825
+ return normalized || "symbol";
826
+ }
827
+ function canonicalKind(kinds) {
828
+ const unique = uniqueSorted(kinds);
829
+ const specific = unique.filter((kind) => kind !== "symbol");
830
+ if (specific.length === 1)
831
+ return specific[0];
832
+ return unique.length === 1 ? unique[0] : "symbol";
833
+ }
834
+ function kindCompatible(left, right) {
835
+ return left === right || left === "symbol" || right === "symbol";
836
+ }
837
+ function rangesEqual(left, right) {
838
+ if (left === undefined || right === undefined)
839
+ return false;
840
+ return (left.startLine === right.startLine &&
841
+ left.startColumn === right.startColumn &&
842
+ left.endLine === right.endLine &&
843
+ left.endColumn === right.endColumn);
844
+ }
845
+ function normalizeSignature(value) {
846
+ return value
847
+ .trim()
848
+ .replace(/\s+/gu, " ")
849
+ .replace(/\s*([(),:;<>[\]{}|&=])\s*/gu, "$1");
850
+ }
851
+ function symbolKeys(value) {
852
+ const normalized = value.trim().replace(/["']/gu, "");
853
+ if (normalized.length === 0)
854
+ return [];
855
+ const keys = new Set([normalized]);
856
+ const parts = normalized.split(/[^A-Za-z0-9_$]+/u).filter((part) => part.length > 0);
857
+ for (const part of parts)
858
+ keys.add(part);
859
+ return [...keys];
860
+ }
861
+ function lastSymbolPart(value) {
862
+ const keys = symbolKeys(value);
863
+ return keys.length === 0 ? undefined : keys[keys.length - 1];
864
+ }
865
+ function intersects(left, right) {
866
+ const rightSet = new Set(right);
867
+ return left.some((value) => rightSet.has(value));
868
+ }
869
+ function sortRules(rules) {
870
+ const ruleSet = new Set(rules);
871
+ return RULE_ORDER.filter((rule) => ruleSet.has(rule));
872
+ }
873
+ function uniqueRules(rules) {
874
+ return [...new Set(rules)];
875
+ }
876
+ function uniqueSorted(values) {
877
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right));
878
+ }
879
+ function cleanOptional(value) {
880
+ const cleaned = value?.trim();
881
+ return cleaned === undefined || cleaned.length === 0 ? undefined : cleaned;
882
+ }
883
+ function stringValue(value) {
884
+ return typeof value === "string" && value.length > 0 ? value : undefined;
885
+ }
886
+ function stringArray(value) {
887
+ return Array.isArray(value) ? value.filter((part) => typeof part === "string") : [];
888
+ }
889
+ function isNumberArray(value) {
890
+ return Array.isArray(value);
891
+ }
892
+ function asRecord(value) {
893
+ return typeof value === "object" && value !== null && !Array.isArray(value)
894
+ ? value
895
+ : undefined;
896
+ }
897
+ function stableSerialize(value) {
898
+ if (value === null)
899
+ return "null";
900
+ if (value === undefined)
901
+ return "undefined";
902
+ if (typeof value === "string")
903
+ return JSON.stringify(value);
904
+ if (typeof value === "number" || typeof value === "boolean")
905
+ return JSON.stringify(value);
906
+ if (typeof value === "bigint")
907
+ return `${value.toString()}n`;
908
+ if (Array.isArray(value))
909
+ return `[${value.map((item) => stableSerialize(item)).join(",")}]`;
910
+ if (typeof value === "object") {
911
+ const record = value;
912
+ return `{${Object.keys(record)
913
+ .sort()
914
+ .map((key) => `${JSON.stringify(key)}:${stableSerialize(record[key])}`)
915
+ .join(",")}}`;
916
+ }
917
+ return JSON.stringify(String(value));
918
+ }
919
+ //# sourceMappingURL=correlation.js.map