thumbgate 1.37.1 → 1.37.3

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 (47) hide show
  1. package/.agents/skills/article-syndication-pipeline/SKILL.md +37 -0
  2. package/.agents/skills/ci-gha-buildkite-patterns-not-clone/SKILL.md +72 -0
  3. package/.agents/skills/cobble-hot-store-compare-not-clone/SKILL.md +80 -0
  4. package/.agents/skills/colab-compute-honesty-not-clone/SKILL.md +70 -0
  5. package/.agents/skills/comet-session-logger/SKILL.md +59 -0
  6. package/.agents/skills/datadog-llm-obs-compare-not-clone/SKILL.md +75 -0
  7. package/.agents/skills/deeppattern-discipline-honesty-not-clone/SKILL.md +73 -0
  8. package/.agents/skills/thumbgate-board-loop/SKILL.md +72 -0
  9. package/.agents/skills/thumbgate-daily-discoveries/SKILL.md +57 -0
  10. package/.agents/skills/token-shunt-honesty-not-clone/SKILL.md +67 -0
  11. package/.agents/skills/typesafe-typed-questions-not-clone/SKILL.md +86 -0
  12. package/.claude-plugin/plugin.json +1 -1
  13. package/.well-known/mcp/server-card.json +1 -1
  14. package/adapters/claude/.mcp.json +2 -2
  15. package/adapters/forge/forge.yaml +3 -3
  16. package/adapters/future-agi/.mcp.json +1 -1
  17. package/adapters/future-agi/config.toml +1 -1
  18. package/adapters/future-agi/opencode.json +1 -1
  19. package/adapters/herdr/herdr-plugin.toml +2 -2
  20. package/adapters/mcp/server-stdio.js +1 -1
  21. package/adapters/opencode/opencode.json +1 -1
  22. package/bin/cli.js +257 -0
  23. package/config/gate-templates.json +96 -0
  24. package/package.json +55 -12
  25. package/public/blog/2026-09-23-datadog-llm-obs-four-practices.html +42 -0
  26. package/public/index.html +2 -2
  27. package/public/install.html +7 -7
  28. package/public/numbers.html +2 -2
  29. package/scripts/ci-gha-buildkite-patterns.js +393 -0
  30. package/scripts/cli-schema.js +90 -0
  31. package/scripts/cobble-hot-store-split.js +600 -0
  32. package/scripts/colab-compute-honesty.js +281 -0
  33. package/scripts/deeppattern-discipline-honesty.js +431 -0
  34. package/scripts/gates-engine.js +16 -5
  35. package/scripts/install-thumbgate-daily-publish-launchd.sh +83 -0
  36. package/scripts/llm-obs-honesty.js +478 -0
  37. package/scripts/thumbgate-board-loop.js +432 -0
  38. package/scripts/thumbgate-daily-discoveries-publish.js +413 -0
  39. package/scripts/thumbgate-daily-discoveries-publish.sh +18 -0
  40. package/scripts/token-shunt-honesty.js +502 -0
  41. package/scripts/typesafe-typed-questions.js +1138 -0
  42. package/server.json +2 -2
  43. package/skills/article-syndication-pipeline/SKILL.md +37 -0
  44. package/skills/comet-session-logger/SKILL.md +59 -0
  45. package/skills/thumbgate-daily-discoveries/SKILL.md +57 -0
  46. package/src/index.js +10 -0
  47. package/src/integrations/ideabrowser-connector.js +190 -0
