@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,22 @@
1
+ /** Longest a write may hold the lock before another process may steal it: a put plus a
2
+ * git commit and push — seconds, not minutes. */
3
+ export declare const STALE_AFTER_MS = 60000;
4
+ /** Longest a writer waits before giving up and telling the caller. */
5
+ export declare const ACQUIRE_TIMEOUT_MS = 10000;
6
+ export interface LockOwner {
7
+ pid: number;
8
+ host: string;
9
+ nonce: string;
10
+ at: string;
11
+ }
12
+ export declare class WriteLockTimeout extends Error {
13
+ readonly path: string;
14
+ readonly heldBy: LockOwner | undefined;
15
+ readonly waitedMs: number;
16
+ constructor(path: string, heldBy: LockOwner | undefined, waitedMs: number);
17
+ }
18
+ export declare function writeLockPath(hunchDir: string): string;
19
+ /** Hold the partition's lock across `fn` (sync or async); released even if it throws. */
20
+ export declare function withWriteLock<T>(hunchDir: string, fn: () => T | Promise<T>, opts?: {
21
+ timeoutMs?: number;
22
+ }): Promise<T>;
@@ -0,0 +1,132 @@
1
+ import { z } from "zod";
2
+ import { type ChangeEvent, type Scope } from "../core/stateContract.js";
3
+ export declare const LEDGER_SCHEMA_VERSION: "nuryel.ledger/1";
4
+ export declare const CHANGES_DIR = "changes";
5
+ declare const IdempotencyEntrySchema: z.ZodObject<{
6
+ record_id: z.ZodString;
7
+ record_hash: z.ZodString;
8
+ payload_hash: z.ZodOptional<z.ZodString>;
9
+ facet: z.ZodString;
10
+ seq: z.ZodNumber;
11
+ at: z.ZodString;
12
+ }, z.core.$strict>;
13
+ export type IdempotencyEntry = z.infer<typeof IdempotencyEntrySchema>;
14
+ export declare const LedgerSchema: z.ZodObject<{
15
+ schema: z.ZodLiteral<"nuryel.ledger/1">;
16
+ scope: z.ZodObject<{
17
+ kind: z.ZodEnum<{
18
+ repository: "repository";
19
+ organization: "organization";
20
+ team: "team";
21
+ user: "user";
22
+ }>;
23
+ id: z.ZodString;
24
+ }, z.core.$strict>;
25
+ head_seq: z.ZodNumber;
26
+ floor_seq: z.ZodDefault<z.ZodNumber>;
27
+ events: z.ZodArray<z.ZodObject<{
28
+ schema: z.ZodLiteral<"nuryel.state.subscribe/1">;
29
+ seq: z.ZodNumber;
30
+ at: z.ZodString;
31
+ scope: z.ZodObject<{
32
+ kind: z.ZodEnum<{
33
+ repository: "repository";
34
+ organization: "organization";
35
+ team: "team";
36
+ user: "user";
37
+ }>;
38
+ id: z.ZodString;
39
+ }, z.core.$strict>;
40
+ facet: z.ZodEnum<{
41
+ decisions: "decisions";
42
+ constraints: "constraints";
43
+ bugs: "bugs";
44
+ findings: "findings";
45
+ receipts: "receipts";
46
+ commitments: "commitments";
47
+ derived: "derived";
48
+ entities: "entities";
49
+ relationships: "relationships";
50
+ }>;
51
+ record_id: z.ZodString;
52
+ record_hash: z.ZodString;
53
+ change: z.ZodEnum<{
54
+ updated: "updated";
55
+ retired: "retired";
56
+ superseded: "superseded";
57
+ created: "created";
58
+ invalidated: "invalidated";
59
+ }>;
60
+ subject: z.ZodOptional<z.ZodString>;
61
+ invalidates: z.ZodDefault<z.ZodArray<z.ZodString>>;
62
+ cause: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
63
+ kind: z.ZodLiteral<"receipt">;
64
+ receipt_id: z.ZodString;
65
+ }, z.core.$strict>, z.ZodObject<{
66
+ kind: z.ZodLiteral<"external">;
67
+ ref: z.ZodObject<{
68
+ system: z.ZodString;
69
+ object_type: z.ZodString;
70
+ object_key: z.ZodString;
71
+ version: z.ZodOptional<z.ZodString>;
72
+ content_hash: z.ZodOptional<z.ZodString>;
73
+ observed_at: z.ZodString;
74
+ locator: z.ZodOptional<z.ZodString>;
75
+ }, z.core.$strict>;
76
+ }, z.core.$strict>, z.ZodObject<{
77
+ kind: z.ZodLiteral<"write">;
78
+ principal: z.ZodString;
79
+ }, z.core.$strict>]>>;
80
+ }, z.core.$strict>>;
81
+ idempotency: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
82
+ record_id: z.ZodString;
83
+ record_hash: z.ZodString;
84
+ payload_hash: z.ZodOptional<z.ZodString>;
85
+ facet: z.ZodString;
86
+ seq: z.ZodNumber;
87
+ at: z.ZodString;
88
+ }, z.core.$strict>>>;
89
+ }, z.core.$strict>;
90
+ export type Ledger = z.infer<typeof LedgerSchema>;
91
+ /** Scope ids may carry `:` `@` `+` (safe in the contract, not in every file system), so
92
+ * the file name is the sanitized id plus a short hash of the exact id — readable AND
93
+ * collision-free. The scope inside the file is authoritative, the name is a locator. */
94
+ export declare function ledgerFile(hunchDir: string, scope: Scope): string;
95
+ export declare function emptyLedger(scope: Scope): Ledger;
96
+ /** Read the ledger for a scope; a missing file is an empty ledger, a corrupt one is an
97
+ * error (never silently treated as empty — that would restart the sequence). */
98
+ export declare function readLedger(hunchDir: string, scope: Scope): Ledger;
99
+ export declare function writeLedger(hunchDir: string, ledger: Ledger): void;
100
+ export type PendingChange = Omit<ChangeEvent, "schema" | "seq" | "at" | "scope">;
101
+ /** Append events (in order) and remember an idempotency key in ONE atomic write, so a
102
+ * crash between "record written" and "event appended" can be detected by the next
103
+ * writer (record present, ledger silent) rather than producing a half-applied write. */
104
+ export declare function appendChanges(hunchDir: string, scope: Scope, changes: readonly PendingChange[], idempotency: {
105
+ key: string;
106
+ entry: Omit<IdempotencyEntry, "seq" | "at">;
107
+ } | null, at?: string,
108
+ /** Already validated under the same uninterrupted partition lock; batch-local only. */
109
+ current?: Ledger): ChangeEvent[];
110
+ /** The latest seq that touched a record in this scope, or 0 when the ledger never saw it. */
111
+ export declare function latestSeqFor(ledger: Ledger, recordId: string): number;
112
+ /** Keep the newest `keep` events; everything older is dropped and the floor moves up. The
113
+ * idempotency table is kept whole (it is what makes replays exact); the records themselves are
114
+ * untouched. Returns how many events were dropped. */
115
+ export declare function compactLedger(hunchDir: string, scope: Scope, opts?: {
116
+ keep?: number;
117
+ }): {
118
+ dropped: number;
119
+ floor_seq: number;
120
+ head_seq: number;
121
+ };
122
+ /** Three-way merge of one scope's ledger, for the git merge driver: two clones that both
123
+ * appended to the same partition. The union of events is kept (identity = what changed, to
124
+ * which hash, when, by whom), ordered by time then ours-before-theirs, and RE-SEQUENCED from
125
+ * the higher floor; every subscriber's cursor is therefore invalid after a merge and the gap
126
+ * rule makes it resynchronize. Idempotency entries are unioned; a key both sides used for
127
+ * different records is a conflict the caller must surface (ours is kept). */
128
+ export declare function mergeLedgers(base: Ledger | null, ours: Ledger, theirs: Ledger): {
129
+ ledger: Ledger;
130
+ conflicts: string[];
131
+ };
132
+ export {};
@@ -10,7 +10,7 @@
10
10
  * here (see docs/nuryel-state-contract.md, "Not decided here").
