Skip to content

scripts: export the rule-precision backtest corpus as a versioned, checksummed JSON snapshot #8084

Description

@JSONbored

Context

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 type:
      export type BacktestCorpusManifest = {
        ruleId: string;
        caseCount: number;
        checksum: string;
        cases: BacktestCase[];
      };
    • 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:
    tsx scripts/backtest-corpus-export.ts --rule-id <ruleId> --output <file.json> [--remote] [--since-date <iso>]
    
    • 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).

Deliverables

  • scripts/backtest-corpus-export-core.ts (pure, exported BacktestCorpusManifest type, buildBacktestCorpusManifest, checksumCases).
  • 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.

Links & Resources

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:featureGittensor-scored feature linked to a feature issue — scores a 0.25x multiplier.help wantedExtra attention is needed

    Projects

    Status
    Done
    Status
    Done

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions