@hegemonart/get-design-done 1.28.8 → 1.30.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +116 -0
- package/README.de.md +25 -0
- package/README.fr.md +25 -0
- package/README.it.md +25 -0
- package/README.ja.md +25 -0
- package/README.ko.md +25 -0
- package/README.md +30 -0
- package/README.zh-CN.md +25 -0
- package/SKILL.md +2 -0
- package/agents/design-authority-watcher.md +42 -1
- package/agents/design-reflector.md +50 -0
- package/package.json +1 -1
- package/reference/capability-gap-stage-gate.md +261 -0
- package/reference/known-failure-modes.md +521 -0
- package/reference/pseudonymization-rules.md +189 -0
- package/reference/registry.json +22 -1
- package/reference/schemas/events.schema.json +158 -3
- package/reference/schemas/generated.d.ts +319 -4
- package/scripts/cli/gdd-events.mjs +35 -2
- package/scripts/gsd-cleanup-incubator.cjs +367 -0
- package/scripts/lib/apply-reflections/incubator-proposals.cjs +455 -0
- package/scripts/lib/authority-watcher/index.cjs +201 -0
- package/scripts/lib/bandit-router.cjs +92 -9
- package/scripts/lib/failure-mode-matcher.cjs +460 -0
- package/scripts/lib/gsd-health-mirror/index.cjs +37 -1
- package/scripts/lib/incubator-author.cjs +845 -0
- package/scripts/lib/install/interactive.cjs +27 -2
- package/scripts/lib/issue-reporter/cli-flag-report.cjs +153 -0
- package/scripts/lib/issue-reporter/consent-prompt.cjs +231 -0
- package/scripts/lib/issue-reporter/dedup.cjs +458 -0
- package/scripts/lib/issue-reporter/destination.cjs +37 -0
- package/scripts/lib/issue-reporter/draft-writer.cjs +157 -0
- package/scripts/lib/issue-reporter/gh-absent-fallback.cjs +220 -0
- package/scripts/lib/issue-reporter/gh-submit.cjs +114 -0
- package/scripts/lib/issue-reporter/kill-switch.cjs +122 -0
- package/scripts/lib/issue-reporter/payload-assembly.cjs +367 -0
- package/scripts/lib/issue-reporter/privacy-diff.cjs +385 -0
- package/scripts/lib/issue-reporter/report-flow.cjs +269 -0
- package/scripts/lib/issue-reporter/triage-matcher.cjs +270 -0
- package/scripts/lib/pseudonymize.cjs +444 -0
- package/scripts/lib/reflections-cycle-writer.cjs +172 -0
- package/scripts/lib/reflector/capability-gap-scan.cjs +751 -0
- package/scripts/lib/reflector-capability-gap-aggregator.cjs +352 -0
- package/scripts/lib/reflector-kfm-proposer.cjs +468 -0
- package/scripts/release-smoke-test.cjs +33 -2
- package/scripts/validate-incubator-scope.cjs +133 -0
- package/skills/apply-reflections/SKILL.md +20 -1
- package/skills/apply-reflections/apply-reflections-procedure.md +106 -4
- package/skills/fast/SKILL.md +46 -0
- package/skills/reflect/SKILL.md +9 -0
- package/skills/reflect/procedures/capability-gap-scan.md +120 -0
- package/skills/report-issue/SKILL.md +53 -0
- package/skills/report-issue/report-issue-procedure.md +120 -0
- package/skills/router/SKILL.md +5 -0
- package/skills/router/capability-gap-emitter.md +65 -0
- package/skills/update/SKILL.md +3 -2
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reflector-capability-gap-aggregator.cjs — Plan 29-03.
|
|
3
|
+
*
|
|
4
|
+
* Aggregates `capability_gap` events (emitted by Plans 29-01 + 29-02) into
|
|
5
|
+
* per-cycle cluster rollups and evaluates the Stage-0 → Stage-1 gate (D-01).
|
|
6
|
+
*
|
|
7
|
+
* Three exports:
|
|
8
|
+
*
|
|
9
|
+
* aggregateCapabilityGaps(eventsOrPath, opts?)
|
|
10
|
+
* - Accepts an iterable of events OR a path string to a JSONL chain file.
|
|
11
|
+
* - Returns { clusters: Cluster[] } where each Cluster is:
|
|
12
|
+
* { id: string, // first 12 chars of context_hash
|
|
13
|
+
* size: number,
|
|
14
|
+
* sources: { fast, router, reflector_pattern },
|
|
15
|
+
* examples: string[] // up to 3 evidence_ref strings
|
|
16
|
+
* }
|
|
17
|
+
* - Filters to records where (record.type === 'capability_gap'
|
|
18
|
+
* OR record.outcome === 'capability_gap') AND payload.context_hash is
|
|
19
|
+
* a non-empty string. Other rows are ignored silently.
|
|
20
|
+
* - Clusters are ordered: size DESC, id ASC tie-break.
|
|
21
|
+
*
|
|
22
|
+
* renderGapsSection(clusters)
|
|
23
|
+
* - Returns a markdown string. Empty list → '' (no section emitted).
|
|
24
|
+
* - Non-empty → '## Capability gaps observed' header + table.
|
|
25
|
+
*
|
|
26
|
+
* evaluateStageGate(history, config)
|
|
27
|
+
* - history: [{ cycle_slug, clusters }] — at least 1 cycle.
|
|
28
|
+
* - config: { K, M, stddev_threshold }. Defaults: K=3, M=10, threshold=0.05.
|
|
29
|
+
* - Returns { crossed, stable_cluster_ids, cycles_observed }.
|
|
30
|
+
* - A cluster is "stable" iff: appears in ≥ M consecutive cycles AND
|
|
31
|
+
* posterior `stddev(Beta(α, β)) < threshold`, where
|
|
32
|
+
* α = appearances + 1, β = (cycles_observed - appearances) + 1
|
|
33
|
+
* (Laplace prior; matches Phase 23.5 posterior store).
|
|
34
|
+
* - D-01 honored: this function EMITS A DECISION ONLY. The caller
|
|
35
|
+
* prompts the user. No auto-stage-flip path exists in this module.
|
|
36
|
+
*
|
|
37
|
+
* D-11 compliance: this module is a pure reader. All tests use synthetic
|
|
38
|
+
* fixtures (tests/reflector-capability-gap-aggregation.test.cjs).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
'use strict';
|
|
42
|
+
|
|
43
|
+
const { readFileSync, existsSync } = require('node:fs');
|
|
44
|
+
|
|
45
|
+
const DEFAULT_GATE_CONFIG = Object.freeze({
|
|
46
|
+
K: 3,
|
|
47
|
+
M: 10,
|
|
48
|
+
stddev_threshold: 0.05,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const ALLOWED_SOURCES = ['fast', 'router', 'reflector_pattern'];
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Internal helpers
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Return a fresh source-count bucket.
|
|
58
|
+
*/
|
|
59
|
+
function emptySources() {
|
|
60
|
+
return { fast: 0, router: 0, reflector_pattern: 0 };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Iterate events from either an in-memory iterable or a JSONL path string.
|
|
65
|
+
* Yields parsed records; invalid JSON lines are skipped silently (matches
|
|
66
|
+
* `event-chain.cjs.readChain` and `event-stream/reader.ts.readEvents`).
|
|
67
|
+
*/
|
|
68
|
+
function* iterateRecords(eventsOrPath) {
|
|
69
|
+
if (eventsOrPath == null) return;
|
|
70
|
+
if (typeof eventsOrPath === 'string') {
|
|
71
|
+
if (!existsSync(eventsOrPath)) return;
|
|
72
|
+
const raw = readFileSync(eventsOrPath, 'utf8');
|
|
73
|
+
for (const line of raw.split('\n')) {
|
|
74
|
+
const trimmed = line.trim();
|
|
75
|
+
if (trimmed === '') continue;
|
|
76
|
+
try {
|
|
77
|
+
yield JSON.parse(trimmed);
|
|
78
|
+
} catch (_err) {
|
|
79
|
+
// Malformed JSON — skip (matches existing readers' tolerance).
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (typeof eventsOrPath[Symbol.iterator] === 'function' ||
|
|
85
|
+
typeof eventsOrPath[Symbol.asyncIterator] === 'function') {
|
|
86
|
+
for (const rec of eventsOrPath) {
|
|
87
|
+
if (rec != null) yield rec;
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Unsupported input shape — yield nothing.
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Test whether a parsed record is a capability_gap event with a usable
|
|
96
|
+
* context_hash. Either the envelope `type` OR the chain-level `outcome`
|
|
97
|
+
* may carry the marker — `appendChainEvent` writes both.
|
|
98
|
+
*/
|
|
99
|
+
function isCapabilityGap(rec) {
|
|
100
|
+
if (rec == null || typeof rec !== 'object') return false;
|
|
101
|
+
const typeMatch = rec.type === 'capability_gap';
|
|
102
|
+
const outcomeMatch = rec.outcome === 'capability_gap';
|
|
103
|
+
if (!typeMatch && !outcomeMatch) return false;
|
|
104
|
+
const ctxHash = rec.payload && rec.payload.context_hash;
|
|
105
|
+
return typeof ctxHash === 'string' && ctxHash.length > 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Stringify an evidence_ref entry for the markdown example column. If it's
|
|
110
|
+
* already a string, return it; otherwise prefer `trajectory_path` and fall
|
|
111
|
+
* back to a JSON.stringify so the test can still match.
|
|
112
|
+
*/
|
|
113
|
+
function refToExample(ref) {
|
|
114
|
+
if (typeof ref === 'string') return ref;
|
|
115
|
+
if (ref && typeof ref === 'object') {
|
|
116
|
+
if (typeof ref.trajectory_path === 'string') return ref.trajectory_path;
|
|
117
|
+
try { return JSON.stringify(ref); } catch (_e) { return '[ref]'; }
|
|
118
|
+
}
|
|
119
|
+
return String(ref);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Closed-form posterior stddev of Beta(α, β):
|
|
124
|
+
* stddev = sqrt(αβ / ((α+β)^2 * (α+β+1)))
|
|
125
|
+
* No external math dependency. α + β > 0 (Laplace prior guarantees this).
|
|
126
|
+
*/
|
|
127
|
+
function betaStddev(alpha, beta) {
|
|
128
|
+
const sum = alpha + beta;
|
|
129
|
+
if (sum <= 0) return Infinity;
|
|
130
|
+
const variance = (alpha * beta) / (sum * sum * (sum + 1));
|
|
131
|
+
return Math.sqrt(variance);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Sanitize config — coerce to defaults if missing / invalid. Mirrors
|
|
136
|
+
* the trust-boundary mitigation in 29-03-PLAN.md threat T-29.03-02.
|
|
137
|
+
*/
|
|
138
|
+
function normalizeConfig(input) {
|
|
139
|
+
const out = { ...DEFAULT_GATE_CONFIG };
|
|
140
|
+
if (!input || typeof input !== 'object') return out;
|
|
141
|
+
if (Number.isInteger(input.K) && input.K > 0) out.K = input.K;
|
|
142
|
+
if (Number.isInteger(input.M) && input.M > 0) out.M = input.M;
|
|
143
|
+
if (typeof input.stddev_threshold === 'number'
|
|
144
|
+
&& Number.isFinite(input.stddev_threshold)
|
|
145
|
+
&& input.stddev_threshold > 0
|
|
146
|
+
&& input.stddev_threshold <= 1) {
|
|
147
|
+
out.stddev_threshold = input.stddev_threshold;
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Public API
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Aggregate capability_gap events into per-context_hash clusters.
|
|
157
|
+
*
|
|
158
|
+
* @param {Iterable<object> | string} eventsOrPath
|
|
159
|
+
* @param {{ exampleLimit?: number }} [opts]
|
|
160
|
+
* @returns {{ clusters: Array<{ id: string, size: number, sources: {fast:number,router:number,reflector_pattern:number}, examples: string[] }> }}
|
|
161
|
+
*/
|
|
162
|
+
function aggregateCapabilityGaps(eventsOrPath, opts = {}) {
|
|
163
|
+
const exampleLimit = Number.isInteger(opts.exampleLimit) && opts.exampleLimit > 0
|
|
164
|
+
? opts.exampleLimit : 3;
|
|
165
|
+
|
|
166
|
+
/** @type {Map<string, { id: string, size: number, sources: object, examples: string[], _hash: string }>} */
|
|
167
|
+
const byHash = new Map();
|
|
168
|
+
|
|
169
|
+
for (const rec of iterateRecords(eventsOrPath)) {
|
|
170
|
+
if (!isCapabilityGap(rec)) continue;
|
|
171
|
+
const payload = rec.payload;
|
|
172
|
+
const fullHash = payload.context_hash;
|
|
173
|
+
const id = fullHash.slice(0, 12);
|
|
174
|
+
let cluster = byHash.get(fullHash);
|
|
175
|
+
if (!cluster) {
|
|
176
|
+
cluster = { id, size: 0, sources: emptySources(), examples: [], _hash: fullHash };
|
|
177
|
+
byHash.set(fullHash, cluster);
|
|
178
|
+
}
|
|
179
|
+
cluster.size += 1;
|
|
180
|
+
const src = payload.source;
|
|
181
|
+
if (ALLOWED_SOURCES.includes(src)) {
|
|
182
|
+
cluster.sources[src] += 1;
|
|
183
|
+
}
|
|
184
|
+
if (Array.isArray(payload.evidence_refs)) {
|
|
185
|
+
for (const ref of payload.evidence_refs) {
|
|
186
|
+
if (cluster.examples.length >= exampleLimit) break;
|
|
187
|
+
cluster.examples.push(refToExample(ref));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const clusters = Array.from(byHash.values());
|
|
193
|
+
clusters.sort((a, b) => {
|
|
194
|
+
if (b.size !== a.size) return b.size - a.size;
|
|
195
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
196
|
+
});
|
|
197
|
+
// Strip internal full-hash field from output (not part of public Cluster shape).
|
|
198
|
+
for (const c of clusters) delete c._hash;
|
|
199
|
+
return { clusters };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Render the markdown section appended to a reflector cycle file.
|
|
204
|
+
* Returns '' when clusters is empty — caller appends unconditionally.
|
|
205
|
+
*
|
|
206
|
+
* @param {Array} clusters
|
|
207
|
+
* @returns {string}
|
|
208
|
+
*/
|
|
209
|
+
function renderGapsSection(clusters) {
|
|
210
|
+
if (!Array.isArray(clusters) || clusters.length === 0) return '';
|
|
211
|
+
const lines = [];
|
|
212
|
+
lines.push('## Capability gaps observed');
|
|
213
|
+
lines.push('');
|
|
214
|
+
lines.push('| Cluster | Size | fast | router | reflector_pattern | Example evidence |');
|
|
215
|
+
lines.push('|---|---|---|---|---|---|');
|
|
216
|
+
for (const c of clusters) {
|
|
217
|
+
const examples = (c.examples || [])
|
|
218
|
+
.slice(0, 3)
|
|
219
|
+
.map((e) => '`' + e + '`')
|
|
220
|
+
.join(', ');
|
|
221
|
+
lines.push(
|
|
222
|
+
`| \`${c.id}\` | ${c.size} | ${c.sources.fast || 0} | ${c.sources.router || 0} | ${c.sources.reflector_pattern || 0} | ${examples} |`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
lines.push('');
|
|
226
|
+
return lines.join('\n');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Evaluate the Stage-0 → Stage-1 gate per D-01 + D-03.
|
|
231
|
+
*
|
|
232
|
+
* @param {Array<{ cycle_slug: string, clusters: Array }>} history
|
|
233
|
+
* @param {{ K?: number, M?: number, stddev_threshold?: number }} [config]
|
|
234
|
+
* @returns {{ crossed: boolean, stable_cluster_ids: string[], cycles_observed: number }}
|
|
235
|
+
*/
|
|
236
|
+
function evaluateStageGate(history, config) {
|
|
237
|
+
const cfg = normalizeConfig(config);
|
|
238
|
+
if (!Array.isArray(history)) {
|
|
239
|
+
return { crossed: false, stable_cluster_ids: [], cycles_observed: 0 };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const cycles_observed = history.length;
|
|
243
|
+
|
|
244
|
+
// Need at least M cycles observed before we can evaluate stability.
|
|
245
|
+
if (cycles_observed < cfg.M) {
|
|
246
|
+
return { crossed: false, stable_cluster_ids: [], cycles_observed };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Stability checks per D-03:
|
|
250
|
+
// 1. Consecutive-presence: cluster appears in M consecutive cycles
|
|
251
|
+
// (the most recent run is the most reliable signal of "still here").
|
|
252
|
+
// 2. Posterior stddev: Beta(α, β) with α = appearances + 1,
|
|
253
|
+
// β = (cycles_observed - appearances) + 1 — Laplace prior matches
|
|
254
|
+
// Phase 23.5's bandit-router posterior store.
|
|
255
|
+
//
|
|
256
|
+
// Appearance counts use the FULL history (cycles_observed = history.length)
|
|
257
|
+
// so a cluster that has been present for many cycles accumulates evidence,
|
|
258
|
+
// even if it occasionally missed a cycle. The consecutive-presence check
|
|
259
|
+
// uses only the most recent run length (must be ≥ M).
|
|
260
|
+
|
|
261
|
+
/** @type {Map<string, number>} appearances across full history */
|
|
262
|
+
const appearances = new Map();
|
|
263
|
+
/** @type {Map<string, number>} most recent consecutive-presence run */
|
|
264
|
+
const currentRun = new Map();
|
|
265
|
+
/** @type {Map<string, number>} longest consecutive-presence run seen */
|
|
266
|
+
const maxConsecutive = new Map();
|
|
267
|
+
|
|
268
|
+
for (const cycle of history) {
|
|
269
|
+
const presentThisCycle = new Set();
|
|
270
|
+
if (Array.isArray(cycle.clusters)) {
|
|
271
|
+
for (const c of cycle.clusters) {
|
|
272
|
+
if (c && typeof c.id === 'string') {
|
|
273
|
+
presentThisCycle.add(c.id);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Update appearance counts + consecutive runs for every id we've
|
|
278
|
+
// seen so far OR seen this cycle.
|
|
279
|
+
const allIds = new Set([...currentRun.keys(), ...presentThisCycle]);
|
|
280
|
+
for (const id of allIds) {
|
|
281
|
+
if (presentThisCycle.has(id)) {
|
|
282
|
+
appearances.set(id, (appearances.get(id) || 0) + 1);
|
|
283
|
+
const run = (currentRun.get(id) || 0) + 1;
|
|
284
|
+
currentRun.set(id, run);
|
|
285
|
+
if (run > (maxConsecutive.get(id) || 0)) {
|
|
286
|
+
maxConsecutive.set(id, run);
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
// Cluster missed this cycle — reset current run; max already captured.
|
|
290
|
+
currentRun.set(id, 0);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const stable_cluster_ids = [];
|
|
296
|
+
for (const [id, appCount] of appearances.entries()) {
|
|
297
|
+
const maxRun = maxConsecutive.get(id) || 0;
|
|
298
|
+
if (maxRun < cfg.M) continue; // must appear in M consecutive cycles
|
|
299
|
+
// Laplace prior (Phase 23.5): α = appearances+1, β = (cycles-appearances)+1
|
|
300
|
+
const alpha = appCount + 1;
|
|
301
|
+
const beta = (cycles_observed - appCount) + 1;
|
|
302
|
+
const sd = betaStddev(alpha, beta);
|
|
303
|
+
if (sd < cfg.stddev_threshold) {
|
|
304
|
+
stable_cluster_ids.push(id);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
stable_cluster_ids.sort();
|
|
309
|
+
const crossed = stable_cluster_ids.length >= cfg.K;
|
|
310
|
+
return { crossed, stable_cluster_ids, cycles_observed };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// Plan 30.5-03 — Reflector KFM proposer wiring.
|
|
315
|
+
//
|
|
316
|
+
// After aggregation, downstream callers may pass the cluster list into the
|
|
317
|
+
// KFM proposer (`scripts/lib/reflector-kfm-proposer.cjs`). The proposer
|
|
318
|
+
// only emits a draft when a cluster has size ≥3 AND no existing catalogue
|
|
319
|
+
// entry matches (D-05). The original 5 Phase 29 proposal classes are
|
|
320
|
+
// untouched — this is an additive 6th pass.
|
|
321
|
+
//
|
|
322
|
+
// We deliberately load the proposer lazily inside `proposeKfmDraftsForClusters`
|
|
323
|
+
// so this aggregator module remains importable in environments that don't
|
|
324
|
+
// have the failure-mode catalogue checked in (e.g. minimal CI shards).
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
function proposeKfmDraftsForClusters(clusters, options) {
|
|
328
|
+
if (!Array.isArray(clusters) || clusters.length === 0) {
|
|
329
|
+
return { drafted: [], skipped: [] };
|
|
330
|
+
}
|
|
331
|
+
// require lazily — see comment above.
|
|
332
|
+
// eslint-disable-next-line global-require
|
|
333
|
+
const proposer = require('./reflector-kfm-proposer.cjs');
|
|
334
|
+
const drafted = [];
|
|
335
|
+
const skipped = [];
|
|
336
|
+
for (const c of clusters) {
|
|
337
|
+
const result = proposer.proposeKfmDraft(c, options);
|
|
338
|
+
if (result.action === 'drafted') drafted.push(result);
|
|
339
|
+
else skipped.push({ cluster_id: c && c.id, ...result });
|
|
340
|
+
}
|
|
341
|
+
return { drafted, skipped };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
module.exports = {
|
|
345
|
+
aggregateCapabilityGaps,
|
|
346
|
+
renderGapsSection,
|
|
347
|
+
evaluateStageGate,
|
|
348
|
+
proposeKfmDraftsForClusters,
|
|
349
|
+
// Exported for testing / introspection only:
|
|
350
|
+
_betaStddev: betaStddev,
|
|
351
|
+
_DEFAULT_GATE_CONFIG: DEFAULT_GATE_CONFIG,
|
|
352
|
+
};
|