@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,387 @@
|
|
|
1
|
+
import { normalizeIdentityString } from "../catalogue/identity.js";
|
|
2
|
+
/** Version of the ranking + match-reason contract exposed to MCP callers. */
|
|
3
|
+
export const RANKING_CONTRACT_VERSION = "1";
|
|
4
|
+
/**
|
|
5
|
+
* Closed vocabulary of match reasons. Order here is also the canonical
|
|
6
|
+
* emission order for a single result's `matchReasons`.
|
|
7
|
+
*/
|
|
8
|
+
export const MATCH_REASONS = [
|
|
9
|
+
"exact_exported_name",
|
|
10
|
+
"exact_declared_name",
|
|
11
|
+
"exact_qualified_name",
|
|
12
|
+
"exact_import_path",
|
|
13
|
+
"case_insensitive_exact",
|
|
14
|
+
"whole_token",
|
|
15
|
+
"prefix",
|
|
16
|
+
"substring",
|
|
17
|
+
"public_export",
|
|
18
|
+
"package_root_export",
|
|
19
|
+
"subpath_export",
|
|
20
|
+
"canonical_source",
|
|
21
|
+
"explicit_documentation",
|
|
22
|
+
"explicit_public_api",
|
|
23
|
+
"has_explicit_example",
|
|
24
|
+
"has_explicit_test",
|
|
25
|
+
"has_direct_usage"
|
|
26
|
+
];
|
|
27
|
+
const REASON_RANK = new Map(MATCH_REASONS.map((reason, index) => [reason, index]));
|
|
28
|
+
/**
|
|
29
|
+
* Rank-affecting weights. Reasons absent from this map are explanatory only and
|
|
30
|
+
* do not influence ordering (they still appear in `matchReasons`).
|
|
31
|
+
*/
|
|
32
|
+
const REASON_WEIGHTS = {
|
|
33
|
+
exact_exported_name: 1000,
|
|
34
|
+
exact_declared_name: 1000,
|
|
35
|
+
exact_qualified_name: 700,
|
|
36
|
+
exact_import_path: 900,
|
|
37
|
+
case_insensitive_exact: 600,
|
|
38
|
+
whole_token: 300,
|
|
39
|
+
prefix: 150,
|
|
40
|
+
substring: 60,
|
|
41
|
+
public_export: 40,
|
|
42
|
+
package_root_export: 30,
|
|
43
|
+
subpath_export: 10,
|
|
44
|
+
canonical_source: 25,
|
|
45
|
+
explicit_public_api: 20,
|
|
46
|
+
has_explicit_example: 8,
|
|
47
|
+
has_explicit_test: 8,
|
|
48
|
+
has_direct_usage: 6,
|
|
49
|
+
explicit_documentation: 5
|
|
50
|
+
};
|
|
51
|
+
/** NFC-normalize and trim a raw query. */
|
|
52
|
+
export function normalizeQuery(query) {
|
|
53
|
+
return normalizeIdentityString(query ?? "").trim();
|
|
54
|
+
}
|
|
55
|
+
/** Lowercase alphanumeric tokens, splitting on separators and camelCase boundaries. */
|
|
56
|
+
export function tokenize(value) {
|
|
57
|
+
if (!value) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const spaced = normalizeIdentityString(value)
|
|
61
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
62
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
|
|
63
|
+
return spaced
|
|
64
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
65
|
+
.map((token) => token.toLowerCase())
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
}
|
|
68
|
+
function orderReasons(reasons) {
|
|
69
|
+
return [...reasons].sort((a, b) => (REASON_RANK.get(a) ?? 99) - (REASON_RANK.get(b) ?? 99));
|
|
70
|
+
}
|
|
71
|
+
function scoreReasons(reasons) {
|
|
72
|
+
let score = 0;
|
|
73
|
+
for (const reason of reasons) {
|
|
74
|
+
score += REASON_WEIGHTS[reason] ?? 0;
|
|
75
|
+
}
|
|
76
|
+
return score;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Compute name-oriented match reasons for a candidate string against the query.
|
|
80
|
+
* `exactReason` selects the exported vs declared exact-name reason.
|
|
81
|
+
*/
|
|
82
|
+
function matchName(candidate, normalizedQuery, exactReason) {
|
|
83
|
+
const reasons = [];
|
|
84
|
+
if (!candidate || !normalizedQuery) {
|
|
85
|
+
return { reasons, matched: false };
|
|
86
|
+
}
|
|
87
|
+
const name = normalizeIdentityString(candidate);
|
|
88
|
+
const nameLower = name.toLowerCase();
|
|
89
|
+
const queryLower = normalizedQuery.toLowerCase();
|
|
90
|
+
if (name === normalizedQuery) {
|
|
91
|
+
reasons.push(exactReason);
|
|
92
|
+
}
|
|
93
|
+
else if (nameLower === queryLower) {
|
|
94
|
+
reasons.push("case_insensitive_exact");
|
|
95
|
+
}
|
|
96
|
+
const nameTokens = tokenize(name);
|
|
97
|
+
if (nameTokens.includes(queryLower)) {
|
|
98
|
+
reasons.push("whole_token");
|
|
99
|
+
}
|
|
100
|
+
if (nameLower.startsWith(queryLower)) {
|
|
101
|
+
reasons.push("prefix");
|
|
102
|
+
}
|
|
103
|
+
if (nameLower.includes(queryLower)) {
|
|
104
|
+
reasons.push("substring");
|
|
105
|
+
}
|
|
106
|
+
return { reasons, matched: reasons.length > 0 };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Public helper: compute the ordered string-oriented match reasons for a
|
|
110
|
+
* candidate name against a raw query. Used by lighter-weight rankers (e.g.
|
|
111
|
+
* reuse candidates) that want explanatory reasons without full scoring.
|
|
112
|
+
*/
|
|
113
|
+
export function nameMatchReasons(candidate, query, exactReason = "exact_declared_name") {
|
|
114
|
+
const match = matchName(candidate, normalizeQuery(query), exactReason);
|
|
115
|
+
return orderReasons(new Set(match.reasons));
|
|
116
|
+
}
|
|
117
|
+
function buildFiltersApplied(filters) {
|
|
118
|
+
const applied = {};
|
|
119
|
+
const keys = [
|
|
120
|
+
"packageId",
|
|
121
|
+
"language",
|
|
122
|
+
"kind",
|
|
123
|
+
"publicOnly",
|
|
124
|
+
"capabilityId",
|
|
125
|
+
"hasExamples",
|
|
126
|
+
"hasTests",
|
|
127
|
+
"stability"
|
|
128
|
+
];
|
|
129
|
+
for (const key of keys) {
|
|
130
|
+
const value = filters[key];
|
|
131
|
+
if (value !== undefined) {
|
|
132
|
+
applied[key] = value;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return applied;
|
|
136
|
+
}
|
|
137
|
+
function classifyExport(api) {
|
|
138
|
+
if (api.exportKey === ".") {
|
|
139
|
+
return "root";
|
|
140
|
+
}
|
|
141
|
+
if (api.exportKey && api.exportKey !== ".") {
|
|
142
|
+
return "subpath";
|
|
143
|
+
}
|
|
144
|
+
const importPath = api.importPath;
|
|
145
|
+
if (!importPath) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
let rest = importPath;
|
|
149
|
+
if (rest.startsWith("@")) {
|
|
150
|
+
const parts = rest.split("/");
|
|
151
|
+
return parts.length > 2 && parts.slice(2).join("/").length > 0 ? "subpath" : "root";
|
|
152
|
+
}
|
|
153
|
+
return rest.includes("/") ? "subpath" : "root";
|
|
154
|
+
}
|
|
155
|
+
function isCanonicalSource(symbol) {
|
|
156
|
+
if (!symbol) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
if (symbol.duplicateOf) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
if (symbol.symbolProvenance === "generated_only" || symbol.symbolProvenance === "generated_declaration") {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
if (symbol.canonicalSymbolId && symbol.canonicalSymbolId !== symbol.id) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
function isPublicSymbol(symbol) {
|
|
171
|
+
return symbol.exported === true || symbol.visibility === "published" || symbol.visibility === "supported_internal";
|
|
172
|
+
}
|
|
173
|
+
function buildDirectUsageIndex(snapshot) {
|
|
174
|
+
const publicApiIds = new Set();
|
|
175
|
+
const symbolIds = new Set();
|
|
176
|
+
for (const relationship of snapshot.relationships) {
|
|
177
|
+
if (relationship.type !== "resolved_direct_usage") {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
for (const endpoint of [relationship.from, relationship.to]) {
|
|
181
|
+
if (endpoint.kind === "publicApi") {
|
|
182
|
+
publicApiIds.add(endpoint.id);
|
|
183
|
+
}
|
|
184
|
+
else if (endpoint.kind === "symbol") {
|
|
185
|
+
symbolIds.add(endpoint.id);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { publicApiIds, symbolIds };
|
|
190
|
+
}
|
|
191
|
+
/** Semantic sort key for public APIs (mirrors ordering.ts sortPublicApis). */
|
|
192
|
+
function publicApiSortKey(api) {
|
|
193
|
+
return [
|
|
194
|
+
api.packageId ?? "",
|
|
195
|
+
api.importPath ?? "",
|
|
196
|
+
api.name,
|
|
197
|
+
api.exportKind ?? "named",
|
|
198
|
+
api.id
|
|
199
|
+
]
|
|
200
|
+
.map((part) => normalizeIdentityString(String(part)))
|
|
201
|
+
.join("\u0000");
|
|
202
|
+
}
|
|
203
|
+
/** Semantic sort key for symbols (mirrors ordering.ts sortSymbols). */
|
|
204
|
+
function symbolSortKey(symbol) {
|
|
205
|
+
return [
|
|
206
|
+
symbol.packageId ?? "",
|
|
207
|
+
symbol.filePath,
|
|
208
|
+
symbol.qualifiedName,
|
|
209
|
+
symbol.kind,
|
|
210
|
+
symbol.id
|
|
211
|
+
]
|
|
212
|
+
.map((part) => normalizeIdentityString(String(part)))
|
|
213
|
+
.join("\u0000");
|
|
214
|
+
}
|
|
215
|
+
function finalize(scored) {
|
|
216
|
+
scored.sort((a, b) => {
|
|
217
|
+
if (b.score !== a.score) {
|
|
218
|
+
return b.score - a.score;
|
|
219
|
+
}
|
|
220
|
+
if (a.sortKey < b.sortKey)
|
|
221
|
+
return -1;
|
|
222
|
+
if (a.sortKey > b.sortKey)
|
|
223
|
+
return 1;
|
|
224
|
+
return 0;
|
|
225
|
+
});
|
|
226
|
+
return scored.map((entry) => ({ record: entry.record, matchReasons: entry.reasons }));
|
|
227
|
+
}
|
|
228
|
+
export function rankPublicApis(query, records, filters, snapshot) {
|
|
229
|
+
const normalizedQuery = normalizeQuery(query);
|
|
230
|
+
const symbolsById = new Map(snapshot.symbols.map((symbol) => [symbol.id, symbol]));
|
|
231
|
+
const directUsage = buildDirectUsageIndex(snapshot);
|
|
232
|
+
const scored = [];
|
|
233
|
+
for (const api of records) {
|
|
234
|
+
const symbol = api.symbolId ? symbolsById.get(api.symbolId) : undefined;
|
|
235
|
+
// Hard eligibility filters (before ranking).
|
|
236
|
+
if (filters.packageId && api.packageId !== filters.packageId)
|
|
237
|
+
continue;
|
|
238
|
+
if (filters.stability && api.stability !== filters.stability)
|
|
239
|
+
continue;
|
|
240
|
+
if (filters.capabilityId && !api.capabilityIds.includes(filters.capabilityId))
|
|
241
|
+
continue;
|
|
242
|
+
if (filters.hasExamples && api.examples.length === 0)
|
|
243
|
+
continue;
|
|
244
|
+
if (filters.hasTests && api.tests.length === 0)
|
|
245
|
+
continue;
|
|
246
|
+
if (filters.language) {
|
|
247
|
+
if (!symbol || symbol.language !== filters.language)
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
// publicOnly is a no-op for public APIs (all are public exports).
|
|
251
|
+
const reasons = new Set();
|
|
252
|
+
if (normalizedQuery) {
|
|
253
|
+
const nameMatch = matchName(api.name, normalizedQuery, "exact_exported_name");
|
|
254
|
+
for (const reason of nameMatch.reasons)
|
|
255
|
+
reasons.add(reason);
|
|
256
|
+
if (api.importPath && normalizeIdentityString(api.importPath).toLowerCase() === normalizedQuery.toLowerCase()) {
|
|
257
|
+
reasons.add("exact_import_path");
|
|
258
|
+
}
|
|
259
|
+
// Require a query match to be eligible when a query is provided.
|
|
260
|
+
if (reasons.size === 0) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
reasons.add("public_export");
|
|
265
|
+
reasons.add("explicit_public_api");
|
|
266
|
+
const exportClass = classifyExport(api);
|
|
267
|
+
if (exportClass === "root")
|
|
268
|
+
reasons.add("package_root_export");
|
|
269
|
+
else if (exportClass === "subpath")
|
|
270
|
+
reasons.add("subpath_export");
|
|
271
|
+
if (isCanonicalSource(symbol))
|
|
272
|
+
reasons.add("canonical_source");
|
|
273
|
+
if (symbol?.doc?.present)
|
|
274
|
+
reasons.add("explicit_documentation");
|
|
275
|
+
if (api.examples.length > 0)
|
|
276
|
+
reasons.add("has_explicit_example");
|
|
277
|
+
if (api.tests.length > 0)
|
|
278
|
+
reasons.add("has_explicit_test");
|
|
279
|
+
if (directUsage.publicApiIds.has(api.id))
|
|
280
|
+
reasons.add("has_direct_usage");
|
|
281
|
+
scored.push({
|
|
282
|
+
record: api,
|
|
283
|
+
reasons: orderReasons(reasons),
|
|
284
|
+
score: scoreReasons(reasons),
|
|
285
|
+
sortKey: publicApiSortKey(api)
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return finalize(scored);
|
|
289
|
+
}
|
|
290
|
+
export function rankSymbols(query, records, filters, snapshot) {
|
|
291
|
+
const normalizedQuery = normalizeQuery(query);
|
|
292
|
+
const directUsage = buildDirectUsageIndex(snapshot);
|
|
293
|
+
const symbolsWithExample = new Set();
|
|
294
|
+
for (const example of snapshot.examples) {
|
|
295
|
+
for (const symbolId of example.symbolIds)
|
|
296
|
+
symbolsWithExample.add(symbolId);
|
|
297
|
+
}
|
|
298
|
+
const symbolsWithTest = new Set();
|
|
299
|
+
for (const test of snapshot.tests) {
|
|
300
|
+
for (const symbolId of test.coversSymbols)
|
|
301
|
+
symbolsWithTest.add(symbolId);
|
|
302
|
+
}
|
|
303
|
+
const scored = [];
|
|
304
|
+
for (const symbol of records) {
|
|
305
|
+
// Hard eligibility filters (before ranking).
|
|
306
|
+
if (filters.packageId && symbol.packageId !== filters.packageId)
|
|
307
|
+
continue;
|
|
308
|
+
if (filters.language && symbol.language !== filters.language)
|
|
309
|
+
continue;
|
|
310
|
+
if (filters.kind && symbol.kind !== filters.kind)
|
|
311
|
+
continue;
|
|
312
|
+
if (filters.publicOnly && !isPublicSymbol(symbol))
|
|
313
|
+
continue;
|
|
314
|
+
if (filters.capabilityId && !symbol.capabilities.includes(filters.capabilityId))
|
|
315
|
+
continue;
|
|
316
|
+
if (filters.hasExamples && !symbolsWithExample.has(symbol.id))
|
|
317
|
+
continue;
|
|
318
|
+
if (filters.hasTests && !symbolsWithTest.has(symbol.id))
|
|
319
|
+
continue;
|
|
320
|
+
const reasons = new Set();
|
|
321
|
+
if (normalizedQuery) {
|
|
322
|
+
const nameMatch = matchName(symbol.name, normalizedQuery, "exact_declared_name");
|
|
323
|
+
for (const reason of nameMatch.reasons)
|
|
324
|
+
reasons.add(reason);
|
|
325
|
+
if (symbol.qualifiedName) {
|
|
326
|
+
const qualified = normalizeIdentityString(symbol.qualifiedName);
|
|
327
|
+
if (qualified === normalizedQuery) {
|
|
328
|
+
reasons.add("exact_qualified_name");
|
|
329
|
+
}
|
|
330
|
+
else if (qualified.toLowerCase().includes(normalizedQuery.toLowerCase())) {
|
|
331
|
+
reasons.add("substring");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (reasons.size === 0) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (isPublicSymbol(symbol))
|
|
339
|
+
reasons.add("public_export");
|
|
340
|
+
if (symbol.publicApi)
|
|
341
|
+
reasons.add("explicit_public_api");
|
|
342
|
+
if (isCanonicalSource(symbol))
|
|
343
|
+
reasons.add("canonical_source");
|
|
344
|
+
if (symbol.doc?.present)
|
|
345
|
+
reasons.add("explicit_documentation");
|
|
346
|
+
if (symbolsWithExample.has(symbol.id))
|
|
347
|
+
reasons.add("has_explicit_example");
|
|
348
|
+
if (symbolsWithTest.has(symbol.id))
|
|
349
|
+
reasons.add("has_explicit_test");
|
|
350
|
+
if (directUsage.symbolIds.has(symbol.id))
|
|
351
|
+
reasons.add("has_direct_usage");
|
|
352
|
+
scored.push({
|
|
353
|
+
record: symbol,
|
|
354
|
+
reasons: orderReasons(reasons),
|
|
355
|
+
score: scoreReasons(reasons),
|
|
356
|
+
sortKey: symbolSortKey(symbol)
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return finalize(scored);
|
|
360
|
+
}
|
|
361
|
+
/** Generic pagination + compaction helper for ranked results. */
|
|
362
|
+
export function paginateRanked(ranked, toCompact, params) {
|
|
363
|
+
const total = ranked.length;
|
|
364
|
+
const offset = Math.max(0, Math.floor(params.offset ?? 0));
|
|
365
|
+
const limit = Math.max(1, Math.floor(params.limit ?? 20));
|
|
366
|
+
const page = ranked.slice(offset, offset + limit);
|
|
367
|
+
const records = page.map((entry) => ({ ...toCompact(entry.record), matchReasons: entry.matchReasons }));
|
|
368
|
+
return {
|
|
369
|
+
total,
|
|
370
|
+
returned: records.length,
|
|
371
|
+
offset,
|
|
372
|
+
limit,
|
|
373
|
+
truncated: offset + records.length < total,
|
|
374
|
+
rankingContractVersion: RANKING_CONTRACT_VERSION,
|
|
375
|
+
filtersApplied: params.filtersApplied,
|
|
376
|
+
normalizedQuery: params.normalizedQuery,
|
|
377
|
+
records
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
export function buildRankedEnvelope(query, ranked, toCompact, filters, paging) {
|
|
381
|
+
return paginateRanked(ranked, toCompact, {
|
|
382
|
+
offset: paging.offset,
|
|
383
|
+
limit: paging.limit,
|
|
384
|
+
normalizedQuery: normalizeQuery(query),
|
|
385
|
+
filtersApplied: buildFiltersApplied(filters)
|
|
386
|
+
});
|
|
387
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only configuration recommendations (Gate 4).
|
|
3
|
+
*
|
|
4
|
+
* Detects high-noise generated trees that are *currently indexed* in the
|
|
5
|
+
* snapshot (e.g. `.next`, `dist`, `build`, `out`, `coverage`) and suggests
|
|
6
|
+
* exclude patterns to remove them from indexing. Impact is reported using the
|
|
7
|
+
* current snapshot only: `directImpact` counts existing files/symbols/packages
|
|
8
|
+
* under the tree, and `diagnosticImpact` is reported as `requires_simulation`
|
|
9
|
+
* because excluding a tree can alter resolution/unresolved-import diagnostics
|
|
10
|
+
* elsewhere and cannot be safely estimated without a re-index.
|
|
11
|
+
*
|
|
12
|
+
* This engine never re-indexes per candidate and never writes configuration.
|
|
13
|
+
*/
|
|
14
|
+
import type { CatalogueSnapshot } from "../catalogue/schema.js";
|
|
15
|
+
/** Path segments treated as high-noise generated trees. */
|
|
16
|
+
export declare const NOISE_SEGMENTS: readonly [".next", "dist", "build", "out", "coverage"];
|
|
17
|
+
export type RecommendationConfidence = "high" | "medium" | "low";
|
|
18
|
+
export type DiagnosticImpactMode = "requires_simulation" | "direct" | "not_estimable";
|
|
19
|
+
export interface ConfigRecommendation {
|
|
20
|
+
pattern: string;
|
|
21
|
+
confidence: RecommendationConfidence;
|
|
22
|
+
rationale: string;
|
|
23
|
+
directImpact: {
|
|
24
|
+
files: number;
|
|
25
|
+
symbols: number;
|
|
26
|
+
packages: number;
|
|
27
|
+
};
|
|
28
|
+
diagnosticImpact: {
|
|
29
|
+
mode: DiagnosticImpactMode;
|
|
30
|
+
value: number | null;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export interface RecommendConfigResult {
|
|
34
|
+
summary: {
|
|
35
|
+
filesTotal: number;
|
|
36
|
+
candidatesTotal: number;
|
|
37
|
+
candidatesReturned: number;
|
|
38
|
+
truncated: boolean;
|
|
39
|
+
filesImpacted: number;
|
|
40
|
+
};
|
|
41
|
+
recommendations: ConfigRecommendation[];
|
|
42
|
+
}
|
|
43
|
+
export interface RecommendConfigOptions {
|
|
44
|
+
/** Maximum number of recommendations returned. Default 50. */
|
|
45
|
+
limit?: number;
|
|
46
|
+
}
|
|
47
|
+
export declare function recommendConfig(snapshot: CatalogueSnapshot, options?: RecommendConfigOptions): RecommendConfigResult;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** Path segments treated as high-noise generated trees. */
|
|
2
|
+
export const NOISE_SEGMENTS = [".next", "dist", "build", "out", "coverage"];
|
|
3
|
+
const DEFAULT_LIMIT = 50;
|
|
4
|
+
/** Normalize a path to forward slashes for segment analysis. */
|
|
5
|
+
function normalizePath(value) {
|
|
6
|
+
return value.replaceAll("\\", "/");
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Find the first noise segment in a path and return the tree prefix up to and
|
|
10
|
+
* including it, or undefined when no noise segment is present.
|
|
11
|
+
*/
|
|
12
|
+
function findNoiseTree(filePath) {
|
|
13
|
+
const segments = normalizePath(filePath).split("/").filter((segment) => segment.length > 0);
|
|
14
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
15
|
+
const segment = segments[index];
|
|
16
|
+
if (NOISE_SEGMENTS.includes(segment)) {
|
|
17
|
+
return { prefix: segments.slice(0, index + 1).join("/"), segment };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
function isUnder(path, prefix) {
|
|
23
|
+
const normalized = normalizePath(path);
|
|
24
|
+
return normalized === prefix || normalized.startsWith(`${prefix}/`);
|
|
25
|
+
}
|
|
26
|
+
export function recommendConfig(snapshot, options = {}) {
|
|
27
|
+
const limit = normalizeLimit(options.limit);
|
|
28
|
+
const trees = new Map();
|
|
29
|
+
for (const file of snapshot.files) {
|
|
30
|
+
const tree = findNoiseTree(file.path);
|
|
31
|
+
if (!tree) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const pattern = `${tree.prefix}/**`;
|
|
35
|
+
const existing = trees.get(pattern);
|
|
36
|
+
if (existing) {
|
|
37
|
+
existing.filePaths.push(file.path);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
trees.set(pattern, { pattern, segment: tree.segment, prefix: tree.prefix, filePaths: [file.path] });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const filesByPath = new Map(snapshot.files.map((file) => [file.path, file]));
|
|
44
|
+
const impactedFilePaths = new Set();
|
|
45
|
+
const recommendations = [];
|
|
46
|
+
for (const tree of trees.values()) {
|
|
47
|
+
const files = tree.filePaths.length;
|
|
48
|
+
for (const filePath of tree.filePaths) {
|
|
49
|
+
impactedFilePaths.add(filePath);
|
|
50
|
+
}
|
|
51
|
+
const symbols = snapshot.symbols.filter((symbol) => isUnder(symbol.filePath, tree.prefix)).length;
|
|
52
|
+
const packages = snapshot.packages.filter((pkg) => pkg.path !== "." && isUnder(pkg.path, tree.prefix)).length;
|
|
53
|
+
const generatedCount = tree.filePaths.filter((filePath) => {
|
|
54
|
+
const file = filesByPath.get(filePath);
|
|
55
|
+
return Boolean(file && (file.isGenerated || file.kind === "generated"));
|
|
56
|
+
}).length;
|
|
57
|
+
const confidence = classifyConfidence(generatedCount, files);
|
|
58
|
+
recommendations.push({
|
|
59
|
+
pattern: tree.pattern,
|
|
60
|
+
confidence,
|
|
61
|
+
rationale: buildRationale(tree.segment, tree.prefix, files, symbols, generatedCount),
|
|
62
|
+
directImpact: { files, symbols, packages },
|
|
63
|
+
diagnosticImpact: { mode: "requires_simulation", value: null }
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
recommendations.sort((a, b) => {
|
|
67
|
+
const confidenceDiff = confidenceRank(a.confidence) - confidenceRank(b.confidence);
|
|
68
|
+
if (confidenceDiff !== 0) {
|
|
69
|
+
return confidenceDiff;
|
|
70
|
+
}
|
|
71
|
+
if (b.directImpact.files !== a.directImpact.files) {
|
|
72
|
+
return b.directImpact.files - a.directImpact.files;
|
|
73
|
+
}
|
|
74
|
+
return a.pattern.localeCompare(b.pattern);
|
|
75
|
+
});
|
|
76
|
+
const returned = recommendations.slice(0, limit);
|
|
77
|
+
return {
|
|
78
|
+
summary: {
|
|
79
|
+
filesTotal: snapshot.files.length,
|
|
80
|
+
candidatesTotal: recommendations.length,
|
|
81
|
+
candidatesReturned: returned.length,
|
|
82
|
+
truncated: returned.length < recommendations.length,
|
|
83
|
+
filesImpacted: impactedFilePaths.size
|
|
84
|
+
},
|
|
85
|
+
recommendations: returned
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function classifyConfidence(generatedCount, files) {
|
|
89
|
+
if (files === 0) {
|
|
90
|
+
return "low";
|
|
91
|
+
}
|
|
92
|
+
if (generatedCount === files) {
|
|
93
|
+
return "high";
|
|
94
|
+
}
|
|
95
|
+
if (generatedCount * 2 >= files) {
|
|
96
|
+
return "medium";
|
|
97
|
+
}
|
|
98
|
+
return "low";
|
|
99
|
+
}
|
|
100
|
+
function buildRationale(segment, prefix, files, symbols, generatedCount) {
|
|
101
|
+
return (`Tree "${prefix}" matches the high-noise generated directory segment "${segment}" and currently contributes `
|
|
102
|
+
+ `${files} indexed file(s) and ${symbols} symbol(s) (${generatedCount} file(s) already classified as generated). `
|
|
103
|
+
+ "Excluding it should reduce catalogue noise. Advisory only; configuration is never written automatically.");
|
|
104
|
+
}
|
|
105
|
+
function confidenceRank(confidence) {
|
|
106
|
+
if (confidence === "high") {
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
if (confidence === "medium") {
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
return 2;
|
|
113
|
+
}
|
|
114
|
+
function normalizeLimit(value) {
|
|
115
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
116
|
+
return DEFAULT_LIMIT;
|
|
117
|
+
}
|
|
118
|
+
const floored = Math.floor(value);
|
|
119
|
+
if (floored <= 0) {
|
|
120
|
+
return DEFAULT_LIMIT;
|
|
121
|
+
}
|
|
122
|
+
return floored;
|
|
123
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CatalogueSnapshot, RelationshipRecord } from "../catalogue/schema.js";
|
|
2
|
+
export declare function listRelationships(snapshot: CatalogueSnapshot): RelationshipRecord[];
|
|
3
|
+
export declare function listImportRelationships(snapshot: CatalogueSnapshot): RelationshipRecord[];
|
|
4
|
+
export declare function listExportRelationships(snapshot: CatalogueSnapshot): RelationshipRecord[];
|
|
5
|
+
export declare function listRelationshipsByFile(snapshot: CatalogueSnapshot, filePath: string): RelationshipRecord[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function listRelationships(snapshot) {
|
|
2
|
+
return snapshot.relationships;
|
|
3
|
+
}
|
|
4
|
+
export function listImportRelationships(snapshot) {
|
|
5
|
+
return snapshot.relationships.filter((relationship) => relationship.type === "imports");
|
|
6
|
+
}
|
|
7
|
+
export function listExportRelationships(snapshot) {
|
|
8
|
+
return snapshot.relationships.filter((relationship) => relationship.type === "exports");
|
|
9
|
+
}
|
|
10
|
+
export function listRelationshipsByFile(snapshot, filePath) {
|
|
11
|
+
return snapshot.relationships.filter((relationship) => {
|
|
12
|
+
return (relationship.from.kind === "file" && relationship.from.id === filePath)
|
|
13
|
+
|| (relationship.to.kind === "file" && relationship.to.id === filePath);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CatalogueSnapshot } from "../catalogue/schema.js";
|
|
2
|
+
import { type MatchReason } from "./ranking.js";
|
|
3
|
+
export interface FindReuseCandidatesOptions {
|
|
4
|
+
query: string;
|
|
5
|
+
capabilityId?: string;
|
|
6
|
+
packageId?: string;
|
|
7
|
+
limit?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ReuseCandidate {
|
|
10
|
+
kind: "capability" | "publicApi";
|
|
11
|
+
id: string;
|
|
12
|
+
score: number;
|
|
13
|
+
reasons: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface ReuseCandidateResult {
|
|
16
|
+
query: string;
|
|
17
|
+
tokens: string[];
|
|
18
|
+
candidates: ReuseCandidate[];
|
|
19
|
+
}
|
|
20
|
+
export declare function findReuseCandidates(snapshot: CatalogueSnapshot, options: FindReuseCandidatesOptions): ReuseCandidateResult;
|
|
21
|
+
export interface RankedReuseCandidate extends ReuseCandidate {
|
|
22
|
+
matchReasons: MatchReason[];
|
|
23
|
+
}
|
|
24
|
+
export interface RankedReuseCandidateResult {
|
|
25
|
+
query: string;
|
|
26
|
+
tokens: string[];
|
|
27
|
+
candidates: RankedReuseCandidate[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Thin wrapper over {@link findReuseCandidates} that annotates each candidate
|
|
31
|
+
* with closed-vocabulary match reasons derived from its name. Ordering and
|
|
32
|
+
* scoring are unchanged from the underlying reuse scorer.
|
|
33
|
+
*/
|
|
34
|
+
export declare function rankReuseCandidates(snapshot: CatalogueSnapshot, options: FindReuseCandidatesOptions): RankedReuseCandidateResult;
|