@tradejs/cli 3.1.29-beta.276 → 3.1.29-beta.278

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.
@@ -34,18 +34,346 @@ __export(strategyRelease_exports, {
34
34
  runStrategyReleaseCommand: () => runStrategyReleaseCommand
35
35
  });
36
36
  module.exports = __toCommonJS(strategyRelease_exports);
37
- var import_promises2 = __toESM(require("fs/promises"));
37
+ var import_promises3 = __toESM(require("fs/promises"));
38
38
  var import_node_fs = require("fs");
39
39
  var import_node_readline = require("readline");
40
- var import_node_path2 = __toESM(require("path"));
40
+ var import_node_path4 = __toESM(require("path"));
41
41
  var import_chalk = __toESM(require("chalk"));
42
42
 
43
- // src/lib/strategyRelease.ts
44
- var import_node_crypto = require("crypto");
43
+ // src/lib/strategyRelease/researchRetention.ts
45
44
  var import_promises = __toESM(require("fs/promises"));
45
+ var import_node_path2 = __toESM(require("path"));
46
+ var import_node_crypto = require("crypto");
47
+ var import_strategyReleaseEvidence = require("@tradejs/infra/strategyReleaseEvidence");
48
+
49
+ // src/lib/strategyRelease/researchFileConsumers.ts
46
50
  var import_node_path = __toESM(require("path"));
