@tradejs/cli 3.1.29-beta.290 → 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,21 +52,172 @@ 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";
69
- var CORE_RESEARCH_RESULT_SCHEMA = "tradejs-core-research-result/v1";
220
+ var CORE_RESEARCH_RESULT_SCHEMA = "tradejs-core-research-result/v2";
70
221
  var CORE_RESEARCH_LEDGER_SCHEMA = "tradejs-core-research-ledger/v1";
71
222
 
72
223
  // src/lib/coreResearch/io.ts
@@ -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(
@@ -740,6 +956,14 @@ var writeCoreResearchTrades = async (params) => {
740
956
  }
741
957
  await closeOutput(output);
742
958
  };
959
+ var summarizeCoreResearchComparison = (comparison) => {
960
+ const { matchedPairs, ...summary } = comparison;
961
+ return {
962
+ ...summary,
963
+ exitReasonChanges: matchedPairs.filter((pair) => pair.exitReasonChanged).length,
964
+ exitCodeChanges: matchedPairs.filter((pair) => pair.exitCodeChanged).length
965
+ };
966
+ };
743
967
 
744
968
  // src/lib/coreResearch/metrics.ts
745
969
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -1453,9 +1677,9 @@ var compareCoreResearchVariants = (params) => {
1453
1677
 
1454
1678
  // src/lib/coreResearch/dataset.ts
1455
1679
  var import_node_crypto3 = require("crypto");
1456
- var import_node_fs3 = require("fs");
1457
- var import_node_path3 = __toESM(require("path"));
1458
- 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"));
1459
1683
  var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
1460
1684
  var finiteNumber = (value, field) => {
1461
1685
  const numeric = Number(value);
@@ -1665,11 +1889,11 @@ var readCoreResearchVariant = async (variant) => {
1665
1889
  const seen = /* @__PURE__ */ new Map();
1666
1890
  let duplicateRowsDropped = 0;
1667
1891
  for (const inputPath of variant.files) {
1668
- const filePath = import_node_path3.default.resolve(inputPath);
1669
- 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);
1670
1894
  const hash = (0, import_node_crypto3.createHash)("sha256");
1671
1895
  input.on("data", (chunk) => hash.update(chunk));
1672
- const lines = import_node_readline.default.createInterface({ input, crlfDelay: Infinity });
1896
+ const lines = import_node_readline2.default.createInterface({ input, crlfDelay: Infinity });
1673
1897
  const fileTrades = [];
1674
1898
  let lineNumber = 0;
1675
1899
  let rows = 0;
@@ -1815,7 +2039,7 @@ var buildCurveSvg = (tradesByVariant, cohort, curve) => {
1815
2039
  )}</svg><div class="legend">${series.map((entry) => `<span><i style="background:${entry.color}"></i>${escapeHtml(entry.id)}</span>`).join("")}</div>`;
1816
2040
  };
1817
2041
  var comparisonCards = (comparisons) => comparisons.map(
1818
- (comparison) => `<article class="card"><h3>${escapeHtml(comparison.candidateId)} vs ${escapeHtml(comparison.controlId)}</h3><p class="verdict ${comparison.selection.passed ? "positive" : "negative"}">${comparison.selection.passed ? "PASS selection" : "REJECT selection"}</p><dl><dt>Matched</dt><dd>${comparison.matched}</dd><dt>Control only</dt><dd>${comparison.controlOnly}</dd><dt>Candidate only</dt><dd>${comparison.candidateOnly}</dd><dt>Identity retained</dt><dd>${format(comparison.matchedIdentityPctOfControl)}%</dd><dt>Exit reason changes</dt><dd>${comparison.matchedPairs.filter((pair) => pair.exitReasonChanged).length}</dd><dt>Exit code changes</dt><dd>${comparison.matchedPairs.filter((pair) => pair.exitCodeChanged).length}</dd><dt>Matched \u0394PnL</dt><dd>${format(comparison.cohorts.ALL.matchedPnlDelta)}</dd><dt>Removed control PnL</dt><dd>${format(comparison.cohorts.ALL.controlOnlyPnl)}</dd><dt>Added candidate PnL</dt><dd>${format(comparison.cohorts.ALL.candidateOnlyPnl)}</dd><dt>Aggregate \u0394PnL</dt><dd>${format(comparison.cohorts.ALL.delta.pnl)}</dd><dt>Aggregate \u0394DD</dt><dd>${format(comparison.cohorts.ALL.delta.realizedMaxDrawdown)}</dd><dt>Bootstrap P(\u0394&gt;0)</dt><dd>${format(comparison.bootstrap.probabilityPositive, 4)}</dd><dt>Holm p</dt><dd>${format(comparison.bootstrap.holmAdjustedPValue, 4)}</dd></dl><p>${comparison.selection.failedRules.length ? escapeHtml(comparison.selection.failedRules.map((rule) => `${rule.scope}.${rule.metric} ${rule.expected}`).join("; ")) : "All preregistered rules passed."}</p></article>`
2042
+ (comparison) => `<article class="card"><h3>${escapeHtml(comparison.candidateId)} vs ${escapeHtml(comparison.controlId)}</h3><p class="verdict ${comparison.selection.passed ? "positive" : "negative"}">${comparison.selection.passed ? "PASS selection" : "REJECT selection"}</p><dl><dt>Matched</dt><dd>${comparison.matched}</dd><dt>Control only</dt><dd>${comparison.controlOnly}</dd><dt>Candidate only</dt><dd>${comparison.candidateOnly}</dd><dt>Identity retained</dt><dd>${format(comparison.matchedIdentityPctOfControl)}%</dd><dt>Exit reason changes</dt><dd>${comparison.exitReasonChanges}</dd><dt>Exit code changes</dt><dd>${comparison.exitCodeChanges}</dd><dt>Matched \u0394PnL</dt><dd>${format(comparison.cohorts.ALL.matchedPnlDelta)}</dd><dt>Removed control PnL</dt><dd>${format(comparison.cohorts.ALL.controlOnlyPnl)}</dd><dt>Added candidate PnL</dt><dd>${format(comparison.cohorts.ALL.candidateOnlyPnl)}</dd><dt>Aggregate \u0394PnL</dt><dd>${format(comparison.cohorts.ALL.delta.pnl)}</dd><dt>Aggregate \u0394DD</dt><dd>${format(comparison.cohorts.ALL.delta.realizedMaxDrawdown)}</dd><dt>Bootstrap P(\u0394&gt;0)</dt><dd>${format(comparison.bootstrap.probabilityPositive, 4)}</dd><dt>Holm p</dt><dd>${format(comparison.bootstrap.holmAdjustedPValue, 4)}</dd></dl><p>${comparison.selection.failedRules.length ? escapeHtml(comparison.selection.failedRules.map((rule) => `${rule.scope}.${rule.metric} ${rule.expected}`).join("; ")) : "All preregistered rules passed."}</p></article>`
1819
2043
  ).join("");
1820
2044
  var regimeRows = (variants) => {
1821
2045
  const keys = [
@@ -2157,79 +2381,6 @@ var applyCoreResearchRobustnessGuardrails = (params) => {
2157
2381
  }
2158
2382
  };
2159
2383
 
2160
- // src/lib/coreResearch/trace.ts
2161
- var import_node_fs4 = require("fs");
2162
- var import_node_readline2 = __toESM(require("readline"));
2163
- var import_node_path4 = __toESM(require("path"));
2164
- var summarizeCoreResearchTrace = async (filePaths = []) => {
2165
- const events = /* @__PURE__ */ Object.create(null);
2166
- const skipCounts = /* @__PURE__ */ Object.create(null);
2167
- const exitCodes = /* @__PURE__ */ Object.create(null);
2168
- const policyRejections = {};
2169
- const policyExamples = [];
2170
- for (const inputPath of filePaths) {
2171
- const filePath = import_node_path4.default.resolve(inputPath);
2172
- const input = (0, import_node_fs4.createReadStream)(filePath);
2173
- const lines = import_node_readline2.default.createInterface({ input, crlfDelay: Infinity });
2174
- let index = -1;
2175
- try {
2176
- for await (const line of lines) {
2177
- index++;
2178
- if (!line.trim()) continue;
2179
- let event;
2180
- try {
2181
- event = JSON.parse(line);
2182
- } catch (error) {
2183
- throw new Error(
2184
- `${filePath}:${index + 1} contains invalid trace JSON: ${String(error)}`
2185
- );
2186
- }
2187
- events[event.event] = (events[event.event] ?? 0) + 1;
2188
- if (event.event === "skip_summary") {
2189
- for (const [reason, count] of Object.entries(event.skipCounts)) {
2190
- skipCounts[reason] = (skipCounts[reason] ?? 0) + count;
2191
- }
2192
- }
2193
- if (event.event === "position_exited" && event.exitCode) {
2194
- exitCodes[event.exitCode] = (exitCodes[event.exitCode] ?? 0) + 1;
2195
- }
2196
- if ("researchEntryDecision" in event && event.researchEntryDecision && !event.researchEntryDecision.allowed) {
2197
- const reason = event.researchEntryDecision.reason;
2198
- policyRejections[reason] = (policyRejections[reason] ?? 0) + 1;
2199
- if (policyExamples.length < 20)
2200
- policyExamples.push({
2201
- timestamp: event.timestamp,
2202
- symbol: event.symbol,
2203
- decision: event.researchEntryDecision
2204
- });
2205
- }
2206
- }
2207
- } finally {
2208
- lines.close();
2209
- input.destroy();
2210
- }
2211
- }
2212
- return {
2213
- events: Object.fromEntries(Object.entries(events).sort()),
2214
- skipCounts: Object.fromEntries(
2215
- Object.entries(skipCounts).sort(
2216
- ([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
2217
- )
2218
- ),
2219
- exitCodes: Object.fromEntries(
2220
- Object.entries(exitCodes).sort(
2221
- ([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
2222
- )
2223
- ),
2224
- ...policyExamples.length ? {
2225
- researchPolicy: {
2226
- rejections: policyRejections,
2227
- examples: policyExamples
2228
- }
2229
- } : {}
2230
- };
2231
- };
2232
-
2233
2384
  // src/lib/researchRoots.ts
2234
2385
  var import_node_path5 = __toESM(require("path"));
2235
2386
  var SOURCE_REPOSITORY_ROOT_ENV = "TRADEJS_SOURCE_REPOSITORY_ROOT";
@@ -2253,6 +2404,7 @@ var summarizeSupplementalFiles = async (params) => {
2253
2404
  var analyzeCoreResearchPrepared = async (spec, options = {}) => {
2254
2405
  const prepared = options.prepared ?? await prepareCoreResearch(spec);
2255
2406
  const { paths, specSha256 } = prepared;
2407
+ await verifyPreparedSupport(prepared);
2256
2408
  if (prepared.manifest.status === "completed") {
2257
2409
  throw new Error(
2258
2410
  `Research ${spec.researchId} is already completed and immutable; use verify or create a new researchId`
@@ -2472,7 +2624,7 @@ var analyzeCoreResearchPrepared = async (spec, options = {}) => {
2472
2624
  regimeCausality: "signal-time payload.additionalIndicators.baseContext only"
2473
2625
  },
2474
2626
  variants: analyses,
2475
- comparisons,
2627
+ comparisons: comparisons.map(summarizeCoreResearchComparison),
2476
2628
  multipleTesting: {
2477
2629
  family: spec.hypothesis.family,
2478
2630
  hypotheses: Math.max(comparisons.length, familyHypotheses),
@@ -2505,7 +2657,7 @@ var analyzeCoreResearchPrepared = async (spec, options = {}) => {
2505
2657
  artifactHashes: sourceHashes
2506
2658
  };
2507
2659
  await writeJsonAtomic(paths.resultPath, result);
2508
- await writeJsonAtomic(paths.comparisonPath, comparisons);
2660
+ await writeJsonAtomic(paths.comparisonPath, result.comparisons);
2509
2661
  await writeCoreResearchMatches({ filePath: paths.matchesPath, comparisons });
2510
2662
  await writeCoreResearchTrades({
2511
2663
  filePath: paths.tradesPath,
@@ -2523,6 +2675,7 @@ var analyzeCoreResearchPrepared = async (spec, options = {}) => {
2523
2675
  "utf8"
2524
2676
  );
2525
2677
  const artifactHashes = {
2678
+ ...prepared.manifest.artifactHashes,
2526
2679
  "spec.json": await sha256File(paths.specPath),
2527
2680
  "result.json": await sha256File(paths.resultPath),
2528
2681
  "comparison.json": await sha256File(paths.comparisonPath),
@@ -2813,6 +2966,7 @@ var runCoreResearch = async (params) => {
2813
2966
  const { spec } = params;
2814
2967
  const cwd = import_node_path6.default.resolve(params.cwd ?? process.cwd());
2815
2968
  const prepared = await prepareCoreResearch(spec);
2969
+ await verifyPreparedSupport(prepared);
2816
2970
  const resolvedVariants = [];
2817
2971
  for (const variant of spec.variants) {
2818
2972
  if (!variant.command?.length) {