@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 +8 -2
- package/dist/cli/taskReport.js +4 -0
- package/dist/constitution/schema.d.ts +2 -2
- package/dist/constitution/scorecard.d.ts +2 -2
- package/dist/core/outcomeExperience.d.ts +1 -1
- package/dist/core/stateContract.d.ts +3 -3
- package/dist/core/taskDelivery.js +3 -1
- package/dist/core/taskRanking.d.ts +5 -0
- package/dist/core/taskRanking.js +13 -1
- package/dist/core/taskRankingMode.d.ts +36 -0
- package/dist/core/taskRankingMode.js +122 -0
- package/dist/core/taskRecord.js +3 -0
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/taskReportTools.js +6 -2
- package/dist/store/hunchStore.d.ts +4 -0
- package/dist/store/hunchStore.js +12 -2
- package/package.json +1 -1
- package/server.json +2 -2
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.
|
|
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.
|
|
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();
|
package/dist/cli/taskReport.js
CHANGED
|
@@ -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 =
|
|
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;
|
package/dist/core/taskRanking.js
CHANGED
|
@@ -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
|
package/dist/core/taskRecord.js
CHANGED
|
@@ -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");
|
package/dist/mcp/server.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
package/dist/store/hunchStore.js
CHANGED
|
@@ -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
|
|
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
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.
|
|
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.
|
|
16
|
+
"version": "1.37.1",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|