@dzhechkov/harness-core 0.7.11 → 0.8.1

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 (137) hide show
  1. package/.dz-manifest.json +287 -87
  2. package/README.md +60 -1
  3. package/dist/amendment-trace.d.ts +12 -1
  4. package/dist/amendment-trace.d.ts.map +1 -1
  5. package/dist/amendment-trace.js +22 -4
  6. package/dist/amendment-trace.js.map +1 -1
  7. package/dist/book-kb.d.ts.map +1 -1
  8. package/dist/book-kb.js +17 -0
  9. package/dist/book-kb.js.map +1 -1
  10. package/dist/feature-adr-checkpoints.d.ts +61 -0
  11. package/dist/feature-adr-checkpoints.d.ts.map +1 -1
  12. package/dist/feature-adr-checkpoints.js +116 -2
  13. package/dist/feature-adr-checkpoints.js.map +1 -1
  14. package/dist/feature-adr-routing.d.ts +69 -12
  15. package/dist/feature-adr-routing.d.ts.map +1 -1
  16. package/dist/feature-adr-routing.js +117 -7
  17. package/dist/feature-adr-routing.js.map +1 -1
  18. package/dist/index.d.ts +23 -6
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +23 -4
  21. package/dist/index.js.map +1 -1
  22. package/dist/learning-backend.d.ts +39 -0
  23. package/dist/learning-backend.d.ts.map +1 -1
  24. package/dist/learning-backend.js +31 -11
  25. package/dist/learning-backend.js.map +1 -1
  26. package/dist/lesson-bandit.d.ts +116 -0
  27. package/dist/lesson-bandit.d.ts.map +1 -0
  28. package/dist/lesson-bandit.js +235 -0
  29. package/dist/lesson-bandit.js.map +1 -0
  30. package/dist/lesson-payoff.d.ts +260 -0
  31. package/dist/lesson-payoff.d.ts.map +1 -0
  32. package/dist/lesson-payoff.js +597 -0
  33. package/dist/lesson-payoff.js.map +1 -0
  34. package/dist/loop-blobs.generated.js +2 -2
  35. package/dist/loop-blobs.generated.js.map +1 -1
  36. package/dist/mutation-gate.d.ts +31 -8
  37. package/dist/mutation-gate.d.ts.map +1 -1
  38. package/dist/mutation-gate.js +57 -16
  39. package/dist/mutation-gate.js.map +1 -1
  40. package/dist/named-lock.d.ts.map +1 -1
  41. package/dist/named-lock.js +11 -9
  42. package/dist/named-lock.js.map +1 -1
  43. package/dist/native-dep-probe.d.ts +11 -0
  44. package/dist/native-dep-probe.d.ts.map +1 -0
  45. package/dist/native-dep-probe.js +100 -0
  46. package/dist/native-dep-probe.js.map +1 -0
  47. package/dist/operations.d.ts.map +1 -1
  48. package/dist/operations.js +82 -1
  49. package/dist/operations.js.map +1 -1
  50. package/dist/parser-safe-region.d.ts +47 -0
  51. package/dist/parser-safe-region.d.ts.map +1 -0
  52. package/dist/parser-safe-region.js +47 -0
  53. package/dist/parser-safe-region.js.map +1 -0
  54. package/dist/patterns.d.ts +21 -0
  55. package/dist/patterns.d.ts.map +1 -1
  56. package/dist/patterns.js +52 -4
  57. package/dist/patterns.js.map +1 -1
  58. package/dist/profile.d.ts +202 -0
  59. package/dist/profile.d.ts.map +1 -0
  60. package/dist/profile.js +536 -0
  61. package/dist/profile.js.map +1 -0
  62. package/dist/project-skills-root.d.ts +44 -0
  63. package/dist/project-skills-root.d.ts.map +1 -0
  64. package/dist/project-skills-root.js +62 -0
  65. package/dist/project-skills-root.js.map +1 -0
  66. package/dist/publish.d.ts +5 -3
  67. package/dist/publish.d.ts.map +1 -1
  68. package/dist/publish.js +46 -8
  69. package/dist/publish.js.map +1 -1
  70. package/dist/qe-rounds.d.ts +75 -0
  71. package/dist/qe-rounds.d.ts.map +1 -0
  72. package/dist/qe-rounds.js +174 -0
  73. package/dist/qe-rounds.js.map +1 -0
  74. package/dist/registry.d.ts.map +1 -1
  75. package/dist/registry.js +5 -1
  76. package/dist/registry.js.map +1 -1
  77. package/dist/run-records.d.ts.map +1 -1
  78. package/dist/run-records.js +8 -1
  79. package/dist/run-records.js.map +1 -1
  80. package/dist/setup.d.ts +35 -4
  81. package/dist/setup.d.ts.map +1 -1
  82. package/dist/setup.js +86 -23
  83. package/dist/setup.js.map +1 -1
  84. package/dist/sign.d.ts +29 -24
  85. package/dist/sign.d.ts.map +1 -1
  86. package/dist/sign.js +183 -61
  87. package/dist/sign.js.map +1 -1
  88. package/dist/store-location.d.ts +56 -0
  89. package/dist/store-location.d.ts.map +1 -0
  90. package/dist/store-location.js +56 -0
  91. package/dist/store-location.js.map +1 -0
  92. package/dist/store-lock.d.ts +1 -0
  93. package/dist/store-lock.d.ts.map +1 -1
  94. package/dist/store-lock.js +7 -1
  95. package/dist/store-lock.js.map +1 -1
  96. package/dist/store-merge.d.ts +55 -0
  97. package/dist/store-merge.d.ts.map +1 -0
  98. package/dist/store-merge.js +77 -0
  99. package/dist/store-merge.js.map +1 -0
  100. package/dist/teach-target.d.ts +61 -0
  101. package/dist/teach-target.d.ts.map +1 -0
  102. package/dist/teach-target.js +105 -0
  103. package/dist/teach-target.js.map +1 -0
  104. package/dist/vector-tier.d.ts +30 -0
  105. package/dist/vector-tier.d.ts.map +1 -1
  106. package/dist/vector-tier.js +178 -20
  107. package/dist/vector-tier.js.map +1 -1
  108. package/package.json +13 -13
  109. package/sbom.json +591 -87
  110. package/src/amendment-trace.ts +34 -4
  111. package/src/book-kb.ts +17 -0
  112. package/src/feature-adr-checkpoints.ts +111 -2
  113. package/src/feature-adr-routing.ts +146 -7
  114. package/src/index.ts +55 -3
  115. package/src/learning-backend.ts +62 -11
  116. package/src/lesson-bandit.ts +279 -0
  117. package/src/lesson-payoff.ts +728 -0
  118. package/src/loop-blobs.generated.ts +2 -2
  119. package/src/mutation-gate.ts +74 -17
  120. package/src/named-lock.ts +10 -8
  121. package/src/native-dep-probe.ts +118 -0
  122. package/src/operations.ts +77 -1
  123. package/src/parser-safe-region.ts +65 -0
  124. package/src/patterns.ts +76 -6
  125. package/src/profile.ts +593 -0
  126. package/src/project-skills-root.ts +63 -0
  127. package/src/publish.ts +43 -7
  128. package/src/qe-rounds.ts +226 -0
  129. package/src/registry.ts +5 -1
  130. package/src/run-records.ts +9 -1
  131. package/src/setup.ts +87 -23
  132. package/src/sign.ts +180 -60
  133. package/src/store-location.ts +76 -0
  134. package/src/store-lock.ts +8 -1
  135. package/src/store-merge.ts +91 -0
  136. package/src/teach-target.ts +126 -0
  137. package/src/vector-tier.ts +236 -21
