@davesheffer/hunch 1.31.0 → 1.32.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.
Files changed (228) hide show
  1. package/README.md +3 -6
  2. package/dist/cli/automaticReviewMemory.d.ts +13 -0
  3. package/dist/cli/dna.d.ts +2 -0
  4. package/dist/cli/index.d.ts +16 -0
  5. package/dist/cli/index.js +602 -43
  6. package/dist/cli/integrations.d.ts +2 -0
  7. package/dist/cli/integrations.js +5 -2
  8. package/dist/cli/invocation.d.ts +35 -0
  9. package/dist/cli/preflight.d.ts +1 -0
  10. package/dist/cli/reviewMemory.d.ts +4 -0
  11. package/dist/cli/reviewMemoryProvider.d.ts +14 -0
  12. package/dist/cli/serve.d.ts +2 -0
  13. package/dist/cli/taskReport.d.ts +6 -0
  14. package/dist/cli/taskReport.js +124 -0
  15. package/dist/cli/update.d.ts +14 -0
  16. package/dist/client/state.d.ts +248 -0
  17. package/dist/client/state.js +2 -0
  18. package/dist/constitution/adapters.d.ts +29 -0
  19. package/dist/constitution/behaviorAttestationBinding.d.ts +3 -0
  20. package/dist/constitution/behaviorEvaluator.d.ts +8 -0
  21. package/dist/constitution/behaviorProof.d.ts +9 -0
  22. package/dist/constitution/behaviorWorkspace.d.ts +25 -0
  23. package/dist/constitution/bootstrap.d.ts +30 -0
  24. package/dist/constitution/canonical.d.ts +15 -0
  25. package/dist/constitution/card.d.ts +58 -0
  26. package/dist/constitution/compiler.d.ts +52 -0
  27. package/dist/constitution/composition.d.ts +10 -0
  28. package/dist/constitution/corpus.d.ts +7 -0
  29. package/dist/constitution/correctionPolicyMaterializer.d.ts +57 -0
  30. package/dist/constitution/delta.d.ts +4 -0
  31. package/dist/constitution/disposition.d.ts +20 -0
  32. package/dist/constitution/evaluator.d.ts +47 -0
  33. package/dist/constitution/experiment.d.ts +466 -0
  34. package/dist/constitution/experimentRunner.d.ts +7 -0
  35. package/dist/constitution/g2.d.ts +209 -0
  36. package/dist/constitution/g2BehaviorAttestation.d.ts +45 -0
  37. package/dist/constitution/g2BehaviorCandidates.d.ts +123 -0
  38. package/dist/constitution/g2BehaviorDependencies.d.ts +83 -0
  39. package/dist/constitution/g2BehaviorMaterialization.d.ts +77 -0
  40. package/dist/constitution/g2BehaviorPolicyMaterializer.d.ts +48 -0
  41. package/dist/constitution/g2CandidateAttestation.d.ts +43 -0
  42. package/dist/constitution/g2Candidates.d.ts +80 -0
  43. package/dist/constitution/g2Drills.d.ts +33 -0
  44. package/dist/constitution/g3.d.ts +311 -0
  45. package/dist/constitution/g3Conformance.d.ts +33 -0
  46. package/dist/constitution/lifecycle.d.ts +25 -0
  47. package/dist/constitution/mutation.d.ts +15 -0
  48. package/dist/constitution/nodeTestEvidence.d.ts +13 -0
  49. package/dist/constitution/plan.d.ts +18 -0
  50. package/dist/constitution/policyRuntime.d.ts +5 -0
  51. package/dist/constitution/proof.d.ts +11 -0
  52. package/dist/constitution/repairPolicies.d.ts +34 -0
  53. package/dist/constitution/replacementFreeGit.d.ts +14 -0
  54. package/dist/constitution/replay.d.ts +40 -0
  55. package/dist/constitution/replayCache.d.ts +14 -0
  56. package/dist/constitution/replayWorker.d.ts +1 -0
  57. package/dist/constitution/repository.d.ts +129 -0
  58. package/dist/constitution/safeCheckout.d.ts +9 -0
  59. package/dist/constitution/schema.d.ts +1763 -0
  60. package/dist/constitution/scorecard.d.ts +219 -0
  61. package/dist/constitution/service.d.ts +398 -0
  62. package/dist/constitution/shadow.d.ts +54 -0
  63. package/dist/constitution/sourceMutation.d.ts +15 -0
  64. package/dist/constitution/staticGraphBaseline.d.ts +13 -0
  65. package/dist/constitution/structural.d.ts +64 -0
  66. package/dist/core/agenthook.d.ts +63 -0
  67. package/dist/core/agenthook.js +1 -0
  68. package/dist/core/automaticReviewMemory.d.ts +60 -0
  69. package/dist/core/autoreview.d.ts +66 -0
  70. package/dist/core/canonicalOrder.d.ts +3 -0
  71. package/dist/core/capturetoken.d.ts +19 -0
  72. package/dist/core/changeIdentity.d.ts +22 -0
  73. package/dist/core/changeProof.d.ts +14 -0
  74. package/dist/core/checkreport.d.ts +140 -0
  75. package/dist/core/commitrepair.d.ts +171 -0
  76. package/dist/core/commitrepair.js +238 -0
  77. package/dist/core/compare.d.ts +21 -0
  78. package/dist/core/config.d.ts +20 -0
  79. package/dist/core/conformance.d.ts +35 -0
  80. package/dist/core/constraintmatch.d.ts +54 -0
  81. package/dist/core/correction.d.ts +42 -0
  82. package/dist/core/correctionStage.d.ts +224 -0
  83. package/dist/core/declarationClusters.d.ts +149 -0
  84. package/dist/core/delivery.d.ts +110 -0
  85. package/dist/core/docanchors.d.ts +32 -0
  86. package/dist/core/docscan.d.ts +26 -0
  87. package/dist/core/drift.d.ts +13 -0
  88. package/dist/core/drift.js +31 -1
  89. package/dist/core/dupdetect.d.ts +43 -0
  90. package/dist/core/escalations.d.ts +129 -0
  91. package/dist/core/escalations.js +165 -0
  92. package/dist/core/events.d.ts +29 -0
  93. package/dist/core/evidenceMap.d.ts +164 -0
  94. package/dist/core/externalImports.d.ts +8 -0
  95. package/dist/core/format.d.ts +13 -0
  96. package/dist/core/glob.d.ts +9 -0
  97. package/dist/core/groundingLag.d.ts +84 -0
  98. package/dist/core/groundingLag.js +16 -2
  99. package/dist/core/groundingMerge.d.ts +15 -0
  100. package/dist/core/groundingMerge.js +95 -0
  101. package/dist/core/hookcache.d.ts +10 -0
  102. package/dist/core/hookpolicy.d.ts +47 -0
  103. package/dist/core/ids.d.ts +36 -0
  104. package/dist/core/importReview.d.ts +30 -0
  105. package/dist/core/io.d.ts +23 -0
  106. package/dist/core/jsonc.d.ts +4 -0
  107. package/dist/core/landscapeAdoption.d.ts +66 -0
  108. package/dist/core/landscapeDelivery.d.ts +76 -0
  109. package/dist/core/memorylog.d.ts +37 -0
  110. package/dist/core/migrate.d.ts +28 -0
  111. package/dist/core/outcomeExperience.d.ts +89 -0
  112. package/dist/core/overlaySafety.d.ts +24 -0
  113. package/dist/core/overlaySafety.js +7 -1
  114. package/dist/core/paths.d.ts +31 -0
  115. package/dist/core/pipeline.d.ts +289 -0
  116. package/dist/core/premises.d.ts +43 -0
  117. package/dist/core/provenance.d.ts +21 -0
  118. package/dist/core/publication.d.ts +54 -0
  119. package/dist/core/refrepair.d.ts +24 -0
  120. package/dist/core/refrepair.js +1 -1
  121. package/dist/core/relativeImports.d.ts +11 -0
  122. package/dist/core/repair.d.ts +44 -0
  123. package/dist/core/repairqueue.d.ts +40 -0
  124. package/dist/core/repairqueue.js +133 -0
  125. package/dist/core/reviewMemory.d.ts +40 -0
  126. package/dist/core/reviewqueue.d.ts +54 -0
  127. package/dist/core/safeRepoFile.d.ts +20 -0
  128. package/dist/core/served.d.ts +57 -0
  129. package/dist/core/served.js +14 -0
  130. package/dist/core/stateContract.d.ts +979 -0
  131. package/dist/core/stateContract.js +87 -4
  132. package/dist/core/stateDelivery.d.ts +78 -0
  133. package/dist/core/stateRecords.d.ts +383 -0
  134. package/dist/core/stateRecords.js +16 -1
  135. package/dist/core/stats.d.ts +88 -0
  136. package/dist/core/strictgate.d.ts +33 -0
  137. package/dist/core/taskReport.d.ts +226 -0
  138. package/dist/core/taskReport.js +514 -0
  139. package/dist/core/taskReportCapture.d.ts +8 -0
  140. package/dist/core/taskReportCapture.js +36 -0
  141. package/dist/core/taskReportEvidence.d.ts +26 -0
  142. package/dist/core/taskReportEvidence.js +268 -0
  143. package/dist/core/taskReportHook.d.ts +20 -0
  144. package/dist/core/taskReportHook.js +76 -0
  145. package/dist/core/taskReportPaths.d.ts +3 -0
  146. package/dist/core/taskReportPaths.js +23 -0
  147. package/dist/core/taskReportPublic.d.ts +31 -0
  148. package/dist/core/taskReportPublic.js +45 -0
  149. package/dist/core/taskReportRender.d.ts +6 -0
  150. package/dist/core/taskReportRender.js +97 -0
  151. package/dist/core/topics.d.ts +73 -0
  152. package/dist/core/types.d.ts +1239 -0
  153. package/dist/core/version.d.ts +10 -0
  154. package/dist/eval/guards.d.ts +45 -0
  155. package/dist/eval/harness.d.ts +58 -0
  156. package/dist/extractors/adrImport.d.ts +59 -0
  157. package/dist/extractors/comments.d.ts +7 -0
  158. package/dist/extractors/correctionSources.d.ts +11 -0
  159. package/dist/extractors/diff.d.ts +40 -0
  160. package/dist/extractors/git.d.ts +304 -0
  161. package/dist/extractors/git.js +208 -20
  162. package/dist/extractors/helm.d.ts +31 -0
  163. package/dist/extractors/indexer.d.ts +55 -0
  164. package/dist/extractors/landscapeDiscovery.d.ts +41 -0
  165. package/dist/extractors/languages.d.ts +77 -0
  166. package/dist/extractors/nativeTreeSitter.d.ts +16 -0
  167. package/dist/extractors/parse.d.ts +54 -0
  168. package/dist/extractors/php.d.ts +26 -0
  169. package/dist/extractors/repoSource.d.ts +53 -0
  170. package/dist/extractors/testreport.d.ts +36 -0
  171. package/dist/integrations/ciAction.d.ts +8 -0
  172. package/dist/integrations/claudeConfig.d.ts +44 -0
  173. package/dist/integrations/claudemd.d.ts +13 -0
  174. package/dist/integrations/claudemd.js +11 -2
  175. package/dist/integrations/gitignore.d.ts +16 -0
  176. package/dist/integrations/gitignore.js +7 -0
  177. package/dist/integrations/health.d.ts +74 -0
  178. package/dist/integrations/hooks.d.ts +41 -0
  179. package/dist/integrations/hooks.js +92 -66
  180. package/dist/integrations/madrExport.d.ts +39 -0
  181. package/dist/integrations/madrManifest.d.ts +69 -0
  182. package/dist/integrations/mergeDriver.d.ts +3 -0
  183. package/dist/integrations/mergeDriver.js +25 -8
  184. package/dist/integrations/probe.d.ts +4 -0
  185. package/dist/integrations/providers.d.ts +103 -0
  186. package/dist/integrations/scaffold.d.ts +29 -0
  187. package/dist/integrations/sync.d.ts +30 -0
  188. package/dist/integrations/sync.js +3 -2
  189. package/dist/integrations/team.d.ts +70 -0
  190. package/dist/integrations/worktree.d.ts +8 -0
  191. package/dist/mcp/roots.d.ts +16 -0
  192. package/dist/mcp/server.d.ts +36 -0
  193. package/dist/mcp/server.js +114 -25
  194. package/dist/mcp/taskReportTools.d.ts +3 -0
  195. package/dist/mcp/taskReportTools.js +106 -0
  196. package/dist/serve/app.d.ts +36 -0
  197. package/dist/serve/app.js +20 -0
  198. package/dist/serve/config.d.ts +100 -0
  199. package/dist/serve/writelock.d.ts +22 -0
  200. package/dist/store/changeLedger.d.ts +132 -0
  201. package/dist/store/changeLedger.js +40 -9
  202. package/dist/store/compact.d.ts +33 -0
  203. package/dist/store/db.d.ts +9 -0
  204. package/dist/store/embedder.d.ts +32 -0
  205. package/dist/store/hunchStore.d.ts +624 -0
  206. package/dist/store/hunchStore.js +13 -5
  207. package/dist/store/jsonStore.d.ts +125 -0
  208. package/dist/store/jsonStore.js +15 -0
  209. package/dist/store/merge.d.ts +39 -0
  210. package/dist/store/privateMigrate.d.ts +24 -0
  211. package/dist/store/replay.d.ts +56 -0
  212. package/dist/store/schema.d.ts +17 -0
  213. package/dist/store/stateBinding.d.ts +145 -0
  214. package/dist/store/stateBinding.js +110 -15
  215. package/dist/store/stateCapture.d.ts +15 -0
  216. package/dist/store/stateCapture.js +145 -0
  217. package/dist/synthesis/cliAdapter.d.ts +22 -0
  218. package/dist/synthesis/initiator.d.ts +13 -0
  219. package/dist/synthesis/provider.d.ts +307 -0
  220. package/dist/synthesis/synthesize.d.ts +117 -0
  221. package/dist/synthesis/tripwires.d.ts +9 -0
  222. package/dist/taskReports.d.ts +76 -0
  223. package/dist/taskReports.js +67 -0
  224. package/dist/wiki/adopt.d.ts +22 -0
  225. package/dist/wiki/graph.d.ts +87 -0
  226. package/dist/wiki/wiki.d.ts +242 -0
  227. package/package.json +6 -5
  228. package/server.json +2 -2
