@tradejs/cli 3.1.29-beta.291 → 3.1.29-beta.296

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.
@@ -380,13 +380,12 @@ var getReferenceSymbols = () => {
380
380
  };
381
381
  var intervalToMarketFeatureInterval = (interval) => {
382
382
  const normalized = String(interval).trim().toLowerCase();
383
- if (normalized === "1" || normalized === "1m") return "1m";
383
+ if (normalized === "1" || normalized === "1m") return null;
384
384
  if (normalized === "5" || normalized === "5m") return "5m";
385
385
  if (normalized === "60" || normalized === "1h") return "1h";
386
386
  return "15m";
387
387
  };
388
388
  var marketIntervalToConnectorInterval = (interval) => {
389
- if (interval === "1m") return "1";
390
389
  if (interval === "5m") return "5";
391
390
  if (interval === "1h") return "60";
392
391
  return "15";
@@ -551,6 +550,7 @@ var backfillBinanceMarketContext = async (params, enabled) => {
551
550
  return skippedBackfillResult();
552
551
  }
553
552
  const interval = intervalToMarketFeatureInterval(params.interval);
553
+ if (interval === null) return skippedBackfillResult();
554
554
  const intervalMs = MARKET_FEATURE_INTERVAL_MS[interval];
555
555
  const { breadthStartMs, tradeFlowStartMs, endMs } = resolveBinanceMarketContextBackfillWindow(params);
556
556
  if (endMs <= Math.min(breadthStartMs, tradeFlowStartMs)) {
@@ -362,7 +362,7 @@ var coinMarketCapFetch = async (params) => {
362
362
  };
363
363
  var fetchCoinMarketCapIndexRows = async (params) => {
364
364
  const rows = [];
365
- const maxChunkDays = 9;
365
+ const maxChunkDays = 4;
366
366
  const totalChunks = Math.max(
367
367
  1,
368
368
  Math.ceil(
@@ -52,17 +52,168 @@ var import_node_crypto5 = require("crypto");
52
52
  var import_coreResearch = require("@tradejs/infra/coreResearch");
53
53
 
54
54
  // src/lib/coreResearch/artifacts.ts
55
- var import_node_fs2 = require("fs");
55
+ var import_node_fs3 = require("fs");
56
56
  var import_node_events = require("events");
57
57
  var import_promises2 = __toESM(require("fs/promises"));
58
- var import_node_path2 = __toESM(require("path"));
58
+ var import_node_path3 = __toESM(require("path"));
59
59
 
60
60
  // src/lib/coreResearch/io.ts
61
61
  var import_node_crypto = require("crypto");
62
- var import_node_fs = require("fs");
62
+ var import_node_fs2 = require("fs");
63
63
  var import_promises = __toESM(require("fs/promises"));
64
64
  var import_backtest = require("@tradejs/core/backtest");
65
+ var import_node_path2 = __toESM(require("path"));
66
+
67
+ // src/lib/coreResearch/trace.ts
68
+ var import_node_fs = require("fs");
69
+ var import_node_readline = __toESM(require("readline"));
65
70
  var import_node_path = __toESM(require("path"));
71
+ async function* readCoreResearchTrace(filePaths) {
72
+ for (const inputPath of filePaths) {
73
+ const filePath = import_node_path.default.resolve(inputPath);
74
+ const input = (0, import_node_fs.createReadStream)(filePath);
75
+ const lines = import_node_readline.default.createInterface({ input, crlfDelay: Infinity });
76
+ let index = -1;
77
+ try {
78
+ for await (const line of lines) {
79
+ index++;
80
+ if (!line.trim()) continue;
81
+ let event;
82
+ try {
83
+ event = JSON.parse(line);
84
+ } catch (error) {
85
+ throw new Error(
86
+ `${filePath}:${index + 1} contains invalid trace JSON: ${String(error)}`
87
+ );
88
+ }
89
+ yield event;
90
+ }
91
+ } finally {
92
+ lines.close();
93
+ input.destroy();
94
+ }
95
+ }
96
+ }
97
+ var summarizeCoreResearchTrace = async (filePaths = []) => {
98
+ const events = /* @__PURE__ */ Object.create(null);
99
+ const skipCounts = /* @__PURE__ */ Object.create(null);
100
+ const exitCodes = /* @__PURE__ */ Object.create(null);
101
+ const policyRejections = {};
102
+ const policyExamples = [];
103
+ for await (const event of readCoreResearchTrace(filePaths)) {
104
+ events[event.event] = (events[event.event] ?? 0) + 1;
105
+ if (event.event === "skip_summary") {
106
+ for (const [reason, count] of Object.entries(event.skipCounts)) {
107
+ skipCounts[reason] = (skipCounts[reason] ?? 0) + count;
108
+ }
109
+ }
110
+ if (event.event === "position_exited" && event.exitCode) {
111
+ exitCodes[event.exitCode] = (exitCodes[event.exitCode] ?? 0) + 1;
112
+ }
113
+ if ("researchEntryDecision" in event && event.researchEntryDecision && !event.researchEntryDecision.allowed) {
114
+ const reason = event.researchEntryDecision.reason;
115
+ policyRejections[reason] = (policyRejections[reason] ?? 0) + 1;
116
+ if (policyExamples.length < 20)
117
+ policyExamples.push({
118
+ timestamp: event.timestamp,
119
+ symbol: event.symbol,
120
+ decision: event.researchEntryDecision
121
+ });
122
+ }
123
+ }
124
+ return {
125
+ events: Object.fromEntries(Object.entries(events).sort()),
126
+ skipCounts: Object.fromEntries(
127
+ Object.entries(skipCounts).sort(
128
+ ([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
129
+ )
130
+ ),
131
+ exitCodes: Object.fromEntries(
132
+ Object.entries(exitCodes).sort(
133
+ ([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
134
+ )
135
+ ),
136
+ ...policyExamples.length ? {
137
+ researchPolicy: {
138
+ rejections: policyRejections,
139
+ examples: policyExamples
140
+ }
141
+ } : {}
142
+ };
143
+ };
144
+
145
+ // src/lib/coreResearch/support.ts
146
+ async function assessCoreResearchSupport(spec, criteria) {
147
+ const symbols = new Set(spec.universe.symbols);
148
+ const blockMs = spec.robustness.clusterDays * 864e5;
149
+ const variants = [];
150
+ for (const variant of spec.variants) {
151
+ if (!variant.traceFiles?.length) continue;
152
+ const setups = /* @__PURE__ */ new Set();
153
+ const timestamps = /* @__PURE__ */ new Set();
154
+ const blocks = /* @__PURE__ */ new Map();
155
+ const seenSymbols = /* @__PURE__ */ new Set();
156
+ const runs = /* @__PURE__ */ new Set();
157
+ let fallbackRows = 0;
158
+ for await (const event of readCoreResearchTrace(variant.traceFiles)) {
159
+ if (variant.runId && event.backtestRunId !== variant.runId) continue;
160
+ runs.add(event.backtestRunId);
161
+ if (event.event !== "signal_emitted" && event.event !== "entry_rejected" || event.strategy !== spec.strategy || !symbols.has(event.symbol) || spec.hypothesis.target !== "ALL" && event.direction !== spec.hypothesis.target || !Number.isSafeInteger(event.timestamp) || event.timestamp < spec.window.start || event.timestamp >= spec.window.end)
162
+ continue;
163
+ timestamps.add(event.timestamp);
164
+ seenSymbols.add(event.symbol);
165
+ if (event.setupIdentitySource === "strategy-context" && event.setupIdentity) {
166
+ setups.add(
167
+ JSON.stringify([event.symbol, event.direction, event.setupIdentity])
168
+ );
169
+ } else fallbackRows++;
170
+ const block = Math.floor((event.timestamp - spec.window.start) / blockMs);
171
+ const times = blocks.get(block) ?? /* @__PURE__ */ new Set();
172
+ times.add(event.timestamp);
173
+ blocks.set(block, times);
174
+ }
175
+ if (runs.size > 1)
176
+ throw new Error(`Support traces mix runs for ${variant.id}`);
177
+ variants.push({
178
+ id: variant.id,
179
+ role: variant.role,
180
+ setups,
181
+ events: timestamps.size,
182
+ symbols: seenSymbols.size,
183
+ fallbackRows,
184
+ blocks: [...blocks].sort(([a], [b]) => a - b).map(([index, times]) => ({
185
+ start: spec.window.start + index * blockMs,
186
+ events: times.size
187
+ })),
188
+ passed: timestamps.size >= criteria.minimumEvents && blocks.size >= criteria.minimumBlocks
189
+ });
190
+ }
191
+ const control = variants.find((v) => v.role === "control");
192
+ const candidates = variants.filter((v) => v.role === "candidate");
193
+ const comparisons = control ? candidates.map((candidate) => ({
194
+ candidate: candidate.id,
195
+ commonSetups: [...candidate.setups].filter(
196
+ (id) => control.setups.has(id)
197
+ ).length
198
+ })) : [];
199
+ const missing = !control || criteria.minimumCommonSetups !== void 0 && candidates.length !== spec.variants.filter((v) => v.role === "candidate").length;
200
+ const insufficient = variants.some((v) => !v.passed) || comparisons.some(
201
+ (c) => c.commonSetups < (criteria.minimumCommonSetups ?? 0)
202
+ );
203
+ return {
204
+ schema: "tradejs-core-research-support/v1",
205
+ window: { start: spec.window.start, end: spec.window.end },
206
+ target: spec.hypothesis.target,
207
+ blockDays: spec.robustness.clusterDays,
208
+ criteria,
209
+ status: missing ? "missing-evidence" : insufficient ? "insufficient-support" : "supported",
210
+ variants: variants.map(({ setups, ...variant }) => ({
211
+ ...variant,
212
+ explicitSetups: setups.size
213
+ })),
214
+ comparisons
215
+ };
216
+ }
66
217
 
67
218
  // src/lib/coreResearch/types.ts
68
219
  var CORE_RESEARCH_SCHEMA = "tradejs-core-research/v1";
@@ -88,7 +239,7 @@ var sha256Text = (value) => (0, import_node_crypto.createHash)("sha256").update(
88
239
  var sha256Json = (value) => sha256Text(canonicalJson(value));
89
240
  var sha256File = async (filePath) => {
90
241
  const hash = (0, import_node_crypto.createHash)("sha256");
91
- const input = (0, import_node_fs.createReadStream)(filePath);
242
+ const input = (0, import_node_fs2.createReadStream)(filePath);
92
243
  for await (const chunk of input) hash.update(chunk);
93
244
  return hash.digest("hex");
94
245
  };
@@ -327,33 +478,49 @@ var validateCoreResearchSpec = (value) => {
327
478
  Boolean(spec.artifacts?.ledgerPath?.trim()),
328
479
  "artifacts.ledgerPath is required"
329
480
  );
481
+ if (spec.support !== void 0) {
482
+ assert(
483
+ spec.support && typeof spec.support === "object",
484
+ "support must be an object"
485
+ );
486
+ for (const [key, value2] of [
487
+ ["minimumEvents", spec.support.minimumEvents],
488
+ ["minimumBlocks", spec.support.minimumBlocks],
489
+ ...spec.support.minimumCommonSetups === void 0 ? [] : [["minimumCommonSetups", spec.support.minimumCommonSetups]]
490
+ ]) {
491
+ assert(
492
+ typeof value2 === "number" && Number.isSafeInteger(value2) && value2 > 0,
493
+ `support.${key} must be a positive integer`
494
+ );
495
+ }
496
+ }
330
497
  return spec;
331
498
  };
332
499
  var loadCoreResearchSpec = async (specPath) => {
333
- const resolvedPath = import_node_path.default.resolve(specPath);
500
+ const resolvedPath = import_node_path2.default.resolve(specPath);
334
501
  const parsed = JSON.parse(await import_promises.default.readFile(resolvedPath, "utf8"));
335
502
  const spec = validateCoreResearchSpec(parsed);
336
503
  return { resolvedPath, spec, specSha256: sha256Json(spec) };
337
504
  };
338
505
  var resolveCoreResearchPaths = (spec) => {
339
- const rootDir = import_node_path.default.resolve(spec.artifacts.rootDir);
340
- const researchDir = import_node_path.default.join(rootDir, spec.researchId);
506
+ const rootDir = import_node_path2.default.resolve(spec.artifacts.rootDir);
507
+ const researchDir = import_node_path2.default.join(rootDir, spec.researchId);
341
508
  return {
342
509
  rootDir,
343
510
  researchDir,
344
- ledgerPath: import_node_path.default.resolve(spec.artifacts.ledgerPath),
345
- specPath: import_node_path.default.join(researchDir, "spec.json"),
346
- manifestPath: import_node_path.default.join(researchDir, "manifest.json"),
347
- resultPath: import_node_path.default.join(researchDir, "result.json"),
348
- comparisonPath: import_node_path.default.join(researchDir, "comparison.json"),
349
- matchesPath: import_node_path.default.join(researchDir, "matches.csv"),
350
- tradesPath: import_node_path.default.join(researchDir, "trades.jsonl"),
351
- reportPath: import_node_path.default.join(researchDir, "report.html"),
352
- runLogDir: import_node_path.default.join(researchDir, "runs")
511
+ ledgerPath: import_node_path2.default.resolve(spec.artifacts.ledgerPath),
512
+ specPath: import_node_path2.default.join(researchDir, "spec.json"),
513
+ manifestPath: import_node_path2.default.join(researchDir, "manifest.json"),
514
+ resultPath: import_node_path2.default.join(researchDir, "result.json"),
515
+ comparisonPath: import_node_path2.default.join(researchDir, "comparison.json"),
516
+ matchesPath: import_node_path2.default.join(researchDir, "matches.csv"),
517
+ tradesPath: import_node_path2.default.join(researchDir, "trades.jsonl"),
518
+ reportPath: import_node_path2.default.join(researchDir, "report.html"),
519
+ runLogDir: import_node_path2.default.join(researchDir, "runs")
353
520
  };
354
521
  };
355
522
  var writeJsonAtomic = async (filePath, value) => {
356
- await import_promises.default.mkdir(import_node_path.default.dirname(filePath), { recursive: true });
523
+ await import_promises.default.mkdir(import_node_path2.default.dirname(filePath), { recursive: true });
357
524
  const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
358
525
  await import_promises.default.writeFile(temporaryPath, `${canonicalJson(value)}
359
526
  `, "utf8");
@@ -400,7 +567,7 @@ var verifyCoreResearchLedger = (records) => {
400
567
  };
401
568
  var appendCoreResearchLedger = async (params) => {
402
569
  const { ledgerPath } = params;
403
- await import_promises.default.mkdir(import_node_path.default.dirname(ledgerPath), { recursive: true });
570
+ await import_promises.default.mkdir(import_node_path2.default.dirname(ledgerPath), { recursive: true });
404
571
  const lockPath = `${ledgerPath}.lock`;
405
572
  let lock = null;
406
573
  try {
@@ -439,14 +606,14 @@ var appendCoreResearchLedger = async (params) => {
439
606
  var validateCoreResearchParentLineage = async (spec) => {
440
607
  if (spec.stage === "screen") return;
441
608
  const expectedParentStage = spec.stage === "isolated_long" ? "screen" : "isolated_long";
442
- const rootDir = import_node_path.default.resolve(spec.artifacts.rootDir);
609
+ const rootDir = import_node_path2.default.resolve(spec.artifacts.rootDir);
443
610
  const passedParentCandidateConfigShas = /* @__PURE__ */ new Set();
444
611
  for (const parentResearchId of spec.parentResearchIds ?? []) {
445
- const parentDir = import_node_path.default.join(rootDir, parentResearchId);
612
+ const parentDir = import_node_path2.default.join(rootDir, parentResearchId);
446
613
  const [parentSpec, parentManifest, parentResult] = await Promise.all([
447
- import_promises.default.readFile(import_node_path.default.join(parentDir, "spec.json"), "utf8").then((text) => JSON.parse(text)),
448
- import_promises.default.readFile(import_node_path.default.join(parentDir, "manifest.json"), "utf8").then((text) => JSON.parse(text)),
449
- import_promises.default.readFile(import_node_path.default.join(parentDir, "result.json"), "utf8").then((text) => JSON.parse(text))
614
+ import_promises.default.readFile(import_node_path2.default.join(parentDir, "spec.json"), "utf8").then((text) => JSON.parse(text)),
615
+ import_promises.default.readFile(import_node_path2.default.join(parentDir, "manifest.json"), "utf8").then((text) => JSON.parse(text)),
616
+ import_promises.default.readFile(import_node_path2.default.join(parentDir, "result.json"), "utf8").then((text) => JSON.parse(text))
450
617
  ]);
451
618
  validateCoreResearchSpec(parentSpec);
452
619
  if (parentManifest.status !== "completed") {
@@ -520,15 +687,46 @@ var prepareCoreResearch = async (spec) => {
520
687
  return { paths, specSha256, manifest: existingManifest };
521
688
  }
522
689
  }
690
+ const supportSources = spec.support ? await Promise.all(
691
+ [
692
+ ...new Set(
693
+ spec.variants.flatMap((variant) => variant.traceFiles ?? [])
694
+ )
695
+ ].map(async (file) => ({
696
+ file: import_node_path2.default.resolve(file),
697
+ sha256: await sha256File(file).catch((error) => {
698
+ if (error.code === "ENOENT") return null;
699
+ throw error;
700
+ })
701
+ }))
702
+ ) : [];
703
+ const missingSources = supportSources.filter(
704
+ (source) => source.sha256 === null
705
+ );
706
+ const support = spec.support ? missingSources.length ? {
707
+ schema: "tradejs-core-research-support/v1",
708
+ status: "missing-evidence",
709
+ criteria: spec.support
710
+ } : await assessCoreResearchSupport(spec, spec.support) : void 0;
711
+ const supportPath = import_node_path2.default.join(paths.researchDir, "support.json");
712
+ if (support) {
713
+ for (const source of supportSources) {
714
+ if (source.sha256 !== null && source.sha256 !== await sha256File(source.file))
715
+ throw new Error("Support trace changed during preparation");
716
+ }
717
+ await writeJsonAtomic(supportPath, { ...support, sources: supportSources });
718
+ }
523
719
  await writeJsonAtomic(paths.specPath, spec);
524
720
  const manifest = {
525
721
  schema: "tradejs-core-research-manifest/v1",
526
722
  researchId: spec.researchId,
527
723
  specSha256,
528
724
  status: "prepared",
725
+ ...support ? { support: support.status } : {},
529
726
  createdAt: spec.createdAt,
530
727
  artifactHashes: {
531
- "spec.json": await sha256File(paths.specPath)
728
+ "spec.json": await sha256File(paths.specPath),
729
+ ...support ? { "support.json": await sha256File(supportPath) } : {}
532
730
  }
533
731
  };
534
732
  await writeJsonAtomic(paths.manifestPath, manifest);
@@ -547,6 +745,24 @@ var prepareCoreResearch = async (spec) => {
547
745
  }
548
746
  return { paths, specSha256, manifest };
549
747
  };
748
+ var verifyPreparedSupport = async (prepared) => {
749
+ if (!prepared.manifest.support) return;
750
+ if (prepared.manifest.support !== "supported") {
751
+ throw new Error(
752
+ `Research preflight: ${prepared.manifest.support}; see support.json`
753
+ );
754
+ }
755
+ const file = import_node_path2.default.join(prepared.paths.researchDir, "support.json");
756
+ const content = await import_promises.default.readFile(file);
757
+ if ((0, import_node_crypto.createHash)("sha256").update(content).digest("hex") !== prepared.manifest.artifactHashes["support.json"]) {
758
+ throw new Error("Support report checksum mismatch");
759
+ }
760
+ const { sources } = JSON.parse(content.toString());
761
+ for (const source of sources) {
762
+ if (source.sha256 !== await sha256File(source.file))
763
+ throw new Error("Support trace changed after preparation");
764
+ }
765
+ };
550
766
  var verifyCoreResearchArtifacts = async (spec) => {
551
767
  const paths = resolveCoreResearchPaths(spec);
552
768
  const manifest = JSON.parse(
@@ -559,7 +775,7 @@ var verifyCoreResearchArtifacts = async (spec) => {
559
775
  manifest.artifactHashes
560
776
  )) {
561
777
  const actualHash = await sha256File(
562
- import_node_path.default.join(paths.researchDir, relativePath)
778
+ import_node_path2.default.join(paths.researchDir, relativePath)
563
779
  );
564
780
  if (actualHash !== expectedHash) {
565
781
  throw new Error(`Artifact hash mismatch: ${relativePath}`);
@@ -575,7 +791,7 @@ var verifyCoreResearchArtifacts = async (spec) => {
575
791
  };
576
792
  };
577
793
  var scanCoreResearchStages = async (rootDir) => {
578
- const resolvedRoot = import_node_path.default.resolve(rootDir);
794
+ const resolvedRoot = import_node_path2.default.resolve(rootDir);
579
795
  let entries = [];
580
796
  try {
581
797
  entries = await import_promises.default.readdir(resolvedRoot);
@@ -593,15 +809,15 @@ var scanCoreResearchStages = async (rootDir) => {
593
809
  for (const entry of entries.sort(
594
810
  (left, right) => left < right ? -1 : left > right ? 1 : 0
595
811
  )) {
596
- const researchDir = import_node_path.default.join(resolvedRoot, entry);
812
+ const researchDir = import_node_path2.default.join(resolvedRoot, entry);
597
813
  const stats = await import_promises.default.stat(researchDir);
598
814
  if (!stats.isDirectory()) continue;
599
815
  let spec;
600
816
  let manifest;
601
817
  try {
602
818
  [spec, manifest] = await Promise.all([
603
- import_promises.default.readFile(import_node_path.default.join(researchDir, "spec.json"), "utf8").then((text) => JSON.parse(text)),
604
- import_promises.default.readFile(import_node_path.default.join(researchDir, "manifest.json"), "utf8").then((text) => JSON.parse(text))
819
+ import_promises.default.readFile(import_node_path2.default.join(researchDir, "spec.json"), "utf8").then((text) => JSON.parse(text)),
820
+ import_promises.default.readFile(import_node_path2.default.join(researchDir, "manifest.json"), "utf8").then((text) => JSON.parse(text))
605
821
  ]);
606
822
  } catch (error) {
607
823
  if (error.code === "ENOENT") continue;
@@ -611,7 +827,7 @@ var scanCoreResearchStages = async (rootDir) => {
611
827
  if (manifest.specSha256 !== sha256Json(spec)) {
612
828
  throw new Error(`Stage index found a spec hash mismatch for ${entry}`);
613
829
  }
614
- const result = manifest.status === "completed" ? await import_promises.default.readFile(import_node_path.default.join(researchDir, "result.json"), "utf8").then((text) => JSON.parse(text)) : null;
830
+ const result = manifest.status === "completed" ? await import_promises.default.readFile(import_node_path2.default.join(researchDir, "result.json"), "utf8").then((text) => JSON.parse(text)) : null;
615
831
  familyByResearch.set(spec.researchId, spec.hypothesis.family);
616
832
  experiments.push({
617
833
  researchId: spec.researchId,
@@ -642,7 +858,7 @@ var scanCoreResearchStages = async (rootDir) => {
642
858
  return { experiments, familyByResearch };
643
859
  };
644
860
  var writeCoreResearchStageIndex = async (rootDir) => {
645
- const resolvedRoot = import_node_path.default.resolve(rootDir);
861
+ const resolvedRoot = import_node_path2.default.resolve(rootDir);
646
862
  const { experiments, familyByResearch } = await scanCoreResearchStages(resolvedRoot);
647
863
  const grouped = /* @__PURE__ */ new Map();
648
864
  for (const experiment of experiments) {
@@ -659,7 +875,7 @@ var writeCoreResearchStageIndex = async (rootDir) => {
659
875
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
660
876
  families: [...grouped.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([family, experiments2]) => ({ family, experiments: experiments2 }))
661
877
  };
662
- await writeJsonAtomic(import_node_path.default.join(resolvedRoot, "index.json"), index);
878
+ await writeJsonAtomic(import_node_path2.default.join(resolvedRoot, "index.json"), index);
663
879
  return index;
664
880
  };
665
881
 
@@ -676,8 +892,8 @@ var closeOutput = async (output) => {
676
892
  await (0, import_node_events.once)(output, "close");
677
893
  };
678
894
  var writeCoreResearchMatches = async (params) => {
679
- await import_promises2.default.mkdir(import_node_path2.default.dirname(params.filePath), { recursive: true });
680
- const output = (0, import_node_fs2.createWriteStream)(params.filePath, { encoding: "utf8" });
895
+ await import_promises2.default.mkdir(import_node_path3.default.dirname(params.filePath), { recursive: true });
896
+ const output = (0, import_node_fs3.createWriteStream)(params.filePath, { encoding: "utf8" });
681
897
  await writeLine(
682
898
  output,
683
899
  [
@@ -727,8 +943,8 @@ var writeCoreResearchMatches = async (params) => {
727
943
  await closeOutput(output);
728
944
  };
729
945
  var writeCoreResearchTrades = async (params) => {
730
- await import_promises2.default.mkdir(import_node_path2.default.dirname(params.filePath), { recursive: true });
731
- const output = (0, import_node_fs2.createWriteStream)(params.filePath, { encoding: "utf8" });
946
+ await import_promises2.default.mkdir(import_node_path3.default.dirname(params.filePath), { recursive: true });
947
+ const output = (0, import_node_fs3.createWriteStream)(params.filePath, { encoding: "utf8" });
732
948
  for (const variant of params.variants) {
733
949
  for (const trade of variant.trades) {
734
950
  await writeLine(
@@ -1461,9 +1677,9 @@ var compareCoreResearchVariants = (params) => {
1461
1677
 
1462
1678
  // src/lib/coreResearch/dataset.ts
1463
1679
  var import_node_crypto3 = require("crypto");
1464
- var import_node_fs3 = require("fs");
1465
- var import_node_path3 = __toESM(require("path"));
1466
- var import_node_readline = __toESM(require("readline"));
1680
+ var import_node_fs4 = require("fs");
1681
+ var import_node_path4 = __toESM(require("path"));
1682
+ var import_node_readline2 = __toESM(require("readline"));
1467
1683
  var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
1468
1684
  var finiteNumber = (value, field) => {
1469
1685
  const numeric = Number(value);
@@ -1673,11 +1889,11 @@ var readCoreResearchVariant = async (variant) => {
1673
1889
  const seen = /* @__PURE__ */ new Map();
1674
1890
  let duplicateRowsDropped = 0;
1675
1891
  for (const inputPath of variant.files) {
1676
- const filePath = import_node_path3.default.resolve(inputPath);
1677
- const input = (0, import_node_fs3.createReadStream)(filePath);
1892
+ const filePath = import_node_path4.default.resolve(inputPath);
1893
+ const input = (0, import_node_fs4.createReadStream)(filePath);
1678
1894
  const hash = (0, import_node_crypto3.createHash)("sha256");
1679
1895
  input.on("data", (chunk) => hash.update(chunk));
1680
- const lines = import_node_readline.default.createInterface({ input, crlfDelay: Infinity });
1896
+ const lines = import_node_readline2.default.createInterface({ input, crlfDelay: Infinity });
1681
1897
  const fileTrades = [];
1682
1898
  let lineNumber = 0;
1683
1899
  let rows = 0;
@@ -2165,79 +2381,6 @@ var applyCoreResearchRobustnessGuardrails = (params) => {
2165
2381
  }
2166
2382
  };
2167
2383
 
2168
- // src/lib/coreResearch/trace.ts
2169
- var import_node_fs4 = require("fs");
2170
- var import_node_readline2 = __toESM(require("readline"));
2171
- var import_node_path4 = __toESM(require("path"));
2172
- var summarizeCoreResearchTrace = async (filePaths = []) => {
2173
- const events = /* @__PURE__ */ Object.create(null);
2174
- const skipCounts = /* @__PURE__ */ Object.create(null);
2175
- const exitCodes = /* @__PURE__ */ Object.create(null);
2176
- const policyRejections = {};
2177
- const policyExamples = [];
2178
- for (const inputPath of filePaths) {
2179
- const filePath = import_node_path4.default.resolve(inputPath);
2180
- const input = (0, import_node_fs4.createReadStream)(filePath);
2181
- const lines = import_node_readline2.default.createInterface({ input, crlfDelay: Infinity });
2182
- let index = -1;
2183
- try {
2184
- for await (const line of lines) {
2185
- index++;
2186
- if (!line.trim()) continue;
2187
- let event;
2188
- try {
2189
- event = JSON.parse(line);
2190
- } catch (error) {
2191
- throw new Error(
2192
- `${filePath}:${index + 1} contains invalid trace JSON: ${String(error)}`
2193
- );
2194
- }
2195
- events[event.event] = (events[event.event] ?? 0) + 1;
2196
- if (event.event === "skip_summary") {
2197
- for (const [reason, count] of Object.entries(event.skipCounts)) {
2198
- skipCounts[reason] = (skipCounts[reason] ?? 0) + count;
2199
- }
2200
- }
2201
- if (event.event === "position_exited" && event.exitCode) {
2202
- exitCodes[event.exitCode] = (exitCodes[event.exitCode] ?? 0) + 1;
2203
- }
2204
- if ("researchEntryDecision" in event && event.researchEntryDecision && !event.researchEntryDecision.allowed) {
2205
- const reason = event.researchEntryDecision.reason;
2206
- policyRejections[reason] = (policyRejections[reason] ?? 0) + 1;
2207
- if (policyExamples.length < 20)
2208
- policyExamples.push({
2209
- timestamp: event.timestamp,
2210
- symbol: event.symbol,
2211
- decision: event.researchEntryDecision
2212
- });
2213
- }
2214
- }
2215
- } finally {
2216
- lines.close();
2217
- input.destroy();
2218
- }
2219
- }
2220
- return {
2221
- events: Object.fromEntries(Object.entries(events).sort()),
2222
- skipCounts: Object.fromEntries(
2223
- Object.entries(skipCounts).sort(
2224
- ([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
2225
- )
2226
- ),
2227
- exitCodes: Object.fromEntries(
2228
- Object.entries(exitCodes).sort(
2229
- ([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
2230
- )
2231
- ),
2232
- ...policyExamples.length ? {
2233
- researchPolicy: {
2234
- rejections: policyRejections,
2235
- examples: policyExamples
2236
- }
2237
- } : {}
2238
- };
2239
- };
2240
-
2241
2384
  // src/lib/researchRoots.ts
2242
2385
  var import_node_path5 = __toESM(require("path"));
2243
2386
  var SOURCE_REPOSITORY_ROOT_ENV = "TRADEJS_SOURCE_REPOSITORY_ROOT";
@@ -2261,6 +2404,7 @@ var summarizeSupplementalFiles = async (params) => {
2261
2404
  var analyzeCoreResearchPrepared = async (spec, options = {}) => {
2262
2405
  const prepared = options.prepared ?? await prepareCoreResearch(spec);
2263
2406
  const { paths, specSha256 } = prepared;
2407
+ await verifyPreparedSupport(prepared);
2264
2408
  if (prepared.manifest.status === "completed") {
2265
2409
  throw new Error(
2266
2410
  `Research ${spec.researchId} is already completed and immutable; use verify or create a new researchId`
@@ -2531,6 +2675,7 @@ var analyzeCoreResearchPrepared = async (spec, options = {}) => {
2531
2675
  "utf8"
2532
2676
  );
2533
2677
  const artifactHashes = {
2678
+ ...prepared.manifest.artifactHashes,
2534
2679
  "spec.json": await sha256File(paths.specPath),
2535
2680
  "result.json": await sha256File(paths.resultPath),
2536
2681
  "comparison.json": await sha256File(paths.comparisonPath),
@@ -2821,6 +2966,7 @@ var runCoreResearch = async (params) => {
2821
2966
  const { spec } = params;
2822
2967
  const cwd = import_node_path6.default.resolve(params.cwd ?? process.cwd());
2823
2968
  const prepared = await prepareCoreResearch(spec);
2969
+ await verifyPreparedSupport(prepared);
2824
2970
  const resolvedVariants = [];
2825
2971
  for (const variant of spec.variants) {
2826
2972
  if (!variant.command?.length) {
@@ -381,13 +381,12 @@ var getReferenceSymbols = () => {
381
381
  };
382
382
  var intervalToMarketFeatureInterval = (interval) => {
383
383
  const normalized = String(interval).trim().toLowerCase();
384
- if (normalized === "1" || normalized === "1m") return "1m";
384
+ if (normalized === "1" || normalized === "1m") return null;
385
385
  if (normalized === "5" || normalized === "5m") return "5m";
386
386
  if (normalized === "60" || normalized === "1h") return "1h";
387
387
  return "15m";
388
388
  };
389
389
  var marketIntervalToConnectorInterval = (interval) => {
390
- if (interval === "1m") return "1";
391
390
  if (interval === "5m") return "5";
392
391
  if (interval === "1h") return "60";
393
392
  return "15";
@@ -552,6 +551,7 @@ var backfillBinanceMarketContext = async (params, enabled) => {
552
551
  return skippedBackfillResult();
553
552
  }
554
553
  const interval = intervalToMarketFeatureInterval(params.interval);
554
+ if (interval === null) return skippedBackfillResult();
555
555
  const intervalMs = MARKET_FEATURE_INTERVAL_MS[interval];
556
556
  const { breadthStartMs, tradeFlowStartMs, endMs } = resolveBinanceMarketContextBackfillWindow(params);
557
557
  if (endMs <= Math.min(breadthStartMs, tradeFlowStartMs)) {
@@ -2132,7 +2132,7 @@ var coinMarketCapFetch = async (params) => {
2132
2132
  };
2133
2133
  var fetchCoinMarketCapIndexRows = async (params) => {
2134
2134
  const rows = [];
2135
- const maxChunkDays = 9;
2135
+ const maxChunkDays = 4;
2136
2136
  const totalChunks = Math.max(
2137
2137
  1,
2138
2138
  Math.ceil(