@loopover/miner 0.1.0

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.
Files changed (218) hide show
  1. package/DEPLOYMENT.md +156 -0
  2. package/Dockerfile +38 -0
  3. package/README.md +269 -0
  4. package/bin/gittensory-miner-mcp.d.ts +64 -0
  5. package/bin/gittensory-miner-mcp.js +279 -0
  6. package/bin/gittensory-miner.js +196 -0
  7. package/docs/coding-agent-driver.md +140 -0
  8. package/docs/config-precedence.md +96 -0
  9. package/docs/cross-repo-discovery-phase1.md +55 -0
  10. package/docs/discovery-index-contract.md +60 -0
  11. package/docs/discovery-plane-operator-guide.md +101 -0
  12. package/docs/env-reference.md +30 -0
  13. package/docs/fleet-run-manifest.md +50 -0
  14. package/docs/miner-goal-spec.md +76 -0
  15. package/docs/miner-selfimprove-calibration.md +105 -0
  16. package/docs/observability.md +60 -0
  17. package/docs/operations-runbook.md +209 -0
  18. package/docs/repo-agnostic-capability-audit.md +129 -0
  19. package/docs/unattended-scheduling.md +107 -0
  20. package/expected-engine.version +1 -0
  21. package/lib/ams-policy.d.ts +20 -0
  22. package/lib/ams-policy.js +68 -0
  23. package/lib/attempt-cli.d.ts +99 -0
  24. package/lib/attempt-cli.js +523 -0
  25. package/lib/attempt-input-builder.d.ts +24 -0
  26. package/lib/attempt-input-builder.js +91 -0
  27. package/lib/attempt-log.d.ts +37 -0
  28. package/lib/attempt-log.js +177 -0
  29. package/lib/attempt-runner.d.ts +59 -0
  30. package/lib/attempt-runner.js +164 -0
  31. package/lib/attempt-worktree.d.ts +31 -0
  32. package/lib/attempt-worktree.js +94 -0
  33. package/lib/calibration-cli.d.ts +1 -0
  34. package/lib/calibration-cli.js +92 -0
  35. package/lib/calibration-run.d.ts +161 -0
  36. package/lib/calibration-run.js +232 -0
  37. package/lib/calibration-types.d.ts +46 -0
  38. package/lib/calibration-types.js +74 -0
  39. package/lib/calibration.d.ts +24 -0
  40. package/lib/calibration.js +99 -0
  41. package/lib/ci-poller.d.ts +33 -0
  42. package/lib/ci-poller.js +234 -0
  43. package/lib/claim-adjudication.d.ts +21 -0
  44. package/lib/claim-adjudication.js +36 -0
  45. package/lib/claim-conflict-resolver.d.ts +29 -0
  46. package/lib/claim-conflict-resolver.js +101 -0
  47. package/lib/claim-ledger-cli.d.ts +59 -0
  48. package/lib/claim-ledger-cli.js +320 -0
  49. package/lib/claim-ledger-expiry.d.ts +20 -0
  50. package/lib/claim-ledger-expiry.js +41 -0
  51. package/lib/claim-ledger.d.ts +63 -0
  52. package/lib/claim-ledger.js +303 -0
  53. package/lib/cli-error.d.ts +3 -0
  54. package/lib/cli-error.js +27 -0
  55. package/lib/cli.d.ts +3 -0
  56. package/lib/cli.js +72 -0
  57. package/lib/coding-agent-construction.d.ts +17 -0
  58. package/lib/coding-agent-construction.js +103 -0
  59. package/lib/coding-agent-house-rules.d.ts +24 -0
  60. package/lib/coding-agent-house-rules.js +64 -0
  61. package/lib/coding-task-spec.d.ts +44 -0
  62. package/lib/coding-task-spec.js +183 -0
  63. package/lib/config-precedence.d.ts +25 -0
  64. package/lib/config-precedence.js +38 -0
  65. package/lib/deny-check.d.ts +11 -0
  66. package/lib/deny-check.js +76 -0
  67. package/lib/deny-hook-synthesis.d.ts +93 -0
  68. package/lib/deny-hook-synthesis.js +399 -0
  69. package/lib/deny-hooks.d.ts +28 -0
  70. package/lib/deny-hooks.js +155 -0
  71. package/lib/deployment-docs-audit.d.ts +41 -0
  72. package/lib/deployment-docs-audit.js +115 -0
  73. package/lib/discover-cli.d.ts +94 -0
  74. package/lib/discover-cli.js +284 -0
  75. package/lib/discovery-throttle.d.ts +8 -0
  76. package/lib/discovery-throttle.js +33 -0
  77. package/lib/env-file-indirection.d.ts +4 -0
  78. package/lib/env-file-indirection.js +45 -0
  79. package/lib/event-ledger-cli.d.ts +82 -0
  80. package/lib/event-ledger-cli.js +266 -0
  81. package/lib/event-ledger.d.ts +37 -0
  82. package/lib/event-ledger.js +210 -0
  83. package/lib/execute-local-write.d.ts +14 -0
  84. package/lib/execute-local-write.js +50 -0
  85. package/lib/feasibility-cli.d.ts +25 -0
  86. package/lib/feasibility-cli.js +80 -0
  87. package/lib/forge-config.d.ts +17 -0
  88. package/lib/forge-config.js +37 -0
  89. package/lib/governor-action-mode.d.ts +26 -0
  90. package/lib/governor-action-mode.js +47 -0
  91. package/lib/governor-chokepoint-persisted.d.ts +18 -0
  92. package/lib/governor-chokepoint-persisted.js +46 -0
  93. package/lib/governor-chokepoint.d.ts +14 -0
  94. package/lib/governor-chokepoint.js +52 -0
  95. package/lib/governor-kill-switch.d.ts +26 -0
  96. package/lib/governor-kill-switch.js +47 -0
  97. package/lib/governor-ledger-cli.d.ts +32 -0
  98. package/lib/governor-ledger-cli.js +155 -0
  99. package/lib/governor-ledger.d.ts +46 -0
  100. package/lib/governor-ledger.js +203 -0
  101. package/lib/governor-open-pr.d.ts +21 -0
  102. package/lib/governor-open-pr.js +27 -0
  103. package/lib/governor-pause-cli.d.ts +23 -0
  104. package/lib/governor-pause-cli.js +171 -0
  105. package/lib/governor-run-halt.d.ts +37 -0
  106. package/lib/governor-run-halt.js +58 -0
  107. package/lib/governor-state.d.ts +63 -0
  108. package/lib/governor-state.js +363 -0
  109. package/lib/governor-write-rate-limit.d.ts +30 -0
  110. package/lib/governor-write-rate-limit.js +64 -0
  111. package/lib/harness-submission-trigger.d.ts +69 -0
  112. package/lib/harness-submission-trigger.js +138 -0
  113. package/lib/http-retry.d.ts +12 -0
  114. package/lib/http-retry.js +52 -0
  115. package/lib/laptop-init.d.ts +53 -0
  116. package/lib/laptop-init.js +332 -0
  117. package/lib/live-issue-snapshot.d.ts +16 -0
  118. package/lib/live-issue-snapshot.js +117 -0
  119. package/lib/local-store.d.ts +18 -0
  120. package/lib/local-store.js +62 -0
  121. package/lib/logger.d.ts +58 -0
  122. package/lib/logger.js +167 -0
  123. package/lib/loop-cli.d.ts +66 -0
  124. package/lib/loop-cli.js +540 -0
  125. package/lib/loop-closure.d.ts +35 -0
  126. package/lib/loop-closure.js +66 -0
  127. package/lib/loop-reentry.d.ts +51 -0
  128. package/lib/loop-reentry.js +124 -0
  129. package/lib/manage-poll.d.ts +79 -0
  130. package/lib/manage-poll.js +254 -0
  131. package/lib/manage-status.d.ts +70 -0
  132. package/lib/manage-status.js +241 -0
  133. package/lib/metrics-cli.d.ts +9 -0
  134. package/lib/metrics-cli.js +50 -0
  135. package/lib/migrate-cli.d.ts +24 -0
  136. package/lib/migrate-cli.js +111 -0
  137. package/lib/miner-goal-spec.d.ts +12 -0
  138. package/lib/miner-goal-spec.js +67 -0
  139. package/lib/opportunity-fanout.d.ts +86 -0
  140. package/lib/opportunity-fanout.js +597 -0
  141. package/lib/opportunity-ranker.d.ts +36 -0
  142. package/lib/opportunity-ranker.js +118 -0
  143. package/lib/orb-export.d.ts +54 -0
  144. package/lib/orb-export.js +206 -0
  145. package/lib/plan-store-cli.d.ts +37 -0
  146. package/lib/plan-store-cli.js +151 -0
  147. package/lib/plan-store.d.ts +51 -0
  148. package/lib/plan-store.js +231 -0
  149. package/lib/policy-doc-cache.d.ts +25 -0
  150. package/lib/policy-doc-cache.js +79 -0
  151. package/lib/policy-verdict-cache.d.ts +35 -0
  152. package/lib/policy-verdict-cache.js +106 -0
  153. package/lib/portfolio-dashboard.d.ts +30 -0
  154. package/lib/portfolio-dashboard.js +105 -0
  155. package/lib/portfolio-discovery.d.ts +29 -0
  156. package/lib/portfolio-discovery.js +100 -0
  157. package/lib/portfolio-queue-cli.d.ts +87 -0
  158. package/lib/portfolio-queue-cli.js +509 -0
  159. package/lib/portfolio-queue-expiry.d.ts +20 -0
  160. package/lib/portfolio-queue-expiry.js +51 -0
  161. package/lib/portfolio-queue-manager.d.ts +50 -0
  162. package/lib/portfolio-queue-manager.js +134 -0
  163. package/lib/portfolio-queue.d.ts +62 -0
  164. package/lib/portfolio-queue.js +351 -0
  165. package/lib/pr-disposition-poller.d.ts +26 -0
  166. package/lib/pr-disposition-poller.js +163 -0
  167. package/lib/pr-number-parse.d.ts +4 -0
  168. package/lib/pr-number-parse.js +23 -0
  169. package/lib/pr-outcome.d.ts +41 -0
  170. package/lib/pr-outcome.js +90 -0
  171. package/lib/prediction-ledger.d.ts +47 -0
  172. package/lib/prediction-ledger.js +222 -0
  173. package/lib/pretooluse-hook.d.ts +31 -0
  174. package/lib/pretooluse-hook.js +93 -0
  175. package/lib/process-lifecycle.d.ts +31 -0
  176. package/lib/process-lifecycle.js +106 -0
  177. package/lib/purge-cli.d.ts +44 -0
  178. package/lib/purge-cli.js +198 -0
  179. package/lib/rejection-signal.d.ts +6 -0
  180. package/lib/rejection-signal.js +101 -0
  181. package/lib/rejection-state-machine.d.ts +37 -0
  182. package/lib/rejection-state-machine.js +81 -0
  183. package/lib/rejection-templates.d.ts +12 -0
  184. package/lib/rejection-templates.js +71 -0
  185. package/lib/replay-objective-anchor.d.ts +79 -0
  186. package/lib/replay-objective-anchor.js +179 -0
  187. package/lib/replay-snapshot.d.ts +41 -0
  188. package/lib/replay-snapshot.js +288 -0
  189. package/lib/replay-task-generation.d.ts +126 -0
  190. package/lib/replay-task-generation.js +223 -0
  191. package/lib/repo-clone.d.ts +19 -0
  192. package/lib/repo-clone.js +102 -0
  193. package/lib/run-state-cli.d.ts +27 -0
  194. package/lib/run-state-cli.js +154 -0
  195. package/lib/run-state.d.ts +37 -0
  196. package/lib/run-state.js +150 -0
  197. package/lib/schema-version.d.ts +17 -0
  198. package/lib/schema-version.js +71 -0
  199. package/lib/self-review-context.d.ts +28 -0
  200. package/lib/self-review-context.js +339 -0
  201. package/lib/slop-assessment.d.ts +3 -0
  202. package/lib/slop-assessment.js +20 -0
  203. package/lib/stack-detection.d.ts +41 -0
  204. package/lib/stack-detection.js +248 -0
  205. package/lib/status.d.ts +64 -0
  206. package/lib/status.js +441 -0
  207. package/lib/store-maintenance.d.ts +31 -0
  208. package/lib/store-maintenance.js +183 -0
  209. package/lib/submission-freshness-check.d.ts +32 -0
  210. package/lib/submission-freshness-check.js +93 -0
  211. package/lib/update-check.d.ts +36 -0
  212. package/lib/update-check.js +161 -0
  213. package/lib/version.d.ts +3 -0
  214. package/lib/version.js +10 -0
  215. package/lib/worktree-allocator.d.ts +39 -0
  216. package/lib/worktree-allocator.js +262 -0
  217. package/package.json +50 -0
  218. package/schema/miner-goal-spec.schema.json +111 -0
