claude-flow 3.17.0 → 3.18.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.
@@ -15,99 +15,120 @@ import { getProjectCwd } from './types.js';
15
15
  import { validateIdentifier, validateText } from './validate-input.js';
16
16
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
17
17
  import { join } from 'node:path';
18
- // Try to import real embeddings.
19
- // Tier 0 (NEW, ADR-089): ruvector@0.2.27 bundled ONNX (no sharp dep, fixes ADR-086's
18
+ // Real embeddings resolved LAZILY on first use.
19
+ // Perf (measured 2026-07): the previous top-level-await version of this block
20
+ // ran `await import('ruvector')` + `initOnnxEmbedder()` + a probe embed at
21
+ // module-import time, adding ~450ms (warm) to ~2800ms (cold) to EVERY CLI
22
+ // command, because mcp-client statically imports this module. ensureEmbeddings()
23
+ // keeps the exact same tier chain and degraded/fallback semantics, but only
24
+ // pays the cost when a handler actually needs an embedding.
25
+ // Tier 0 (ADR-089): ruvector@0.2.27 bundled ONNX (no sharp dep, fixes ADR-086's
20
26
  // silent-fallback bug at source; closes the chain described in ruvnet/ruvector#523).
21
27
  // Tier 1: agentic-flow v3 ReasoningBank (was Tier 1 — broken on darwin-arm64 without sharp)
22
28
  // Tier 2-3: @claude-flow/embeddings
23
29
  let realEmbeddings = null;
24
30
  let embeddingServiceName = 'none';