@@ -30,6 +30,11 @@ export interface LearningSample {
30
30
  readonly kind: LearningSampleKind;
31
31
  readonly reward?: number;
32
32
  readonly ts: string;
33
+ /** The recall domain this sample happened in, when the caller knows it. Threaded through to the
34
+ * bandit so a confirmation lands in the SAME context bucket the recall read from — without it a
35
+ * `dz recall --domain <x>` hit confirmed later writes to `general`, and `<x>` never sees it, which
36
+ * leaves domain-scoped re-ranking permanently inert (cross-family QE, gpt-5.6-sol). */
37
+ readonly domain?: string;
33
38
  }
34
39
 
35
40
  export interface TrainingResult {
@@ -122,6 +127,7 @@ export class NativeReinforcementBackend implements LearningSignalBackend {
122
127
  ts: sample.ts,
123
128
  ...(sample.reward !== undefined ? { reward: sample.reward } : {}),
124
129
  ...(sample.kind === 'recall-hit' ? { exposure: true } : {}),
130
+ ...(sample.domain !== undefined ? { domain: sample.domain } : {}),
125
131
  });
126
132
  if (r.ok) flushed += 1;
127
133
  else {
@@ -192,19 +198,70 @@ export function resolveLearningBackend(projectRoot: string, config: MemoryLearni
192
198
  }
193
199
  }