51
+ var import_node_child_process = require("child_process");
52
+ var import_node_util = require("util");
53
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
54
+ var runCommand = (command, args) => execFileAsync(command, args, { maxBuffer: 32 * 1024 * 1024 });
55
+ var GUEST_FILE_CHECK = `
56
+ import os, sys, json
57
+ if os.geteuid() != 0:
58
+ raise RuntimeError('Cannot inspect every container process')
59
+ wanted = set(json.loads(sys.argv[1]))
60
+ opened = set()
61
+ for pid in os.listdir('/proc'):
62
+ if not pid.isdigit():
63
+ continue
64
+ for kind in ('fd', 'map_files'):
65
+ directory = '/proc/' + pid + '/' + kind
66
+ try:
67
+ names = os.listdir(directory)
68
+ except FileNotFoundError:
69
+ continue
70
+ for name in names:
71
+ try:
72
+ target = os.readlink(directory + '/' + name)
73
+ except FileNotFoundError:
74
+ continue
75
+ if target in wanted:
76
+ opened.add(target)
77
+ print(json.dumps(sorted(opened)))
78
+ `;
79
+ var assertNoResearchFileConsumers = async (files, run = runCommand) => {
80
+ const virtualPids = /* @__PURE__ */ new Set();
81
+ const wanted = new Set(files.map((file) => import_node_path.default.resolve(file)));
82
+ let commonRoot = files.length ? import_node_path.default.dirname(files[0]) : "";
83
+ while (commonRoot && files.some((file) => {
84
+ const relative = import_node_path.default.relative(commonRoot, file);
85
+ return relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative);
86
+ }))
87
+ commonRoot = import_node_path.default.dirname(commonRoot);
88
+ const selectors = [];
89
+ if (files.length > 1e3 && commonRoot !== import_node_path.default.parse(commonRoot).root) {
90
+ selectors.push(["+D", commonRoot]);
91
+ } else {
92
+ for (let offset = 0; offset < files.length; offset += 100)
93
+ selectors.push(["--", ...files.slice(offset, offset + 100)]);
94
+ }
95
+ for (const selector of selectors) {
96
+ let output;
97
+ try {
98
+ output = (await run("lsof", ["-F", "pcn", ...selector])).stdout;
99
+ } catch (error) {
100
+ if (error.code === 1) continue;
101
+ throw error;
102
+ }
103
+ let pid = "";
104
+ let virtual = false;
105
+ for (const line of output.split("\n")) {
106
+ if (line.startsWith("p")) {
107
+ pid = line.slice(1);
108
+ virtual = false;
109
+ }
110
+ if (line.startsWith("c"))
111
+ virtual = line.startsWith("ccom.apple.Virtualization.");
112
+ if (line.startsWith("n")) {
113
+ if (!wanted.has(import_node_path.default.resolve(line.slice(1)))) continue;
114
+ if (!virtual || !/^\d+$/.test(pid))
115
+ throw new Error(
116
+ `Research cleanup found open files: ${line.slice(1)}`
117
+ );
118
+ virtualPids.add(pid);
119
+ }
120
+ }
121
+ }
122
+ if (!virtualPids.size) return;
123
+ for (const pid of virtualPids) {
124
+ const comm = (await run("ps", ["-p", pid, "-o", "comm="])).stdout.trim();
125
+ if (comm !== "/System/Library/Frameworks/Virtualization.framework/Versions/A/XPCServices/com.apple.Virtualization.VirtualMachine.xpc/Contents/MacOS/com.apple.Virtualization.VirtualMachine") {
126
+ throw new Error("Unrecognized virtual file consumer");
127
+ }
128
+ }
129
+ const contexts = JSON.parse(
130
+ (await run("docker", ["context", "inspect"])).stdout
131
+ );
132
+ if (contexts.length !== 1 || !contexts[0].Endpoints.docker.Host.startsWith("unix://")) {
133
+ throw new Error("Research file inspection requires a local Docker socket");
134
+ }
135
+ const ids = (await run("docker", ["ps", "-q"])).stdout.trim().split(/\s+/).filter(Boolean);
136
+ if (!ids.length)
137
+ throw new Error("Cannot resolve virtual research file handles");
138
+ const containers = JSON.parse(
139
+ (await run("docker", ["inspect", ...ids])).stdout
140
+ );
141
+ const covered = /* @__PURE__ */ new Set();
142
+ for (const container of containers) {
143
+ const guestFiles = /* @__PURE__ */ new Set();
144
+ for (const mount of container.Mounts) {
145
+ if (mount.Type !== "bind") continue;
146
+ for (const file of files) {
147
+ const relative = import_node_path.default.relative(mount.Source, file);
148
+ if (relative === "" || !relative.startsWith(`..${import_node_path.default.sep}`) && relative !== ".." && !import_node_path.default.isAbsolute(relative)) {
149
+ guestFiles.add(import_node_path.default.posix.join(mount.Destination, relative));
150
+ covered.add(file);
151
+ }
152
+ }
153
+ }
154
+ if (!guestFiles.size) continue;
155
+ const paths = [...guestFiles];
156
+ for (let offset = 0; offset < paths.length; offset += 250) {
157
+ const opened = JSON.parse(
158
+ (await run("docker", [
159
+ "exec",
160
+ container.Id,
161
+ "python",
162
+ "-c",
163
+ GUEST_FILE_CHECK,
164
+ JSON.stringify(paths.slice(offset, offset + 250))
165
+ ])).stdout
166
+ );
167
+ if (!Array.isArray(opened) || opened.length)
168
+ throw new Error("Research cleanup found open files in a container");
169
+ }
170
+ }
171
+ if (covered.size !== files.length)
172
+ throw new Error("Unresolved virtual research file handles");
173
+ };
174
+
175
+ // src/lib/strategyRelease/evidenceRetention.ts
176
+ var DAY_MS = 864e5;
177
+ var isResearchRetired = (research) => research?.status === "retired" && research.retirement?.policy === "discard-unrecoverable-without-rerun" && Boolean(research.retirement.reason.trim()) && Boolean(research.retirement.missingEvidence.length) && research.retirement.missingEvidence.every((item) => Boolean(item.trim()));
178
+ var planStrategyEvidenceRetention = ({
179
+ now,
180
+ entries,
181
+ research,
182
+ retentionDays = {
183
+ operational_redis: 3,
184
+ verbose_payload: 14,
185
+ research_payload: 0,
186
+ research_cache: 0,
187
+ verified_runtime_bundle: 90,
188
+ compact_ledger: null
189
+ }
190
+ }) => {
191
+ const keep = [];
192
+ const remove = [];
193
+ const retired = isResearchRetired(research);
194
+ const roles = new Set(
195
+ research?.permanentArtifacts.map((artifact) => artifact.role)
196
+ );
197
+ const terminalResearch = Boolean(
198
+ (research?.status === "completed" || retired) && Number.isFinite(research?.completedAt) && research.completedAt <= now && (retired ? ["note"] : ["note", "candidates", "metrics"]).every(
199
+ (role) => roles.has(role)
200
+ )
201
+ );
202
+ for (const entry of entries) {
203
+ const days = retentionDays[entry.kind];
204
+ const isResearch = entry.kind === "research_payload" || entry.kind === "research_cache";
205
+ const completedResearch = Boolean(
206
+ terminalResearch && research?.researchId === entry.researchId && /^[a-f0-9]{64}$/.test(entry.sha256 ?? "")
207
+ );
208
+ if (isResearch && !completedResearch || days == null || !entry.verified || !entry.aggregated && !(isResearch && retired) || !isResearch && now - entry.createdAt <= days * DAY_MS) {
209
+ keep.push(entry);
210
+ } else {
211
+ remove.push(entry);
212
+ }
213
+ }
214
+ return {
215
+ keep,
216
+ delete: remove,
217
+ bytesReclaimable: remove.reduce((total, entry) => total + entry.bytes, 0)
218
+ };
219
+ };
220
+
221
+ // src/lib/strategyRelease/researchRetention.ts
222
+ var SHA256_RE = /^[a-f0-9]{64}$/;
223
+ var mapFiles = async (entries, visit) => {
224
+ const results = new Array(entries.length);
225
+ let next = 0;
226
+ const workers = await Promise.allSettled(
227
+ Array.from({ length: Math.min(8, entries.length) }, async () => {
228
+ while (next < entries.length) {
229
+ const index = next++;
230
+ results[index] = await visit(entries[index]);
231
+ }
232
+ })
233
+ );
234
+ for (const worker of workers)
235
+ if (worker.status === "rejected") throw worker.reason;
236
+ return results;
237
+ };
238
+ var fileSha256 = async (file, bytes) => bytes <= 1024 * 1024 ? (0, import_node_crypto.createHash)("sha256").update(await import_promises.default.readFile(file)).digest("hex") : (0, import_strategyReleaseEvidence.strategyEvidenceFileSha256)(file);
239
+ var inside = (root, file) => {
240
+ const relative = import_node_path2.default.relative(root, file);
241
+ return relative !== "" && !relative.startsWith(`..${import_node_path2.default.sep}`) && relative !== ".." && !import_node_path2.default.isAbsolute(relative);
242
+ };
243
+ var applyResearchRetention = async (options) => {
244
+ const projectRoot = await import_promises.default.realpath(options.projectRoot);
245
+ const dataRoot = import_node_path2.default.join(projectRoot, "data");
246
+ const resolve = (file) => import_node_path2.default.resolve(projectRoot, file);
247
+ const receiptPath = resolve(options.receiptPath);
248
+ const journalPath = `${receiptPath}.deleted.jsonl`;
249
+ const permanent = new Set(
250
+ options.research.permanentArtifacts.map((entry) => resolve(entry.path))
251
+ );
252
+ if (!inside(projectRoot, receiptPath) || permanent.has(receiptPath) || permanent.has(journalPath)) {
253
+ throw new Error(
254
+ "Cleanup receipt must be separate from permanent research artifacts"
255
+ );
256
+ }
257
+ permanent.add(receiptPath);
258
+ permanent.add(journalPath);
259
+ for (const file of options.protectedPaths ?? []) permanent.add(resolve(file));
260
+ const retired = isResearchRetired(options.research);
261
+ if (options.research.status !== "completed" && !retired || !Number.isFinite(options.research.completedAt) || options.research.completedAt > Date.now()) {
262
+ throw new Error("Research must be completed before cleanup");
263
+ }
264
+ for (const role of retired ? ["note"] : ["note", "candidates", "metrics"]) {
265
+ if (!options.research.permanentArtifacts.some((entry) => entry.role === role)) {
266
+ throw new Error(`Missing permanent research ${role}`);
267
+ }
268
+ }
269
+ await mapFiles(options.research.permanentArtifacts, async (artifact) => {
270
+ const file = resolve(artifact.path);
271
+ if (!inside(projectRoot, await import_promises.default.realpath(file)) || !SHA256_RE.test(artifact.sha256) || await fileSha256(file, (await import_promises.default.stat(file)).size) !== artifact.sha256) {
272
+ throw new Error(
273
+ `Permanent research artifact checksum/path mismatch: ${file}`
274
+ );
275
+ }
276
+ if (artifact.role === "note") {
277
+ const handle = await import_promises.default.open(file, "r");
278
+ try {
279
+ const header = Buffer.alloc(4096);
280
+ const { bytesRead } = await handle.read(header, 0, header.length, 0);
281
+ const frontmatter = header.subarray(0, bytesRead).toString("utf8").match(/^---\r?\n([\s\S]*?)\r?\n---/);
282
+ if (!frontmatter || !/^schema:\s*tradejs-research\/v1\s*$/m.test(frontmatter[1]) || !(retired ? /^reproduction:\s*(?:complete|partial|blocked|legacy-partial)\s*$/m.test(
283
+ frontmatter[1]
284
+ ) : /^reproduction:\s*complete\s*$/m.test(frontmatter[1]))) {
285
+ throw new Error(
286
+ `Research note must have reproduction: complete: ${file}`
287
+ );
288
+ }
289
+ } finally {
290
+ await handle.close();
291
+ }
292
+ }
293
+ });
294
+ const checkedPaths = /* @__PURE__ */ new Set();
295
+ const checked = await mapFiles(options.entries, async (entry) => {
296
+ const file = resolve(entry.path);
297
+ if (!["research_payload", "research_cache"].includes(entry.kind) || entry.researchId !== options.research.researchId || !entry.verified || !entry.aggregated && !retired || !SHA256_RE.test(entry.sha256 ?? "") || !inside(dataRoot, file) || permanent.has(file) || /\.(?:md|patch|diff)$/i.test(file)) {
298
+ throw new Error(`Not a disposable research file: ${file}`);
299
+ }
300
+ if (checkedPaths.has(file))
301
+ throw new Error(`Duplicate cleanup path: ${file}`);
302
+ checkedPaths.add(file);
303
+ if (await import_promises.default.realpath(file) !== file)
304
+ throw new Error(`Research cleanup refuses symlinks: ${file}`);
305
+ const stat = await import_promises.default.lstat(file);
306
+ if (!stat.isFile() || stat.size !== entry.bytes || await fileSha256(file, stat.size) !== entry.sha256) {
307
+ throw new Error(
308
+ `Disposable research file checksum/size mismatch: ${file}`
309
+ );
310
+ }
311
+ return { entry, file, mtimeMs: stat.mtimeMs, ino: stat.ino };
312
+ });
313
+ await assertNoResearchFileConsumers(checked.map((entry) => entry.file));
314
+ const receipt = {
315
+ schema: "tradejs-research-cleanup-receipt/v1",
316
+ researchId: options.research.researchId,
317
+ completedAt: options.research.completedAt,
318
+ ...retired ? { retirement: options.research.retirement } : {},
319
+ journalPath,
320
+ permanentArtifacts: options.research.permanentArtifacts,
321
+ scope: "reported aggregates and frozen candidates; new trade-level analyses require a rerun",
322
+ status: "planned",
323
+ files: checked.map(({ entry, file }) => ({
324
+ path: file,
325
+ sha256: entry.sha256,
326
+ bytes: entry.bytes,
327
+ removed: false
328
+ }))
329
+ };
330
+ if (!options.apply) return receipt;
331
+ await import_promises.default.mkdir(import_node_path2.default.dirname(receiptPath), { recursive: true });
332
+ if (await import_promises.default.realpath(import_node_path2.default.dirname(receiptPath)) !== import_node_path2.default.dirname(receiptPath)) {
333
+ throw new Error("Cleanup receipt refuses symlinked directories");
334
+ }
335
+ const lock = await import_promises.default.open(`${receiptPath}.lock`, "wx");
336
+ let journal;
337
+ const writeReceipt = async () => {
338
+ const temp = `${receiptPath}.${process.pid}.tmp`;
339
+ await import_promises.default.writeFile(temp, `${JSON.stringify(receipt, null, 2)}
340
+ `, "utf8");
341
+ await import_promises.default.rename(temp, receiptPath);
342
+ };
343
+ try {
344
+ journal = await import_promises.default.open(journalPath, "wx");
345
+ receipt.status = "applying";
346
+ await writeReceipt();
347
+ for (let index = 0; index < checked.length; index += 1) {
348
+ const { file, entry, ino, mtimeMs } = checked[index];
349
+ const stat = await import_promises.default.lstat(file);
350
+ if (!stat.isFile() || stat.ino !== ino || stat.mtimeMs !== mtimeMs || stat.size !== entry.bytes) {
351
+ throw new Error(`Research file changed during cleanup: ${file}`);
352
+ }
353
+ await import_promises.default.unlink(file);
354
+ receipt.files[index].removed = true;
355
+ await journal.write(`${JSON.stringify(receipt.files[index])}
356
+ `);
357
+ }
358
+ receipt.status = "completed";
359
+ await writeReceipt();
360
+ return receipt;
361
+ } catch (error) {
362
+ if (journal) await writeReceipt();
363
+ throw error;
364
+ } finally {
365
+ await journal?.close();
366
+ await lock.close();
367
+ await import_promises.default.unlink(`${receiptPath}.lock`);
368
+ }
369
+ };
370
+
371
+ // src/lib/strategyRelease.ts
372
+ var import_node_crypto2 = require("crypto");
373
+ var import_promises2 = __toESM(require("fs/promises"));
374
+ var import_node_path3 = __toESM(require("path"));
47
375
  var import_types2 = require("@tradejs/types");