11
11
  */
12
12
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
13
- import { join } from "node:path";
13
+ import { join, resolve } from "node:path";
14
14
  import { createHash } from "node:crypto";
15
15
  import { z } from "zod";
16
16
  import { writeFileAtomic } from "../core/io.js";
@@ -39,6 +39,12 @@ export const LedgerSchema = z.object({
39
39
  events: z.array(ChangeEventSchema),
40
40
  idempotency: z.record(z.string(), IdempotencyEntrySchema).default({}),
41
41
  }).strict();
42
+ // Process-local acceleration only: compare the actual bytes on EVERY read, never
43
+ // timestamps or a TTL. Separate processes and same-size replacements stay visible.
44
+ // Keep only four small snapshots; oversized ledgers follow the uncached path.
45
+ const validatedSnapshots = new Map();
46
+ const MAX_CACHED_LEDGERS = 4;
47
+ const MAX_CACHED_CHARACTERS = 1024 * 1024;
42
48
  /** Scope ids may carry `:` `@` `+` (safe in the contract, not in every file system), so
43
49
  * the file name is the sanitized id plus a short hash of the exact id — readable AND
44
50
  * collision-free. The scope inside the file is authoritative, the name is a locator. */
@@ -53,10 +59,21 @@ export function emptyLedger(scope) {
53
59
  /** Read the ledger for a scope; a missing file is an empty ledger, a corrupt one is an
54
60
  * error (never silently treated as empty — that would restart the sequence). */
55
61
  export function readLedger(hunchDir, scope) {
56
- const file = ledgerFile(hunchDir, scope);
57
- if (!existsSync(file))
62
+ const file = resolve(ledgerFile(hunchDir, scope));
63
+ if (!existsSync(file)) {
64
+ validatedSnapshots.delete(file);
58
65
  return emptyLedger(scope);
59
- const raw = JSON.parse(readFileSync(file, "utf8"));
66
+ }
67
+ const text = readFileSync(file, "utf8");
68
+ const cached = validatedSnapshots.get(file);
69
+ if (cached?.text === text && cached.scope === scopePath(scope)) {
70
+ validatedSnapshots.delete(file);
71
+ validatedSnapshots.set(file, cached);
72
+ // append/compaction callers mutate their copy. Never expose the cached object.
73
+ return JSON.parse(cached.normalized);
74
+ }
75
+ validatedSnapshots.delete(file);
76
+ const raw = JSON.parse(text);
60
77
  const ledger = LedgerSchema.parse(raw);
61
78
  if (scopePath(ledger.scope) !== scopePath(scope))
62
79
  throw new Error(`ledger ${file} belongs to scope ${scopePath(ledger.scope)}, not ${scopePath(scope)}`);
@@ -68,18 +85,30 @@ export function readLedger(hunchDir, scope) {
68
85
  }
69
86
  if (ledger.head_seq !== ledger.floor_seq + ledger.events.length)
70
87
  throw new Error(`ledger ${file} head_seq ${ledger.head_seq} disagrees with floor ${ledger.floor_seq} + ${ledger.events.length} events`);
88
+ if (text.length <= MAX_CACHED_CHARACTERS) {
89
+ while (validatedSnapshots.size >= MAX_CACHED_LEDGERS)
90
+ validatedSnapshots.delete(validatedSnapshots.keys().next().value);
91
+ validatedSnapshots.set(file, { text, normalized: JSON.stringify(ledger), scope: scopePath(scope) });
92
+ }
71
93
  return ledger;
72
94
  }
73
95
  export function writeLedger(hunchDir, ledger) {
96
+ writeValidatedLedger(hunchDir, LedgerSchema.parse(ledger));
97
+ }
98
+ function writeValidatedLedger(hunchDir, ledger) {
74
99
  const file = ledgerFile(hunchDir, ledger.scope);
75
100
  mkdirSync(join(hunchDir, CHANGES_DIR), { recursive: true });
76
- writeFileAtomic(file, JSON.stringify(LedgerSchema.parse(ledger), null, 2) + "\n");
101
+ writeFileAtomic(file, JSON.stringify(ledger, null, 2) + "\n");
77
102
  }
78
103
  /** Append events (in order) and remember an idempotency key in ONE atomic write, so a
79
104
  * crash between "record written" and "event appended" can be detected by the next
80
105
  * writer (record present, ledger silent) rather than producing a half-applied write. */
81
- export function appendChanges(hunchDir, scope, changes, idempotency, at = new Date().toISOString()) {
82
- const ledger = readLedger(hunchDir, scope);
106
+ export function appendChanges(hunchDir, scope, changes, idempotency, at = new Date().toISOString(),
107
+ /** Already validated under the same uninterrupted partition lock; batch-local only. */
108
+ current) {
109
+ const ledger = current ?? readLedger(hunchDir, scope);
110
+ if (scopePath(ledger.scope) !== scopePath(scope))
111
+ throw new Error("cached ledger belongs to another scope");
83
112
  const appended = [];
84
113
  for (const change of changes) {
85
114
  const event = ChangeEventSchema.parse({ schema: "nuryel.state.subscribe/1", seq: ledger.head_seq + 1, at, scope, ...change });
@@ -88,9 +117,11 @@ export function appendChanges(hunchDir, scope, changes, idempotency, at = new Da
88
117
  appended.push(event);
89
118
  }
90
119
  if (idempotency) {
91
- ledger.idempotency[idempotency.key] = { ...idempotency.entry, seq: ledger.head_seq, at };
120
+ ledger.idempotency[idempotency.key] = IdempotencyEntrySchema.parse({ ...idempotency.entry, seq: ledger.head_seq, at });
92
121
  }
93
- writeLedger(hunchDir, ledger);
122
+ // The initial ledger and each new event/entry are validated; don't re-validate the
123
+ // full history per assertion. Each append still reaches disk atomically before return.
124
+ writeValidatedLedger(hunchDir, ledger);
94
125
  return appended;
95
126
  }
96
127
  /** The latest seq that touched a record in this scope, or 0 when the ledger never saw it. */
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Record compaction (bounds unbounded Hunch growth). Auto-captured drafts
3
+ * accumulate forever; `hunch compact` prunes the LOW-VALUE ones — never an
4
+ * accepted/human-confirmed decision, never an open bug, never a constraint
5
+ * (invariants are sacred), and never a record another record still points to.
6
+ *
7
+ * planCompaction is PURE (takes `now`) so it is fully testable; the CLI supplies
8
+ * Date.now(). Reference safety uses a greatest-fixpoint so records being pruned in
9
+ * the same pass don't keep each other (or a cycle of dead drafts) alive.
10
+ */
11
+ import type { Bug, Constraint, Decision, EntityKind } from "../core/types.js";
12
+ export interface CompactionCandidate {
13
+ kind: EntityKind;
14
+ id: string;
15
+ title: string;
16
+ reason: string;
17
+ }
18
+ export interface CompactionPlan {
19
+ remove: CompactionCandidate[];
20
+ /** how many records were considered (decisions + bugs). */
21
+ considered: number;
22
+ }
23
+ export interface CompactionInput {
24
+ decisions: Decision[];
25
+ bugs: Bug[];
26
+ constraints: Constraint[];
27
+ }
28
+ export interface CompactionOpts {
29
+ now: number;
30
+ maxAgeDays?: number;
31
+ minConfidence?: number;
32
+ }
33
+ export declare function planCompaction(input: CompactionInput, opts: CompactionOpts): CompactionPlan;
@@ -0,0 +1,9 @@
1
+ export type DB = import("node:sqlite").DatabaseSync;
2
+ export declare function openDb(sqlitePath: string): DB;
3
+ /** In-memory db (tests / ephemeral queries). */
4
+ export declare function openMemoryDb(options?: {
5
+ forcePlainSearch?: boolean;
6
+ }): DB;
7
+ /** Run `fn` inside one transaction: BEGIN → fn → COMMIT, ROLLBACK on throw.
8
+ * (node:sqlite has no better-sqlite3-style transaction() helper.) */
9
+ export declare function withTx<T>(db: DB, fn: () => T): T;
@@ -0,0 +1,32 @@
1
+ export interface Embedder {
2
+ /** Embed each text → an L2-normalized Float32Array of length `dim`. */
3
+ embed(texts: string[]): Promise<Float32Array[]>;
4
+ readonly dim: number;
5
+ /** Stable id; also the `embeddings.model` key, so vectors from different models
6
+ * never get compared. */
7
+ readonly id: string;
8
+ }
9
+ export declare class TransformersEmbedder implements Embedder {
10
+ readonly dim = 384;
11
+ readonly id = "all-MiniLM-L6-v2";
12
+ private extractor;
13
+ private load;
14
+ embed(texts: string[]): Promise<Float32Array[]>;
15
+ }
16
+ /** Deterministic, dependency-free embedder for tests (no model download).
17
+ * Bag-of-token-hashes → L2-normalized vector: shared tokens ⇒ similar vectors,
18
+ * enough to exercise hybrid ranking without semantics. Intentionally returns
19
+ * SUBARRAY VIEWS into one backing buffer, so any caller that fails to copy
20
+ * tightly before persisting will corrupt data — that keeps the BLOB round-trip
21
+ * honest. */
22
+ export declare class StubEmbedder implements Embedder {
23
+ readonly dim: number;
24
+ readonly id = "stub-v1";
25
+ constructor(dim?: number);
26
+ embed(texts: string[]): Promise<Float32Array[]>;
27
+ }
28
+ /** Choose an embedder, or null to signal "no semantic search → use pure FTS".
29
+ * Never throws. `HUNCH_EMBEDDER=stub` forces the test stub; `=none` forces the
30
+ * null (FTS-only) path; otherwise use the local model iff its optional dep is
31
+ * installed. */
32
+ export declare function selectEmbedder(): Promise<Embedder | null>;