@mortice/mapwright 0.8.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/CHANGELOG.md +161 -0
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/catalogue/canonicalisation.d.ts +36 -0
- package/dist/catalogue/canonicalisation.js +262 -0
- package/dist/catalogue/config.d.ts +13 -0
- package/dist/catalogue/config.js +77 -0
- package/dist/catalogue/deterministic.d.ts +3 -0
- package/dist/catalogue/deterministic.js +25 -0
- package/dist/catalogue/diagnostics.d.ts +2 -0
- package/dist/catalogue/diagnostics.js +34 -0
- package/dist/catalogue/evidence.d.ts +1 -0
- package/dist/catalogue/evidence.js +10 -0
- package/dist/catalogue/examples.d.ts +3 -0
- package/dist/catalogue/examples.js +20 -0
- package/dist/catalogue/identity.d.ts +61 -0
- package/dist/catalogue/identity.js +114 -0
- package/dist/catalogue/ignore.d.ts +4 -0
- package/dist/catalogue/ignore.js +29 -0
- package/dist/catalogue/indexer.d.ts +20 -0
- package/dist/catalogue/indexer.js +1393 -0
- package/dist/catalogue/linkage.d.ts +16 -0
- package/dist/catalogue/linkage.js +419 -0
- package/dist/catalogue/moduleResolution.d.ts +4 -0
- package/dist/catalogue/moduleResolution.js +67 -0
- package/dist/catalogue/ordering.d.ts +13 -0
- package/dist/catalogue/ordering.js +86 -0
- package/dist/catalogue/packages.d.ts +12 -0
- package/dist/catalogue/packages.js +475 -0
- package/dist/catalogue/pythonReExports.d.ts +7 -0
- package/dist/catalogue/pythonReExports.js +113 -0
- package/dist/catalogue/resolveRefs.d.ts +40 -0
- package/dist/catalogue/resolveRefs.js +284 -0
- package/dist/catalogue/scanner.d.ts +8 -0
- package/dist/catalogue/scanner.js +99 -0
- package/dist/catalogue/schema.d.ts +289 -0
- package/dist/catalogue/schema.js +1 -0
- package/dist/catalogue/store.d.ts +41 -0
- package/dist/catalogue/store.js +209 -0
- package/dist/catalogue/templates.d.ts +18 -0
- package/dist/catalogue/templates.js +158 -0
- package/dist/catalogue/version.d.ts +12 -0
- package/dist/catalogue/version.js +30 -0
- package/dist/cli/commands/adoptionPlan.d.ts +8 -0
- package/dist/cli/commands/adoptionPlan.js +24 -0
- package/dist/cli/commands/auditDocs.d.ts +8 -0
- package/dist/cli/commands/auditDocs.js +24 -0
- package/dist/cli/commands/changeImpact.d.ts +10 -0
- package/dist/cli/commands/changeImpact.js +36 -0
- package/dist/cli/commands/changes.d.ts +12 -0
- package/dist/cli/commands/changes.js +31 -0
- package/dist/cli/commands/check.d.ts +9 -0
- package/dist/cli/commands/check.js +4 -0
- package/dist/cli/commands/compatibility.d.ts +10 -0
- package/dist/cli/commands/compatibility.js +43 -0
- package/dist/cli/commands/diff.d.ts +31 -0
- package/dist/cli/commands/diff.js +118 -0
- package/dist/cli/commands/dogfood.d.ts +8 -0
- package/dist/cli/commands/dogfood.js +24 -0
- package/dist/cli/commands/drift.d.ts +8 -0
- package/dist/cli/commands/drift.js +13 -0
- package/dist/cli/commands/index.d.ts +9 -0
- package/dist/cli/commands/index.js +9 -0
- package/dist/cli/commands/indexWorkspace.d.ts +8 -0
- package/dist/cli/commands/indexWorkspace.js +23 -0
- package/dist/cli/commands/recommendConfig.d.ts +7 -0
- package/dist/cli/commands/recommendConfig.js +20 -0
- package/dist/cli/commands/schema.d.ts +1 -0
- package/dist/cli/commands/schema.js +37 -0
- package/dist/cli/commands/template.d.ts +5 -0
- package/dist/cli/commands/template.js +44 -0
- package/dist/cli/commands/validate.d.ts +9 -0
- package/dist/cli/commands/validate.js +85 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +211 -0
- package/dist/comparison/artefactValidate.d.ts +19 -0
- package/dist/comparison/artefactValidate.js +334 -0
- package/dist/comparison/candidates.d.ts +12 -0
- package/dist/comparison/candidates.js +391 -0
- package/dist/comparison/compare.d.ts +15 -0
- package/dist/comparison/compare.js +240 -0
- package/dist/comparison/compatibility.d.ts +26 -0
- package/dist/comparison/compatibility.js +316 -0
- package/dist/comparison/drift.d.ts +34 -0
- package/dist/comparison/drift.js +71 -0
- package/dist/comparison/identity.d.ts +30 -0
- package/dist/comparison/identity.js +73 -0
- package/dist/comparison/impact.d.ts +14 -0
- package/dist/comparison/impact.js +291 -0
- package/dist/comparison/index.d.ts +6 -0
- package/dist/comparison/index.js +6 -0
- package/dist/comparison/integrity.d.ts +12 -0
- package/dist/comparison/integrity.js +199 -0
- package/dist/comparison/ordering.d.ts +4 -0
- package/dist/comparison/ordering.js +69 -0
- package/dist/comparison/projections.d.ts +19 -0
- package/dist/comparison/projections.js +185 -0
- package/dist/comparison/relationshipPolicy.d.ts +19 -0
- package/dist/comparison/relationshipPolicy.js +98 -0
- package/dist/comparison/schema.d.ts +178 -0
- package/dist/comparison/schema.js +16 -0
- package/dist/comparison/store.d.ts +58 -0
- package/dist/comparison/store.js +462 -0
- package/dist/comparison/validate.d.ts +23 -0
- package/dist/comparison/validate.js +137 -0
- package/dist/comparison/version.d.ts +9 -0
- package/dist/comparison/version.js +11 -0
- package/dist/extractors/docTags.d.ts +9 -0
- package/dist/extractors/docTags.js +106 -0
- package/dist/extractors/generic.d.ts +5 -0
- package/dist/extractors/generic.js +3 -0
- package/dist/extractors/javascript.d.ts +3 -0
- package/dist/extractors/javascript.js +4 -0
- package/dist/extractors/markdown.d.ts +7 -0
- package/dist/extractors/markdown.js +269 -0
- package/dist/extractors/python.d.ts +7 -0
- package/dist/extractors/python.js +314 -0
- package/dist/extractors/typescript.d.ts +7 -0
- package/dist/extractors/typescript.js +387 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp/resources.d.ts +18 -0
- package/dist/mcp/resources.js +57 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +95 -0
- package/dist/mcp/tools.d.ts +29 -0
- package/dist/mcp/tools.js +902 -0
- package/dist/queries/adoption.d.ts +62 -0
- package/dist/queries/adoption.js +366 -0
- package/dist/queries/capabilities.d.ts +45 -0
- package/dist/queries/capabilities.js +124 -0
- package/dist/queries/changes.d.ts +33 -0
- package/dist/queries/changes.js +53 -0
- package/dist/queries/dependencies.d.ts +17 -0
- package/dist/queries/dependencies.js +94 -0
- package/dist/queries/dogfood.d.ts +50 -0
- package/dist/queries/dogfood.js +116 -0
- package/dist/queries/editingContext.d.ts +84 -0
- package/dist/queries/editingContext.js +240 -0
- package/dist/queries/examples.d.ts +8 -0
- package/dist/queries/examples.js +16 -0
- package/dist/queries/files.d.ts +8 -0
- package/dist/queries/files.js +14 -0
- package/dist/queries/lists.d.ts +94 -0
- package/dist/queries/lists.js +184 -0
- package/dist/queries/overview.d.ts +2 -0
- package/dist/queries/overview.js +8 -0
- package/dist/queries/publicApis.d.ts +60 -0
- package/dist/queries/publicApis.js +120 -0
- package/dist/queries/ranking.d.ts +67 -0
- package/dist/queries/ranking.js +387 -0
- package/dist/queries/recommendConfig.d.ts +47 -0
- package/dist/queries/recommendConfig.js +123 -0
- package/dist/queries/relationships.d.ts +5 -0
- package/dist/queries/relationships.js +15 -0
- package/dist/queries/reuseCandidates.d.ts +34 -0
- package/dist/queries/reuseCandidates.js +181 -0
- package/dist/queries/symbols.d.ts +40 -0
- package/dist/queries/symbols.js +41 -0
- package/dist/security/pathPolicy.d.ts +12 -0
- package/dist/security/pathPolicy.js +46 -0
- package/dist/security/secretPatterns.d.ts +1 -0
- package/dist/security/secretPatterns.js +4 -0
- package/dist/security/sourceAccess.d.ts +12 -0
- package/dist/security/sourceAccess.js +35 -0
- package/package.json +60 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { changeIdentityId } from "./identity.js";
|
|
2
|
+
import { indexDomain } from "./projections.js";
|
|
3
|
+
const GLOBAL_BUDGET = 50_000;
|
|
4
|
+
const MAX_PER_REMOVED = 5;
|
|
5
|
+
function symbolFields(rec) {
|
|
6
|
+
if (!rec)
|
|
7
|
+
return {};
|
|
8
|
+
const doc = rec.doc;
|
|
9
|
+
return {
|
|
10
|
+
language: rec.language,
|
|
11
|
+
kind: rec.kind,
|
|
12
|
+
name: rec.name,
|
|
13
|
+
packageId: rec.packageId,
|
|
14
|
+
modulePath: rec.modulePath,
|
|
15
|
+
qualifiedName: rec.qualifiedName,
|
|
16
|
+
qualifiedContainerPath: rec.qualifiedContainerPath,
|
|
17
|
+
signatureFingerprint: rec.signatureFingerprint,
|
|
18
|
+
implementationFingerprint: rec.implementationFingerprint,
|
|
19
|
+
docFp: doc?.text ? `doc:${doc.text.length}:${doc.text.slice(0, 32)}` : undefined,
|
|
20
|
+
capabilities: Array.isArray(rec.capabilities) ? rec.capabilities : undefined
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function topologyBucket(modulePath) {
|
|
24
|
+
if (!modulePath)
|
|
25
|
+
return "";
|
|
26
|
+
const parts = modulePath.replaceAll("\\", "/").split("/");
|
|
27
|
+
if (parts.includes("src"))
|
|
28
|
+
return "src";
|
|
29
|
+
if (parts.includes("dist") || parts.includes("build") || parts.includes("out"))
|
|
30
|
+
return "generated";
|
|
31
|
+
return parts.slice(0, -1).join("/") || "root";
|
|
32
|
+
}
|
|
33
|
+
function scorePair(categories) {
|
|
34
|
+
let score = 0;
|
|
35
|
+
if (categories.has("signature_fingerprint"))
|
|
36
|
+
score += 40;
|
|
37
|
+
if (categories.has("implementation_fingerprint"))
|
|
38
|
+
score += 30;
|
|
39
|
+
if (categories.has("language_kind"))
|
|
40
|
+
score += 10;
|
|
41
|
+
if (categories.has("package_continuity"))
|
|
42
|
+
score += 10;
|
|
43
|
+
if (categories.has("module_topology"))
|
|
44
|
+
score += 8;
|
|
45
|
+
if (categories.has("qualified_container"))
|
|
46
|
+
score += 8;
|
|
47
|
+
if (categories.has("public_export_target"))
|
|
48
|
+
score += 12;
|
|
49
|
+
if (categories.has("relationship_neighbourhood"))
|
|
50
|
+
score += 10;
|
|
51
|
+
if (categories.has("linkage"))
|
|
52
|
+
score += 6;
|
|
53
|
+
if (categories.has("documentation_fingerprint"))
|
|
54
|
+
score += 4;
|
|
55
|
+
if (categories.has("exported_name"))
|
|
56
|
+
score += 5;
|
|
57
|
+
if (categories.has("import_path"))
|
|
58
|
+
score += 5;
|
|
59
|
+
return score;
|
|
60
|
+
}
|
|
61
|
+
function sufficientEvidence(categories) {
|
|
62
|
+
if (categories.size < 2)
|
|
63
|
+
return false;
|
|
64
|
+
// public surface: package + (name or import path)
|
|
65
|
+
if (categories.has("package_continuity") && (categories.has("exported_name") || categories.has("import_path"))) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
// package+name alone insufficient for symbol declaration matching
|
|
69
|
+
const onlyWeak = [...categories].every((c) => ["package_continuity", "exported_name", "basename", "documentation_fingerprint"].includes(c)) && !categories.has("signature_fingerprint") && !categories.has("implementation_fingerprint");
|
|
70
|
+
if (onlyWeak)
|
|
71
|
+
return false;
|
|
72
|
+
// module path + name alone insufficient
|
|
73
|
+
if (categories.size === 2 &&
|
|
74
|
+
categories.has("module_topology") &&
|
|
75
|
+
(categories.has("exported_name") || categories.has("basename"))) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
// one fingerprint + one structural signal OK when uniqueness proven later
|
|
79
|
+
if (categories.has("signature_fingerprint") || categories.has("implementation_fingerprint")) {
|
|
80
|
+
return categories.size >= 2;
|
|
81
|
+
}
|
|
82
|
+
// need ≥2 independent strong-ish categories including language_kind + package + something
|
|
83
|
+
return (categories.has("language_kind") &&
|
|
84
|
+
categories.has("package_continuity") &&
|
|
85
|
+
(categories.has("qualified_container") ||
|
|
86
|
+
categories.has("public_export_target") ||
|
|
87
|
+
categories.has("relationship_neighbourhood") ||
|
|
88
|
+
categories.has("linkage")));
|
|
89
|
+
}
|
|
90
|
+
function classifyKind(rem, add, reasons) {
|
|
91
|
+
const pathChanged = rem.modulePath !== add.modulePath ||
|
|
92
|
+
(rem.importPath != null && add.importPath != null && rem.importPath !== add.importPath);
|
|
93
|
+
const nameChanged = rem.name !== add.name;
|
|
94
|
+
if (pathChanged && nameChanged)
|
|
95
|
+
return "probable_move_and_rename";
|
|
96
|
+
if (pathChanged && !nameChanged)
|
|
97
|
+
return "probable_move";
|
|
98
|
+
if (!pathChanged && nameChanged)
|
|
99
|
+
return "probable_rename";
|
|
100
|
+
if (reasons.includes("exported_name_match") || reasons.includes("import_path_match")) {
|
|
101
|
+
return "public_surface_transition";
|
|
102
|
+
}
|
|
103
|
+
return "possible_replacement";
|
|
104
|
+
}
|
|
105
|
+
function collectDeclarationPairs(comparisonId, changes, base, head, budget, warnings) {
|
|
106
|
+
const removed = changes.filter((c) => c.domain === "symbols" && c.status === "removed");
|
|
107
|
+
const added = changes.filter((c) => c.domain === "symbols" && c.status === "added");
|
|
108
|
+
const baseIdx = indexDomain("symbols", base);
|
|
109
|
+
const headIdx = indexDomain("symbols", head);
|
|
110
|
+
const pairs = [];
|
|
111
|
+
for (const rem of removed) {
|
|
112
|
+
const baseRec = baseIdx.get(rem.baseRecordId ?? "");
|
|
113
|
+
const bf = symbolFields(baseRec);
|
|
114
|
+
for (const add of added) {
|
|
115
|
+
if (budget.remaining-- <= 0) {
|
|
116
|
+
warnings.push("candidate_work_budget_exhausted");
|
|
117
|
+
return pairs;
|
|
118
|
+
}
|
|
119
|
+
const headRec = headIdx.get(add.headRecordId ?? "");
|
|
120
|
+
const hf = symbolFields(headRec);
|
|
121
|
+
if (bf.language && hf.language && bf.language !== hf.language)
|
|
122
|
+
continue;
|
|
123
|
+
if (bf.kind && hf.kind && bf.kind !== hf.kind)
|
|
124
|
+
continue;
|
|
125
|
+
const categories = new Set();
|
|
126
|
+
const reasons = [];
|
|
127
|
+
const facets = [];
|
|
128
|
+
if (bf.language && hf.language && bf.kind && hf.kind && bf.language === hf.language && bf.kind === hf.kind) {
|
|
129
|
+
categories.add("language_kind");
|
|
130
|
+
reasons.push("language_kind_match");
|
|
131
|
+
}
|
|
132
|
+
if (bf.packageId && hf.packageId && bf.packageId === hf.packageId) {
|
|
133
|
+
categories.add("package_continuity");
|
|
134
|
+
reasons.push("package_continuity");
|
|
135
|
+
}
|
|
136
|
+
if (bf.name && hf.name && bf.name === hf.name) {
|
|
137
|
+
reasons.push("declaration_name_match");
|
|
138
|
+
}
|
|
139
|
+
if (bf.signatureFingerprint &&
|
|
140
|
+
hf.signatureFingerprint &&
|
|
141
|
+
bf.signatureFingerprint === hf.signatureFingerprint) {
|
|
142
|
+
categories.add("signature_fingerprint");
|
|
143
|
+
reasons.push("signature_fingerprint_match");
|
|
144
|
+
}
|
|
145
|
+
if (bf.implementationFingerprint &&
|
|
146
|
+
hf.implementationFingerprint &&
|
|
147
|
+
bf.implementationFingerprint === hf.implementationFingerprint) {
|
|
148
|
+
categories.add("implementation_fingerprint");
|
|
149
|
+
reasons.push("implementation_fingerprint_match");
|
|
150
|
+
}
|
|
151
|
+
if (bf.qualifiedContainerPath != null &&
|
|
152
|
+
hf.qualifiedContainerPath != null &&
|
|
153
|
+
bf.qualifiedContainerPath === hf.qualifiedContainerPath) {
|
|
154
|
+
categories.add("qualified_container");
|
|
155
|
+
reasons.push("qualified_container_match");
|
|
156
|
+
}
|
|
157
|
+
if (topologyBucket(bf.modulePath) && topologyBucket(bf.modulePath) === topologyBucket(hf.modulePath)) {
|
|
158
|
+
categories.add("module_topology");
|
|
159
|
+
reasons.push("module_topology_match");
|
|
160
|
+
if (bf.modulePath !== hf.modulePath)
|
|
161
|
+
facets.push("module_path_changed");
|
|
162
|
+
}
|
|
163
|
+
if (bf.docFp && hf.docFp && bf.docFp === hf.docFp) {
|
|
164
|
+
categories.add("documentation_fingerprint");
|
|
165
|
+
reasons.push("documentation_fingerprint_match");
|
|
166
|
+
}
|
|
167
|
+
if (bf.capabilities &&
|
|
168
|
+
hf.capabilities &&
|
|
169
|
+
bf.capabilities.length > 0 &&
|
|
170
|
+
JSON.stringify([...bf.capabilities].sort()) === JSON.stringify([...hf.capabilities].sort())) {
|
|
171
|
+
categories.add("linkage");
|
|
172
|
+
reasons.push("capability_linkage_match");
|
|
173
|
+
}
|
|
174
|
+
// Name alone is not a category for sufficiency
|
|
175
|
+
if (!sufficientEvidence(categories))
|
|
176
|
+
continue;
|
|
177
|
+
// Still require name OR fingerprint continuity to avoid random package matches
|
|
178
|
+
const hasIdentityAnchor = categories.has("signature_fingerprint") ||
|
|
179
|
+
categories.has("implementation_fingerprint") ||
|
|
180
|
+
(reasons.includes("declaration_name_match") && categories.has("package_continuity") && categories.size >= 3);
|
|
181
|
+
if (!hasIdentityAnchor)
|
|
182
|
+
continue;
|
|
183
|
+
pairs.push({
|
|
184
|
+
rem,
|
|
185
|
+
add,
|
|
186
|
+
domain: "symbols",
|
|
187
|
+
reasons,
|
|
188
|
+
categories: [...categories],
|
|
189
|
+
facets,
|
|
190
|
+
kind: classifyKind(rem, add, reasons),
|
|
191
|
+
score: scorePair(categories)
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return pairs;
|
|
196
|
+
}
|
|
197
|
+
function collectPublicSurfacePairs(changes, budget, warnings) {
|
|
198
|
+
const removed = changes.filter((c) => c.domain === "publicApis" && c.status === "removed");
|
|
199
|
+
const added = changes.filter((c) => c.domain === "publicApis" && c.status === "added");
|
|
200
|
+
const pairs = [];
|
|
201
|
+
for (const rem of removed) {
|
|
202
|
+
for (const add of added) {
|
|
203
|
+
if (budget.remaining-- <= 0) {
|
|
204
|
+
warnings.push("candidate_work_budget_exhausted");
|
|
205
|
+
return pairs;
|
|
206
|
+
}
|
|
207
|
+
const categories = new Set();
|
|
208
|
+
const reasons = [];
|
|
209
|
+
const facets = [];
|
|
210
|
+
if (rem.packageId && add.packageId && rem.packageId === add.packageId) {
|
|
211
|
+
categories.add("package_continuity");
|
|
212
|
+
reasons.push("package_continuity");
|
|
213
|
+
}
|
|
214
|
+
if (rem.name && add.name && rem.name === add.name) {
|
|
215
|
+
categories.add("exported_name");
|
|
216
|
+
reasons.push("exported_name_match");
|
|
217
|
+
if (rem.importPath !== add.importPath)
|
|
218
|
+
facets.push("import_path_changed");
|
|
219
|
+
}
|
|
220
|
+
if (rem.importPath && add.importPath && rem.importPath === add.importPath) {
|
|
221
|
+
categories.add("import_path");
|
|
222
|
+
reasons.push("import_path_match");
|
|
223
|
+
if (rem.name !== add.name)
|
|
224
|
+
facets.push("exported_name_changed");
|
|
225
|
+
}
|
|
226
|
+
if (!sufficientEvidence(categories))
|
|
227
|
+
continue;
|
|
228
|
+
pairs.push({
|
|
229
|
+
rem,
|
|
230
|
+
add,
|
|
231
|
+
domain: "publicApis",
|
|
232
|
+
reasons,
|
|
233
|
+
categories: [...categories],
|
|
234
|
+
facets,
|
|
235
|
+
kind: classifyKind(rem, add, reasons),
|
|
236
|
+
score: scorePair(categories)
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return pairs;
|
|
241
|
+
}
|
|
242
|
+
function collectFilePairs(changes, base, head, budget, warnings) {
|
|
243
|
+
const removed = changes.filter((c) => c.domain === "files" && c.status === "removed");
|
|
244
|
+
const added = changes.filter((c) => c.domain === "files" && c.status === "added");
|
|
245
|
+
const baseIdx = indexDomain("files", base);
|
|
246
|
+
const headIdx = indexDomain("files", head);
|
|
247
|
+
const pairs = [];
|
|
248
|
+
for (const rem of removed) {
|
|
249
|
+
const baseRec = baseIdx.get(rem.baseRecordId ?? rem.modulePath ?? "");
|
|
250
|
+
for (const add of added) {
|
|
251
|
+
if (budget.remaining-- <= 0) {
|
|
252
|
+
warnings.push("candidate_work_budget_exhausted");
|
|
253
|
+
return pairs;
|
|
254
|
+
}
|
|
255
|
+
const headRec = headIdx.get(add.headRecordId ?? add.modulePath ?? "");
|
|
256
|
+
const categories = new Set();
|
|
257
|
+
const reasons = [];
|
|
258
|
+
const facets = [];
|
|
259
|
+
const baseHash = baseRec?.hash;
|
|
260
|
+
const headHash = headRec?.hash;
|
|
261
|
+
if (baseHash && headHash && baseHash === headHash) {
|
|
262
|
+
categories.add("implementation_fingerprint");
|
|
263
|
+
reasons.push("content_hash_match");
|
|
264
|
+
}
|
|
265
|
+
if (rem.packageId && add.packageId && rem.packageId === add.packageId) {
|
|
266
|
+
categories.add("package_continuity");
|
|
267
|
+
reasons.push("package_continuity");
|
|
268
|
+
}
|
|
269
|
+
const remBase = (rem.modulePath ?? rem.baseRecordId ?? "").split(/[/\\]/).pop();
|
|
270
|
+
const addBase = (add.modulePath ?? add.headRecordId ?? "").split(/[/\\]/).pop();
|
|
271
|
+
if (remBase && addBase && remBase === addBase) {
|
|
272
|
+
reasons.push("basename_match");
|
|
273
|
+
facets.push("file_path_changed");
|
|
274
|
+
}
|
|
275
|
+
// Require content hash + package (basename alone insufficient)
|
|
276
|
+
if (!categories.has("implementation_fingerprint") || !categories.has("package_continuity"))
|
|
277
|
+
continue;
|
|
278
|
+
pairs.push({
|
|
279
|
+
rem,
|
|
280
|
+
add,
|
|
281
|
+
domain: "files",
|
|
282
|
+
reasons,
|
|
283
|
+
categories: [...categories],
|
|
284
|
+
facets,
|
|
285
|
+
kind: "probable_move",
|
|
286
|
+
score: scorePair(categories)
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return pairs;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Resolve eligible pairs globally: do not greedily consume added IDs per removed record.
|
|
294
|
+
*/
|
|
295
|
+
function resolveAmbiguity(pairs) {
|
|
296
|
+
const byRemoved = new Map();
|
|
297
|
+
const byAdded = new Map();
|
|
298
|
+
for (const pair of pairs) {
|
|
299
|
+
const rk = `${pair.domain}|${pair.rem.baseRecordId}`;
|
|
300
|
+
const ak = `${pair.domain}|${pair.add.headRecordId}`;
|
|
301
|
+
byRemoved.set(rk, [...(byRemoved.get(rk) ?? []), pair]);
|
|
302
|
+
byAdded.set(ak, [...(byAdded.get(ak) ?? []), pair]);
|
|
303
|
+
}
|
|
304
|
+
const out = [];
|
|
305
|
+
const emitted = new Set();
|
|
306
|
+
const removedKeys = [...byRemoved.keys()].sort();
|
|
307
|
+
for (const rk of removedKeys) {
|
|
308
|
+
const candidates = (byRemoved.get(rk) ?? []).sort((a, b) => b.score - a.score ||
|
|
309
|
+
(a.add.headRecordId ?? "").localeCompare(b.add.headRecordId ?? ""));
|
|
310
|
+
const limited = candidates.slice(0, MAX_PER_REMOVED);
|
|
311
|
+
for (const pair of limited) {
|
|
312
|
+
const ak = `${pair.domain}|${pair.add.headRecordId}`;
|
|
313
|
+
const remCount = limited.length;
|
|
314
|
+
const addCount = (byAdded.get(ak) ?? []).length;
|
|
315
|
+
const ambiguous = remCount > 1 || addCount > 1;
|
|
316
|
+
const idKey = `${pair.domain}|${pair.rem.baseRecordId}|${pair.add.headRecordId}|${pair.kind}`;
|
|
317
|
+
if (emitted.has(idKey))
|
|
318
|
+
continue;
|
|
319
|
+
emitted.add(idKey);
|
|
320
|
+
out.push({
|
|
321
|
+
id: "", // filled by caller with comparisonId
|
|
322
|
+
candidateKind: ambiguous ? "ambiguous_candidate" : pair.kind,
|
|
323
|
+
domain: pair.domain,
|
|
324
|
+
baseRecordId: pair.rem.baseRecordId,
|
|
325
|
+
headRecordId: pair.add.headRecordId,
|
|
326
|
+
confidence: ambiguous ? "ambiguous" : pair.score >= 50 ? "high" : "medium",
|
|
327
|
+
evidenceReasons: pair.reasons,
|
|
328
|
+
conflictingEvidence: ambiguous
|
|
329
|
+
? [
|
|
330
|
+
...(remCount > 1 ? ["multiple_eligible_head_records"] : []),
|
|
331
|
+
...(addCount > 1 ? ["multiple_eligible_base_records"] : [])
|
|
332
|
+
]
|
|
333
|
+
: [],
|
|
334
|
+
alternativeCandidateIds: limited
|
|
335
|
+
.filter((p) => p.add.headRecordId !== pair.add.headRecordId)
|
|
336
|
+
.map((p) => p.add.headRecordId),
|
|
337
|
+
ambiguity: ambiguous,
|
|
338
|
+
transitionFacets: pair.facets
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
export function matchAllTransitionCandidates(comparisonId, changes, collisions, catalogues) {
|
|
345
|
+
const budget = { remaining: GLOBAL_BUDGET };
|
|
346
|
+
const warnings = [];
|
|
347
|
+
const base = catalogues?.base;
|
|
348
|
+
const head = catalogues?.head;
|
|
349
|
+
const pairs = [
|
|
350
|
+
...collectPublicSurfacePairs(changes, budget, warnings),
|
|
351
|
+
...(base && head ? collectDeclarationPairs(comparisonId, changes, base, head, budget, warnings) : []),
|
|
352
|
+
...(base && head ? collectFilePairs(changes, base, head, budget, warnings) : [])
|
|
353
|
+
];
|
|
354
|
+
const resolved = resolveAmbiguity(pairs);
|
|
355
|
+
const candidates = [];
|
|
356
|
+
for (const partial of resolved.sort((a, b) => a.domain.localeCompare(b.domain) ||
|
|
357
|
+
a.baseRecordId.localeCompare(b.baseRecordId) ||
|
|
358
|
+
a.headRecordId.localeCompare(b.headRecordId))) {
|
|
359
|
+
const id = changeIdentityId({
|
|
360
|
+
comparisonId,
|
|
361
|
+
domain: partial.domain,
|
|
362
|
+
baseRecordId: partial.baseRecordId,
|
|
363
|
+
headRecordId: partial.headRecordId,
|
|
364
|
+
factualChangeKind: "modified",
|
|
365
|
+
discriminator: partial.ambiguity
|
|
366
|
+
? `ambiguous_${partial.candidateKind}:${partial.headRecordId}`
|
|
367
|
+
: partial.candidateKind
|
|
368
|
+
});
|
|
369
|
+
collisions.register(id, `${comparisonId}|cand|${partial.domain}|${partial.baseRecordId}|${partial.headRecordId}`);
|
|
370
|
+
const identityDiscriminator = partial.ambiguity
|
|
371
|
+
? `ambiguous_${partial.candidateKind}:${partial.headRecordId}`
|
|
372
|
+
: partial.candidateKind;
|
|
373
|
+
candidates.push({ ...partial, id, identityDiscriminator });
|
|
374
|
+
}
|
|
375
|
+
if (warnings.includes("candidate_work_budget_exhausted")) {
|
|
376
|
+
// incomplete analysis — do not emit high-confidence results
|
|
377
|
+
for (const c of candidates) {
|
|
378
|
+
if (c.confidence === "high")
|
|
379
|
+
c.confidence = "medium";
|
|
380
|
+
c.conflictingEvidence = [...c.conflictingEvidence, "candidate_analysis_incomplete"];
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
candidates: candidates.sort((a, b) => a.id.localeCompare(b.id)),
|
|
385
|
+
warnings: [...new Set(warnings)]
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
/** @deprecated Use matchAllTransitionCandidates */
|
|
389
|
+
export function matchPublicSurfaceTransitions(comparisonId, changes, collisions) {
|
|
390
|
+
return matchAllTransitionCandidates(comparisonId, changes, collisions).candidates.filter((c) => c.domain === "publicApis");
|
|
391
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CatalogueSnapshot } from "../catalogue/schema.js";
|
|
2
|
+
import { recordKey } from "./projections.js";
|
|
3
|
+
import type { ComparisonOptions, ComparisonSnapshot, ComparisonError } from "./schema.js";
|
|
4
|
+
export type CompareResult = {
|
|
5
|
+
ok: true;
|
|
6
|
+
comparison: ComparisonSnapshot;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
error: ComparisonError;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Compare two in-memory Schema 2 catalogues deterministically.
|
|
13
|
+
*/
|
|
14
|
+
export declare function compareCatalogues(base: CatalogueSnapshot, head: CatalogueSnapshot, options?: ComparisonOptions): CompareResult;
|
|
15
|
+
export { recordKey };
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { MAPWRIGHT_GENERATOR_VERSION } from "../catalogue/version.js";
|
|
2
|
+
import { changeIdentityId, comparisonIdentityId, ComparisonCollisionTracker, encodeComparisonPreimage } from "./identity.js";
|
|
3
|
+
import { indexDomain, projectFacets, displayFields, recordKey } from "./projections.js";
|
|
4
|
+
import { orderChangeRecords } from "./ordering.js";
|
|
5
|
+
import { validateCataloguePair } from "./validate.js";
|
|
6
|
+
import { validateReferentialIntegrity } from "./integrity.js";
|
|
7
|
+
import { analyzePublicApiCompatibility } from "./compatibility.js";
|
|
8
|
+
import { matchAllTransitionCandidates } from "./candidates.js";
|
|
9
|
+
import { normalizeEndpointKind } from "./relationshipPolicy.js";
|
|
10
|
+
import { COMPARISON_SCHEMA_VERSION, CHANGE_CONTRACT_VERSION, COMPATIBILITY_RULE_VERSION, COMPARISON_IDENTITY_SCHEME, COMPARISON_PROJECTION_VERSION, CANDIDATE_DETECTION_POLICY_VERSION } from "./version.js";
|
|
11
|
+
import { DEFAULT_COMPARISON_OPTIONS } from "./schema.js";
|
|
12
|
+
function emptyUnchanged() {
|
|
13
|
+
return {
|
|
14
|
+
packages: 0,
|
|
15
|
+
files: 0,
|
|
16
|
+
symbols: 0,
|
|
17
|
+
publicApis: 0,
|
|
18
|
+
tests: 0,
|
|
19
|
+
examples: 0,
|
|
20
|
+
capabilities: 0,
|
|
21
|
+
relationships: 0,
|
|
22
|
+
diagnostics: 0
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function buildGraphIndex(snapshot) {
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
const edges = [];
|
|
28
|
+
for (const r of snapshot.relationships) {
|
|
29
|
+
const fromKind = normalizeEndpointKind(r.from.kind);
|
|
30
|
+
const toKind = normalizeEndpointKind(r.to.kind);
|
|
31
|
+
const fromId = r.from.id;
|
|
32
|
+
const toId = r.to.id;
|
|
33
|
+
// Prefer canonical symbol endpoints when present on symbol records
|
|
34
|
+
let canonicalFrom = fromId;
|
|
35
|
+
let canonicalTo = toId;
|
|
36
|
+
if (fromKind === "symbol") {
|
|
37
|
+
const sym = snapshot.symbols.find((s) => s.id === fromId);
|
|
38
|
+
if (sym?.canonicalSymbolId)
|
|
39
|
+
canonicalFrom = sym.canonicalSymbolId;
|
|
40
|
+
}
|
|
41
|
+
if (toKind === "symbol") {
|
|
42
|
+
const sym = snapshot.symbols.find((s) => s.id === toId);
|
|
43
|
+
if (sym?.canonicalSymbolId)
|
|
44
|
+
canonicalTo = sym.canonicalSymbolId;
|
|
45
|
+
}
|
|
46
|
+
const key = [
|
|
47
|
+
r.type,
|
|
48
|
+
fromKind,
|
|
49
|
+
canonicalFrom,
|
|
50
|
+
toKind,
|
|
51
|
+
canonicalTo,
|
|
52
|
+
r.localDiscriminant ?? ""
|
|
53
|
+
].join("|");
|
|
54
|
+
if (seen.has(key))
|
|
55
|
+
continue;
|
|
56
|
+
seen.add(key);
|
|
57
|
+
edges.push({
|
|
58
|
+
id: r.id,
|
|
59
|
+
type: r.type,
|
|
60
|
+
fromKind,
|
|
61
|
+
fromId: canonicalFrom,
|
|
62
|
+
toKind,
|
|
63
|
+
toId: canonicalTo,
|
|
64
|
+
provenance: r.provenance?.source,
|
|
65
|
+
localDiscriminant: r.localDiscriminant ?? null,
|
|
66
|
+
confidence: r.confidence
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return edges.sort((a, b) => `${a.type}|${a.fromKind}:${a.fromId}|${a.toKind}:${a.toId}`.localeCompare(`${b.type}|${b.fromKind}:${b.fromId}|${b.toKind}:${b.toId}`));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Compare two in-memory Schema 2 catalogues deterministically.
|
|
73
|
+
*/
|
|
74
|
+
export function compareCatalogues(base, head, options = DEFAULT_COMPARISON_OPTIONS) {
|
|
75
|
+
const validated = validateCataloguePair(base, head);
|
|
76
|
+
if (!validated.ok) {
|
|
77
|
+
return validated;
|
|
78
|
+
}
|
|
79
|
+
const baseIntegrity = validateReferentialIntegrity(base, "base");
|
|
80
|
+
if (baseIntegrity)
|
|
81
|
+
return { ok: false, error: baseIntegrity };
|
|
82
|
+
const headIntegrity = validateReferentialIntegrity(head, "head");
|
|
83
|
+
if (headIntegrity)
|
|
84
|
+
return { ok: false, error: headIntegrity };
|
|
85
|
+
const comparisonId = comparisonIdentityId({
|
|
86
|
+
baseCatalogueId: base.catalogueId,
|
|
87
|
+
headCatalogueId: head.catalogueId,
|
|
88
|
+
options
|
|
89
|
+
});
|
|
90
|
+
const collisions = new ComparisonCollisionTracker();
|
|
91
|
+
collisions.register(comparisonId, encodeComparisonPreimage([comparisonId, base.catalogueId, head.catalogueId]));
|
|
92
|
+
const changes = [];
|
|
93
|
+
const unchanged = emptyUnchanged();
|
|
94
|
+
const domains = options.enabledDomains;
|
|
95
|
+
for (const domain of domains) {
|
|
96
|
+
const baseMap = indexDomain(domain, base);
|
|
97
|
+
const headMap = indexDomain(domain, head);
|
|
98
|
+
const keys = new Set([...baseMap.keys(), ...headMap.keys()]);
|
|
99
|
+
for (const key of [...keys].sort()) {
|
|
100
|
+
const baseRec = baseMap.get(key);
|
|
101
|
+
const headRec = headMap.get(key);
|
|
102
|
+
if (baseRec && !headRec) {
|
|
103
|
+
const id = changeIdentityId({
|
|
104
|
+
comparisonId,
|
|
105
|
+
domain,
|
|
106
|
+
baseRecordId: key,
|
|
107
|
+
headRecordId: null,
|
|
108
|
+
factualChangeKind: "removed"
|
|
109
|
+
});
|
|
110
|
+
collisions.register(id, `${comparisonId}|${domain}|${key}|removed`);
|
|
111
|
+
changes.push({
|
|
112
|
+
id,
|
|
113
|
+
domain,
|
|
114
|
+
status: "removed",
|
|
115
|
+
baseRecordId: key,
|
|
116
|
+
headRecordId: null,
|
|
117
|
+
identityDiscriminator: null,
|
|
118
|
+
facets: [],
|
|
119
|
+
...displayFields(domain, baseRec)
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!baseRec && headRec) {
|
|
124
|
+
const id = changeIdentityId({
|
|
125
|
+
comparisonId,
|
|
126
|
+
domain,
|
|
127
|
+
baseRecordId: null,
|
|
128
|
+
headRecordId: key,
|
|
129
|
+
factualChangeKind: "added"
|
|
130
|
+
});
|
|
131
|
+
collisions.register(id, `${comparisonId}|${domain}|${key}|added`);
|
|
132
|
+
changes.push({
|
|
133
|
+
id,
|
|
134
|
+
domain,
|
|
135
|
+
status: "added",
|
|
136
|
+
baseRecordId: null,
|
|
137
|
+
headRecordId: key,
|
|
138
|
+
identityDiscriminator: null,
|
|
139
|
+
facets: [],
|
|
140
|
+
...displayFields(domain, headRec)
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (baseRec && headRec) {
|
|
145
|
+
const facets = projectFacets(domain, baseRec, headRec);
|
|
146
|
+
if (facets.length === 0) {
|
|
147
|
+
unchanged[domain] += 1;
|
|
148
|
+
if (options.includeUnchanged) {
|
|
149
|
+
const id = changeIdentityId({
|
|
150
|
+
comparisonId,
|
|
151
|
+
domain,
|
|
152
|
+
baseRecordId: key,
|
|
153
|
+
headRecordId: key,
|
|
154
|
+
factualChangeKind: "unchanged"
|
|
155
|
+
});
|
|
156
|
+
collisions.register(id, `${comparisonId}|${domain}|${key}|unchanged`);
|
|
157
|
+
changes.push({
|
|
158
|
+
id,
|
|
159
|
+
domain,
|
|
160
|
+
status: "unchanged",
|
|
161
|
+
baseRecordId: key,
|
|
162
|
+
headRecordId: key,
|
|
163
|
+
identityDiscriminator: null,
|
|
164
|
+
facets: [],
|
|
165
|
+
...displayFields(domain, headRec)
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const id = changeIdentityId({
|
|
171
|
+
comparisonId,
|
|
172
|
+
domain,
|
|
173
|
+
baseRecordId: key,
|
|
174
|
+
headRecordId: key,
|
|
175
|
+
factualChangeKind: "modified"
|
|
176
|
+
});
|
|
177
|
+
collisions.register(id, `${comparisonId}|${domain}|${key}|modified`);
|
|
178
|
+
changes.push({
|
|
179
|
+
id,
|
|
180
|
+
domain,
|
|
181
|
+
status: "modified",
|
|
182
|
+
baseRecordId: key,
|
|
183
|
+
headRecordId: key,
|
|
184
|
+
identityDiscriminator: null,
|
|
185
|
+
facets,
|
|
186
|
+
...displayFields(domain, headRec)
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const orderedChanges = orderChangeRecords(changes);
|
|
193
|
+
const { candidates, warnings: candidateWarnings } = matchAllTransitionCandidates(comparisonId, orderedChanges, collisions, { base, head });
|
|
194
|
+
const { findings, aggregate } = analyzePublicApiCompatibility({
|
|
195
|
+
comparisonId,
|
|
196
|
+
changes: orderedChanges,
|
|
197
|
+
candidates,
|
|
198
|
+
base,
|
|
199
|
+
head,
|
|
200
|
+
collisions
|
|
201
|
+
});
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
comparison: {
|
|
205
|
+
manifest: {
|
|
206
|
+
comparisonSchemaVersion: COMPARISON_SCHEMA_VERSION,
|
|
207
|
+
changeContractVersion: CHANGE_CONTRACT_VERSION,
|
|
208
|
+
compatibilityRuleVersion: COMPATIBILITY_RULE_VERSION,
|
|
209
|
+
comparisonIdentityScheme: COMPARISON_IDENTITY_SCHEME,
|
|
210
|
+
comparisonProjectionVersion: COMPARISON_PROJECTION_VERSION,
|
|
211
|
+
candidateDetectionPolicyVersion: CANDIDATE_DETECTION_POLICY_VERSION,
|
|
212
|
+
comparisonId,
|
|
213
|
+
baseCatalogueId: base.catalogueId,
|
|
214
|
+
headCatalogueId: head.catalogueId,
|
|
215
|
+
baseGeneratorVersion: base.generator?.version ?? null,
|
|
216
|
+
headGeneratorVersion: head.generator?.version ?? null,
|
|
217
|
+
comparisonGeneratorVersion: MAPWRIGHT_GENERATOR_VERSION,
|
|
218
|
+
options,
|
|
219
|
+
counts: {
|
|
220
|
+
added: orderedChanges.filter((c) => c.status === "added").length,
|
|
221
|
+
removed: orderedChanges.filter((c) => c.status === "removed").length,
|
|
222
|
+
modified: orderedChanges.filter((c) => c.status === "modified").length,
|
|
223
|
+
unchanged,
|
|
224
|
+
compatibilityFindings: findings.length,
|
|
225
|
+
candidates: candidates.length
|
|
226
|
+
},
|
|
227
|
+
aggregateCompatibility: aggregate
|
|
228
|
+
},
|
|
229
|
+
changes: orderedChanges,
|
|
230
|
+
compatibilityFindings: findings,
|
|
231
|
+
candidates,
|
|
232
|
+
graphIndex: {
|
|
233
|
+
base: buildGraphIndex(base),
|
|
234
|
+
head: buildGraphIndex(head)
|
|
235
|
+
},
|
|
236
|
+
warnings: candidateWarnings.length > 0 ? candidateWarnings : undefined
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
export { recordKey };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CatalogueSnapshot } from "../catalogue/schema.js";
|
|
2
|
+
import { type ComparisonCollisionTracker } from "./identity.js";
|
|
3
|
+
import type { ChangeRecord, CompatibilityFinding, CompatibilityClassification, TransitionCandidate } from "./schema.js";
|
|
4
|
+
type Stability = "stable" | "experimental" | "deprecated" | "internal" | string;
|
|
5
|
+
/**
|
|
6
|
+
* Versioned stability transition matrix (compatibilityRuleVersion 1 corrective).
|
|
7
|
+
*/
|
|
8
|
+
export declare function classifyStabilityTransition(from: Stability | null | undefined, to: Stability | null | undefined): {
|
|
9
|
+
classification: CompatibilityClassification;
|
|
10
|
+
ruleId: string;
|
|
11
|
+
} | null;
|
|
12
|
+
/**
|
|
13
|
+
* Conservative public API compatibility analysis (rule version 1).
|
|
14
|
+
*/
|
|
15
|
+
export declare function analyzePublicApiCompatibility(input: {
|
|
16
|
+
comparisonId: string;
|
|
17
|
+
changes: ChangeRecord[];
|
|
18
|
+
candidates: TransitionCandidate[];
|
|
19
|
+
base: CatalogueSnapshot;
|
|
20
|
+
head: CatalogueSnapshot;
|
|
21
|
+
collisions: ComparisonCollisionTracker;
|
|
22
|
+
}): {
|
|
23
|
+
findings: CompatibilityFinding[];
|
|
24
|
+
aggregate: CompatibilityClassification | null;
|
|
25
|
+
};
|
|
26
|
+
export {};
|