194
200
 
195
- export function applyLearningSignals<H extends { readonly score: number }>(
201
+ /**
202
+ * One bounded re-rank term in the pipeline {@link applyLearningSignalsWithTerms} applies
203
+ * (feature lesson-bandit-rerank, architecture §6).
204
+ *
205
+ * WHY THIS EXISTS. `memory.learning.deltaRerank` is not a re-rank hook: it is a boolean that
206
+ * switches on ONE computation and selects between exactly two call sites. Reusing it for a second
207
+ * payoff signal would force one of two bad shapes — two independent features behind one switch, or
208
+ * a third `applyLearningSignalsWith…` function (and a fourth after that). A term LIST is the shape
209
+ * that stops growing.
210
+ */
211
+ export interface RerankTerm {
212
+ /** `'delta'` | `'bandit'` — travels into the observability record. */
213
+ readonly id: string;
214
+ /** Raw per-candidate value, positionally aligned with `hits`/`candidates`. */
215
+ readonly byIndex: readonly number[];
216
+ /** Absolute bound on THIS term's contribution to a score. */
217
+ readonly cap: number;
218
+ /** Default `Math.tanh` (any magnitude → [-1,1]). A pre-bounded term passes identity. */
219
+ readonly squash?: (v: number) => number;
220
+ }
221
+
222
+ /**
223
+ * The one scoring pipeline: `adjusted = hit.score + cap*signal[i] + Σ_t t.cap * squash(t.byIndex[i])`,
224
+ * stable-sorted with the original index as tie-break.
225
+ *
226
+ * With `terms: []` this is {@link applyLearningSignals} in behaviour; with one `'delta'` term it is
227
+ * {@link applyLearningSignalsWithDelta}. Both remain exported as thin wrappers, so every existing
228
+ * caller and test is untouched — the widening is additive at the API level too.
229
+ *
230
+ * A non-finite raw value contributes 0 and never NaN-poisons the sort (the guard the delta function
231
+ * already carried; generalising it would have been the easiest place to drop it).
232
+ */
233
+ export function applyLearningSignalsWithTerms<H extends { readonly score: number }>(
196
234
  hits: readonly H[],
197
235
  backend: LearningSignalBackend,
198
236
  candidates: readonly SignalCandidate[],
199
237
  cap: number,
238
+ terms: readonly RerankTerm[] = [],
200
239
  ): H[] {
201
240
  const signals = backend.enhance(candidates, { kind: 'recall', cap });
202
241
  return hits
203
- .map((hit, i) => ({ hit, adjusted: hit.score + cap * (signals[i] ?? 0), i }))
242
+ .map((hit, i) => {
243
+ let extra = 0;
244
+ for (const t of terms) {
245
+ const raw = t.byIndex[i];
246
+ if (typeof raw !== 'number' || !Number.isFinite(raw)) continue;
247
+ const squashed = (t.squash ?? Math.tanh)(raw);
248
+ if (Number.isFinite(squashed)) extra += t.cap * squashed;
249
+ }
250
+ return { hit, adjusted: hit.score + cap * (signals[i] ?? 0) + extra, i };
251
+ })
204
252
  .sort((a, b) => b.adjusted - a.adjusted || a.i - b.i)
205
253
  .map((x) => x.hit);
206
254
  }
207
255
 
256
+ export function applyLearningSignals<H extends { readonly score: number }>(
257
+ hits: readonly H[],
258
+ backend: LearningSignalBackend,
259
+ candidates: readonly SignalCandidate[],
260
+ cap: number,
261
+ ): H[] {
262
+ return applyLearningSignalsWithTerms(hits, backend, candidates, cap, []);
263
+ }
264
+
208
265
  /**
209
266
  * {@link applyLearningSignals} plus a bounded ± SAFLA-delta term (rUv-scout #2 Phase 3). `deltaByIndex[i]`
210
267
  * is candidate `i`'s raw payoff SLOPE (0 when it has no slope signal); each is squashed through `tanh`
@@ -220,13 +277,7 @@ export function applyLearningSignalsWithDelta<H extends { readonly score: number
220
277
  deltaByIndex: readonly number[],
221
278
  deltaCap: number,
222
279
  ): H[] {
223
- const signals = backend.enhance(candidates, { kind: 'recall', cap });
224
- return hits
225
- .map((hit, i) => {
226
- const d = deltaByIndex[i];
227
- const deltaTerm = typeof d === 'number' && Number.isFinite(d) ? deltaCap * Math.tanh(d) : 0;
228
- return { hit, adjusted: hit.score + cap * (signals[i] ?? 0) + deltaTerm, i };
229
- })
230
- .sort((a, b) => b.adjusted - a.adjusted || a.i - b.i)
231
- .map((x) => x.hit);
280
+ return applyLearningSignalsWithTerms(hits, backend, candidates, cap, [
281
+ { id: 'delta', byIndex: deltaByIndex, cap: deltaCap },
282
+ ]);
232
283
  }
@@ -0,0 +1,279 @@
1
+ /**
2
+ * VENDORED — contextual Thompson-Sampling bandit (feature lesson-bandit-rerank, ADR-001 D-1).
3
+ *
4
+ * Upstream: agentdb@3.0.0-alpha.20 — dist/src/backends/rvf/SolverBandit.js
5
+ * Licence: MIT, Copyright (c) 2024-2025 ruv
6
+ * Vendored: 2026-08-26. 215 lines, ZERO imports (both re-verified at copy time:
7
+ * `wc -l` → 215, `grep -cE 'require|^import'` → 0).
8
+ * SHA-256: 0199299fb60ef67afa030182185894c950126038398032349fe3618d910f6d7d
9
+ * (of the upstream .js at copy time; pinned by lesson-bandit-vendor.test.ts)
10
+ * Reason: the path is NOT in agentdb's package.json "exports" map (verified 2026-08-26);
11
+ * a deep import of a private path in a 3.0.0-alpha prerelease can change silently,
12
+ * and a ranking feature that quietly stops ranking looks exactly like one that works.
13
+ *
14
+ * Do not edit logic. Re-vendor from upstream and re-diff instead.
15
+ *
16
+ * WHAT CHANGED relative to the upstream .js, and nothing else:
17
+ * · `class SolverBandit` → `class LessonBandit` (ADR-001 D-1: one name, no alias);
18
+ * · TYPES added (field declarations, method signatures, the three exported interfaces);
19
+ * · `armKeys[0]` → `armKeys[0]!` and `ctx.get(armKey)` → `!` — required by this package's
20
+ * `noUncheckedIndexedAccess`; the emitted arithmetic is unchanged.
21
+ * The Jöhnk / Marsaglia-Tsang samplers, the `a<=1 && b<=1 ⇒ Math.random()` short-circuit, the
22
+ * exploration bonus and the cost EMA are preserved verbatim — the MEASURED API behaviour (200
23
+ * pulls on a 0.85-vs-0.15 pair → 100/100 correct picks) is a property of exactly this arithmetic.
24
+ *
25
+ * THIS FILE MUST STAY IMPORT-FREE (NFR-2 / C-7 / INV-6) — asserted by a repo test.
26
+ */
27
+
28
+ /** Beta posterior + bookkeeping for one `(ContextKey, ArmKey)` pair. */
29
+ export interface ArmStats {
30
+ alpha: number;
31
+ beta: number;
32
+ pulls: number;
33
+ totalReward: number;
34
+ costEma: number;
35
+ }
36
+
37
+ /** Engine hyperparameters. This feature passes the vendored defaults through untouched. */
38
+ export interface BanditConfig {
39
+ costWeight: number;
40
+ costDecay: number;
41
+ explorationBonus: number;
42
+ }
43
+
44
+ /** The engine's own JSON-safe state. `version` belongs to the vendored copy, never to our envelope. */
45
+ export interface SerializedBanditState {
46
+ version: number;
47
+ config: BanditConfig;
48
+ contexts: Record<string, Record<string, ArmStats>>;
49
+ }
50
+
51
+ /** Aggregate counters over every context. */
52
+ export interface BanditAggregateStats {
53
+ contexts: number;
54
+ totalArms: number;
55
+ totalPulls: number;
56
+ totalReward: number;
57
+ }
58
+
59
+ /**
60
+ * Thompson Sampling bandit with contextual arms.
61
+ *
62
+ * Usage:
63
+ * const bandit = new LessonBandit();
64
+ * const arm = bandit.selectArm('code_review', ['skill-a', 'skill-b', 'skill-c']);
65
+ * // ... execute the selected arm ...
66
+ * bandit.recordReward('code_review', arm, 0.85);
67
+ */
68
+ export class LessonBandit {
69
+ private contexts = new Map<string, Map<string, ArmStats>>();
70
+ private readonly config: BanditConfig;
71
+
72
+ constructor(config?: Partial<BanditConfig>) {
73
+ this.config = {
74
+ costWeight: config?.costWeight ?? 0.01,
75
+ costDecay: config?.costDecay ?? 0.1,
76
+ explorationBonus: config?.explorationBonus ?? 0.1,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Select the best arm for a given context using Thompson Sampling.
82
+ *
83
+ * For each candidate arm, samples from its Beta(alpha, beta) distribution
84
+ * and subtracts a cost penalty. Returns the arm with the highest score.
85
+ * Unknown arms get an exploration bonus.
86
+ */
87
+ selectArm(contextKey: string, armKeys: readonly string[]): string {
88
+ if (armKeys.length === 0)
89
+ throw new Error('No arms provided');
90
+ if (armKeys.length === 1)
91
+ return armKeys[0]!;
92
+ const ctx = this.contexts.get(contextKey);
93
+ let bestArm = armKeys[0]!;
94
+ let bestScore = -Infinity;
95
+ for (const arm of armKeys) {
96
+ const stats = ctx?.get(arm);
97
+ let score: number;
98
+ if (!stats || stats.pulls === 0) {
99
+ // Unknown arm: sample from uniform + exploration bonus
100
+ score = Math.random() + this.config.explorationBonus;
101
+ }
102
+ else {
103
+ // Thompson sample from Beta(alpha, beta)
104
+ const sample = this.sampleBeta(stats.alpha, stats.beta);
105
+ score = sample - stats.costEma * this.config.costWeight;
106
+ }
107
+ if (score > bestScore) {
108
+ bestScore = score;
109
+ bestArm = arm;
110
+ }
111
+ }
112
+ return bestArm;
113
+ }
114
+
115
+ /**
116
+ * Record the outcome of pulling an arm.
117
+ *
118
+ * @param contextKey - The context bucket (e.g., task type)
119
+ * @param armKey - The arm that was pulled (e.g., skill name)
120
+ * @param reward - Success signal in [0, 1]
121
+ * @param cost - Optional cost signal (latency, tokens, etc.)
122
+ */
123
+ recordReward(contextKey: string, armKey: string, reward: number, cost?: number): void {
124
+ if (!this.contexts.has(contextKey)) {
125
+ this.contexts.set(contextKey, new Map());
126
+ }
127
+ const ctx = this.contexts.get(contextKey)!;
128
+ if (!ctx.has(armKey)) {
129
+ ctx.set(armKey, { alpha: 1, beta: 1, pulls: 0, totalReward: 0, costEma: 0 });
130
+ }
131
+ const arm = ctx.get(armKey)!;
132
+ // Update Beta distribution
133
+ const r = Math.max(0, Math.min(1, reward));
134
+ arm.alpha += r;
135
+ arm.beta += (1 - r);
136
+ arm.pulls++;
137
+ arm.totalReward += r;
138
+ // Update cost EMA
139
+ if (cost !== undefined) {
140
+ arm.costEma = arm.costEma * (1 - this.config.costDecay) + cost * this.config.costDecay;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Rerank a list of candidates using bandit scores.
146
+ * Returns indices sorted by Thompson-sampled score (best first).
147
+ */
148
+ rerank(contextKey: string, armKeys: readonly string[]): string[] {
149
+ if (armKeys.length <= 1)
150
+ return [...armKeys];
151
+ const ctx = this.contexts.get(contextKey);
152
+ const scored = armKeys.map((arm) => {
153
+ const stats = ctx?.get(arm);
154
+ let score: number;
155
+ if (!stats || stats.pulls === 0) {
156
+ score = Math.random() + this.config.explorationBonus;
157
+ }
158
+ else {
159
+ score = this.sampleBeta(stats.alpha, stats.beta) - stats.costEma * this.config.costWeight;
160
+ }
161
+ return { arm, score };
162
+ });
163
+ scored.sort((a, b) => b.score - a.score);
164
+ return scored.map((s) => s.arm);
165
+ }
166
+
167
+ /** Get arm stats for a specific context */
168
+ getArmStats(contextKey: string, armKey: string): ArmStats | null {
169
+ return this.contexts.get(contextKey)?.get(armKey) ?? null;
170
+ }
171
+
172
+ /** Get aggregate statistics */
173
+ getStats(): BanditAggregateStats {
174
+ let totalArms = 0, totalPulls = 0, totalReward = 0;
175
+ for (const ctx of this.contexts.values()) {
176
+ totalArms += ctx.size;
177
+ for (const arm of ctx.values()) {
178
+ totalPulls += arm.pulls;
179
+ totalReward += arm.totalReward;
180
+ }
181
+ }
182
+ return { contexts: this.contexts.size, totalArms, totalPulls, totalReward };
183
+ }
184
+
185
+ /** Serialize to JSON-safe state */
186
+ serialize(): SerializedBanditState {
187
+ const contexts: Record<string, Record<string, ArmStats>> = {};
188
+ for (const [ctxKey, arms] of this.contexts) {
189
+ contexts[ctxKey] = {};
190
+ for (const [armKey, stats] of arms) {
191
+ contexts[ctxKey]![armKey] = { ...stats };
192
+ }
193
+ }
194
+ return { version: 1, config: { ...this.config }, contexts };
195
+ }
196
+
197
+ /**
198
+ * Restore from serialized state.
199
+ *
200
+ * STATIC (C-2). A first draft that called it as an instance method threw `TypeError`; the
201
+ * signature here makes the static call the only one that compiles.
202
+ */
203
+ static deserialize(state: SerializedBanditState): LessonBandit {
204
+ const bandit = new LessonBandit(state.config);
205
+ for (const [ctxKey, arms] of Object.entries(state.contexts)) {
206
+ const ctx = new Map<string, ArmStats>();
207
+ for (const [armKey, stats] of Object.entries(arms)) {
208
+ ctx.set(armKey, { ...stats });
209
+ }
210
+ bandit.contexts.set(ctxKey, ctx);
211
+ }
212
+ return bandit;
213
+ }
214
+
215
+ /** Reset all learned state */
216
+ reset(): void {
217
+ this.contexts.clear();
218
+ }
219
+
220
+ // ─── Private ───
221
+
222
+ /**
223
+ * Sample from Beta(a, b) using the Jöhnk algorithm.
224
+ * Fast approximation for typical bandit parameters.
225
+ */
226
+ private sampleBeta(a: number, b: number): number {
227
+ // For a=1, b=1 (uniform): just return Math.random()
228
+ if (a <= 1 && b <= 1)
229
+ return Math.random();
230
+ // Jöhnk's algorithm for general Beta
231
+ if (a < 1 && b < 1) {
232
+ for (let iter = 0; iter < 1000; iter++) {
233
+ const u = Math.random();
234
+ const v = Math.random();
235
+ const x = Math.pow(u, 1 / a);
236
+ const y = Math.pow(v, 1 / b);
237
+ if (x + y <= 1)
238
+ return x / (x + y);
239
+ }
240
+ return Math.random(); // fallback (extremely unlikely)
241
+ }
242
+ // For larger parameters, use Gamma ratio
243
+ const ga = this.sampleGamma(a);
244
+ const gb = this.sampleGamma(b);
245
+ return ga / (ga + gb);
246
+ }
247
+
248
+ /**
249
+ * Sample from Gamma(shape, 1) using Marsaglia & Tsang's method.
250
+ */
251
+ private sampleGamma(shape: number): number {
252
+ if (shape < 1) {
253
+ return this.sampleGamma(shape + 1) * Math.pow(Math.random(), 1 / shape);
254
+ }
255
+ const d = shape - 1 / 3;
256
+ const c = 1 / Math.sqrt(9 * d);
257
+ for (let iter = 0; iter < 1000; iter++) {
258
+ let x: number, v: number;
259
+ do {
260
+ x = this.sampleNormal();
261
+ v = 1 + c * x;
262
+ } while (v <= 0);
263
+ v = v * v * v;
264
+ const u = Math.random();
265
+ if (u < 1 - 0.0331 * (x * x) * (x * x))
266
+ return d * v;
267
+ if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v)))
268
+ return d * v;
269
+ }
270
+ return d; // fallback (extremely unlikely)
271
+ }
272
+
273
+ /** Box-Muller normal sample */
274
+ private sampleNormal(): number {
275
+ const u1 = Math.random();
276
+ const u2 = Math.random();
277
+ return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
278
+ }
279
+ }