@@ -0,0 +1,624 @@
1
+ import { type HunchPaths } from "../core/paths.js";
2
+ import { type Component, type Constraint, type Bug, type Decision, type Symbol, type Edge, type Finding, type EntityKind, type EntityFor } from "../core/types.js";
3
+ import { type DB } from "./db.js";
4
+ import { type Embedder } from "./embedder.js";
5
+ import { JsonStore } from "./jsonStore.js";
6
+ import { type VetoTier } from "../core/strictgate.js";
7
+ import { type DiffAnalysis } from "../extractors/diff.js";
8
+ import type { CheckReport, CausalWhy, ImpactReport } from "../core/checkreport.js";
9
+ import { type ReviewedLandscapeSelection } from "../core/landscapeDelivery.js";
10
+ import { type StateSlice } from "../core/stateDelivery.js";
11
+ export interface SearchHit {
12
+ ref: string;
13
+ kind: string;
14
+ title: string;
15
+ snippet: string;
16
+ score: number;
17
+ }
18
+ export interface HybridSearchOpts {
19
+ embedder?: Embedder | null;
20
+ graphWeight?: number;
21
+ graphDepth?: number;
22
+ graphNodeCap?: number;
23
+ graphTokenCap?: number;
24
+ /** Repository whose HEAD/file history proves decision-anchor freshness. Defaults to this store's root. */
25
+ freshnessRoot?: string;
26
+ }
27
+ export interface RankedSearchOpts {
28
+ /** Repository whose HEAD/file history proves decision-anchor freshness. Defaults to this store's root. */
29
+ freshnessRoot?: string;
30
+ }
31
+ export type OverlayResolutionSource = "environment" | "local-config" | null;
32
+ export interface OverlayOverride {
33
+ /** Repo/worktree-local target that would have won without the environment override. */
34
+ configuredDir: string;
35
+ /** Process-global target selected by HUNCH_PRIVATE_DIR. */
36
+ environmentDir: string;
37
+ }
38
+ export interface WhyResult {
39
+ target: string;
40
+ decisions: Decision[];
41
+ bugs: Bug[];
42
+ constraints: Constraint[];
43
+ symbols: Symbol[];
44
+ components: Component[];
45
+ }
46
+ export interface FragileNode {
47
+ id: string;
48
+ file: string;
49
+ name: string;
50
+ score: number;
51
+ churn_90d: number;
52
+ bug_count: number;
53
+ fan_in: number;
54
+ evidence: string[];
55
+ }
56
+ export declare class HunchStore {
57
+ private readonly paths;
58
+ readonly json: JsonStore;
59
+ /** Optional PRIVATE overlay (HUNCH_PRIVATE_DIR) — a second store in a repo the
60
+ * user controls. Unioned into reads via recs(); never written by public paths. */
61
+ private readonly privateJson?;
62
+ /** The resolved private-overlay hunch dir (from env or .hunch/local.json), or undefined
63
+ * when no overlay is configured. Surfaced so `hunch doctor` reflects the true state. */
64
+ readonly privateDir?: string;
65
+ /** How privateDir was selected. Multi-store consumers can use this instead of
66
+ * inferring process-global routing from process.env. */
67
+ readonly overlaySource: OverlayResolutionSource;
68
+ /** Present only when HUNCH_PRIVATE_DIR redirects this store away from the
69
+ * repo/worktree-local pointer. Precedence is compatibility-sensitive and stays
70
+ * env-first; making the redirection queryable removes the silent footgun. */
71
+ readonly overlayOverride?: OverlayOverride;
72
+ /** Whether captures auto-commit the store they land in — ON by default in EVERY mode;
73
+ * `--no-auto-commit` (hunch init/private/shared) persists `autoCommit: false` in
74
+ * local.json to opt out. Read by the MCP write tools and `hunch sync`. */
75
+ readonly autoCommit: boolean;
76
+ /** How memory is homed: "public" (no overlay — the repo-tracked .hunch/ is the one store),
77
+ * "private" (overlay holds ONLY private:true records; public records stay committed here),
78
+ * or "shared" (the overlay IS the store — every capture routes there, one source of truth
79
+ * across branches, worktrees, teammates, and agents). Absent `mode` in an existing
80
+ * config reads as "private" — no behavior change on upgrade. */
81
+ readonly mode: "public" | "private" | "shared";
82
+ /** mode === "shared" with a configured overlay: ALL captures route to the overlay. */
83
+ readonly unified: boolean;
84
+ /** autoCommit AND a private overlay is configured: private writes auto commit+push the
85
+ * overlay repo. (Public writes auto-commit the repo-tracked .hunch/ WITHOUT pushing —
86
+ * see commitAndPushHunch push:false / bug_overlay_clobber.) */
87
+ readonly privateAutoCommit: boolean;
88
+ /** When true, recs() ignores the private overlay (public-only). Set transiently by
89
+ * buildCheckReport({publicOnly}) so any PUBLICLY-POSTED report (the CI PR comment)
90
+ * can never render a private record — a publicly-posted output is a leak surface
91
+ * equal to a committed file (dec_d7bad4ccb7). */
92
+ private suppressPrivate;
93
+ private _db;
94
+ /** HEAD-keyed, process-local ranking evidence. It never changes record authority or storage. */
95
+ private decisionFreshnessRoot;
96
+ private decisionFreshnessHead;
97
+ private readonly decisionFreshnessScopes;
98
+ private readonly decisionFreshnessChanges;
99
+ constructor(paths: HunchPaths);
100
+ /** Human-facing warning for the compatibility-preserving env-first resolution.
101
+ * Callers decide where it is safe to emit (CLI/MCP stderr, doctor output); the
102
+ * store constructor stays side-effect-free for hooks and embedded consumers. */
103
+ overlayResolutionWarning(teamConfigBypassed?: boolean): string | null;
104
+ /** Where a capture belongs: an explicit private:true always goes to the overlay
105
+ * (putPrivate throws rather than silently landing public when none is configured);
106
+ * otherwise the overlay in unified ("shared") mode, else the public store. ONE home
107
+ * per record — the single-source-of-truth contract. */
108
+ captureHome(isPrivate?: boolean): "private" | "public";
109
+ /** Write a capture to its ONE home (see captureHome). Every capture path — MCP tools,
110
+ * post-commit synthesis, inline intents, record-constraint/conform, runbooks — funnels
111
+ * through here so all modes, branches, worktrees, teams, and agents agree on where
112
+ * memory lives. */
113
+ putCapture<K extends EntityKind>(kind: K, record: EntityFor[K], isPrivate?: boolean): EntityFor[K];
114
+ /** Replace one capture kind in its routed home as a single bulk operation.
115
+ *
116
+ * Snapshot producers use this instead of calling putCapture once per record.
117
+ * That distinction is material for array-backed graph kinds: repeated puts
118
+ * parse, sort, and atomically rewrite the complete index for every symbol or
119
+ * edge, while JsonStore.replaceAll validates the full input before performing
120
+ * one write. Routing and cross-home collision rules remain identical to an
121
+ * ordinary capture, so the optimization cannot create a second source of truth.
122
+ */
123
+ replaceCaptures<K extends EntityKind>(kind: K, records: EntityFor[K][], isPrivate?: boolean): void;
124
+ /** Read a record by id from wherever it lives (private overlay wins on collision). */
125
+ getRec<K extends EntityKind>(kind: K, id: string): EntityFor[K] | undefined;
126
+ /** Read a record only from the configured private overlay. Callers that must
127
+ * preserve privacy boundaries (for example, an explicit `--private` repair)
128
+ * should use this instead of overlay-first `getRec`. */
129
+ getPrivateRec<K extends EntityKind>(kind: K, id: string): EntityFor[K] | undefined;
130
+ getStateDirect<K extends "derived" | "receipts" | "commitments">(kind: K, id: string, home: "public" | "private"): EntityFor[K] | undefined;
131
+ /** Update an EXISTING record in the store that holds it — an overlay record must never
132
+ * fork a public copy on update (and vice versa). Falls back to captureHome routing for
133
+ * a record that exists nowhere yet. */
134
+ putWhereItLives<K extends EntityKind>(kind: K, record: EntityFor[K]): EntityFor[K];
135
+ /** Delete an existing record from its actual home. The review/curation path
136
+ * uses this so rejecting a private draft cannot silently leave it behind or
137
+ * accidentally target a public record with the same id. */
138
+ deleteWhereItLives<K extends EntityKind>(kind: K, id: string): boolean;
139
+ /** The private-overlay config from the gitignored `.hunch/local.json` (per-machine,
140
+ * never committed). Tolerant: returns {} on missing/invalid so reads never crash.
141
+ * `autoCommit` is tri-state: true/false when the file says so, undefined when unset.
142
+ * `mode` records HOW the overlay was set up ("private" split vs "shared" unified). */
143
+ private localConfig;
144
+ /** Merged read: public ∪ private overlay (private wins on id collision). Every
145
+ * QUERY / REINDEX path uses this so MCP + the guards see private memory. Public-
146
+ * artifact writers keep using this.json.loadAll (public-only) so a private record
147
+ * can never reach a committed file — see dec_d7bad4ccb7. */
148
+ recs<K extends EntityKind>(kind: K): EntityFor[K][];
149
+ /** Records for an AGENT-FACING advisory surface (escalations, orientation).
150
+ *
151
+ * In unified ("shared") mode the overlay IS the one store — the public `.hunch/` is
152
+ * only a routing shell — so reading the public home alone returns NOTHING and the
153
+ * surface reports "all clear" for a store whose every record is elsewhere. That is
154
+ * the worst possible answer for escalations, whose entire job is to raise the
155
+ * questions only a human can settle: a real topic collision came back as an empty
156
+ * list and the agent was affirmatively told there was nothing to escalate.
157
+ *
158
+ * In "private" mode the split is a real privacy boundary, so this stays public-only:
159
+ * private records must not surface on a public advisory surface. Mode-aware, not a
160
+ * blanket union — the distinction is the point. */
161
+ advisoryRecs<K extends EntityKind>(kind: K): EntityFor[K][];
162
+ /** Records from exactly one storage home (no public/private union). Capture
163
+ * paths use this for identity/lineage checks so a private record can never
164
+ * inherit or disclose relationships from an identically-shaped public record. */
165
+ recsInHome<K extends EntityKind>(kind: K, home: "public" | "private"): EntityFor[K][];
166
+ /** Whether a private overlay store is configured (HUNCH_PRIVATE_DIR is set). */
167
+ get hasPrivate(): boolean;
168
+ /** Public project root protected from private-overlay commit/push operations. */
169
+ get publicRoot(): string;
170
+ /** Write a record into the PRIVATE overlay (never the public repo). Throws if no
171
+ * HUNCH_PRIVATE_DIR is configured, so a "private" write can never silently land
172
+ * in the public `.hunch/`. */
173
+ putPrivate<K extends EntityKind>(kind: K, record: EntityFor[K]): EntityFor[K];
174
+ get db(): DB;
175
+ close(): void;
176
+ /** Revision marker for every JSON source currently visible to this store.
177
+ * SQLite/FTS is derived state; long-lived consumers compare this marker with
178
+ * the marker captured only after a successful rebuild. */
179
+ sourceStamp(): string;
180
+ /** Rebuild from fresh disk reads after an out-of-process Git pull or capture.
181
+ * Normal in-process writes invalidate their own kind cache; cross-process
182
+ * refreshes clear both homes explicitly so schema-only changes are included. */
183
+ reindexFresh(): {
184
+ counts: Record<string, number>;
185
+ };
186
+ /** Rebuild the entire SQLite index + FTS from the JSON source of truth. */
187
+ reindex(): {
188
+ counts: Record<string, number>;
189
+ };
190
+ /** FTS5 ranked search — the RAW bm25 primitive, no priors. Callers wanting
191
+ * relevance ordering (liveness/provenance/recency + topic-chain promotion)
192
+ * go through hybridSearch/searchScoped, where rerankByPriors applies.
193
+ * Falls back to LIKE if the query has no FTS-tokenizable terms. */
194
+ search(query: string, limit?: number): SearchHit[];
195
+ /** State-of-record ordering for nuryel.state/1 hits (superseded derived, done/cancelled
196
+ * commitments, failed receipts, retired entities): indexed and findable, but ranked BELOW
197
+ * the live record of the same subject. bm25 is negative (lower = better), so a history
198
+ * hit's score is scaled toward 0 by STATE_HISTORY_SCORE_FACTOR and the pool is re-sorted
199
+ * STABLY by score — a store with no state history returns the exact SQL order, and a
200
+ * LIKE-fallback pool (all scores 0) is partitioned live-first in its existing order.
201
+ * Bounded (a dimmer, not an exclusion) so the previous summary stays reachable one query
202
+ * away; hybridSearch/rankedSearch additionally apply the liveness prior via priorMeta. */
203
+ private demoteHistoricalState;
204
+ /** Liveness + clock for a state hit (null for every non-state kind). */
205
+ private stateMeta;
206
+ /** The bounded "State" slice for a context brief (hunch_context / `hunch context`): the
207
+ * current derived summaries, in-force commitments and latest verified receipts whose
208
+ * subject or text matches `target`. Matching is AND over the target's tokens (every token
209
+ * must appear, prefix-tolerant) so a file path such as src/store/x.ts never drags in a
210
+ * summary that merely mentions "store"; an exact subject match always qualifies. Order is
211
+ * deterministic: score (best first), then observed_at DESC, then id. Caps per kind are
212
+ * STATE_SLICE_CAPS. A store with no state records returns three empty lists. */
213
+ stateSlice(target: string): StateSlice;
214
+ /** AND-shaped FTS over one state kind (every token required, prefix-tolerant); degrades to
215
+ * the kind-scoped LIKE scan (OR-shaped — the caller re-checks AND) without FTS5. */
216
+ private stateFts;
217
+ /** Portable bounded fallback over titles/bodies. Each natural-language token
218
+ * is an OR candidate, mirroring the high-recall FTS query closely enough for
219
+ * runtimes whose SQLite build omits the optional FTS5 module.
220
+ *
221
+ * `_` is BOTH a LIKE single-character wildcard and the dominant character in this
222
+ * codebase's identifiers (dec_/con_/bug_ ids, snake_case symbols). The old code
223
+ * STRIPPED it, so a search for `hunch_record_decision` looked for the literal
224
+ * `hunchrecorddecision` and matched nothing — on precisely the runtimes with no FTS5,
225
+ * where this fallback is the only search there is. Escaping keeps the term literal;
226
+ * leaving `_` unescaped would silently over-match instead. */
227
+ private likeSearch;
228
+ /** The exact (ref, kind, title, body) docs that feed FTS — and thus embeddings.
229
+ * A single source so FTS, the doc_hash, and the stored vectors never disagree. */
230
+ private searchDocs;
231
+ /** Delete embedding rows whose source doc was removed or whose text changed
232
+ * (doc_hash mismatch). Model-free and cheap; run at the end of every reindex()
233
+ * so vectors track the JSON truth without ever being reset. Returns the count. */
234
+ pruneStaleEmbeddings(): number;
235
+ /** Embedding coverage for a model: up-to-date vectors vs total docs (doctor). */
236
+ embeddingStats(model: string): {
237
+ embedded: number;
238
+ total: number;
239
+ };
240
+ /** The SINGLE gate for "can semantic search run right now": an embedder exists and
241
+ * it has at least one stored vector. Used by both hybridSearch and the CLI so the
242
+ * definition can't drift between them. */
243
+ semanticReady(embedder: Embedder | null): embedder is Embedder;
244
+ /** Generate/refresh embeddings for every doc missing an up-to-date vector for
245
+ * this embedder's model. Batched + flushed per batch so a Ctrl-C leaves a
246
+ * coherent partial index that a re-run resumes. Assumes reindex() ran first. */
247
+ embedAll(embedder: Embedder, opts?: {
248
+ batch?: number;
249
+ onProgress?: (done: number, total: number) => void;
250
+ }): Promise<{
251
+ embedded: number;
252
+ skipped: number;
253
+ total: number;
254
+ }>;
255
+ /** Post-fusion rerank by graph PRIORS (dec_25e277f479): relevance ordering, not
256
+ * just reachability. Trust weight w = liveness × provenance × recency: liveness 0.6
257
+ * for superseded/retired/rejected, provenance 1.0 / 0.85 / 0.75 for
258
+ * human_confirmed / llm_draft / extracted-inferred, recency 0.7 + 0.3·½^(age/90d),
259
+ * and proven anchored-file staleness 0.8. Every factor is ranking-only.
260
+ * Runbook trigger phrases matching the query boost ×1.5 (exact intent beats
261
+ * keyword luck). Structural refs (symbols/components/edges) stay neutral.
262
+ *
263
+ * The prior is applied as a BOUNDED POSITIONAL SHIFT, not as a multiplier on a
264
+ * rank-derived score. That is deliberate and load-bearing. The old form —
265
+ * `1/(60+i) × w`, sorted descending, sliced to `limit` — could not keep the
266
+ * "dims, never disappears" promise it made: across a fused pool of 24 the
267
+ * positional term spans only 1/60…1/83 (a 0.72 ratio) while w spans 0.48…1.0 on
268
+ * this repo's own records, so the prior was WIDER than the entire pool's
269
+ * positional spread and simply overrode fusion. Worse, `priorMeta` returns null
270
+ * for structural refs, which left them at w = 1 — a ceiling above what any record
271
+ * that is not both human_confirmed and brand-new can reach. Measured over this
272
+ * repo's 152 committed decisions with the rest of the pool structural (symbols are
273
+ * ~91% of the corpus): 38% of decisions were dropped from the top-12 even when
274
+ * they were the #1 fused hit, and 88% from fused rank 8. Both retrieval layers
275
+ * ranked the record first and the rerank alone threw it away.
276
+ *
277
+ * A shift of at most ±MAX_PRIOR_SHIFT positions restores the intended semantics:
278
+ * a stale or low-provenance record visibly dims, an exact runbook-trigger match
279
+ * visibly promotes, and neither can leapfrog the whole pool. Same measurement
280
+ * after: 0% evicted from fused ranks 0–8. Deterministic; ties keep fused order. */
281
+ private rerankByPriors;
282
+ /**
283
+ * Score only freshness that the existing graph clocks can prove: an anchored path changed after
284
+ * `provenance.last_verified`. One bounded Git pass fills a HEAD-keyed cache for newly encountered
285
+ * scopes; repeated MCP/CLI queries perform no history walk until HEAD changes.
286
+ */
287
+ private staleDecisionIds;
288
+ /** Fast relevance ranking for task-phrase context fallback: FTS + bounded graph priors, no model. */
289
+ rankedSearch(query: string, limit?: number, opts?: RankedSearchOpts): SearchHit[];
290
+ /** The prior-bearing metadata for a hit: liveness, provenance, effective date,
291
+ * and (runbooks) trigger phrases. null = structural ref, neutral prior. */
292
+ private priorMeta;
293
+ /** Hybrid search (hunch_query / `hunch query --semantic`): FTS bm25 fused with
294
+ * cosine over stored embeddings via Reciprocal Rank Fusion. Degrades to pure
295
+ * sync FTS (zero added latency) when there's no embedder or no vectors yet, so
296
+ * the lean install and fallback regressions are unaffected. Pass
297
+ * `embedder: null` to FORCE FTS-only without auto-selecting. */
298
+ hybridSearch(query: string, limit?: number, opts?: HybridSearchOpts): Promise<SearchHit[]>;
299
+ /** Runbook-scoped retrieval (roadmap #5): the same FTS+graph(+semantic) fusion,
300
+ * restricted to the `runbooks` kind — so a "what's the procedure for X" query
301
+ * competes only with other runbooks, not the whole graph. Measurement showed
302
+ * whole-corpus retrieval buries terse runbooks (33% recall); scoping + semantic
303
+ * lifted recall@5 to 83% (dec_1239efae54 follow-up). Pass an embedder for the
304
+ * semantic leg; omit for keyword+graph. */
305
+ searchRunbooks(query: string, limit?: number, opts?: {
306
+ embedder?: Embedder | null;
307
+ }): Promise<SearchHit[]>;
308
+ /** Kind-SCOPED retrieval: FTS + (optional) semantic fused, but the candidate pool is
309
+ * restricted to one record kind from the START — not over-fetched from a whole-corpus
310
+ * ranking (whose top-50 cap can bury a terse record before any filter). This is what
311
+ * lifted runbook recall@5 from 33% → 83% in the measurement (dec_1239efae54 follow-up). */
312
+ searchScoped(query: string, kind: string, limit?: number, opts?: {
313
+ embedder?: Embedder | null;
314
+ }): Promise<SearchHit[]>;
315
+ /** FTS bm25 over a single kind (the `kind` column is UNINDEXED, so a plain `=`
316
+ * constraint composes with MATCH). Empty when the query has no FTS-able terms. */
317
+ private scopedFts;
318
+ /** Brute-force exact cosine top-n over stored vectors for one model. Vectors are
319
+ * pre-normalized, so cosine == dot product. Scoped to `dim = qvec.length` so a
320
+ * row stored at a different dimension (model id reused at a new dim) can never
321
+ * drive an out-of-bounds BLOB read; any with an unexpected byte length are
322
+ * skipped defensively rather than crashing the query. */
323
+ private cosineRank;
324
+ /** Rank-based Reciprocal Rank Fusion of the FTS, semantic, and graph lists. Ranks
325
+ * (not raw scores) erase the bm25-vs-cosine-vs-graph scale mismatch; a small lexical
326
+ * weight keeps exact symbol/path matches from being displaced by paraphrase or
327
+ * neighbor hits. An empty list contributes nothing, so 2-stream behavior is exactly
328
+ * preserved when graph (or sem) is absent. */
329
+ private rrfFuse;
330
+ /** Bounded relevance traversal over the dependency graph. Lexical/semantic symbol
331
+ * and component hits seed a small number of depth layers; support decays per hop
332
+ * and adds across multiple useful paths. Each frontier and the returned context
333
+ * obey a hard node cap, while hydration obeys a separate token cap. Only records
334
+ * present in the indexed symbol/component tables can enter the frontier, so
335
+ * shared external-package hubs never become context or bridge unrelated symbols. */
336
+ private graphExpand;
337
+ /** All decisions/bugs/constraints/symbols/components touching a file path or
338
+ * symbol name (hunch_why). Pass `{ asOf }` (an ISO instant) to TIME-TRAVEL:
339
+ * return only decisions/constraints whose valid-time window contained that
340
+ * instant — "what did we believe as of commit X?". Omit `asOf` for the full,
341
+ * history-inclusive view (backward-compatible default). */
342
+ why(target: string, opts?: {
343
+ asOf?: string;
344
+ }): WhyResult;
345
+ /** Transitive blast radius: every symbol/component that (in)directly depends on
346
+ * `id`, via a recursive CTE over the edges graph (hunch_get_dependents). We
347
+ * walk edges BACKWARD (edges.to = current) following call/dep/import/contains. */
348
+ getDependents(id: string, maxDepth?: number): Array<{
349
+ id: string;
350
+ depth: number;
351
+ via: string;
352
+ }>;
353
+ /** Symbols/components this id depends ON (forward walk) — used for refactor blast radius. */
354
+ getDependencies(id: string, maxDepth?: number): Array<{
355
+ id: string;
356
+ depth: number;
357
+ via: string;
358
+ }>;
359
+ /** Files whose symbols (in)directly DEPEND ON a symbol defined in `file` — the
360
+ * blast radius of editing `file`, collapsed to file granularity (nearest depth
361
+ * wins per file). Powers `hunch check` near-violation detection and `--blast`. */
362
+ blastRadiusFiles(file: string, maxDepth?: number): Array<{
363
+ file: string;
364
+ via: string;
365
+ depth: number;
366
+ }>;
367
+ /** Shortest undirected path between two graph nodes (symbols/components) over
368
+ * call/dep/import/contains/type-relation edges — "how does A reach B?" (hunch
369
+ * path / hunch_path). An iterative BFS visits each node once; the old recursive
370
+ * SQL enumerated simple paths and grew exponentially on dense real repositories.
371
+ * Returns the node chain in order, or null when no path exists within maxDepth. */
372
+ shortestPath(fromId: string, toId: string, maxDepth?: number): Array<{
373
+ id: string;
374
+ via: string;
375
+ }> | null;
376
+ /** Human label for a graph node id: "name @ file" for a symbol, the component name,
377
+ * or the id itself when unindexed. */
378
+ nodeLabel(id: string): string;
379
+ /** Resolve a free-form target (symbol id / name / file path, component id / name)
380
+ * to graph node ids — symbols win over components, exact file before suffix. */
381
+ resolveNodeIds(target: string): string[];
382
+ /** PR impact (read-only, ADVISORY — never gates): the dependency + memory surface
383
+ * of a change. Composes the SAME primitives as buildCheckReport (blast radius,
384
+ * scope-matched constraints, why) so impact and gating can never disagree. */
385
+ prImpact(files: string[], diff: string): ImpactReport;
386
+ /** Structure view (hunch_structure / hunch structure): serve the indexed shape of
387
+ * the repo so an agent ORIENTS from the graph instead of running grep/glob rounds.
388
+ * Resolution: no target -> repo map; a directory -> its files+symbols; a file ->
389
+ * its outline; a symbol name -> exact definition site(s) with one-hop neighbors.
390
+ * Deterministic, read-only, straight from the derived index. */
391
+ structure(target?: string): StructureView;
392
+ /** Labelled one-hop edge neighbors of a node ("in" = who reaches it, "out" = what it reaches). */
393
+ private edgeNeighbors;
394
+ /** Constraints whose scope glob matches a path/glob (hunch_check_constraints).
395
+ * By default only ACTIVE invariants are returned — a retired constraint is no
396
+ * longer enforced. Pass `{ asOf }` to instead return the invariants in force at
397
+ * that instant (time-travel: "what must I not have broken as of commit X?"). */
398
+ checkConstraints(scope: string, opts?: {
399
+ asOf?: string;
400
+ }): Constraint[];
401
+ /** LIVE findings (observations — audited, no diff yet) concerning a file/scope:
402
+ * triage open / accepted-risk / scheduled; resolved and stale stay silent. The
403
+ * matcher mirrors checkConstraints: an affected entry may be a concrete path or a
404
+ * glob, and the queried scope may be either too. Advisory only — findings never
405
+ * enter any block path. Sorted worst-first, then id for stable output. */
406
+ liveFindingsFor(scope: string): Finding[];
407
+ /** The causal chain behind a constraint — the WHY a diff-only reviewer can't see.
408
+ * Deterministic graph join: constraint → source_decision (the decision that
409
+ * motivated the guard) → the bug whose root cause spawned it (via
410
+ * lineage.spawned_constraint, else the source decision's caused_by_bug). Read-only. */
411
+ causalChain(constraintId: string): CausalWhy;
412
+ /** Assemble a CheckReport from a diff: direct invariant hits, near hits (blast
413
+ * radius), and regressions (re-added retired code), with the hardened strict
414
+ * gate and a causal `why` citation per direct hit. Read-only — shared by
415
+ * `hunch check`, the CI guard, and hunch_merge_verdict so they never drift. */
416
+ buildCheckReport(files: string[], diff: string, opts: {
417
+ strict: boolean;
418
+ lastChange?: (f: string) => string;
419
+ publicOnly?: boolean;
420
+ }): CheckReport;
421
+ /** Sprawl/"this already exists" guard (ADVISORY, never blocks). A symbol the diff
422
+ * ADDS whose name already exists in the indexed graph in a file NOT touched by the
423
+ * diff is a likely re-implementation the agent's local context window couldn't see.
424
+ * Read-only. Heuristic, so noise is controlled: top-level function/class/const only,
425
+ * name length ≥ 4, a stopword list of generic names, deduped, and capped. */
426
+ redundantSymbols(added: Array<{
427
+ name: string;
428
+ kind: string;
429
+ }>, files: string[], opts?: {
430
+ movedFrom?: string[];
431
+ removedNames?: Set<string>;
432
+ }): Array<{
433
+ name: string;
434
+ kind: string;
435
+ existingFile: string;
436
+ }>;
437
+ /** Time-travel: the decision history for a target — every decision touching it,
438
+ * newest-first, with its valid-time window and supersession links. Answers
439
+ * "what did we believe, and when/why did it change?" (hunch_timeline). */
440
+ timeline(target: string): Decision[];
441
+ /** Invalidate, don't delete (Zep edge-invalidation): close `oldId`'s valid-time
442
+ * window at the superseding decision's `valid_from`, mark it superseded + linked,
443
+ * and write a `supersedes` edge. Returns the updated old decision, or null if it
444
+ * doesn't exist. All writes are atomic via json.put (con_902759b3dc). */
445
+ supersede(oldId: string, by: Decision): Decision | null;
446
+ /** Look up a decision by id in a SPECIFIC store — the public store, or the private
447
+ * overlay when `priv` is true — NOT the union. The capture guard uses this to know
448
+ * whether a supersede will actually close its target: `supersede`/`supersedePrivate`
449
+ * each look in only one store, so a cross-store supersede silently no-ops and would
450
+ * leave two live decisions on one topic. Returns undefined if absent (or no overlay). */
451
+ decisionInStore(id: string, priv: boolean): Decision | undefined;
452
+ /** Private-overlay counterpart of `supersede`: close + link the old decision inside
453
+ * the HUNCH_PRIVATE_DIR store, so a PRIVATE decision can supersede another private
454
+ * one (the MCP record path is private→private). A private write never mutates the
455
+ * committed public store. Returns null if no private store is configured or the old
456
+ * record isn't in it. */
457
+ supersedePrivate(oldId: string, by: Decision): Decision | null;
458
+ /** Shared body for supersede / supersedePrivate against a specific store. */
459
+ private supersedeIn;
460
+ /** Regression Guard: detect a change RE-INTRODUCING something an in-force
461
+ * decision deliberately removed. Matches the added symbols/deps of a diff
462
+ * against the `retired` signal of decisions concerning the touched files. A hit
463
+ * is `blocking` when the retiring decision is tied to an ACTIVE blocking
464
+ * constraint (via source_decision) — that's the only case the strict guard
465
+ * fails the commit on; everything else is an advisory warning. */
466
+ regressionHits(added: {
467
+ symbols: string[];
468
+ deps: string[];
469
+ }, files: string[]): RegressionHit[];
470
+ /** Veto Guard: detect a change RE-INTRODUCING an approach an in-force decision
471
+ * deliberately REJECTED (`decision.rejected_tripwires`). The counterpart to
472
+ * regressionHits, which only sees code that once existed — a rejected alternative
473
+ * never did. Precision-first ladder (dep > symbol > pattern); the semantic tier is
474
+ * advisory and lives elsewhere. A hit `blocks` only when isVetoBlocker passes (a
475
+ * human-confirmed tripwire on an in-force, non-stale decision — dec_a466655539).
476
+ * Read-only; shared by buildCheckReport. */
477
+ vetoHits(an: DiffAnalysis, files: string[], staleDecisions?: Set<string>): VetoHit[];
478
+ /** Resolve a veto's causal citation: the bug whose root cause spawned the decision
479
+ * (decision → caused_by_bug). Distinct from causalChain, which is constraint-keyed. */
480
+ private vetoWhy;
481
+ /** Veto check for a LIVE edit (the agent pre-edit hook): no diff exists yet, so
482
+ * synthesize a minimal added-only diff from the proposed new lines and run the
483
+ * same vetoHits ladder. Freshness needs git lastChange (unavailable at edit time),
484
+ * so the staleness gate is skipped here — the commit/CI path applies it. */
485
+ vetoForFileEdit(file: string, addedLines: string[]): VetoHit[];
486
+ /** The symbols/deps an in-force decision deliberately RETIRED from a file — the
487
+ * agent-hook grounding ("don't re-add X here; dec_Y removed it"). No diff is
488
+ * available at edit time, so this surfaces the risk as context, not a block. */
489
+ retiredForFile(file: string): RetiredNote[];
490
+ /** Bugs matching a symptom (FTS over bugs) or a symbol, with lineage (hunch_bug_lineage). */
491
+ bugLineage(symptomOrSymbol: string): Bug[];
492
+ /** Ranked fragility report (hunch fragile). fragility = weighted churn + bugs + fan-in. */
493
+ fragility(limit?: number): FragileNode[];
494
+ /** Convenience: load a single entity from JSON by id (any kind). */
495
+ resolve(id: string): {
496
+ kind: string;
497
+ record: unknown;
498
+ } | undefined;
499
+ /** All edges (for graph export). */
500
+ allEdges(): Edge[];
501
+ /** Drift detection (DESIGN §9 "staleness kills trust"): a decision/constraint
502
+ * is STALE when a file in its scope changed AFTER it was last verified. The
503
+ * caller supplies `lastChange(file) -> ISO date | ""` (git-backed). */
504
+ staleness(lastChange: (file: string) => string): StaleRecord[];
505
+ /** The Context Assembler (DESIGN §2.1/§6): the MINIMAL relevant Hunch slice for
506
+ * a task on `target`, ordered by what matters most — invariants first, then the
507
+ * why, then blast radius and bug history — trimmed to a rough token budget. */
508
+ assembleContext(target: string, budget?: number, opts?: {
509
+ asOf?: string;
510
+ }): AssembledContext;
511
+ }
512
+ /** The graph-served repo shape (hunch_structure) — orient without grep rounds. */
513
+ export type StructureView = {
514
+ kind: "repo";
515
+ components: Array<{
516
+ id: string;
517
+ name: string;
518
+ responsibility: string;
519
+ paths: string[];
520
+ }>;
521
+ dirs: Array<{
522
+ dir: string;
523
+ files: number;
524
+ symbols: number;
525
+ }>;
526
+ } | {
527
+ kind: "dir";
528
+ dir: string;
529
+ files: Array<{
530
+ file: string;
531
+ symbols: Array<{
532
+ name: string;
533
+ kind: string;
534
+ fan_in: number;
535
+ }>;
536
+ }>;
537
+ } | {
538
+ kind: "file";
539
+ file: string;
540
+ symbols: Array<{
541
+ id: string;
542
+ name: string;
543
+ kind: string;
544
+ loc: number;
545
+ fan_in: number;
546
+ fan_out: number;
547
+ callers: string[];
548
+ }>;
549
+ } | {
550
+ kind: "symbol";
551
+ matches: Array<{
552
+ id: string;
553
+ name: string;
554
+ kind: string;
555
+ file: string;
556
+ fan_in: number;
557
+ fan_out: number;
558
+ callers: string[];
559
+ callees: string[];
560
+ }>;
561
+ } | {
562
+ kind: "none";
563
+ target: string;
564
+ };
565
+ export interface StaleRecord {
566
+ kind: string;
567
+ id: string;
568
+ last_verified: string;
569
+ changed_at: string;
570
+ files: string[];
571
+ }
572
+ /** A diff re-introducing something a decision deliberately removed. */
573
+ export interface RegressionHit {
574
+ decision: string;
575
+ title: string;
576
+ kind: "symbol" | "dep";
577
+ name: string;
578
+ /** True only when the retiring decision is tied to an active blocking invariant. */
579
+ blocking: boolean;
580
+ reason: string;
581
+ }
582
+ /** A diff re-introducing an approach an in-force decision deliberately REJECTED. */
583
+ export interface VetoHit {
584
+ decision: string;
585
+ title: string;
586
+ /** The rejected approach's text — the receipt headline. */
587
+ alternative: string;
588
+ /** What was chosen instead (decision.decision). */
589
+ chosen: string;
590
+ tier: VetoTier;
591
+ evidence: string[];
592
+ /** True only when the matched tripwire is human-confirmed, in-force, non-stale. */
593
+ blocks: boolean;
594
+ why?: {
595
+ bug?: {
596
+ id: string;
597
+ title: string;
598
+ root_cause: string;
599
+ };
600
+ };
601
+ }
602
+ /** What an in-force decision retired from a file (agent-hook grounding). */
603
+ export interface RetiredNote {
604
+ decision: string;
605
+ title: string;
606
+ symbols: string[];
607
+ deps: string[];
608
+ }
609
+ export interface AssembledContext {
610
+ target: string;
611
+ constraints: Constraint[];
612
+ decisions: Decision[];
613
+ bugs: Bug[];
614
+ blast_radius: Array<{
615
+ id: string;
616
+ depth: number;
617
+ via: string;
618
+ }>;
619
+ components: Component[];
620
+ findings: Finding[];
621
+ /** Current, explicitly reviewed Engineering Landscape orientation. */
622
+ landscape?: ReviewedLandscapeSelection;
623
+ budget_tokens: number;
624
+ }
@@ -192,8 +192,10 @@ export class HunchStore {
192
192
  putCapture(kind, record, isPrivate = false) {
193
193
  const home = this.captureHome(isPrivate);
194
194
  const id = record.id;
195
- const targetHasRecord = home === "private" ? !!this.privateJson?.get(kind, id) : !!this.json.get(kind, id);
196
- const otherHasRecord = home === "private" ? !!this.json.get(kind, id) : !!this.privateJson?.get(kind, id);
195
+ const lookup = (json) => kind === "derived" || kind === "receipts" || kind === "commitments"
196
+ ? json?.getDirect(kind, id) : json?.get(kind, id);
197
+ const targetHasRecord = !!lookup(home === "private" ? this.privateJson : this.json);
198
+ const otherHasRecord = !!lookup(home === "private" ? this.json : this.privateJson);
197
199
  // Legacy repositories can already contain twins, so an idempotent update in
198
200
  // the selected home remains possible. A new capture must never CREATE that
199
201
  // ambiguous state: merged/private-first reads would make later writers and
@@ -244,6 +246,9 @@ export class HunchStore {
244
246
  getPrivateRec(kind, id) {
245
247
  return this.privateJson?.get(kind, id);
246
248
  }
249
+ getStateDirect(kind, id, home) {
250
+ return (home === "private" ? this.privateJson : this.json)?.getDirect(kind, id);
251
+ }
247
252
  /** Update an EXISTING record in the store that holds it — an overlay record must never
248
253
  * fork a public copy on update (and vice versa). Falls back to captureHome routing for
249
254
  * a record that exists nowhere yet. */
@@ -1987,9 +1992,12 @@ const DECISION_FRESHNESS_PATH_CACHE_CAP = 4_096;
1987
1992
  * had correctly injected just below the cut line. This lifts memory records by a
1988
1993
  * bounded number of positions; it never EXCLUDES a kind (a symbol-name query still
1989
1994
  * returns symbols, and a constraint stays reachable), it only breaks the tie toward
1990
- * intent. Measured on bench/golden-retrieval.json: Recall@10 70% -> 90%, MRR
1991
- * 0.402 -> 0.575. Set HUNCH_MEMORY_PRIOR_SHIFT=0 to disable. */
1992
- const MEMORY_PRIOR_SHIFT = numEnv("HUNCH_MEMORY_PRIOR_SHIFT", 12);
1995
+ * intent. The task-report additions diluted one previously reachable decision:
1996
+ * on the same memory corpus, 12 positions gave Recall@10 8/11 (MRR .470);
1997
+ * 16 restores 9/11 (.483), preserving the existing floor and exact-symbol controls.
1998
+ * This is bounded calibration, not immunity to arbitrary corpus growth.
1999
+ * Set HUNCH_MEMORY_PRIOR_SHIFT=0 to disable. */
2000
+ const MEMORY_PRIOR_SHIFT = numEnv("HUNCH_MEMORY_PRIOR_SHIFT", 16);
1993
2001
  const MEMORY_KINDS = new Set(["decisions", "constraints", "bugs", "runbooks", "policies", ...STATE_KINDS]);
1994
2002
  /** State-of-record ordering in the RAW search path: a nuryel.state/1 history hit (superseded
1995
2003
  * derived, done/cancelled commitment, failed receipt, retired entity) keeps this fraction of