@davesheffer/hunch 1.18.1 → 1.19.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/README.md +59 -2
- package/dist/cli/index.js +110 -7
- package/dist/core/agenthook.js +41 -0
- package/dist/core/correctionStage.js +513 -0
- package/dist/core/declarationClusters.js +321 -0
- package/dist/core/delivery.js +307 -6
- package/dist/core/evidenceMap.js +202 -0
- package/dist/core/jsonc.js +72 -0
- package/dist/core/pipeline.js +953 -10
- package/dist/extractors/correctionSources.js +40 -0
- package/dist/extractors/landscapeDiscovery.js +1205 -0
- package/dist/integrations/providers.js +2 -78
- package/dist/integrations/scaffold.js +6 -0
- package/dist/mcp/server.js +84 -2
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export const FILE_FIRST_DECLARATION_CLUSTER_RULE = "flat-file-anchored-semantic-clusters-v3";
|
|
3
|
+
export const FILE_FIRST_FILE_LIMIT = 5;
|
|
4
|
+
export const FILE_FIRST_CLUSTER_LIMIT = 2;
|
|
5
|
+
export const FILE_FIRST_MEMBER_LIMIT = 3;
|
|
6
|
+
export const PROGRESSIVE_DECLARATION_PLAN_RULE = "promoted-only-progressive-inspection-v4";
|
|
7
|
+
export const PROGRESSIVE_DECLARATION_BASELINE_LIMIT = 5;
|
|
8
|
+
export const PROGRESSIVE_DECLARATION_PRIMARY_LIMIT = 10;
|
|
9
|
+
export const PROGRESSIVE_DECLARATION_TOTAL_LIMIT = 11;
|
|
10
|
+
export const FILE_FIRST_DECLARATION_CLUSTER_TRANSFER = {
|
|
11
|
+
benchmark: "flat-file-anchored-clusters-transfer-v3",
|
|
12
|
+
scorable_tasks: 12,
|
|
13
|
+
baseline_top_five_hits: 3,
|
|
14
|
+
combined_hits: 6,
|
|
15
|
+
combined_improvement_points: 0.25,
|
|
16
|
+
baseline_file_hits: 8,
|
|
17
|
+
cluster_file_hits: 10,
|
|
18
|
+
rescues: 3,
|
|
19
|
+
average_inspected_declarations: 18.83,
|
|
20
|
+
max_inspected_declarations: 24,
|
|
21
|
+
decision: "promoted-supplemental-diagnostic",
|
|
22
|
+
exact_owner_policy: "disabled",
|
|
23
|
+
};
|
|
24
|
+
export const PROGRESSIVE_DECLARATION_DEVELOPMENT = {
|
|
25
|
+
benchmark: "revealed-cluster-transfer-v1-v2-v3-replay",
|
|
26
|
+
scorable_tasks: 36,
|
|
27
|
+
baseline_top_five_hits: 15,
|
|
28
|
+
progressive_plan_hits: 21,
|
|
29
|
+
full_cluster_union_hits: 21,
|
|
30
|
+
previous_average_inspected_declarations: 19.81,
|
|
31
|
+
progressive_max_inspected_declarations: 11,
|
|
32
|
+
inspection_reduction: 0.4446,
|
|
33
|
+
decision: "candidate-awaiting-fresh-transfer",
|
|
34
|
+
exact_owner_policy: "disabled",
|
|
35
|
+
};
|
|
36
|
+
export const PROGRESSIVE_DECLARATION_TRANSFER = {
|
|
37
|
+
benchmark: "progressive-inspection-cross-repository-transfer-v4",
|
|
38
|
+
scorable_tasks: 12,
|
|
39
|
+
baseline_top_five_hits: 5,
|
|
40
|
+
progressive_plan_hits: 5,
|
|
41
|
+
full_cluster_union_hits: 5,
|
|
42
|
+
rescues: 0,
|
|
43
|
+
losses: 0,
|
|
44
|
+
average_inspected_declarations: 11,
|
|
45
|
+
previous_average_inspected_declarations: 18.92,
|
|
46
|
+
inspection_reduction: 0.4185,
|
|
47
|
+
decision: "retain-efficiency-advisory-v4",
|
|
48
|
+
accuracy_promotion: "rejected-no-fresh-rescue",
|
|
49
|
+
exact_owner_policy: "disabled",
|
|
50
|
+
};
|
|
51
|
+
const SEMANTIC_STOP_WORDS = new Set([
|
|
52
|
+
"src", "source", "lib", "library", "package", "packages", "core", "index",
|
|
53
|
+
"schema", "schemas", "type", "types", "zod", "mini", "internals", "internal",
|
|
54
|
+
"params", "param", "options", "option", "context", "config", "props", "def",
|
|
55
|
+
"input", "output", "result", "results", "handle", "handler", "get", "set",
|
|
56
|
+
"make", "create", "build", "process", "processor", "run", "apply", "with",
|
|
57
|
+
"from", "into", "this", "that", "value", "values",
|
|
58
|
+
]);
|
|
59
|
+
function ownerPath(owner) {
|
|
60
|
+
return owner.slice(0, owner.indexOf("::"));
|
|
61
|
+
}
|
|
62
|
+
function ownerSymbol(owner) {
|
|
63
|
+
return owner.slice(owner.indexOf("::") + 2);
|
|
64
|
+
}
|
|
65
|
+
function semanticTerms(symbol) {
|
|
66
|
+
return [...new Set(symbol
|
|
67
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
68
|
+
.replace(/([A-Z])([A-Z][a-z])/g, "$1 $2")
|
|
69
|
+
.toLowerCase()
|
|
70
|
+
.split(/[^a-z0-9]+/)
|
|
71
|
+
.filter((term) => term.length >= 3 && !SEMANTIC_STOP_WORDS.has(term)))];
|
|
72
|
+
}
|
|
73
|
+
function fallbackTerm(symbol) {
|
|
74
|
+
return symbol.replace(/[^A-Za-z0-9]+/g, "").toLowerCase() || "anonymous";
|
|
75
|
+
}
|
|
76
|
+
function overlap(left, right) {
|
|
77
|
+
const rightSet = new Set(right);
|
|
78
|
+
return left.filter((term) => rightSet.has(term)).length;
|
|
79
|
+
}
|
|
80
|
+
function shortHash(value) {
|
|
81
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 24);
|
|
82
|
+
}
|
|
83
|
+
function clusterFileDeclarations(path, candidates, clusterLimit, memberLimit) {
|
|
84
|
+
const clusters = [];
|
|
85
|
+
for (const item of candidates) {
|
|
86
|
+
const symbol = ownerSymbol(item.candidate.owner);
|
|
87
|
+
const terms = semanticTerms(symbol);
|
|
88
|
+
const effectiveTerms = terms.length ? terms : [fallbackTerm(symbol)];
|
|
89
|
+
const matching = clusters.map((cluster, index) => ({ index, overlap: overlap(effectiveTerms, cluster.terms) }))
|
|
90
|
+
.filter((entry) => entry.overlap > 0)
|
|
91
|
+
.sort((left, right) => right.overlap - left.overlap || left.index - right.index)[0];
|
|
92
|
+
const member = {
|
|
93
|
+
owner: item.candidate.owner,
|
|
94
|
+
global_rank: item.globalRank,
|
|
95
|
+
static_score: item.candidate.score,
|
|
96
|
+
runtime_declaration: item.candidate.runtime_declaration,
|
|
97
|
+
type_scaffolding: item.candidate.type_scaffolding,
|
|
98
|
+
};
|
|
99
|
+
if (matching) {
|
|
100
|
+
const cluster = clusters[matching.index];
|
|
101
|
+
cluster.terms = [...new Set([...cluster.terms, ...effectiveTerms])].sort();
|
|
102
|
+
cluster.members.push(member);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
clusters.push({ terms: effectiveTerms, label: effectiveTerms.join(" / "), members: [member] });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return clusters.slice(0, clusterLimit).map((cluster) => {
|
|
109
|
+
const members = cluster.members.slice(0, memberLimit);
|
|
110
|
+
const representative = members[0];
|
|
111
|
+
const value = {
|
|
112
|
+
path,
|
|
113
|
+
terms: cluster.terms,
|
|
114
|
+
representative: representative.owner,
|
|
115
|
+
members: members.map((member) => member.owner),
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
cluster_id: shortHash(value),
|
|
119
|
+
label: cluster.label,
|
|
120
|
+
semantic_terms: cluster.terms,
|
|
121
|
+
representative: representative.owner,
|
|
122
|
+
representative_global_rank: representative.global_rank,
|
|
123
|
+
members,
|
|
124
|
+
members_truncated: Math.max(0, cluster.members.length - members.length),
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/** Build a hierarchical diagnostic from the existing static ranking. The flat
|
|
129
|
+
* shortlist remains untouched: this view collapses related declaration
|
|
130
|
+
* scaffolding into semantic families so one noisy family cannot consume every
|
|
131
|
+
* inspection slot inside a promising file. It is still a candidate view, not
|
|
132
|
+
* an exact-owner claim. */
|
|
133
|
+
export function buildFileFirstDeclarationClusters(rankedCandidatesValue, requestedFileLimit = FILE_FIRST_FILE_LIMIT, requestedClusterLimit = FILE_FIRST_CLUSTER_LIMIT, requestedMemberLimit = FILE_FIRST_MEMBER_LIMIT) {
|
|
134
|
+
const fileLimit = Number.isSafeInteger(requestedFileLimit)
|
|
135
|
+
? Math.max(1, Math.min(FILE_FIRST_FILE_LIMIT, requestedFileLimit))
|
|
136
|
+
: FILE_FIRST_FILE_LIMIT;
|
|
137
|
+
const clusterLimit = Number.isSafeInteger(requestedClusterLimit)
|
|
138
|
+
? Math.max(1, Math.min(FILE_FIRST_CLUSTER_LIMIT, requestedClusterLimit))
|
|
139
|
+
: FILE_FIRST_CLUSTER_LIMIT;
|
|
140
|
+
const memberLimit = Number.isSafeInteger(requestedMemberLimit)
|
|
141
|
+
? Math.max(1, Math.min(FILE_FIRST_MEMBER_LIMIT, requestedMemberLimit))
|
|
142
|
+
: FILE_FIRST_MEMBER_LIMIT;
|
|
143
|
+
const distinctCandidates = rankedCandidatesValue.filter((candidate, index, all) => all.findIndex((entry) => entry.owner === candidate.owner) === index);
|
|
144
|
+
const byFile = new Map();
|
|
145
|
+
distinctCandidates.forEach((candidate, index) => {
|
|
146
|
+
const path = ownerPath(candidate.owner);
|
|
147
|
+
const entries = byFile.get(path) ?? [];
|
|
148
|
+
entries.push({ candidate, globalRank: index + 1 });
|
|
149
|
+
byFile.set(path, entries);
|
|
150
|
+
});
|
|
151
|
+
const prepared = [...byFile].map(([path, entries]) => {
|
|
152
|
+
const allClusters = clusterFileDeclarations(path, entries, Number.MAX_SAFE_INTEGER, memberLimit);
|
|
153
|
+
const scoringClusters = allClusters.slice(0, clusterLimit);
|
|
154
|
+
return {
|
|
155
|
+
path,
|
|
156
|
+
firstCandidateRank: entries[0].globalRank,
|
|
157
|
+
fileScore: Math.round(scoringClusters.reduce((sum, cluster) => sum + (cluster.members[0]?.static_score ?? 0), 0) * 100) / 100,
|
|
158
|
+
clusters: scoringClusters,
|
|
159
|
+
};
|
|
160
|
+
}).sort((left, right) => right.fileScore - left.fileScore
|
|
161
|
+
|| left.firstCandidateRank - right.firstCandidateRank
|
|
162
|
+
|| left.path.localeCompare(right.path));
|
|
163
|
+
// The first design re-ranked files by aggregate family score and lost a file
|
|
164
|
+
// already represented by the working flat shortlist on fresh transfer. V2
|
|
165
|
+
// makes the hierarchy supplemental in fact as well as name: preserve every
|
|
166
|
+
// distinct file from the flat top five first, then use aggregate scoring only
|
|
167
|
+
// to fill any remaining file slots.
|
|
168
|
+
const flatFileAnchors = [...new Set(distinctCandidates.slice(0, 5).map((candidate) => ownerPath(candidate.owner)))];
|
|
169
|
+
const selectedPaths = [...new Set([...flatFileAnchors, ...prepared.map((entry) => entry.path)])].slice(0, fileLimit);
|
|
170
|
+
const selected = selectedPaths.flatMap((path) => {
|
|
171
|
+
const entry = prepared.find((candidate) => candidate.path === path);
|
|
172
|
+
return entry ? [entry] : [];
|
|
173
|
+
});
|
|
174
|
+
const files = selected.map((entry, index) => ({
|
|
175
|
+
path: entry.path,
|
|
176
|
+
file_rank: index + 1,
|
|
177
|
+
file_score: entry.fileScore,
|
|
178
|
+
first_candidate_rank: entry.firstCandidateRank,
|
|
179
|
+
declaration_clusters: entry.clusters,
|
|
180
|
+
}));
|
|
181
|
+
const receiptWithoutId = {
|
|
182
|
+
version: 3,
|
|
183
|
+
rule: FILE_FIRST_DECLARATION_CLUSTER_RULE,
|
|
184
|
+
source_candidates: distinctCandidates.length,
|
|
185
|
+
file_limit: fileLimit,
|
|
186
|
+
cluster_limit_per_file: clusterLimit,
|
|
187
|
+
member_limit_per_cluster: memberLimit,
|
|
188
|
+
selected_files: files.map((file) => file.path),
|
|
189
|
+
selected_cluster_ids: files.flatMap((file) => file.declaration_clusters.map((cluster) => cluster.cluster_id)),
|
|
190
|
+
file_selection_strategy: "flat-shortlist-file-anchor",
|
|
191
|
+
flat_shortlist_anchor_size: 5,
|
|
192
|
+
flat_shortlist_preserved: true,
|
|
193
|
+
exact_owner_enabled: false,
|
|
194
|
+
};
|
|
195
|
+
return {
|
|
196
|
+
files,
|
|
197
|
+
receipt: { ...receiptWithoutId, receipt_id: shortHash(receiptWithoutId) },
|
|
198
|
+
transfer_calibration: FILE_FIRST_DECLARATION_CLUSTER_TRANSFER,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** Turn the promoted flat shortlist and semantic clusters into a progressive
|
|
202
|
+
* inspection queue. The first five candidates remain byte-for-byte intact.
|
|
203
|
+
* Supplemental candidates must already belong to a selected semantic family,
|
|
204
|
+
* and are ordered by the frozen repository-adaptive rank instead of widening
|
|
205
|
+
* to every declaration in the file. The last slot is explicitly a fallback so
|
|
206
|
+
* callers can stop after ten inspections when the behavior is already owned. */
|
|
207
|
+
export function buildProgressiveDeclarationPlan(rankedCandidatesValue, clusters, requestedBaselineLimit = PROGRESSIVE_DECLARATION_BASELINE_LIMIT, requestedTotalLimit = PROGRESSIVE_DECLARATION_TOTAL_LIMIT) {
|
|
208
|
+
const baselineLimit = Number.isSafeInteger(requestedBaselineLimit)
|
|
209
|
+
? Math.max(1, Math.min(PROGRESSIVE_DECLARATION_BASELINE_LIMIT, requestedBaselineLimit))
|
|
210
|
+
: PROGRESSIVE_DECLARATION_BASELINE_LIMIT;
|
|
211
|
+
const totalLimit = Number.isSafeInteger(requestedTotalLimit)
|
|
212
|
+
? Math.max(baselineLimit, Math.min(PROGRESSIVE_DECLARATION_TOTAL_LIMIT, requestedTotalLimit))
|
|
213
|
+
: PROGRESSIVE_DECLARATION_TOTAL_LIMIT;
|
|
214
|
+
const primaryLimit = Math.min(PROGRESSIVE_DECLARATION_PRIMARY_LIMIT, totalLimit);
|
|
215
|
+
const distinctCandidates = rankedCandidatesValue.filter((candidate, index, all) => all.findIndex((entry) => entry.owner === candidate.owner) === index);
|
|
216
|
+
const byOwner = new Map(distinctCandidates.map((candidate, index) => [candidate.owner, {
|
|
217
|
+
candidate,
|
|
218
|
+
globalRank: index + 1,
|
|
219
|
+
}]));
|
|
220
|
+
const clusterMembership = new Map();
|
|
221
|
+
for (const file of clusters.files) {
|
|
222
|
+
for (const cluster of file.declaration_clusters) {
|
|
223
|
+
for (const member of cluster.members) {
|
|
224
|
+
if (!clusterMembership.has(member.owner)) {
|
|
225
|
+
clusterMembership.set(member.owner, {
|
|
226
|
+
clusterId: cluster.cluster_id,
|
|
227
|
+
clusterLabel: cluster.label,
|
|
228
|
+
filePath: file.path,
|
|
229
|
+
fileRank: file.file_rank,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const baselineOwners = distinctCandidates.slice(0, baselineLimit).map((candidate) => candidate.owner);
|
|
236
|
+
const baselineSet = new Set(baselineOwners);
|
|
237
|
+
const expansion = clusters.files.flatMap((file) => file.declaration_clusters.flatMap((cluster) => cluster.members.map((member) => ({ member, fileRank: file.file_rank, clusterId: cluster.cluster_id }))))
|
|
238
|
+
.filter((entry, index, all) => !baselineSet.has(entry.member.owner)
|
|
239
|
+
&& all.findIndex((candidate) => candidate.member.owner === entry.member.owner) === index)
|
|
240
|
+
.sort((left, right) => left.member.global_rank - right.member.global_rank
|
|
241
|
+
|| Number(right.member.runtime_declaration) - Number(left.member.runtime_declaration)
|
|
242
|
+
|| Number(left.member.type_scaffolding) - Number(right.member.type_scaffolding)
|
|
243
|
+
|| left.fileRank - right.fileRank
|
|
244
|
+
|| left.clusterId.localeCompare(right.clusterId)
|
|
245
|
+
|| left.member.owner.localeCompare(right.member.owner))
|
|
246
|
+
.slice(0, Math.max(0, totalLimit - baselineOwners.length));
|
|
247
|
+
const baseline = baselineOwners.flatMap((owner, index) => {
|
|
248
|
+
const ranked = byOwner.get(owner);
|
|
249
|
+
if (!ranked)
|
|
250
|
+
return [];
|
|
251
|
+
const membership = clusterMembership.get(owner);
|
|
252
|
+
return [{
|
|
253
|
+
owner,
|
|
254
|
+
inspection_rank: index + 1,
|
|
255
|
+
phase: "flat-shortlist",
|
|
256
|
+
file_path: ownerPath(owner),
|
|
257
|
+
global_rank: ranked.globalRank,
|
|
258
|
+
cluster_id: membership?.clusterId ?? null,
|
|
259
|
+
cluster_label: membership?.clusterLabel ?? null,
|
|
260
|
+
runtime_declaration: ranked.candidate.runtime_declaration,
|
|
261
|
+
type_scaffolding: ranked.candidate.type_scaffolding,
|
|
262
|
+
}];
|
|
263
|
+
});
|
|
264
|
+
const supplemental = expansion.map((entry, index) => {
|
|
265
|
+
const membership = clusterMembership.get(entry.member.owner);
|
|
266
|
+
const inspectionRank = baseline.length + index + 1;
|
|
267
|
+
return {
|
|
268
|
+
owner: entry.member.owner,
|
|
269
|
+
inspection_rank: inspectionRank,
|
|
270
|
+
phase: inspectionRank <= primaryLimit ? "cluster-expansion" : "cluster-fallback",
|
|
271
|
+
file_path: membership?.filePath ?? ownerPath(entry.member.owner),
|
|
272
|
+
global_rank: entry.member.global_rank,
|
|
273
|
+
cluster_id: membership?.clusterId ?? null,
|
|
274
|
+
cluster_label: membership?.clusterLabel ?? null,
|
|
275
|
+
runtime_declaration: entry.member.runtime_declaration,
|
|
276
|
+
type_scaffolding: entry.member.type_scaffolding,
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
const candidates = [...baseline, ...supplemental];
|
|
280
|
+
const phases = [
|
|
281
|
+
{
|
|
282
|
+
phase: "flat-shortlist",
|
|
283
|
+
instruction: "Inspect the preserved repository-adaptive shortlist first.",
|
|
284
|
+
stop_condition: "Stop when one candidate explains and reproduces the behavior; otherwise expand within the anchored files.",
|
|
285
|
+
candidates: baseline,
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
phase: "cluster-expansion",
|
|
289
|
+
instruction: "Inspect the strongest remaining declarations from the selected semantic families.",
|
|
290
|
+
stop_condition: "Stop by inspection ten when a family owns the behavior; use the final fallback only while ownership remains unresolved.",
|
|
291
|
+
candidates: supplemental.filter((candidate) => candidate.phase === "cluster-expansion"),
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
phase: "cluster-fallback",
|
|
295
|
+
instruction: "Inspect one final ranked family member without widening to another file.",
|
|
296
|
+
stop_condition: "After this bounded fallback, report uncertainty instead of generating more owner guesses.",
|
|
297
|
+
candidates: supplemental.filter((candidate) => candidate.phase === "cluster-fallback"),
|
|
298
|
+
},
|
|
299
|
+
];
|
|
300
|
+
const receiptWithoutId = {
|
|
301
|
+
version: 1,
|
|
302
|
+
rule: PROGRESSIVE_DECLARATION_PLAN_RULE,
|
|
303
|
+
baseline_limit: baselineLimit,
|
|
304
|
+
primary_limit: primaryLimit,
|
|
305
|
+
total_limit: totalLimit,
|
|
306
|
+
selected_owners: candidates.map((candidate) => candidate.owner),
|
|
307
|
+
selected_cluster_ids: [...new Set(candidates.flatMap((candidate) => candidate.cluster_id ? [candidate.cluster_id] : []))],
|
|
308
|
+
promoted_mechanisms: ["repository-adaptive-ranking", "flat-file-anchored-semantic-clusters"],
|
|
309
|
+
rejected_rerankers_disabled: true,
|
|
310
|
+
flat_shortlist_preserved: true,
|
|
311
|
+
exact_owner_enabled: false,
|
|
312
|
+
};
|
|
313
|
+
return {
|
|
314
|
+
phases,
|
|
315
|
+
candidates,
|
|
316
|
+
receipt: { ...receiptWithoutId, receipt_id: shortHash(receiptWithoutId) },
|
|
317
|
+
development_calibration: PROGRESSIVE_DECLARATION_DEVELOPMENT,
|
|
318
|
+
transfer_calibration: PROGRESSIVE_DECLARATION_TRANSFER,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
//# sourceMappingURL=declarationClusters.js.map
|