@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.
Files changed (166) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/LICENSE +21 -0
  3. package/README.md +262 -0
  4. package/dist/catalogue/canonicalisation.d.ts +36 -0
  5. package/dist/catalogue/canonicalisation.js +262 -0
  6. package/dist/catalogue/config.d.ts +13 -0
  7. package/dist/catalogue/config.js +77 -0
  8. package/dist/catalogue/deterministic.d.ts +3 -0
  9. package/dist/catalogue/deterministic.js +25 -0
  10. package/dist/catalogue/diagnostics.d.ts +2 -0
  11. package/dist/catalogue/diagnostics.js +34 -0
  12. package/dist/catalogue/evidence.d.ts +1 -0
  13. package/dist/catalogue/evidence.js +10 -0
  14. package/dist/catalogue/examples.d.ts +3 -0
  15. package/dist/catalogue/examples.js +20 -0
  16. package/dist/catalogue/identity.d.ts +61 -0
  17. package/dist/catalogue/identity.js +114 -0
  18. package/dist/catalogue/ignore.d.ts +4 -0
  19. package/dist/catalogue/ignore.js +29 -0
  20. package/dist/catalogue/indexer.d.ts +20 -0
  21. package/dist/catalogue/indexer.js +1393 -0
  22. package/dist/catalogue/linkage.d.ts +16 -0
  23. package/dist/catalogue/linkage.js +419 -0
  24. package/dist/catalogue/moduleResolution.d.ts +4 -0
  25. package/dist/catalogue/moduleResolution.js +67 -0
  26. package/dist/catalogue/ordering.d.ts +13 -0
  27. package/dist/catalogue/ordering.js +86 -0
  28. package/dist/catalogue/packages.d.ts +12 -0
  29. package/dist/catalogue/packages.js +475 -0
  30. package/dist/catalogue/pythonReExports.d.ts +7 -0
  31. package/dist/catalogue/pythonReExports.js +113 -0
  32. package/dist/catalogue/resolveRefs.d.ts +40 -0
  33. package/dist/catalogue/resolveRefs.js +284 -0
  34. package/dist/catalogue/scanner.d.ts +8 -0
  35. package/dist/catalogue/scanner.js +99 -0
  36. package/dist/catalogue/schema.d.ts +289 -0
  37. package/dist/catalogue/schema.js +1 -0
  38. package/dist/catalogue/store.d.ts +41 -0
  39. package/dist/catalogue/store.js +209 -0
  40. package/dist/catalogue/templates.d.ts +18 -0
  41. package/dist/catalogue/templates.js +158 -0
  42. package/dist/catalogue/version.d.ts +12 -0
  43. package/dist/catalogue/version.js +30 -0
  44. package/dist/cli/commands/adoptionPlan.d.ts +8 -0
  45. package/dist/cli/commands/adoptionPlan.js +24 -0
  46. package/dist/cli/commands/auditDocs.d.ts +8 -0
  47. package/dist/cli/commands/auditDocs.js +24 -0
  48. package/dist/cli/commands/changeImpact.d.ts +10 -0
  49. package/dist/cli/commands/changeImpact.js +36 -0
  50. package/dist/cli/commands/changes.d.ts +12 -0
  51. package/dist/cli/commands/changes.js +31 -0
  52. package/dist/cli/commands/check.d.ts +9 -0
  53. package/dist/cli/commands/check.js +4 -0
  54. package/dist/cli/commands/compatibility.d.ts +10 -0
  55. package/dist/cli/commands/compatibility.js +43 -0
  56. package/dist/cli/commands/diff.d.ts +31 -0
  57. package/dist/cli/commands/diff.js +118 -0
  58. package/dist/cli/commands/dogfood.d.ts +8 -0
  59. package/dist/cli/commands/dogfood.js +24 -0
  60. package/dist/cli/commands/drift.d.ts +8 -0
  61. package/dist/cli/commands/drift.js +13 -0
  62. package/dist/cli/commands/index.d.ts +9 -0
  63. package/dist/cli/commands/index.js +9 -0
  64. package/dist/cli/commands/indexWorkspace.d.ts +8 -0
  65. package/dist/cli/commands/indexWorkspace.js +23 -0
  66. package/dist/cli/commands/recommendConfig.d.ts +7 -0
  67. package/dist/cli/commands/recommendConfig.js +20 -0
  68. package/dist/cli/commands/schema.d.ts +1 -0
  69. package/dist/cli/commands/schema.js +37 -0
  70. package/dist/cli/commands/template.d.ts +5 -0
  71. package/dist/cli/commands/template.js +44 -0
  72. package/dist/cli/commands/validate.d.ts +9 -0
  73. package/dist/cli/commands/validate.js +85 -0
  74. package/dist/cli/index.d.ts +2 -0
  75. package/dist/cli/index.js +211 -0
  76. package/dist/comparison/artefactValidate.d.ts +19 -0
  77. package/dist/comparison/artefactValidate.js +334 -0
  78. package/dist/comparison/candidates.d.ts +12 -0
  79. package/dist/comparison/candidates.js +391 -0
  80. package/dist/comparison/compare.d.ts +15 -0
  81. package/dist/comparison/compare.js +240 -0
  82. package/dist/comparison/compatibility.d.ts +26 -0
  83. package/dist/comparison/compatibility.js +316 -0
  84. package/dist/comparison/drift.d.ts +34 -0
  85. package/dist/comparison/drift.js +71 -0
  86. package/dist/comparison/identity.d.ts +30 -0
  87. package/dist/comparison/identity.js +73 -0
  88. package/dist/comparison/impact.d.ts +14 -0
  89. package/dist/comparison/impact.js +291 -0
  90. package/dist/comparison/index.d.ts +6 -0
  91. package/dist/comparison/index.js +6 -0
  92. package/dist/comparison/integrity.d.ts +12 -0
  93. package/dist/comparison/integrity.js +199 -0
  94. package/dist/comparison/ordering.d.ts +4 -0
  95. package/dist/comparison/ordering.js +69 -0
  96. package/dist/comparison/projections.d.ts +19 -0
  97. package/dist/comparison/projections.js +185 -0
  98. package/dist/comparison/relationshipPolicy.d.ts +19 -0
  99. package/dist/comparison/relationshipPolicy.js +98 -0
  100. package/dist/comparison/schema.d.ts +178 -0
  101. package/dist/comparison/schema.js +16 -0
  102. package/dist/comparison/store.d.ts +58 -0
  103. package/dist/comparison/store.js +462 -0
  104. package/dist/comparison/validate.d.ts +23 -0
  105. package/dist/comparison/validate.js +137 -0
  106. package/dist/comparison/version.d.ts +9 -0
  107. package/dist/comparison/version.js +11 -0
  108. package/dist/extractors/docTags.d.ts +9 -0
  109. package/dist/extractors/docTags.js +106 -0
  110. package/dist/extractors/generic.d.ts +5 -0
  111. package/dist/extractors/generic.js +3 -0
  112. package/dist/extractors/javascript.d.ts +3 -0
  113. package/dist/extractors/javascript.js +4 -0
  114. package/dist/extractors/markdown.d.ts +7 -0
  115. package/dist/extractors/markdown.js +269 -0
  116. package/dist/extractors/python.d.ts +7 -0
  117. package/dist/extractors/python.js +314 -0
  118. package/dist/extractors/typescript.d.ts +7 -0
  119. package/dist/extractors/typescript.js +387 -0
  120. package/dist/index.d.ts +2 -0
  121. package/dist/index.js +2 -0
  122. package/dist/mcp/resources.d.ts +18 -0
  123. package/dist/mcp/resources.js +57 -0
  124. package/dist/mcp/server.d.ts +2 -0
  125. package/dist/mcp/server.js +95 -0
  126. package/dist/mcp/tools.d.ts +29 -0
  127. package/dist/mcp/tools.js +902 -0
  128. package/dist/queries/adoption.d.ts +62 -0
  129. package/dist/queries/adoption.js +366 -0
  130. package/dist/queries/capabilities.d.ts +45 -0
  131. package/dist/queries/capabilities.js +124 -0
  132. package/dist/queries/changes.d.ts +33 -0
  133. package/dist/queries/changes.js +53 -0
  134. package/dist/queries/dependencies.d.ts +17 -0
  135. package/dist/queries/dependencies.js +94 -0
  136. package/dist/queries/dogfood.d.ts +50 -0
  137. package/dist/queries/dogfood.js +116 -0
  138. package/dist/queries/editingContext.d.ts +84 -0
  139. package/dist/queries/editingContext.js +240 -0
  140. package/dist/queries/examples.d.ts +8 -0
  141. package/dist/queries/examples.js +16 -0
  142. package/dist/queries/files.d.ts +8 -0
  143. package/dist/queries/files.js +14 -0
  144. package/dist/queries/lists.d.ts +94 -0
  145. package/dist/queries/lists.js +184 -0
  146. package/dist/queries/overview.d.ts +2 -0
  147. package/dist/queries/overview.js +8 -0
  148. package/dist/queries/publicApis.d.ts +60 -0
  149. package/dist/queries/publicApis.js +120 -0
  150. package/dist/queries/ranking.d.ts +67 -0
  151. package/dist/queries/ranking.js +387 -0
  152. package/dist/queries/recommendConfig.d.ts +47 -0
  153. package/dist/queries/recommendConfig.js +123 -0
  154. package/dist/queries/relationships.d.ts +5 -0
  155. package/dist/queries/relationships.js +15 -0
  156. package/dist/queries/reuseCandidates.d.ts +34 -0
  157. package/dist/queries/reuseCandidates.js +181 -0
  158. package/dist/queries/symbols.d.ts +40 -0
  159. package/dist/queries/symbols.js +41 -0
  160. package/dist/security/pathPolicy.d.ts +12 -0
  161. package/dist/security/pathPolicy.js +46 -0
  162. package/dist/security/secretPatterns.d.ts +1 -0
  163. package/dist/security/secretPatterns.js +4 -0
  164. package/dist/security/sourceAccess.d.ts +12 -0
  165. package/dist/security/sourceAccess.js +35 -0
  166. package/package.json +60 -0