48
- var import_strategyReleaseEvidence = require("@tradejs/infra/strategyReleaseEvidence");
376
+ var import_strategyReleaseEvidence2 = require("@tradejs/infra/strategyReleaseEvidence");
49
377
 
50
378
  // src/lib/strategyRelease/liveDiagnosis.ts
51
379
  var import_types = require("@tradejs/types");
@@ -107,7 +435,7 @@ var buildStrategyLiveDiagnosis = (input) => {
107
435
  };
108
436
 
109
437
  // src/lib/strategyRelease/monitoringProfile.ts
110
- var DAY_MS = 864e5;
438
+ var DAY_MS2 = 864e5;
111
439
  function assert(condition, message) {
112
440
  if (!condition) throw new Error(`Invalid strategy release: ${message}`);
113
441
  }
@@ -163,13 +491,13 @@ var buildStrategyMonitoringProfile = ({
163
491
  Number.isInteger(windowDays) && windowDays > 0,
164
492
  "monitoring profile days must be positive integers"
165
493
  );
166
- const duration = windowDays * DAY_MS;
494
+ const duration = windowDays * DAY_MS2;
167
495
  assert(
168
496
  endTime - startTime >= duration,
169
497
  `${windowDays}d monitoring window exceeds historical coverage`
170
498
  );
171
499
  const drawdowns = [];
172
- for (let windowStart = startTime; windowStart + duration <= endTime; windowStart += DAY_MS) {
500
+ for (let windowStart = startTime; windowStart + duration <= endTime; windowStart += DAY_MS2) {
173
501
  const windowEnd = windowStart + duration;
174
502
  drawdowns.push(
175
503
  realizedDrawdown(
@@ -197,54 +525,25 @@ var buildStrategyMonitoringProfile = ({
197
525
  };
198
526
  };
199
527
 
200
- // src/lib/strategyRelease/evidenceRetention.ts
201
- var DAY_MS2 = 864e5;
202
- var planStrategyEvidenceRetention = ({
203
- now,
204
- entries,
205
- retentionDays = {
206
- operational_redis: 3,
207
- verbose_payload: 14,
208
- verified_runtime_bundle: 90,
209
- compact_ledger: null
210
- }
211
- }) => {
212
- const keep = [];
213
- const remove = [];
214
- for (const entry of entries) {
215
- const days = retentionDays[entry.kind];
216
- if (days == null || !entry.verified || !entry.aggregated || now - entry.createdAt <= days * DAY_MS2) {
217
- keep.push(entry);
218
- } else {
219
- remove.push(entry);
220
- }
221
- }
222
- return {
223
- keep,
224
- delete: remove,
225
- bytesReclaimable: remove.reduce((total, entry) => total + entry.bytes, 0)
226
- };
227
- };
228
-
229
528
  // src/lib/strategyRelease.ts
230
- var SHA256_RE = /^[a-f0-9]{64}$/;
529
+ var SHA256_RE2 = /^[a-f0-9]{64}$/;
231
530
  var LINEAGE_FINGERPRINT_RE = /^[a-f0-9]{16}$/;
232
531
  var STRATEGY_REVISION_RE = /^sr1:[a-f0-9]{16}$/;
233
532
  var DEPLOYMENT_COMPOSITION_ID_RE = /^dc1:[a-f0-9]{16}$/;
234
533
  var REQUIRED_LONG_WINDOWS = [1095, 1460, 1825];
235
534
  var REQUIRED_DIAGNOSTIC_TERMINAL_WINDOWS = [365, 180, 90];
236
535
  var MIN_RECENT_DIRECTION_REPAIR_EVENTS = 20;
237
- var safeSegment = import_strategyReleaseEvidence.safeStrategyEvidenceSegment;
238
- var compactTimestamp = import_strategyReleaseEvidence.compactStrategyEvidenceTimestamp;
536
+ var safeSegment = import_strategyReleaseEvidence2.safeStrategyEvidenceSegment;
537
+ var compactTimestamp = import_strategyReleaseEvidence2.compactStrategyEvidenceTimestamp;
239
538
  var verifyFullPeriodChartArtifact = async (input) => {
240
539
  const artifact = input.chartArtifact;
241
- if (!artifact?.path || !SHA256_RE.test(artifact.sha256)) return false;
540
+ if (!artifact?.path || !SHA256_RE2.test(artifact.sha256)) return false;
242
541
  try {
243
- const artifactPath = import_node_path.default.resolve(artifact.path);
244
- if (await (0, import_strategyReleaseEvidence.strategyEvidenceFileSha256)(artifactPath) !== artifact.sha256) {
542
+ const artifactPath = import_node_path3.default.resolve(artifact.path);
543
+ if (await (0, import_strategyReleaseEvidence2.strategyEvidenceFileSha256)(artifactPath) !== artifact.sha256) {
245
544
  return false;
246
545
  }
247
- const report = JSON.parse(await import_promises.default.readFile(artifactPath, "utf8"));
546
+ const report = JSON.parse(await import_promises2.default.readFile(artifactPath, "utf8"));
248
547
  return Boolean(
249
548
  report.chart?.persisted === true && report.chart.cardIds?.length && report.errors?.failed === 0 && report.run?.strategy === input.strategy && report.run.mode === "local-deterministic" && report.run.recent === 0 && report.run.since == null && report.run.until == null && Number(report.run.sourceRows) > 0
250
549
  );
@@ -253,13 +552,13 @@ var verifyFullPeriodChartArtifact = async (input) => {
253
552
  }
254
553
  };
255
554
  var readVerifiedJsonArtifact = async (artifact) => {
256
- if (!artifact?.path || !SHA256_RE.test(artifact.sha256)) return null;
555
+ if (!artifact?.path || !SHA256_RE2.test(artifact.sha256)) return null;
257
556
  try {
258
- const artifactPath = import_node_path.default.resolve(artifact.path);
259
- if (await (0, import_strategyReleaseEvidence.strategyEvidenceFileSha256)(artifactPath) !== artifact.sha256) {
557
+ const artifactPath = import_node_path3.default.resolve(artifact.path);
558
+ if (await (0, import_strategyReleaseEvidence2.strategyEvidenceFileSha256)(artifactPath) !== artifact.sha256) {
260
559
  return null;
261
560
  }
262
- return JSON.parse(await import_promises.default.readFile(artifactPath, "utf8"));
561
+ return JSON.parse(await import_promises2.default.readFile(artifactPath, "utf8"));
263
562
  } catch {
264
563
  return null;
265
564
  }
@@ -272,7 +571,7 @@ var verifyResearchDecisionLineage = async (input) => {
272
571
  if (!progress || !selected) return false;
273
572
  const progressSelection = progress.selectedComposition;
274
573
  return Boolean(
275
- progress.schema === "tradejs-release-progress/v2" && progress.strategy === input.strategy && progress.status === "complete" && progress.verdictAllowed === true && typeof progress.lineageId === "string" && progress.lineageId.length > 0 && typeof progress.objectiveFingerprint === "string" && SHA256_RE.test(progress.objectiveFingerprint) && typeof progressSelection?.candidateId === "string" && progressSelection.candidateId.length > 0 && typeof progressSelection.compositionFingerprint === "string" && SHA256_RE.test(progressSelection.compositionFingerprint) && progressSelection.artifactSha256 === input.selectedCompositionArtifact?.sha256 && selected.schema === "tradejs-release-selected-composition/v2" && selected.strategy === input.strategy && selected.lineageId === progress.lineageId && selected.objectiveFingerprint === progress.objectiveFingerprint && selected.candidateId === progressSelection.candidateId && selected.compositionFingerprint === progressSelection.compositionFingerprint && selected.chartSha256 === input.chartArtifact?.sha256
574
+ progress.schema === "tradejs-release-progress/v2" && progress.strategy === input.strategy && progress.status === "complete" && progress.verdictAllowed === true && typeof progress.lineageId === "string" && progress.lineageId.length > 0 && typeof progress.objectiveFingerprint === "string" && SHA256_RE2.test(progress.objectiveFingerprint) && typeof progressSelection?.candidateId === "string" && progressSelection.candidateId.length > 0 && typeof progressSelection.compositionFingerprint === "string" && SHA256_RE2.test(progressSelection.compositionFingerprint) && progressSelection.artifactSha256 === input.selectedCompositionArtifact?.sha256 && selected.schema === "tradejs-release-selected-composition/v2" && selected.strategy === input.strategy && selected.lineageId === progress.lineageId && selected.objectiveFingerprint === progress.objectiveFingerprint && selected.candidateId === progressSelection.candidateId && selected.compositionFingerprint === progressSelection.compositionFingerprint && selected.chartSha256 === input.chartArtifact?.sha256
276
575
  );
277
576
  };
278
577
  var hasExactRuntimeTarget = (target, strategyName) => Boolean(
@@ -413,12 +712,12 @@ var deriveStrategyReleaseResearchDecision = async (input) => {
413
712
  summary: "Start the exact frozen composition as a micro-forward test at MAX_LOSS_VALUE=1; do not retune an underpowered or exposed recent tail."
414
713
  };
415
714
  };
416
- var canonicalStrategyReleaseJson = (value) => (0, import_strategyReleaseEvidence.canonicalStrategyEvidenceJson)(value);
417
- var strategyReleaseSha256 = (value) => (0, import_strategyReleaseEvidence.strategyEvidenceSha256)(value);
715
+ var canonicalStrategyReleaseJson = (value) => (0, import_strategyReleaseEvidence2.canonicalStrategyEvidenceJson)(value);
716
+ var strategyReleaseSha256 = (value) => (0, import_strategyReleaseEvidence2.strategyEvidenceSha256)(value);
418
717
  function assert2(condition, message) {
419
718
  if (!condition) throw new Error(`Invalid strategy release: ${message}`);
420
719
  }
421
- var assertFingerprint = (value, label) => assert2(SHA256_RE.test(value), `${label} must be a lowercase SHA-256`);
720
+ var assertFingerprint = (value, label) => assert2(SHA256_RE2.test(value), `${label} must be a lowercase SHA-256`);
422
721
  var assertLineageFingerprint = (value, label) => assert2(
423
722
  LINEAGE_FINGERPRINT_RE.test(value),
424
723
  `${label} must be a 16-character lowercase lineage fingerprint`
@@ -808,7 +1107,7 @@ var createStrategyReleaseEnvelope = (manifest) => ({
808
1107
  manifest
809
1108
  });
810
1109
  var verifyStrategyReleaseEnvelope = async (valueOrPath) => {
811
- const value = typeof valueOrPath === "string" ? JSON.parse(await import_promises.default.readFile(valueOrPath, "utf8")) : valueOrPath;
1110
+ const value = typeof valueOrPath === "string" ? JSON.parse(await import_promises2.default.readFile(valueOrPath, "utf8")) : valueOrPath;
812
1111
  const envelope = value;
813
1112
  assert2(
814
1113
  envelope?.schema === "tradejs-strategy-release-envelope/v1",
@@ -859,8 +1158,8 @@ var verifyStrategyReleaseEnvelope = async (valueOrPath) => {
859
1158
  return envelope;
860
1159
  };
861
1160
  var verifyStrategyEvidenceMarkerEnvelope = async (valueOrPath) => {
862
- const value = typeof valueOrPath === "string" ? JSON.parse(await import_promises.default.readFile(valueOrPath, "utf8")) : valueOrPath;
863
- return (0, import_strategyReleaseEvidence.verifyStrategyEvidenceMarkerEnvelope)(value);
1161
+ const value = typeof valueOrPath === "string" ? JSON.parse(await import_promises2.default.readFile(valueOrPath, "utf8")) : valueOrPath;
1162
+ return (0, import_strategyReleaseEvidence2.verifyStrategyEvidenceMarkerEnvelope)(value);
864
1163
  };
865
1164
  var releaseMarkers = (manifest) => {
866
1165
  const common = {
@@ -921,21 +1220,21 @@ var releaseMarkers = (manifest) => {
921
1220
  };
922
1221
  var writeJsonAtomic = async (filePath, value) => {
923
1222
  const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
924
- await import_promises.default.mkdir(import_node_path.default.dirname(filePath), { recursive: true });
925
- await import_promises.default.writeFile(
1223
+ await import_promises2.default.mkdir(import_node_path3.default.dirname(filePath), { recursive: true });
1224
+ await import_promises2.default.writeFile(
926
1225
  temporaryPath,
927
1226
  `${canonicalStrategyReleaseJson(value)}
928
1227
  `,
929
1228
  "utf8"
930
1229
  );
931
- await import_promises.default.rename(temporaryPath, filePath);
1230
+ await import_promises2.default.rename(temporaryPath, filePath);
932
1231
  };
933
1232
  var publishStrategyRelease = async ({
934
1233
  rootDir,
935
1234
  manifest
936
1235
  }) => {
937
1236
  const envelope = createStrategyReleaseEnvelope(manifest);
938
- const markerEnvelope = (0, import_strategyReleaseEvidence.createStrategyEvidenceMarkerEnvelope)({
1237
+ const markerEnvelope = (0, import_strategyReleaseEvidence2.createStrategyEvidenceMarkerEnvelope)({
939
1238
  strategy: manifest.strategy,
940
1239
  createdAt: manifest.createdAt,
941
1240
  markers: releaseMarkers(manifest),
@@ -945,13 +1244,13 @@ var publishStrategyRelease = async ({
945
1244
  path: entry.path
946
1245
  }))
947
1246
  });
948
- const releasePath = import_node_path.default.join(
1247
+ const releasePath = import_node_path3.default.join(
949
1248
  rootDir,
950
1249
  "releases",
951
1250
  safeSegment(manifest.strategy, "strategy"),
952
1251
  `${manifest.releaseId}.json`
953
1252
  );
954
- const markerPath = import_node_path.default.join(
1253
+ const markerPath = import_node_path3.default.join(
955
1254
  rootDir,
956
1255
  "markers",
957
1256
  safeSegment(manifest.strategy, "strategy"),
@@ -1025,19 +1324,19 @@ var publishStrategyLiveDiagnosis = async ({
1025
1324
  severity: diagnosis.verdict === "EXPECTED_DRAWDOWN" ? "info" : "warning"
1026
1325
  }
1027
1326
  ];
1028
- const markerEnvelope = (0, import_strategyReleaseEvidence.createStrategyEvidenceMarkerEnvelope)({
1327
+ const markerEnvelope = (0, import_strategyReleaseEvidence2.createStrategyEvidenceMarkerEnvelope)({
1029
1328
  strategy: diagnosis.strategy,
1030
1329
  createdAt: diagnosis.createdAt,
1031
1330
  markers,
1032
1331
  sourceArtifacts
1033
1332
  });
1034
- const diagnosisPath = import_node_path.default.join(
1333
+ const diagnosisPath = import_node_path3.default.join(
1035
1334
  rootDir,
1036
1335
  "diagnoses",
1037
1336
  safeSegment(diagnosis.strategy, "strategy"),
1038
1337
  `${diagnosisId}.json`
1039
1338
  );
1040
- const markerPath = import_node_path.default.join(
1339
+ const markerPath = import_node_path3.default.join(
1041
1340
  rootDir,
1042
1341
  "markers",
1043
1342
  safeSegment(diagnosis.strategy, "strategy"),
@@ -1227,11 +1526,11 @@ var verifyCompletedCoreResearchBundle = async ({
1227
1526
  const specSha256 = finiteString(artifact.specSha256);
1228
1527
  assert2(researchId != null, "core result researchId is required");
1229
1528
  assert2(specSha256 != null, "core result specSha256 is required");
1230
- const researchDir = import_node_path.default.dirname(resultPath);
1231
- const manifestPath = import_node_path.default.join(researchDir, "manifest.json");
1232
- const specPath = import_node_path.default.join(researchDir, "spec.json");
1233
- const manifestBytes = await import_promises.default.readFile(manifestPath).catch(() => null);
1234
- const specBytes = await import_promises.default.readFile(specPath).catch(() => null);
1529
+ const researchDir = import_node_path3.default.dirname(resultPath);
1530
+ const manifestPath = import_node_path3.default.join(researchDir, "manifest.json");
1531
+ const specPath = import_node_path3.default.join(researchDir, "spec.json");
1532
+ const manifestBytes = await import_promises2.default.readFile(manifestPath).catch(() => null);
1533
+ const specBytes = await import_promises2.default.readFile(specPath).catch(() => null);
1235
1534
  assert2(
1236
1535
  manifestBytes != null && specBytes != null,
1237
1536
  "core result must belong to a completed core research bundle"
@@ -1240,22 +1539,22 @@ var verifyCompletedCoreResearchBundle = async ({
1240
1539
  const artifactHashes = manifest.artifactHashes != null && typeof manifest.artifactHashes === "object" && !Array.isArray(manifest.artifactHashes) ? manifest.artifactHashes : null;
1241
1540
  const parsedSpec = JSON.parse(specBytes.toString("utf8"));
1242
1541
  assert2(
1243
- manifest.schema === "tradejs-core-research-manifest/v1" && manifest.status === "completed" && manifest.researchId === researchId && manifest.specSha256 === specSha256 && artifactHashes?.["result.json"] === resultSha256 && (0, import_node_crypto.createHash)("sha256").update(canonicalStrategyReleaseJson(parsedSpec)).digest("hex") === specSha256,
1542
+ manifest.schema === "tradejs-core-research-manifest/v1" && manifest.status === "completed" && manifest.researchId === researchId && manifest.specSha256 === specSha256 && artifactHashes?.["result.json"] === resultSha256 && (0, import_node_crypto2.createHash)("sha256").update(canonicalStrategyReleaseJson(parsedSpec)).digest("hex") === specSha256,
1244
1543
  "core result is not bound by its completed core research manifest"
1245
1544
  );
1246
1545
  for (const [relativePath, expectedSha256] of Object.entries(
1247
1546
  artifactHashes ?? {}
1248
1547
  )) {
1249
1548
  assert2(
1250
- !import_node_path.default.isAbsolute(relativePath) && !relativePath.split(/[\\/]/).includes(".."),
1549
+ !import_node_path3.default.isAbsolute(relativePath) && !relativePath.split(/[\\/]/).includes(".."),
1251
1550
  `core bundle artifact ${relativePath} escapes its research directory`
1252
1551
  );
1253
1552
  assert2(
1254
- typeof expectedSha256 === "string" && SHA256_RE.test(expectedSha256),
1553
+ typeof expectedSha256 === "string" && SHA256_RE2.test(expectedSha256),
1255
1554
  `core bundle artifact ${relativePath} has an invalid checksum`
1256
1555
  );
1257
- const actualSha256 = await (0, import_strategyReleaseEvidence.strategyEvidenceFileSha256)(
1258
- import_node_path.default.join(researchDir, relativePath)
1556
+ const actualSha256 = await (0, import_strategyReleaseEvidence2.strategyEvidenceFileSha256)(
1557
+ import_node_path3.default.join(researchDir, relativePath)
1259
1558
  );
1260
1559
  assert2(
1261
1560
  actualSha256 === expectedSha256,
@@ -1266,7 +1565,7 @@ var verifyCompletedCoreResearchBundle = async ({
1266
1565
  async function collectReleaseEvidenceReferences(references) {
1267
1566
  return Promise.all(
1268
1567
  references.map(async (reference) => {
1269
- const actualSha256 = await (0, import_strategyReleaseEvidence.strategyEvidenceFileSha256)(reference.path);
1568
+ const actualSha256 = await (0, import_strategyReleaseEvidence2.strategyEvidenceFileSha256)(reference.path);
1270
1569
  assert2(
1271
1570
  actualSha256 === reference.sha256,
1272
1571
  `evidence ${reference.artifactId} checksum mismatch`
@@ -1274,7 +1573,7 @@ async function collectReleaseEvidenceReferences(references) {
1274
1573
  let artifact;
1275
1574
  try {
1276
1575
  const parsed = JSON.parse(
1277
- await import_promises.default.readFile(reference.path, "utf8")
1576
+ await import_promises2.default.readFile(reference.path, "utf8")
1278
1577
  );
1279
1578
  assert2(
1280
1579
  parsed != null && typeof parsed === "object" && !Array.isArray(parsed),
@@ -1433,17 +1732,17 @@ var required = (value, name) => {
1433
1732
  };
1434
1733
  var writeJsonAtomic2 = async (filePath, value) => {
1435
1734
  const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
1436
- await import_promises2.default.mkdir(import_node_path2.default.dirname(filePath), { recursive: true });
1437
- await import_promises2.default.writeFile(
1735
+ await import_promises3.default.mkdir(import_node_path4.default.dirname(filePath), { recursive: true });
1736
+ await import_promises3.default.writeFile(
1438
1737
  temporaryPath,
1439
1738
  `${JSON.stringify(value, null, 2)}
1440
1739
  `,
1441
1740
  "utf8"
1442
1741
  );
1443
- await import_promises2.default.rename(temporaryPath, filePath);
1742
+ await import_promises3.default.rename(temporaryPath, filePath);
1444
1743
  };
1445
1744
  var runStrategyReleaseCommand = async (options) => {
1446
- const inputPath = import_node_path2.default.resolve(options.inputPath);
1745
+ const inputPath = import_node_path4.default.resolve(options.inputPath);
1447
1746
  if (options.command === "verify") {
1448
1747
  const envelope = await verifyStrategyReleaseEnvelope(inputPath);
1449
1748
  return {
@@ -1481,7 +1780,7 @@ var runStrategyReleaseCommand = async (options) => {
1481
1780
  endTime,
1482
1781
  days: options.days?.length ? options.days : [7, 30, 90]
1483
1782
  });
1484
- const outputPath = import_node_path2.default.resolve(
1783
+ const outputPath = import_node_path4.default.resolve(
1485
1784
  options.outputPath ?? "output/strategy-monitoring-profile.json"
1486
1785
  );
1487
1786
  await writeJsonAtomic2(outputPath, profile);
@@ -1492,22 +1791,40 @@ var runStrategyReleaseCommand = async (options) => {
1492
1791
  profile
1493
1792
  };
1494
1793
  }
1495
- const input = JSON.parse(await import_promises2.default.readFile(inputPath, "utf8"));
1794
+ const input = JSON.parse(await import_promises3.default.readFile(inputPath, "utf8"));
1496
1795
  if (options.command === "decide") {
1497
1796
  const decision = await deriveStrategyReleaseResearchDecision(input);
1498
1797
  if (options.outputPath) {
1499
- await writeJsonAtomic2(import_node_path2.default.resolve(options.outputPath), decision);
1798
+ await writeJsonAtomic2(import_node_path4.default.resolve(options.outputPath), decision);
1500
1799
  }
1501
1800
  return {
1502
1801
  kind: "decided",
1503
1802
  ...decision,
1504
- outputPath: options.outputPath ? import_node_path2.default.resolve(options.outputPath) : null
1803
+ outputPath: options.outputPath ? import_node_path4.default.resolve(options.outputPath) : null
1505
1804
  };
1506
1805
  }
1507
1806
  if (options.command === "retention") {
1508
1807
  const plan = planStrategyEvidenceRetention(input);
1509
- if (options.apply) {
1510
- await Promise.all(plan.delete.map((entry) => import_promises2.default.rm(entry.path)));
1808
+ const researchEntries = plan.delete.filter(
1809
+ (entry) => entry.kind === "research_payload" || entry.kind === "research_cache"
1810
+ );
1811
+ const receiptPath = options.outputPath ?? `${inputPath}.receipt.json`;
1812
+ if (researchEntries.length) {
1813
+ if (researchEntries.length !== plan.delete.length) {
1814
+ throw new Error(
1815
+ "Research cleanup requires a separate inventory from runtime retention"
1816
+ );
1817
+ }
1818
+ await applyResearchRetention({
1819
+ projectRoot: process.env.PROJECT_CWD ?? process.cwd(),
1820
+ research: input.research,
1821
+ entries: researchEntries,
1822
+ receiptPath,
1823
+ apply: Boolean(options.apply),
1824
+ protectedPaths: [inputPath]
1825
+ });
1826
+ } else if (options.apply) {
1827
+ await Promise.all(plan.delete.map((entry) => import_promises3.default.rm(entry.path)));
1511
1828
  }
1512
1829
  return {
1513
1830
  kind: "retention",
@@ -1515,17 +1832,18 @@ var runStrategyReleaseCommand = async (options) => {
1515
1832
  keepCount: plan.keep.length,
1516
1833
  deleteCount: plan.delete.length,
1517
1834
  bytesReclaimable: plan.bytesReclaimable,
1518
- delete: plan.delete.map((entry) => entry.path)
1835
+ delete: plan.delete.map((entry) => entry.path),
1836
+ ...researchEntries.length && options.apply ? { receiptPath } : {}
1519
1837
  };
1520
1838
  }
1521
1839
  if (options.command === "diagnose") {
1522
1840
  const diagnosis = buildStrategyLiveDiagnosis(input);
1523
- const outputPath = import_node_path2.default.resolve(
1841
+ const outputPath = import_node_path4.default.resolve(
1524
1842
  options.outputPath ?? "output/strategy-live-diagnosis.json"
1525
1843
  );
1526
1844
  await writeJsonAtomic2(outputPath, diagnosis);
1527
1845
  const published2 = options.rootDir ? await publishStrategyLiveDiagnosis({
1528
- rootDir: import_node_path2.default.resolve(options.rootDir),
1846
+ rootDir: import_node_path4.default.resolve(options.rootDir),
1529
1847
  diagnosis,
1530
1848
  sourceArtifacts: input.sourceArtifacts ?? []
1531
1849
  }) : null;
@@ -1542,7 +1860,7 @@ var runStrategyReleaseCommand = async (options) => {
1542
1860
  evidence: await collectReleaseEvidenceReferences(input.evidence ?? [])
1543
1861
  });
1544
1862
  const published = await publishStrategyRelease({
1545
- rootDir: import_node_path2.default.resolve(options.rootDir ?? "data/strategy-release"),
1863
+ rootDir: import_node_path4.default.resolve(options.rootDir ?? "data/strategy-release"),
1546
1864
  manifest
1547
1865
  });
1548
1866
  return {