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

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.
@@ -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) {
@@ -174,7 +174,7 @@ function normalizeTrade(raw, fallbackStrategy, candlesBySymbol = {}) {
174
174
  pockets,
175
175
  features,
176
176
  groups,
177
- candles: normalizeCandles(r.candles ?? candlesBySymbol[symbol]),
177
+ candles: r.candles === void 0 ? candlesBySymbol[symbol] ?? [] : normalizeCandles(r.candles),
178
178
  figures: normalizeFigures(r.figures ?? payload.figures)
179
179
  };
180
180
  }
@@ -196,11 +196,16 @@ function normalizeDataset(input, options = {}) {
196
196
  );
197
197
  if (runs.size > 1)
198
198
  throw new Error("Mixed backtest runs; provide one immutable lineage");
199
+ const candlesBySymbol = Object.fromEntries(
200
+ Object.entries(options.candlesBySymbol ?? object(root.candlesBySymbol)).map(
201
+ ([symbol, candles]) => [symbol, normalizeCandles(candles)]
202
+ )
203
+ );
199
204
  const trades = rows.map(
200
205
  (r) => normalizeTrade(
201
206
  r,
202
207
  options.strategy ?? String(root.strategy ?? ""),
203
- options.candlesBySymbol ?? object(root.candlesBySymbol)
208
+ candlesBySymbol
204
209
  )
205
210
  );
206
211
  const ids = /* @__PURE__ */ new Set();
@@ -217,6 +222,21 @@ function normalizeDataset(input, options = {}) {
217
222
  trades
218
223
  };
219
224
  }
225
+ function tradeCandleWindow(trade, intervalMs) {
226
+ const lowerBound = (timestamp) => {
227
+ let lo = 0, hi = trade.candles.length;
228
+ while (lo < hi) {
229
+ const mid = Math.floor((lo + hi) / 2);
230
+ if (trade.candles[mid].timestamp < timestamp) lo = mid + 1;
231
+ else hi = mid;
232
+ }
233
+ return lo;
234
+ };
235
+ return trade.candles.slice(
236
+ lowerBound(trade.signalTime - 140 * intervalMs),
237
+ lowerBound(trade.exitTime + 20 * intervalMs + 1)
238
+ );
239
+ }
220
240
 
221
241
  // src/lib/researchVisuals/analysis.ts
222
242
  var import_node_crypto = require("crypto");
@@ -538,7 +558,10 @@ function pricePanel(t, c, step, x, y, w, h) {
538
558
  );
539
559
  }
540
560
  function renderTradeSvg(t, intervalMs) {
541
- const candles = chartCandles(t);
561
+ const candles = chartCandles({
562
+ ...t,
563
+ candles: tradeCandleWindow(t, intervalMs)
564
+ });
542
565
  const start = Math.min(
543
566
  t.signalTime - 90 * intervalMs,
544
567
  t.entryTime - 90 * intervalMs
@@ -813,9 +836,7 @@ async function generateResearchVisuals(options) {
813
836
  population: "selected",
814
837
  trades: selected.map((trade) => ({
815
838
  ...trade,
816
- candles: trade.candles.filter(
817
- (bar) => bar.timestamp >= trade.signalTime - 140 * dataset.intervalMs && bar.timestamp <= trade.exitTime + 20 * dataset.intervalMs
818
- )
839
+ candles: tradeCandleWindow(trade, dataset.intervalMs)
819
840
  }))
820
841
  };
821
842
  await write("chart-inputs.json.gz", (0, import_node_zlib.gzipSync)(JSON.stringify(retained)));