claude-flow 3.10.4 → 3.10.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.
Files changed (58) hide show
  1. package/package.json +1 -1
  2. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-agent-planning.smoke.d.ts +18 -0
  3. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-agent-planning.smoke.js +253 -0
  4. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-agent.d.ts +198 -0
  5. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-agent.js +651 -0
  6. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-causal-memory.d.ts +133 -0
  7. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-causal-memory.js +281 -0
  8. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-causal-memory.smoke.d.ts +22 -0
  9. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-causal-memory.smoke.js +300 -0
  10. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-convergence.d.ts +138 -0
  11. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-convergence.js +260 -0
  12. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-convergence.smoke.d.ts +19 -0
  13. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-convergence.smoke.js +246 -0
  14. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-critic.d.ts +123 -0
  15. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-critic.js +312 -0
  16. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-critic.smoke.d.ts +21 -0
  17. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-critic.smoke.js +327 -0
  18. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-decomposer.d.ts +125 -0
  19. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-decomposer.js +350 -0
  20. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-decomposer.smoke.d.ts +21 -0
  21. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-decomposer.smoke.js +228 -0
  22. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-e2e-smoke.d.ts +27 -0
  23. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-e2e-smoke.js +136 -0
  24. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-extract.smoke.d.ts +45 -0
  25. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-extract.smoke.js +242 -0
  26. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/features.d.ts +46 -0
  27. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/features.js +170 -0
  28. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/predictor.d.ts +105 -0
  29. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/predictor.js +260 -0
  30. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/predictor.smoke.d.ts +20 -0
  31. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/predictor.smoke.js +235 -0
  32. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/train-data-loader.d.ts +51 -0
  33. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/train-data-loader.js +179 -0
  34. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-judge.d.ts +88 -0
  35. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-judge.js +437 -0
  36. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-loader.d.ts +87 -0
  37. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-loader.js +326 -0
  38. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/file_read.d.ts +35 -0
  39. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/file_read.js +403 -0
  40. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/grounded_query.d.ts +126 -0
  41. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/grounded_query.js +225 -0
  42. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/index.d.ts +32 -0
  43. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/index.js +36 -0
  44. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/types.d.ts +62 -0
  45. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/types.js +12 -0
  46. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/web_search.d.ts +30 -0
  47. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/web_search.js +210 -0
  48. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-voting.d.ts +88 -0
  49. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-voting.js +297 -0
  50. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-voting.smoke.d.ts +20 -0
  51. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-voting.smoke.js +332 -0
  52. package/v3/@claude-flow/cli/dist/src/commands/gaia-bench.d.ts +40 -0
  53. package/v3/@claude-flow/cli/dist/src/commands/gaia-bench.js +597 -0
  54. package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
  55. package/v3/@claude-flow/cli/dist/src/commands/init.js +41 -4
  56. package/v3/@claude-flow/cli/dist/src/init/executor.js +25 -6
  57. package/v3/@claude-flow/cli/dist/src/init/mcp-generator.js +9 -4
  58. package/v3/@claude-flow/cli/package.json +1 -1
