@davesheffer/hunch 1.19.0 → 1.20.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -61
- package/dist/cli/index.js +99 -2
- package/dist/core/delivery.js +213 -13
- package/dist/core/landscapeAdoption.js +271 -0
- package/dist/core/landscapeDelivery.js +281 -0
- package/dist/core/types.js +128 -1
- package/dist/extractors/git.js +2 -2
- package/dist/extractors/landscapeDiscovery.js +2217 -280
- package/dist/mcp/server.js +45 -4
- package/dist/store/hunchStore.js +7 -0
- package/dist/synthesis/synthesize.js +22 -7
- package/package.json +3 -2
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -1,49 +1,33 @@
|
|
|
1
1
|
# Hunch
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Give your AI coding assistant the missing story behind your code.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@davesheffer/hunch)
|
|
6
6
|
[](https://github.com/davesheffer/hunch)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
layer — the decisions, constraints, rejected approaches, and bug history behind your code, delivered
|
|
13
|
-
as evidence before an assistant changes anything, with the result checked deterministically after.
|
|
9
|
+
Git remembers what changed. Chat history disappears. A future Claude, Cursor, Codex, or Copilot
|
|
10
|
+
session can read your code, but it usually does not know why your team chose this design, which
|
|
11
|
+
alternative failed, or which old bug a strange-looking line prevents.
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
**Hunch is project memory for the AI coding tools you already use.** It saves decisions, bug fixes,
|
|
14
|
+
rejected approaches, and important rules. Before an assistant changes code, Hunch brings back the
|
|
15
|
+
relevant reasons. After the change, it can check whether the work conflicts with a rule your team
|
|
16
|
+
explicitly trusts.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
then a deterministic check of the change against the rules your team has explicitly trusted.
|
|
18
|
+
### A simple example
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
Your team once moved login sessions to the server so stolen tokens could be disabled immediately.
|
|
21
|
+
Months later, an AI assistant sees the extra code and proposes a “simpler” token-only design. Hunch
|
|
22
|
+
shows the assistant why the server-side design exists and which security bug it prevents—before the
|
|
23
|
+
edit happens.
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
That means less time repeating old explanations, fewer old mistakes returning, and the same project
|
|
26
|
+
context across every coding assistant.
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
| Correct file found | 8/12 (66.7%) | 10/12 (83.3%) | +16.6 percentage points |
|
|
32
|
-
| Declarations inspected for the same five hits | 18.9 average | 11 average | 41.9% less |
|
|
33
|
-
|
|
34
|
-
These are bounded diagnostic and inspection results, not a claim that Hunch is universally twice as
|
|
35
|
-
accurate. Failed evidence and causal rerankers remain disabled; the receipts annotate the shortlist
|
|
36
|
-
without changing its order or asserting an exact owner.
|
|
37
|
-
|
|
38
|
-
> **New in v1.18.0:** YAML and Helm now enter the same dependency graph as application code.
|
|
39
|
-
> Anchors and aliases become reference edges; chart-scoped `define`, `include`, and `template`
|
|
40
|
-
> relationships survive pre-render syntax without weakening fail-closed handling for ordinary
|
|
41
|
-
> invalid YAML.
|
|
42
|
-
|
|
43
|
-
Read the [v1.18 release story](https://hunch-pi.vercel.app/blog/post?slug=configuration-joins-the-graph)
|
|
44
|
-
for the graph semantics, chart boundaries, honest limits, and Oliver Sampson's contribution.
|
|
45
|
-
|
|
46
|
-
See the public [roadmap](ROADMAP.md) for what is next and what is deliberately out of scope.
|
|
28
|
+
Hunch is **not another AI model** and it does not replace your coding assistant. It is the memory and
|
|
29
|
+
safety layer behind it. Memory is advisory by default; nothing blocks work unless you deliberately
|
|
30
|
+
turn on strict checks for a precise rule.
|
|
47
31
|
|
|
48
32
|
## Start in five minutes
|
|
49
33
|
|
|
@@ -56,44 +40,76 @@ hunch init
|
|
|
56
40
|
hunch backfill --since 90d # optional: seed memory from recent history
|
|
57
41
|
```
|
|
58
42
|
|
|
59
|
-
Reload your coding assistant, then ask:
|
|
43
|
+
Reload your coding assistant, then ask a normal question:
|
|
60
44
|
|
|
61
45
|
> Why is this built this way?
|
|
62
46
|
|
|
63
|
-
|
|
64
|
-
|
|
47
|
+
Hunch answers from your project's saved history and shows where the answer came from. `hunch init`
|
|
48
|
+
indexes the repo, installs local hooks, and connects supported assistants without replacing their
|
|
49
|
+
existing configuration.
|
|
65
50
|
|
|
66
51
|
## What Hunch gives you
|
|
67
52
|
|
|
68
|
-
- **
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
- **
|
|
75
|
-
|
|
76
|
-
- **
|
|
77
|
-
|
|
78
|
-
|
|
53
|
+
- **A memory that outlives chat** — decisions and corrections are still available next week, next
|
|
54
|
+
year, and in a different assistant.
|
|
55
|
+
- **One shared story** — Claude Code, Cursor, Copilot, Windsurf, Antigravity, Codex, and any MCP
|
|
56
|
+
client get the same project context.
|
|
57
|
+
- **Warnings with reasons** — review a change against trusted project rules and see exactly why it
|
|
58
|
+
passes, needs attention, or should be blocked.
|
|
59
|
+
- **Past bugs stay useful** — see which old incident a piece of code fixed before accidentally
|
|
60
|
+
undoing it.
|
|
61
|
+
- **Understands how code connects** — for TypeScript, JavaScript, Python, Go, YAML, and Helm, Hunch
|
|
62
|
+
can see what calls or depends on the code you are about to change. Its memory works with any
|
|
63
|
+
language.
|
|
64
|
+
- **Works with existing decision documents** — import your architecture decision records into
|
|
65
|
+
Hunch, or export Hunch decisions back to a standard format other tools can read.
|
|
79
66
|
|
|
80
67
|
The source of truth is readable JSON in `.hunch/`. A local SQLite index makes retrieval fast but
|
|
81
68
|
is always rebuildable.
|
|
82
69
|
|
|
83
|
-
##
|
|
70
|
+
## What improved in v1.19
|
|
71
|
+
|
|
72
|
+
Hunch now gives developers a better, shorter list of code to inspect when they describe a problem.
|
|
73
|
+
In a 12-problem test on unfamiliar code, it found the changed piece of code in 6 cases instead of 3
|
|
74
|
+
and found the correct file in 10 cases instead of 8. In a separate test, it kept the same five
|
|
75
|
+
successful finds while reducing the average number of named code items to inspect—such as functions
|
|
76
|
+
or classes—from 18.9 to 11.
|
|
77
|
+
|
|
78
|
+
| What the test measured | Before | v1.19 | Result |
|
|
79
|
+
| --- | ---: | ---: | ---: |
|
|
80
|
+
| Problems where Hunch found the changed code | 3/12 | 6/12 | 2× as many in this test |
|
|
81
|
+
| Problems where Hunch found the correct file | 8/12 | 10/12 | 2 more correct files |
|
|
82
|
+
| Pieces of code inspected for the same five finds | 18.9 average | 11 average | 41.9% less to inspect |
|
|
83
|
+
|
|
84
|
+
These are small, controlled tests—not a promise that Hunch is twice as accurate everywhere. Hunch
|
|
85
|
+
also refuses to pretend it knows the exact fix when the evidence only shows where to investigate.
|
|
86
|
+
The detailed receipts live in [`bench/external/results`](bench/external/results).
|
|
87
|
+
|
|
88
|
+
See the public [roadmap](ROADMAP.md) for what is next and what is deliberately out of scope.
|
|
89
|
+
|
|
90
|
+
## Common tasks
|
|
84
91
|
|
|
85
92
|
Most memory work happens automatically after commits. These commands cover the common manual paths:
|
|
86
93
|
|
|
87
94
|
| Command | Use it for |
|
|
88
95
|
| --- | --- |
|
|
89
|
-
| `hunch why <file>` |
|
|
96
|
+
| `hunch why <file>` | Explain why a file is built this way and what could be affected by changing it |
|
|
90
97
|
| `hunch query "<question>"` | Search project memory |
|
|
91
|
-
| `hunch check --working` |
|
|
92
|
-
| `hunch
|
|
93
|
-
| `hunch
|
|
94
|
-
| `hunch
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
| `hunch check --working` | Check current changes against the decisions and rules your team trusts |
|
|
99
|
+
| `hunch log` | See what Hunch remembered and undo a memory change if needed |
|
|
100
|
+
| `hunch escalations` | See the rare questions that need a human answer |
|
|
101
|
+
| `hunch doctor` | Diagnose setup problems |
|
|
102
|
+
|
|
103
|
+
<details>
|
|
104
|
+
<summary><strong>Advanced: problem shortlisting and evidence receipts</strong></summary>
|
|
105
|
+
|
|
106
|
+
These tools help researchers and maintainers investigate where a described behavior may live. They
|
|
107
|
+
show uncertainty instead of claiming to know the exact fix.
|
|
108
|
+
|
|
109
|
+
| Command | Use it for |
|
|
110
|
+
| --- | --- |
|
|
111
|
+
| `hunch shortlist --issue "..."` | Build a bounded list of files and named code items to inspect |
|
|
112
|
+
| `hunch evidence-map receipt.json` | Add observed execution evidence without guessing the exact owner |
|
|
97
113
|
|
|
98
114
|
`hunch evidence-map` accepts a bounded JSON receipt containing a red target, a distinct green
|
|
99
115
|
control, optional execution counts, and optional intervention outcomes. It reports target-only and
|
|
@@ -133,6 +149,8 @@ one-hop relationship expansion added none, and evidence/causal rerankers also fa
|
|
|
133
149
|
transfer gates. The receipts remain in `bench/external/results`; rejected mechanisms cannot silently
|
|
134
150
|
change the production ordering.
|
|
135
151
|
|
|
152
|
+
</details>
|
|
153
|
+
|
|
136
154
|
Corrections can become scoped rules, but captured memory cannot hard-block on its own. Enforcement is
|
|
137
155
|
deterministic and opt-in:
|
|
138
156
|
|
|
@@ -141,12 +159,11 @@ hunch firmness strict
|
|
|
141
159
|
hunch check --staged --strict
|
|
142
160
|
```
|
|
143
161
|
|
|
144
|
-
## Share
|
|
162
|
+
## Share the same memory with your team
|
|
145
163
|
|
|
146
|
-
|
|
147
|
-
repository, separate from the code repository. Hunch does not host
|
|
148
|
-
|
|
149
|
-
have one maintainer run:
|
|
164
|
+
For a team, Hunch can keep everyone’s decisions, corrections, and rules in one private Git
|
|
165
|
+
repository, separate from the code repository. Hunch does not host it. Create a private repository
|
|
166
|
+
that every teammate can access, install Hunch on team machines and CI, then have one maintainer run:
|
|
150
167
|
|
|
151
168
|
```bash
|
|
152
169
|
npm i -g @davesheffer/hunch@1.19.0
|
package/dist/cli/index.js
CHANGED
|
@@ -81,6 +81,8 @@ import { pendingEscalations, policyEscalations } from "../core/escalations.js";
|
|
|
81
81
|
import { premiseEscalations } from "../core/premises.js";
|
|
82
82
|
import { parseDocAnchors, renderDocGrounding } from "../core/docanchors.js";
|
|
83
83
|
import { compareCandidates } from "../core/compare.js";
|
|
84
|
+
import { planLandscapeAdoption } from "../core/landscapeAdoption.js";
|
|
85
|
+
import { discoverRepositoryLandscape } from "../extractors/landscapeDiscovery.js";
|
|
84
86
|
import { checkConformance } from "../core/conformance.js";
|
|
85
87
|
import { ConstitutionService, policyEvaluationEnvelope } from "../constitution/service.js";
|
|
86
88
|
import { sourceGraphSnapshot } from "../constitution/evaluator.js";
|
|
@@ -2853,6 +2855,87 @@ for (const command of ["status", "report"]) {
|
|
|
2853
2855
|
}
|
|
2854
2856
|
});
|
|
2855
2857
|
}
|
|
2858
|
+
// ---- landscape (review + explicitly adopt exact repository candidates) ---
|
|
2859
|
+
const landscapeCmd = program
|
|
2860
|
+
.command("landscape")
|
|
2861
|
+
.description("Review exact-revision Engineering Landscape candidates and explicitly adopt them into the graph.");
|
|
2862
|
+
landscapeCmd
|
|
2863
|
+
.command("review")
|
|
2864
|
+
.description("Discover a read-only, hash-bound candidate set. This command never writes graph authority.")
|
|
2865
|
+
.option("--ref <ref>", "Git commit/ref to inspect", "HEAD")
|
|
2866
|
+
.option("--json", "emit the complete machine-readable discovery envelope")
|
|
2867
|
+
.action((opts) => {
|
|
2868
|
+
const root = findRoot();
|
|
2869
|
+
const discovery = discoverRepositoryLandscape(root, opts.ref);
|
|
2870
|
+
if (opts.json) {
|
|
2871
|
+
console.log(JSON.stringify(discovery, null, 2));
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
console.log(`Landscape candidates at ${discovery.sourceRevision}`);
|
|
2875
|
+
console.log(`Discovery: ${discovery.discoveryHash}`);
|
|
2876
|
+
console.log(`Repository identity: ${discovery.repositoryRootIdentity}\n`);
|
|
2877
|
+
console.log(`RESOURCES (${discovery.resources.length})`);
|
|
2878
|
+
for (const candidate of discovery.resources) {
|
|
2879
|
+
console.log(` ${candidate.candidateHash} ${candidate.record.id} ${candidate.record.name}`);
|
|
2880
|
+
}
|
|
2881
|
+
console.log(`\nRELATIONSHIPS (${discovery.relationships.length})`);
|
|
2882
|
+
for (const candidate of discovery.relationships) {
|
|
2883
|
+
console.log(` ${candidate.candidateHash} ${candidate.record.from} --${candidate.record.type}--> ${candidate.record.to}`);
|
|
2884
|
+
}
|
|
2885
|
+
if (discovery.issues.length) {
|
|
2886
|
+
console.log(`\nISSUES (${discovery.issues.length})`);
|
|
2887
|
+
for (const issue of discovery.issues) {
|
|
2888
|
+
console.log(` ${issue.code} ${issue.sourcePath}${issue.sourceField ? `#${issue.sourceField}` : ""} — ${issue.detail}`);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
console.log("\nNothing was written. Review the candidates, then adopt all of them with:");
|
|
2892
|
+
console.log(` hunch landscape adopt --ref ${discovery.sourceRevision} --expected ${discovery.discoveryHash} --all --reviewed-by <you>${discovery.issues.length ? " --acknowledge-issues" : ""}`);
|
|
2893
|
+
console.log("Or pass --candidate <hash...> to adopt an explicit subset; relationships require both endpoint resources.");
|
|
2894
|
+
});
|
|
2895
|
+
landscapeCmd
|
|
2896
|
+
.command("adopt")
|
|
2897
|
+
.description("Human-confirm a reviewed candidate set and write only those exact records through Hunch's normal graph boundary.")
|
|
2898
|
+
.option("--ref <ref>", "exact Git commit/ref that was reviewed", "HEAD")
|
|
2899
|
+
.requiredOption("--expected <hash>", "discovery hash printed by landscape review")
|
|
2900
|
+
.requiredOption("--reviewed-by <label>", "credential-free operator/reviewer label")
|
|
2901
|
+
.option("--all", "adopt every candidate in the reviewed discovery")
|
|
2902
|
+
.option("--candidate <hashes...>", "adopt only these candidate hashes")
|
|
2903
|
+
.option("--acknowledge-issues", "confirm that the printed discovery issues were reviewed")
|
|
2904
|
+
.action((opts) => {
|
|
2905
|
+
if (opts.all && opts.candidate?.length)
|
|
2906
|
+
return fail("choose either --all or --candidate, not both");
|
|
2907
|
+
if (!opts.all && !opts.candidate?.length)
|
|
2908
|
+
return fail("choose --all or name reviewed hashes with --candidate <hashes...>");
|
|
2909
|
+
const { store, root } = storeFor();
|
|
2910
|
+
try {
|
|
2911
|
+
const discovery = discoverRepositoryLandscape(root, opts.ref);
|
|
2912
|
+
const plan = planLandscapeAdoption({
|
|
2913
|
+
discovery,
|
|
2914
|
+
expectedDiscoveryHash: opts.expected,
|
|
2915
|
+
reviewer: opts.reviewedBy,
|
|
2916
|
+
candidateHashes: opts.all ? "all" : opts.candidate,
|
|
2917
|
+
acknowledgeIssues: opts.acknowledgeIssues,
|
|
2918
|
+
existingResources: store.recs("resources"),
|
|
2919
|
+
existingRelationships: store.recs("edges"),
|
|
2920
|
+
});
|
|
2921
|
+
for (const resource of plan.resourcesToWrite)
|
|
2922
|
+
store.putCapture("resources", resource);
|
|
2923
|
+
for (const relationship of plan.relationshipsToWrite)
|
|
2924
|
+
store.putCapture("edges", relationship);
|
|
2925
|
+
store.reindex();
|
|
2926
|
+
if (plan.resourcesToWrite.length || plan.relationshipsToWrite.length) {
|
|
2927
|
+
pumpMemoryHome(store, root, store.captureHome(false), "hunch: adopt reviewed Engineering Landscape candidates");
|
|
2928
|
+
}
|
|
2929
|
+
console.log(JSON.stringify(plan.receipt, null, 2));
|
|
2930
|
+
console.log(`✓ accepted ${plan.receipt.acceptedResourceIds.length} resource(s) and ${plan.receipt.acceptedRelationshipIds.length} relationship(s); wrote ${plan.resourcesToWrite.length + plan.relationshipsToWrite.length}, reused ${plan.receipt.reusedResourceIds.length + plan.receipt.reusedRelationshipIds.length}.`);
|
|
2931
|
+
}
|
|
2932
|
+
catch (error) {
|
|
2933
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
2934
|
+
}
|
|
2935
|
+
finally {
|
|
2936
|
+
store.close();
|
|
2937
|
+
}
|
|
2938
|
+
});
|
|
2856
2939
|
// ---- compare (rank N candidate solutions by architectural fit) ------------
|
|
2857
2940
|
program
|
|
2858
2941
|
.command("compare")
|
|
@@ -3720,7 +3803,13 @@ program
|
|
|
3720
3803
|
// used to come back empty while the graph held the answer one FTS query away —
|
|
3721
3804
|
// the task-shaped entry point must not whiff on task-shaped input. Fall back to
|
|
3722
3805
|
// search so the caller always leaves with the closest graph matches.
|
|
3723
|
-
const empty = !ctx.constraints.length &&
|
|
3806
|
+
const empty = !ctx.constraints.length &&
|
|
3807
|
+
!ctx.decisions.length &&
|
|
3808
|
+
!ctx.bugs.length &&
|
|
3809
|
+
!ctx.blast_radius.length &&
|
|
3810
|
+
!ctx.findings.length &&
|
|
3811
|
+
!ctx.landscape?.resources.length &&
|
|
3812
|
+
!ctx.landscape?.relationships.length;
|
|
3724
3813
|
if (empty && !asOf) {
|
|
3725
3814
|
const hits = store.search(target, 8);
|
|
3726
3815
|
if (hits.length) {
|
|
@@ -4325,7 +4414,15 @@ program
|
|
|
4325
4414
|
// from this file. No diff exists yet, so this is context — "don't re-add X" —
|
|
4326
4415
|
// not a block; the commit-time `hunch check` does the actual gating.
|
|
4327
4416
|
const retired = store.retiredForFile(target).filter((r) => r.symbols.length || r.deps.length);
|
|
4328
|
-
const hasContent = ctx.constraints.length ||
|
|
4417
|
+
const hasContent = ctx.constraints.length ||
|
|
4418
|
+
ctx.decisions.length ||
|
|
4419
|
+
ctx.bugs.length ||
|
|
4420
|
+
ctx.blast_radius.length ||
|
|
4421
|
+
ctx.findings.length ||
|
|
4422
|
+
ctx.landscape?.resources.length ||
|
|
4423
|
+
ctx.landscape?.relationships.length ||
|
|
4424
|
+
retired.length ||
|
|
4425
|
+
docGround;
|
|
4329
4426
|
if (!hasContent)
|
|
4330
4427
|
return; // no noise on files Hunch hasn't learned yet
|
|
4331
4428
|
const envelope = buildDeliveryEnvelope(ctx, {
|
package/dist/core/delivery.js
CHANGED
|
@@ -12,6 +12,8 @@ import { isAbsolute, join } from "node:path";
|
|
|
12
12
|
import { pathMatchesGlob, pathsRelated } from "./glob.js";
|
|
13
13
|
import { toPosixTarget } from "./paths.js";
|
|
14
14
|
import { renderGrounding } from "./topics.js";
|
|
15
|
+
import { LANDSCAPE_FRAGMENT_SCHEMA_VERSION, assertLandscapeDeliveryFragment, createLandscapeDeliveryFragment, landscapeFragmentHash, } from "./landscapeDelivery.js";
|
|
16
|
+
export const DELIVERY_ENVELOPE_SCHEMA_VERSION = "hunch.delivery-envelope/1";
|
|
15
17
|
const SEVERITY = { advisory: 1, warning: 2, blocking: 3, low: 1, medium: 2, high: 3, critical: 4 };
|
|
16
18
|
const MIN_ADVISORY_CONFIDENCE = 0.5;
|
|
17
19
|
const MIN_UNCONDITIONED_CONFIDENCE = 0.7;
|
|
@@ -361,6 +363,67 @@ function charCount(text) {
|
|
|
361
363
|
function estimatedTokens(text) {
|
|
362
364
|
return Math.max(1, Math.ceil(charCount(text) / 4));
|
|
363
365
|
}
|
|
366
|
+
function finalizeDeliveryEnvelope(unsigned) {
|
|
367
|
+
const digest = landscapeFragmentHash(unsigned);
|
|
368
|
+
const envelope = {
|
|
369
|
+
...unsigned,
|
|
370
|
+
receipt_id: `hdr_${digest.slice("sha256:".length, "sha256:".length + 24)}`,
|
|
371
|
+
};
|
|
372
|
+
assertDeliveryEnvelope(envelope);
|
|
373
|
+
return envelope;
|
|
374
|
+
}
|
|
375
|
+
/** Validate the public receipt without trusting a caller-supplied identity. */
|
|
376
|
+
export function assertDeliveryEnvelope(envelope) {
|
|
377
|
+
if (envelope.schema_version !== DELIVERY_ENVELOPE_SCHEMA_VERSION) {
|
|
378
|
+
throw new Error("delivery envelope schema is unsupported");
|
|
379
|
+
}
|
|
380
|
+
if (!/^hdr_[a-f0-9]{24}$/.test(envelope.receipt_id))
|
|
381
|
+
throw new Error("delivery envelope receipt id is invalid");
|
|
382
|
+
if (!Number.isSafeInteger(envelope.budget_tokens) || envelope.budget_tokens < 0
|
|
383
|
+
|| !Number.isSafeInteger(envelope.used_chars) || envelope.used_chars !== charCount(envelope.text)
|
|
384
|
+
|| !Number.isSafeInteger(envelope.accounted_chars) || envelope.accounted_chars < envelope.used_chars) {
|
|
385
|
+
throw new Error("delivery envelope budget accounting is invalid");
|
|
386
|
+
}
|
|
387
|
+
if (!envelope.blocking_overflow && envelope.accounted_chars > envelope.budget_tokens * 4) {
|
|
388
|
+
throw new Error("delivery envelope exceeds its hard caller budget");
|
|
389
|
+
}
|
|
390
|
+
if (envelope.landscape)
|
|
391
|
+
assertLandscapeDeliveryFragment(envelope.landscape);
|
|
392
|
+
const landscapeReceipts = new Map((envelope.landscape
|
|
393
|
+
? [...envelope.landscape.resources, ...envelope.landscape.relationships]
|
|
394
|
+
: []).map((item) => [`${item.record.schema === "hunch.resource/1" ? "resources" : "relationships"}:${item.record.id}`, item]));
|
|
395
|
+
const deliveredLandscape = envelope.delivered.filter((item) => item.kind === "resources" || item.kind === "relationships");
|
|
396
|
+
if (deliveredLandscape.length !== landscapeReceipts.size) {
|
|
397
|
+
throw new Error("delivery envelope landscape receipts do not match delivered records");
|
|
398
|
+
}
|
|
399
|
+
const deliveredLandscapeKeys = new Set();
|
|
400
|
+
for (const receipt of deliveredLandscape) {
|
|
401
|
+
const key = `${receipt.kind}:${receipt.record_id}`;
|
|
402
|
+
const nested = landscapeReceipts.get(key);
|
|
403
|
+
if (!nested || deliveredLandscapeKeys.has(key)
|
|
404
|
+
|| nested.rank !== receipt.rank || nested.tokenCost !== receipt.token_cost
|
|
405
|
+
|| nested.deliveryReason !== receipt.delivery_reason
|
|
406
|
+
|| nested.provenanceStatus !== receipt.provenance_status) {
|
|
407
|
+
throw new Error("delivery envelope landscape receipt is inconsistent");
|
|
408
|
+
}
|
|
409
|
+
deliveredLandscapeKeys.add(key);
|
|
410
|
+
}
|
|
411
|
+
if (deliveredLandscapeKeys.size !== landscapeReceipts.size) {
|
|
412
|
+
throw new Error("delivery envelope landscape receipts are not one-to-one");
|
|
413
|
+
}
|
|
414
|
+
const deliveredRecordChars = envelope.landscape
|
|
415
|
+
? [...envelope.landscape.resources, ...envelope.landscape.relationships]
|
|
416
|
+
.reduce((sum, item) => sum + charCount(JSON.stringify(item.record)), 0)
|
|
417
|
+
: 0;
|
|
418
|
+
if (envelope.accounted_chars < envelope.used_chars + deliveredRecordChars) {
|
|
419
|
+
throw new Error("delivery envelope undercounts its structured landscape records");
|
|
420
|
+
}
|
|
421
|
+
const { receipt_id: _receiptId, ...unsigned } = envelope;
|
|
422
|
+
const expected = landscapeFragmentHash(unsigned);
|
|
423
|
+
if (envelope.receipt_id !== `hdr_${expected.slice("sha256:".length, "sha256:".length + 24)}`) {
|
|
424
|
+
throw new Error("delivery envelope receipt does not match its content");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
364
427
|
/** Build the one envelope used by CLI, MCP, and the edit hook. */
|
|
365
428
|
export function buildDeliveryEnvelope(ctx, options = {}) {
|
|
366
429
|
const budget = Number.isFinite(ctx.budget_tokens) ? Math.max(0, Math.floor(ctx.budget_tokens)) : 1500;
|
|
@@ -438,19 +501,73 @@ export function buildDeliveryEnvelope(ctx, options = {}) {
|
|
|
438
501
|
line: `${finding.id} | finding/${finding.triage}/${finding.severity} | ${clipHeadline(`${finding.title} — ${finding.observation}${evidence}`, 240)} | ${sourceTier(finding.provenance.source)}/${validation.state} | hunch_why("${finding.id}")`,
|
|
439
502
|
});
|
|
440
503
|
}
|
|
504
|
+
for (const item of ctx.landscape?.resources ?? []) {
|
|
505
|
+
const record = item.record;
|
|
506
|
+
const reviewId = String(record.metadata.landscape_review_id);
|
|
507
|
+
const revision = record.currentness.source_revision ?? "unknown";
|
|
508
|
+
const line = `${record.id} | resource/${record.kind}/${record.lifecycle} | ${clipHeadline(record.name, 160)} | ${item.selectionReason} | current@${revision} | review ${reviewId}`;
|
|
509
|
+
candidates.push({
|
|
510
|
+
ref: { kind: "resources", record_id: record.id },
|
|
511
|
+
mandatory: false,
|
|
512
|
+
score: 550 - item.selectionRank,
|
|
513
|
+
provenance: "current",
|
|
514
|
+
line,
|
|
515
|
+
landscapeResource: item,
|
|
516
|
+
// The structured record is part of what an MCP caller receives. Charge
|
|
517
|
+
// it conservatively instead of pretending only the duplicate headline
|
|
518
|
+
// consumes the caller's context budget.
|
|
519
|
+
accountedChars: charCount(line) + charCount(JSON.stringify(record)) + 240,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
for (const item of ctx.landscape?.relationships ?? []) {
|
|
523
|
+
const record = item.record;
|
|
524
|
+
const reviewId = String(record.metadata.landscape_review_id);
|
|
525
|
+
const revision = record.currentness?.source_revision ?? "unknown";
|
|
526
|
+
const line = `${record.id} | relationship/${record.type} | ${record.from} -> ${record.to} | graph-connection | current@${revision} | review ${reviewId}`;
|
|
527
|
+
candidates.push({
|
|
528
|
+
ref: { kind: "relationships", record_id: record.id },
|
|
529
|
+
mandatory: false,
|
|
530
|
+
score: 525 - item.selectionRank,
|
|
531
|
+
provenance: "current",
|
|
532
|
+
line,
|
|
533
|
+
landscapeRelationship: item,
|
|
534
|
+
accountedChars: charCount(line) + charCount(JSON.stringify(record)) + 240,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
441
537
|
for (const dependent of ctx.blast_radius) {
|
|
442
538
|
candidates.push({ mandatory: false, score: 400 - dependent.depth, provenance: "current", line: `graph | blast/d${dependent.depth} | ${clipHeadline(dependent.via, 220)}` });
|
|
443
539
|
}
|
|
444
540
|
if (ctx.components.length) {
|
|
445
541
|
candidates.push({ mandatory: false, score: 300, provenance: "current", line: `graph | components | ${clipHeadline(ctx.components.map((component) => component.name).join(", "), 240)}` });
|
|
446
542
|
}
|
|
447
|
-
const hasAnything = candidates.length > 0
|
|
543
|
+
const hasAnything = candidates.length > 0
|
|
544
|
+
|| (ctx.landscape?.omitted.length ?? 0) > 0
|
|
545
|
+
|| (options.supplements?.length ?? 0) > 0;
|
|
448
546
|
if (!hasAnything) {
|
|
449
547
|
const empty = `# Hunch context for "${ctx.target}"\n\n(No recorded constraints/decisions/bugs for this target yet — Hunch is still learning it.)\n`;
|
|
450
548
|
const text = fitText(empty, cap);
|
|
451
|
-
return {
|
|
549
|
+
return finalizeDeliveryEnvelope({
|
|
550
|
+
schema_version: DELIVERY_ENVELOPE_SCHEMA_VERSION,
|
|
551
|
+
text,
|
|
552
|
+
delivered: [],
|
|
553
|
+
hypotheses: [],
|
|
554
|
+
obligations: [],
|
|
555
|
+
supplements: [],
|
|
556
|
+
omitted: [],
|
|
557
|
+
landscape: null,
|
|
558
|
+
budget_tokens: budget,
|
|
559
|
+
used_chars: charCount(text),
|
|
560
|
+
accounted_chars: charCount(text),
|
|
561
|
+
blocking_overflow: false,
|
|
562
|
+
abstention: emptyAbstention(),
|
|
563
|
+
});
|
|
452
564
|
}
|
|
453
|
-
const omitted = []
|
|
565
|
+
const omitted = (ctx.landscape?.omitted ?? []).map((item) => ({
|
|
566
|
+
kind: item.kind,
|
|
567
|
+
record_id: item.recordId,
|
|
568
|
+
reason: item.reason,
|
|
569
|
+
detail: item.detail,
|
|
570
|
+
}));
|
|
454
571
|
const eligible = [];
|
|
455
572
|
for (const candidate of candidates) {
|
|
456
573
|
if (candidate.retiredDetail && candidate.ref) {
|
|
@@ -515,28 +632,48 @@ export function buildDeliveryEnvelope(ctx, options = {}) {
|
|
|
515
632
|
if (query.taskPhrase) {
|
|
516
633
|
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
634
|
}
|
|
518
|
-
|
|
635
|
+
if ((ctx.landscape?.resources.length ?? 0) + (ctx.landscape?.relationships.length ?? 0) > 0) {
|
|
636
|
+
lines.push(`Landscape: only current human-reviewed ${LANDSCAPE_FRAGMENT_SCHEMA_VERSION} records may share this envelope and budget.`);
|
|
637
|
+
}
|
|
638
|
+
let text = fitText(`${lines.join("\n")}\n`, cap);
|
|
639
|
+
let accountedChars = charCount(text);
|
|
519
640
|
const delivered = [];
|
|
520
641
|
const hypotheses = [];
|
|
521
642
|
const obligations = [];
|
|
522
643
|
const supplements = [];
|
|
523
644
|
let blockingOverflow = false;
|
|
645
|
+
const deliveredLandscapeResourceIds = new Set();
|
|
524
646
|
for (const [index, candidate] of recordCandidates.entries()) {
|
|
525
647
|
const next = `- ${candidate.line}\n`;
|
|
526
|
-
if (
|
|
648
|
+
if (candidate.landscapeRelationship) {
|
|
649
|
+
const relationship = candidate.landscapeRelationship.record;
|
|
650
|
+
if (!deliveredLandscapeResourceIds.has(relationship.from) || !deliveredLandscapeResourceIds.has(relationship.to)) {
|
|
651
|
+
omitted.push({
|
|
652
|
+
...candidate.ref,
|
|
653
|
+
reason: "endpoint-not-delivered",
|
|
654
|
+
detail: "reviewed relationship was withheld because both endpoint resources were not delivered in this budget",
|
|
655
|
+
});
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const chargedChars = candidate.accountedChars ?? charCount(next);
|
|
660
|
+
if (accountedChars + chargedChars <= cap || candidate.mandatory) {
|
|
527
661
|
text += next;
|
|
662
|
+
accountedChars += chargedChars;
|
|
528
663
|
delivered.push({
|
|
529
664
|
...candidate.ref,
|
|
530
665
|
rank: index + 1,
|
|
531
666
|
delivery_reason: candidate.mandatory ? "blocking-reserved" : "ranked",
|
|
532
667
|
provenance_status: candidate.provenance,
|
|
533
|
-
token_cost:
|
|
668
|
+
token_cost: Math.max(1, Math.ceil(chargedChars / 4)),
|
|
534
669
|
});
|
|
670
|
+
if (candidate.landscapeResource)
|
|
671
|
+
deliveredLandscapeResourceIds.add(candidate.landscapeResource.record.id);
|
|
535
672
|
if (candidate.hypothesis) {
|
|
536
673
|
hypotheses.push({ ...candidate.hypothesis, rank: index + 1 });
|
|
537
674
|
obligations.push(...candidate.hypothesis.obligations);
|
|
538
675
|
}
|
|
539
|
-
if (
|
|
676
|
+
if (accountedChars > cap && candidate.mandatory)
|
|
540
677
|
blockingOverflow = true;
|
|
541
678
|
}
|
|
542
679
|
else if (candidate.ref) {
|
|
@@ -564,8 +701,9 @@ export function buildDeliveryEnvelope(ctx, options = {}) {
|
|
|
564
701
|
if (abstainedMemory && delivered.length === 0 && supplement.kind.startsWith("search-")) {
|
|
565
702
|
supplements.push({ id: supplement.id, kind: supplement.kind, delivered: false, reason: "abstained", rank: index + 1, token_cost: tokenCost });
|
|
566
703
|
}
|
|
567
|
-
else if (
|
|
704
|
+
else if (accountedChars + charCount(next) <= cap) {
|
|
568
705
|
text += next;
|
|
706
|
+
accountedChars += charCount(next);
|
|
569
707
|
supplements.push({ id: supplement.id, kind: supplement.kind, delivered: true, reason: "supplemental", rank: index + 1, token_cost: tokenCost });
|
|
570
708
|
}
|
|
571
709
|
else {
|
|
@@ -574,8 +712,10 @@ export function buildDeliveryEnvelope(ctx, options = {}) {
|
|
|
574
712
|
}
|
|
575
713
|
for (const candidate of structuralCandidates) {
|
|
576
714
|
const next = `- ${candidate.line}\n`;
|
|
577
|
-
if (
|
|
715
|
+
if (accountedChars + charCount(next) <= cap) {
|
|
578
716
|
text += next;
|
|
717
|
+
accountedChars += charCount(next);
|
|
718
|
+
}
|
|
579
719
|
}
|
|
580
720
|
const staleCount = omitted.filter((item) => item.reason === "stale-provenance" || item.reason === "retired").length;
|
|
581
721
|
const budgetCount = omitted.filter((item) => item.reason === "budget").length;
|
|
@@ -599,25 +739,85 @@ export function buildDeliveryEnvelope(ctx, options = {}) {
|
|
|
599
739
|
].filter(Boolean);
|
|
600
740
|
if (notes.length) {
|
|
601
741
|
const footer = `… ${notes.join(" ")}\n`;
|
|
602
|
-
if (
|
|
742
|
+
if (accountedChars + charCount(footer) <= cap) {
|
|
603
743
|
text += footer;
|
|
744
|
+
accountedChars += charCount(footer);
|
|
745
|
+
}
|
|
604
746
|
}
|
|
605
|
-
if (!text.endsWith("\n") &&
|
|
747
|
+
if (!text.endsWith("\n") && accountedChars < cap) {
|
|
606
748
|
text += "\n";
|
|
749
|
+
accountedChars += 1;
|
|
750
|
+
}
|
|
607
751
|
if (!blockingOverflow)
|
|
608
752
|
text = fitText(text, cap);
|
|
609
753
|
omitted.sort((left, right) => left.record_id.localeCompare(right.record_id) || left.reason.localeCompare(right.reason));
|
|
610
|
-
|
|
754
|
+
const deliveredById = new Map(delivered.map((item) => [`${item.kind}:${item.record_id}`, item]));
|
|
755
|
+
const landscapeResources = (ctx.landscape?.resources ?? [])
|
|
756
|
+
.flatMap((selection) => {
|
|
757
|
+
const receipt = deliveredById.get(`resources:${selection.record.id}`);
|
|
758
|
+
if (!receipt)
|
|
759
|
+
return [];
|
|
760
|
+
return [{
|
|
761
|
+
...selection,
|
|
762
|
+
rank: receipt.rank,
|
|
763
|
+
deliveryReason: "ranked",
|
|
764
|
+
required: false,
|
|
765
|
+
blocking: false,
|
|
766
|
+
provenanceStatus: "current",
|
|
767
|
+
tokenCost: receipt.token_cost,
|
|
768
|
+
}];
|
|
769
|
+
});
|
|
770
|
+
const landscapeRelationships = (ctx.landscape?.relationships ?? [])
|
|
771
|
+
.flatMap((selection) => {
|
|
772
|
+
const receipt = deliveredById.get(`relationships:${selection.record.id}`);
|
|
773
|
+
if (!receipt)
|
|
774
|
+
return [];
|
|
775
|
+
return [{
|
|
776
|
+
...selection,
|
|
777
|
+
rank: receipt.rank,
|
|
778
|
+
deliveryReason: "ranked",
|
|
779
|
+
required: false,
|
|
780
|
+
blocking: false,
|
|
781
|
+
provenanceStatus: "current",
|
|
782
|
+
tokenCost: receipt.token_cost,
|
|
783
|
+
}];
|
|
784
|
+
});
|
|
785
|
+
const landscapeOmissions = omitted
|
|
786
|
+
.filter((item) => item.kind === "resources" || item.kind === "relationships")
|
|
787
|
+
.flatMap((item) => {
|
|
788
|
+
if (!["budget", "stale-provenance", "endpoint-not-delivered", "landscape-cap"].includes(item.reason))
|
|
789
|
+
return [];
|
|
790
|
+
return [{
|
|
791
|
+
kind: item.kind,
|
|
792
|
+
recordId: item.record_id,
|
|
793
|
+
reason: item.reason,
|
|
794
|
+
detail: item.detail,
|
|
795
|
+
}];
|
|
796
|
+
});
|
|
797
|
+
const landscape = ctx.landscape && (landscapeResources.length > 0
|
|
798
|
+
|| landscapeRelationships.length > 0
|
|
799
|
+
|| landscapeOmissions.length > 0)
|
|
800
|
+
? createLandscapeDeliveryFragment({
|
|
801
|
+
selection: ctx.landscape,
|
|
802
|
+
resources: landscapeResources,
|
|
803
|
+
relationships: landscapeRelationships,
|
|
804
|
+
omitted: landscapeOmissions,
|
|
805
|
+
})
|
|
806
|
+
: null;
|
|
807
|
+
return finalizeDeliveryEnvelope({
|
|
808
|
+
schema_version: DELIVERY_ENVELOPE_SCHEMA_VERSION,
|
|
611
809
|
text,
|
|
612
810
|
delivered,
|
|
613
811
|
hypotheses,
|
|
614
812
|
obligations,
|
|
615
813
|
supplements,
|
|
616
814
|
omitted,
|
|
815
|
+
landscape,
|
|
617
816
|
budget_tokens: budget,
|
|
618
817
|
used_chars: charCount(text),
|
|
818
|
+
accounted_chars: accountedChars,
|
|
619
819
|
blocking_overflow: blockingOverflow,
|
|
620
820
|
abstention,
|
|
621
|
-
};
|
|
821
|
+
});
|
|
622
822
|
}
|
|
623
823
|
//# sourceMappingURL=delivery.js.map
|