@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
package/dist/core/delivery.js
CHANGED
|
@@ -13,6 +13,15 @@ import { pathMatchesGlob, pathsRelated } from "./glob.js";
|
|
|
13
13
|
import { toPosixTarget } from "./paths.js";
|
|
14
14
|
import { renderGrounding } from "./topics.js";
|
|
15
15
|
const SEVERITY = { advisory: 1, warning: 2, blocking: 3, low: 1, medium: 2, high: 3, critical: 4 };
|
|
16
|
+
const MIN_ADVISORY_CONFIDENCE = 0.5;
|
|
17
|
+
const MIN_UNCONDITIONED_CONFIDENCE = 0.7;
|
|
18
|
+
const MAX_ACTIONABLE_HYPOTHESES = 2;
|
|
19
|
+
const TASK_STOP_WORDS = new Set([
|
|
20
|
+
"a", "an", "and", "are", "as", "at", "be", "been", "but", "by", "can", "does", "for", "from",
|
|
21
|
+
"has", "have", "in", "into", "is", "it", "its", "of", "on", "or", "that", "the", "this", "to",
|
|
22
|
+
"use", "uses", "using", "was", "when", "where", "which", "while", "with", "without",
|
|
23
|
+
"bug", "fix", "issue", "problem",
|
|
24
|
+
]);
|
|
16
25
|
function clipHeadline(value, max) {
|
|
17
26
|
const flat = value.replace(/\s+/g, " ").trim();
|
|
18
27
|
if (flat.length <= max)
|
|
@@ -29,6 +38,217 @@ function sourceTier(source) {
|
|
|
29
38
|
return "model";
|
|
30
39
|
return source || "unknown";
|
|
31
40
|
}
|
|
41
|
+
function stemToken(token) {
|
|
42
|
+
if (!/^[a-z0-9]+$/.test(token))
|
|
43
|
+
return token;
|
|
44
|
+
if (token.endsWith("ies") && token.length > 5)
|
|
45
|
+
return `${token.slice(0, -3)}y`;
|
|
46
|
+
for (const suffix of ["ing", "ed"]) {
|
|
47
|
+
if (token.endsWith(suffix) && token.length - suffix.length >= 4) {
|
|
48
|
+
const base = token.slice(0, -suffix.length);
|
|
49
|
+
if (base.endsWith("s") && !base.endsWith("ss"))
|
|
50
|
+
return `${base}e`;
|
|
51
|
+
return base;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (/(?:sses|xes|zes|ches|shes)$/.test(token) && token.length > 5)
|
|
55
|
+
return token.slice(0, -2);
|
|
56
|
+
if (token.endsWith("s") && !token.endsWith("ss") && token.length > 4)
|
|
57
|
+
return token.slice(0, -1);
|
|
58
|
+
return token;
|
|
59
|
+
}
|
|
60
|
+
function lexicalTokens(value) {
|
|
61
|
+
const expanded = value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase();
|
|
62
|
+
const words = expanded.match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
63
|
+
return new Set(words.map(stemToken).filter((token) => token.length >= 3 && !TASK_STOP_WORDS.has(token)));
|
|
64
|
+
}
|
|
65
|
+
function codeSyntaxTokens(value) {
|
|
66
|
+
const out = new Set();
|
|
67
|
+
for (const fragment of value.split(/\s+/)) {
|
|
68
|
+
// Code-shaped spans identify the API/file nouns without treating every symbol
|
|
69
|
+
// name anywhere in a large repository as a noun. The latter erased ordinary
|
|
70
|
+
// evidence words such as "empty" and "throws" in real task phrases.
|
|
71
|
+
if (!/[.$()[\]`/\\_:<>]/.test(fragment))
|
|
72
|
+
continue;
|
|
73
|
+
for (const token of lexicalTokens(fragment))
|
|
74
|
+
out.add(token);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/** Separate task evidence (symptoms/expected behavior) from code nouns. A file,
|
|
79
|
+
* symbol, or API noun proves scope, not that a prescriptive decision answers the
|
|
80
|
+
* current problem. Low-authority memory must match the evidence terms too. */
|
|
81
|
+
function queryProfile(target, options) {
|
|
82
|
+
const normalized = toPosixTarget(target.trim());
|
|
83
|
+
const exactSymbol = (options.symbols ?? []).some((symbol) => symbol.id === normalized || symbol.name === normalized || symbol.file === normalized || pathsRelated(symbol.file, normalized));
|
|
84
|
+
const exactComponent = (options.components ?? []).some((component) => component.id === normalized);
|
|
85
|
+
const hasWhitespace = /\s/.test(normalized);
|
|
86
|
+
const pathLike = !hasWhitespace && (normalized.includes("/") || /\.[a-z0-9]{1,8}$/i.test(normalized));
|
|
87
|
+
const taskPhrase = !exactSymbol && !exactComponent && !pathLike && hasWhitespace;
|
|
88
|
+
const allTerms = lexicalTokens(normalized);
|
|
89
|
+
if (!taskPhrase)
|
|
90
|
+
return { taskPhrase, evidenceTerms: new Set(), allTerms };
|
|
91
|
+
const codeTerms = codeSyntaxTokens(normalized);
|
|
92
|
+
const evidenceTerms = new Set([...allTerms].filter((token) => !codeTerms.has(token)));
|
|
93
|
+
return { taskPhrase, evidenceTerms, allTerms };
|
|
94
|
+
}
|
|
95
|
+
function decisionRecordTerms(decision) {
|
|
96
|
+
return lexicalTokens([
|
|
97
|
+
decision.title,
|
|
98
|
+
decision.context,
|
|
99
|
+
decision.decision,
|
|
100
|
+
...decision.consequences,
|
|
101
|
+
...decision.alternatives_rejected,
|
|
102
|
+
...decision.related_files,
|
|
103
|
+
...decision.related_components,
|
|
104
|
+
].join(" "));
|
|
105
|
+
}
|
|
106
|
+
/** Ranking uses only the record's claim, not consequences or path metadata.
|
|
107
|
+
* Otherwise prose such as "the test moved away from tuple" can falsely make an
|
|
108
|
+
* unrelated lazy-schema decision look tuple-specific. */
|
|
109
|
+
function decisionPrimaryTerms(decision) {
|
|
110
|
+
return lexicalTokens([decision.title, decision.context, decision.decision].join(" "));
|
|
111
|
+
}
|
|
112
|
+
function decisionEvidenceMatches(decision, query) {
|
|
113
|
+
const recordTerms = decisionPrimaryTerms(decision);
|
|
114
|
+
return [...query.evidenceTerms].filter((term) => recordTerms.has(term));
|
|
115
|
+
}
|
|
116
|
+
function decisionHypothesis(decision, matches, target) {
|
|
117
|
+
const files = decision.related_files.filter(isSafeDeliveryAnchor).slice(0, 3);
|
|
118
|
+
const components = decision.related_components.slice(0, Math.max(0, 3 - files.length)).map((id) => `component:${id}`);
|
|
119
|
+
const where = [...files, ...components];
|
|
120
|
+
const location = where.length ? where.join(", ") : "an unanchored record";
|
|
121
|
+
const why = matches.length
|
|
122
|
+
? `Matches task evidence (${matches.join(", ")}) and is anchored to ${location}.`
|
|
123
|
+
: `No symptom-term overlap was found; it is included because its authority/confidence passed delivery and its recorded scope is ${location}. Treat it as a hypothesis, not proof.`;
|
|
124
|
+
const consequences = decision.consequences.slice(0, 2).join("; ");
|
|
125
|
+
const pattern = clipHeadline(`${decision.decision || decision.title}${consequences ? ` Expected outcomes: ${consequences}` : ""}`, 420);
|
|
126
|
+
const historicalPattern = decision.commit
|
|
127
|
+
? `Commit ${decision.commit}: ${pattern}`
|
|
128
|
+
: `Recorded decision (no fix commit attached): ${pattern}`;
|
|
129
|
+
const evidence = decision.provenance.evidence.map((item) => clipHeadline(item, 120)).filter(Boolean).slice(0, 2);
|
|
130
|
+
const conformance = decision.conformance?.[0];
|
|
131
|
+
const premise = decision.premises?.[0];
|
|
132
|
+
const reproduction = clipHeadline(target, 180);
|
|
133
|
+
let verify;
|
|
134
|
+
const safeCommit = decision.commit && /^[0-9a-f]{7,64}$/i.test(decision.commit) ? decision.commit : null;
|
|
135
|
+
const diffPaths = files.filter((file) => /^[a-zA-Z0-9._/-]+$/.test(file)).slice(0, 2);
|
|
136
|
+
if (safeCommit) {
|
|
137
|
+
const scoped = diffPaths.length ? ` -- ${diffPaths.join(" ")}` : "";
|
|
138
|
+
verify = `Inspect the recorded change before editing: git show --stat --oneline ${safeCommit}${scoped}; then git show ${safeCommit}${scoped}. Compare that diff with the current code, then reproduce: ${reproduction}.`;
|
|
139
|
+
}
|
|
140
|
+
else if (evidence.length) {
|
|
141
|
+
verify = `Check recorded evidence (${evidence.join("; ")}), then reproduce: ${reproduction}.`;
|
|
142
|
+
}
|
|
143
|
+
else if (conformance) {
|
|
144
|
+
verify = `Check whether ${conformance.subject} ${conformance.assert}${conformance.object ? ` ${conformance.object}` : ""}${conformance.transitive ? " transitively" : ""}, then reproduce: ${reproduction}.`;
|
|
145
|
+
}
|
|
146
|
+
else if (premise) {
|
|
147
|
+
verify = `Check the recorded premise (${clipHeadline(premise.claim, 150)}), then reproduce: ${reproduction}.`;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
verify = `Reproduce: ${reproduction}. Inspect ${location} and run the narrowest existing test that exercises that path before changing code.`;
|
|
151
|
+
}
|
|
152
|
+
const disprove = where.length
|
|
153
|
+
? `Reject this hypothesis if the reproduction does not execute ${location}, or if checking the recorded pattern leaves the observed failure unchanged.`
|
|
154
|
+
: "Reject this hypothesis if the smallest reproduction contradicts the recorded pattern or passes without it.";
|
|
155
|
+
const obligations = [];
|
|
156
|
+
if (safeCommit) {
|
|
157
|
+
obligations.push({
|
|
158
|
+
id: `${decision.id}:inspect:${safeCommit.slice(0, 12)}`,
|
|
159
|
+
origin: "memory",
|
|
160
|
+
category: "evidence",
|
|
161
|
+
phase: "session",
|
|
162
|
+
description: `Inspect recorded commit ${safeCommit.slice(0, 12)} and compare it with the current code.`,
|
|
163
|
+
command_alternatives: [["git", "show", safeCommit.slice(0, 7)]],
|
|
164
|
+
expected: { success: true, output_includes: [safeCommit.slice(0, 7)] },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const testPath = decision.provenance.evidence
|
|
168
|
+
.flatMap((item) => item.match(/[A-Za-z0-9_.\/-]+\.(?:test|spec)\.[cm]?[jt]sx?/gi) ?? [])
|
|
169
|
+
.find((item) => isSafeDeliveryAnchor(item));
|
|
170
|
+
if (testPath) {
|
|
171
|
+
obligations.push({
|
|
172
|
+
id: `${decision.id}:proof:${testPath.replace(/[^A-Za-z0-9._-]/g, "_").slice(-60)}`,
|
|
173
|
+
origin: "memory",
|
|
174
|
+
category: "behavior",
|
|
175
|
+
phase: "after-edit",
|
|
176
|
+
description: `Re-run the recorded proof ${testPath} after the latest product edit.`,
|
|
177
|
+
command_alternatives: [
|
|
178
|
+
["vitest", testPath],
|
|
179
|
+
["jest", testPath],
|
|
180
|
+
["pytest", testPath],
|
|
181
|
+
["tsx", "--test", testPath],
|
|
182
|
+
["node", "--test", testPath],
|
|
183
|
+
["npm", "test", testPath],
|
|
184
|
+
],
|
|
185
|
+
expected: { success: true },
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
kind: "decision",
|
|
190
|
+
record_id: decision.id,
|
|
191
|
+
why,
|
|
192
|
+
where,
|
|
193
|
+
historical_pattern: historicalPattern,
|
|
194
|
+
verify,
|
|
195
|
+
disprove,
|
|
196
|
+
obligations,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function renderHypothesis(hypothesis, provenance, source) {
|
|
200
|
+
const lines = [
|
|
201
|
+
`${hypothesis.record_id} | hypothesis/decision | ${sourceTier(source)}/${provenance} | hunch_why("${hypothesis.record_id}")`,
|
|
202
|
+
` why: ${hypothesis.why}`,
|
|
203
|
+
` where: ${hypothesis.where.join(", ") || "unanchored"}`,
|
|
204
|
+
` historical pattern: ${hypothesis.historical_pattern}`,
|
|
205
|
+
` verify: ${hypothesis.verify}`,
|
|
206
|
+
` disprove: ${hypothesis.disprove}`,
|
|
207
|
+
];
|
|
208
|
+
if (hypothesis.obligations.length) {
|
|
209
|
+
lines.push(` controller: ${hypothesis.obligations.map((item) => `${item.id} [${item.category}/${item.phase}] ${item.description}`).join("; ")}`);
|
|
210
|
+
}
|
|
211
|
+
return lines.join("\n ");
|
|
212
|
+
}
|
|
213
|
+
function decisionAbstention(decision, query) {
|
|
214
|
+
const source = decision.provenance.source ?? "";
|
|
215
|
+
if (source.split("+").includes("human_confirmed"))
|
|
216
|
+
return null;
|
|
217
|
+
const confidence = decision.provenance.confidence ?? 0;
|
|
218
|
+
if (confidence < MIN_ADVISORY_CONFIDENCE) {
|
|
219
|
+
return {
|
|
220
|
+
reason: "low-confidence",
|
|
221
|
+
detail: `non-human decision confidence ${confidence.toFixed(2)} is below the ${MIN_ADVISORY_CONFIDENCE.toFixed(2)} delivery floor`,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (confidence >= MIN_UNCONDITIONED_CONFIDENCE)
|
|
225
|
+
return null;
|
|
226
|
+
if (!query.taskPhrase || !query.evidenceTerms.size) {
|
|
227
|
+
return {
|
|
228
|
+
reason: "insufficient-context",
|
|
229
|
+
detail: "low-authority prescriptive memory needs a task phrase with symptoms or expected behavior, not only a file/symbol/API target",
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const recordTerms = decisionRecordTerms(decision);
|
|
233
|
+
const evidenceOverlap = [...query.evidenceTerms].filter((term) => recordTerms.has(term)).length;
|
|
234
|
+
const allOverlap = [...query.allTerms].filter((term) => recordTerms.has(term)).length;
|
|
235
|
+
const requiredEvidence = Math.max(1, Math.ceil(query.evidenceTerms.size * 0.4));
|
|
236
|
+
if (evidenceOverlap < requiredEvidence || allOverlap / Math.max(1, query.allTerms.size) < 0.2) {
|
|
237
|
+
return {
|
|
238
|
+
reason: "low-relevance",
|
|
239
|
+
detail: `weak task-evidence match (${evidenceOverlap}/${query.evidenceTerms.size} symptom terms; ${allOverlap}/${query.allTerms.size} total terms) for confidence ${confidence.toFixed(2)}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
function emptyAbstention() {
|
|
245
|
+
return {
|
|
246
|
+
active: false,
|
|
247
|
+
withheld: 0,
|
|
248
|
+
reasons: { "low-confidence": 0, "insufficient-context": 0, "low-relevance": 0 },
|
|
249
|
+
retry_hint: null,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
32
252
|
function isSafeDeliveryAnchor(value) {
|
|
33
253
|
const path = toPosixTarget(value);
|
|
34
254
|
const segments = path.split("/");
|
|
@@ -149,6 +369,7 @@ export function buildDeliveryEnvelope(ctx, options = {}) {
|
|
|
149
369
|
?? (options.root ? localCommitReachability(options.root) : (() => "unknown"));
|
|
150
370
|
const canValidateAnchors = options.root !== undefined || options.symbols !== undefined || options.components !== undefined;
|
|
151
371
|
const candidates = [];
|
|
372
|
+
const query = queryProfile(ctx.target, options);
|
|
152
373
|
for (const constraint of ctx.constraints) {
|
|
153
374
|
const retired = !options.historical && (constraint.status === "retired" || constraint.valid_to != null);
|
|
154
375
|
const validation = validationState(constraint.scope, (anchor) => anchorResolves(anchor, ctx.target, options), undefined, reachability, false, canValidateAnchors);
|
|
@@ -169,6 +390,9 @@ export function buildDeliveryEnvelope(ctx, options = {}) {
|
|
|
169
390
|
const validation = validationState([...fileAnchors, ...componentAnchors], (anchor) => fileAnchors.includes(anchor)
|
|
170
391
|
? anchorResolves(anchor, ctx.target, options)
|
|
171
392
|
: componentResolves(anchor, ctx.target, options), decision.commit, reachability, true, canValidateAnchors);
|
|
393
|
+
const abstention = decisionAbstention(decision, query);
|
|
394
|
+
const relevanceTerms = query.taskPhrase ? decisionEvidenceMatches(decision, query) : undefined;
|
|
395
|
+
const hypothesis = query.taskPhrase ? decisionHypothesis(decision, relevanceTerms ?? [], ctx.target) : undefined;
|
|
172
396
|
candidates.push({
|
|
173
397
|
ref: { kind: "decisions", record_id: decision.id },
|
|
174
398
|
mandatory: false,
|
|
@@ -176,7 +400,13 @@ export function buildDeliveryEnvelope(ctx, options = {}) {
|
|
|
176
400
|
provenance: validation.state,
|
|
177
401
|
staleDetail: validation.detail,
|
|
178
402
|
retiredDetail: retired ? `decision is ${decision.status} at HEAD` : undefined,
|
|
179
|
-
|
|
403
|
+
abstainReason: abstention?.reason,
|
|
404
|
+
abstainDetail: abstention?.detail,
|
|
405
|
+
hypothesis,
|
|
406
|
+
relevanceTerms,
|
|
407
|
+
line: hypothesis
|
|
408
|
+
? renderHypothesis(hypothesis, validation.state, decision.provenance.source)
|
|
409
|
+
: `${decision.id} | decision/${decision.status} | ${clipHeadline(`${decision.title}: ${decision.decision}`, 220)} | scope ${clipHeadline(decision.related_files.join(", ") || decision.related_components.join(", ") || "unanchored", 100)} | ${sourceTier(decision.provenance.source)}/${validation.state} | hunch_why("${decision.id}")`,
|
|
180
410
|
});
|
|
181
411
|
}
|
|
182
412
|
for (const bug of ctx.bugs) {
|
|
@@ -218,7 +448,7 @@ export function buildDeliveryEnvelope(ctx, options = {}) {
|
|
|
218
448
|
if (!hasAnything) {
|
|
219
449
|
const empty = `# Hunch context for "${ctx.target}"\n\n(No recorded constraints/decisions/bugs for this target yet — Hunch is still learning it.)\n`;
|
|
220
450
|
const text = fitText(empty, cap);
|
|
221
|
-
return { text, delivered: [], supplements: [], omitted: [], budget_tokens: budget, used_chars: charCount(text), blocking_overflow: false };
|
|
451
|
+
return { text, delivered: [], hypotheses: [], obligations: [], supplements: [], omitted: [], budget_tokens: budget, used_chars: charCount(text), blocking_overflow: false, abstention: emptyAbstention() };
|
|
222
452
|
}
|
|
223
453
|
const omitted = [];
|
|
224
454
|
const eligible = [];
|
|
@@ -231,18 +461,64 @@ export function buildDeliveryEnvelope(ctx, options = {}) {
|
|
|
231
461
|
omitted.push({ ...candidate.ref, reason: "stale-provenance", detail: candidate.staleDetail ?? "provenance is stale" });
|
|
232
462
|
continue;
|
|
233
463
|
}
|
|
464
|
+
if (candidate.abstainReason && !candidate.mandatory && candidate.ref) {
|
|
465
|
+
omitted.push({ ...candidate.ref, reason: candidate.abstainReason, detail: candidate.abstainDetail ?? "delivery confidence gate abstained" });
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
234
468
|
eligible.push(candidate);
|
|
235
469
|
}
|
|
470
|
+
const hypothesisCandidates = eligible.filter((candidate) => candidate.hypothesis);
|
|
471
|
+
const documentFrequency = new Map();
|
|
472
|
+
for (const candidate of hypothesisCandidates) {
|
|
473
|
+
for (const term of new Set(candidate.relevanceTerms ?? [])) {
|
|
474
|
+
documentFrequency.set(term, (documentFrequency.get(term) ?? 0) + 1);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
for (const candidate of hypothesisCandidates) {
|
|
478
|
+
const rarity = (candidate.relevanceTerms ?? []).reduce((sum, term) => {
|
|
479
|
+
const frequency = documentFrequency.get(term) ?? hypothesisCandidates.length;
|
|
480
|
+
return sum + Math.log2((hypothesisCandidates.length + 1) / (frequency + 1)) + 1;
|
|
481
|
+
}, 0);
|
|
482
|
+
// Rank specific task evidence (rare across the retrieved candidate set)
|
|
483
|
+
// above generic verbs such as "parse", "value", and "property".
|
|
484
|
+
candidate.score += rarity * 10;
|
|
485
|
+
}
|
|
236
486
|
eligible.sort((left, right) => Number(right.mandatory) - Number(left.mandatory) || right.score - left.score || (left.ref?.record_id ?? left.line).localeCompare(right.ref?.record_id ?? right.line));
|
|
237
|
-
const
|
|
238
|
-
|
|
487
|
+
const boundedEligible = [];
|
|
488
|
+
let actionableHypotheses = 0;
|
|
489
|
+
for (const candidate of eligible) {
|
|
490
|
+
if (candidate.hypothesis && candidate.ref) {
|
|
491
|
+
if (actionableHypotheses >= MAX_ACTIONABLE_HYPOTHESES) {
|
|
492
|
+
omitted.push({
|
|
493
|
+
...candidate.ref,
|
|
494
|
+
reason: "actionability-cap",
|
|
495
|
+
detail: `only the top ${MAX_ACTIONABLE_HYPOTHESES} decision hypotheses are delivered; refine the task evidence or inspect this record with hunch_why`,
|
|
496
|
+
});
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
actionableHypotheses++;
|
|
500
|
+
}
|
|
501
|
+
boundedEligible.push(candidate);
|
|
502
|
+
}
|
|
503
|
+
const recordCandidates = boundedEligible.filter((candidate) => candidate.ref);
|
|
504
|
+
const structuralCandidates = boundedEligible.filter((candidate) => !candidate.ref);
|
|
239
505
|
const lines = [
|
|
240
506
|
`# Hunch context for "${ctx.target}"`,
|
|
241
507
|
"",
|
|
242
|
-
|
|
508
|
+
query.taskPhrase
|
|
509
|
+
? `## 🧠 Bounded memory (Invariants · max ${MAX_ACTIONABLE_HYPOTHESES} decision hypotheses · Bugs · Known findings)`
|
|
510
|
+
: "## 🧠 Ranked memory (Invariants · Decisions · Bugs · Known findings)",
|
|
243
511
|
];
|
|
512
|
+
if (candidates.some((candidate) => candidate.abstainReason)) {
|
|
513
|
+
lines.push("Evidence rule: explicit task, repro, and test evidence outranks advisory memory; weak unverified matches are withheld.");
|
|
514
|
+
}
|
|
515
|
+
if (query.taskPhrase) {
|
|
516
|
+
lines.push("Diagnostic loop: before editing, call hunch_context again with the first concrete failing assertion, stack frame, expected behavior, and API/code path you observe.");
|
|
517
|
+
}
|
|
244
518
|
let text = `${lines.join("\n")}\n`;
|
|
245
519
|
const delivered = [];
|
|
520
|
+
const hypotheses = [];
|
|
521
|
+
const obligations = [];
|
|
246
522
|
const supplements = [];
|
|
247
523
|
let blockingOverflow = false;
|
|
248
524
|
for (const [index, candidate] of recordCandidates.entries()) {
|
|
@@ -256,6 +532,10 @@ export function buildDeliveryEnvelope(ctx, options = {}) {
|
|
|
256
532
|
provenance_status: candidate.provenance,
|
|
257
533
|
token_cost: estimatedTokens(next),
|
|
258
534
|
});
|
|
535
|
+
if (candidate.hypothesis) {
|
|
536
|
+
hypotheses.push({ ...candidate.hypothesis, rank: index + 1 });
|
|
537
|
+
obligations.push(...candidate.hypothesis.obligations);
|
|
538
|
+
}
|
|
259
539
|
if (charCount(text) > cap && candidate.mandatory)
|
|
260
540
|
blockingOverflow = true;
|
|
261
541
|
}
|
|
@@ -280,7 +560,11 @@ export function buildDeliveryEnvelope(ctx, options = {}) {
|
|
|
280
560
|
}
|
|
281
561
|
const next = `- supplemental/${supplement.kind} | ${content}\n`;
|
|
282
562
|
const tokenCost = estimatedTokens(next);
|
|
283
|
-
|
|
563
|
+
const abstainedMemory = omitted.some((item) => item.reason === "low-confidence" || item.reason === "insufficient-context" || item.reason === "low-relevance");
|
|
564
|
+
if (abstainedMemory && delivered.length === 0 && supplement.kind.startsWith("search-")) {
|
|
565
|
+
supplements.push({ id: supplement.id, kind: supplement.kind, delivered: false, reason: "abstained", rank: index + 1, token_cost: tokenCost });
|
|
566
|
+
}
|
|
567
|
+
else if (charCount(text) + charCount(next) <= cap) {
|
|
284
568
|
text += next;
|
|
285
569
|
supplements.push({ id: supplement.id, kind: supplement.kind, delivered: true, reason: "supplemental", rank: index + 1, token_cost: tokenCost });
|
|
286
570
|
}
|
|
@@ -295,9 +579,23 @@ export function buildDeliveryEnvelope(ctx, options = {}) {
|
|
|
295
579
|
}
|
|
296
580
|
const staleCount = omitted.filter((item) => item.reason === "stale-provenance" || item.reason === "retired").length;
|
|
297
581
|
const budgetCount = omitted.filter((item) => item.reason === "budget").length;
|
|
582
|
+
const actionabilityCount = omitted.filter((item) => item.reason === "actionability-cap").length;
|
|
583
|
+
const abstention = emptyAbstention();
|
|
584
|
+
for (const item of omitted) {
|
|
585
|
+
if (item.reason === "low-confidence" || item.reason === "insufficient-context" || item.reason === "low-relevance") {
|
|
586
|
+
abstention.active = true;
|
|
587
|
+
abstention.withheld++;
|
|
588
|
+
abstention.reasons[item.reason]++;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (abstention.active) {
|
|
592
|
+
abstention.retry_hint = "Retry hunch_context with the concrete symptom, expected behavior, failing API, and repro evidence; do not let advisory memory override the task or tests.";
|
|
593
|
+
}
|
|
298
594
|
const notes = [
|
|
299
595
|
staleCount ? `${staleCount} stale/retired record(s) withheld; run hunch drift or hunch_why(id) to inspect.` : "",
|
|
300
596
|
budgetCount ? `${budgetCount} lower-ranked record(s) omitted by budget; use hunch_why(id) to drill down.` : "",
|
|
597
|
+
actionabilityCount ? `${actionabilityCount} additional decision hypothesis/hypotheses withheld by the actionability cap; refine the task evidence or use hunch_why(id).` : "",
|
|
598
|
+
abstention.active ? `${abstention.withheld} weak prescriptive record(s) withheld by confidence/relevance abstention. ${abstention.retry_hint}` : "",
|
|
301
599
|
].filter(Boolean);
|
|
302
600
|
if (notes.length) {
|
|
303
601
|
const footer = `… ${notes.join(" ")}\n`;
|
|
@@ -312,11 +610,14 @@ export function buildDeliveryEnvelope(ctx, options = {}) {
|
|
|
312
610
|
return {
|
|
313
611
|
text,
|
|
314
612
|
delivered,
|
|
613
|
+
hypotheses,
|
|
614
|
+
obligations,
|
|
315
615
|
supplements,
|
|
316
616
|
omitted,
|
|
317
617
|
budget_tokens: budget,
|
|
318
618
|
used_chars: charCount(text),
|
|
319
619
|
blocking_overflow: blockingOverflow,
|
|
620
|
+
abstention,
|
|
320
621
|
};
|
|
321
622
|
}
|
|
322
623
|
//# sourceMappingURL=delivery.js.map
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const EvidenceOutcomeSchema = z.enum(["red", "green", "error", "not-run"]);
|
|
3
|
+
const ownerIsSafe = (value) => {
|
|
4
|
+
if (/\0|[\r\n]/.test(value))
|
|
5
|
+
return false;
|
|
6
|
+
const separator = value.indexOf("::");
|
|
7
|
+
if (separator <= 0 || separator === value.length - 2)
|
|
8
|
+
return false;
|
|
9
|
+
const path = value.slice(0, separator);
|
|
10
|
+
if (path.startsWith("/") || path.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(path) || path.includes("\\"))
|
|
11
|
+
return false;
|
|
12
|
+
return path.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
13
|
+
};
|
|
14
|
+
export const EvidenceOwnerSchema = z.string().min(4).max(500).refine(ownerIsSafe, "owner must be a safe repo-relative path and declaration separated by ::");
|
|
15
|
+
export const EvidenceProbeSchema = z.object({
|
|
16
|
+
target_before: EvidenceOutcomeSchema,
|
|
17
|
+
control_before: EvidenceOutcomeSchema,
|
|
18
|
+
target_after: EvidenceOutcomeSchema.optional(),
|
|
19
|
+
control_after: EvidenceOutcomeSchema.optional(),
|
|
20
|
+
}).strict();
|
|
21
|
+
export const EvidenceExecutionSchema = z.object({
|
|
22
|
+
owner: EvidenceOwnerSchema,
|
|
23
|
+
target_count: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
24
|
+
control_count: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
25
|
+
}).strict();
|
|
26
|
+
export const EvidenceInterventionSchema = z.object({
|
|
27
|
+
owner: EvidenceOwnerSchema,
|
|
28
|
+
mutation_id: z.string().min(1).max(200).optional(),
|
|
29
|
+
target_after: EvidenceOutcomeSchema,
|
|
30
|
+
control_after: EvidenceOutcomeSchema,
|
|
31
|
+
}).strict();
|
|
32
|
+
export const VerifiedEvidenceReceiptSchema = z.object({
|
|
33
|
+
version: z.literal(1),
|
|
34
|
+
claim: z.string().trim().min(1).max(100_000),
|
|
35
|
+
probe: EvidenceProbeSchema,
|
|
36
|
+
execution: z.array(EvidenceExecutionSchema).max(500).default([]),
|
|
37
|
+
interventions: z.array(EvidenceInterventionSchema).max(500).default([]),
|
|
38
|
+
}).strict();
|
|
39
|
+
function ownerPath(owner) {
|
|
40
|
+
return owner.slice(0, owner.indexOf("::"));
|
|
41
|
+
}
|
|
42
|
+
function unique(values) {
|
|
43
|
+
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
44
|
+
}
|
|
45
|
+
function closureStatus(receipt, authenticated) {
|
|
46
|
+
const target = receipt.probe.target_after ?? null;
|
|
47
|
+
const control = receipt.probe.control_after ?? null;
|
|
48
|
+
if (!authenticated)
|
|
49
|
+
return { status: "unverified", target_after: target, control_after: control };
|
|
50
|
+
if (!target || target === "not-run")
|
|
51
|
+
return { status: "open", target_after: target, control_after: control };
|
|
52
|
+
if (target === "error")
|
|
53
|
+
return { status: "probe-error", target_after: target, control_after: control };
|
|
54
|
+
if (target === "red")
|
|
55
|
+
return { status: "still-red", target_after: target, control_after: control };
|
|
56
|
+
if (control === "red" || control === "error")
|
|
57
|
+
return { status: "control-regressed", target_after: target, control_after: control };
|
|
58
|
+
if (!control || control === "not-run")
|
|
59
|
+
return { status: "control-unchecked", target_after: target, control_after: control };
|
|
60
|
+
return { status: "closed", target_after: target, control_after: control };
|
|
61
|
+
}
|
|
62
|
+
/** Compile externally observed receipts into a bounded evidence map. This is
|
|
63
|
+
* deliberately pure: it runs no probe, edits no source, and never turns causal
|
|
64
|
+
* influence into a correction-owner claim. */
|
|
65
|
+
export function compileVerifiedEvidenceMap(value) {
|
|
66
|
+
const parsed = VerifiedEvidenceReceiptSchema.safeParse(value);
|
|
67
|
+
if (!parsed.success) {
|
|
68
|
+
const details = parsed.error.issues.slice(0, 5).map((issue) => `${issue.path.join(".") || "receipt"}: ${issue.message}`).join("; ");
|
|
69
|
+
throw new Error(`invalid verified-evidence receipt: ${details}`);
|
|
70
|
+
}
|
|
71
|
+
const receipt = parsed.data;
|
|
72
|
+
const authenticated = receipt.probe.target_before === "red" && receipt.probe.control_before === "green";
|
|
73
|
+
const verificationReason = authenticated
|
|
74
|
+
? "the target was red while the distinct control stayed green"
|
|
75
|
+
: "authentication requires target_before=red and control_before=green";
|
|
76
|
+
const executionByOwner = new Map();
|
|
77
|
+
for (const entry of receipt.execution) {
|
|
78
|
+
const current = executionByOwner.get(entry.owner) ?? { target: 0, control: 0 };
|
|
79
|
+
current.target = Math.max(current.target, entry.target_count);
|
|
80
|
+
current.control = Math.max(current.control, entry.control_count);
|
|
81
|
+
executionByOwner.set(entry.owner, current);
|
|
82
|
+
}
|
|
83
|
+
const targetObserved = unique([...executionByOwner].filter(([, counts]) => counts.target > 0).map(([owner]) => owner));
|
|
84
|
+
const targetOnly = unique([...executionByOwner].filter(([, counts]) => counts.target > 0 && counts.control === 0).map(([owner]) => owner));
|
|
85
|
+
const shared = unique([...executionByOwner].filter(([, counts]) => counts.target > 0 && counts.control > 0).map(([owner]) => owner));
|
|
86
|
+
// A single target-only call is often setup noise. The transfer-development
|
|
87
|
+
// rule therefore requires at least two target calls and a 2x target/control
|
|
88
|
+
// ratio before execution may reserve one file-level shortlist slot.
|
|
89
|
+
const strongDifferential = unique([...executionByOwner]
|
|
90
|
+
.filter(([, counts]) => counts.target > counts.control && counts.target >= 2 * Math.max(1, counts.control))
|
|
91
|
+
.map(([owner]) => owner));
|
|
92
|
+
const strongDifferentialFiles = unique(strongDifferential.map(ownerPath));
|
|
93
|
+
const strongDifferentialEntries = strongDifferential.map((owner) => {
|
|
94
|
+
const counts = executionByOwner.get(owner);
|
|
95
|
+
return {
|
|
96
|
+
owner,
|
|
97
|
+
target_count: counts.target,
|
|
98
|
+
control_count: counts.control,
|
|
99
|
+
ratio: counts.target / Math.max(1, counts.control),
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
const admitted = authenticated
|
|
103
|
+
? receipt.interventions.filter((entry) => entry.target_after === "green" && entry.control_after === "green")
|
|
104
|
+
: [];
|
|
105
|
+
const behaviorSensitive = unique(admitted.map((entry) => entry.owner));
|
|
106
|
+
const sensitiveFiles = unique(behaviorSensitive.map(ownerPath));
|
|
107
|
+
const paths = unique([...targetObserved.map(ownerPath), ...sensitiveFiles]);
|
|
108
|
+
const files = paths.map((path) => {
|
|
109
|
+
const targetExecutionOwners = targetObserved.filter((owner) => ownerPath(owner) === path);
|
|
110
|
+
const targetOnlyOwners = targetOnly.filter((owner) => ownerPath(owner) === path);
|
|
111
|
+
const sharedOwners = shared.filter((owner) => ownerPath(owner) === path);
|
|
112
|
+
const strongDifferentialOwners = strongDifferential.filter((owner) => ownerPath(owner) === path);
|
|
113
|
+
const strongDifferentialSupport = strongDifferentialOwners.reduce((support, owner) => {
|
|
114
|
+
const counts = executionByOwner.get(owner);
|
|
115
|
+
return support + counts.target - counts.control;
|
|
116
|
+
}, 0);
|
|
117
|
+
const sensitiveOwners = behaviorSensitive.filter((owner) => ownerPath(owner) === path);
|
|
118
|
+
const evidence = [
|
|
119
|
+
...(targetExecutionOwners.length ? ["target-execution"] : []),
|
|
120
|
+
...(targetOnlyOwners.length ? ["target-only-execution"] : []),
|
|
121
|
+
...(sharedOwners.length ? ["shared-execution"] : []),
|
|
122
|
+
...(strongDifferentialOwners.length ? ["strong-differential-execution"] : []),
|
|
123
|
+
...(sensitiveOwners.length ? ["behavior-sensitive"] : []),
|
|
124
|
+
];
|
|
125
|
+
return {
|
|
126
|
+
path,
|
|
127
|
+
target_execution_owners: targetExecutionOwners,
|
|
128
|
+
target_only_owners: targetOnlyOwners,
|
|
129
|
+
shared_execution_owners: sharedOwners,
|
|
130
|
+
strong_differential_owners: strongDifferentialOwners,
|
|
131
|
+
strong_differential_support: strongDifferentialSupport,
|
|
132
|
+
behavior_sensitive_owners: sensitiveOwners,
|
|
133
|
+
evidence,
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
const level = !authenticated
|
|
137
|
+
? "unverified"
|
|
138
|
+
: behaviorSensitive.length
|
|
139
|
+
? "behavior-sensitive"
|
|
140
|
+
: targetObserved.length
|
|
141
|
+
? "execution-verified"
|
|
142
|
+
: "probe-authenticated";
|
|
143
|
+
return {
|
|
144
|
+
version: 1,
|
|
145
|
+
claim: receipt.claim,
|
|
146
|
+
level,
|
|
147
|
+
verification: {
|
|
148
|
+
authenticated,
|
|
149
|
+
target_before: receipt.probe.target_before,
|
|
150
|
+
control_before: receipt.probe.control_before,
|
|
151
|
+
reason: verificationReason,
|
|
152
|
+
},
|
|
153
|
+
closure: closureStatus(receipt, authenticated),
|
|
154
|
+
execution_slice: {
|
|
155
|
+
target_observed_owners: targetObserved,
|
|
156
|
+
target_only_owners: targetOnly,
|
|
157
|
+
shared_owners: shared,
|
|
158
|
+
strong_differential_owners: strongDifferential,
|
|
159
|
+
strong_differential_files: strongDifferentialFiles,
|
|
160
|
+
strong_differential: strongDifferentialEntries,
|
|
161
|
+
},
|
|
162
|
+
intervention_slice: {
|
|
163
|
+
admitted_receipts: admitted.length,
|
|
164
|
+
behavior_sensitive_owners: behaviorSensitive,
|
|
165
|
+
behavior_sensitive_files: sensitiveFiles,
|
|
166
|
+
},
|
|
167
|
+
files,
|
|
168
|
+
owner_claim: {
|
|
169
|
+
enabled: false,
|
|
170
|
+
owner: null,
|
|
171
|
+
reason: "Execution and successful interventions establish behavioral influence, not correction ownership.",
|
|
172
|
+
},
|
|
173
|
+
limitations: [
|
|
174
|
+
"The compiler trusts supplied observations; it does not execute or independently authenticate probes.",
|
|
175
|
+
"Unexecuted code and unsupported mutation shapes remain outside the evidence slice.",
|
|
176
|
+
"A behavior-sensitive declaration may be an upstream lever, wrapper, or downstream symptom site rather than the correction owner.",
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function listed(values) {
|
|
181
|
+
return values.length ? values.map((value) => ` - ${value}`).join("\n") : " (none)";
|
|
182
|
+
}
|
|
183
|
+
export function formatVerifiedEvidenceMap(map) {
|
|
184
|
+
return [
|
|
185
|
+
"Verified evidence map (read-only receipt compiler)",
|
|
186
|
+
`Claim: ${map.claim}`,
|
|
187
|
+
`Evidence level: ${map.level}`,
|
|
188
|
+
`Probe authentication: ${map.verification.authenticated ? "authenticated" : "not authenticated"} — ${map.verification.reason}`,
|
|
189
|
+
`Closure: ${map.closure.status}`,
|
|
190
|
+
"Target-only execution:",
|
|
191
|
+
listed(map.execution_slice.target_only_owners),
|
|
192
|
+
"Shared execution:",
|
|
193
|
+
listed(map.execution_slice.shared_owners),
|
|
194
|
+
"Strong differential execution files:",
|
|
195
|
+
listed(map.execution_slice.strong_differential_files),
|
|
196
|
+
"Behavior-sensitive files:",
|
|
197
|
+
listed(map.intervention_slice.behavior_sensitive_files),
|
|
198
|
+
`Exact-owner claim: disabled — ${map.owner_claim.reason}`,
|
|
199
|
+
"This command compiled supplied observations; it did not run code or mutate the repository.",
|
|
200
|
+
].join("\n");
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=evidenceMap.js.map
|