@davesheffer/hunch 1.36.0 → 1.37.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/dist/cli/index.js +26 -17
- package/dist/cli/taskReport.js +33 -0
- package/dist/constitution/renderEvaluations.d.ts +7 -1
- package/dist/constitution/renderEvaluations.js +21 -1
- package/dist/core/cochange.d.ts +15 -0
- package/dist/core/cochange.js +113 -0
- package/dist/core/taskDelivery.d.ts +4 -0
- package/dist/core/taskDelivery.js +30 -0
- package/dist/core/taskQuery.d.ts +7 -0
- package/dist/core/taskQuery.js +44 -0
- package/dist/core/taskRankEval.d.ts +70 -0
- package/dist/core/taskRankEval.js +195 -0
- package/dist/core/taskRanking.d.ts +113 -0
- package/dist/core/taskRanking.js +202 -0
- package/dist/core/taskRecord.d.ts +5 -0
- package/dist/core/taskRecord.js +30 -2
- package/dist/core/taskRecordStats.d.ts +14 -0
- package/dist/core/taskRecordStats.js +45 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/core/types.js +1 -0
- package/dist/mcp/server.js +24 -15
- package/dist/store/hunchStore.d.ts +20 -0
- package/dist/store/hunchStore.js +110 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/mcp/server.js
CHANGED
|
@@ -32,7 +32,8 @@ import { withWriteLock } from "../serve/writelock.js";
|
|
|
32
32
|
import { advertisedTeamRemoteContract, ensureTeamOverlay, overlayMatchesTeamRemote, readTeamConfig, teamRemoteContract, teamSharedRef } from "../integrations/team.js";
|
|
33
33
|
import { formatSearchHit, formatStructure } from "../core/format.js";
|
|
34
34
|
import { isStateKind, stateSupplements } from "../core/stateDelivery.js";
|
|
35
|
-
import {
|
|
35
|
+
import { taskSelectionSupplements } from "../core/taskDelivery.js";
|
|
36
|
+
import { buildTaskRankingQuery } from "../core/taskQuery.js";
|
|
36
37
|
import { diagnoseIssueCorrectionStage, formatCorrectionStageDiagnostic } from "../core/correctionStage.js";
|
|
37
38
|
import { compileVerifiedEvidenceMap, EvidenceExecutionSchema, EvidenceInterventionSchema, EvidenceProbeSchema, formatVerifiedEvidenceMap, VerifiedEvidenceReceiptSchema, } from "../core/evidenceMap.js";
|
|
38
39
|
import { collectCorrectionStageSources } from "../extractors/correctionSources.js";
|
|
@@ -360,19 +361,27 @@ function deliveredContext(root, target, envelope, sessionId) {
|
|
|
360
361
|
const state = armExecutionObligations(loadPipelineState(sessionId), structuredContent.obligations, { replaceOrigin: "memory" });
|
|
361
362
|
savePipelineState(sessionId, state);
|
|
362
363
|
}
|
|
363
|
-
recordServed(root,
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
364
|
+
recordServed(root, [
|
|
365
|
+
...structuredContent.delivered.map((item) => ({
|
|
366
|
+
event: "served",
|
|
367
|
+
kind: item.kind,
|
|
368
|
+
record_id: item.record_id,
|
|
369
|
+
target,
|
|
370
|
+
session_id: sessionId,
|
|
371
|
+
rank: item.rank,
|
|
372
|
+
delivery_reason: item.delivery_reason,
|
|
373
|
+
provenance_status: item.provenance_status,
|
|
374
|
+
token_cost: item.token_cost,
|
|
375
|
+
delivery_profile: structuredContent.profile,
|
|
376
|
+
ranking_policy: structuredContent.ranking_policy,
|
|
377
|
+
})),
|
|
378
|
+
// Delivered task lines are receipts too: they feed access-based recency.
|
|
379
|
+
...envelope.supplements.filter((s) => s.kind === "recent-task" && s.delivered).map((s) => ({
|
|
380
|
+
event: "served", kind: "tasks", record_id: s.id, target, session_id: sessionId,
|
|
381
|
+
rank: s.rank, delivery_reason: "supplemental", token_cost: s.token_cost,
|
|
382
|
+
delivery_profile: structuredContent.profile, ranking_policy: structuredContent.ranking_policy,
|
|
383
|
+
})),
|
|
384
|
+
]);
|
|
376
385
|
return {
|
|
377
386
|
content: [{ type: "text", text: structuredContent.text }],
|
|
378
387
|
structuredContent,
|
|
@@ -1104,7 +1113,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1104
1113
|
const stateGrounding = asOf ? [] : stateSupplements(store.stateSlice(target), target);
|
|
1105
1114
|
// Recent finished tasks that touched the target: what earlier agent work did
|
|
1106
1115
|
// here, from graph memory. Advisory history sharing the brief's budget.
|
|
1107
|
-
const recentTasks = asOf ? [] :
|
|
1116
|
+
const recentTasks = asOf ? [] : taskSelectionSupplements(store.selectTasksFor(target, buildTaskRankingQuery(root, task_id ?? null, target)), target);
|
|
1108
1117
|
const options = {
|
|
1109
1118
|
root,
|
|
1110
1119
|
symbols: store.recs("symbols"),
|
|
@@ -3,6 +3,7 @@ import { type Component, type Constraint, type Bug, type Decision, type Symbol,
|
|
|
3
3
|
import { type DB } from "./db.js";
|
|
4
4
|
import { type Embedder } from "./embedder.js";
|
|
5
5
|
import { JsonStore } from "./jsonStore.js";
|
|
6
|
+
import { type RankingContext, type RankingQuery, type RankingWeights, type SlotOptions, type TaskSelection } from "../core/taskRanking.js";
|
|
6
7
|
import { type VetoTier } from "../core/strictgate.js";
|
|
7
8
|
import { type DiffAnalysis } from "../extractors/diff.js";
|
|
8
9
|
import type { CheckReport, CausalWhy, ImpactReport } from "../core/checkreport.js";
|
|
@@ -409,6 +410,25 @@ export declare class HunchStore {
|
|
|
409
410
|
* denied edits), newest first. Graph memory, so it spans machines and survives
|
|
410
411
|
* the local ledger's retention window. */
|
|
411
412
|
tasksFor(scope: string, limit?: number): TaskRecord[];
|
|
413
|
+
/** Every task record that could matter for `target` under the ranking gate:
|
|
414
|
+
* same file (exact or glob), a dependent's file, a co-changed file, or a
|
|
415
|
+
* record sharing one of the current task's own record ids. Bounded; the
|
|
416
|
+
* ranker does the gating and scoring. */
|
|
417
|
+
taskCandidates(target: string, query: RankingQuery, ctx: RankingContext): TaskRecord[];
|
|
418
|
+
/** Every task id some later record verified over. */
|
|
419
|
+
supersededTaskIds(): Set<string>;
|
|
420
|
+
/** Last delivery time per task record from the local receipt ledger, if any. */
|
|
421
|
+
taskLastDelivered(): Map<string, number>;
|
|
422
|
+
/** bm25 of a phrase over task titles and lesson titles, normalized to the top hit. */
|
|
423
|
+
taskLexicalScores(phrase: string | null, limit?: number): Map<string, number>;
|
|
424
|
+
/** Corpus inputs for ranking: dependents' files via the symbol graph, co-change
|
|
425
|
+
* from git history (bounded, cached), record-id document frequencies across
|
|
426
|
+
* task records, lexical scores, and which anchors still exist. */
|
|
427
|
+
taskRankingContext(target: string, query: RankingQuery): RankingContext;
|
|
428
|
+
/** Gate → score → slots for one target and the current task's query (dec_66925aa0ee). */
|
|
429
|
+
selectTasksFor(target: string, query: RankingQuery, options?: SlotOptions & {
|
|
430
|
+
weights?: Readonly<RankingWeights>;
|
|
431
|
+
}): TaskSelection;
|
|
412
432
|
/** The causal chain behind a constraint — the WHY a diff-only reviewer can't see.
|
|
413
433
|
* Deterministic graph join: constraint → source_decision (the decision that
|
|
414
434
|
* motivated the guard) → the bug whose root cause spawned it (via
|
package/dist/store/hunchStore.js
CHANGED
|
@@ -20,6 +20,9 @@ import { selectEmbedder } from "./embedder.js";
|
|
|
20
20
|
import { JsonStore } from "./jsonStore.js";
|
|
21
21
|
import { gitCommonDir, gitWorktreeRoot, isolatedHeadSha, sameGitPublication, scopedLastChangeDates, } from "../extractors/git.js";
|
|
22
22
|
import { pathMatchesGlob, pathsRelated } from "../core/glob.js";
|
|
23
|
+
import { cochangeFor } from "../core/cochange.js";
|
|
24
|
+
import { withServedDatabase } from "../core/served.js";
|
|
25
|
+
import { normalizePath, rankTaskRecords, recordIdsOf, selectTaskSlots } from "../core/taskRanking.js";
|
|
23
26
|
import { currentForTopic, isInForce } from "../core/topics.js";
|
|
24
27
|
import { edgeId } from "../core/ids.js";
|
|
25
28
|
import { isStrictBlocker, isVetoBlocker } from "../core/strictgate.js";
|
|
@@ -1453,6 +1456,113 @@ export class HunchStore {
|
|
|
1453
1456
|
.sort((a, b) => b.finished_at.localeCompare(a.finished_at) || a.id.localeCompare(b.id))
|
|
1454
1457
|
.slice(0, Math.max(1, limit));
|
|
1455
1458
|
}
|
|
1459
|
+
/** Every task record that could matter for `target` under the ranking gate:
|
|
1460
|
+
* same file (exact or glob), a dependent's file, a co-changed file, or a
|
|
1461
|
+
* record sharing one of the current task's own record ids. Bounded; the
|
|
1462
|
+
* ranker does the gating and scoring. */
|
|
1463
|
+
taskCandidates(target, query, ctx) {
|
|
1464
|
+
const t = normalizePath(toPosixTarget(target));
|
|
1465
|
+
const out = new Map();
|
|
1466
|
+
for (const r of this.tasksFor(t, 200))
|
|
1467
|
+
out.set(r.id, r);
|
|
1468
|
+
if (ctx.dependents.size || ctx.cochange.size || query.recordIds.size) {
|
|
1469
|
+
for (const r of this.recs("tasks")) {
|
|
1470
|
+
if (out.has(r.id))
|
|
1471
|
+
continue;
|
|
1472
|
+
const files = r.files.map(normalizePath);
|
|
1473
|
+
if (files.some((f) => ctx.dependents.has(f) || ctx.cochange.has(f))) {
|
|
1474
|
+
out.set(r.id, r);
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
if (query.recordIds.size) {
|
|
1478
|
+
const ids = recordIdsOf(r);
|
|
1479
|
+
for (const id of query.recordIds)
|
|
1480
|
+
if (ids.has(id)) {
|
|
1481
|
+
out.set(r.id, r);
|
|
1482
|
+
break;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return [...out.values()];
|
|
1488
|
+
}
|
|
1489
|
+
/** Every task id some later record verified over. */
|
|
1490
|
+
supersededTaskIds() {
|
|
1491
|
+
const out = new Set();
|
|
1492
|
+
for (const r of this.recs("tasks"))
|
|
1493
|
+
for (const id of r.supersedes)
|
|
1494
|
+
out.add(id);
|
|
1495
|
+
return out;
|
|
1496
|
+
}
|
|
1497
|
+
/** Last delivery time per task record from the local receipt ledger, if any. */
|
|
1498
|
+
taskLastDelivered() {
|
|
1499
|
+
const out = new Map();
|
|
1500
|
+
try {
|
|
1501
|
+
withServedDatabase(this.paths.root, (db) => {
|
|
1502
|
+
const rows = db.prepare("SELECT record_id, MAX(at) AS at FROM served WHERE kind = 'tasks' GROUP BY record_id").all();
|
|
1503
|
+
for (const row of rows) {
|
|
1504
|
+
const t = Date.parse(row.at);
|
|
1505
|
+
if (Number.isFinite(t))
|
|
1506
|
+
out.set(row.record_id, t);
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
catch { /* no ledger on this machine: recency falls back to finished_at */ }
|
|
1511
|
+
return out;
|
|
1512
|
+
}
|
|
1513
|
+
/** bm25 of a phrase over task titles and lesson titles, normalized to the top hit. */
|
|
1514
|
+
taskLexicalScores(phrase, limit = 50) {
|
|
1515
|
+
const scores = new Map();
|
|
1516
|
+
if (!phrase || !phrase.trim())
|
|
1517
|
+
return scores;
|
|
1518
|
+
const hits = this.scopedFts(phrase, "tasks", limit).filter((h) => Number.isFinite(h.score));
|
|
1519
|
+
if (!hits.length)
|
|
1520
|
+
return scores;
|
|
1521
|
+
// bm25 from FTS5 is negative, lower is better; normalize magnitude to the best hit.
|
|
1522
|
+
const best = Math.max(...hits.map((h) => Math.abs(h.score)));
|
|
1523
|
+
if (!(best > 0))
|
|
1524
|
+
return scores;
|
|
1525
|
+
for (const h of hits)
|
|
1526
|
+
scores.set(h.ref, Math.max(0, Math.min(1, Math.abs(h.score) / best)));
|
|
1527
|
+
return scores;
|
|
1528
|
+
}
|
|
1529
|
+
/** Corpus inputs for ranking: dependents' files via the symbol graph, co-change
|
|
1530
|
+
* from git history (bounded, cached), record-id document frequencies across
|
|
1531
|
+
* task records, lexical scores, and which anchors still exist. */
|
|
1532
|
+
taskRankingContext(target, query) {
|
|
1533
|
+
const t = normalizePath(toPosixTarget(target));
|
|
1534
|
+
const dependents = new Set();
|
|
1535
|
+
try {
|
|
1536
|
+
const symbolFile = new Map(this.recs("symbols").map((s) => [s.id, normalizePath(s.file)]));
|
|
1537
|
+
for (const sym of this.why(t).symbols) {
|
|
1538
|
+
for (const d of this.getDependents(sym.id)) {
|
|
1539
|
+
const f = symbolFile.get(d.id);
|
|
1540
|
+
if (f && f !== t)
|
|
1541
|
+
dependents.add(f);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
catch { /* no symbol graph: the dependents term is simply absent */ }
|
|
1546
|
+
const cochange = /[./]/.test(t) && !/\s/.test(t) ? cochangeFor(this.paths.root, t) : new Map();
|
|
1547
|
+
const tasks = this.recs("tasks");
|
|
1548
|
+
const df = new Map();
|
|
1549
|
+
for (const r of tasks)
|
|
1550
|
+
for (const id of recordIdsOf(r))
|
|
1551
|
+
df.set(id, (df.get(id) ?? 0) + 1);
|
|
1552
|
+
const n = Math.max(1, tasks.length);
|
|
1553
|
+
const ruleStats = (id) => { const d = df.get(id) ?? 0; return { df: d, idf: Math.log((n + 1) / (d + 1)) + 1e-6 }; };
|
|
1554
|
+
const lexical = this.taskLexicalScores(query.phrase);
|
|
1555
|
+
const anchorsAlive = (r) => r.files.length ? r.files.filter((f) => existsSync(join(this.paths.root, normalizePath(f)))).length / r.files.length : 1;
|
|
1556
|
+
const superseded = this.supersededTaskIds();
|
|
1557
|
+
const delivered = this.taskLastDelivered();
|
|
1558
|
+
return { dependents, cochange, ruleStats, lexical, anchorsAlive, superseded, lastDelivered: (id) => delivered.get(id) ?? null };
|
|
1559
|
+
}
|
|
1560
|
+
/** Gate → score → slots for one target and the current task's query (dec_66925aa0ee). */
|
|
1561
|
+
selectTasksFor(target, query, options = {}) {
|
|
1562
|
+
const ctx = this.taskRankingContext(target, query);
|
|
1563
|
+
const ranked = rankTaskRecords(this.taskCandidates(target, query, ctx), query, ctx, options.weights);
|
|
1564
|
+
return selectTaskSlots(ranked, options);
|
|
1565
|
+
}
|
|
1456
1566
|
/** The causal chain behind a constraint — the WHY a diff-only reviewer can't see.
|
|
1457
1567
|
* Deterministic graph join: constraint → source_decision (the decision that
|
|
1458
1568
|
* 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.
|
|
10
|
+
"version": "1.37.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@davesheffer/hunch",
|
|
16
|
-
"version": "1.
|
|
16
|
+
"version": "1.37.0",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|