@@ -0,0 +1,316 @@
1
+ import { changeIdentityId } from "./identity.js";
2
+ import { orderCompatibilityFindings, aggregateCompatibility } from "./ordering.js";
3
+ import { normalizeConsoleScripts } from "./projections.js";
4
+ import { COMPATIBILITY_RULE_VERSION } from "./version.js";
5
+ import { attachFindingIdentity } from "./artefactValidate.js";
6
+ function findingId(comparisonId, ruleId, publicApiId, changeId) {
7
+ return changeIdentityId({
8
+ comparisonId,
9
+ domain: "publicApis",
10
+ baseRecordId: publicApiId,
11
+ headRecordId: changeId,
12
+ factualChangeKind: "modified",
13
+ discriminator: `compat:${ruleId}`
14
+ });
15
+ }
16
+ function pushFinding(findings, finding, identityBaseRecordId, identityHeadRecordId) {
17
+ findings.push(attachFindingIdentity(finding, identityBaseRecordId, identityHeadRecordId));
18
+ }
19
+ /**
20
+ * Versioned stability transition matrix (compatibilityRuleVersion 1 corrective).
21
+ */
22
+ export function classifyStabilityTransition(from, to) {
23
+ if (from == null || to == null || from === to)
24
+ return null;
25
+ const key = `${from}->${to}`;
26
+ const matrix = {
27
+ "stable->deprecated": { classification: "potentially_breaking", ruleId: "stability-stable-to-deprecated" },
28
+ "stable->internal": { classification: "breaking", ruleId: "stability-stable-to-internal" },
29
+ "stable->experimental": { classification: "potentially_breaking", ruleId: "stability-stable-to-experimental" },
30
+ "experimental->stable": { classification: "non_breaking", ruleId: "stability-experimental-to-stable" },
31
+ "deprecated->stable": { classification: "non_breaking", ruleId: "stability-deprecated-to-stable" },
32
+ "deprecated->internal": { classification: "breaking", ruleId: "stability-deprecated-to-internal" },
33
+ "experimental->deprecated": { classification: "potentially_breaking", ruleId: "stability-experimental-to-deprecated" },
34
+ "experimental->internal": { classification: "breaking", ruleId: "stability-experimental-to-internal" },
35
+ "internal->stable": { classification: "non_breaking", ruleId: "stability-internal-to-stable" },
36
+ "internal->experimental": { classification: "non_breaking", ruleId: "stability-internal-to-experimental" }
37
+ };
38
+ return (matrix[key] ?? {
39
+ classification: "potentially_breaking",
40
+ ruleId: "stability-transition-unknown-policy"
41
+ });
42
+ }
43
+ function analyzeConsoleScripts(comparisonId, change, collisions) {
44
+ const oldScripts = normalizeConsoleScripts(change.facets.find((f) => f.facet === "console_scripts_changed")?.oldValue);
45
+ const newScripts = normalizeConsoleScripts(change.facets.find((f) => f.facet === "console_scripts_changed")?.newValue);
46
+ if (!change.facets.some((f) => f.facet === "console_scripts_changed"))
47
+ return [];
48
+ const oldMap = new Map(oldScripts.map((s) => [s.name, s.target]));
49
+ const newMap = new Map(newScripts.map((s) => [s.name, s.target]));
50
+ const findings = [];
51
+ for (const [name, target] of oldMap) {
52
+ if (!newMap.has(name)) {
53
+ const id = findingId(comparisonId, "python-console-script-removed", change.baseRecordId, change.id);
54
+ collisions.register(id, `${comparisonId}|console-removed|${name}`);
55
+ findings.push({
56
+ id,
57
+ classification: "breaking",
58
+ ruleId: "python-console-script-removed",
59
+ ruleVersion: COMPATIBILITY_RULE_VERSION,
60
+ language: "python",
61
+ publicApiId: null,
62
+ changeId: change.id,
63
+ changedFacets: ["console_scripts_changed"],
64
+ evidence: [`Console script "${name}" removed (was ${target}).`],
65
+ limitations: [],
66
+ confidence: "high"
67
+ });
68
+ }
69
+ else if (newMap.get(name) !== target) {
70
+ const id = findingId(comparisonId, "python-console-script-target-changed", change.baseRecordId, change.id);
71
+ collisions.register(id, `${comparisonId}|console-target|${name}`);
72
+ findings.push({
73
+ id,
74
+ classification: "potentially_breaking",
75
+ ruleId: "python-console-script-target-changed",
76
+ ruleVersion: COMPATIBILITY_RULE_VERSION,
77
+ language: "python",
78
+ publicApiId: null,
79
+ changeId: change.id,
80
+ changedFacets: ["console_scripts_changed"],
81
+ evidence: [`Console script "${name}" target changed from ${target} to ${newMap.get(name)}.`],
82
+ limitations: ["Cannot prove consumer command compatibility without runtime analysis."],
83
+ confidence: "medium"
84
+ });
85
+ }
86
+ }
87
+ for (const [name, target] of newMap) {
88
+ if (!oldMap.has(name)) {
89
+ // detect rename: same target, different name
90
+ const renamedFrom = [...oldMap.entries()].find(([oldName, oldTarget]) => oldTarget === target && !newMap.has(oldName));
91
+ if (renamedFrom) {
92
+ const id = findingId(comparisonId, "python-console-script-renamed", change.baseRecordId, change.id);
93
+ collisions.register(id, `${comparisonId}|console-renamed|${name}`);
94
+ findings.push({
95
+ id,
96
+ classification: "breaking",
97
+ ruleId: "python-console-script-renamed",
98
+ ruleVersion: COMPATIBILITY_RULE_VERSION,
99
+ language: "python",
100
+ publicApiId: null,
101
+ changeId: change.id,
102
+ changedFacets: ["console_scripts_changed"],
103
+ evidence: [`Console script renamed from "${renamedFrom[0]}" to "${name}" (target ${target}).`],
104
+ limitations: [],
105
+ confidence: "high"
106
+ });
107
+ }
108
+ else {
109
+ const id = findingId(comparisonId, "python-console-script-added", change.baseRecordId, change.id);
110
+ collisions.register(id, `${comparisonId}|console-added|${name}`);
111
+ findings.push({
112
+ id,
113
+ classification: "non_breaking",
114
+ ruleId: "python-console-script-added",
115
+ ruleVersion: COMPATIBILITY_RULE_VERSION,
116
+ language: "python",
117
+ publicApiId: null,
118
+ changeId: change.id,
119
+ changedFacets: ["console_scripts_changed"],
120
+ evidence: [`Console script "${name}" added (target ${target}).`],
121
+ limitations: [],
122
+ confidence: "high"
123
+ });
124
+ }
125
+ }
126
+ }
127
+ // order-only / identical maps should not reach here as a facet; if empty delta, documentation_only
128
+ if (findings.length === 0) {
129
+ const id = findingId(comparisonId, "python-console-script-metadata", change.baseRecordId, change.id);
130
+ collisions.register(id, `${comparisonId}|console-meta|${change.baseRecordId}`);
131
+ findings.push({
132
+ id,
133
+ classification: "documentation_only",
134
+ ruleId: "python-console-script-metadata",
135
+ ruleVersion: COMPATIBILITY_RULE_VERSION,
136
+ language: "python",
137
+ publicApiId: null,
138
+ changeId: change.id,
139
+ changedFacets: ["console_scripts_changed"],
140
+ evidence: ["Console script metadata ordering or representation changed without command/target delta."],
141
+ limitations: [],
142
+ confidence: "medium"
143
+ });
144
+ }
145
+ return findings;
146
+ }
147
+ /**
148
+ * Conservative public API compatibility analysis (rule version 1).
149
+ */
150
+ export function analyzePublicApiCompatibility(input) {
151
+ const findings = [];
152
+ const publicChanges = input.changes.filter((c) => c.domain === "publicApis");
153
+ for (const change of publicChanges) {
154
+ if (change.status === "removed") {
155
+ const id = findingId(input.comparisonId, "public-export-removed", change.baseRecordId, change.id);
156
+ input.collisions.register(id, `${input.comparisonId}|public-export-removed|${change.baseRecordId}`);
157
+ findings.push({
158
+ id,
159
+ classification: "breaking",
160
+ ruleId: "public-export-removed",
161
+ ruleVersion: COMPATIBILITY_RULE_VERSION,
162
+ language: "any",
163
+ publicApiId: change.baseRecordId,
164
+ changeId: change.id,
165
+ changedFacets: [],
166
+ evidence: ["Public API record absent in head catalogue."],
167
+ limitations: [],
168
+ confidence: "high"
169
+ });
170
+ continue;
171
+ }
172
+ if (change.status === "added") {
173
+ const id = findingId(input.comparisonId, "public-export-added", change.headRecordId, change.id);
174
+ input.collisions.register(id, `${input.comparisonId}|public-export-added|${change.headRecordId}`);
175
+ findings.push({
176
+ id,
177
+ classification: "non_breaking",
178
+ ruleId: "public-export-added",
179
+ ruleVersion: COMPATIBILITY_RULE_VERSION,
180
+ language: "any",
181
+ publicApiId: change.headRecordId,
182
+ changeId: change.id,
183
+ changedFacets: [],
184
+ evidence: ["Public API record present only in head catalogue."],
185
+ limitations: [],
186
+ confidence: "high"
187
+ });
188
+ continue;
189
+ }
190
+ if (change.status === "modified") {
191
+ const facetNames = change.facets.map((f) => f.facet);
192
+ const stabilityFacet = change.facets.find((f) => f.facet === "stability_changed");
193
+ if (stabilityFacet) {
194
+ const transition = classifyStabilityTransition(stabilityFacet.oldValue, stabilityFacet.newValue);
195
+ if (transition) {
196
+ const id = findingId(input.comparisonId, transition.ruleId, change.baseRecordId, change.id);
197
+ input.collisions.register(id, `${input.comparisonId}|${transition.ruleId}|${change.baseRecordId}`);
198
+ findings.push({
199
+ id,
200
+ classification: transition.classification,
201
+ ruleId: transition.ruleId,
202
+ ruleVersion: COMPATIBILITY_RULE_VERSION,
203
+ language: "any",
204
+ publicApiId: change.baseRecordId,
205
+ changeId: change.id,
206
+ changedFacets: ["stability_changed"],
207
+ evidence: [`Stability changed from ${String(stabilityFacet.oldValue)} to ${String(stabilityFacet.newValue)}.`],
208
+ limitations: [],
209
+ confidence: "high"
210
+ });
211
+ }
212
+ }
213
+ const docsOnly = facetNames.length > 0 &&
214
+ facetNames.every((f) => f === "documentation_changed") &&
215
+ !stabilityFacet;
216
+ const implOnly = facetNames.includes("implementation_target_changed") &&
217
+ !facetNames.includes("signature_changed") &&
218
+ !facetNames.some((f) => f.endsWith("path_changed") || f.includes("export"));
219
+ if (docsOnly) {
220
+ const id = findingId(input.comparisonId, "documentation-only", change.baseRecordId, change.id);
221
+ input.collisions.register(id, `${input.comparisonId}|documentation-only|${change.baseRecordId}`);
222
+ findings.push({
223
+ id,
224
+ classification: "documentation_only",
225
+ ruleId: "documentation-only",
226
+ ruleVersion: COMPATIBILITY_RULE_VERSION,
227
+ language: "any",
228
+ publicApiId: change.baseRecordId,
229
+ changeId: change.id,
230
+ changedFacets: facetNames,
231
+ evidence: facetNames.map((f) => `Facet ${f} differed between base and head.`),
232
+ limitations: [],
233
+ confidence: "high"
234
+ });
235
+ }
236
+ else if (implOnly) {
237
+ const id = findingId(input.comparisonId, "implementation-target-stable-export", change.baseRecordId, change.id);
238
+ input.collisions.register(id, `${input.comparisonId}|impl-target|${change.baseRecordId}`);
239
+ findings.push({
240
+ id,
241
+ classification: "non_breaking",
242
+ ruleId: "implementation-target-stable-export",
243
+ ruleVersion: COMPATIBILITY_RULE_VERSION,
244
+ language: "any",
245
+ publicApiId: change.baseRecordId,
246
+ changeId: change.id,
247
+ changedFacets: facetNames,
248
+ evidence: facetNames.map((f) => `Facet ${f} differed between base and head.`),
249
+ limitations: ["Assumes consumer contract identity unchanged; signature structure not fully modelled."],
250
+ confidence: "medium"
251
+ });
252
+ }
253
+ else if (facetNames.includes("signature_changed")) {
254
+ const id = findingId(input.comparisonId, "signature-fingerprint-or-display-changed", change.baseRecordId, change.id);
255
+ input.collisions.register(id, `${input.comparisonId}|sig|${change.baseRecordId}`);
256
+ findings.push({
257
+ id,
258
+ classification: "potentially_breaking",
259
+ ruleId: "signature-fingerprint-or-display-changed",
260
+ ruleVersion: COMPATIBILITY_RULE_VERSION,
261
+ language: "any",
262
+ publicApiId: change.baseRecordId,
263
+ changeId: change.id,
264
+ changedFacets: facetNames,
265
+ evidence: facetNames.map((f) => `Facet ${f} differed between base and head.`),
266
+ limitations: ["Schema 2 lacks structured parameter/type models; cannot prove assignability."],
267
+ confidence: "low"
268
+ });
269
+ }
270
+ else if (!stabilityFacet && facetNames.length > 0) {
271
+ const id = findingId(input.comparisonId, "public-api-modified-unknown", change.baseRecordId, change.id);
272
+ input.collisions.register(id, `${input.comparisonId}|unknown-mod|${change.baseRecordId}`);
273
+ findings.push({
274
+ id,
275
+ classification: "unknown",
276
+ ruleId: "public-api-modified-unknown",
277
+ ruleVersion: COMPATIBILITY_RULE_VERSION,
278
+ language: "any",
279
+ publicApiId: change.baseRecordId,
280
+ changeId: change.id,
281
+ changedFacets: facetNames,
282
+ evidence: facetNames.map((f) => `Facet ${f} differed between base and head.`),
283
+ limitations: [],
284
+ confidence: "medium"
285
+ });
286
+ }
287
+ }
288
+ }
289
+ for (const candidate of input.candidates) {
290
+ if (candidate.domain !== "publicApis")
291
+ continue;
292
+ const id = findingId(input.comparisonId, "public-surface-transition", candidate.baseRecordId, candidate.headRecordId);
293
+ input.collisions.register(id, `${input.comparisonId}|public-surface-transition|${candidate.id}`);
294
+ pushFinding(findings, {
295
+ id,
296
+ classification: candidate.ambiguity ? "potentially_breaking" : "breaking",
297
+ ruleId: "public-surface-transition",
298
+ ruleVersion: COMPATIBILITY_RULE_VERSION,
299
+ language: "any",
300
+ publicApiId: candidate.baseRecordId,
301
+ changeId: null,
302
+ changedFacets: candidate.transitionFacets,
303
+ evidence: candidate.evidenceReasons,
304
+ limitations: candidate.conflictingEvidence,
305
+ confidence: candidate.confidence
306
+ }, candidate.baseRecordId, candidate.headRecordId);
307
+ }
308
+ for (const change of input.changes.filter((c) => c.domain === "packages" && c.status === "modified")) {
309
+ findings.push(...analyzeConsoleScripts(input.comparisonId, change, input.collisions));
310
+ }
311
+ const withIdentity = findings.map((f) => f.identityDiscriminator
312
+ ? f
313
+ : attachFindingIdentity(f, f.publicApiId ?? null, f.changeId ?? null));
314
+ const ordered = orderCompatibilityFindings(withIdentity);
315
+ return { findings: ordered, aggregate: aggregateCompatibility(ordered) };
316
+ }
@@ -0,0 +1,34 @@
1
+ import type { DriftStatus, RegenerationRecommendation } from "../comparison/schema.js";
2
+ /**
3
+ * Explicit read-only drift detection: load committed catalogue, index workspace in memory, compare.
4
+ * Does not write catalogues or comparison artefacts.
5
+ * Does not execute Git or other VCS tools.
6
+ */
7
+ export declare function checkCatalogueDrift(input: {
8
+ workspaceRoot: string;
9
+ catalogueDir?: string;
10
+ configPath?: string;
11
+ }): {
12
+ ok: true;
13
+ data: {
14
+ driftStatus: DriftStatus;
15
+ driftClass: DriftStatus;
16
+ regenerationRecommendation: RegenerationRecommendation;
17
+ baseCatalogueId: string;
18
+ headCatalogueId: string;
19
+ counts: {
20
+ added: number;
21
+ removed: number;
22
+ modified: number;
23
+ };
24
+ aggregateCompatibility: string | null;
25
+ wroteFiles: false;
26
+ indexedInMemory: true;
27
+ };
28
+ } | {
29
+ ok: false;
30
+ error: {
31
+ code: string;
32
+ message: string;
33
+ };
34
+ };
@@ -0,0 +1,71 @@
1
+ import path from "node:path";
2
+ import { indexWorkspace } from "../catalogue/indexer.js";
3
+ import { loadCommittedCatalogue } from "../catalogue/store.js";
4
+ import { compareCatalogues } from "../comparison/compare.js";
5
+ /**
6
+ * Explicit read-only drift detection: load committed catalogue, index workspace in memory, compare.
7
+ * Does not write catalogues or comparison artefacts.
8
+ * Does not execute Git or other VCS tools.
9
+ */
10
+ export function checkCatalogueDrift(input) {
11
+ const workspaceRoot = path.resolve(input.workspaceRoot);
12
+ const catalogueDir = input.catalogueDir ?? ".mapwright";
13
+ const loaded = loadCommittedCatalogue(workspaceRoot, catalogueDir);
14
+ if (!loaded.ok) {
15
+ return { ok: false, error: loaded.error };
16
+ }
17
+ const { snapshot: head } = indexWorkspace({
18
+ workspaceRoot,
19
+ configPath: input.configPath,
20
+ mode: "memory",
21
+ write: false
22
+ });
23
+ const compared = compareCatalogues(loaded.snapshot, head);
24
+ if (!compared.ok) {
25
+ return { ok: false, error: compared.error };
26
+ }
27
+ const { counts, aggregateCompatibility } = compared.comparison.manifest;
28
+ const total = counts.added + counts.removed + counts.modified;
29
+ const hasPublic = compared.comparison.changes.some((c) => c.domain === "publicApis" && c.status !== "unchanged");
30
+ let driftClass = "no_drift";
31
+ if (total === 0) {
32
+ driftClass = "no_drift";
33
+ }
34
+ else if (aggregateCompatibility === "breaking" && hasPublic) {
35
+ driftClass = "breaking_public_drift";
36
+ }
37
+ else if (hasPublic) {
38
+ driftClass = "public_surface_drift";
39
+ }
40
+ else if (total > 0) {
41
+ driftClass = "non_public_drift";
42
+ }
43
+ else {
44
+ driftClass = "unknown";
45
+ }
46
+ let regenerationRecommendation = "none";
47
+ if (driftClass === "breaking_public_drift") {
48
+ regenerationRecommendation = "catalogue_regeneration_required";
49
+ }
50
+ else if (driftClass === "public_surface_drift" || driftClass === "non_public_drift") {
51
+ regenerationRecommendation = "catalogue_regeneration_recommended";
52
+ }
53
+ return {
54
+ ok: true,
55
+ data: {
56
+ driftStatus: driftClass,
57
+ driftClass,
58
+ regenerationRecommendation,
59
+ baseCatalogueId: loaded.snapshot.catalogueId,
60
+ headCatalogueId: head.catalogueId,
61
+ counts: {
62
+ added: counts.added,
63
+ removed: counts.removed,
64
+ modified: counts.modified
65
+ },
66
+ aggregateCompatibility,
67
+ wroteFiles: false,
68
+ indexedInMemory: true
69
+ }
70
+ };
71
+ }
@@ -0,0 +1,30 @@
1
+ import type { ComparisonOptions, FactualChangeKind, RecordDomain } from "./schema.js";
2
+ export type ComparisonIdentityField = string | number | boolean | null;
3
+ export declare function normalizeComparisonString(value: string): string;
4
+ export declare function encodeComparisonPreimage(fields: ComparisonIdentityField[]): string;
5
+ export declare function hashComparisonPreimage(preimageJson: string): string;
6
+ export declare function comparisonIdentityId(input: {
7
+ baseCatalogueId: string;
8
+ headCatalogueId: string;
9
+ options: ComparisonOptions;
10
+ }): string;
11
+ export declare function changeIdentityId(input: {
12
+ comparisonId: string;
13
+ domain: RecordDomain;
14
+ baseRecordId: string | null;
15
+ headRecordId: string | null;
16
+ factualChangeKind: FactualChangeKind;
17
+ discriminator?: string | null;
18
+ }): string;
19
+ /** Fail-closed collision tracker for comparison/change IDs. */
20
+ export declare class ComparisonCollisionTracker {
21
+ private readonly seen;
22
+ register(id: string, preimage: string): void;
23
+ }
24
+ export declare class ComparisonIdentityCollisionError extends Error {
25
+ readonly id: string;
26
+ readonly existingPreimage: string;
27
+ readonly conflictingPreimage: string;
28
+ readonly code = "identity_hash_collision";
29
+ constructor(id: string, existingPreimage: string, conflictingPreimage: string);
30
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Deterministic comparison and change identity encoder (`comparison-sha256-v1`).
3
+ */
4
+ import { createHash } from "node:crypto";
5
+ import { COMPARISON_NAMESPACE, CHANGE_NAMESPACE, COMPARISON_IDENTITY_SCHEME, COMPARISON_SCHEMA_VERSION, CHANGE_CONTRACT_VERSION, COMPATIBILITY_RULE_VERSION, COMPARISON_PROJECTION_VERSION, CANDIDATE_DETECTION_POLICY_VERSION } from "./version.js";
6
+ export function normalizeComparisonString(value) {
7
+ return value.normalize("NFC");
8
+ }
9
+ export function encodeComparisonPreimage(fields) {
10
+ return JSON.stringify(fields);
11
+ }
12
+ export function hashComparisonPreimage(preimageJson) {
13
+ return createHash("sha256").update(preimageJson, "utf8").digest("hex");
14
+ }
15
+ export function comparisonIdentityId(input) {
16
+ const domains = [...input.options.enabledDomains].sort();
17
+ const fields = [
18
+ COMPARISON_NAMESPACE,
19
+ COMPARISON_IDENTITY_SCHEME,
20
+ COMPARISON_SCHEMA_VERSION,
21
+ CHANGE_CONTRACT_VERSION,
22
+ COMPATIBILITY_RULE_VERSION,
23
+ COMPARISON_PROJECTION_VERSION,
24
+ CANDIDATE_DETECTION_POLICY_VERSION,
25
+ normalizeComparisonString(input.baseCatalogueId),
26
+ normalizeComparisonString(input.headCatalogueId),
27
+ JSON.stringify(domains),
28
+ normalizeComparisonString(input.options.normalizationPolicyId),
29
+ input.options.includeUnchanged
30
+ ];
31
+ const digest = hashComparisonPreimage(encodeComparisonPreimage(fields));
32
+ return `comparison:${digest}`;
33
+ }
34
+ export function changeIdentityId(input) {
35
+ const fields = [
36
+ CHANGE_NAMESPACE,
37
+ COMPARISON_IDENTITY_SCHEME,
38
+ normalizeComparisonString(input.comparisonId),
39
+ normalizeComparisonString(input.domain),
40
+ input.baseRecordId === null ? null : normalizeComparisonString(input.baseRecordId),
41
+ input.headRecordId === null ? null : normalizeComparisonString(input.headRecordId),
42
+ normalizeComparisonString(input.factualChangeKind),
43
+ input.discriminator == null || input.discriminator === ""
44
+ ? null
45
+ : normalizeComparisonString(input.discriminator)
46
+ ];
47
+ const digest = hashComparisonPreimage(encodeComparisonPreimage(fields));
48
+ return `change:${digest}`;
49
+ }
50
+ /** Fail-closed collision tracker for comparison/change IDs. */
51
+ export class ComparisonCollisionTracker {
52
+ seen = new Map();
53
+ register(id, preimage) {
54
+ const existing = this.seen.get(id);
55
+ if (existing !== undefined && existing !== preimage) {
56
+ throw new ComparisonIdentityCollisionError(id, existing, preimage);
57
+ }
58
+ this.seen.set(id, preimage);
59
+ }
60
+ }
61
+ export class ComparisonIdentityCollisionError extends Error {
62
+ id;
63
+ existingPreimage;
64
+ conflictingPreimage;
65
+ code = "identity_hash_collision";
66
+ constructor(id, existingPreimage, conflictingPreimage) {
67
+ super(`Comparison identity hash collision for ${id}`);
68
+ this.id = id;
69
+ this.existingPreimage = existingPreimage;
70
+ this.conflictingPreimage = conflictingPreimage;
71
+ this.name = "ComparisonIdentityCollisionError";
72
+ }
73
+ }
@@ -0,0 +1,14 @@
1
+ import type { ChangeImpactResult, ComparisonSnapshot } from "./schema.js";
2
+ export declare const IMPACT_DEFAULT_DEPTH = 3;
3
+ export declare const IMPACT_MAX_DEPTH = 8;
4
+ export declare const IMPACT_DEFAULT_LIMIT = 50;
5
+ export declare const IMPACT_MAX_LIMIT = 200;
6
+ export declare const IMPACT_MAX_VISITED_NODES = 10000;
7
+ export declare const IMPACT_MAX_TRAVERSED_EDGES = 50000;
8
+ export declare const IMPACT_MAX_PATHS_PER_ENDPOINT = 500;
9
+ export type GraphEdge = ComparisonSnapshot["graphIndex"]["base"][number];
10
+ export declare function getChangeImpact(comparison: ComparisonSnapshot, changeId: string, options?: {
11
+ maxDepth?: number;
12
+ limit?: number;
13
+ offset?: number;
14
+ }): ChangeImpactResult | null;