@davesheffer/hunch 1.37.0 → 1.37.1

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/dist/cli/index.js CHANGED
@@ -68,6 +68,7 @@ import { formatSearchHit, formatStructure } from "../core/format.js";
68
68
  import { isStateKind, renderStateLine, stateSupplements } from "../core/stateDelivery.js";
69
69
  import { taskSelectionSupplements } from "../core/taskDelivery.js";
70
70
  import { buildTaskRankingQuery } from "../core/taskQuery.js";
71
+ import { rankingStatusLine, resolveTaskRankingMode } from "../core/taskRankingMode.js";
71
72
  import { diagnoseIssueCorrectionStage, formatCorrectionStageDiagnostic } from "../core/correctionStage.js";
72
73
  import { compileVerifiedEvidenceMap, formatVerifiedEvidenceMap } from "../core/evidenceMap.js";
73
74
  import { collectCorrectionStageSources } from "../extractors/correctionSources.js";
@@ -4212,7 +4213,7 @@ program
4212
4213
  decisionCorpus: store.recs("decisions"),
4213
4214
  historical: !!asOf,
4214
4215
  profile: opts.profile,
4215
- supplements: [...stateGrounding, ...(asOf ? [] : taskSelectionSupplements(store.selectTasksFor(target, buildTaskRankingQuery(root, opts.task ?? null, target)), target))],
4216
+ supplements: [...stateGrounding, ...(asOf ? [] : taskSelectionSupplements(store.selectTasksAuto(target, buildTaskRankingQuery(root, opts.task ?? null, target)), target))],
4216
4217
  });
4217
4218
  process.stdout.write(envelope.text);
4218
4219
  if (opts.task) {
@@ -4882,7 +4883,7 @@ program
4882
4883
  // from this file. No diff exists yet, so this is context — "don't re-add X" —
4883
4884
  // not a block; the commit-time `hunch check` does the actual gating.
4884
4885
  const retired = store.retiredForFile(target).filter((r) => r.symbols.length || r.deps.length);
4885
- const recentTasks = taskSelectionSupplements(store.selectTasksFor(target, buildTaskRankingQuery(root, hookReportTaskId(root, provider, evt), target)), target);
4886
+ const recentTasks = taskSelectionSupplements(store.selectTasksAuto(target, buildTaskRankingQuery(root, hookReportTaskId(root, provider, evt), target)), target);
4886
4887
  const hasContent = ctx.constraints.length ||
4887
4888
  ctx.decisions.length ||
4888
4889
  ctx.bugs.length ||
@@ -6473,6 +6474,11 @@ program
6473
6474
  console.log(` • ${r.title} (${r.id}${r.topic ? `, ${r.topic}` : ""}, since ${r.date})\n ${r.note}`);
6474
6475
  if (pendingReview > 0)
6475
6476
  console.log(`\n (${pendingReview} legacy un-vouched draft(s) — \`hunch adopt-drafts\` to auto-trust them as advisory)`);
6477
+ // Task-record ranking: evaluated automatically on every task write; the kill rule applies itself.
6478
+ try {
6479
+ console.log(`\n📊 ${rankingStatusLine(resolveTaskRankingMode(store.publicRoot, store))}`);
6480
+ }
6481
+ catch { /* no task records or no cache dir: nothing to say */ }
6476
6482
  }