@@ -0,0 +1,600 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * CobbleDB FORMAT steal — not a product clone.
6
+ *
7
+ * Source: https://www.perplexity.ai/hub/blog/cobbledb
8
+ *
9
+ * Transfers (process only):
10
+ * 1. Three-plane split — durable document state ≠ batched update delivery
11
+ * ≠ query-time hot store. Processing must not write the live read path.
12
+ * 2. Batch MultiGet + hedge — given a batch of prepared keys, group by
13
+ * partition, fetch in parallel, and hedge a slow/failed replica.
14
+ * 3. Policy-defined hot subset + coexisting representation versions —
15
+ * only promoted/matchable records occupy the expensive hot path;
16
+ * a new embedding/chunk version must not clobber the previous one.
17
+ *
18
+ * Maps onto existing ThumbGate rails (not a RocksDB / YTsaurus / Dynamo
19
+ * replacement):
20
+ * durable → feedback-log.jsonl + memory-log.jsonl + feedback-schema.js
21
+ * delivery → feedback-to-memory.js + memory-vs-rag dreaming=dynamic
22
+ * hot → lesson-retrieval.js + hybrid-feedback-context.js
23
+ *
24
+ * Does NOT clone CobbleDB, Pillar, Lorry, RocksDB clusters, or YTsaurus.
25
+ * ECI: maintenance of existing retrieval rails only — no storage SKU.
26
+ */
27
+
28
+ const fs = require('node:fs');
29
+ const path = require('node:path');
30
+
31
+ const SOURCE_URL = 'https://www.perplexity.ai/hub/blog/cobbledb';
32
+
33
+ const PLANES = Object.freeze(['durable', 'delivery', 'hot']);
34
+
35
+ const PLANE_RAILS = Object.freeze({
36
+ durable: {
37
+ cobble: 'Pillar — versioned document state on cheap HDD, subset membership',
38
+ rails: [
39
+ 'feedback-log.jsonl',
40
+ 'memory-log.jsonl',
41
+ 'scripts/feedback-schema.js',
42
+ ],
43
+ when: 'Raw thumbs and schema-valid lessons land here first. Never the live PreToolUse read.',
44
+ },
45
+ delivery: {
46
+ cobble: 'Lorry — partition-aligned batches, data plane off the control plane, independent ingest',
47
+ rails: [
48
+ 'scripts/feedback-to-memory.js',
49
+ 'scripts/memory-vs-rag-route.js (dreaming=dynamic)',
50
+ 'scripts/compact-memory-store.js',
51
+ ],
52
+ when: 'Promotion batches prepared records onto the hot path at the hot store\'s pace.',
53
+ },
54
+ hot: {
55
+ cobble: 'CobbleDB — partitioned MultiGet, replica hedge, no transactions',
56
+ rails: [
57
+ 'scripts/lesson-retrieval.js',
58
+ 'scripts/hybrid-feedback-context.js',
59
+ 'config/performance-budgets.json',
60
+ ],
61
+ when: 'Query-time batch fetch of prepared lesson records for ranking / PreToolUse.',
62
+ },
63
+ });
64
+
65
+ const HOT_SUBSET_POLICIES = Object.freeze([
66
+ 'promoted_matchable',
67
+ 'fresh_high_value',
68
+ 'unbounded',
69
+ ]);
70
+
71
+ const DEFAULT_GOLD_TRACE = Object.freeze({
72
+ writes: [
73
+ { source: 'processing', plane: 'durable', keys: ['lesson-1', 'lesson-2'] },
74
+ { source: 'export', plane: 'delivery', keys: ['lesson-1'] },
75
+ { source: 'ingest', plane: 'hot', keys: ['lesson-1'] },
76
+ ],
77
+ reads: [
78
+ {
79
+ keys: ['lesson-1', 'lesson-2', 'lesson-3', 'lesson-4', 'lesson-5'],
80
+ mode: 'batched',
81
+ hedgeMs: 15,
82
+ },
83
+ ],
84
+ hotSubset: { policy: 'promoted_matchable', keys: ['lesson-1'] },
85
+ versions: [
86
+ { key: 'lesson-1', representations: ['embed-v1', 'embed-v2'], overwrite: false },
87
+ ],
88
+ transactionsOnHot: false,
89
+ });
90
+
91
+ const CLONE_PATTERNS = Object.freeze([
92
+ { id: 'cobbledb_sku', re: /\b(clone|install|vendor|ship)\b.{0,40}\bcobble[- ]?db\b/i },
93
+ { id: 'rocksdb_cluster', re: /\b(rocksdb cluster|yt-?saurus|ytsaurus)\b/i },
94
+ { id: 'pillar_lorry_product', re: /\b(clone pillar|clone lorry|pillar \+ lorry sku)\b/i },
95
+ { id: 'dynamo_replacement', re: /\breplace dynamo(db)? with cobble/i },
96
+ ]);
97
+
98
+ function normalizeBoolean(value) {
99
+ if (value === true || value === 1) return true;
100
+ if (value === false || value === 0 || value == null) return false;
101
+ return /^(1|true|yes|on)$/i.test(String(value).trim());
102
+ }
103
+
104
+ function detectCloneAttempt(text) {
105
+ const t = String(text || '');
106
+ const hits = [];
107
+ for (const p of CLONE_PATTERNS) {
108
+ if (p.re.test(t)) hits.push(p.id);
109
+ }
110
+ return hits;
111
+ }
112
+
113
+ function hashPartition(key, partitionCount = 8) {
114
+ const n = Math.max(1, Number(partitionCount) || 8);
115
+ const s = String(key);
116
+ let h = 2166136261;
117
+ for (let i = 0; i < s.length; i += 1) {
118
+ h ^= s.charCodeAt(i);
119
+ h = Math.imul(h, 16777619);
120
+ }
121
+ return (h >>> 0) % n;
122
+ }
123
+
124
+ function normalizeKeyList(keys) {
125
+ if (Array.isArray(keys)) {
126
+ return keys.map((k) => String(k).trim()).filter(Boolean);
127
+ }
128
+ return String(keys || '')
129
+ .split(',')
130
+ .map((s) => s.trim())
131
+ .filter(Boolean);
132
+ }
133
+
134
+ function toStoreMap(store) {
135
+ if (store instanceof Map) return store;
136
+ if (Array.isArray(store)) {
137
+ return new Map(store.map((row) => {
138
+ if (Array.isArray(row) && row.length >= 2) return [String(row[0]), row[1]];
139
+ const key = row && (row.key || row.id);
140
+ return [String(key), row];
141
+ }).filter(([key]) => key && key !== 'undefined'));
142
+ }
143
+ if (store && typeof store === 'object') {
144
+ return new Map(Object.entries(store));
145
+ }
146
+ return new Map();
147
+ }
148
+
149
+ /**
150
+ * Local MultiGet analog: partition keys, read replicas in parallel (simulated),
151
+ * hedge a slow or failed primary. Not a RocksDB binding.
152
+ */
153
+ function batchGetPrepared(keys, options = {}) {
154
+ const list = normalizeKeyList(keys);
155
+ const partitionCount = Math.max(1, Number(options.partitionCount) || 8);
156
+ const hedgeMs = options.hedgeMs == null ? 15 : Number(options.hedgeMs);
157
+ const store = toStoreMap(options.store);
158
+ const replicaLatencies = options.replicaLatencies || {};
159
+ const replicaOk = options.replicaOk || {};
160
+
161
+ const groups = new Map();
162
+ for (const key of list) {
163
+ const p = hashPartition(key, partitionCount);
164
+ if (!groups.has(p)) groups.set(p, []);
165
+ groups.get(p).push(key);
166
+ }
167
+
168
+ const partitions = [];
169
+ const records = [];
170
+ let hedgedPartitions = 0;
171
+
172
+ for (const [partition, pkeys] of groups) {
173
+ const primaryLatency = Number(replicaLatencies[`${partition}:0`] ?? 0);
174
+ const backupLatency = Number(replicaLatencies[`${partition}:1`] ?? 0);
175
+ const primaryOk = replicaOk[`${partition}:0`] !== false;
176
+ const backupOk = replicaOk[`${partition}:1`] !== false;
177
+ const primarySlow = Number.isFinite(hedgeMs) && hedgeMs >= 0 && primaryLatency > hedgeMs;
178
+ let replica = 0;
179
+ let shouldHedge = false;
180
+ let unavailable = false;
181
+ if (primaryOk && !primarySlow) {
182
+ replica = 0;
183
+ } else if (backupOk) {
184
+ replica = 1;
185
+ shouldHedge = true;
186
+ } else if (primaryOk) {
187
+ replica = 0;
188
+ } else {
189
+ unavailable = true;
190
+ replica = null;
191
+ }
192
+ if (shouldHedge) hedgedPartitions += 1;
193
+ const latencyMs = unavailable
194
+ ? null
195
+ : (replica === 1 ? backupLatency : primaryLatency);
196
+ const hits = pkeys.map((key) => {
197
+ const found = !unavailable && store.has(key);
198
+ const value = found ? store.get(key) : null;
199
+ records.push({
200
+ key, partition, replica, found, value, hedged: shouldHedge, unavailable,
201
+ });
202
+ return { key, found };
203
+ });
204
+ partitions.push({
205
+ partition,
206
+ keys: pkeys,
207
+ replica,
208
+ hedged: shouldHedge,
209
+ unavailable,
210
+ latencyMs,
211
+ hits,
212
+ });
213
+ }
214
+
215
+ return {
216
+ keys: list,
217
+ partitionCount,
218
+ hedgeMs,
219
+ hedgedPartitions,
220
+ found: records.filter((r) => r.found).length,
221
+ missing: records.filter((r) => !r.found).map((r) => r.key),
222
+ partitions,
223
+ records,
224
+ };
225
+ }
226
+
227
+ function selectHotSubset(records, policy = 'promoted_matchable') {
228
+ const list = Array.isArray(records) ? records : [];
229
+ const resolved = HOT_SUBSET_POLICIES.includes(policy) ? policy : 'promoted_matchable';
230
+ let selected = list;
231
+ if (resolved === 'unbounded') {
232
+ selected = list;
233
+ } else if (resolved === 'fresh_high_value') {
234
+ selected = list.filter((row) => (
235
+ row.fresh === true || row.highValue === true || row.importance === 'high'
236
+ ));
237
+ } else {
238
+ selected = list.filter((row) => {
239
+ if (row.promoted === false) return false;
240
+ if (row.matchable === false) return false;
241
+ if (row.transportBlob === true) return false;
242
+ return true;
243
+ });
244
+ }
245
+ return {
246
+ policy: resolved,
247
+ keys: selected.map((row) => String(row.id || row.key || '')).filter(Boolean),
248
+ selected,
249
+ dropped: list.length - selected.length,
250
+ };
251
+ }
252
+
253
+ function auditTrace(trace = {}) {
254
+ const findings = [];
255
+ if (!trace || typeof trace !== 'object' || Array.isArray(trace)) {
256
+ findings.push({
257
+ severity: 'fail',
258
+ id: 'empty_trace',
259
+ message: 'Trace is missing or not an object. A fail-closed three-plane audit needs writes, reads, hotSubset.policy, and versions.',
260
+ });
261
+ return findings;
262
+ }
263
+
264
+ const writes = Array.isArray(trace.writes) ? trace.writes : null;
265
+ const reads = Array.isArray(trace.reads) ? trace.reads : null;
266
+ if (!writes || writes.length === 0) {
267
+ findings.push({
268
+ severity: 'fail',
269
+ id: 'missing_write_evidence',
270
+ message: 'Trace omits writes[]. Require durable, delivery, and hot-ingest evidence (or a coupled-write finding).',
271
+ });
272
+ }
273
+ if (!reads || reads.length === 0) {
274
+ findings.push({
275
+ severity: 'fail',
276
+ id: 'missing_read_evidence',
277
+ message: 'Trace omits reads[]. Query-time batch evidence is required before certifying the serving path.',
278
+ });
279
+ }
280
+ if (!trace.hotSubset || typeof trace.hotSubset !== 'object' || !trace.hotSubset.policy) {
281
+ findings.push({
282
+ severity: 'fail',
283
+ id: 'missing_subset_policy',
284
+ message: 'Trace omits hotSubset.policy. Unspecified hot sets are not a pass.',
285
+ });
286
+ }
287
+ if (!Array.isArray(trace.versions)) {
288
+ findings.push({
289
+ severity: 'fail',
290
+ id: 'missing_version_evidence',
291
+ message: 'Trace omits versions[]. Representation coexistence must be declared as an array.',
292
+ });
293
+ }
294
+
295
+ const writeList = writes || [];
296
+ const readList = reads || [];
297
+
298
+ for (const write of writeList) {
299
+ if (write.source === 'processing' && write.plane === 'hot') {
300
+ findings.push({
301
+ severity: 'fail',
302
+ id: 'coupled_processing_hot_write',
303
+ message: 'Processing wrote prepared records straight to the hot store. Durable state and Lorry-style delivery must sit in between.',
304
+ });
305
+ }
306
+ }
307
+
308
+ const processingDurable = writeList.some((w) => w.source === 'processing' && w.plane === 'durable');
309
+ const hasDelivery = writeList.some((w) => w.plane === 'delivery' || w.source === 'export');
310
+ const hotWrite = writeList.some((w) => w.plane === 'hot');
311
+ const coupled = writeList.some((w) => w.source === 'processing' && w.plane === 'hot');
312
+ if (processingDurable && hotWrite && !hasDelivery && !coupled) {
313
+ findings.push({
314
+ severity: 'fail',
315
+ id: 'missing_delivery_plane',
316
+ message: 'Durable writes jump to hot ingest with no batched delivery plane. Map that hop onto feedback-to-memory / dreaming=dynamic.',
317
+ });
318
+ }
319
+
320
+ for (const read of readList) {
321
+ const keys = normalizeKeyList(read.keys);
322
+ const mode = String(read.mode || '').toLowerCase();
323
+ if (keys.length >= 3 && mode === 'sequential') {
324
+ findings.push({
325
+ severity: 'fail',
326
+ id: 'sequential_hot_batch',
327
+ message: `Read ${keys.length} hot keys sequentially. Batch MultiGet (partition + parallel) is the serving contract.`,
328
+ });
329
+ }
330
+ if (keys.length >= 5 && mode === 'batched' && !(Number(read.hedgeMs) > 0)) {
331
+ findings.push({
332
+ severity: 'warn',
333
+ id: 'missing_hedge',
334
+ message: 'Batched hot read has no hedge timeout. A slow replica can stall the whole batch.',
335
+ });
336
+ }
337
+ }
338
+
339
+ const subset = trace.hotSubset && typeof trace.hotSubset === 'object' ? trace.hotSubset : {};
340
+ if (subset.policy === 'unbounded') {
341
+ findings.push({
342
+ severity: 'fail',
343
+ id: 'unbounded_hot_subset',
344
+ message: 'Hot set has no subset policy. Only promoted/matchable (or fresh/high-value) records belong on the expensive read path.',
345
+ });
346
+ }
347
+
348
+ const versions = Array.isArray(trace.versions) ? trace.versions : [];
349
+ for (const version of versions) {
350
+ const reps = Array.isArray(version.representations) ? version.representations : [];
351
+ if (version.overwrite && reps.length >= 1) {
352
+ findings.push({
353
+ severity: 'fail',
354
+ id: 'version_overwrite',
355
+ message: `Key ${version.key || '(unknown)'} overwrote an existing representation. Versions must coexist so embedding/chunk changes can roll forward.`,
356
+ });
357
+ }
358
+ }
359
+
360
+ if (trace.transactionsOnHot === true) {
361
+ findings.push({
362
+ severity: 'fail',
363
+ id: 'hot_transactions',
364
+ message: 'Hot store requested transactions/synchronized replicas. Omit them: a short ingest lag is acceptable on this path.',
365
+ });
366
+ }
367
+
368
+ return findings;
369
+ }
370
+
371
+ function probeRails(rootDir) {
372
+ const root = rootDir || path.resolve(__dirname, '..');
373
+ const checks = [
374
+ { id: 'feedback_to_memory', rel: 'scripts/feedback-to-memory.js' },
375
+ { id: 'lesson_retrieval', rel: 'scripts/lesson-retrieval.js' },
376
+ { id: 'hybrid_feedback_context', rel: 'scripts/hybrid-feedback-context.js' },
377
+ { id: 'memory_vs_rag', rel: 'scripts/memory-vs-rag-route.js' },
378
+ { id: 'perf_budgets', rel: 'config/performance-budgets.json' },
379
+ ];
380
+ return checks.map((c) => {
381
+ const full = path.join(root, c.rel);
382
+ return { id: c.id, path: c.rel, exists: fs.existsSync(full) };
383
+ });
384
+ }
385
+
386
+ function loadJsonFile(filePath) {
387
+ if (!filePath) return null;
388
+ const raw = fs.readFileSync(filePath, 'utf8');
389
+ return JSON.parse(raw);
390
+ }
391
+
392
+ function collectCloneHaystack(options = {}) {
393
+ const parts = [
394
+ options.task,
395
+ options.query,
396
+ options.clone,
397
+ options['clone-cobbledb'] ? 'clone cobbledb' : '',
398
+ options.cloneCobbleDb ? 'clone cobbledb' : '',
399
+ ];
400
+ if (Array.isArray(options.argv)) parts.push(...options.argv);
401
+ return parts.filter(Boolean).join(' ');
402
+ }
403
+
404
+ function buildCobbleHotStoreSplitReport(options = {}) {
405
+ const root = options.root
406
+ ? path.resolve(String(options.root))
407
+ : path.resolve(__dirname, '..');
408
+ const findings = [];
409
+ const haystack = collectCloneHaystack(options);
410
+ const cloneHits = [...new Set(detectCloneAttempt(haystack))];
411
+ if (cloneHits.length) {
412
+ findings.push({
413
+ severity: 'fail',
414
+ id: 'cobble_clone_refused',
415
+ message: `Refusing CobbleDB/RocksDB/YTsaurus clone path (${cloneHits.join(', ')}). Map the three-plane FORMAT onto existing lesson rails only.`,
416
+ });
417
+ }
418
+
419
+ const mapOnly = normalizeBoolean(options.map || options['map-only']);
420
+ let trace = null;
421
+ if (!mapOnly) {
422
+ if (options.trace && typeof options.trace === 'object' && !Array.isArray(options.trace)) {
423
+ trace = options.trace;
424
+ } else if (options.trace) {
425
+ trace = loadJsonFile(String(options.trace));
426
+ } else {
427
+ trace = DEFAULT_GOLD_TRACE;
428
+ }
429
+ findings.push(...auditTrace(trace));
430
+ }
431
+
432
+ const probes = probeRails(root);
433
+ for (const missing of probes.filter((p) => !p.exists)) {
434
+ findings.push({
435
+ severity: 'warn',
436
+ id: `missing_rail_${missing.id}`,
437
+ message: `Expected rail missing: ${missing.path}`,
438
+ });
439
+ }
440
+
441
+ let batchGet = null;
442
+ const keyList = normalizeKeyList(options.keys);
443
+ if (keyList.length) {
444
+ let store = options.store;
445
+ if (typeof store === 'string') store = loadJsonFile(store);
446
+ batchGet = batchGetPrepared(keyList, {
447
+ store,
448
+ hedgeMs: options.hedgeMs != null ? options.hedgeMs : options['hedge-ms'],
449
+ partitionCount: options.partitionCount || options['partition-count'],
450
+ replicaLatencies: options.replicaLatencies,
451
+ replicaOk: options.replicaOk,
452
+ });
453
+ }
454
+
455
+ let status = 'ready';
456
+ if (findings.some((f) => f.severity === 'fail')) status = 'fail';
457
+ else if (findings.some((f) => f.severity === 'warn')) status = 'ready_with_warnings';
458
+
459
+ const report = {
460
+ name: 'thumbgate-cobble-hot-store-split',
461
+ status,
462
+ ok: status !== 'fail',
463
+ planes: PLANES.map((id) => ({ id, ...PLANE_RAILS[id] })),
464
+ hotSubsetPolicies: HOT_SUBSET_POLICIES.filter((p) => p !== 'unbounded'),
465
+ trace: mapOnly ? null : trace,
466
+ batchGet,
467
+ railProbes: probes,
468
+ compareNotClone: true,
469
+ never: [
470
+ 'clone CobbleDB / Pillar / Lorry as a ThumbGate SKU',
471
+ 'vendor a RocksDB or YTsaurus cluster',
472
+ 'write processing output straight onto the PreToolUse hot path',
473
+ 'dump the raw feedback log into lesson-retrieval',
474
+ 'overwrite embedding/chunk versions',
475
+ 'dual-edit DIRTY lesson-graph PR #3650',
476
+ 'claim Perplexity 5× / 20% numbers as ThumbGate measurements',
477
+ ],
478
+ source: SOURCE_URL,
479
+ disclaimer: 'FORMAT steal only. Not affiliated with Perplexity or CobbleDB. Maps existing ThumbGate lesson rails; does not ship a key-value database.',
480
+ findings,
481
+ };
482
+
483
+ if (mapOnly) {
484
+ report.map = report.planes;
485
+ }
486
+
487
+ return report;
488
+ }
489
+
490
+ function formatCobbleHotStoreSplitReport(report) {
491
+ const lines = [
492
+ 'ThumbGate Cobble hot-store split (CobbleDB FORMAT steal)',
493
+ `Status : ${report.status}`,
494
+ `ok : ${report.ok}`,
495
+ 'Planes :',
496
+ ];
497
+ for (const plane of report.planes || []) {
498
+ lines.push(` [${plane.id}] ${plane.cobble}`);
499
+ lines.push(` rails=${(plane.rails || []).join(' · ')}`);
500
+ }
501
+ if (report.batchGet) {
502
+ lines.push(
503
+ `BatchGet : found=${report.batchGet.found}/${report.batchGet.keys.length}`
504
+ + ` hedged=${report.batchGet.hedgedPartitions}`
505
+ + ` hedgeMs=${report.batchGet.hedgeMs}`
506
+ );
507
+ }
508
+ if (report.findings?.length) {
509
+ lines.push('Findings:');
510
+ for (const finding of report.findings) {
511
+ lines.push(` [${finding.severity}] ${finding.id}: ${finding.message}`);
512
+ }
513
+ }
514
+ lines.push(`Never : ${(report.never || []).join('; ')}`);
515
+ lines.push(`Source : ${report.source}`);
516
+ lines.push(report.disclaimer);
517
+ return `${lines.join('\n')}\n`;
518
+ }
519
+
520
+ function printHelp() {
521
+ process.stdout.write(`Usage: node scripts/cobble-hot-store-split.js [options]
522
+
523
+ CobbleDB FORMAT steal — separate durable lesson state, batched promotion,
524
+ and query-time MultiGet. Does not clone CobbleDB / RocksDB / YTsaurus.
525
+
526
+ Options:
527
+ --trace=<file.json> Audit a write/read/subset/version trace
528
+ --keys=a,b,c Run partitioned MultiGet (+ hedge)
529
+ --store=<file.json> Prepared-record map for --keys
530
+ --hedge-ms=N Hedge timeout (default 15)
531
+ --partition-count=N Partition count (default 8)
532
+ --map-only Print three-plane rail map
533
+ --clone-cobbledb Fail closed (clone refused)
534
+ --json
535
+ --strict Exit 1 unless status=ready
536
+ --root=<dir>
537
+ `);
538
+ }
539
+
540
+ function parseArgv(argv) {
541
+ const options = { argv };
542
+ for (const arg of argv) {
543
+ if (arg === '--json') options.json = true;
544
+ else if (arg === '--strict') options.strict = true;
545
+ else if (arg === '--map' || arg === '--map-only') options.map = true;
546
+ else if (arg === '--clone-cobbledb' || arg === '--clone-cobble') {
547
+ options['clone-cobbledb'] = true;
548
+ } else if (arg.startsWith('--trace=')) options.trace = arg.slice('--trace='.length);
549
+ else if (arg.startsWith('--keys=')) options.keys = arg.slice('--keys='.length);
550
+ else if (arg.startsWith('--store=')) options.store = arg.slice('--store='.length);
551
+ else if (arg.startsWith('--hedge-ms=')) options.hedgeMs = Number(arg.slice('--hedge-ms='.length));
552
+ else if (arg.startsWith('--partition-count=')) {
553
+ options.partitionCount = Number(arg.slice('--partition-count='.length));
554
+ } else if (arg.startsWith('--root=')) options.root = arg.slice('--root='.length);
555
+ else if (arg.startsWith('--task=')) options.task = arg.slice('--task='.length);
556
+ else if (arg.startsWith('--query=')) options.query = arg.slice('--query='.length);
557
+ }
558
+ return options;
559
+ }
560
+
561
+ function main(argv = process.argv.slice(2)) {
562
+ if (argv.includes('--help') || argv.includes('-h')) {
563
+ printHelp();
564
+ return 0;
565
+ }
566
+ const options = parseArgv(argv);
567
+ const report = buildCobbleHotStoreSplitReport(options);
568
+ if (options.json) {
569
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
570
+ } else {
571
+ process.stdout.write(formatCobbleHotStoreSplitReport(report));
572
+ }
573
+ if (options.strict && report.status !== 'ready') return 1;
574
+ if (report.status === 'fail') return 1;
575
+ return 0;
576
+ }
577
+
578
+ module.exports = {
579
+ PLANES,
580
+ PLANE_RAILS,
581
+ HOT_SUBSET_POLICIES,
582
+ DEFAULT_GOLD_TRACE,
583
+ SOURCE_URL,
584
+ hashPartition,
585
+ batchGetPrepared,
586
+ selectHotSubset,
587
+ auditTrace,
588
+ detectCloneAttempt,
589
+ probeRails,
590
+ buildCobbleHotStoreSplitReport,
591
+ formatCobbleHotStoreSplitReport,
592
+ main,
593
+ };
594
+
595
+ if (
596
+ process.argv[1]
597
+ && path.resolve(process.argv[1]) === path.resolve(__filename)
598
+ ) {
599
+ process.exitCode = main();
600
+ }