@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.
@@ -39,14 +39,165 @@ var import_chalk = __toESM(require("chalk"));
39
39
 
40
40
  // src/lib/coreResearch/io.ts
41
41
  var import_node_crypto = require("crypto");
42
- var import_node_fs = require("fs");
42
+ var import_node_fs2 = require("fs");
43
43
  var import_promises = __toESM(require("fs/promises"));
44
44
  var import_backtest = require("@tradejs/core/backtest");
45
+ var import_node_path2 = __toESM(require("path"));
46
+
47
+ // src/lib/coreResearch/trace.ts
48
+ var import_node_fs = require("fs");
49
+ var import_node_readline = __toESM(require("readline"));
45
50
  var import_node_path = __toESM(require("path"));
51
+ async function* readCoreResearchTrace(filePaths) {
52
+ for (const inputPath of filePaths) {
53
+ const filePath = import_node_path.default.resolve(inputPath);
54
+ const input = (0, import_node_fs.createReadStream)(filePath);
55
+ const lines = import_node_readline.default.createInterface({ input, crlfDelay: Infinity });
56
+ let index = -1;
57
+ try {
58
+ for await (const line of lines) {
59
+ index++;
60
+ if (!line.trim()) continue;
61
+ let event;
62
+ try {
63
+ event = JSON.parse(line);
64
+ } catch (error) {
65
+ throw new Error(
66
+ `${filePath}:${index + 1} contains invalid trace JSON: ${String(error)}`
67
+ );
68
+ }
69
+ yield event;
70
+ }
71
+ } finally {
72
+ lines.close();
73
+ input.destroy();
74
+ }
75
+ }
76
+ }
77
+ var summarizeCoreResearchTrace = async (filePaths = []) => {
78
+ const events = /* @__PURE__ */ Object.create(null);
79
+ const skipCounts = /* @__PURE__ */ Object.create(null);
80
+ const exitCodes = /* @__PURE__ */ Object.create(null);
81
+ const policyRejections = {};
82
+ const policyExamples = [];
83
+ for await (const event of readCoreResearchTrace(filePaths)) {
84
+ events[event.event] = (events[event.event] ?? 0) + 1;
85
+ if (event.event === "skip_summary") {
86
+ for (const [reason, count] of Object.entries(event.skipCounts)) {
87
+ skipCounts[reason] = (skipCounts[reason] ?? 0) + count;
88
+ }
89
+ }
90
+ if (event.event === "position_exited" && event.exitCode) {
91
+ exitCodes[event.exitCode] = (exitCodes[event.exitCode] ?? 0) + 1;
92
+ }
93
+ if ("researchEntryDecision" in event && event.researchEntryDecision && !event.researchEntryDecision.allowed) {
94
+ const reason = event.researchEntryDecision.reason;
95
+ policyRejections[reason] = (policyRejections[reason] ?? 0) + 1;
96
+ if (policyExamples.length < 20)
97
+ policyExamples.push({
98
+ timestamp: event.timestamp,
99
+ symbol: event.symbol,
100
+ decision: event.researchEntryDecision
101
+ });
102
+ }
103
+ }
104
+ return {
105
+ events: Object.fromEntries(Object.entries(events).sort()),
106
+ skipCounts: Object.fromEntries(
107
+ Object.entries(skipCounts).sort(
108
+ ([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
109
+ )
110
+ ),
111
+ exitCodes: Object.fromEntries(
112
+ Object.entries(exitCodes).sort(
113
+ ([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
114
+ )
115
+ ),
116
+ ...policyExamples.length ? {
117
+ researchPolicy: {
118
+ rejections: policyRejections,
119
+ examples: policyExamples
120
+ }
121
+ } : {}
122
+ };
123
+ };
124
+
125
+ // src/lib/coreResearch/support.ts
126
+ async function assessCoreResearchSupport(spec, criteria) {
127
+ const symbols = new Set(spec.universe.symbols);
128
+ const blockMs = spec.robustness.clusterDays * 864e5;
129
+ const variants = [];
130
+ for (const variant of spec.variants) {
131
+ if (!variant.traceFiles?.length) continue;
132
+ const setups = /* @__PURE__ */ new Set();
133
+ const timestamps = /* @__PURE__ */ new Set();
134
+ const blocks = /* @__PURE__ */ new Map();
135
+ const seenSymbols = /* @__PURE__ */ new Set();
136
+ const runs = /* @__PURE__ */ new Set();
137
+ let fallbackRows = 0;
138
+ for await (const event of readCoreResearchTrace(variant.traceFiles)) {
139
+ if (variant.runId && event.backtestRunId !== variant.runId) continue;
140
+ runs.add(event.backtestRunId);
141
+ 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)
142
+ continue;
143
+ timestamps.add(event.timestamp);
144
+ seenSymbols.add(event.symbol);
145
+ if (event.setupIdentitySource === "strategy-context" && event.setupIdentity) {
146
+ setups.add(
147
+ JSON.stringify([event.symbol, event.direction, event.setupIdentity])
148
+ );
149
+ } else fallbackRows++;
150
+ const block = Math.floor((event.timestamp - spec.window.start) / blockMs);
151
+ const times = blocks.get(block) ?? /* @__PURE__ */ new Set();
152
+ times.add(event.timestamp);
153
+ blocks.set(block, times);
154
+ }
155
+ if (runs.size > 1)
156
+ throw new Error(`Support traces mix runs for ${variant.id}`);
157
+ variants.push({
158
+ id: variant.id,
159
+ role: variant.role,
160
+ setups,
161
+ events: timestamps.size,
162
+ symbols: seenSymbols.size,
163
+ fallbackRows,
164
+ blocks: [...blocks].sort(([a], [b]) => a - b).map(([index, times]) => ({
165
+ start: spec.window.start + index * blockMs,
166
+ events: times.size
167
+ })),
168
+ passed: timestamps.size >= criteria.minimumEvents && blocks.size >= criteria.minimumBlocks
169
+ });
170
+ }
171
+ const control = variants.find((v) => v.role === "control");
172
+ const candidates = variants.filter((v) => v.role === "candidate");
173
+ const comparisons = control ? candidates.map((candidate) => ({
174
+ candidate: candidate.id,
175
+ commonSetups: [...candidate.setups].filter(
176
+ (id) => control.setups.has(id)
177
+ ).length
178
+ })) : [];
179
+ const missing = !control || criteria.minimumCommonSetups !== void 0 && candidates.length !== spec.variants.filter((v) => v.role === "candidate").length;
180
+ const insufficient = variants.some((v) => !v.passed) || comparisons.some(
181
+ (c) => c.commonSetups < (criteria.minimumCommonSetups ?? 0)
182
+ );
183
+ return {
184
+ schema: "tradejs-core-research-support/v1",
185
+ window: { start: spec.window.start, end: spec.window.end },
186
+ target: spec.hypothesis.target,
187
+ blockDays: spec.robustness.clusterDays,
188
+ criteria,
189
+ status: missing ? "missing-evidence" : insufficient ? "insufficient-support" : "supported",
190
+ variants: variants.map(({ setups, ...variant }) => ({
191
+ ...variant,
192
+ explicitSetups: setups.size
193
+ })),
194
+ comparisons
195
+ };
196
+ }
46
197
 
47
198
  // src/lib/coreResearch/types.ts
48
199
  var CORE_RESEARCH_SCHEMA = "tradejs-core-research/v1";
49
- var CORE_RESEARCH_RESULT_SCHEMA = "tradejs-core-research-result/v1";
200
+ var CORE_RESEARCH_RESULT_SCHEMA = "tradejs-core-research-result/v2";
50
201
  var CORE_RESEARCH_LEDGER_SCHEMA = "tradejs-core-research-ledger/v1";
51
202
 
52
203
  // src/lib/coreResearch/io.ts
@@ -68,7 +219,7 @@ var sha256Text = (value) => (0, import_node_crypto.createHash)("sha256").update(
68
219
  var sha256Json = (value) => sha256Text(canonicalJson(value));
69
220
  var sha256File = async (filePath) => {
70
221
  const hash = (0, import_node_crypto.createHash)("sha256");
71
- const input = (0, import_node_fs.createReadStream)(filePath);
222
+ const input = (0, import_node_fs2.createReadStream)(filePath);
72
223
  for await (const chunk of input) hash.update(chunk);
73
224
  return hash.digest("hex");
74
225
  };
@@ -307,33 +458,49 @@ var validateCoreResearchSpec = (value) => {
307
458
  Boolean(spec.artifacts?.ledgerPath?.trim()),
308
459
  "artifacts.ledgerPath is required"
309
460
  );
461
+ if (spec.support !== void 0) {
462
+ assert(
463
+ spec.support && typeof spec.support === "object",
464
+ "support must be an object"
465
+ );
466
+ for (const [key, value2] of [
467
+ ["minimumEvents", spec.support.minimumEvents],
468
+ ["minimumBlocks", spec.support.minimumBlocks],
469
+ ...spec.support.minimumCommonSetups === void 0 ? [] : [["minimumCommonSetups", spec.support.minimumCommonSetups]]
470
+ ]) {
471
+ assert(
472
+ typeof value2 === "number" && Number.isSafeInteger(value2) && value2 > 0,
473
+ `support.${key} must be a positive integer`
474
+ );
475
+ }
476
+ }
310
477
  return spec;
311
478
  };
312
479
  var loadCoreResearchSpec = async (specPath) => {
313
- const resolvedPath = import_node_path.default.resolve(specPath);
480
+ const resolvedPath = import_node_path2.default.resolve(specPath);
314
481
  const parsed = JSON.parse(await import_promises.default.readFile(resolvedPath, "utf8"));
315
482
  const spec = validateCoreResearchSpec(parsed);
316
483
  return { resolvedPath, spec, specSha256: sha256Json(spec) };
317
484
  };
318
485
  var resolveCoreResearchPaths = (spec) => {
319
- const rootDir = import_node_path.default.resolve(spec.artifacts.rootDir);
320
- const researchDir = import_node_path.default.join(rootDir, spec.researchId);
486
+ const rootDir = import_node_path2.default.resolve(spec.artifacts.rootDir);
487
+ const researchDir = import_node_path2.default.join(rootDir, spec.researchId);
321
488
  return {
322
489
  rootDir,
323
490
  researchDir,
324
- ledgerPath: import_node_path.default.resolve(spec.artifacts.ledgerPath),
325
- specPath: import_node_path.default.join(researchDir, "spec.json"),
326
- manifestPath: import_node_path.default.join(researchDir, "manifest.json"),
327
- resultPath: import_node_path.default.join(researchDir, "result.json"),
328
- comparisonPath: import_node_path.default.join(researchDir, "comparison.json"),
329
- matchesPath: import_node_path.default.join(researchDir, "matches.csv"),
330
- tradesPath: import_node_path.default.join(researchDir, "trades.jsonl"),
331
- reportPath: import_node_path.default.join(researchDir, "report.html"),
332
- runLogDir: import_node_path.default.join(researchDir, "runs")
491
+ ledgerPath: import_node_path2.default.resolve(spec.artifacts.ledgerPath),
492
+ specPath: import_node_path2.default.join(researchDir, "spec.json"),
493
+ manifestPath: import_node_path2.default.join(researchDir, "manifest.json"),
494
+ resultPath: import_node_path2.default.join(researchDir, "result.json"),
495
+ comparisonPath: import_node_path2.default.join(researchDir, "comparison.json"),
496
+ matchesPath: import_node_path2.default.join(researchDir, "matches.csv"),
497
+ tradesPath: import_node_path2.default.join(researchDir, "trades.jsonl"),
498
+ reportPath: import_node_path2.default.join(researchDir, "report.html"),
499
+ runLogDir: import_node_path2.default.join(researchDir, "runs")
333
500
  };
334
501
  };
335
502
  var writeJsonAtomic = async (filePath, value) => {
336
- await import_promises.default.mkdir(import_node_path.default.dirname(filePath), { recursive: true });
503
+ await import_promises.default.mkdir(import_node_path2.default.dirname(filePath), { recursive: true });
337
504
  const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
338
505
  await import_promises.default.writeFile(temporaryPath, `${canonicalJson(value)}
339
506
  `, "utf8");
@@ -380,7 +547,7 @@ var verifyCoreResearchLedger = (records) => {
380
547
  };
381
548
  var appendCoreResearchLedger = async (params) => {
382
549
  const { ledgerPath } = params;
383
- await import_promises.default.mkdir(import_node_path.default.dirname(ledgerPath), { recursive: true });
550
+ await import_promises.default.mkdir(import_node_path2.default.dirname(ledgerPath), { recursive: true });
384
551
  const lockPath = `${ledgerPath}.lock`;
385
552
  let lock = null;
386
553
  try {
@@ -419,14 +586,14 @@ var appendCoreResearchLedger = async (params) => {
419
586
  var validateCoreResearchParentLineage = async (spec) => {
420
587
  if (spec.stage === "screen") return;
421
588
  const expectedParentStage = spec.stage === "isolated_long" ? "screen" : "isolated_long";
422
- const rootDir = import_node_path.default.resolve(spec.artifacts.rootDir);
589
+ const rootDir = import_node_path2.default.resolve(spec.artifacts.rootDir);
423
590
  const passedParentCandidateConfigShas = /* @__PURE__ */ new Set();
424
591
  for (const parentResearchId of spec.parentResearchIds ?? []) {
425
- const parentDir = import_node_path.default.join(rootDir, parentResearchId);
592
+ const parentDir = import_node_path2.default.join(rootDir, parentResearchId);
426
593
  const [parentSpec, parentManifest, parentResult] = await Promise.all([
427
- import_promises.default.readFile(import_node_path.default.join(parentDir, "spec.json"), "utf8").then((text) => JSON.parse(text)),
428
- import_promises.default.readFile(import_node_path.default.join(parentDir, "manifest.json"), "utf8").then((text) => JSON.parse(text)),
429
- import_promises.default.readFile(import_node_path.default.join(parentDir, "result.json"), "utf8").then((text) => JSON.parse(text))
594
+ import_promises.default.readFile(import_node_path2.default.join(parentDir, "spec.json"), "utf8").then((text) => JSON.parse(text)),
595
+ import_promises.default.readFile(import_node_path2.default.join(parentDir, "manifest.json"), "utf8").then((text) => JSON.parse(text)),
596
+ import_promises.default.readFile(import_node_path2.default.join(parentDir, "result.json"), "utf8").then((text) => JSON.parse(text))
430
597
  ]);
431
598
  validateCoreResearchSpec(parentSpec);
432
599
  if (parentManifest.status !== "completed") {
@@ -500,15 +667,46 @@ var prepareCoreResearch = async (spec) => {
500
667
  return { paths, specSha256, manifest: existingManifest };
501
668
  }
502
669
  }
670
+ const supportSources = spec.support ? await Promise.all(
671
+ [
672
+ ...new Set(
673
+ spec.variants.flatMap((variant) => variant.traceFiles ?? [])
674
+ )
675
+ ].map(async (file) => ({
676
+ file: import_node_path2.default.resolve(file),
677
+ sha256: await sha256File(file).catch((error) => {
678
+ if (error.code === "ENOENT") return null;
679
+ throw error;
680
+ })
681
+ }))
682
+ ) : [];
683
+ const missingSources = supportSources.filter(
684
+ (source) => source.sha256 === null
685
+ );
686
+ const support = spec.support ? missingSources.length ? {
687
+ schema: "tradejs-core-research-support/v1",
688
+ status: "missing-evidence",
689
+ criteria: spec.support
690
+ } : await assessCoreResearchSupport(spec, spec.support) : void 0;
691
+ const supportPath = import_node_path2.default.join(paths.researchDir, "support.json");
692
+ if (support) {
693
+ for (const source of supportSources) {
694
+ if (source.sha256 !== null && source.sha256 !== await sha256File(source.file))
695
+ throw new Error("Support trace changed during preparation");
696
+ }
697
+ await writeJsonAtomic(supportPath, { ...support, sources: supportSources });
698
+ }
503
699
  await writeJsonAtomic(paths.specPath, spec);
504
700
  const manifest = {
505
701
  schema: "tradejs-core-research-manifest/v1",
506
702
  researchId: spec.researchId,
507
703
  specSha256,
508
704
  status: "prepared",
705
+ ...support ? { support: support.status } : {},
509
706
  createdAt: spec.createdAt,
510
707
  artifactHashes: {
511
- "spec.json": await sha256File(paths.specPath)
708
+ "spec.json": await sha256File(paths.specPath),
709
+ ...support ? { "support.json": await sha256File(supportPath) } : {}
512
710
  }
513
711
  };
514
712
  await writeJsonAtomic(paths.manifestPath, manifest);
@@ -527,6 +725,24 @@ var prepareCoreResearch = async (spec) => {
527
725
  }
528
726
  return { paths, specSha256, manifest };
529
727
  };
728
+ var verifyPreparedSupport = async (prepared) => {
729
+ if (!prepared.manifest.support) return;
730
+ if (prepared.manifest.support !== "supported") {
731
+ throw new Error(
732
+ `Research preflight: ${prepared.manifest.support}; see support.json`
733
+ );
734
+ }
735
+ const file = import_node_path2.default.join(prepared.paths.researchDir, "support.json");
736
+ const content = await import_promises.default.readFile(file);
737
+ if ((0, import_node_crypto.createHash)("sha256").update(content).digest("hex") !== prepared.manifest.artifactHashes["support.json"]) {
738
+ throw new Error("Support report checksum mismatch");
739
+ }
740
+ const { sources } = JSON.parse(content.toString());
741
+ for (const source of sources) {
742
+ if (source.sha256 !== await sha256File(source.file))
743
+ throw new Error("Support trace changed after preparation");
744
+ }
745
+ };
530
746
  var verifyCoreResearchArtifacts = async (spec) => {
531
747
  const paths = resolveCoreResearchPaths(spec);
532
748
  const manifest = JSON.parse(
@@ -539,7 +755,7 @@ var verifyCoreResearchArtifacts = async (spec) => {
539
755
  manifest.artifactHashes
540
756
  )) {
541
757
  const actualHash = await sha256File(
542
- import_node_path.default.join(paths.researchDir, relativePath)
758
+ import_node_path2.default.join(paths.researchDir, relativePath)
543
759
  );
544
760
  if (actualHash !== expectedHash) {
545
761
  throw new Error(`Artifact hash mismatch: ${relativePath}`);
@@ -555,7 +771,7 @@ var verifyCoreResearchArtifacts = async (spec) => {
555
771
  };
556
772
  };
557
773
  var scanCoreResearchStages = async (rootDir) => {
558
- const resolvedRoot = import_node_path.default.resolve(rootDir);
774
+ const resolvedRoot = import_node_path2.default.resolve(rootDir);
559
775
  let entries = [];
560
776
  try {
561
777
  entries = await import_promises.default.readdir(resolvedRoot);
@@ -573,15 +789,15 @@ var scanCoreResearchStages = async (rootDir) => {
573
789
  for (const entry of entries.sort(
574
790
  (left, right) => left < right ? -1 : left > right ? 1 : 0
575
791
  )) {
576
- const researchDir = import_node_path.default.join(resolvedRoot, entry);
792
+ const researchDir = import_node_path2.default.join(resolvedRoot, entry);
577
793
  const stats = await import_promises.default.stat(researchDir);
578
794
  if (!stats.isDirectory()) continue;
579
795
  let spec;
580
796
  let manifest;
581
797
  try {
582
798
  [spec, manifest] = await Promise.all([
583
- import_promises.default.readFile(import_node_path.default.join(researchDir, "spec.json"), "utf8").then((text) => JSON.parse(text)),
584
- import_promises.default.readFile(import_node_path.default.join(researchDir, "manifest.json"), "utf8").then((text) => JSON.parse(text))
799
+ import_promises.default.readFile(import_node_path2.default.join(researchDir, "spec.json"), "utf8").then((text) => JSON.parse(text)),
800
+ import_promises.default.readFile(import_node_path2.default.join(researchDir, "manifest.json"), "utf8").then((text) => JSON.parse(text))
585
801
  ]);
586
802
  } catch (error) {
587
803
  if (error.code === "ENOENT") continue;
@@ -591,7 +807,7 @@ var scanCoreResearchStages = async (rootDir) => {
591
807
  if (manifest.specSha256 !== sha256Json(spec)) {
592
808
  throw new Error(`Stage index found a spec hash mismatch for ${entry}`);
593
809
  }
594
- 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;
810
+ 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;
595
811
  familyByResearch.set(spec.researchId, spec.hypothesis.family);
596
812
  experiments.push({
597
813
  researchId: spec.researchId,
@@ -622,7 +838,7 @@ var scanCoreResearchStages = async (rootDir) => {
622
838
  return { experiments, familyByResearch };
623
839
  };
624
840
  var writeCoreResearchStageIndex = async (rootDir) => {
625
- const resolvedRoot = import_node_path.default.resolve(rootDir);
841
+ const resolvedRoot = import_node_path2.default.resolve(rootDir);
626
842
  const { experiments, familyByResearch } = await scanCoreResearchStages(resolvedRoot);
627
843
  const grouped = /* @__PURE__ */ new Map();
628
844
  for (const experiment of experiments) {
@@ -639,7 +855,7 @@ var writeCoreResearchStageIndex = async (rootDir) => {
639
855
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
640
856
  families: [...grouped.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([family, experiments2]) => ({ family, experiments: experiments2 }))
641
857
  };
642
- await writeJsonAtomic(import_node_path.default.join(resolvedRoot, "index.json"), index);
858
+ await writeJsonAtomic(import_node_path2.default.join(resolvedRoot, "index.json"), index);
643
859
  return index;
644
860
  };
645
861
 
@@ -651,10 +867,10 @@ var import_node_crypto5 = require("crypto");
651
867
  var import_coreResearch = require("@tradejs/infra/coreResearch");
652
868
 
653
869
  // src/lib/coreResearch/artifacts.ts
654
- var import_node_fs2 = require("fs");
870
+ var import_node_fs3 = require("fs");
655
871
  var import_node_events = require("events");
656
872
  var import_promises2 = __toESM(require("fs/promises"));
657
- var import_node_path2 = __toESM(require("path"));
873
+ var import_node_path3 = __toESM(require("path"));
658
874
  var csv = (value) => {
659
875
  const text = String(value ?? "");
660
876
  return /[",\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
@@ -667,8 +883,8 @@ var closeOutput = async (output) => {
667
883
  await (0, import_node_events.once)(output, "close");
668
884
  };
669
885
  var writeCoreResearchMatches = async (params) => {
670
- await import_promises2.default.mkdir(import_node_path2.default.dirname(params.filePath), { recursive: true });
671
- const output = (0, import_node_fs2.createWriteStream)(params.filePath, { encoding: "utf8" });
886
+ await import_promises2.default.mkdir(import_node_path3.default.dirname(params.filePath), { recursive: true });
887
+ const output = (0, import_node_fs3.createWriteStream)(params.filePath, { encoding: "utf8" });
672
888
  await writeLine(
673
889
  output,
674
890
  [
@@ -718,8 +934,8 @@ var writeCoreResearchMatches = async (params) => {
718
934
  await closeOutput(output);
719
935
  };
720
936
  var writeCoreResearchTrades = async (params) => {
721
- await import_promises2.default.mkdir(import_node_path2.default.dirname(params.filePath), { recursive: true });
722
- const output = (0, import_node_fs2.createWriteStream)(params.filePath, { encoding: "utf8" });
937
+ await import_promises2.default.mkdir(import_node_path3.default.dirname(params.filePath), { recursive: true });
938
+ const output = (0, import_node_fs3.createWriteStream)(params.filePath, { encoding: "utf8" });
723
939
  for (const variant of params.variants) {
724
940
  for (const trade of variant.trades) {
725
941
  await writeLine(
@@ -731,6 +947,14 @@ var writeCoreResearchTrades = async (params) => {
731
947
  }
732
948
  await closeOutput(output);
733
949
  };
950
+ var summarizeCoreResearchComparison = (comparison) => {
951
+ const { matchedPairs, ...summary } = comparison;
952
+ return {
953
+ ...summary,
954
+ exitReasonChanges: matchedPairs.filter((pair) => pair.exitReasonChanged).length,
955
+ exitCodeChanges: matchedPairs.filter((pair) => pair.exitCodeChanged).length
956
+ };
957
+ };
734
958
 
735
959
  // src/lib/coreResearch/metrics.ts
736
960
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -1444,9 +1668,9 @@ var compareCoreResearchVariants = (params) => {
1444
1668
 
1445
1669
  // src/lib/coreResearch/dataset.ts
1446
1670
  var import_node_crypto3 = require("crypto");
1447
- var import_node_fs3 = require("fs");
1448
- var import_node_path3 = __toESM(require("path"));
1449
- var import_node_readline = __toESM(require("readline"));
1671
+ var import_node_fs4 = require("fs");
1672
+ var import_node_path4 = __toESM(require("path"));
1673
+ var import_node_readline2 = __toESM(require("readline"));
1450
1674
  var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
1451
1675
  var finiteNumber = (value, field) => {
1452
1676
  const numeric = Number(value);
@@ -1656,11 +1880,11 @@ var readCoreResearchVariant = async (variant) => {
1656
1880
  const seen = /* @__PURE__ */ new Map();
1657
1881
  let duplicateRowsDropped = 0;
1658
1882
  for (const inputPath of variant.files) {
1659
- const filePath = import_node_path3.default.resolve(inputPath);
1660
- const input = (0, import_node_fs3.createReadStream)(filePath);
1883
+ const filePath = import_node_path4.default.resolve(inputPath);
1884
+ const input = (0, import_node_fs4.createReadStream)(filePath);
1661
1885
  const hash = (0, import_node_crypto3.createHash)("sha256");
1662
1886
  input.on("data", (chunk) => hash.update(chunk));
1663
- const lines = import_node_readline.default.createInterface({ input, crlfDelay: Infinity });
1887
+ const lines = import_node_readline2.default.createInterface({ input, crlfDelay: Infinity });
1664
1888
  const fileTrades = [];
1665
1889
  let lineNumber = 0;
1666
1890
  let rows = 0;
@@ -1806,7 +2030,7 @@ var buildCurveSvg = (tradesByVariant, cohort, curve) => {
1806
2030
  )}</svg><div class="legend">${series.map((entry) => `<span><i style="background:${entry.color}"></i>${escapeHtml(entry.id)}</span>`).join("")}</div>`;
1807
2031
  };
1808
2032
  var comparisonCards = (comparisons) => comparisons.map(
1809
- (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>`
2033
+ (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>`
1810
2034
  ).join("");
1811
2035
  var regimeRows = (variants) => {
1812
2036
  const keys = [
@@ -2148,79 +2372,6 @@ var applyCoreResearchRobustnessGuardrails = (params) => {
2148
2372
  }
2149
2373
  };
2150
2374
 
2151
- // src/lib/coreResearch/trace.ts
2152
- var import_node_fs4 = require("fs");
2153
- var import_node_readline2 = __toESM(require("readline"));
2154
- var import_node_path4 = __toESM(require("path"));
2155
- var summarizeCoreResearchTrace = async (filePaths = []) => {
2156
- const events = /* @__PURE__ */ Object.create(null);
2157
- const skipCounts = /* @__PURE__ */ Object.create(null);
2158
- const exitCodes = /* @__PURE__ */ Object.create(null);
2159
- const policyRejections = {};
2160
- const policyExamples = [];
2161
- for (const inputPath of filePaths) {
2162
- const filePath = import_node_path4.default.resolve(inputPath);
2163
- const input = (0, import_node_fs4.createReadStream)(filePath);
2164
- const lines = import_node_readline2.default.createInterface({ input, crlfDelay: Infinity });
2165
- let index = -1;
2166
- try {
2167
- for await (const line of lines) {
2168
- index++;
2169
- if (!line.trim()) continue;
2170
- let event;
2171
- try {
2172
- event = JSON.parse(line);
2173
- } catch (error) {
2174
- throw new Error(
2175
- `${filePath}:${index + 1} contains invalid trace JSON: ${String(error)}`
2176
- );
2177
- }
2178
- events[event.event] = (events[event.event] ?? 0) + 1;
2179
- if (event.event === "skip_summary") {
2180
- for (const [reason, count] of Object.entries(event.skipCounts)) {
2181
- skipCounts[reason] = (skipCounts[reason] ?? 0) + count;
2182
- }
2183
- }
2184
- if (event.event === "position_exited" && event.exitCode) {
2185
- exitCodes[event.exitCode] = (exitCodes[event.exitCode] ?? 0) + 1;
2186
- }
2187
- if ("researchEntryDecision" in event && event.researchEntryDecision && !event.researchEntryDecision.allowed) {
2188
- const reason = event.researchEntryDecision.reason;
2189
- policyRejections[reason] = (policyRejections[reason] ?? 0) + 1;
2190
- if (policyExamples.length < 20)
2191
- policyExamples.push({
2192
- timestamp: event.timestamp,
2193
- symbol: event.symbol,
2194
- decision: event.researchEntryDecision
2195
- });
2196
- }
2197
- }
2198
- } finally {
2199
- lines.close();
2200
- input.destroy();
2201
- }
2202
- }
2203
- return {
2204
- events: Object.fromEntries(Object.entries(events).sort()),
2205
- skipCounts: Object.fromEntries(
2206
- Object.entries(skipCounts).sort(
2207
- ([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
2208
- )
2209
- ),
2210
- exitCodes: Object.fromEntries(
2211
- Object.entries(exitCodes).sort(
2212
- ([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
2213
- )
2214
- ),
2215
- ...policyExamples.length ? {
2216
- researchPolicy: {
2217
- rejections: policyRejections,
2218
- examples: policyExamples
2219
- }
2220
- } : {}
2221
- };
2222
- };
2223
-
2224
2375
  // src/lib/researchRoots.ts
2225
2376
  var import_node_path5 = __toESM(require("path"));
2226
2377
  var SOURCE_REPOSITORY_ROOT_ENV = "TRADEJS_SOURCE_REPOSITORY_ROOT";
@@ -2272,6 +2423,7 @@ var summarizeSupplementalFiles = async (params) => {
2272
2423
  var analyzeCoreResearchPrepared = async (spec, options = {}) => {
2273
2424
  const prepared = options.prepared ?? await prepareCoreResearch(spec);
2274
2425
  const { paths, specSha256 } = prepared;
2426
+ await verifyPreparedSupport(prepared);
2275
2427
  if (prepared.manifest.status === "completed") {
2276
2428
  throw new Error(
2277
2429
  `Research ${spec.researchId} is already completed and immutable; use verify or create a new researchId`
@@ -2491,7 +2643,7 @@ var analyzeCoreResearchPrepared = async (spec, options = {}) => {
2491
2643
  regimeCausality: "signal-time payload.additionalIndicators.baseContext only"
2492
2644
  },
2493
2645
  variants: analyses,
2494
- comparisons,
2646
+ comparisons: comparisons.map(summarizeCoreResearchComparison),
2495
2647
  multipleTesting: {
2496
2648
  family: spec.hypothesis.family,
2497
2649
  hypotheses: Math.max(comparisons.length, familyHypotheses),
@@ -2524,7 +2676,7 @@ var analyzeCoreResearchPrepared = async (spec, options = {}) => {
2524
2676
  artifactHashes: sourceHashes
2525
2677
  };
2526
2678
  await writeJsonAtomic(paths.resultPath, result);
2527
- await writeJsonAtomic(paths.comparisonPath, comparisons);
2679
+ await writeJsonAtomic(paths.comparisonPath, result.comparisons);
2528
2680
  await writeCoreResearchMatches({ filePath: paths.matchesPath, comparisons });
2529
2681
  await writeCoreResearchTrades({
2530
2682
  filePath: paths.tradesPath,
@@ -2542,6 +2694,7 @@ var analyzeCoreResearchPrepared = async (spec, options = {}) => {
2542
2694
  "utf8"
2543
2695
  );
2544
2696
  const artifactHashes = {
2697
+ ...prepared.manifest.artifactHashes,
2545
2698
  "spec.json": await sha256File(paths.specPath),
2546
2699
  "result.json": await sha256File(paths.resultPath),
2547
2700
  "comparison.json": await sha256File(paths.comparisonPath),
@@ -2832,6 +2985,7 @@ var runCoreResearch = async (params) => {
2832
2985
  const { spec } = params;
2833
2986
  const cwd = import_node_path6.default.resolve(params.cwd ?? process.cwd());
2834
2987
  const prepared = await prepareCoreResearch(spec);
2988
+ await verifyPreparedSupport(prepared);
2835
2989
  const resolvedVariants = [];
2836
2990
  for (const variant of spec.variants) {
2837
2991
  if (!variant.command?.length) {
@@ -3077,6 +3231,8 @@ var main = async () => {
3077
3231
  `Prepared ${loaded.spec.researchId}: ${result.paths.researchDir}`
3078
3232
  )
3079
3233
  );
3234
+ if (result.manifest.support)
3235
+ console.log(`Support: ${result.manifest.support} (support.json)`);
3080
3236
  return;
3081
3237
  }
3082
3238
  if (command === "analyze") {