6477
6483
  finally {
6478
6484
  store.close();
@@ -9,6 +9,7 @@ import { assertReportPath } from "../core/taskReportPaths.js";
9
9
  import { publicTaskReport } from "../core/taskReportPublic.js";
10
10
  import { mergeDurableTaskSummaries, persistTaskRecord } from "../core/taskRecord.js";
11
11
  import { evaluateTaskRanking, renderRankEval } from "../core/taskRankEval.js";
12
+ import { rankingStatusLine, refreshRankEval, resolveTaskRankingMode } from "../core/taskRankingMode.js";
12
13
  import { taskRecordStats } from "../core/taskRecordStats.js";
13
14
  export function registerTaskReportCommands(program, openStore) {
14
15
  const task = program.command("task").description("Record an explicit task lifecycle for Hunch contribution reports");
@@ -117,6 +118,7 @@ export function registerTaskReportCommands(program, openStore) {
117
118
  console.log(`Graph task records: ${rs.records}`);
118
119
  console.log(` re-verified an earlier check (24h) ${rate(rs.reverification_rate, rs.reverify_candidates)}`);
119
120
  console.log(` repeated an earlier violation ${rate(rs.repeat_violation_rate, rs.violation_candidates)}`);
121
+ console.log(` ${rankingStatusLine(resolveTaskRankingMode(opened.root, opened.store))}`);
120
122
  }
121
123
  finally {
122
124
  opened.store.close();
@@ -134,6 +136,8 @@ export function registerTaskReportCommands(program, openStore) {
134
136
  const cutoff = Date.now() - (Number(opts.since) || 365) * 86_400_000;
135
137
  const records = store.recs("tasks").filter((r) => (Date.parse(r.finished_at) || 0) >= cutoff);
136
138
  const report = evaluateTaskRanking(records, { split: Math.min(1, Math.max(0.05, Number(opts.split) || 0.3)) });
139
+ // Keep the automatic cache current too, so delivery and `hunch now` agree with what was just printed.
140
+ refreshRankEval(findRoot(), store, { force: true });
137
141
  console.log(opts.json ? JSON.stringify(report, null, 2) : renderRankEval(report));
138
142
  }
139
143
  finally {
@@ -275,11 +275,11 @@ export declare const EvidenceEventSchema: z.ZodObject<{
275
275
  }, z.core.$strict>>;
276
276
  compiler: z.ZodOptional<z.ZodObject<{
277
277
  status: z.ZodEnum<{
278
- conflicted: "conflicted";
279
- eligible: "eligible";
280
278
  compiled: "compiled";
279
+ eligible: "eligible";
281
280
  covered: "covered";
282
281
  uncompilable: "uncompilable";
282
+ conflicted: "conflicted";
283
283
  }>;
284
284
  policy: z.ZodDefault<z.ZodNullable<z.ZodString>>;
285
285
  reason: z.ZodDefault<z.ZodString>;
@@ -10,9 +10,9 @@ export declare const CompilerCaseBankSchema: z.ZodObject<{
10
10
  evidence: z.ZodString;
11
11
  expected: z.ZodObject<{
12
12
  outcome: z.ZodEnum<{
13
- conflicted: "conflicted";
14
13
  covered: "covered";
15
14
  uncompilable: "uncompilable";
15
+ conflicted: "conflicted";
16
16
  assertion: "assertion";
17
17
  }>;
18
18
  assertion: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -97,9 +97,9 @@ export declare const CompilerCaseBankSchema: z.ZodObject<{
97
97
  }, z.core.$strict>;
98
98
  actual: z.ZodObject<{
99
99
  outcome: z.ZodEnum<{
100
- conflicted: "conflicted";
101
100
  covered: "covered";
102
101
  uncompilable: "uncompilable";
102
+ conflicted: "conflicted";
103
103
  assertion: "assertion";
104
104
  }>;
105
105
  assertion: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -59,8 +59,8 @@ export declare const UsefulnessObservationSchema: z.ZodObject<{
59
59
  kind: z.ZodEnum<{
60
60
  observation: "observation";
61
61
  artifact: "artifact";
62
- receipt: "receipt";
63
62
  event: "event";
63
+ receipt: "receipt";
64
64
  verification: "verification";
65
65
  }>;
66
66
  ref: z.ZodString;
@@ -92,8 +92,8 @@ export declare const CaptureRequestSchema: z.ZodObject<{
92
92
  relevance: z.ZodObject<{
93
93
  use: z.ZodEnum<{
94
94
  decision: "decision";
95
- constraint: "constraint";
96
95
  preference: "preference";
96
+ constraint: "constraint";
97
97
  operational_fact: "operational_fact";
98
98
  ongoing_issue: "ongoing_issue";
99
99
  }>;
@@ -160,8 +160,8 @@ export declare const CaptureBatchRequestSchema: z.ZodObject<{
160
160
  relevance: z.ZodObject<{
161
161
  use: z.ZodEnum<{
162
162
  decision: "decision";
163
- constraint: "constraint";
164
163
  preference: "preference";
164
+ constraint: "constraint";
165
165
  operational_fact: "operational_fact";
166
166
  ongoing_issue: "ongoing_issue";
167
167
  }>;
@@ -255,8 +255,8 @@ export declare const ReadRequestSchema: z.ZodObject<{
255
255
  subject: z.ZodOptional<z.ZodString>;
256
256
  task: z.ZodOptional<z.ZodString>;
257
257
  profile: z.ZodOptional<z.ZodEnum<{
258
- builder: "builder";
259
258
  reviewer: "reviewer";
259
+ builder: "builder";
260
260
  architect: "architect";
261
261
  }>>;
262
262
  budget_tokens: z.ZodOptional<z.ZodNumber>;
@@ -36,7 +36,9 @@ export function taskSelectionSupplements(selection, target) {
36
36
  const counts = { latest: 0, violation: 0, relevant: 0 };
37
37
  for (const p of selection.picks)
38
38
  counts[p.slot]++;
39
- const parts = [counts.latest ? "latest" : null, counts.violation ? "problem" : null, counts.relevant ? `relevant ${counts.relevant}` : null].filter(Boolean).join(" · ");
39
+ const parts = selection.mode === "latest"
40
+ ? `latest ${counts.latest} (ranking off: it lost its evaluation; hunch task rank-eval)`
41
+ : [counts.latest ? "latest" : null, counts.violation ? "problem" : null, counts.relevant ? `relevant ${counts.relevant}` : null].filter(Boolean).join(" · ");
40
42
  return [
41
43
  {
42
44
  id: "recent-tasks", kind: "recent-tasks", priority: 415,
@@ -90,7 +90,12 @@ export interface TaskSelection {
90
90
  /** Ranked candidates not shown. */
91
91
  more: number;
92
92
  candidates: number;
93
+ /** How the picks were chosen: the ranker, or the "latest 3" fallback the kill rule imposes. */
94
+ mode?: "ranked" | "latest";
93
95
  }
96
+ /** The pre-ranking behaviour, kept as the baseline and the fallback: the three
97
+ * most recent records on the exact file, no scoring. */
98
+ export declare function selectLatestTasks(records: readonly TaskRecord[], query: RankingQuery, ctx: RankingContext, limit?: number): TaskSelection;
94
99
  export interface SlotOptions {
95
100
  limit?: number;
96
101
  relevant?: number;
@@ -9,6 +9,18 @@ export const COCHANGE_MIN_COUNT = 2;
9
9
  * at most half of all task records (idf ≥ ln 2). A rule every task receives
10
10
  * says nothing about relatedness; it still contributes to the score, weakly. */
11
11
  export const RULE_GATE_MIN_IDF = Math.log(2);
12
+ /** The pre-ranking behaviour, kept as the baseline and the fallback: the three
13
+ * most recent records on the exact file, no scoring. */
14
+ export function selectLatestTasks(records, query, ctx, limit = 3) {
15
+ const target = normalizePath(query.target);
16
+ const ranked = records
17
+ .filter((r) => !ctx.superseded?.has(r.id) && recordFiles(r).includes(target))
18
+ .map((r) => rankTaskRecord(r, query, ctx))
19
+ .filter((r) => r !== null)
20
+ .sort(newestFirst);
21
+ const picks = ranked.slice(0, Math.max(1, limit)).map((r) => ({ slot: "latest", ranked: r }));
22
+ return { picks, more: ranked.length - picks.length, candidates: ranked.length, mode: "latest" };
23
+ }
12
24
  const DAY_MS = 86_400_000;
13
25
  export function normalizePath(path) {
14
26
  return path.trim().replace(/\\/g, "/").replace(/^\.\//, "");
@@ -197,6 +209,6 @@ export function selectTaskSlots(ranked, options = {}) {
197
209
  take("relevant", best);
198
210
  remaining = remaining.filter((r) => r !== best);
199
211
  }
200
- return { picks, more: ranked.length - picks.length, candidates: ranked.length };
212
+ return { picks, more: ranked.length - picks.length, candidates: ranked.length, mode: "ranked" };
201
213
  }
202
214
  //# sourceMappingURL=taskRanking.js.map
@@ -0,0 +1,36 @@
1
+ import type { HunchStore } from "../store/hunchStore.js";
2
+ import { type RankEvalReport } from "./taskRankEval.js";
3
+ export declare const RANK_EVAL_CACHE_SCHEMA: "hunch.task-rank-eval-cache/1";
4
+ /** The decision's threshold: the kill rule needs at least this many task records. */
5
+ export declare const KILL_MIN_TASKS = 200;
6
+ export type TaskRankingMode = "ranked" | "latest";
7
+ export interface RankEvalCache {
8
+ schema: typeof RANK_EVAL_CACHE_SCHEMA;
9
+ computed_at: string;
10
+ /** Task-record count the report was computed over; a different count triggers a recompute. */
11
+ records: number;
12
+ /** Content hash of the record ids + report hashes, so an edit without a count change also refreshes. */
13
+ corpus_hash: string;
14
+ report: RankEvalReport;
15
+ }
16
+ export declare function readRankEvalCache(root: string): RankEvalCache | null;
17
+ /** Recompute when the corpus changed; otherwise return the cached report. Never throws. */
18
+ export declare function refreshRankEval(root: string, store: HunchStore, options?: {
19
+ force?: boolean;
20
+ now?: () => string;
21
+ }): RankEvalCache | null;
22
+ /** The rule from the decision, applied to a report. */
23
+ export declare function modeFromReport(report: RankEvalReport | null | undefined): {
24
+ mode: TaskRankingMode;
25
+ reason: string;
26
+ };
27
+ export interface ResolvedRankingMode {
28
+ mode: TaskRankingMode;
29
+ reason: string;
30
+ source: "override" | "auto";
31
+ cache: RankEvalCache | null;
32
+ }
33
+ /** What delivery should do right now for this repository. */
34
+ export declare function resolveTaskRankingMode(root: string, store: HunchStore): ResolvedRankingMode;
35
+ /** One line for `hunch now` and the task stats. */
36
+ export declare function rankingStatusLine(resolved: ResolvedRankingMode): string;
@@ -0,0 +1,122 @@
1
+ /** Automatic evaluation and the automatic kill rule for task-record ranking
2
+ * (dec_66925aa0ee). Nobody has to run anything:
3
+ *
4
+ * - every time a task record is written, the leave-one-out evaluation is
5
+ * recomputed over the graph's task records (pure, a few hundred records)
6
+ * and cached under .hunch-cache — derived state, safe to delete;
7
+ * - delivery reads the cache to pick its mode: `ranked` until the
8
+ * pre-registered rule says otherwise, `latest` (three most recent on the
9
+ * file) once the ranked selection has LOST to the baseline with a
10
+ * confidence interval excluding zero over at least KILL_MIN_TASKS records;
11
+ * - `hunch now` prints the current line; a verdict change becomes a finding.
12
+ *
13
+ * `.hunch/local.json` `taskRanking: "ranked" | "latest"` overrides the automatic
14
+ * choice for a repository that wants to pin it. */
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { flushCapture } from "../integrations/sync.js";
18
+ import { writeFileAtomic } from "./io.js";
19
+ import { hunchPaths } from "./paths.js";
20
+ import { evaluateTaskRanking } from "./taskRankEval.js";
21
+ import { reportHash } from "./taskReport.js";
22
+ import { FindingSchema } from "./types.js";
23
+ export const RANK_EVAL_CACHE_SCHEMA = "hunch.task-rank-eval-cache/1";
24
+ /** The decision's threshold: the kill rule needs at least this many task records. */
25
+ export const KILL_MIN_TASKS = 200;
26
+ function cachePath(root) {
27
+ return join(root, ".hunch-cache", "task-rank-eval.json");
28
+ }
29
+ export function readRankEvalCache(root) {
30
+ try {
31
+ const parsed = JSON.parse(readFileSync(cachePath(root), "utf8"));
32
+ return parsed && parsed.schema === RANK_EVAL_CACHE_SCHEMA && parsed.report ? parsed : null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ function corpusHash(store) {
39
+ const records = store.recs("tasks");
40
+ const ids = records.map((r) => `${r.id}:${r.report_hash}`).sort();
41
+ return { hash: reportHash(ids), count: records.length };
42
+ }
43
+ /** Recompute when the corpus changed; otherwise return the cached report. Never throws. */
44
+ export function refreshRankEval(root, store, options = {}) {
45
+ try {
46
+ const { hash, count } = corpusHash(store);
47
+ const cached = readRankEvalCache(root);
48
+ if (!options.force && cached && cached.corpus_hash === hash)
49
+ return cached;
50
+ const report = evaluateTaskRanking(store.recs("tasks"));
51
+ const next = { schema: RANK_EVAL_CACHE_SCHEMA, computed_at: (options.now ?? (() => new Date().toISOString()))(), records: count, corpus_hash: hash, report };
52
+ try {
53
+ const dir = join(root, ".hunch-cache");
54
+ if (!existsSync(dir))
55
+ return next; // no cache dir: still return the fresh report, just do not persist
56
+ writeFileAtomic(cachePath(root), JSON.stringify(next, null, 2));
57
+ }
58
+ catch { /* cache is a convenience */ }
59
+ if (cached && cached.report.verdict !== report.verdict)
60
+ noteVerdictChange(root, store, cached, next);
61
+ return next;
62
+ }
63
+ catch {
64
+ return readRankEvalCache(root);
65
+ }
66
+ }
67
+ /** The rule from the decision, applied to a report. */
68
+ export function modeFromReport(report) {
69
+ if (!report)
70
+ return { mode: "ranked", reason: "no evaluation yet" };
71
+ if (report.verdict === "baseline-better" && report.tasks >= KILL_MIN_TASKS) {
72
+ return { mode: "latest", reason: `ranked selection lost to latest3 on ${report.split.evaluated} cases (Hit@5 ${pct(report.rankers[0]?.hit5)} vs ${pct(report.rankers[1]?.hit5)}, CI [${pts(report.delta_hit5.ci95[0])}, ${pts(report.delta_hit5.ci95[1])}])` };
73
+ }
74
+ return { mode: "ranked", reason: report.verdict === "ranked-better" ? "ranked selection beats latest3" : report.verdict === "baseline-better" ? `latest3 ahead but only ${report.tasks} of ${KILL_MIN_TASKS} task records; not yet decisive` : `evaluation ${report.verdict} (${report.split.evaluated} cases)` };
75
+ }
76
+ /** What delivery should do right now for this repository. */
77
+ export function resolveTaskRankingMode(root, store) {
78
+ let override;
79
+ try {
80
+ override = JSON.parse(readFileSync(join(root, ".hunch", "local.json"), "utf8")).taskRanking;
81
+ }
82
+ catch { /* none */ }
83
+ const cache = refreshRankEval(root, store);
84
+ if (override === "ranked" || override === "latest")
85
+ return { mode: override, reason: `pinned by .hunch/local.json taskRanking`, source: "override", cache };
86
+ const { mode, reason } = modeFromReport(cache?.report);
87
+ return { mode, reason, source: "auto", cache };
88
+ }
89
+ function pct(x) { return x === undefined ? "–" : `${Math.round(x * 100)}%`; }
90
+ function pts(x) { return `${x >= 0 ? "+" : ""}${Math.round(x * 100)}`; }
91
+ /** One line for `hunch now` and the task stats. */
92
+ export function rankingStatusLine(resolved) {
93
+ const r = resolved.cache?.report;
94
+ if (!r)
95
+ return `task ranking: ${resolved.mode} · no evaluation yet (no finished task records)`;
96
+ const ranked = r.rankers.find((x) => x.name === "ranked"), base = r.rankers.find((x) => x.name === "latest3");
97
+ return `task ranking: ${resolved.mode}${resolved.source === "override" ? " (pinned)" : ""} · Hit@5 ranked ${pct(ranked?.hit5)} vs latest3 ${pct(base?.hit5)}, CI [${pts(r.delta_hit5.ci95[0])}, ${pts(r.delta_hit5.ci95[1])}], n=${r.split.evaluated} of ${r.tasks} records · ${r.verdict}${r.tasks < KILL_MIN_TASKS ? ` · kill rule armed at ${KILL_MIN_TASKS} records` : ""}`;
98
+ }
99
+ /** A verdict change is memory: record it once, through the normal capture path. */
100
+ function noteVerdictChange(root, store, prev, next) {
101
+ try {
102
+ const title = `Task ranking evaluation verdict changed: ${prev.report.verdict} → ${next.report.verdict} at ${next.records} task records`;
103
+ const id = `fnd_${reportHash(title).slice(7, 17)}`;
104
+ if (store.getRec("findings", id))
105
+ return;
106
+ const r = next.report;
107
+ const finding = FindingSchema.parse({
108
+ id, title,
109
+ observation: `Automatic leave-one-out evaluation of task-record ranking (dec_66925aa0ee) recomputed after a task record was written. Ranked Hit@5 ${pct(r.rankers[0]?.hit5)} vs latest3 ${pct(r.rankers[1]?.hit5)}, MRR ${r.rankers[0]?.mrr.toFixed(2)} vs ${r.rankers[1]?.mrr.toFixed(2)}, paired bootstrap CI on Hit@5 [${pts(r.delta_hit5.ci95[0])}, ${pts(r.delta_hit5.ci95[1])}] over ${r.split.evaluated} cases (${r.evaluable} evaluable of ${r.tasks}). Delivery mode now: ${modeFromReport(r).mode} — ${modeFromReport(r).reason}.`,
110
+ evidence: [`hunch task rank-eval --json (computed ${next.computed_at})`, `.hunch-cache/task-rank-eval.json corpus ${next.corpus_hash}`],
111
+ severity: next.report.verdict === "baseline-better" ? "high" : "low",
112
+ triage: "open",
113
+ affected_files: ["src/core/taskRanking.ts", "src/core/taskRankingMode.ts"],
114
+ observed_at: next.computed_at,
115
+ provenance: { source: "task_rank_eval", confidence: 0.9, evidence: ["hunch task rank-eval"], last_verified: next.computed_at },
116
+ });
117
+ store.putCapture("findings", finding, false);
118
+ flushCapture(store, hunchPaths(root).hunch, false, `hunch: capture ${id}`);
119
+ }
120
+ catch { /* the cache still carries the verdict; memory of the change is best-effort */ }
121
+ }
122
+ //# sourceMappingURL=taskRankingMode.js.map
@@ -17,6 +17,7 @@ import { hunchPaths } from "./paths.js";
17
17
  import { isEmptyTaskReport, readTaskReport, reportHash } from "./taskReport.js";
18
18
  import { reportSourceSnapshot } from "./taskReportEvidence.js";
19
19
  import { ENTITY_KINDS, TaskRecordSchema } from "./types.js";
20
+ import { refreshRankEval } from "./taskRankingMode.js";
20
21
  function localConfig(root) {
21
22
  try {
22
23
  return JSON.parse(readFileSync(join(root, ".hunch", "local.json"), "utf8"));
@@ -159,6 +160,8 @@ export function persistTaskRecord(root, store, taskId, options = {}) {
159
160
  store.reindex();
160
161
  const flushNow = options.flush ?? taskRecordFlushMode(root) === "each";
161
162
  const flushed = flushNow ? flushCapture(store, hunchPaths(root).hunch, home === "private", `hunch: task ${record.id}`) : null;
163
+ // The ranking evaluation follows the corpus: recomputed here, read at delivery. Never blocks a finish.
164
+ refreshRankEval(root, store);
162
165
  return { record: stored, home, flushed, changed: true };
163
166
  }
164
167
  const GRAPH_SCOPE = reportHash("graph-record");
@@ -1113,7 +1113,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
1113
1113
  const stateGrounding = asOf ? [] : stateSupplements(store.stateSlice(target), target);
1114
1114
  // Recent finished tasks that touched the target: what earlier agent work did
1115
1115
  // here, from graph memory. Advisory history sharing the brief's budget.
1116
- const recentTasks = asOf ? [] : taskSelectionSupplements(store.selectTasksFor(target, buildTaskRankingQuery(root, task_id ?? null, target)), target);
1116
+ const recentTasks = asOf ? [] : taskSelectionSupplements(store.selectTasksAuto(target, buildTaskRankingQuery(root, task_id ?? null, target)), target);
1117
1117
  const options = {
1118
1118
  root,
1119
1119
  symbols: store.recs("symbols"),
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { fileURLToPath } from "node:url";
2
+ import { fileURLToPath, pathToFileURL } from "node:url";
3
3
  import { TaskIdSchema, ReportClaimSchema, LessonReferenceSchema, finishReportTask, listReportTasks, readTaskReport, readLessonHistory, recordReportClaim, reportPresentationEnabled, startReportTask } from "../core/taskReport.js";
4
4
  import { persistTaskRecord } from "../core/taskRecord.js";
5
5
  import { reportSourceSnapshot, runReportConformance } from "../core/taskReportEvidence.js";
@@ -53,7 +53,11 @@ export function boundedTaskReportForHost(report) {
53
53
  function verificationLauncher() {
54
54
  const dev = import.meta.url.endsWith(".ts");
55
55
  const entry = fileURLToPath(new URL(`../cli/index.${dev ? "ts" : "js"}`, import.meta.url));
56
- const argv = [process.execPath, ...(dev ? ["--import", fileURLToPath(import.meta.resolve("tsx"))] : []), entry];
56
+ // `--import` takes a URL. Converting the resolved loader to a path made Node on
57
+ // Windows reject it ("Received protocol 'c:'"), so every verification launched
58
+ // from a source checkout there failed before running and cards showed no check.
59
+ const loader = import.meta.resolve("tsx");
60
+ const argv = [process.execPath, ...(dev ? ["--import", loader.startsWith("file:") ? loader : pathToFileURL(loader).href] : []), entry];
57
61
  const quote = (s) => process.platform === "win32" ? `'${s.replace(/'/g, "''")}'` : `'${s.replace(/'/g, "'\\''")}'`;
58
62
  return { argv, shell: `${process.platform === "win32" ? "& " : ""}${argv.map(quote).join(" ")}` };
59
63
  }
@@ -428,7 +428,11 @@ export declare class HunchStore {
428
428
  /** Gate → score → slots for one target and the current task's query (dec_66925aa0ee). */
429
429
  selectTasksFor(target: string, query: RankingQuery, options?: SlotOptions & {
430
430
  weights?: Readonly<RankingWeights>;
431
+ mode?: "ranked" | "latest";
431
432
  }): TaskSelection;
433
+ /** What delivery uses: the mode comes from the automatic evaluation (or a
434
+ * local pin), never from a flag the caller has to remember. */
435
+ selectTasksAuto(target: string, query: RankingQuery): TaskSelection;
432
436
  /** The causal chain behind a constraint — the WHY a diff-only reviewer can't see.
433
437
  * Deterministic graph join: constraint → source_decision (the decision that
434
438
  * motivated the guard) → the bug whose root cause spawned it (via
@@ -22,7 +22,8 @@ import { gitCommonDir, gitWorktreeRoot, isolatedHeadSha, sameGitPublication, sco
22
22
  import { pathMatchesGlob, pathsRelated } from "../core/glob.js";
23
23
  import { cochangeFor } from "../core/cochange.js";
24
24
  import { withServedDatabase } from "../core/served.js";
25
- import { normalizePath, rankTaskRecords, recordIdsOf, selectTaskSlots } from "../core/taskRanking.js";
25
+ import { normalizePath, rankTaskRecords, recordIdsOf, selectLatestTasks, selectTaskSlots } from "../core/taskRanking.js";
26
+ import { resolveTaskRankingMode } from "../core/taskRankingMode.js";
26
27
  import { currentForTopic, isInForce } from "../core/topics.js";
27
28
  import { edgeId } from "../core/ids.js";
28
29
  import { isStrictBlocker, isVetoBlocker } from "../core/strictgate.js";
@@ -1560,9 +1561,18 @@ export class HunchStore {
1560
1561
  /** Gate → score → slots for one target and the current task's query (dec_66925aa0ee). */
1561
1562
  selectTasksFor(target, query, options = {}) {
1562
1563
  const ctx = this.taskRankingContext(target, query);
1563
- const ranked = rankTaskRecords(this.taskCandidates(target, query, ctx), query, ctx, options.weights);
1564
+ const candidates = this.taskCandidates(target, query, ctx);
1565
+ if (options.mode === "latest")
1566
+ return selectLatestTasks(candidates, query, ctx);
1567
+ const ranked = rankTaskRecords(candidates, query, ctx, options.weights);
1564
1568
  return selectTaskSlots(ranked, options);
1565
1569
  }
1570
+ /** What delivery uses: the mode comes from the automatic evaluation (or a
1571
+ * local pin), never from a flag the caller has to remember. */
1572
+ selectTasksAuto(target, query) {
1573
+ const resolved = resolveTaskRankingMode(this.paths.root, this);
1574
+ return this.selectTasksFor(target, query, { mode: resolved.mode });
1575
+ }
1566
1576
  /** The causal chain behind a constraint — the WHY a diff-only reviewer can't see.
1567
1577
  * Deterministic graph join: constraint → source_decision (the decision that
1568
1578
  * motivated the guard) → the bug whose root cause spawned it (via
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.37.0",
3
+ "version": "1.37.1",
4
4
  "mcpName": "io.github.davesheffer/hunch",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://www.hunchmemory.com",
10
- "version": "1.37.0",
10
+ "version": "1.37.1",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@davesheffer/hunch",
16
- "version": "1.37.0",
16
+ "version": "1.37.1",
17
17
  "runtimeHint": "npx",
18
18
  "packageArguments": [
19
19
  {