From 7c8ca0324e54683150524a2128de2a93ba2dc63c Mon Sep 17 00:00:00 2001 From: Lourince Daging Date: Thu, 16 Jul 2026 03:30:41 +0200 Subject: [PATCH] feat(engine): add a stable, versioned getTrackRecordSummary read API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps the existing computeTrackRecordSummary in a single documented, versioned envelope (TRACK_RECORD_SUMMARY_READ_VERSION + the computed summary) so a future consumer (#6208's reputation bridge) can depend on one fixed read contract regardless of how TrackRecordSummary evolves. Read-only, adds no score/ranking fields — preserves the summary's existing public-safe boundary. Closes #6246. --- packages/loopover-engine/src/index.ts | 3 ++ .../src/track-record-summary.ts | 27 +++++++++++++++ .../test/track-record-summary.test.ts | 34 +++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/packages/loopover-engine/src/index.ts b/packages/loopover-engine/src/index.ts index e60e780f6d..37c826f3a6 100644 --- a/packages/loopover-engine/src/index.ts +++ b/packages/loopover-engine/src/index.ts @@ -127,6 +127,8 @@ export { } from "./reviewer-consensus-calibration.js"; export { computeTrackRecordSummary, + getTrackRecordSummary, + TRACK_RECORD_SUMMARY_READ_VERSION, renderTrackRecordSummaryMarkdown, resolveTrackRecordSummaryConfig, shouldIncludeTrackRecordSummary, @@ -141,6 +143,7 @@ export { type TrackRecordSummaryConfig, type TrackRecordSummaryManifest, type TrackRecordSummaryOutcomeCounts, + type TrackRecordSummaryReadResult, type TrackRecordTenure, } from "./track-record-summary.js"; export * from "./governor/rate-limit.js"; diff --git a/packages/loopover-engine/src/track-record-summary.ts b/packages/loopover-engine/src/track-record-summary.ts index e19dda3505..d3bf5cf126 100644 --- a/packages/loopover-engine/src/track-record-summary.ts +++ b/packages/loopover-engine/src/track-record-summary.ts @@ -376,6 +376,33 @@ export function computeTrackRecordSummary(input: { }; } +/** Bump when the {@link TrackRecordSummaryReadResult} envelope shape changes in a non-additive way (#6246). */ +export const TRACK_RECORD_SUMMARY_READ_VERSION = 1 as const; + +/** The stable, versioned read envelope returned by {@link getTrackRecordSummary}. */ +export type TrackRecordSummaryReadResult = { + version: typeof TRACK_RECORD_SUMMARY_READ_VERSION; + summary: TrackRecordSummary; +}; + +/** + * Stable, documented public read contract for a miner's track-record summary (#6246, groundwork for #6208's + * reputation-bridge spec). A thin, versioned wrapper over {@link computeTrackRecordSummary}: it takes the same + * public PR-outcome and moderation-record reads the summary is already derived from and returns them under a + * fixed envelope ({@link TRACK_RECORD_SUMMARY_READ_VERSION} + the computed summary), so a future consumer can + * depend on one documented shape regardless of how the underlying {@link TrackRecordSummary} evolves. Read-only; + * the envelope adds no score/ranking fields, preserving the same public-safe boundary the summary itself keeps. + */ +export function getTrackRecordSummary(input: { + login: string; + outcomes: readonly TrackRecordPullRequestOutcome[]; + incidents?: readonly TrackRecordIncidentRecord[] | undefined; + now?: string | Date | null | undefined; + config?: TrackRecordSummaryConfig | TrackRecordSummaryManifest | Record | null | undefined; +}): TrackRecordSummaryReadResult { + return { version: TRACK_RECORD_SUMMARY_READ_VERSION, summary: computeTrackRecordSummary(input) }; +} + export function shouldIncludeTrackRecordSummary( config: TrackRecordSummaryConfig | TrackRecordSummaryManifest | Record | null | undefined, ): boolean { diff --git a/packages/loopover-engine/test/track-record-summary.test.ts b/packages/loopover-engine/test/track-record-summary.test.ts index 83bfb244d2..159017ab57 100644 --- a/packages/loopover-engine/test/track-record-summary.test.ts +++ b/packages/loopover-engine/test/track-record-summary.test.ts @@ -3,6 +3,8 @@ import assert from "node:assert/strict"; import { computeTrackRecordSummary, + getTrackRecordSummary, + TRACK_RECORD_SUMMARY_READ_VERSION, renderTrackRecordSummaryMarkdown, resolveTrackRecordSummaryConfig, shouldIncludeTrackRecordSummary, @@ -660,3 +662,35 @@ test("computeTrackRecordSummary accepts manifest config directly", () => { assert.equal(summary.enabled, true); }); + +test("getTrackRecordSummary wraps the computed summary in a stable versioned envelope: populated (#6246)", () => { + const input = { + login: "MinerOne", + now: NOW, + config: { includeTrackRecordSummary: true, warnings: [] }, + outcomes: [ + { id: 11, repoFullName: "jsonbored/loopover", authorLogin: "minerone", state: "merged", createdAt: "2026-06-01T00:00:00Z", mergedAt: "2026-06-02T00:00:00Z" }, + { id: "pr-12", repoFullName: "owner/repo", authorLogin: "MinerOne", state: "closed", createdAt: "2026-06-03T00:00:00Z", closedAt: "2026-06-04T00:00:00Z" }, + ], + incidents: [], + } as const; + const result = getTrackRecordSummary(input); + assert.equal(result.version, TRACK_RECORD_SUMMARY_READ_VERSION); + assert.equal(result.version, 1); + // The envelope carries exactly the computed summary, unmodified. + assert.deepEqual(result.summary, computeTrackRecordSummary(input)); + assert.equal(result.summary.enabled, true); + assert.equal(result.summary.login, "minerone"); + assert.equal(result.summary.outcomes.merged, 1); + assert.equal(result.summary.outcomes.closedWithoutMerge, 1); +}); + +test("getTrackRecordSummary returns a versioned envelope for an empty/no-history miner (#6246)", () => { + const result = getTrackRecordSummary({ login: "ghost", now: NOW, outcomes: [], incidents: [] }); + assert.equal(result.version, 1); + assert.equal(result.summary.login, "ghost"); + assert.equal(result.summary.outcomes.merged, 0); + assert.equal(result.summary.outcomes.closedWithoutMerge, 0); + // No config supplied → the summary defaults to disabled, same as computeTrackRecordSummary. + assert.equal(result.summary.enabled, false); +});