@@ -0,0 +1,92 @@
1
+ // `gittensory-miner calibration [--json]` (#4849): a read-only report joining the miner's own predicted gate
2
+ // verdicts (prediction-ledger) with the realized PR outcomes it later observed (event-ledger `pr_outcome`
3
+ // events), via the pure buildCalibrationReport join. Opens both local stores, maps their rows to the
4
+ // calibration record shapes, renders, and closes. Never modifies the live scoring/calibration logic.
5
+ import { buildCalibrationReport } from "./calibration.js";
6
+ import { initEventLedger, resolveEventLedgerDbPath } from "./event-ledger.js";
7
+ import { MINER_PR_OUTCOME_EVENT } from "./pr-outcome.js";
8
+ import { initPredictionLedger, resolvePredictionLedgerDbPath } from "./prediction-ledger.js";
9
+ import { reportCliFailure, describeCliError } from "./cli-error.js";
10
+
11
+ const CALIBRATION_USAGE = "Usage: gittensory-miner calibration [--json]";
12
+
13
+ /** Map prediction-ledger rows to predicted-verdict records: the target id becomes a string key and the recorded
14
+ * prediction verdict is the `conclusion`. */
15
+ function toPredictionRecords(rows) {
16
+ return rows.map((row) => ({
17
+ project: row.repoFullName,
18
+ targetId: String(row.targetId),
19
+ predictedDecision: row.conclusion,
20
+ recordedAt: row.ts,
21
+ }));
22
+ }
23
+
24
+ /** Reduce the append-only `pr_outcome` event stream to the LATEST observed outcome per (repo, PR), as
25
+ * observed-outcome records. `recordedAt` comes from the event's own timestamp (always present), so an outcome is
26
+ * never dropped for lacking a `closedAt`. Malformed payloads are skipped. */
27
+ function toOutcomeRecords(events) {
28
+ const latest = new Map();
29
+ for (const event of events) {
30
+ if (event?.type !== MINER_PR_OUTCOME_EVENT) continue;
31
+ const payload = event.payload;
32
+ if (!payload || !Number.isInteger(payload.prNumber) || typeof payload.decision !== "string") continue;
33
+ latest.set(`${event.repoFullName}:${payload.prNumber}`, {
34
+ project: event.repoFullName,
35
+ targetId: String(payload.prNumber),
36
+ outcomeDecision: payload.decision,
37
+ recordedAt: event.createdAt,
38
+ });
39
+ }
40
+ return [...latest.values()];
41
+ }
42
+
43
+ function renderReportText(report) {
44
+ if (!report.hasSignal) {
45
+ console.log("calibration: no decided predictions yet (predictions need a realized merge/close outcome).");
46
+ return;
47
+ }
48
+ for (const row of report.rows) {
49
+ const merge = row.mergePrecision === null ? "n/a" : `${Math.round(row.mergePrecision * 100)}%`;
50
+ const close = row.closePrecision === null ? "n/a" : `${Math.round(row.closePrecision * 100)}%`;
51
+ console.log(
52
+ `${row.project}: ${row.decided} decided | ` +
53
+ `merge ${row.mergeConfirmed}/${row.wouldMerge} (${merge}) | ` +
54
+ `close ${row.closeConfirmed}/${row.wouldClose} (${close}) | hold ${row.hold}`,
55
+ );
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Run `gittensory-miner calibration [--json]`. Reads the prediction ledger + PR-outcome events, joins them into a
61
+ * calibration report, and prints it (a JSON dump under `--json`, else a per-project text summary). Returns the
62
+ * process exit code: 0 on success, 1 on an unknown option.
63
+ * @param {string[]} [args]
64
+ * @param {NodeJS.ProcessEnv} [env]
65
+ * @returns {number}
66
+ */
67
+ export function runCalibrationCli(args = [], env = process.env) {
68
+ const json = args.includes("--json");
69
+ const unknown = args.find((token) => token.startsWith("-") && token !== "--json");
70
+ if (unknown) {
71
+ return reportCliFailure(json, `Unknown option: ${unknown}. ${CALIBRATION_USAGE}`, 1);
72
+ }
73
+
74
+ let predictionStore;
75
+ let eventLedger;
76
+ try {
77
+ predictionStore = initPredictionLedger(resolvePredictionLedgerDbPath(env));
78
+ eventLedger = initEventLedger(resolveEventLedgerDbPath(env));
79
+ const report = buildCalibrationReport(
80
+ toPredictionRecords(predictionStore.readPredictions()),
81
+ toOutcomeRecords(eventLedger.readEvents()),
82
+ );
83
+ if (json) console.log(JSON.stringify(report, null, 2));
84
+ else renderReportText(report);
85
+ return 0;
86
+ } catch (error) {
87
+ return reportCliFailure(json, describeCliError(error));
88
+ } finally {
89
+ predictionStore?.close();
90
+ eventLedger?.close();
91
+ }
92
+ }
@@ -0,0 +1,161 @@
1
+ import type {
2
+ HistoricalReplayCalibrationInput,
3
+ Phase7CalibrationConfig,
4
+ Phase7CalibrationLoopResult,
5
+ Phase7CalibrationManifest,
6
+ PrOutcomeCalibrationInput,
7
+ ReplayHarnessStatus,
8
+ } from "@loopover/engine";
9
+
10
+ import type { AppendEventInput, LedgerEntry } from "./event-ledger.js";
11
+ import type {
12
+ ObjectiveAnchorResult,
13
+ ReplayPlanInput,
14
+ RevealedHistoryEntry,
15
+ } from "./replay-objective-anchor.js";
16
+
17
+ export const MINER_CALIBRATION_SNAPSHOT_EVENT: "calibration_snapshot";
18
+
19
+ /** One completed replay-run task result: what the replay targeted, and the revealed post-T history to score it. */
20
+ export interface ReplayTaskResult {
21
+ replayPlan?: ReplayPlanInput | null;
22
+ revealedHistory?: RevealedHistoryEntry[] | RevealedHistoryEntry | null;
23
+ }
24
+
25
+ export interface ScoreCompositeOptions {
26
+ computeObjectiveAnchor?: (
27
+ input: { replayPlan?: ReplayPlanInput | null; revealedHistory?: RevealedHistoryEntry[] | RevealedHistoryEntry | null },
28
+ ) => ObjectiveAnchorResult;
29
+ }
30
+
31
+ export interface HistoricalReplayCompositeScore {
32
+ compositeScore: number | null;
33
+ sampleSize: number;
34
+ scores: number[];
35
+ }
36
+
37
+ export function scoreHistoricalReplayComposite(
38
+ replayResults: readonly ReplayTaskResult[] | null | undefined,
39
+ options?: ScoreCompositeOptions,
40
+ ): HistoricalReplayCompositeScore;
41
+
42
+ /** A completed replay run's descriptor: its per-task results plus the run's identity/freshness/harness health. */
43
+ export interface ReplayRunDescriptor {
44
+ replayResults?: readonly ReplayTaskResult[] | null;
45
+ replayRunId?: string;
46
+ observedAt?: string;
47
+ harnessStatus?: ReplayHarnessStatus;
48
+ }
49
+
50
+ export interface BuiltHistoricalReplayInput {
51
+ historicalReplay: HistoricalReplayCalibrationInput | null;
52
+ compositeScore: number | null;
53
+ sampleSize: number;
54
+ scores: number[];
55
+ }
56
+
57
+ export function buildHistoricalReplayCalibrationInput(
58
+ replayRun: ReplayRunDescriptor | null | undefined,
59
+ options?: ScoreCompositeOptions,
60
+ ): BuiltHistoricalReplayInput;
61
+
62
+ /** The persisted, public-safe projection of a Phase7CalibrationLoopResult. */
63
+ export interface CalibrationSnapshotPayload {
64
+ enabled: boolean;
65
+ combinedAccuracy: number | null;
66
+ baselineAccuracy: number;
67
+ deltaFromBaseline: number | null;
68
+ autonomyIncreasePermitted: boolean;
69
+ replayHarnessHold: boolean;
70
+ replayHarnessStatus: string;
71
+ replayRunDue: boolean;
72
+ holdReasons: string[];
73
+ contributingSources: string[];
74
+ replayRunId: string | null;
75
+ observedAt: string | null;
76
+ replaySampleSize: number;
77
+ }
78
+
79
+ export interface SnapshotMeta {
80
+ replayRunId?: string | null;
81
+ observedAt?: string | null;
82
+ sampleSize?: number;
83
+ }
84
+
85
+ export function snapshotPayloadFromResult(
86
+ result: Phase7CalibrationLoopResult,
87
+ meta?: SnapshotMeta,
88
+ ): CalibrationSnapshotPayload;
89
+
90
+ export function normalizeCalibrationSnapshotPayload(payload: unknown): CalibrationSnapshotPayload | null;
91
+
92
+ export interface RecordCalibrationSnapshotOptions {
93
+ /** Optional at the type level so a caller can pass an unusable ledger to exercise the fail-closed guard; the
94
+ * writer throws `invalid_event_ledger` at runtime when this is absent or lacks `appendEvent`. */
95
+ eventLedger?: { appendEvent(event: AppendEventInput): LedgerEntry };
96
+ repoFullName?: string;
97
+ }
98
+
99
+ export function recordCalibrationSnapshot(
100
+ input: unknown,
101
+ options?: RecordCalibrationSnapshotOptions,
102
+ ): LedgerEntry | null;
103
+
104
+ export interface CalibrationSnapshotReader {
105
+ readEvents(filter?: { since?: number | null; repoFullName?: string | null }): unknown[];
106
+ }
107
+
108
+ export interface CalibrationSnapshotFilter {
109
+ since?: number | null;
110
+ repoFullName?: string | null;
111
+ }
112
+
113
+ export interface PersistedCalibrationSnapshot extends CalibrationSnapshotPayload {
114
+ repoFullName: string | null;
115
+ seq: number | null;
116
+ createdAt: string | null;
117
+ }
118
+
119
+ export function readCalibrationSnapshots(
120
+ eventLedger: CalibrationSnapshotReader,
121
+ filter?: CalibrationSnapshotFilter,
122
+ ): PersistedCalibrationSnapshot[];
123
+
124
+ export function latestCalibrationSnapshot(
125
+ eventLedger: CalibrationSnapshotReader,
126
+ filter?: CalibrationSnapshotFilter,
127
+ ): PersistedCalibrationSnapshot | null;
128
+
129
+ export interface RunCalibrationCycleInput {
130
+ config?: Phase7CalibrationConfig | Phase7CalibrationManifest | Record<string, unknown> | null;
131
+ prOutcome?: PrOutcomeCalibrationInput | null;
132
+ replayRun?: ReplayRunDescriptor | null;
133
+ now?: string | Date | null;
134
+ observedAt?: string | null;
135
+ repoFullName?: string;
136
+ }
137
+
138
+ export interface RunCalibrationCycleDeps extends ScoreCompositeOptions {
139
+ computeLoop?: (input: {
140
+ config?: Phase7CalibrationConfig | Phase7CalibrationManifest | Record<string, unknown> | null;
141
+ prOutcome?: PrOutcomeCalibrationInput | null;
142
+ historicalReplay?: HistoricalReplayCalibrationInput | null;
143
+ now?: string | Date | null;
144
+ }) => Phase7CalibrationLoopResult;
145
+ eventLedger?: { appendEvent(event: AppendEventInput): LedgerEntry };
146
+ }
147
+
148
+ export interface RunCalibrationCycleResult {
149
+ result: Phase7CalibrationLoopResult;
150
+ snapshot: CalibrationSnapshotPayload;
151
+ recorded: LedgerEntry | null;
152
+ historicalReplay: HistoricalReplayCalibrationInput | null;
153
+ compositeScore: number | null;
154
+ sampleSize: number;
155
+ scores: number[];
156
+ }
157
+
158
+ export function runHistoricalReplayCalibrationCycle(
159
+ input?: RunCalibrationCycleInput,
160
+ deps?: RunCalibrationCycleDeps,
161
+ ): RunCalibrationCycleResult;
@@ -0,0 +1,232 @@
1
+ // Phase 7 calibration runner (#4248): the miner-side runner that finally CONNECTS the two finished-but-unwired
2
+ // halves #3014 left apart. #3014 landed the engine's pure calibration *combine* contract
3
+ // (`computePhase7CalibrationLoop`, packages/gittensory-engine/src/phase7-calibration-loop.ts) and #3012 landed the
4
+ // deterministic replay *scorer* (`computeObjectiveAnchor`, ./replay-objective-anchor.js), but nothing ever called
5
+ // one with the other -- #3014's issue claimed "wired" while only the engine side shipped. This module is the
6
+ // missing runner: it scores a completed historical-replay run with the objective-anchor scorer, folds the
7
+ // resulting composite into the `HistoricalReplayCalibrationInput` shape the engine expects, calls the combine with
8
+ // the existing pr_outcome signal, and PERSISTS the combined snapshot to the local append-only event ledger (a typed
9
+ // event layered on event-ledger.js exactly like pr-outcome.js's MINER_PR_OUTCOME_EVENT), queryable via
10
+ // `gittensory-miner ledger list --type calibration_snapshot`.
11
+ //
12
+ // SCOPE: this runner is read/measure-only. It produces and persists the tracked calibration metric; it NEVER acts
13
+ // on it (no autonomy-level bump, no gate-threshold tune) -- that enforcement is maintainer-only and fail-closed
14
+ // (see docs/miner-selfimprove-calibration.md's maintainer-only boundary). The engine owns the deterministic
15
+ // combine/freshness/threshold/hold-reason logic; this module owns scheduling the score and persisting the row.
16
+
17
+ import { computePhase7CalibrationLoop } from "@loopover/engine";
18
+ import { computeObjectiveAnchor } from "./replay-objective-anchor.js";
19
+
20
+ /** Event-ledger vocabulary for a persisted Phase 7 calibration snapshot (mirrors MINER_PR_OUTCOME_EVENT). */
21
+ export const MINER_CALIBRATION_SNAPSHOT_EVENT = "calibration_snapshot";
22
+
23
+ const SCORE_PRECISION = 1e6;
24
+
25
+ function roundScore(value) {
26
+ return Math.round(Math.min(1, Math.max(0, value)) * SCORE_PRECISION) / SCORE_PRECISION;
27
+ }
28
+
29
+ function isFiniteNumber(value) {
30
+ return typeof value === "number" && Number.isFinite(value);
31
+ }
32
+
33
+ function numberOrNull(value) {
34
+ return isFiniteNumber(value) ? value : null;
35
+ }
36
+
37
+ function optionalString(value) {
38
+ if (typeof value !== "string") return null;
39
+ const trimmed = value.trim();
40
+ return trimmed || null;
41
+ }
42
+
43
+ /**
44
+ * Score a completed replay run's per-task results with the deterministic objective-anchor scorer and reduce them to
45
+ * one composite `[0, 1]` accuracy (the mean of the per-task scores). `replayResults` is a list of
46
+ * `{ replayPlan, revealedHistory }` pairs; each non-object entry is defensively skipped. Returns `compositeScore:
47
+ * null` (never a fabricated 0) when there is no scorable task. Pure aside from the injected scorer.
48
+ */
49
+ export function scoreHistoricalReplayComposite(replayResults, options = {}) {
50
+ const scoreOne = options.computeObjectiveAnchor ?? computeObjectiveAnchor;
51
+ const list = Array.isArray(replayResults) ? replayResults : [];
52
+ const scores = [];
53
+ for (const entry of list) {
54
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
55
+ const { score } = scoreOne({ replayPlan: entry.replayPlan, revealedHistory: entry.revealedHistory });
56
+ if (isFiniteNumber(score)) scores.push(score);
57
+ }
58
+ const sampleSize = scores.length;
59
+ const compositeScore = sampleSize === 0 ? null : roundScore(scores.reduce((sum, s) => sum + s, 0) / sampleSize);
60
+ return { compositeScore, sampleSize, scores };
61
+ }
62
+
63
+ /**
64
+ * Build the engine's `HistoricalReplayCalibrationInput` from a replay run descriptor
65
+ * (`{ replayResults, replayRunId, observedAt, harnessStatus }`). Returns `historicalReplay: null` when no run
66
+ * descriptor is supplied (the engine then holds `no_historical_replay_signal` when the loop is enabled). When a run
67
+ * IS supplied its `harnessStatus` flows through verbatim so a degraded/unavailable harness still reaches the
68
+ * engine's fail-closed hold path even if it scored zero tasks; a null composite becomes `0` only for the engine's
69
+ * numeric contract (the un-fabricated `compositeScore`/`sampleSize` are returned alongside for the snapshot).
70
+ */
71
+ export function buildHistoricalReplayCalibrationInput(replayRun, options = {}) {
72
+ if (!replayRun || typeof replayRun !== "object" || Array.isArray(replayRun)) {
73
+ return { historicalReplay: null, compositeScore: null, sampleSize: 0, scores: [] };
74
+ }
75
+ const composite = scoreHistoricalReplayComposite(replayRun.replayResults, options);
76
+ return {
77
+ historicalReplay: {
78
+ compositeScore: composite.compositeScore ?? 0,
79
+ replayRunId: replayRun.replayRunId,
80
+ observedAt: replayRun.observedAt,
81
+ harnessStatus: replayRun.harnessStatus,
82
+ },
83
+ compositeScore: composite.compositeScore,
84
+ sampleSize: composite.sampleSize,
85
+ scores: composite.scores,
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Derive a JSON-safe, public-safe snapshot payload from a computed `Phase7CalibrationLoopResult`. Only accuracies,
91
+ * the documented baseline, hold-reason CODES, and provenance are surfaced -- never raw replay scores or rewards.
92
+ * Every field is a number/null, boolean, string/null, or string[] so it round-trips through the event ledger's
93
+ * verbatim-JSON serializer unchanged.
94
+ */
95
+ export function snapshotPayloadFromResult(result, meta = {}) {
96
+ return {
97
+ enabled: result.enabled === true,
98
+ combinedAccuracy: numberOrNull(result.combinedAccuracy),
99
+ baselineAccuracy: isFiniteNumber(result.baselineAccuracy) ? result.baselineAccuracy : 0,
100
+ deltaFromBaseline: numberOrNull(result.deltaFromBaseline),
101
+ autonomyIncreasePermitted: result.autonomyIncreasePermitted === true,
102
+ replayHarnessHold: result.replayHarnessHold === true,
103
+ replayHarnessStatus: optionalString(result.replayHarnessStatus) ?? "missing",
104
+ replayRunDue: result.replayRunDue === true,
105
+ holdReasons: Array.isArray(result.holdReasons) ? result.holdReasons.map(String) : [],
106
+ contributingSources: Array.isArray(result.audit?.contributingSources)
107
+ ? result.audit.contributingSources.map(String)
108
+ : [],
109
+ replayRunId: optionalString(meta.replayRunId),
110
+ observedAt: optionalString(meta.observedAt),
111
+ replaySampleSize: Number.isInteger(meta.sampleSize) && meta.sampleSize >= 0 ? meta.sampleSize : 0,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Validate + normalize a calibration-snapshot payload, returning `null` on any malformed shape (mirrors
117
+ * pr-outcome.js's `normalizePrOutcomePayload`, so a corrupted row can neither be written nor read back). Skipped
118
+ * rows are dropped by the reader rather than throwing.
119
+ */
120
+ export function normalizeCalibrationSnapshotPayload(payload) {
121
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
122
+ if (payload.combinedAccuracy !== null && !isFiniteNumber(payload.combinedAccuracy)) return null;
123
+ if (!isFiniteNumber(payload.baselineAccuracy)) return null;
124
+ if (payload.deltaFromBaseline !== null && !isFiniteNumber(payload.deltaFromBaseline)) return null;
125
+ if (typeof payload.autonomyIncreasePermitted !== "boolean") return null;
126
+ const replayHarnessStatus = optionalString(payload.replayHarnessStatus);
127
+ if (!replayHarnessStatus) return null;
128
+ if (!Array.isArray(payload.holdReasons) || payload.holdReasons.some((code) => typeof code !== "string")) {
129
+ return null;
130
+ }
131
+ const contributingSources = Array.isArray(payload.contributingSources)
132
+ ? payload.contributingSources.filter((code) => typeof code === "string")
133
+ : [];
134
+ return {
135
+ enabled: payload.enabled === true,
136
+ combinedAccuracy: payload.combinedAccuracy,
137
+ baselineAccuracy: payload.baselineAccuracy,
138
+ deltaFromBaseline: payload.deltaFromBaseline,
139
+ autonomyIncreasePermitted: payload.autonomyIncreasePermitted,
140
+ replayHarnessHold: payload.replayHarnessHold === true,
141
+ replayHarnessStatus,
142
+ replayRunDue: payload.replayRunDue === true,
143
+ holdReasons: payload.holdReasons,
144
+ contributingSources,
145
+ replayRunId: optionalString(payload.replayRunId),
146
+ observedAt: optionalString(payload.observedAt),
147
+ replaySampleSize:
148
+ Number.isInteger(payload.replaySampleSize) && payload.replaySampleSize >= 0 ? payload.replaySampleSize : 0,
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Persist one calibration snapshot to an INJECTED event ledger (same dependency-injection shape as pr-outcome.js's
154
+ * `recordPrOutcomeSnapshot`, so it's unit-testable without a real SQLite file). Fail-soft: a malformed payload
155
+ * returns `null` without appending. An unusable ledger is the only hard error (a programmer wiring mistake).
156
+ */
157
+ export function recordCalibrationSnapshot(input, options = {}) {
158
+ const eventLedger = options.eventLedger;
159
+ if (!eventLedger || typeof eventLedger.appendEvent !== "function") throw new Error("invalid_event_ledger");
160
+ const payload = normalizeCalibrationSnapshotPayload(input);
161
+ if (!payload) return null;
162
+ const repoFullName = optionalString(options.repoFullName);
163
+ return eventLedger.appendEvent({
164
+ type: MINER_CALIBRATION_SNAPSHOT_EVENT,
165
+ ...(repoFullName ? { repoFullName } : {}),
166
+ payload,
167
+ });
168
+ }
169
+
170
+ /**
171
+ * Read every persisted calibration snapshot from the injected ledger's ascending append-only stream (mirrors
172
+ * pr-outcome.js's `readPrOutcomes`). Foreign event types and malformed payloads are skipped; a ledger that cannot
173
+ * read reduces to an empty list. Returns snapshots in ledger order (oldest first).
174
+ */
175
+ export function readCalibrationSnapshots(eventLedger, filter = {}) {
176
+ const events =
177
+ eventLedger && typeof eventLedger.readEvents === "function" ? eventLedger.readEvents(filter) : [];
178
+ const snapshots = [];
179
+ for (const event of Array.isArray(events) ? events : []) {
180
+ if (event?.type !== MINER_CALIBRATION_SNAPSHOT_EVENT) continue;
181
+ const normalized = normalizeCalibrationSnapshotPayload(event.payload);
182
+ if (!normalized) continue;
183
+ snapshots.push({
184
+ ...normalized,
185
+ repoFullName: typeof event.repoFullName === "string" ? event.repoFullName : null,
186
+ seq: Number.isInteger(event.seq) ? event.seq : null,
187
+ createdAt: optionalString(event.createdAt),
188
+ });
189
+ }
190
+ return snapshots;
191
+ }
192
+
193
+ /** The most recent persisted calibration snapshot, or `null` when none exist. */
194
+ export function latestCalibrationSnapshot(eventLedger, filter = {}) {
195
+ const snapshots = readCalibrationSnapshots(eventLedger, filter);
196
+ return snapshots.length > 0 ? snapshots[snapshots.length - 1] : null;
197
+ }
198
+
199
+ /**
200
+ * The runner. Scores the replay run (via the objective-anchor scorer), calls the engine's calibration combine with
201
+ * the resulting historical-replay composite plus the existing pr_outcome signal, and -- when an event ledger is
202
+ * injected -- persists the combined snapshot. Returns the engine result, the derived snapshot payload, the recorded
203
+ * ledger entry (or null when no ledger was injected or the payload was malformed), and the un-fabricated
204
+ * composite/sample provenance. The engine combine (`computeLoop`) is injectable so unit tests can pin it.
205
+ */
206
+ export function runHistoricalReplayCalibrationCycle(input = {}, deps = {}) {
207
+ const computeLoop = deps.computeLoop ?? computePhase7CalibrationLoop;
208
+ const built = buildHistoricalReplayCalibrationInput(input.replayRun, deps);
209
+ const result = computeLoop({
210
+ config: input.config,
211
+ prOutcome: input.prOutcome,
212
+ historicalReplay: built.historicalReplay,
213
+ now: input.now,
214
+ });
215
+ const snapshot = snapshotPayloadFromResult(result, {
216
+ replayRunId: built.historicalReplay?.replayRunId ?? null,
217
+ observedAt: input.observedAt ?? built.historicalReplay?.observedAt ?? null,
218
+ sampleSize: built.sampleSize,
219
+ });
220
+ const recorded = deps.eventLedger
221
+ ? recordCalibrationSnapshot(snapshot, { eventLedger: deps.eventLedger, repoFullName: input.repoFullName })
222
+ : null;
223
+ return {
224
+ result,
225
+ snapshot,
226
+ recorded,
227
+ historicalReplay: built.historicalReplay,
228
+ compositeScore: built.compositeScore,
229
+ sampleSize: built.sampleSize,
230
+ scores: built.scores,
231
+ };
232
+ }
@@ -0,0 +1,46 @@
1
+ /** A single gate-prediction row the miner will replay against observed outcomes. */
2
+ export type PredictedVerdictRecord = {
3
+ targetId: string;
4
+ project: string;
5
+ predictedDecision: string;
6
+ recordedAt: string;
7
+ source?: string;
8
+ };
9
+
10
+ /** The realized human outcome for a previously predicted target. */
11
+ export type ObservedOutcomeRecord = {
12
+ targetId: string;
13
+ project: string;
14
+ outcomeDecision: string;
15
+ recordedAt: string;
16
+ };
17
+
18
+ /** Per-project confusion-matrix row — field names mirror `GateEvalRow` in `src/review/parity.ts`. */
19
+ export type CalibrationRow = {
20
+ project: string;
21
+ wouldMerge: number;
22
+ mergeConfirmed: number;
23
+ mergeFalse: number;
24
+ wouldClose: number;
25
+ closeConfirmed: number;
26
+ closeFalse: number;
27
+ hold: number;
28
+ decided: number;
29
+ mergePrecision: number | null;
30
+ closePrecision: number | null;
31
+ };
32
+
33
+ /** Aggregate calibration report over one or more projects. */
34
+ export type CalibrationReport = {
35
+ rows: CalibrationRow[];
36
+ /** True once at least one project has enough decided samples to read meaningfully. */
37
+ hasSignal: boolean;
38
+ };
39
+
40
+ export function isPredictedVerdictRecord(value: unknown): value is PredictedVerdictRecord;
41
+
42
+ export function isObservedOutcomeRecord(value: unknown): value is ObservedOutcomeRecord;
43
+
44
+ export function isCalibrationRow(value: unknown): value is CalibrationRow;
45
+
46
+ export function isCalibrationReport(value: unknown): value is CalibrationReport;
@@ -0,0 +1,74 @@
1
+ // Shared calibration shapes for the miner self-improvement phase (#2332). Types-only scaffolding —
2
+ // report/ledger/metrics issues build on this module. Field names mirror `GateEvalRow` /
3
+ // `GateEvalReport` in `src/review/parity.ts` for easy mental mapping without importing cloud code.
4
+
5
+ function isNonEmptyString(value) {
6
+ return typeof value === "string" && value.trim().length > 0;
7
+ }
8
+
9
+ function isOptionalString(value) {
10
+ return value === undefined || isNonEmptyString(value);
11
+ }
12
+
13
+ function isNonNegativeInteger(value) {
14
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
15
+ }
16
+
17
+ function isNullableRatio(value) {
18
+ return value === null || (typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1);
19
+ }
20
+
21
+ /** @param {unknown} value */
22
+ export function isPredictedVerdictRecord(value) {
23
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
24
+ const record = /** @type {Record<string, unknown>} */ (value);
25
+ return (
26
+ isNonEmptyString(record.targetId)
27
+ && isNonEmptyString(record.project)
28
+ && isNonEmptyString(record.predictedDecision)
29
+ && isNonEmptyString(record.recordedAt)
30
+ && isOptionalString(record.source)
31
+ );
32
+ }
33
+
34
+ /** @param {unknown} value */
35
+ export function isObservedOutcomeRecord(value) {
36
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
37
+ const record = /** @type {Record<string, unknown>} */ (value);
38
+ return (
39
+ isNonEmptyString(record.targetId)
40
+ && isNonEmptyString(record.project)
41
+ && isNonEmptyString(record.outcomeDecision)
42
+ && isNonEmptyString(record.recordedAt)
43
+ );
44
+ }
45
+
46
+ /** @param {unknown} value */
47
+ export function isCalibrationRow(value) {
48
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
49
+ const row = /** @type {Record<string, unknown>} */ (value);
50
+ return (
51
+ isNonEmptyString(row.project)
52
+ && isNonNegativeInteger(row.wouldMerge)
53
+ && isNonNegativeInteger(row.mergeConfirmed)
54
+ && isNonNegativeInteger(row.mergeFalse)
55
+ && isNonNegativeInteger(row.wouldClose)
56
+ && isNonNegativeInteger(row.closeConfirmed)
57
+ && isNonNegativeInteger(row.closeFalse)
58
+ && isNonNegativeInteger(row.hold)
59
+ && isNonNegativeInteger(row.decided)
60
+ && isNullableRatio(row.mergePrecision)
61
+ && isNullableRatio(row.closePrecision)
62
+ );
63
+ }
64
+
65
+ /** @param {unknown} value */
66
+ export function isCalibrationReport(value) {
67
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
68
+ const report = /** @type {Record<string, unknown>} */ (value);
69
+ return (
70
+ typeof report.hasSignal === "boolean"
71
+ && Array.isArray(report.rows)
72
+ && report.rows.every((row) => isCalibrationRow(row))
73
+ );
74
+ }
@@ -0,0 +1,24 @@
1
+ import type {
2
+ CalibrationReport,
3
+ ObservedOutcomeRecord,
4
+ PredictedVerdictRecord,
5
+ } from "./calibration-types.js";
6
+
7
+ export type {
8
+ CalibrationReport,
9
+ CalibrationRow,
10
+ ObservedOutcomeRecord,
11
+ PredictedVerdictRecord,
12
+ } from "./calibration-types.js";
13
+
14
+ export {
15
+ isCalibrationReport,
16
+ isCalibrationRow,
17
+ isObservedOutcomeRecord,
18
+ isPredictedVerdictRecord,
19
+ } from "./calibration-types.js";
20
+
21
+ export function buildCalibrationReport(
22
+ predictions: PredictedVerdictRecord[],
23
+ outcomes: ObservedOutcomeRecord[],
24
+ ): CalibrationReport;