@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.
@@ -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
- line: `${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}")`,
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 recordCandidates = eligible.filter((candidate) => candidate.ref);
238
- const structuralCandidates = eligible.filter((candidate) => !candidate.ref);
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
- "## 🧠 Ranked memory (Invariants · Decisions · Bugs · Known findings)",
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
- if (charCount(text) + charCount(next) <= cap) {
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