You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
packages/loopover-engine/src/calibration/backtest-corpus.ts's buildBacktestCorpus(ruleId, fired, overrides) (#8083) turns raw RuleFiredEvent/HumanOverrideEvent history into a labeled BacktestCase[] corpus, but there is no way to get that history OUT of ORB's live D1 audit_events table and into a portable file a backtest scorer can run against repeatedly without re-querying the database each time. scripts/export-d1-core.ts + scripts/export-d1-data.ts already solve exactly this shape of problem for a different dataset (the full self-host D1 export): a pure, unit-tested "build the manifest" core file, plus a thin, untested IO wrapper that shells out to wrangler d1 execute --json and writes files. This issue mirrors that split for the rule-precision backtest corpus.
⚠️ Scope: ORB only. This issue exports the signal.rule_fired:* / signal.human_override:*audit_events rows recorded by src/review/signal-tracking-wire.ts (ORB's adapter, shipped in #7982) for one rule ID at a time, passed as a required CLI flag. Exporting AMS's local event-ledger equivalent is explicitly OUT OF SCOPE — do not add AMS support in this PR.
Requirements
Add a new pure file scripts/backtest-corpus-export-core.ts:
Export a function buildBacktestCorpusManifest(ruleId: string, cases: readonly BacktestCase[], meta: Record<string, unknown> = {}): BacktestCorpusManifest & Record<string, unknown> that returns { ruleId, caseCount: cases.length, checksum: <see below>, cases: [...cases], ...meta } — mirror scripts/export-d1-core.ts's buildExportManifest(tableExports, meta) signature and its "spread an optional meta bag into the result" shape exactly (read that function before starting). The CLI wrapper below uses meta to attach a generatedAt timestamp; this core file must not read the clock itself.
checksum is computed by a new exported function checksumCases(cases: readonly BacktestCase[]): string that: (1) canonicalizes each case's own key order (sort the object's keys, same technique as export-d1-core.ts's canonicalizeRow), (2) JSON-stringifies the resulting array, (3) hashes it with createHash("sha256").update(...).digest("hex") from node:crypto — mirror export-d1-core.ts's checksumRows function exactly (same algorithm, same canonicalize-then-hash order).
Import BacktestCase from packages/loopover-engine (the package import, not a relative cross-package path).
Add a thin CLI wrapper scripts/backtest-corpus-export.ts:
Mirror scripts/export-d1-data.ts's parseArgs shape and its d1Query helper (the spawnSync-based wrangler d1 execute --json shell-out) — do not introduce a different D1 access method.
Query audit_events for rows where event_type is signal.rule_fired:<ruleId> or signal.human_override:<ruleId> (and created_at >= --since-date when that flag is provided), ordered by created_at ascending.
Reconstruct RuleFiredEvent[] and HumanOverrideEvent[] from those raw rows using the same logic as src/review/signal-tracking-wire.ts's toRuleFiredEvent/toHumanOverrideEvent (the metadata-JSON-parsing and event-type-prefix-stripping approach). Those two functions are not currently exported from signal-tracking-wire.ts — copy their logic into this new file with a comment noting they mirror signal-tracking-wire.ts and must be kept in sync; do not modify signal-tracking-wire.ts itself (that file is ORB's live adapter, out of scope for this change).
Call buildBacktestCorpus (from packages/loopover-engine, calibration: pure BacktestCase corpus builder from RuleFiredEvent/HumanOverrideEvent pairs #8083) on the reconstructed events, then buildBacktestCorpusManifest (passing { generatedAt: new Date().toISOString() } as meta), then write the result as pretty-printed JSON (JSON.stringify(manifest, null, 2)) to --output via writeFileSync.
--remote and --since-date behave exactly as they do in export-d1-data.ts (remote vs. local D1; incremental vs. full).
test/unit/backtest-corpus-export-core.test.ts covering: checksum changes when cases changes; checksum is stable for the same input regardless of key order within each case object; caseCount matches cases.length; an empty corpus produces a valid manifest with caseCount: 0; meta fields are spread into the result.
scripts/backtest-corpus-export.ts (thin IO CLI). No dedicated test is required for this file — mirrors scripts/export-d1-data.ts, which itself carries no test — provided it contains no logic beyond argument parsing, the D1 shell-out, and calling the functions above. If any additional branching logic proves necessary, move it into backtest-corpus-export-core.ts instead, where it must be covered.
Test Coverage Requirements
99%+ patch coverage (branch-counted) on scripts/backtest-corpus-export-core.ts and its test, per this repo's standard gate — see scripts/export-d1-core.ts + test/unit/export-d1-core.test.ts as the existing precedent for this exact kind of file. scripts/backtest-corpus-export.ts (the thin wrapper) is explicitly exempt from its own dedicated test under the condition stated in Deliverables above, matching export-d1-data.ts's existing precedent.
Expected Outcome
A maintainer can run one command to snapshot a rule's full fired+override history into a portable, checksummed JSON file — the concrete input format the backtest scorer (a follow-up issue in this epic) reads.
Context
packages/loopover-engine/src/calibration/backtest-corpus.ts'sbuildBacktestCorpus(ruleId, fired, overrides)(#8083) turns rawRuleFiredEvent/HumanOverrideEventhistory into a labeledBacktestCase[]corpus, but there is no way to get that history OUT of ORB's live D1audit_eventstable and into a portable file a backtest scorer can run against repeatedly without re-querying the database each time.scripts/export-d1-core.ts+scripts/export-d1-data.tsalready solve exactly this shape of problem for a different dataset (the full self-host D1 export): a pure, unit-tested "build the manifest" core file, plus a thin, untested IO wrapper that shells out towrangler d1 execute --jsonand writes files. This issue mirrors that split for the rule-precision backtest corpus.Requirements
scripts/backtest-corpus-export-core.ts:buildBacktestCorpusManifest(ruleId: string, cases: readonly BacktestCase[], meta: Record<string, unknown> = {}): BacktestCorpusManifest & Record<string, unknown>that returns{ ruleId, caseCount: cases.length, checksum: <see below>, cases: [...cases], ...meta }— mirrorscripts/export-d1-core.ts'sbuildExportManifest(tableExports, meta)signature and its "spread an optionalmetabag into the result" shape exactly (read that function before starting). The CLI wrapper below usesmetato attach ageneratedAttimestamp; this core file must not read the clock itself.checksumis computed by a new exported functionchecksumCases(cases: readonly BacktestCase[]): stringthat: (1) canonicalizes each case's own key order (sort the object's keys, same technique asexport-d1-core.ts'scanonicalizeRow), (2) JSON-stringifies the resulting array, (3) hashes it withcreateHash("sha256").update(...).digest("hex")fromnode:crypto— mirrorexport-d1-core.ts'schecksumRowsfunction exactly (same algorithm, same canonicalize-then-hash order).BacktestCasefrompackages/loopover-engine(the package import, not a relative cross-package path).scripts/backtest-corpus-export.ts:scripts/export-d1-data.ts'sparseArgsshape and itsd1Queryhelper (thespawnSync-basedwrangler d1 execute --jsonshell-out) — do not introduce a different D1 access method.audit_eventsfor rows whereevent_typeissignal.rule_fired:<ruleId>orsignal.human_override:<ruleId>(andcreated_at >= --since-datewhen that flag is provided), ordered bycreated_atascending.RuleFiredEvent[]andHumanOverrideEvent[]from those raw rows using the same logic assrc/review/signal-tracking-wire.ts'stoRuleFiredEvent/toHumanOverrideEvent(the metadata-JSON-parsing and event-type-prefix-stripping approach). Those two functions are not currently exported fromsignal-tracking-wire.ts— copy their logic into this new file with a comment noting they mirrorsignal-tracking-wire.tsand must be kept in sync; do not modifysignal-tracking-wire.tsitself (that file is ORB's live adapter, out of scope for this change).buildBacktestCorpus(frompackages/loopover-engine, calibration: pure BacktestCase corpus builder from RuleFiredEvent/HumanOverrideEvent pairs #8083) on the reconstructed events, thenbuildBacktestCorpusManifest(passing{ generatedAt: new Date().toISOString() }asmeta), then write the result as pretty-printed JSON (JSON.stringify(manifest, null, 2)) to--outputviawriteFileSync.--remoteand--since-datebehave exactly as they do inexport-d1-data.ts(remote vs. local D1; incremental vs. full).Deliverables
scripts/backtest-corpus-export-core.ts(pure, exportedBacktestCorpusManifesttype,buildBacktestCorpusManifest,checksumCases).test/unit/backtest-corpus-export-core.test.tscovering: checksum changes whencaseschanges; checksum is stable for the same input regardless of key order within each case object;caseCountmatchescases.length; an empty corpus produces a valid manifest withcaseCount: 0;metafields are spread into the result.scripts/backtest-corpus-export.ts(thin IO CLI). No dedicated test is required for this file — mirrorsscripts/export-d1-data.ts, which itself carries no test — provided it contains no logic beyond argument parsing, the D1 shell-out, and calling the functions above. If any additional branching logic proves necessary, move it intobacktest-corpus-export-core.tsinstead, where it must be covered.Test Coverage Requirements
99%+ patch coverage (branch-counted) on
scripts/backtest-corpus-export-core.tsand its test, per this repo's standard gate — seescripts/export-d1-core.ts+test/unit/export-d1-core.test.tsas the existing precedent for this exact kind of file.scripts/backtest-corpus-export.ts(the thin wrapper) is explicitly exempt from its own dedicated test under the condition stated in Deliverables above, matchingexport-d1-data.ts's existing precedent.Expected Outcome
A maintainer can run one command to snapshot a rule's full fired+override history into a portable, checksummed JSON file — the concrete input format the backtest scorer (a follow-up issue in this epic) reads.
Links & Resources
buildBacktestCorpus,BacktestCase)scripts/export-d1-core.ts+scripts/export-d1-data.ts(the pure-core/thin-IO pattern this mirrors — read both in full before starting)src/review/signal-tracking-wire.ts(toRuleFiredEvent,toHumanOverrideEvent, the event-type prefix constants)test/unit/export-d1-core.test.ts(test-style precedent)