From 30c5b79b2ab50cb44258e712eff4cce6f8d096aa Mon Sep 17 00:00:00 2001 From: jony376 Date: Sun, 5 Jul 2026 22:03:58 -0700 Subject: [PATCH] chore(miner): scaffold calibration module types (#2332) Add shared PredictedVerdictRecord, ObservedOutcomeRecord, CalibrationRow, and CalibrationReport shapes under packages/gittensory-miner/lib/calibration with runtime validators and unit tests. No calibration logic yet. Co-authored-by: Cursor --- .../lib/calibration/index.d.ts | 13 ++++ .../gittensory-miner/lib/calibration/index.js | 6 ++ .../lib/calibration/types.d.ts | 46 ++++++++++++ .../gittensory-miner/lib/calibration/types.js | 74 +++++++++++++++++++ packages/gittensory-miner/package.json | 2 +- test/unit/miner-calibration-types.test.ts | 70 ++++++++++++++++++ 6 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 packages/gittensory-miner/lib/calibration/index.d.ts create mode 100644 packages/gittensory-miner/lib/calibration/index.js create mode 100644 packages/gittensory-miner/lib/calibration/types.d.ts create mode 100644 packages/gittensory-miner/lib/calibration/types.js create mode 100644 test/unit/miner-calibration-types.test.ts diff --git a/packages/gittensory-miner/lib/calibration/index.d.ts b/packages/gittensory-miner/lib/calibration/index.d.ts new file mode 100644 index 0000000000..8dd672e950 --- /dev/null +++ b/packages/gittensory-miner/lib/calibration/index.d.ts @@ -0,0 +1,13 @@ +export type { + CalibrationReport, + CalibrationRow, + ObservedOutcomeRecord, + PredictedVerdictRecord, +} from "./types.js"; + +export { + isCalibrationReport, + isCalibrationRow, + isObservedOutcomeRecord, + isPredictedVerdictRecord, +} from "./types.js"; diff --git a/packages/gittensory-miner/lib/calibration/index.js b/packages/gittensory-miner/lib/calibration/index.js new file mode 100644 index 0000000000..92cc60b3a1 --- /dev/null +++ b/packages/gittensory-miner/lib/calibration/index.js @@ -0,0 +1,6 @@ +export { + isCalibrationReport, + isCalibrationRow, + isObservedOutcomeRecord, + isPredictedVerdictRecord, +} from "./types.js"; diff --git a/packages/gittensory-miner/lib/calibration/types.d.ts b/packages/gittensory-miner/lib/calibration/types.d.ts new file mode 100644 index 0000000000..097cc40352 --- /dev/null +++ b/packages/gittensory-miner/lib/calibration/types.d.ts @@ -0,0 +1,46 @@ +/** A single gate-prediction row the miner will replay against observed outcomes. */ +export type PredictedVerdictRecord = { + targetId: string; + project: string; + predictedDecision: string; + recordedAt: string; + source?: string; +}; + +/** The realized human outcome for a previously predicted target. */ +export type ObservedOutcomeRecord = { + targetId: string; + project: string; + outcomeDecision: string; + recordedAt: string; +}; + +/** Per-project confusion-matrix row — field names mirror `GateEvalRow` in `src/review/parity.ts`. */ +export type CalibrationRow = { + project: string; + wouldMerge: number; + mergeConfirmed: number; + mergeFalse: number; + wouldClose: number; + closeConfirmed: number; + closeFalse: number; + hold: number; + decided: number; + mergePrecision: number | null; + closePrecision: number | null; +}; + +/** Aggregate calibration report over one or more projects. */ +export type CalibrationReport = { + rows: CalibrationRow[]; + /** True once at least one project has enough decided samples to read meaningfully. */ + hasSignal: boolean; +}; + +export function isPredictedVerdictRecord(value: unknown): value is PredictedVerdictRecord; + +export function isObservedOutcomeRecord(value: unknown): value is ObservedOutcomeRecord; + +export function isCalibrationRow(value: unknown): value is CalibrationRow; + +export function isCalibrationReport(value: unknown): value is CalibrationReport; diff --git a/packages/gittensory-miner/lib/calibration/types.js b/packages/gittensory-miner/lib/calibration/types.js new file mode 100644 index 0000000000..a857b1f7f7 --- /dev/null +++ b/packages/gittensory-miner/lib/calibration/types.js @@ -0,0 +1,74 @@ +// Shared calibration shapes for the miner self-improvement phase (#2332). Types-only scaffolding — +// report/ledger/metrics issues build on this module. Field names mirror `GateEvalRow` / +// `GateEvalReport` in `src/review/parity.ts` for easy mental mapping without importing cloud code. + +function isNonEmptyString(value) { + return typeof value === "string" && value.trim().length > 0; +} + +function isOptionalString(value) { + return value === undefined || isNonEmptyString(value); +} + +function isNonNegativeInteger(value) { + return typeof value === "number" && Number.isInteger(value) && value >= 0; +} + +function isNullableRatio(value) { + return value === null || (typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1); +} + +/** @param {unknown} value */ +export function isPredictedVerdictRecord(value) { + if (value === null || typeof value !== "object" || Array.isArray(value)) return false; + const record = /** @type {Record} */ (value); + return ( + isNonEmptyString(record.targetId) + && isNonEmptyString(record.project) + && isNonEmptyString(record.predictedDecision) + && isNonEmptyString(record.recordedAt) + && isOptionalString(record.source) + ); +} + +/** @param {unknown} value */ +export function isObservedOutcomeRecord(value) { + if (value === null || typeof value !== "object" || Array.isArray(value)) return false; + const record = /** @type {Record} */ (value); + return ( + isNonEmptyString(record.targetId) + && isNonEmptyString(record.project) + && isNonEmptyString(record.outcomeDecision) + && isNonEmptyString(record.recordedAt) + ); +} + +/** @param {unknown} value */ +export function isCalibrationRow(value) { + if (value === null || typeof value !== "object" || Array.isArray(value)) return false; + const row = /** @type {Record} */ (value); + return ( + isNonEmptyString(row.project) + && isNonNegativeInteger(row.wouldMerge) + && isNonNegativeInteger(row.mergeConfirmed) + && isNonNegativeInteger(row.mergeFalse) + && isNonNegativeInteger(row.wouldClose) + && isNonNegativeInteger(row.closeConfirmed) + && isNonNegativeInteger(row.closeFalse) + && isNonNegativeInteger(row.hold) + && isNonNegativeInteger(row.decided) + && isNullableRatio(row.mergePrecision) + && isNullableRatio(row.closePrecision) + ); +} + +/** @param {unknown} value */ +export function isCalibrationReport(value) { + if (value === null || typeof value !== "object" || Array.isArray(value)) return false; + const report = /** @type {Record} */ (value); + return ( + typeof report.hasSignal === "boolean" + && Array.isArray(report.rows) + && report.rows.every((row) => isCalibrationRow(row)) + ); +} diff --git a/packages/gittensory-miner/package.json b/packages/gittensory-miner/package.json index 3672db3642..addc3122e9 100644 --- a/packages/gittensory-miner/package.json +++ b/packages/gittensory-miner/package.json @@ -31,7 +31,7 @@ "lib" ], "scripts": { - "build": "node --check bin/gittensory-miner.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/run-state-cli.js && node --check lib/update-check.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/event-ledger-cli.js && node --check lib/claim-ledger.js && node --check lib/claim-ledger-expiry.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-discovery.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/plan-store-cli.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js && node --check lib/governor-ledger-cli.js && node --check lib/manage-status.js && node --check lib/manage-poll.js && node --check lib/status.js && node --check lib/laptop-init.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-task-generation.js" + "build": "node --check bin/gittensory-miner.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/run-state-cli.js && node --check lib/update-check.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/event-ledger-cli.js && node --check lib/claim-ledger.js && node --check lib/claim-ledger-expiry.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-discovery.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/plan-store-cli.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js && node --check lib/governor-ledger-cli.js && node --check lib/manage-status.js && node --check lib/manage-poll.js && node --check lib/status.js && node --check lib/laptop-init.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-task-generation.js && node --check lib/calibration/types.js && node --check lib/calibration/index.js" }, "dependencies": { "@jsonbored/gittensory-engine": "0.1.0" diff --git a/test/unit/miner-calibration-types.test.ts b/test/unit/miner-calibration-types.test.ts new file mode 100644 index 0000000000..e877a1bbb3 --- /dev/null +++ b/test/unit/miner-calibration-types.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; +import { + isCalibrationReport, + isCalibrationRow, + isObservedOutcomeRecord, + isPredictedVerdictRecord, +} from "../../packages/gittensory-miner/lib/calibration/index.js"; +import type { + CalibrationReport, + CalibrationRow, + ObservedOutcomeRecord, + PredictedVerdictRecord, +} from "../../packages/gittensory-miner/lib/calibration/index.js"; + +describe("gittensory-miner calibration types scaffold (#2332)", () => { + const predicted: PredictedVerdictRecord = { + targetId: "pr:JSONbored/gittensory#42", + project: "JSONbored/gittensory", + predictedDecision: "merge", + recordedAt: "2026-07-06T00:00:00.000Z", + source: "reviewbot", + }; + + const observed: ObservedOutcomeRecord = { + targetId: "pr:JSONbored/gittensory#42", + project: "JSONbored/gittensory", + outcomeDecision: "merged", + recordedAt: "2026-07-06T01:00:00.000Z", + }; + + const row: CalibrationRow = { + project: "JSONbored/gittensory", + wouldMerge: 10, + mergeConfirmed: 8, + mergeFalse: 2, + wouldClose: 3, + closeConfirmed: 2, + closeFalse: 1, + hold: 1, + decided: 13, + mergePrecision: 0.8, + closePrecision: 2 / 3, + }; + + const report: CalibrationReport = { + rows: [row], + hasSignal: true, + }; + + it("accepts minimal fixtures for every shared calibration shape", () => { + expect(isPredictedVerdictRecord(predicted)).toBe(true); + expect(isObservedOutcomeRecord(observed)).toBe(true); + expect(isCalibrationRow(row)).toBe(true); + expect(isCalibrationReport(report)).toBe(true); + }); + + it("rejects malformed prediction and outcome records", () => { + expect(isPredictedVerdictRecord(null)).toBe(false); + expect(isPredictedVerdictRecord({ ...predicted, targetId: "" })).toBe(false); + expect(isPredictedVerdictRecord({ ...predicted, source: 1 })).toBe(false); + expect(isObservedOutcomeRecord({ ...observed, outcomeDecision: "" })).toBe(false); + }); + + it("rejects malformed calibration rows and reports", () => { + expect(isCalibrationRow({ ...row, mergePrecision: 1.5 })).toBe(false); + expect(isCalibrationRow({ ...row, decided: -1 })).toBe(false); + expect(isCalibrationReport({ rows: [row], hasSignal: "yes" })).toBe(false); + expect(isCalibrationReport({ rows: [{ ...row, project: "" }], hasSignal: true })).toBe(false); + }); +});