@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 CHANGED
@@ -1,49 +1,33 @@
1
1
  # Hunch
2
2
 
3
- ## Your repo remembers why.
3
+ ## Give your AI coding assistant the missing story behind your code.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@davesheffer/hunch?color=2742ff&label=npm)](https://www.npmjs.com/package/@davesheffer/hunch)
6
6
  [![GitHub stars](https://img.shields.io/github/stars/davesheffer/hunch?color=2742ff&label=%E2%98%85%20star)](https://github.com/davesheffer/hunch)
7
7
  [![license](https://img.shields.io/npm/l/@davesheffer/hunch?color=2742ff)](LICENSE)
8
8
 
9
- For the precise rules your team explicitly trusts and proves, Hunch gives a concrete guarantee:
10
- **your agents do not re-make a decided decision or re-introduce a fixed bug without a deterministic
11
- violation being surfaced.** The mechanism is an engineering-memory and architectural-conformance
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
- Memory starts **advisory**. Nothing blocks until you explicitly trust a precise rule and choose
16
- strict enforcement.
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
- **Memory is the input. The product boundary is the receipt:** relevant evidence before an edit,
19
- then a deterministic check of the change against the rules your team has explicitly trusted.
18
+ ### A simple example
20
19
 
21
- > **New in v1.19.0:** correction search now keeps the flat top five but adds
22
- > transfer-tested semantic declaration families and an efficiency-tested progressive queue.
23
- > Evidence receipts are read-only annotations: they never reorder candidates or claim an exact
24
- > owner. Install the stable release with `npm i -g @davesheffer/hunch`.
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
- The measured improvement is deliberately scoped to fresh, preregistered transfer cases:
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
- | Measure | Flat/baseline | v1.19 bounded view | Change |
29
- | --- | ---: | ---: | ---: |
30
- | Changed declaration found | 3/12 (25%) | 6/12 (50%) | +25 percentage points |
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
- `hunch init` indexes the repo, installs the local memory hooks, and connects supported assistants
64
- to the same graph. It merges into existing configuration instead of replacing it.
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
- - **Durable context** — decisions and corrections survive the chat session that produced them.
69
- - **One memory for every assistant** — Claude Code, Cursor, Copilot, Windsurf, Antigravity, Codex,
70
- and any MCP client see the same evidence.
71
- - **Change receipts** review a working tree, commit, or branch against recorded intent and get a
72
- cited PASS / WARN / BLOCK result.
73
- - **Bug lineage** understand which old incident a line fixed before accidentally undoing it.
74
- - **Code awareness** — TypeScript, JavaScript, Python, Go, YAML, and chart-scoped Helm templates
75
- feed dependency, blast-radius, and redundancy checks. The reasoning layer works with any language.
76
- - **ADR interop** — `hunch import-adr` populates the graph from an existing MADR/Nygard corpus;
77
- `hunch export-adr` projects it back as standard MADR any ADR reader understands, and the
78
- projection then tracks the graph automatically and reports its own drift.
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
- ## Day-to-day
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>` | Decisions, bugs, constraints, and blast radius behind a 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` | Review all current changes against recorded intent |
92
- | `hunch shortlist --issue "..."` | Experimental flat shortlist plus a progressive, file-anchored semantic inspection plan; add `--evidence receipt.json` for read-only evidence annotations |
93
- | `hunch evidence-map receipt.json` | Compile observed probe/execution receipts without guessing an exact owner |
94
- | `hunch log` | See the memory timeline and its reversible moves |
95
- | `hunch escalations` | See the rare decisions only a human can make |
96
- | `hunch doctor` | Diagnose setup, provider, index, or private-overlay problems |
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 one living team memory
162
+ ## Share the same memory with your team
145
163
 
146
- Matrix mode keeps the team's decisions, corrections, constraints, and proofs in one dedicated Git
147
- repository, separate from the code repository. Hunch does not host that repository: create a private
148
- Git repo that every teammate can access, install the Matrix release on team machines and CI, then
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 && !ctx.decisions.length && !ctx.bugs.length && !ctx.blast_radius.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 || ctx.decisions.length || ctx.bugs.length || ctx.blast_radius.length || ctx.findings.length || retired.length || docGround;
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, {
@@ -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 || (options.supplements?.length ?? 0) > 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 { text, delivered: [], hypotheses: [], obligations: [], supplements: [], omitted: [], budget_tokens: budget, used_chars: charCount(text), blocking_overflow: false, abstention: emptyAbstention() };
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
- let text = `${lines.join("\n")}\n`;
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 (charCount(text) + charCount(next) <= cap || candidate.mandatory) {
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: estimatedTokens(next),
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 (charCount(text) > cap && candidate.mandatory)
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 (charCount(text) + charCount(next) <= cap) {
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 (charCount(text) + charCount(next) <= cap)
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 (charCount(text) + charCount(footer) <= cap)
742
+ if (accountedChars + charCount(footer) <= cap) {
603
743
  text += footer;
744
+ accountedChars += charCount(footer);
745
+ }
604
746
  }
605
- if (!text.endsWith("\n") && charCount(text) < cap)
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
- return {
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