@@ -0,0 +1,260 @@
1
+ /**
2
+ * GAIA Hardness Predictor — Linear Classifier (ADR-136 Track Q)
3
+ *
4
+ * Classifies GAIA questions into easy / medium / hard using a
5
+ * hand-rolled logistic regression (no external ML dependencies).
6
+ *
7
+ * Training:
8
+ * `predictor.train(labeledData)` — fits weights via gradient descent
9
+ * on cross-entropy loss using the 17-dim feature vectors.
10
+ *
11
+ * Inference:
12
+ * `predictor.predict(question)` — returns difficulty class + confidence
13
+ * + a ComputeBudget that drives model/turns/voting choices in gaia-bench.
14
+ *
15
+ * Cold-start:
16
+ * When untrained (weights = null), classifies everything as "medium".
17
+ * This is the correct safe default: no wasted Haiku-on-hard, no missed
18
+ * Sonnet-on-easy.
19
+ *
20
+ * Compute budget policy (from ADR-136 Track Q research):
21
+ * easy → Haiku, max 4 turns, 1 attempt
22
+ * medium → Sonnet, max 8 turns, 1 attempt
23
+ * hard → Sonnet, max 12 turns, 3-vote (Track A)
24
+ *
25
+ * Conservative threshold:
26
+ * If in doubt, classify UP (medium→hard preferred over medium→easy).
27
+ * `conservativeMode: true` (default) shifts the easy/medium boundary
28
+ * so fewer questions fall into "easy".
29
+ *
30
+ * Refs: ADR-136, ADR-135, #2156
31
+ */
32
+ import { extractFeatures } from './features.js';
33
+ // ---------------------------------------------------------------------------
34
+ // Compute budget policy
35
+ // ---------------------------------------------------------------------------
36
+ export const COMPUTE_BUDGETS = {
37
+ easy: {
38
+ model: 'haiku',
39
+ maxTurns: 4,
40
+ votingAttempts: 1,
41
+ },
42
+ medium: {
43
+ model: 'sonnet',
44
+ maxTurns: 8,
45
+ votingAttempts: 1,
46
+ },
47
+ hard: {
48
+ model: 'sonnet',
49
+ maxTurns: 12,
50
+ votingAttempts: 3,
51
+ },
52
+ };
53
+ // ---------------------------------------------------------------------------
54
+ // Internal constants
55
+ // ---------------------------------------------------------------------------
56
+ const FEATURE_DIM = 17;
57
+ const NUM_CLASSES = 3; // easy=0, medium=1, hard=2
58
+ const CLASS_NAMES = ['easy', 'medium', 'hard'];
59
+ // Learning-rate and regularisation tuned for ~300-example datasets
60
+ const LEARNING_RATE = 0.05;
61
+ const REGULARISATION_LAMBDA = 0.01;
62
+ const TRAINING_EPOCHS = 200;
63
+ // Conservative mode: shift the probability threshold so fewer questions
64
+ // are classified as "easy" (reduces risk of underpowering hard questions).
65
+ // With conservativeMode: the easy threshold is 0.6 (not 0.5).
66
+ const EASY_THRESHOLD_CONSERVATIVE = 0.55;
67
+ // ---------------------------------------------------------------------------
68
+ // Math helpers (pure functions, no deps)
69
+ // ---------------------------------------------------------------------------
70
+ function softmax(logits) {
71
+ const maxLogit = Math.max(...logits);
72
+ const exps = logits.map((l) => Math.exp(l - maxLogit));
73
+ const sumExps = exps.reduce((a, b) => a + b, 0);
74
+ return exps.map((e) => e / sumExps);
75
+ }
76
+ function dot(a, b) {
77
+ let sum = 0;
78
+ for (let i = 0; i < a.length; i++)
79
+ sum += a[i] * b[i];
80
+ return sum;
81
+ }
82
+ // ---------------------------------------------------------------------------
83
+ // HardnessPredictor
84
+ // ---------------------------------------------------------------------------
85
+ export class HardnessPredictor {
86
+ /**
87
+ * Weight matrix: weights[classIdx][featureIdx].
88
+ * null = untrained (cold-start: return medium for everything).
89
+ */
90
+ weights = null;
91
+ /** Bias terms per class. */
92
+ biases = null;
93
+ /** Whether conservative mode is active (default: true). */
94
+ conservativeMode;
95
+ constructor(options = {}) {
96
+ this.conservativeMode = options.conservativeMode ?? true;
97
+ }
98
+ /**
99
+ * Returns true when the predictor has been trained and is ready
100
+ * to make non-trivial predictions.
101
+ */
102
+ get isTrained() {
103
+ return this.weights !== null;
104
+ }
105
+ // ── Training ─────────────────────────────────────────────────────────────
106
+ /**
107
+ * Train the linear classifier using labelled examples from prior runs.
108
+ *
109
+ * Labelling strategy (weak supervision):
110
+ * - All correct + turns ≤ median turns → easy
111
+ * - All correct + turns > median turns → medium
112
+ * - Incorrect → hard
113
+ *
114
+ * With < 10 examples, refuses to train (cold-start is safer).
115
+ * With 10-50 examples, trains but sets `conservativeMode`-threshold high.
116
+ */
117
+ train(labeledData) {
118
+ if (labeledData.length < 10) {
119
+ // Too few examples for meaningful generalisation.
120
+ this.weights = null;
121
+ this.biases = null;
122
+ return;
123
+ }
124
+ // Derive labels using weak-supervision strategy.
125
+ const allTurns = labeledData
126
+ .filter((d) => d.turns !== undefined)
127
+ .map((d) => d.turns);
128
+ const medianTurns = allTurns.length > 0 ? median(allTurns) : 6;
129
+ const X = [];
130
+ const y = [];
131
+ for (const example of labeledData) {
132
+ const fv = extractFeatures(example.question);
133
+ X.push(fv.values);
134
+ let classIdx;
135
+ if (example.wasCorrect) {
136
+ const t = example.turns ?? medianTurns;
137
+ classIdx = t <= medianTurns ? 0 : 1; // easy=0, medium=1
138
+ }
139
+ else {
140
+ classIdx = 2; // hard
141
+ }
142
+ y.push(classIdx);
143
+ }
144
+ // Initialise weights to 0.
145
+ const W = Array.from({ length: NUM_CLASSES }, () => new Array(FEATURE_DIM).fill(0));
146
+ const b = new Array(NUM_CLASSES).fill(0);
147
+ // Mini-batch gradient descent (batch = full dataset for small sizes).
148
+ for (let epoch = 0; epoch < TRAINING_EPOCHS; epoch++) {
149
+ // Accumulate gradients.
150
+ const dW = Array.from({ length: NUM_CLASSES }, () => new Array(FEATURE_DIM).fill(0));
151
+ const db = new Array(NUM_CLASSES).fill(0);
152
+ for (let n = 0; n < X.length; n++) {
153
+ const x = X[n];
154
+ const trueClass = y[n];
155
+ // Compute logits and softmax probabilities.
156
+ const logits = W.map((w, k) => dot(w, x) + b[k]);
157
+ const probs = softmax(logits);
158
+ // Cross-entropy gradient for each class.
159
+ for (let k = 0; k < NUM_CLASSES; k++) {
160
+ const grad = probs[k] - (k === trueClass ? 1 : 0);
161
+ for (let f = 0; f < FEATURE_DIM; f++) {
162
+ dW[k][f] += grad * x[f];
163
+ }
164
+ db[k] += grad;
165
+ }
166
+ }
167
+ // Update weights with L2 regularisation.
168
+ const N = X.length;
169
+ for (let k = 0; k < NUM_CLASSES; k++) {
170
+ for (let f = 0; f < FEATURE_DIM; f++) {
171
+ W[k][f] -= LEARNING_RATE * (dW[k][f] / N + REGULARISATION_LAMBDA * W[k][f]);
172
+ }
173
+ b[k] -= LEARNING_RATE * (db[k] / N);
174
+ }
175
+ }
176
+ this.weights = W;
177
+ this.biases = b;
178
+ }
179
+ // ── Inference ─────────────────────────────────────────────────────────────
180
+ /**
181
+ * Predict the hardness class of a single GAIA question.
182
+ *
183
+ * Cold-start (untrained): returns medium with confidence=0.5.
184
+ */
185
+ predict(question) {
186
+ const features = extractFeatures(question);
187
+ if (!this.weights || !this.biases) {
188
+ // Cold-start fallback: medium for everything.
189
+ return {
190
+ difficulty: 'medium',
191
+ confidence: 0.5,
192
+ budget: COMPUTE_BUDGETS.medium,
193
+ features,
194
+ };
195
+ }
196
+ const logits = this.weights.map((w, k) => dot(w, features.values) + this.biases[k]);
197
+ const probs = softmax(logits);
198
+ // Conservative mode: down-weight easy probability.
199
+ let adjustedProbs = [...probs];
200
+ if (this.conservativeMode) {
201
+ // Transfer a fraction of easy probability to medium.
202
+ const easyExcess = Math.max(0, probs[0] - EASY_THRESHOLD_CONSERVATIVE);
203
+ adjustedProbs[0] = probs[0] - easyExcess;
204
+ adjustedProbs[1] = probs[1] + easyExcess;
205
+ }
206
+ // Re-normalise after adjustment.
207
+ const sumAdj = adjustedProbs.reduce((a, b) => a + b, 0);
208
+ adjustedProbs = adjustedProbs.map((p) => p / sumAdj);
209
+ // Pick argmax.
210
+ let bestClass = 0;
211
+ for (let k = 1; k < NUM_CLASSES; k++) {
212
+ if (adjustedProbs[k] > adjustedProbs[bestClass])
213
+ bestClass = k;
214
+ }
215
+ const difficulty = CLASS_NAMES[bestClass];
216
+ const confidence = adjustedProbs[bestClass];
217
+ return {
218
+ difficulty,
219
+ confidence,
220
+ budget: COMPUTE_BUDGETS[difficulty],
221
+ features,
222
+ };
223
+ }
224
+ // ── Serialisation ─────────────────────────────────────────────────────────
225
+ /**
226
+ * Export weights as a plain JSON-serialisable object.
227
+ * Returns null if untrained.
228
+ */
229
+ export() {
230
+ if (!this.weights || !this.biases)
231
+ return null;
232
+ return { weights: this.weights, biases: this.biases };
233
+ }
234
+ /**
235
+ * Import previously exported weights.
236
+ */
237
+ import(state) {
238
+ if (!Array.isArray(state.weights) ||
239
+ state.weights.length !== NUM_CLASSES ||
240
+ !Array.isArray(state.biases) ||
241
+ state.biases.length !== NUM_CLASSES) {
242
+ throw new Error(`Invalid weight state: expected ${NUM_CLASSES}×${FEATURE_DIM} matrix + ${NUM_CLASSES} biases`);
243
+ }
244
+ this.weights = state.weights.map((row) => [...row]);
245
+ this.biases = [...state.biases];
246
+ }
247
+ }
248
+ // ---------------------------------------------------------------------------
249
+ // Utility
250
+ // ---------------------------------------------------------------------------
251
+ function median(values) {
252
+ if (values.length === 0)
253
+ return 0;
254
+ const sorted = [...values].sort((a, b) => a - b);
255
+ const mid = Math.floor(sorted.length / 2);
256
+ return sorted.length % 2 === 0
257
+ ? (sorted[mid - 1] + sorted[mid]) / 2
258
+ : sorted[mid];
259
+ }
260
+ //# sourceMappingURL=predictor.js.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Smoke tests for gaia-hardness predictor — ADR-136 Track Q
3
+ *
4
+ * All tests are mock-based (no live API calls, no HF token, $0 cost).
5
+ *
6
+ * Test matrix:
7
+ * 1. Feature extraction: math question fires requiresMath + qword_calc
8
+ * 2. Feature extraction: simple factual question has low complexity signal
9
+ * 3. Feature extraction: question with file attachment fires has_file_attachment
10
+ * 4. Feature extraction: multi-hop question fires multi_hop_signal
11
+ * 5. Cold-start fallback: untrained predictor returns medium with confidence=0.5
12
+ * 6. Train on mock data: predictor becomes trained after ≥ 10 examples
13
+ * 7. Trained predictor: math-heavy question predicts harder than trivial question
14
+ * 8. Export/import: weights round-trip through JSON without loss
15
+ *
16
+ * Refs: ADR-136, #2156
17
+ */
18
+ declare function runSmoke(): Promise<void>;
19
+ export { runSmoke };
20
+ //# sourceMappingURL=predictor.smoke.d.ts.map
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Smoke tests for gaia-hardness predictor — ADR-136 Track Q
3
+ *
4
+ * All tests are mock-based (no live API calls, no HF token, $0 cost).
5
+ *
6
+ * Test matrix:
7
+ * 1. Feature extraction: math question fires requiresMath + qword_calc
8
+ * 2. Feature extraction: simple factual question has low complexity signal
9
+ * 3. Feature extraction: question with file attachment fires has_file_attachment
10
+ * 4. Feature extraction: multi-hop question fires multi_hop_signal
11
+ * 5. Cold-start fallback: untrained predictor returns medium with confidence=0.5
12
+ * 6. Train on mock data: predictor becomes trained after ≥ 10 examples
13
+ * 7. Trained predictor: math-heavy question predicts harder than trivial question
14
+ * 8. Export/import: weights round-trip through JSON without loss
15
+ *
16
+ * Refs: ADR-136, #2156
17
+ */
18
+ import assert from 'node:assert/strict';
19
+ import { extractFeatures, FEATURE_LABELS } from './features.js';
20
+ import { HardnessPredictor } from './predictor.js';
21
+ // ---------------------------------------------------------------------------
22
+ // Fixture helpers
23
+ // ---------------------------------------------------------------------------
24
+ function makeQuestion(text, overrides = {}) {
25
+ return {
26
+ task_id: 'smoke-test',
27
+ level: 1,
28
+ question: text,
29
+ final_answer: 'X',
30
+ file_name: null,
31
+ file_path: null,
32
+ ...overrides,
33
+ };
34
+ }
35
+ function labelIndex(label) {
36
+ return FEATURE_LABELS.indexOf(label);
37
+ }
38
+ // ---------------------------------------------------------------------------
39
+ // Test 1: Math question fires expected features
40
+ // ---------------------------------------------------------------------------
41
+ function testMathQuestionFeatures() {
42
+ const q = makeQuestion('What is 15 multiplied by 4? Calculate the exact product.');
43
+ const fv = extractFeatures(q);
44
+ const calcIdx = labelIndex('qword_calc_compute');
45
+ const mathIdx = labelIndex('requires_math');
46
+ const numIdx = labelIndex('has_numeric');
47
+ assert.ok(calcIdx >= 0, 'qword_calc_compute feature not found');
48
+ assert.ok(mathIdx >= 0, 'requires_math feature not found');
49
+ assert.ok(numIdx >= 0, 'has_numeric feature not found');
50
+ assert.equal(fv.values[numIdx], 1, 'has_numeric should be 1 for "15 multiplied by 4"');
51
+ // "Calculate" or "what" triggers calc compute or requires_math
52
+ const eitherMath = fv.values[calcIdx] === 1 || fv.values[mathIdx] === 1;
53
+ assert.ok(eitherMath, 'math question should fire qword_calc_compute or requires_math');
54
+ console.log(' [PASS] Test 1: math question fires expected features');
55
+ }
56
+ // ---------------------------------------------------------------------------
57
+ // Test 2: Simple factual question has low complexity
58
+ // ---------------------------------------------------------------------------
59
+ function testSimpleFactualQuestion() {
60
+ const q = makeQuestion('What is the capital of France?');
61
+ const fv = extractFeatures(q);
62
+ assert.equal(fv.values.length, 17, 'feature vector should have 17 dims');
63
+ const multiHopIdx = labelIndex('multi_hop_signal');
64
+ const toolIdx = labelIndex('tool_implication_norm');
65
+ const fileIdx = labelIndex('has_file_attachment');
66
+ assert.equal(fv.values[multiHopIdx], 0, 'simple question should have no multi-hop signal');
67
+ assert.equal(fv.values[toolIdx], 0, 'simple question should have no tool implication');
68
+ assert.equal(fv.values[fileIdx], 0, 'simple question should have no file attachment');
69
+ // Word count should be normalised well below 1
70
+ const wordNormIdx = labelIndex('len_words_norm');
71
+ assert.ok(fv.values[wordNormIdx] < 0.3, 'short question should have normalised word count < 0.3');
72
+ console.log(' [PASS] Test 2: simple factual question has low complexity signal');
73
+ }
74
+ // ---------------------------------------------------------------------------
75
+ // Test 3: File attachment fires has_file_attachment
76
+ // ---------------------------------------------------------------------------
77
+ function testFileAttachmentFeature() {
78
+ const q = makeQuestion('Analyse the attached PDF document and extract the table.', {
79
+ file_name: 'document.pdf',
80
+ file_path: '/tmp/document.pdf',
81
+ });
82
+ const fv = extractFeatures(q);
83
+ const fileIdx = labelIndex('has_file_attachment');
84
+ const toolIdx = labelIndex('tool_implication_norm');
85
+ assert.equal(fv.values[fileIdx], 1, 'question with file_name should have has_file_attachment=1');
86
+ assert.ok(fv.values[toolIdx] > 0, 'PDF question should have tool_implication > 0');
87
+ console.log(' [PASS] Test 3: file attachment fires has_file_attachment');
88
+ }
89
+ // ---------------------------------------------------------------------------
90
+ // Test 4: Multi-hop question fires multi_hop_signal
91
+ // ---------------------------------------------------------------------------
92
+ function testMultiHopFeature() {
93
+ const q = makeQuestion('Who was the president of the country that won the most gold medals at the Olympics that was held in the city that hosted the 1936 Games?');
94
+ const fv = extractFeatures(q);
95
+ const multiHopIdx = labelIndex('multi_hop_signal');
96
+ assert.equal(fv.values[multiHopIdx], 1, 'multi-hop question should fire multi_hop_signal');
97
+ console.log(' [PASS] Test 4: multi-hop question fires multi_hop_signal');
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // Test 5: Cold-start fallback returns medium with confidence=0.5
101
+ // ---------------------------------------------------------------------------
102
+ function testColdStartFallback() {
103
+ const predictor = new HardnessPredictor();
104
+ assert.equal(predictor.isTrained, false, 'new predictor should not be trained');
105
+ const q = makeQuestion('What is the capital of France?');
106
+ const result = predictor.predict(q);
107
+ assert.equal(result.difficulty, 'medium', 'cold-start should return medium');
108
+ assert.equal(result.confidence, 0.5, 'cold-start should return confidence=0.5');
109
+ assert.equal(result.budget.model, 'sonnet', 'medium budget should use sonnet');
110
+ assert.equal(result.budget.maxTurns, 8, 'medium budget should have maxTurns=8');
111
+ assert.equal(result.budget.votingAttempts, 1, 'medium budget should have votingAttempts=1');
112
+ console.log(' [PASS] Test 5: cold-start fallback returns medium with confidence=0.5');
113
+ }
114
+ // ---------------------------------------------------------------------------
115
+ // Test 6: Becomes trained after ≥ 10 labelled examples
116
+ // ---------------------------------------------------------------------------
117
+ function testBecomesTrainedWith10Examples() {
118
+ const predictor = new HardnessPredictor();
119
+ // Generate 15 mock examples (mix of correct/incorrect)
120
+ const examples = [];
121
+ for (let i = 0; i < 15; i++) {
122
+ examples.push({
123
+ question: makeQuestion(`Mock question number ${i} about topic ${i % 3}`),
124
+ wasCorrect: i % 3 !== 0,
125
+ turns: 3 + (i % 6),
126
+ });
127
+ }
128
+ assert.equal(predictor.isTrained, false, 'should not be trained before training');
129
+ predictor.train(examples);
130
+ assert.equal(predictor.isTrained, true, 'should be trained after ≥10 examples');
131
+ console.log(' [PASS] Test 6: predictor becomes trained after ≥ 10 examples');
132
+ }
133
+ // ---------------------------------------------------------------------------
134
+ // Test 7: Trained predictor scores math question harder than trivial question
135
+ // ---------------------------------------------------------------------------
136
+ function testTrainedRelativeHardness() {
137
+ const predictor = new HardnessPredictor({ conservativeMode: false });
138
+ // Train on 20 examples with clear pattern:
139
+ // - Short simple questions → correct (easy)
140
+ // - Long multi-step questions → incorrect (hard)
141
+ const examples = [];
142
+ for (let i = 0; i < 10; i++) {
143
+ examples.push({
144
+ question: makeQuestion(`What is the capital of country ${i}?`),
145
+ wasCorrect: true,
146
+ turns: 2,
147
+ });
148
+ examples.push({
149
+ question: makeQuestion(`Calculate the exact percentage difference between ${i * 100} and ${i * 100 + 50}, ` +
150
+ `then multiply by the number of years between 1990 and 2023. ` +
151
+ `Express as a decimal rounded to 3 places.`),
152
+ wasCorrect: false,
153
+ turns: 12,
154
+ });
155
+ }
156
+ predictor.train(examples);
157
+ const trivialQ = makeQuestion('What is the capital of France?');
158
+ const mathQ = makeQuestion('Calculate the exact percentage difference between 750 and 1250, ' +
159
+ 'then multiply by the number of years between 1990 and 2023. ' +
160
+ 'Express as a decimal rounded to 3 places.');
161
+ const trivialResult = predictor.predict(trivialQ);
162
+ const mathResult = predictor.predict(mathQ);
163
+ // The difficulty ordering should not put math as easier than trivial.
164
+ const ORDER = { easy: 0, medium: 1, hard: 2 };
165
+ assert.ok(ORDER[mathResult.difficulty] >= ORDER[trivialResult.difficulty], `math question (${mathResult.difficulty}) should be ≥ trivial (${trivialResult.difficulty})`);
166
+ console.log(` [PASS] Test 7: math="${mathResult.difficulty}" (conf=${mathResult.confidence.toFixed(2)}) ≥ trivial="${trivialResult.difficulty}" (conf=${trivialResult.confidence.toFixed(2)})`);
167
+ }
168
+ // ---------------------------------------------------------------------------
169
+ // Test 8: Weights round-trip through JSON export/import
170
+ // ---------------------------------------------------------------------------
171
+ function testWeightRoundTrip() {
172
+ const predictor = new HardnessPredictor();
173
+ const examples = [];
174
+ for (let i = 0; i < 12; i++) {
175
+ examples.push({
176
+ question: makeQuestion(`Question ${i}`),
177
+ wasCorrect: i % 2 === 0,
178
+ turns: 4 + i,
179
+ });
180
+ }
181
+ predictor.train(examples);
182
+ const exported = predictor.export();
183
+ assert.ok(exported !== null, 'exported state should not be null after training');
184
+ const predictor2 = new HardnessPredictor();
185
+ predictor2.import(exported);
186
+ assert.equal(predictor2.isTrained, true, 'imported predictor should be trained');
187
+ // Both predictors should give the same result for the same question.
188
+ const q = makeQuestion('How many sides does a hexagon have?');
189
+ const r1 = predictor.predict(q);
190
+ const r2 = predictor2.predict(q);
191
+ assert.equal(r1.difficulty, r2.difficulty, 'exported/imported predictor should give same difficulty');
192
+ assert.ok(Math.abs(r1.confidence - r2.confidence) < 0.001, 'exported/imported predictor should give same confidence');
193
+ console.log(' [PASS] Test 8: weights round-trip through export/import without loss');
194
+ }
195
+ // ---------------------------------------------------------------------------
196
+ // Runner
197
+ // ---------------------------------------------------------------------------
198
+ async function runSmoke() {
199
+ console.log('\n=== gaia-hardness predictor.smoke.ts — ADR-136 Track Q ===');
200
+ console.log('(mock-based, no live API, $0 cost)\n');
201
+ let passed = 0;
202
+ let total = 0;
203
+ function run(name, fn) {
204
+ total++;
205
+ try {
206
+ fn();
207
+ passed++;
208
+ }
209
+ catch (err) {
210
+ console.error(` [FAIL] ${name}: ${err instanceof Error ? err.message : String(err)}`);
211
+ }
212
+ }
213
+ run('Feature extraction: math question', testMathQuestionFeatures);
214
+ run('Feature extraction: simple factual', testSimpleFactualQuestion);
215
+ run('Feature extraction: file attachment', testFileAttachmentFeature);
216
+ run('Feature extraction: multi-hop signal', testMultiHopFeature);
217
+ run('Cold-start fallback: medium + confidence=0.5', testColdStartFallback);
218
+ run('Train: becomes trained with ≥10 examples', testBecomesTrainedWith10Examples);
219
+ run('Trained: math harder than trivial', testTrainedRelativeHardness);
220
+ run('Export/import: weights round-trip', testWeightRoundTrip);
221
+ console.log(`\n=== Summary: ${passed}/${total} tests passed ===`);
222
+ if (passed < total) {
223
+ process.exit(1);
224
+ }
225
+ }
226
+ // Run when invoked directly.
227
+ if (process.argv[1]?.endsWith('predictor.smoke.js') ||
228
+ process.argv[1]?.endsWith('predictor.smoke.ts')) {
229
+ runSmoke().catch((err) => {
230
+ console.error('Smoke crashed:', err);
231
+ process.exit(2);
232
+ });
233
+ }
234
+ export { runSmoke };
235
+ //# sourceMappingURL=predictor.smoke.js.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * GAIA Hardness Predictor — Training Data Loader (ADR-136 Track Q)
3
+ *
4
+ * Loads labelled training examples from prior bench-run result JSONs
5
+ * (iter-15, iter-23, iter-28 outputs) and converts them into the
6
+ * `LabeledExample[]` format consumed by `HardnessPredictor.train()`.
7
+ *
8
+ * Expected result JSON schema (matches gaia-bench --output json):
9
+ * {
10
+ * level: number,
11
+ * model: string,
12
+ * summary: { total, passed, passRate, estCostUsd, meanTurns, meanWallMs },
13
+ * results: [
14
+ * {
15
+ * task_id: string, question: string, model: string, correct: boolean,
16
+ * answer: string | null, expected_output: string, error?: string,
17
+ * turns?: number, wallMs?: number, inputTokens?: number, outputTokens?: number
18
+ * }
19
+ * ]
20
+ * }
21
+ *
22
+ * The file may contain either:
23
+ * (a) a single JSON object (one model run), or
24
+ * (b) a JSON array of objects (multi-model run from --models a,b,c), or
25
+ * (c) a text preamble followed by JSON (raw output from gaia-bench text mode
26
+ * — we scan for the first '[' or '{' and parse from there).
27
+ *
28
+ * Missing files are silently skipped (returns empty array).
29
+ * Malformed files emit a warning to stderr and are skipped.
30
+ *
31
+ * Default search paths (tried in order, first found wins per iter):
32
+ * /tmp/gaia-l1-full.json
33
+ * /tmp/gaia-l1-haiku.json
34
+ * /tmp/gaia-all-p1b.json
35
+ * /tmp/gaia-all-p2.json
36
+ * <custom paths passed by caller>
37
+ *
38
+ * Refs: ADR-136, #2156
39
+ */
40
+ import type { LabeledExample } from './predictor.js';
41
+ /** Default candidate paths for historical bench-run result JSONs. */
42
+ export declare const DEFAULT_RESULT_PATHS: readonly string[];
43
+ /**
44
+ * Load labelled training examples from historical bench-run result JSONs.
45
+ *
46
+ * @param additionalPaths - Extra file paths to scan beyond the defaults.
47
+ * @param verbose - If true, log loaded example counts to stderr.
48
+ * @returns Deduplicated array of LabeledExample (dedup by task_id, last write wins).
49
+ */
50
+ export declare function loadTrainingData(additionalPaths?: string[], verbose?: boolean): LabeledExample[];
51
+ //# sourceMappingURL=train-data-loader.d.ts.map