079project 8.0.0 → 9.1.0

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.
@@ -0,0 +1,720 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn } = require('child_process');
6
+
7
+ const DEFAULT_TEST = path.join(__dirname, 'test_automatic.cjs');
8
+ const DEFAULT_MAIN_MODULE = path.join(__dirname, 'main.cjs');
9
+
10
+ const nowStamp = () => {
11
+ const d = new Date();
12
+ const pad = (n) => String(n).padStart(2, '0');
13
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
14
+ };
15
+
16
+ const ensureDir = (dir) => {
17
+ if (!dir) return;
18
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
19
+ };
20
+
21
+ const parseArgs = (argv) => {
22
+ const out = { _: [], mainArgs: [] };
23
+ for (const item of argv) {
24
+ if (item === '-h' || item === '--help') {
25
+ out.help = true;
26
+ continue;
27
+ }
28
+ if (!item.startsWith('--')) {
29
+ out._.push(item);
30
+ continue;
31
+ }
32
+ const eq = item.indexOf('=');
33
+ const key = eq === -1 ? item.slice(2) : item.slice(2, eq);
34
+ const value = eq === -1 ? true : item.slice(eq + 1);
35
+
36
+ if (key === 'main-arg') {
37
+ // allow repeated --main-arg=--foo=bar
38
+ out.mainArgs.push(String(value));
39
+ continue;
40
+ }
41
+
42
+ if (Object.prototype.hasOwnProperty.call(out, key)) {
43
+ // allow repeated keys by array
44
+ if (Array.isArray(out[key])) out[key].push(value);
45
+ else out[key] = [out[key], value];
46
+ } else {
47
+ out[key] = value;
48
+ }
49
+ }
50
+ return out;
51
+ };
52
+
53
+ const printHelp = ({ mainDefault, testDefault } = {}) => {
54
+ const mainPath = mainDefault || DEFAULT_MAIN_MODULE;
55
+ const testPath = testDefault || DEFAULT_TEST;
56
+ const lines = [
57
+ 'Usage:',
58
+ ` node optimization.cjs [options]`,
59
+ '',
60
+ 'Quick start (recommended):',
61
+ ` node optimization.cjs --iters=12 --robots-limit=10000000 --params=decayK,iteration,memeNgramMin,memeNgramMax,minOverlapThreshold,maxMemeWords`,
62
+ '',
63
+ 'How to choose command-line parameters:',
64
+ ' 1) --params: choose which numeric model params to optimize.',
65
+ ' - Valid keys come ONLY from main.cjs exported MODEL_DEFAULTS (no invented keys).',
66
+ ' - Example: --params=decayK,iteration,memeNgramMin,memeNgramMax,minOverlapThreshold,maxMemeWords',
67
+ ' 2) --init-json: set your starting point (full/partial) before optimization.',
68
+ ' - Example: --init-json={"decayK":1.2,"iteration":6}',
69
+ ' 3) --cats: choose categorical params to sweep occasionally (if supported by main exports).',
70
+ ' - Example: --cats=activationType,transferType',
71
+ ' 4) Keep eval comparable by default: learning is frozen unless you pass --allow-learning.',
72
+ ' 5) Use --main-arg to forward CLI args to test_automatic/main if needed (repeatable).',
73
+ ' 6) Deterministic control-variable search:',
74
+ ' - Numeric params are scanned in the exact order of --params.',
75
+ ' - Each numeric param is swept on a grid (float step defaults to 0.1).',
76
+ ' - Categorical params (e.g. activationType) are tried in order, and for each choice we run a numeric sweep.',
77
+ '',
78
+ 'Options:',
79
+ ` -h, --help Show this help and exit`,
80
+ ` --test=FILE Path to evaluator (default: ${testPath})`,
81
+ ` --main=FILE Path to main module that exports MODEL_DEFAULTS (default: ${mainPath})`,
82
+ ` --robots-limit=N Forwarded to evaluator as --robots-limit (default: 10000000)`,
83
+ ` --host=HOST Evaluator target host (optional)`,
84
+ ` --port=PORT Evaluator target port (optional)`,
85
+ ` --pass=SCORE Pass threshold forwarded to evaluator as --pass (optional)`,
86
+ ` --timeout=MS Kill eval process after timeout (default: 1200000)`,
87
+ ` --allow-learning=true Allow learning during eval (default: frozen/disabled learning)`,
88
+ ` --main-arg=--k=v Forward arg (repeatable). Example: --main-arg=--disable-adv=true`,
89
+ ` --grid=FLOAT Grid step for float params (default: 0.1)`,
90
+ ` --grid-max=N Max grid points per param (default: 200; step auto-expands if range too wide)`,
91
+ '',
92
+ 'Optimization settings:',
93
+ ` --iters=N Passes over all params (default: 12)`,
94
+ ` --seed=N RNG seed (kept for logging/compat) (default: 1337)`,
95
+ ` --categorical-every=N Try categorical vars on iter=1 and then every N iters (default: 2; 0 disables)`,
96
+ '',
97
+ 'Logging:',
98
+ ` --log-dir=DIR Output directory (default: ./optimization_logs)`,
99
+ ` --run=ID Run id (default: timestamp)`,
100
+ '',
101
+ 'Notes:',
102
+ ' - This script evaluates by spawning the evaluator (test_automatic.cjs).',
103
+ ' - All model params are applied via evaluator feature (--model.* -> /api/model/params).'
104
+ ];
105
+ console.log(lines.join('\n'));
106
+ };
107
+
108
+ const coerceScalar = (value) => {
109
+ if (value === true) return true;
110
+ if (value === false) return false;
111
+ if (value == null) return value;
112
+ const s = String(value).trim();
113
+ if (!s) return s;
114
+ const low = s.toLowerCase();
115
+ if (low === 'true' || low === 'on' || low === 'yes') return true;
116
+ if (low === 'false' || low === 'off' || low === 'no') return false;
117
+ if (/^-?\d+(?:\.\d+)?$/.test(s)) return Number(s);
118
+ return s;
119
+ };
120
+
121
+ const clamp = (x, min, max) => {
122
+ if (!Number.isFinite(x)) return min;
123
+ return Math.max(min, Math.min(max, x));
124
+ };
125
+
126
+ const coerceNumber = (v, fallback) => {
127
+ const n = Number(v);
128
+ return Number.isFinite(n) ? n : fallback;
129
+ };
130
+
131
+ // Build a deterministic grid of candidate values for one numeric param.
132
+ // For very wide ranges, the step will auto-expand to keep total points <= maxPoints.
133
+ const listGridValues = (spec, { step, maxPoints } = {}) => {
134
+ const min = Number(spec?.min);
135
+ const max = Number(spec?.max);
136
+ if (!Number.isFinite(min) || !Number.isFinite(max) || max < min) return [];
137
+
138
+ const isInt = Boolean(spec?.isInt);
139
+ let s = Number(step);
140
+ if (!Number.isFinite(s) || s <= 0) s = isInt ? 1 : 0.1;
141
+
142
+ const cap = Math.max(20, Math.floor(coerceNumber(maxPoints, 200)));
143
+
144
+ if (isInt) {
145
+ const stepInt = Math.max(1, Math.round(s));
146
+ const out = [];
147
+ for (let v = Math.ceil(min); v <= Math.floor(max); v += stepInt) {
148
+ out.push(v);
149
+ if (out.length >= cap) break;
150
+ }
151
+ const maxInt = Math.floor(max);
152
+ if (out.length && out[out.length - 1] !== maxInt && out.length < cap) out.push(maxInt);
153
+ return out;
154
+ }
155
+
156
+ // Float grid
157
+ const rough = Math.floor((max - min) / s) + 1;
158
+ if (rough > cap) {
159
+ s = (max - min) / (cap - 1);
160
+ }
161
+
162
+ const out = [];
163
+ let v = min;
164
+ for (let i = 0; i < cap; i++) {
165
+ const vv = Math.min(max, Math.max(min, Number(v.toFixed(10))));
166
+ out.push(vv);
167
+ if (vv >= max) break;
168
+ v += s;
169
+ }
170
+ if (out.length && out[out.length - 1] !== max && out.length < cap) out.push(max);
171
+ return out;
172
+ };
173
+
174
+ const deepClone = (obj) => JSON.parse(JSON.stringify(obj || {}));
175
+
176
+ const stableStringify = (obj) => {
177
+ const seen = new WeakSet();
178
+ const sortKeys = (v) => {
179
+ if (!v || typeof v !== 'object') return v;
180
+ if (seen.has(v)) return null;
181
+ seen.add(v);
182
+ if (Array.isArray(v)) return v.map(sortKeys);
183
+ const keys = Object.keys(v).sort();
184
+ const out = {};
185
+ for (const k of keys) out[k] = sortKeys(v[k]);
186
+ return out;
187
+ };
188
+ return JSON.stringify(sortKeys(obj), null, 2);
189
+ };
190
+
191
+ const pickRandom = (arr, k, rng) => {
192
+ const a = arr.slice();
193
+ for (let i = a.length - 1; i > 0; i--) {
194
+ const j = Math.floor(rng() * (i + 1));
195
+ [a[i], a[j]] = [a[j], a[i]];
196
+ }
197
+ return a.slice(0, Math.max(0, Math.min(k, a.length)));
198
+ };
199
+
200
+ const makeRng = (seed) => {
201
+ // xorshift32
202
+ let x = (seed >>> 0) || 0x9e3779b9;
203
+ return () => {
204
+ x ^= x << 13;
205
+ x ^= x >>> 17;
206
+ x ^= x << 5;
207
+ return ((x >>> 0) / 0xffffffff);
208
+ };
209
+ };
210
+
211
+ const parseSummary = (combinedOutput) => {
212
+ const text = String(combinedOutput || '');
213
+ const mScore = text.match(/\[test_automatic\]\s+avgScore=([0-9.]+)/i);
214
+ const mPass = text.match(/\[test_automatic\]\s+avgScore=[0-9.]+\s+pass=(\d+)\/(\d+)/i);
215
+ const score = mScore ? Number(mScore[1]) : NaN;
216
+ const passed = mPass ? Number(mPass[1]) : NaN;
217
+ const total = mPass ? Number(mPass[2]) : NaN;
218
+ return {
219
+ avgScore: Number.isFinite(score) ? score : null,
220
+ passed: Number.isFinite(passed) ? passed : null,
221
+ total: Number.isFinite(total) ? total : null
222
+ };
223
+ };
224
+
225
+ const runEval = (testFile, {
226
+ robotsLimit,
227
+ frozen,
228
+ forwardMainArgs,
229
+ modelParams,
230
+ passThreshold,
231
+ host,
232
+ port,
233
+ timeoutMs
234
+ }) => {
235
+ return new Promise((resolve) => {
236
+ const started = Date.now();
237
+
238
+ const args = [];
239
+ args.push(testFile);
240
+
241
+ if (host) args.push(`--host=${host}`);
242
+ if (port) args.push(`--port=${port}`);
243
+ if (passThreshold != null) args.push(`--pass=${passThreshold}`);
244
+
245
+ // Always maximize corpus as requested
246
+ if (robotsLimit != null) args.push(`--robots-limit=${robotsLimit}`);
247
+
248
+ // Freeze learning by default to keep evaluation comparable across steps
249
+ if (frozen) args.push('--disable-learning=true');
250
+
251
+ for (const a of (forwardMainArgs || [])) {
252
+ if (a && String(a).startsWith('--')) args.push(String(a));
253
+ }
254
+
255
+ // Model patch via test_automatic feature
256
+ for (const [k, v] of Object.entries(modelParams || {})) {
257
+ const raw = typeof v === 'string' ? v : String(v);
258
+ args.push(`--model.${k}=${raw}`);
259
+ }
260
+
261
+ const child = spawn(process.execPath, args, {
262
+ cwd: __dirname,
263
+ stdio: ['ignore', 'pipe', 'pipe'],
264
+ env: { ...process.env }
265
+ });
266
+
267
+ let out = '';
268
+ let err = '';
269
+
270
+ const killTimer = timeoutMs
271
+ ? setTimeout(() => {
272
+ try {
273
+ child.kill('SIGKILL');
274
+ } catch (_e) {
275
+ // ignore
276
+ }
277
+ }, timeoutMs)
278
+ : null;
279
+
280
+ child.stdout.on('data', (d) => {
281
+ out += d.toString('utf8');
282
+ });
283
+ child.stderr.on('data', (d) => {
284
+ err += d.toString('utf8');
285
+ });
286
+
287
+ child.on('close', (code) => {
288
+ if (killTimer) clearTimeout(killTimer);
289
+ const elapsedMs = Date.now() - started;
290
+ const summary = parseSummary(out + '\n' + err);
291
+ resolve({
292
+ code,
293
+ elapsedMs,
294
+ stdout: out,
295
+ stderr: err,
296
+ ...summary
297
+ });
298
+ });
299
+ });
300
+ };
301
+
302
+ const loadMainModelSpec = (mainModulePath) => {
303
+ try {
304
+ // eslint-disable-next-line global-require, import/no-dynamic-require
305
+ const mod = require(mainModulePath);
306
+ const defaults = mod?.MODEL_DEFAULTS && typeof mod.MODEL_DEFAULTS === 'object' ? mod.MODEL_DEFAULTS : null;
307
+ const activations = Array.isArray(mod?.BUILTIN_ACTIVATION_TYPES) ? mod.BUILTIN_ACTIVATION_TYPES : null;
308
+ const transfers = Array.isArray(mod?.BUILTIN_TRANSFER_TYPES) ? mod.BUILTIN_TRANSFER_TYPES : null;
309
+ return { defaults, activations, transfers };
310
+ } catch (e) {
311
+ return { defaults: null, activations: null, transfers: null, error: e };
312
+ }
313
+ };
314
+
315
+ // 仅基于 main.cjs 真实存在的参数键生成可搜索空间(不虚构参数名)。
316
+ // 这里的 min/max/eps/step 是优化器的超参,不属于 main 的“参数选项”。
317
+ const buildSearchSpaceFromDefaults = (modelDefaults, { activationTypes, transferTypes } = {}) => {
318
+ const numeric = {};
319
+ const categorical = {};
320
+
321
+ if (!modelDefaults || typeof modelDefaults !== 'object') {
322
+ return { numeric, categorical };
323
+ }
324
+
325
+ const keys = Object.keys(modelDefaults);
326
+ for (const k of keys) {
327
+ const v = modelDefaults[k];
328
+ if (typeof v === 'number' && Number.isFinite(v)) {
329
+ const isInt = Number.isInteger(v);
330
+ // Heuristic bounds per common param family
331
+ let min;
332
+ let max;
333
+ let eps;
334
+ let step;
335
+
336
+ if (k === 'decayK') {
337
+ min = 0;
338
+ max = 12;
339
+ eps = 0.2;
340
+ step = 0.6;
341
+ } else if (k === 'decayFactor') {
342
+ min = 0.01;
343
+ max = 0.995;
344
+ eps = 0.03;
345
+ step = 0.08;
346
+ } else if (k === 'maliciousThreshold') {
347
+ min = 0;
348
+ max = 1;
349
+ eps = 0.05;
350
+ step = 0.1;
351
+ } else if (k === 'edgeWeight') {
352
+ min = 0.05;
353
+ max = 8;
354
+ eps = 0.25;
355
+ step = 0.6;
356
+ } else if (k.toLowerCase().includes('ngram')) {
357
+ min = 1;
358
+ max = 10;
359
+ eps = 1;
360
+ step = 1;
361
+ } else if (k.toLowerCase().includes('threshold')) {
362
+ min = 0;
363
+ max = 20;
364
+ eps = 1;
365
+ step = 1;
366
+ } else if (k.toLowerCase().includes('iteration')) {
367
+ min = 1;
368
+ max = 20;
369
+ eps = 1;
370
+ step = 1;
371
+ } else if (k.toLowerCase().includes('len')) {
372
+ min = 4;
373
+ max = 256;
374
+ eps = 4;
375
+ step = 4;
376
+ } else if (k.toLowerCase().includes('max')) {
377
+ min = 8;
378
+ max = 5000;
379
+ eps = isInt ? 8 : 0.5;
380
+ step = isInt ? 16 : 1;
381
+ } else if (k.toLowerCase().includes('decay')) {
382
+ min = 0;
383
+ max = 10;
384
+ eps = 0.25;
385
+ step = 0.5;
386
+ } else {
387
+ // default numeric range around default value
388
+ min = isInt ? 0 : Math.min(0, v - Math.abs(v) * 2);
389
+ max = isInt ? Math.max(4, v * 4 + 10) : Math.max(1, v + Math.abs(v) * 3 + 1);
390
+ eps = isInt ? 1 : 0.1;
391
+ step = isInt ? 1 : 0.25;
392
+ }
393
+
394
+ numeric[k] = { min, max, eps, step, isInt };
395
+ }
396
+ }
397
+
398
+ if (Array.isArray(activationTypes) && activationTypes.length) {
399
+ categorical.activationType = activationTypes.slice();
400
+ }
401
+ if (Array.isArray(transferTypes) && transferTypes.length) {
402
+ categorical.transferType = transferTypes.slice();
403
+ }
404
+
405
+ // Only keep categorical keys that actually exist in defaults
406
+ for (const k of Object.keys(categorical)) {
407
+ if (!(k in modelDefaults)) {
408
+ delete categorical[k];
409
+ }
410
+ }
411
+
412
+ return { numeric, categorical };
413
+ };
414
+
415
+ const normalizeParams = (params, space) => {
416
+ const out = { ...(params || {}) };
417
+
418
+ const numericSpace = space?.numeric && typeof space.numeric === 'object' ? space.numeric : {};
419
+ for (const [k, spec] of Object.entries(numericSpace)) {
420
+ if (out[k] == null) continue;
421
+ let v = Number(out[k]);
422
+ if (!Number.isFinite(v)) continue;
423
+ v = clamp(v, spec.min, spec.max);
424
+ if (spec.isInt) v = Math.round(v);
425
+ out[k] = v;
426
+ }
427
+
428
+ // dependency: memeNgramMin <= memeNgramMax
429
+ if (out.memeNgramMin != null && out.memeNgramMax != null) {
430
+ if (Number(out.memeNgramMin) > Number(out.memeNgramMax)) {
431
+ const tmp = out.memeNgramMin;
432
+ out.memeNgramMin = out.memeNgramMax;
433
+ out.memeNgramMax = tmp;
434
+ }
435
+ }
436
+
437
+ return out;
438
+ };
439
+
440
+ const validateNoUnknownKeys = ({ obj, allowedKeys, label }) => {
441
+ if (!obj || typeof obj !== 'object') return;
442
+ const unknown = Object.keys(obj).filter((k) => !allowedKeys.has(k));
443
+ if (unknown.length) {
444
+ const msg = `[optimization] ${label} contains unknown keys not in main.cjs modelDefaults: ${unknown.join(', ')}`;
445
+ throw new Error(msg);
446
+ }
447
+ };
448
+
449
+ const main = async () => {
450
+ const args = parseArgs(process.argv.slice(2));
451
+
452
+ if (args.help || args.h === true) {
453
+ printHelp({ mainDefault: DEFAULT_MAIN_MODULE, testDefault: DEFAULT_TEST });
454
+ process.exit(0);
455
+ }
456
+
457
+ const testFile = path.resolve(String(args.test || DEFAULT_TEST));
458
+ if (!fs.existsSync(testFile)) {
459
+ console.error('[optimization] test_automatic.cjs not found:', testFile);
460
+ process.exit(2);
461
+ }
462
+
463
+ const mainModulePath = path.resolve(String(args.main || DEFAULT_MAIN_MODULE));
464
+ const spec = loadMainModelSpec(mainModulePath);
465
+ if (!spec.defaults) {
466
+ console.error('[optimization] failed to load MODEL_DEFAULTS from main.cjs.');
467
+ console.error('[optimization] expected main.cjs to export MODEL_DEFAULTS/BUILTIN_ACTIVATION_TYPES/BUILTIN_TRANSFER_TYPES.');
468
+ if (spec.error) console.error('[optimization] load error:', spec.error.message);
469
+ process.exit(3);
470
+ }
471
+
472
+ const SPACE = buildSearchSpaceFromDefaults(spec.defaults, {
473
+ activationTypes: spec.activations,
474
+ transferTypes: spec.transfers
475
+ });
476
+ const allowedKeys = new Set(Object.keys(spec.defaults));
477
+
478
+ const robotsLimit = Number(args['robots-limit'] ?? 10000000);
479
+ const frozen = String(args['allow-learning'] || '').trim() ? false : true;
480
+
481
+ const host = args.host ? String(args.host) : undefined;
482
+ const port = args.port != null ? Number(args.port) : undefined;
483
+
484
+ const iterations = Math.max(1, Number(args.iters ?? 12));
485
+ const categoricalEvery = Math.max(0, Number(args['categorical-every'] ?? 2));
486
+ const pass = args.pass != null ? Number(args.pass) : undefined;
487
+ const timeoutMs = args.timeout != null ? Number(args.timeout) : 20 * 60 * 1000;
488
+
489
+ const seed = Number(args.seed ?? 1337);
490
+ // NOTE: optimizer is deterministic in coordinate/grid mode; seed kept for logging/compat.
491
+
492
+ const gridStep = coerceNumber(args.grid, 0.1);
493
+ const gridMax = Math.max(20, Math.floor(coerceNumber(args['grid-max'], 200)));
494
+
495
+ const logDir = path.resolve(String(args['log-dir'] || path.join(__dirname, 'optimization_logs')));
496
+ ensureDir(logDir);
497
+
498
+ const runId = String(args.run || nowStamp());
499
+ const logFile = path.join(logDir, `opt_${runId}.jsonl`);
500
+ const bestFile = path.join(logDir, `best_${runId}.json`);
501
+
502
+ const forwardMainArgs = Array.isArray(args.mainArgs) ? args.mainArgs : (args.mainArgs ? [args.mainArgs] : []);
503
+
504
+ let init = {};
505
+ if (args['init-json']) {
506
+ try {
507
+ init = JSON.parse(String(args['init-json']));
508
+ } catch (e) {
509
+ console.warn('[optimization] init-json parse failed:', e.message);
510
+ }
511
+ }
512
+
513
+ // init-json 允许设置所有真实参数(包括字符串参数),但不允许出现 main 未声明的键
514
+ validateNoUnknownKeys({ obj: init, allowedKeys, label: 'init-json' });
515
+
516
+ // Allow selecting subset of numeric params
517
+ const selectedNumeric = (() => {
518
+ const raw = args.params;
519
+ if (!raw) return Object.keys(SPACE.numeric).slice().sort();
520
+ const list = Array.isArray(raw) ? raw : [raw];
521
+ const parts = list.flatMap((x) => String(x).split(',').map((s) => s.trim()).filter(Boolean));
522
+ const allowed = new Set(Object.keys(SPACE.numeric));
523
+ const unknown = parts.filter((p) => !allowed.has(p));
524
+ if (unknown.length) {
525
+ throw new Error(`[optimization] --params contains unknown/non-numeric keys: ${unknown.join(', ')}`);
526
+ }
527
+ return parts;
528
+ })();
529
+
530
+ const selectedCategorical = (() => {
531
+ const raw = args.cats;
532
+ if (!raw) return Object.keys(SPACE.categorical).slice().sort();
533
+ const list = Array.isArray(raw) ? raw : [raw];
534
+ const parts = list.flatMap((x) => String(x).split(',').map((s) => s.trim()).filter(Boolean));
535
+ const allowed = new Set(Object.keys(SPACE.categorical));
536
+ const unknown = parts.filter((p) => !allowed.has(p));
537
+ if (unknown.length) {
538
+ throw new Error(`[optimization] --cats contains unknown/non-categorical keys: ${unknown.join(', ')}`);
539
+ }
540
+ return parts;
541
+ })();
542
+
543
+ const logLine = (obj) => {
544
+ fs.appendFileSync(logFile, JSON.stringify(obj) + '\n');
545
+ };
546
+
547
+ // current 包含所有 init-json 提供的真实键;数值键会按 SPACE 进行 clamp/round
548
+ let current = normalizeParams(init, SPACE);
549
+ let best = { params: deepClone(current), score: -Infinity, passed: null, total: null };
550
+
551
+ const evaluate = async (params, meta = {}) => {
552
+ // 不允许出现未知键(避免“虚构参数”)
553
+ validateNoUnknownKeys({ obj: params, allowedKeys, label: 'params' });
554
+ const normalized = normalizeParams(params, SPACE);
555
+ const result = await runEval(testFile, {
556
+ robotsLimit,
557
+ frozen,
558
+ forwardMainArgs,
559
+ modelParams: normalized,
560
+ passThreshold: pass,
561
+ host,
562
+ port,
563
+ timeoutMs
564
+ });
565
+
566
+ const entry = {
567
+ t: new Date().toISOString(),
568
+ runId,
569
+ seed,
570
+ meta,
571
+ robotsLimit,
572
+ frozen,
573
+ host: host || null,
574
+ port: port || null,
575
+ passThreshold: pass ?? null,
576
+ elapsedMs: result.elapsedMs,
577
+ exitCode: result.code,
578
+ avgScore: result.avgScore,
579
+ passed: result.passed,
580
+ total: result.total,
581
+ params: normalized
582
+ };
583
+ logLine(entry);
584
+
585
+ if (result.avgScore != null && result.avgScore > best.score) {
586
+ best = { params: deepClone(normalized), score: result.avgScore, passed: result.passed, total: result.total };
587
+ fs.writeFileSync(bestFile, stableStringify({
588
+ runId,
589
+ t: entry.t,
590
+ bestScore: best.score,
591
+ passed: best.passed,
592
+ total: best.total,
593
+ robotsLimit,
594
+ frozen,
595
+ host: host || null,
596
+ port: port || null,
597
+ passThreshold: pass ?? null,
598
+ params: best.params
599
+ }));
600
+ }
601
+
602
+ return { normalized, result };
603
+ };
604
+
605
+ console.log('[optimization] log:', logFile);
606
+ console.log('[optimization] best:', bestFile);
607
+ console.log('[optimization] main spec:', mainModulePath);
608
+ console.log('[optimization] robots-limit:', robotsLimit);
609
+ console.log('[optimization] frozen (disable-learning):', frozen);
610
+ console.log('[optimization] selected numeric params:', selectedNumeric.join(', '));
611
+ console.log('[optimization] selected categorical params:', selectedCategorical.join(', '));
612
+ console.log('[optimization] grid step (float):', gridStep);
613
+ console.log('[optimization] grid max points:', gridMax);
614
+
615
+ // baseline
616
+ const baseEval = await evaluate(current, { kind: 'baseline' });
617
+ let baseScore = baseEval.result.avgScore ?? -Infinity;
618
+ console.log(`[optimization] baseline avgScore=${baseEval.result.avgScore} pass=${baseEval.result.passed}/${baseEval.result.total}`);
619
+
620
+ const coordinateSweepNumericOnce = async ({ iter, startParams, startScore }) => {
621
+ let local = deepClone(startParams);
622
+ let localScore = startScore;
623
+
624
+ for (const key of selectedNumeric) {
625
+ const spec = SPACE.numeric[key];
626
+ if (!spec) continue;
627
+
628
+ const step = spec.isInt ? 1 : gridStep;
629
+ const values = listGridValues(spec, { step, maxPoints: gridMax });
630
+ if (!values.length) continue;
631
+
632
+ let bestVal = local[key];
633
+ let bestScoreLocal = localScore;
634
+
635
+ console.log(`[optimization] sweep ${key}: candidates=${values.length} (step=${spec.isInt ? 1 : step})`);
636
+ for (const v of values) {
637
+ const trial = deepClone(local);
638
+ trial[key] = v;
639
+ const ev = await evaluate(trial, { kind: 'coord', iter, key, v });
640
+ const s = ev.result.avgScore ?? -Infinity;
641
+ if (s >= bestScoreLocal) {
642
+ bestScoreLocal = s;
643
+ bestVal = v;
644
+ }
645
+ }
646
+
647
+ local[key] = bestVal;
648
+ local = normalizeParams(local, SPACE);
649
+ localScore = bestScoreLocal;
650
+ console.log(`[optimization] picked ${key}=${bestVal} score=${localScore}`);
651
+ }
652
+
653
+ return { params: local, score: localScore };
654
+ };
655
+
656
+ const sweepCategoricals = async ({ iter, startParams, startScore }) => {
657
+ let local = deepClone(startParams);
658
+ let localScore = startScore;
659
+
660
+ for (const catKey of selectedCategorical) {
661
+ const candidates = SPACE?.categorical?.[catKey] || [];
662
+ if (!Array.isArray(candidates) || candidates.length === 0) continue;
663
+
664
+ console.log(`[optimization] categorical ${catKey}: trying ${candidates.length} candidates in order`);
665
+
666
+ let bestParams = deepClone(local);
667
+ let bestScoreLocal = localScore;
668
+
669
+ for (const cand of candidates) {
670
+ const trialBase = deepClone(local);
671
+ trialBase[catKey] = cand;
672
+
673
+ const baseEv2 = await evaluate(trialBase, { kind: 'cat_base', iter, catKey, cand });
674
+ const baseS2 = baseEv2.result.avgScore ?? -Infinity;
675
+ const refined = await coordinateSweepNumericOnce({ iter, startParams: trialBase, startScore: baseS2 });
676
+
677
+ if (refined.score >= bestScoreLocal) {
678
+ bestScoreLocal = refined.score;
679
+ bestParams = refined.params;
680
+ console.log(`[optimization] best so far for ${catKey}: ${cand} score=${bestScoreLocal}`);
681
+ }
682
+ }
683
+
684
+ local = deepClone(bestParams);
685
+ localScore = bestScoreLocal;
686
+ console.log(`[optimization] categorical picked ${catKey}=${local[catKey]} score=${localScore}`);
687
+ }
688
+
689
+ return { params: local, score: localScore };
690
+ };
691
+
692
+ for (let iter = 1; iter <= iterations; iter++) {
693
+ console.log(`\n[optimization] iter ${iter}/${iterations} (deterministic coordinate/grid)`);
694
+
695
+ // Categorical sweep: do it on iter=1, and then every N iterations if enabled.
696
+ if (selectedCategorical.length && (iter === 1 || (categoricalEvery > 0 && (iter % categoricalEvery === 0)))) {
697
+ const outCats = await sweepCategoricals({ iter, startParams: current, startScore: baseScore });
698
+ current = deepClone(outCats.params);
699
+ baseScore = outCats.score;
700
+ }
701
+
702
+ // Numeric sweep: one full pass in fixed order.
703
+ const outNum = await coordinateSweepNumericOnce({ iter, startParams: current, startScore: baseScore });
704
+ current = deepClone(outNum.params);
705
+ baseScore = outNum.score;
706
+
707
+ console.log(`[optimization] end iter ${iter}: score=${baseScore}`);
708
+ console.log(`[optimization] bestSoFar score=${best.score} pass=${best.passed}/${best.total}`);
709
+ }
710
+
711
+ console.log('\n[optimization] finished');
712
+ console.log('[optimization] best score:', best.score);
713
+ console.log('[optimization] best params written:', bestFile);
714
+ console.log('[optimization] full log:', logFile);
715
+ };
716
+
717
+ main().catch((err) => {
718
+ console.error('[optimization] fatal:', err);
719
+ process.exit(1);
720
+ });