25
- try {
26
- // Tier 0: ruvector@0.2.27 — bundled all-MiniLM-L6-v2 + parallel worker pool.
27
- // Probe with isOnnxAvailable() and verify an actual embed succeeds (avoids
28
- // the type-load-success-but-runtime-fails trap from ADR-086).
29
- // NOTE: ruvector's embed() returns `{embedding, dimension, timeMs}` we
30
- // unwrap to plain number[] for the shared interface.
31
- const rv = await import('ruvector').catch(() => null);
32
- if (rv?.embed && typeof rv.embed === 'function' && rv.isOnnxAvailable?.()) {
31
+ let embeddingsPromise = null;
32
+ /**
33
+ * Memoized lazy initialiser for the embedding provider chain. Safe to call
34
+ * concurrently (single shared promise). If every tier fails to import or
35
+ * probe, `realEmbeddings` stays null and callers use the explicit
36
+ * hash-fallback path identical degraded semantics to the old eager block.
37
+ */
38
+ function ensureEmbeddings() {
39
+ if (embeddingsPromise)
40
+ return embeddingsPromise;
41
+ embeddingsPromise = (async () => {
33
42
  try {
34
- if (typeof rv.initOnnxEmbedder === 'function')
35
- await rv.initOnnxEmbedder();
36
- const probe = await rv.embed('probe');
37
- // Handle both shapes: ruvector wraps as {embedding, dimension, timeMs};
38
- // some versions returned raw Float32Array.
39
- const probeVec = probe?.embedding ?? probe;
40
- if (probeVec && (Array.isArray(probeVec) || probeVec.length > 0)) {
41
- realEmbeddings = {
42
- embed: async (text) => {
43
- const r = await rv.embed(text);
44
- const v = r?.embedding ?? r;
45
- return Array.isArray(v) ? v : Array.from(v);
46
- },
47
- };
48
- embeddingServiceName = 'ruvector@0.2.27 (bundled all-MiniLM-L6-v2)';
49
- }
50
- }
51
- catch {
52
- // ruvector embed failed at runtime; fall through to next tier
53
- }
54
- }
55
- // Tier 1: agentic-flow v3 ReasoningBank (kept for backward-compat; may
56
- // silently fall back on darwin-arm64 without sharp — that's the bug
57
- // Tier 0 was added to bypass).
58
- if (!realEmbeddings) {
59
- const rb = await import('agentic-flow/reasoningbank').catch(() => null);
60
- if (rb?.computeEmbedding) {
61
- realEmbeddings = { embed: (text) => rb.computeEmbedding(text) };
62
- embeddingServiceName = 'agentic-flow/reasoningbank';
63
- }
64
- }
65
- // Tier 2: @claude-flow/embeddings with agentic-flow provider
66
- if (!realEmbeddings) {
67
- const embeddingsModule = await import('@claude-flow/embeddings').catch(() => null);
68
- if (embeddingsModule?.createEmbeddingService) {
69
- try {
70
- const service = embeddingsModule.createEmbeddingService({ provider: 'agentic-flow' });
71
- realEmbeddings = {
72
- embed: async (text) => {
73
- const result = await service.embed(text);
74
- return Array.from(result.embedding);
75
- },
76
- };
77
- embeddingServiceName = 'agentic-flow';
43
+ // Tier 0: ruvector@0.2.27 bundled all-MiniLM-L6-v2 + parallel worker pool.
44
+ // Probe with isOnnxAvailable() and verify an actual embed succeeds (avoids
45
+ // the type-load-success-but-runtime-fails trap from ADR-086). The probe now
46
+ // runs on first embed request instead of at import time.
47
+ // NOTE: ruvector's embed() returns `{embedding, dimension, timeMs}` — we
48
+ // unwrap to plain number[] for the shared interface.
49
+ const rv = await import('ruvector').catch(() => null);
50
+ if (rv?.embed && typeof rv.embed === 'function' && rv.isOnnxAvailable?.()) {
51
+ try {
52
+ if (typeof rv.initOnnxEmbedder === 'function')
53
+ await rv.initOnnxEmbedder();
54
+ const probe = await rv.embed('probe');
55
+ // Handle both shapes: ruvector wraps as {embedding, dimension, timeMs};
56
+ // some versions returned raw Float32Array.
57
+ const probeVec = probe?.embedding ?? probe;
58
+ if (probeVec && (Array.isArray(probeVec) || probeVec.length > 0)) {
59
+ realEmbeddings = {
60
+ embed: async (text) => {
61
+ const r = await rv.embed(text);
62
+ const v = r?.embedding ?? r;
63
+ return Array.isArray(v) ? v : Array.from(v);
64
+ },
65
+ };
66
+ embeddingServiceName = 'ruvector@0.2.27 (bundled all-MiniLM-L6-v2)';
67
+ }
68
+ }
69
+ catch {
70
+ // ruvector embed failed at runtime; fall through to next tier
71
+ }
78
72
  }
79
- catch {
80
- // agentic-flow provider not available, try ONNX
73
+ // Tier 1: agentic-flow v3 ReasoningBank (kept for backward-compat; may
74
+ // silently fall back on darwin-arm64 without sharp that's the bug
75
+ // Tier 0 was added to bypass).
76
+ if (!realEmbeddings) {
77
+ const rb = await import('agentic-flow/reasoningbank').catch(() => null);
78
+ if (rb?.computeEmbedding) {
79
+ realEmbeddings = { embed: (text) => rb.computeEmbedding(text) };
80
+ embeddingServiceName = 'agentic-flow/reasoningbank';
81
+ }
81
82
  }
82
- }
83
- }
84
- // Tier 3: @claude-flow/embeddings with ONNX provider
85
- if (!realEmbeddings) {
86
- const embeddingsModule = await import('@claude-flow/embeddings').catch(() => null);
87
- if (embeddingsModule?.createEmbeddingService) {
88
- try {
89
- const service = embeddingsModule.createEmbeddingService({ provider: 'onnx' });
90
- realEmbeddings = {
91
- embed: async (text) => {
92
- const result = await service.embed(text);
93
- return Array.from(result.embedding);
94
- },
95
- };
96
- embeddingServiceName = 'onnx';
83
+ // Tier 2: @claude-flow/embeddings with agentic-flow provider
84
+ if (!realEmbeddings) {
85
+ const embeddingsModule = await import('@claude-flow/embeddings').catch(() => null);
86
+ if (embeddingsModule?.createEmbeddingService) {
87
+ try {
88
+ const service = embeddingsModule.createEmbeddingService({ provider: 'agentic-flow' });
89
+ realEmbeddings = {
90
+ embed: async (text) => {
91
+ const result = await service.embed(text);
92
+ return Array.from(result.embedding);
93
+ },
94
+ };
95
+ embeddingServiceName = 'agentic-flow';
96
+ }
97
+ catch {
98
+ // agentic-flow provider not available, try ONNX
99
+ }
100
+ }
97
101
  }
98
- catch {
99
- // ONNX provider not available, fall through to mock
102
+ // Tier 3: @claude-flow/embeddings with ONNX provider
103
+ if (!realEmbeddings) {
104
+ const embeddingsModule = await import('@claude-flow/embeddings').catch(() => null);
105
+ if (embeddingsModule?.createEmbeddingService) {
106
+ try {
107
+ const service = embeddingsModule.createEmbeddingService({ provider: 'onnx' });
108
+ realEmbeddings = {
109
+ embed: async (text) => {
110
+ const result = await service.embed(text);
111
+ return Array.from(result.embedding);
112
+ },
113
+ };
114
+ embeddingServiceName = 'onnx';
115
+ }
116
+ catch {
117
+ // ONNX provider not available, fall through to mock
118
+ }
119
+ }
100
120
  }
121
+ // No Tier 4 mock fallback. If all real-embedder tiers fail to import or
122
+ // probe, leave realEmbeddings null and let downstream code use the
123
+ // explicit hash-fallback path with a clear _embeddingNote in stats.
124
+ // Silently substituting mock embeddings would hide a missing production
125
+ // dependency from callers — that's the bug ADR-086 was about.
101
126
  }
102
- }
103
- // No Tier 4 mock fallback. If all real-embedder tiers fail to import or
104
- // probe, leave realEmbeddings null and let downstream code use the
105
- // explicit hash-fallback path with a clear _embeddingNote in stats.
106
- // Silently substituting mock embeddings would hide a missing production
107
- // dependency from callers — that's the bug ADR-086 was about.
108
- }
109
- catch {
110
- // No embedding provider available, will use fallback
127
+ catch {
128
+ // No embedding provider available, will use fallback
129
+ }
130
+ })();
131
+ return embeddingsPromise;
111
132
  }
112
133
  // Storage paths
113
134
  const STORAGE_DIR = '.claude-flow';
@@ -173,8 +194,9 @@ export function getNeuralStoreStats() {
173
194
  export async function storeNeuralPatterns(items) {
174
195
  if (!items || items.length === 0)
175
196
  return { stored: 0, total: 0 };
176
- // realEmbeddings is initialised by the top-level IIFE in this module;
177
- // generateEmbedding() falls back to a hash-based embedding if it isn't.
197
+ // realEmbeddings is initialised lazily by ensureEmbeddings() (invoked from
198
+ // generateEmbedding()); it falls back to a hash-based embedding if no
199
+ // provider is available.
178
200
  const store = loadNeuralStore();
179
201
  let stored = 0;
180
202
  for (const item of items) {
@@ -200,6 +222,10 @@ export async function storeNeuralPatterns(items) {
200
222
  }
201
223
  // Generate embedding - uses real ML embeddings if available, falls back to deterministic hash
202
224
  async function generateEmbedding(text, dims = 384) {
225
+ // Lazily resolve the embedding provider on first real use (perf: keeps
226
+ // ONNX init off the CLI startup path — see ensureEmbeddings()).
227
+ if (text)
228
+ await ensureEmbeddings();
203
229
  // If real embeddings available and text provided, use them
204
230
  if (realEmbeddings && text) {
205
231
  try {
@@ -747,6 +773,9 @@ export const neuralTools = [
747
773
  if (!v.valid)
748
774
  return { success: false, error: v.error };
749
775
  }
776
+ // Resolve provider so embeddingProvider in the result matches the old
777
+ // eager-init reporting.
778
+ await ensureEmbeddings();
750
779
  const store = loadNeuralStore();
751
780
  const method = input.method || 'quantize';
752
781
  const targetReduction = input.targetSize || 0.5;
@@ -860,6 +889,9 @@ export const neuralTools = [
860
889
  if (!v.valid)
861
890
  return { success: false, error: v.error };
862
891
  }
892
+ // Resolve provider so _realEmbeddings/embeddingProvider report the same
893
+ // values the old eager-init version produced.
894
+ await ensureEmbeddings();
863
895
  const store = loadNeuralStore();
864
896
  if (input.modelId) {
865
897
  const model = store.models[input.modelId];
@@ -928,6 +960,9 @@ export const neuralTools = [
928
960
  if (!v.valid)
929
961
  return { success: false, error: v.error };
930
962
  }
963
+ // Resolve provider so embeddingProvider in the result matches the old
964
+ // eager-init reporting.
965
+ await ensureEmbeddings();
931
966
  const store = loadNeuralStore();
932
967
  const target = input.target || 'balanced';
933
968
  const patterns = Object.values(store.patterns);
@@ -14,6 +14,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
14
14
  import { homedir } from 'node:os';
15
15
  import { createRequire } from 'node:module';
16
16
  import { join } from 'node:path';
17
+ import { resolveTrainingBackend } from '../ruvector/lora-adapter.js';
17
18
  // ============================================================================
18
19
  // Persistence Configuration
19
20
  // ============================================================================
@@ -1006,15 +1007,29 @@ export function getIntelligenceStats() {
1006
1007
  loadRuvllmCoordinatorSync();
1007
1008
  }
1008
1009
  const ruvllmStats = ruvllmCoordinator?.stats?.() || null;
1009
- // Fetch cross-module stats for unified reporting
1010
+ // Fetch cross-module stats for unified reporting.
1011
+ //
1012
+ // #2549 — two prior defects here: `trainingBackend` was declared and
1013
+ // returned but never assigned (always 'unavailable'), and contrastive
1014
+ // availability was read ONLY from an in-process global that a fresh
1015
+ // read-only `neural status` process never populates. Both made the
1016
+ // native @ruvector/ruvllm path invisible even when installed. Backend
1017
+ // now comes from the lora-adapter's capability probe; the global still
1018
+ // wins when present because it carries live in-process session counts.
1010
1019
  let contrastiveTrainer = 'unavailable';
1011
1020
  let trainingBackend = 'unavailable';
1012
1021
  try {
1013
- // Synchronous check — contrastiveTrainer is module-level in sona-optimizer
1014
- // We read it via the SONAOptimizer singleton if available
1022
+ trainingBackend = resolveTrainingBackend();
1023
+ }
1024
+ catch { /* module absent — stay 'unavailable' */ }
1025
+ try {
1015
1026
  const sonaModule = globalThis.__claudeFlowSonaStats;
1016
- if (sonaModule) {
1017
- contrastiveTrainer = sonaModule._contrastiveTrainer || 'unavailable';
1027
+ if (sonaModule?._contrastiveTrainer) {
1028
+ contrastiveTrainer = sonaModule._contrastiveTrainer;
1029
+ }
1030
+ else if (trainingBackend === 'ruvllm') {
1031
+ // Module resolves but no in-process session — available, idle.
1032
+ contrastiveTrainer = 'available';
1018
1033
  }
1019
1034
  }
1020
1035
  catch { /* not available */ }
@@ -419,14 +419,26 @@ export declare function bridgeHealthCheck(dbPath?: string): Promise<{
419
419
  *
420
420
  * Real HierarchicalMemory API (agentdb alpha.10+):
421
421
  * store(content, importance?, tier?, options?) → Promise<string>
422
- * Stub API (fallback):
423
- * store(key, value, tier) — synchronous
422
+ * Fallback API (@claude-flow/memory TieredMemoryStore):
423
+ * store(key, value, tier, temporalOptions?) — synchronous, returns
424
+ * { id, key, tier, superseded? }
425
+ *
426
+ * Temporal validity (Zep/Graphiti-style, impl/memory-sota):
427
+ * - validFrom / validUntil (ISO) travel with the entry.
428
+ * - supersedes=<entryId|key> INVALIDATES the old entry (validUntil=now +
429
+ * supersededBy=newId) instead of deleting it. Natively supported by the
430
+ * TieredMemoryStore fallback; on the real agentdb HierarchicalMemory the
431
+ * temporal fields are stored in metadata, and supersede is reported as
432
+ * unsupported (no public update API) rather than silently dropped.
424
433
  */
425
434
  export declare function bridgeHierarchicalStore(params: {
426
435
  key: string;
427
436
  value: string;
428
437
  tier?: string;
429
438
  importance?: number;
439
+ validFrom?: string;
440
+ validUntil?: string;
441
+ supersedes?: string;
430
442
  }): Promise<any>;
431
443
  /**
432
444
  * Recall from hierarchical memory.
@@ -441,6 +453,7 @@ export declare function bridgeHierarchicalRecall(params: {
441
453
  query: string;
442
454
  tier?: string;
443
455
  topK?: number;
456
+ includeExpired?: boolean;
444
457
  }): Promise<any>;
445
458
  /**
446
459
  * Run memory consolidation.
@@ -1970,8 +1970,17 @@ export async function bridgeHealthCheck(dbPath) {
1970
1970
  *
1971
1971
  * Real HierarchicalMemory API (agentdb alpha.10+):
1972
1972
  * store(content, importance?, tier?, options?) → Promise<string>
1973
- * Stub API (fallback):
1974
- * store(key, value, tier) — synchronous
1973
+ * Fallback API (@claude-flow/memory TieredMemoryStore):
1974
+ * store(key, value, tier, temporalOptions?) — synchronous, returns
1975
+ * { id, key, tier, superseded? }
1976
+ *
1977
+ * Temporal validity (Zep/Graphiti-style, impl/memory-sota):
1978
+ * - validFrom / validUntil (ISO) travel with the entry.
1979
+ * - supersedes=<entryId|key> INVALIDATES the old entry (validUntil=now +
1980
+ * supersededBy=newId) instead of deleting it. Natively supported by the
1981
+ * TieredMemoryStore fallback; on the real agentdb HierarchicalMemory the
1982
+ * temporal fields are stored in metadata, and supersede is reported as
1983
+ * unsupported (no public update API) rather than silently dropped.
1975
1984
  */
1976
1985
  export async function bridgeHierarchicalStore(params) {
1977
1986
  const registry = await getRegistry();
@@ -1984,15 +1993,36 @@ export async function bridgeHierarchicalStore(params) {
1984
1993
  const tier = params.tier || 'working';
1985
1994
  // Detect real HierarchicalMemory (has async store returning id) vs stub
1986
1995
  if (typeof hm.getStats === 'function' && typeof hm.promote === 'function') {
1987
- // Real agentdb HierarchicalMemory
1996
+ // Real agentdb HierarchicalMemory — temporal fields ride in metadata
1997
+ // so bridgeHierarchicalRecall can filter on them.
1998
+ const metadata = { key: params.key };
1999
+ if (params.validFrom)
2000
+ metadata.validFrom = params.validFrom;
2001
+ if (params.validUntil)
2002
+ metadata.validUntil = params.validUntil;
1988
2003
  const id = await hm.store(params.value, params.importance || 0.5, tier, {
1989
- metadata: { key: params.key },
2004
+ metadata,
1990
2005
  tags: [params.key],
1991
2006
  });
1992
- return { success: true, id, key: params.key, tier };
2007
+ const result = { success: true, id, key: params.key, tier };
2008
+ if (params.supersedes) {
2009
+ // No public update/invalidate API on agentdb HierarchicalMemory —
2010
+ // surface the limitation honestly instead of silently dropping it.
2011
+ result.superseded = null;
2012
+ result.warning = 'supersedes is not supported by the native agentdb HierarchicalMemory backend (no update API); the referenced entry was left untouched';
2013
+ }
2014
+ return result;
2015
+ }
2016
+ // TieredMemoryStore fallback (temporal-aware) / legacy stub
2017
+ const storeResult = hm.store(params.key, params.value, tier, {
2018
+ validFrom: params.validFrom,
2019
+ validUntil: params.validUntil,
2020
+ supersedes: params.supersedes,
2021
+ });
2022
+ if (storeResult && typeof storeResult === 'object') {
2023
+ return { success: true, ...storeResult };
1993
2024
  }
1994
- // Stub fallback
1995
- hm.store(params.key, params.value, tier);
2025
+ // Legacy stub (returns void) — temporal options were ignored
1996
2026
  return { success: true, key: params.key, tier };
1997
2027
  }
1998
2028
  catch (e) {
@@ -2027,10 +2057,34 @@ export async function bridgeHierarchicalRecall(params) {
2027
2057
  memoryQuery.tier = params.tier;
2028
2058
  }
2029
2059
  const results = await hm.recall(memoryQuery);
2030
- return { results: results || [], controller: 'hierarchicalMemory' };
2060
+ // Temporal filter over metadata (validFrom/validUntil stamped by
2061
+ // bridgeHierarchicalStore). Entries without the fields are always
2062
+ // valid — legacy behavior unchanged.
2063
+ const filtered = params.includeExpired
2064
+ ? (results || [])
2065
+ : (results || []).filter((r) => {
2066
+ const meta = r?.metadata || {};
2067
+ const now = Date.now();
2068
+ if (meta.validFrom) {
2069
+ const from = Date.parse(String(meta.validFrom));
2070
+ if (!Number.isNaN(from) && from > now)
2071
+ return false;
2072
+ }
2073
+ if (meta.validUntil) {
2074
+ const until = Date.parse(String(meta.validUntil));
2075
+ if (!Number.isNaN(until) && until <= now)
2076
+ return false;
2077
+ }
2078
+ return true;
2079
+ });
2080
+ return { results: filtered, controller: 'hierarchicalMemory' };
2031
2081
  }
2032
- // Stub fallback — recall(string, number)
2033
- const results = hm.recall(params.query, params.topK || 5);
2082
+ // TieredMemoryStore fallback — recall(string, number, options?).
2083
+ // Temporal filtering happens inside the store; the legacy stub simply
2084
+ // ignores the extra argument.
2085
+ const results = hm.recall(params.query, params.topK || 5, {
2086
+ includeExpired: params.includeExpired === true,
2087
+ });
2034
2088
  const filtered = params.tier
2035
2089
  ? results.filter((r) => r.tier === params.tier)
2036
2090
  : results;
@@ -97,6 +97,15 @@ export interface LoRAStats {
97
97
  /** Training backend in use ('ruvllm' | 'js-fallback') */
98
98
  _trainingBackend?: string;
99
99
  }
100
+ /**
101
+ * Which training backend a train call would use — WITHOUT loading the
102
+ * pipeline (#2549). Before this existed, status surfaces read module state
103
+ * that only a prior in-process train populates, so a fresh read-only
104
+ * process always reported 'js-fallback'/'unavailable' even with
105
+ * @ruvector/ruvllm installed. The pipeline stays lazy: this only probes
106
+ * module resolution.
107
+ */
108
+ export declare function resolveTrainingBackend(): 'ruvllm' | 'js-fallback';
100
109
  /**
101
110
  * Low-Rank Adaptation module for efficient embedding fine-tuning
102
111
  */
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
21
21
  import { dirname, join } from 'path';
22
+ import { createRequire } from 'module';
22
23
  // ============================================================================
23
24
  // Types & Constants
24
25
  // ============================================================================
@@ -57,6 +58,25 @@ const DEFAULT_CONFIG = {
57
58
  // ============================================================================
58
59
  let ruvllmPipeline = null;
59
60
  let pipelineLoaded = false;
61
+ /**
62
+ * Which training backend a train call would use — WITHOUT loading the
63
+ * pipeline (#2549). Before this existed, status surfaces read module state
64
+ * that only a prior in-process train populates, so a fresh read-only
65
+ * process always reported 'js-fallback'/'unavailable' even with
66
+ * @ruvector/ruvllm installed. The pipeline stays lazy: this only probes
67
+ * module resolution.
68
+ */
69
+ export function resolveTrainingBackend() {
70
+ if (pipelineLoaded)
71
+ return ruvllmPipeline ? 'ruvllm' : 'js-fallback';
72
+ try {
73
+ createRequire(import.meta.url).resolve('@ruvector/ruvllm');
74
+ return 'ruvllm';
75
+ }
76
+ catch {
77
+ return 'js-fallback';
78
+ }
79
+ }
60
80
  async function loadTrainingPipeline(adapter) {
61
81
  if (pipelineLoaded)
62
82
  return ruvllmPipeline;
@@ -295,9 +315,7 @@ export class LoRAAdapter {
295
315
  ? this.adaptationNormSum / this.totalAdaptations
296
316
  : 0,
297
317
  lastUpdate: this.lastUpdate,
298
- _trainingBackend: pipelineLoaded
299
- ? (ruvllmPipeline ? 'ruvllm' : 'js-fallback')
300
- : 'js-fallback',
318
+ _trainingBackend: resolveTrainingBackend(),
301
319
  };
302
320
  }
303
321
  /**
@@ -0,0 +1,83 @@
1
+ /**
2
+ * output-verifier.ts — Confidence-gated tier escalation (post-generation).
3
+ *
4
+ * ADR-026/143 route tasks to a tier BEFORE generation. 2026 SOTA cascade
5
+ * routing adds a post-generation gate: attempt the cheap tier, run a CHEAP
6
+ * verifier over the produced output, and escalate to the next tier only when
7
+ * the verifier is not confident. This module is that verifier.
8
+ *
9
+ * Design constraints (load-bearing):
10
+ * - $0 by default — NO LLM call. Every signal is a structural / lexical
11
+ * check: emptiness, refusal patterns, truncation, delimiter balance,
12
+ * degenerate repetition, and (for code tasks) a real syntax parse via the
13
+ * TypeScript compiler (lazy-imported; degrades to delimiter checks when
14
+ * typescript is not installed) or JSON.parse for JSON output.
15
+ * - Pure with respect to router state — recording the verdict into the
16
+ * bandit's learning stream is the CALLER's job (the hooks_model-verify
17
+ * MCP tool does it via ModelRouter.recordOutcome), keeping this module
18
+ * trivially unit-testable.
19
+ * - Escalation ladder mirrors the tier table: tier 2 (haiku) → tier 3
20
+ * (sonnet), sonnet → opus, opus has no bump (escalate=false even when the
21
+ * verdict is not confident — the caller should retry or surface instead).
22
+ *
23
+ * Verdict semantics: `confident=false` means "cheap signals say this output
24
+ * is likely unusable"; it is NOT a semantic-quality judgment. False
25
+ * negatives (bad output that parses fine) are expected — this gate trades
26
+ * recall for being free.
27
+ *
28
+ * @module ruvector/output-verifier
29
+ */
30
+ export type VerifyTier = 1 | 2 | 3;
31
+ export type VerifyModel = 'haiku' | 'sonnet' | 'opus';
32
+ export type VerifyTaskKind = 'code' | 'json' | 'text' | 'auto';
33
+ export interface VerifyInput {
34
+ /** The task the output was generated for. */
35
+ task: string;
36
+ /** The generated output to verify. */
37
+ output: string;
38
+ /** Model that produced the output (drives the escalation ladder). */
39
+ model?: VerifyModel;
40
+ /** Tier that produced the output; derived from `model` when absent. */
41
+ tierUsed?: VerifyTier;
42
+ /** Force the task kind; 'auto' (default) detects from task + output. */
43
+ taskKind?: VerifyTaskKind;
44
+ /** Minimum trimmed output length considered plausible (default 20). */
45
+ minLength?: number;
46
+ }
47
+ export interface VerifySignal {
48
+ name: string;
49
+ ok: boolean;
50
+ detail?: string;
51
+ }
52
+ export interface VerifyVerdict {
53
+ confident: boolean;
54
+ /** Fraction of signals that passed (0..1). */
55
+ score: number;
56
+ /** Human-readable failure reasons; empty when confident. */
57
+ reasons: string[];
58
+ /** Every signal evaluated, pass or fail. */
59
+ signals: VerifySignal[];
60
+ /** Tier the caller should use next: unchanged when confident, bumped 2→3 on failure. */
61
+ suggestedTier: VerifyTier;
62
+ /** Concrete next model on the ladder (haiku→sonnet→opus); null when no bump exists. */
63
+ suggestedModel: VerifyModel | null;
64
+ /** True when not confident AND a higher tier exists to escalate to. */
65
+ escalate: boolean;
66
+ /** Detected task kind after 'auto' resolution. */
67
+ taskKind: Exclude<VerifyTaskKind, 'auto'>;
68
+ }
69
+ interface ExtractedCode {
70
+ lang: string;
71
+ code: string;
72
+ }
73
+ /** Pull fenced code blocks out of markdown-ish output. */
74
+ export declare function extractCodeBlocks(output: string): ExtractedCode[];
75
+ /** Cheap stack-based bracket balance check that skips string/comment-ish content. */
76
+ export declare function bracketsBalanced(code: string): boolean;
77
+ /**
78
+ * Compute a confidence verdict for `output` from cheap structural signals,
79
+ * and suggest an escalation target when not confident.
80
+ */
81
+ export declare function verifyAndEscalate(input: VerifyInput): Promise<VerifyVerdict>;
82
+ export default verifyAndEscalate;
83
+ //# sourceMappingURL=output-verifier.d.ts.map