@loopover/engine 1.0.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 (272) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/LICENSE +661 -0
  3. package/README.md +676 -0
  4. package/dist/advisory/gate-advisory.d.ts +151 -0
  5. package/dist/advisory/gate-advisory.js +512 -0
  6. package/dist/ai-policy-map.d.ts +97 -0
  7. package/dist/ai-policy-map.js +377 -0
  8. package/dist/ams-policy-spec.d.ts +56 -0
  9. package/dist/ams-policy-spec.js +165 -0
  10. package/dist/calibration-dashboard.d.ts +23 -0
  11. package/dist/calibration-dashboard.js +65 -0
  12. package/dist/calibration-trend.d.ts +40 -0
  13. package/dist/calibration-trend.js +60 -0
  14. package/dist/contributor-fit.d.ts +24 -0
  15. package/dist/contributor-fit.js +50 -0
  16. package/dist/discovery-index-contract.d.ts +74 -0
  17. package/dist/discovery-index-contract.js +184 -0
  18. package/dist/discovery-soft-claim.d.ts +35 -0
  19. package/dist/discovery-soft-claim.js +50 -0
  20. package/dist/duplicate-winner.d.ts +57 -0
  21. package/dist/duplicate-winner.js +98 -0
  22. package/dist/feasibility.d.ts +27 -0
  23. package/dist/feasibility.js +86 -0
  24. package/dist/finding-severity-calibration.d.ts +116 -0
  25. package/dist/finding-severity-calibration.js +457 -0
  26. package/dist/fleet-run-manifest.d.ts +41 -0
  27. package/dist/fleet-run-manifest.js +151 -0
  28. package/dist/focus-manifest/guidance.d.ts +21 -0
  29. package/dist/focus-manifest/guidance.js +222 -0
  30. package/dist/focus-manifest.d.ts +826 -0
  31. package/dist/focus-manifest.js +2730 -0
  32. package/dist/gate-verdict-calibration.d.ts +111 -0
  33. package/dist/gate-verdict-calibration.js +411 -0
  34. package/dist/github/constants.d.ts +1 -0
  35. package/dist/github/constants.js +1 -0
  36. package/dist/github/sanitize-public-comment.d.ts +1 -0
  37. package/dist/github/sanitize-public-comment.js +26 -0
  38. package/dist/goal-model.d.ts +7 -0
  39. package/dist/goal-model.js +87 -0
  40. package/dist/governor/action-mode.d.ts +55 -0
  41. package/dist/governor/action-mode.js +74 -0
  42. package/dist/governor/budget-cap.d.ts +49 -0
  43. package/dist/governor/budget-cap.js +34 -0
  44. package/dist/governor/chokepoint.d.ts +58 -0
  45. package/dist/governor/chokepoint.js +269 -0
  46. package/dist/governor/kill-switch.d.ts +38 -0
  47. package/dist/governor/kill-switch.js +62 -0
  48. package/dist/governor/rate-limit.d.ts +40 -0
  49. package/dist/governor/rate-limit.js +63 -0
  50. package/dist/governor/reputation-throttle.d.ts +50 -0
  51. package/dist/governor/reputation-throttle.js +94 -0
  52. package/dist/governor/run-halt.d.ts +39 -0
  53. package/dist/governor/run-halt.js +83 -0
  54. package/dist/governor/self-plagiarism.d.ts +47 -0
  55. package/dist/governor/self-plagiarism.js +200 -0
  56. package/dist/governor/write-rate-limit.d.ts +52 -0
  57. package/dist/governor/write-rate-limit.js +132 -0
  58. package/dist/governor-ledger.d.ts +25 -0
  59. package/dist/governor-ledger.js +91 -0
  60. package/dist/index.d.ts +99 -0
  61. package/dist/index.js +120 -0
  62. package/dist/issue-plan-decomposition.d.ts +18 -0
  63. package/dist/issue-plan-decomposition.js +56 -0
  64. package/dist/issue-rag-query.d.ts +12 -0
  65. package/dist/issue-rag-query.js +35 -0
  66. package/dist/local-scorer.d.ts +34 -0
  67. package/dist/local-scorer.js +36 -0
  68. package/dist/metadata-best-min-score.d.ts +9 -0
  69. package/dist/metadata-best-min-score.js +9 -0
  70. package/dist/metadata-best-pick.d.ts +9 -0
  71. package/dist/metadata-best-pick.js +9 -0
  72. package/dist/metadata-min-score.d.ts +9 -0
  73. package/dist/metadata-min-score.js +13 -0
  74. package/dist/metadata-top-min-score.d.ts +9 -0
  75. package/dist/metadata-top-min-score.js +13 -0
  76. package/dist/metadata-top-pick.d.ts +9 -0
  77. package/dist/metadata-top-pick.js +13 -0
  78. package/dist/miner/acceptance-criteria.d.ts +43 -0
  79. package/dist/miner/acceptance-criteria.js +77 -0
  80. package/dist/miner/agent-sdk-driver.d.ts +39 -0
  81. package/dist/miner/agent-sdk-driver.js +162 -0
  82. package/dist/miner/attempt-log.d.ts +29 -0
  83. package/dist/miner/attempt-log.js +100 -0
  84. package/dist/miner/attempt-metering.d.ts +39 -0
  85. package/dist/miner/attempt-metering.js +67 -0
  86. package/dist/miner/cli-subprocess-driver.d.ts +68 -0
  87. package/dist/miner/cli-subprocess-driver.js +272 -0
  88. package/dist/miner/coding-agent-driver.d.ts +33 -0
  89. package/dist/miner/coding-agent-driver.js +36 -0
  90. package/dist/miner/coding-agent-invoke.d.ts +11 -0
  91. package/dist/miner/coding-agent-invoke.js +80 -0
  92. package/dist/miner/coding-agent-mode.d.ts +25 -0
  93. package/dist/miner/coding-agent-mode.js +43 -0
  94. package/dist/miner/driver-factory.d.ts +82 -0
  95. package/dist/miner/driver-factory.js +169 -0
  96. package/dist/miner/harness-submission-trigger.d.ts +37 -0
  97. package/dist/miner/harness-submission-trigger.js +46 -0
  98. package/dist/miner/iterate-loop.d.ts +97 -0
  99. package/dist/miner/iterate-loop.js +278 -0
  100. package/dist/miner/iterate-policy.d.ts +98 -0
  101. package/dist/miner/iterate-policy.js +98 -0
  102. package/dist/miner/lint-guard.d.ts +50 -0
  103. package/dist/miner/lint-guard.js +68 -0
  104. package/dist/miner/local-write-tools.d.ts +81 -0
  105. package/dist/miner/local-write-tools.js +113 -0
  106. package/dist/miner/loop-reentry-policy.d.ts +36 -0
  107. package/dist/miner/loop-reentry-policy.js +53 -0
  108. package/dist/miner/repo-map.d.ts +51 -0
  109. package/dist/miner/repo-map.js +268 -0
  110. package/dist/miner/self-review-adapter.d.ts +106 -0
  111. package/dist/miner/self-review-adapter.js +83 -0
  112. package/dist/miner/submission-gate.d.ts +40 -0
  113. package/dist/miner/submission-gate.js +89 -0
  114. package/dist/miner/worktree-allocator.d.ts +59 -0
  115. package/dist/miner/worktree-allocator.js +60 -0
  116. package/dist/miner/worktree-pool.d.ts +55 -0
  117. package/dist/miner/worktree-pool.js +65 -0
  118. package/dist/miner-goal-lane-fit.d.ts +18 -0
  119. package/dist/miner-goal-lane-fit.js +76 -0
  120. package/dist/miner-goal-spec.d.ts +139 -0
  121. package/dist/miner-goal-spec.js +281 -0
  122. package/dist/miner-prediction-metrics.d.ts +16 -0
  123. package/dist/miner-prediction-metrics.js +61 -0
  124. package/dist/miner-telemetry.d.ts +32 -0
  125. package/dist/miner-telemetry.js +87 -0
  126. package/dist/objective-anchor.d.ts +125 -0
  127. package/dist/objective-anchor.js +401 -0
  128. package/dist/opportunity-competition.d.ts +6 -0
  129. package/dist/opportunity-competition.js +29 -0
  130. package/dist/opportunity-freshness.d.ts +18 -0
  131. package/dist/opportunity-freshness.js +55 -0
  132. package/dist/opportunity-metadata.d.ts +53 -0
  133. package/dist/opportunity-metadata.js +204 -0
  134. package/dist/opportunity-ranker.d.ts +46 -0
  135. package/dist/opportunity-ranker.js +74 -0
  136. package/dist/pairwise-calibration.d.ts +49 -0
  137. package/dist/pairwise-calibration.js +106 -0
  138. package/dist/phase7-calibration-loop.d.ts +136 -0
  139. package/dist/phase7-calibration-loop.js +437 -0
  140. package/dist/plan-blocked.d.ts +6 -0
  141. package/dist/plan-blocked.js +24 -0
  142. package/dist/plan-completed.d.ts +5 -0
  143. package/dist/plan-completed.js +6 -0
  144. package/dist/plan-completion.d.ts +6 -0
  145. package/dist/plan-completion.js +7 -0
  146. package/dist/plan-empty.d.ts +5 -0
  147. package/dist/plan-empty.js +6 -0
  148. package/dist/plan-export.d.ts +27 -0
  149. package/dist/plan-export.js +86 -0
  150. package/dist/plan-failure.d.ts +5 -0
  151. package/dist/plan-failure.js +6 -0
  152. package/dist/plan-overall-status.d.ts +6 -0
  153. package/dist/plan-overall-status.js +25 -0
  154. package/dist/plan-pending.d.ts +5 -0
  155. package/dist/plan-pending.js +6 -0
  156. package/dist/plan-progress-complete.d.ts +7 -0
  157. package/dist/plan-progress-complete.js +10 -0
  158. package/dist/plan-ready.d.ts +6 -0
  159. package/dist/plan-ready.js +12 -0
  160. package/dist/plan-running.d.ts +5 -0
  161. package/dist/plan-running.js +6 -0
  162. package/dist/plan-skipped.d.ts +5 -0
  163. package/dist/plan-skipped.js +6 -0
  164. package/dist/plan-step-count.d.ts +5 -0
  165. package/dist/plan-step-count.js +6 -0
  166. package/dist/plan-step-stats.d.ts +5 -0
  167. package/dist/plan-step-stats.js +6 -0
  168. package/dist/plan-templates.d.ts +25 -0
  169. package/dist/plan-templates.js +107 -0
  170. package/dist/plan-terminated.d.ts +6 -0
  171. package/dist/plan-terminated.js +9 -0
  172. package/dist/portfolio/non-convergence.d.ts +37 -0
  173. package/dist/portfolio/non-convergence.js +57 -0
  174. package/dist/portfolio/queue.d.ts +30 -0
  175. package/dist/portfolio/queue.js +150 -0
  176. package/dist/predicted-gate.d.ts +103 -0
  177. package/dist/predicted-gate.js +218 -0
  178. package/dist/prompt-packet.d.ts +11 -0
  179. package/dist/prompt-packet.js +34 -0
  180. package/dist/ranked-opportunity-best-min-score.d.ts +8 -0
  181. package/dist/ranked-opportunity-best-min-score.js +9 -0
  182. package/dist/ranked-opportunity-best-pick.d.ts +8 -0
  183. package/dist/ranked-opportunity-best-pick.js +9 -0
  184. package/dist/ranked-opportunity-min-score.d.ts +8 -0
  185. package/dist/ranked-opportunity-min-score.js +13 -0
  186. package/dist/ranked-opportunity-top-min-score.d.ts +8 -0
  187. package/dist/ranked-opportunity-top-min-score.js +13 -0
  188. package/dist/review/advisory-ai-routing-config.d.ts +8 -0
  189. package/dist/review/advisory-ai-routing-config.js +40 -0
  190. package/dist/review/check-names.d.ts +1 -0
  191. package/dist/review/check-names.js +1 -0
  192. package/dist/review/cla-check.d.ts +49 -0
  193. package/dist/review/cla-check.js +71 -0
  194. package/dist/review/diff-file-priority.d.ts +1 -0
  195. package/dist/review/diff-file-priority.js +12 -0
  196. package/dist/review/enrichment-analyzer-names.d.ts +3 -0
  197. package/dist/review/enrichment-analyzer-names.js +63 -0
  198. package/dist/review/guardrail-config.d.ts +10 -0
  199. package/dist/review/guardrail-config.js +62 -0
  200. package/dist/review/linked-issue-hard-rules-config.d.ts +4 -0
  201. package/dist/review/linked-issue-hard-rules-config.js +79 -0
  202. package/dist/review/linked-issue-label-propagation.d.ts +9 -0
  203. package/dist/review/linked-issue-label-propagation.js +114 -0
  204. package/dist/review/pre-merge-checks.d.ts +28 -0
  205. package/dist/review/pre-merge-checks.js +66 -0
  206. package/dist/review/review-thread-findings.d.ts +1 -0
  207. package/dist/review/review-thread-findings.js +1 -0
  208. package/dist/review/safe-url.d.ts +5 -0
  209. package/dist/review/safe-url.js +137 -0
  210. package/dist/review/screenshot-table-gate.d.ts +87 -0
  211. package/dist/review/screenshot-table-gate.js +340 -0
  212. package/dist/review/unlinked-issue-guardrail-config.d.ts +9 -0
  213. package/dist/review/unlinked-issue-guardrail-config.js +44 -0
  214. package/dist/reviewer-consensus-calibration.d.ts +113 -0
  215. package/dist/reviewer-consensus-calibration.js +493 -0
  216. package/dist/reward-risk.d.ts +223 -0
  217. package/dist/reward-risk.js +618 -0
  218. package/dist/scoring/label-match.d.ts +4 -0
  219. package/dist/scoring/label-match.js +145 -0
  220. package/dist/scoring/model.d.ts +31 -0
  221. package/dist/scoring/model.js +157 -0
  222. package/dist/scoring/pending-pr-scenarios.d.ts +44 -0
  223. package/dist/scoring/pending-pr-scenarios.js +145 -0
  224. package/dist/scoring/preview.d.ts +246 -0
  225. package/dist/scoring/preview.js +1108 -0
  226. package/dist/scoring/types.d.ts +131 -0
  227. package/dist/scoring/types.js +7 -0
  228. package/dist/settings/auto-close-exempt.d.ts +10 -0
  229. package/dist/settings/auto-close-exempt.js +58 -0
  230. package/dist/settings/autonomy.d.ts +33 -0
  231. package/dist/settings/autonomy.js +80 -0
  232. package/dist/settings/command-authorization.d.ts +49 -0
  233. package/dist/settings/command-authorization.js +222 -0
  234. package/dist/settings/contributor-blacklist.d.ts +17 -0
  235. package/dist/settings/contributor-blacklist.js +86 -0
  236. package/dist/settings/moderation-rules.d.ts +51 -0
  237. package/dist/settings/moderation-rules.js +104 -0
  238. package/dist/settings/pr-type-label.d.ts +70 -0
  239. package/dist/settings/pr-type-label.js +149 -0
  240. package/dist/signals/change-guardrail.d.ts +49 -0
  241. package/dist/signals/change-guardrail.js +164 -0
  242. package/dist/signals/check-summary.d.ts +9 -0
  243. package/dist/signals/check-summary.js +12 -0
  244. package/dist/signals/contributor-calibration.d.ts +23 -0
  245. package/dist/signals/contributor-calibration.js +47 -0
  246. package/dist/signals/duplicate-winner.d.ts +9 -0
  247. package/dist/signals/duplicate-winner.js +9 -0
  248. package/dist/signals/path-matchers.d.ts +36 -0
  249. package/dist/signals/path-matchers.js +371 -0
  250. package/dist/signals/predicted-gate-engine.d.ts +62 -0
  251. package/dist/signals/predicted-gate-engine.js +895 -0
  252. package/dist/signals/preflight-limits.d.ts +14 -0
  253. package/dist/signals/preflight-limits.js +14 -0
  254. package/dist/signals/slop.d.ts +56 -0
  255. package/dist/signals/slop.js +324 -0
  256. package/dist/signals/test-evidence.d.ts +25 -0
  257. package/dist/signals/test-evidence.js +108 -0
  258. package/dist/subprocess-env.d.ts +23 -0
  259. package/dist/subprocess-env.js +74 -0
  260. package/dist/track-record-summary.d.ts +92 -0
  261. package/dist/track-record-summary.js +301 -0
  262. package/dist/types/manifest-deps-types.d.ts +514 -0
  263. package/dist/types/manifest-deps-types.js +4 -0
  264. package/dist/types/predicted-gate-types.d.ts +338 -0
  265. package/dist/types/predicted-gate-types.js +3 -0
  266. package/dist/types/reward-risk-types.d.ts +319 -0
  267. package/dist/types/reward-risk-types.js +11 -0
  268. package/dist/utils/json.d.ts +1 -0
  269. package/dist/utils/json.js +3 -0
  270. package/dist/version.d.ts +2 -0
  271. package/dist/version.js +3 -0
  272. package/package.json +80 -0
@@ -0,0 +1,113 @@
1
+ import type { ObjectiveAnchorScore } from "./objective-anchor.js";
2
+ import type { PairwiseCalibrationScore } from "./pairwise-calibration.js";
3
+ export type ReviewerConsensusDimension = "correctness" | "tests" | "security" | "maintainability" | "scope" | "freshness" | "ci" | "policy";
4
+ export type ReviewerConsensusVote = "pass" | "warn" | "fail";
5
+ export type ReviewerConsensusCalibrationManifest = {
6
+ miner?: {
7
+ calibration?: {
8
+ /** Explicit maintainer opt-in. Default false. */
9
+ shareStructuredReviewerConsensus?: unknown;
10
+ /** Optional weight for the structured reviewer-consensus signal when composed into a replay score. */
11
+ structuredReviewerConsensusWeight?: unknown;
12
+ } | null;
13
+ } | null;
14
+ calibration?: {
15
+ /** Back-compat/future-friendly alias, still explicit and default-off. */
16
+ shareStructuredReviewerConsensus?: unknown;
17
+ structuredReviewerConsensusWeight?: unknown;
18
+ } | null;
19
+ };
20
+ export type ReviewerConsensusCalibrationConfig = {
21
+ shareStructuredReviewerConsensus: boolean;
22
+ structuredReviewerConsensusWeight: number;
23
+ warnings: string[];
24
+ };
25
+ export type ReviewerConsensusDimensionInput = {
26
+ dimension: ReviewerConsensusDimension | string;
27
+ /** One verdict per independent reviewer. Unrecognized / abstention votes are dropped before agreement is measured. */
28
+ votes: readonly (ReviewerConsensusVote | string)[];
29
+ };
30
+ export type ReviewerConsensusCalibrationSignalInput = {
31
+ repoFullName: string;
32
+ replayRunId: string;
33
+ reviewRunId: string;
34
+ optedIn: boolean;
35
+ observedAt?: string | undefined;
36
+ dimensions: readonly ReviewerConsensusDimensionInput[];
37
+ };
38
+ export type ReviewerConsensusDimensionSignal = {
39
+ dimension: ReviewerConsensusDimension;
40
+ voteCount: number;
41
+ majorityOutcome: ReviewerConsensusVote;
42
+ agreement: number;
43
+ score: number;
44
+ };
45
+ export type ReviewerConsensusCalibrationSignal = {
46
+ repoFullName: string;
47
+ replayRunId: string;
48
+ reviewRunId: string;
49
+ observedAt: string | null;
50
+ dimensions: ReviewerConsensusDimensionSignal[];
51
+ score: number;
52
+ };
53
+ export type ReviewerConsensusCalibrationIngestion = {
54
+ accepted: ReviewerConsensusCalibrationSignal[];
55
+ rejected: Array<{
56
+ repoFullName: string;
57
+ replayRunId: string;
58
+ reviewRunId: string;
59
+ reason: "not_opted_in" | "empty_dimensions" | "invalid_repo" | "invalid_run_id";
60
+ }>;
61
+ };
62
+ export type ReviewerConsensusCalibrationWeights = {
63
+ objectiveAnchor?: number | undefined;
64
+ pairwiseJudge?: number | undefined;
65
+ structuredReviewerConsensus?: number | undefined;
66
+ };
67
+ export type ReviewerConsensusCompositeCalibrationScore = {
68
+ compositeScore: number;
69
+ objectiveAnchorScore: number;
70
+ pairwiseJudgeScore: number | null;
71
+ structuredReviewerConsensusScore: number | null;
72
+ weights: {
73
+ objectiveAnchor: number;
74
+ pairwiseJudge: number;
75
+ structuredReviewerConsensus: number;
76
+ };
77
+ audit: {
78
+ contributingRepos: Array<{
79
+ repoFullName: string;
80
+ replayRunId: string;
81
+ reviewRunId: string;
82
+ observedAt: string | null;
83
+ score: number;
84
+ dimensions: ReviewerConsensusDimensionSignal[];
85
+ }>;
86
+ rejected: ReviewerConsensusCalibrationIngestion["rejected"];
87
+ };
88
+ };
89
+ /**
90
+ * Resolve the explicit per-repo opt-in from a parsed `.gittensory.yml`-style object. Default is opted out. The
91
+ * preferred path is `miner.calibration.shareStructuredReviewerConsensus`;
92
+ * `calibration.shareStructuredReviewerConsensus` is accepted as a narrow alias so private-config surfaces can place
93
+ * the field at top level if needed.
94
+ */
95
+ export declare function resolveReviewerConsensusCalibrationConfig(manifest: ReviewerConsensusCalibrationManifest | Record<string, unknown> | null | undefined): ReviewerConsensusCalibrationConfig;
96
+ /**
97
+ * Ingest only currently opted-in structured reviewer-consensus signals. The opt-in check happens at ingestion time, so
98
+ * a maintainer opt-out immediately prevents additional calibration rows from contributing even if older collected data
99
+ * exists elsewhere.
100
+ */
101
+ export declare function ingestReviewerConsensusCalibrationSignals(signals: readonly ReviewerConsensusCalibrationSignalInput[]): ReviewerConsensusCalibrationIngestion;
102
+ export declare function computeReviewerConsensusCompositeCalibrationScore(input: {
103
+ objectiveAnchor: number | ObjectiveAnchorScore;
104
+ pairwise: number | PairwiseCalibrationScore | null;
105
+ reviewerConsensus: ReviewerConsensusCalibrationIngestion | readonly ReviewerConsensusCalibrationSignalInput[];
106
+ weights?: ReviewerConsensusCalibrationWeights | undefined;
107
+ }): ReviewerConsensusCompositeCalibrationScore;
108
+ /**
109
+ * Render a deterministic, public-safe Markdown report for a structured reviewer-consensus calibration result. The
110
+ * report is local-run evidence: it includes aggregate scores, normalized weights, opted-in contributors, and rejected
111
+ * rows, but never accepts or emits raw review text or private scoring fields.
112
+ */
113
+ export declare function renderReviewerConsensusCalibrationAuditMarkdown(result: ReviewerConsensusCompositeCalibrationScore): string;
@@ -0,0 +1,493 @@
1
+ // Opt-in structured reviewer-consensus calibration signal (#1955 calibration family).
2
+ //
3
+ // This module is the pure engine half of reviewer-consensus calibration. When a review runs more than one
4
+ // independent reviewer (multiple models, or the same model sampled multiple times), each reviewer casts a per-dimension
5
+ // verdict. This signal measures how much those reviewers AGREE per dimension: a high-agreement verdict is reliable,
6
+ // while a split verdict is unstable and the replay harness should weight it less. It is a companion to the pairwise
7
+ // judge (which measures order-stability of a single judge) at the level of independent reviewers.
8
+ //
9
+ // The hosted review stack decides whether a repo is currently opted in from its resolved `.gittensory.yml`/private
10
+ // config; the miner replay harness can then ingest only the structured per-dimension vote fields exposed here. No raw
11
+ // review text, secrets, trust values, rewards, rankings, or maintainer evidence is represented in this type surface.
12
+ const DIMENSION_ORDER = [
13
+ "correctness",
14
+ "tests",
15
+ "security",
16
+ "maintainability",
17
+ "scope",
18
+ "freshness",
19
+ "ci",
20
+ "policy",
21
+ ];
22
+ // Tie-break order when two outcomes draw the plurality: prefer the more severe outcome, so a genuine split never
23
+ // rounds a real `fail`/`warn` signal down to `pass`.
24
+ const VOTE_SEVERITY = {
25
+ fail: 2,
26
+ warn: 1,
27
+ pass: 0,
28
+ };
29
+ const DEFAULT_STRUCTURED_REVIEWER_CONSENSUS_WEIGHT = 0.2;
30
+ const DEFAULT_COMPOSITE_WEIGHTS = {
31
+ objectiveAnchor: 0.45,
32
+ pairwiseJudge: 0.35,
33
+ structuredReviewerConsensus: 0.2,
34
+ };
35
+ function isRecord(value) {
36
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
37
+ }
38
+ function finiteNonNegative(value, fallback) {
39
+ if (value === undefined)
40
+ return fallback;
41
+ if (!Number.isFinite(value) || value < 0)
42
+ return 0;
43
+ return value;
44
+ }
45
+ function roundScore(value) {
46
+ return Math.round(Math.min(1, Math.max(0, value)) * 1_000_000) / 1_000_000;
47
+ }
48
+ function normalizeRepoFullName(value) {
49
+ const trimmed = value.trim().toLowerCase();
50
+ if (!/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/u.test(trimmed))
51
+ return null;
52
+ return trimmed;
53
+ }
54
+ function normalizeId(value) {
55
+ const trimmed = value.trim();
56
+ if (!trimmed || trimmed.length > 160 || /[\r\n\0]/u.test(trimmed))
57
+ return null;
58
+ return trimmed;
59
+ }
60
+ function normalizeObservedAt(value) {
61
+ if (!value)
62
+ return null;
63
+ const ms = Date.parse(value);
64
+ if (!Number.isFinite(ms))
65
+ return null;
66
+ return new Date(ms).toISOString();
67
+ }
68
+ function normalizeBoolean(value) {
69
+ if (typeof value === "boolean")
70
+ return value;
71
+ if (typeof value !== "string")
72
+ return undefined;
73
+ const normalized = value.trim().toLowerCase();
74
+ if (["true", "1", "yes", "on"].includes(normalized))
75
+ return true;
76
+ if (["false", "0", "no", "off"].includes(normalized))
77
+ return false;
78
+ return undefined;
79
+ }
80
+ function normalizeOptionalWeight(value) {
81
+ if (value === undefined || value === null)
82
+ return undefined;
83
+ const number = typeof value === "number" ? value : typeof value === "string" ? Number(value.trim()) : Number.NaN;
84
+ if (!Number.isFinite(number) || number < 0)
85
+ return undefined;
86
+ return number;
87
+ }
88
+ function normalizeDimension(value) {
89
+ const normalized = value.trim().toLowerCase().replace(/[_\s-]+/gu, "_");
90
+ if (normalized === "quality" || normalized === "code_quality")
91
+ return "correctness";
92
+ if (normalized === "test" || normalized === "coverage")
93
+ return "tests";
94
+ if (normalized === "maintenance")
95
+ return "maintainability";
96
+ if (normalized === "size" || normalized === "blast_radius")
97
+ return "scope";
98
+ if (normalized === "rebase" || normalized === "up_to_date")
99
+ return "freshness";
100
+ if (normalized === "workflow" || normalized === "checks")
101
+ return "ci";
102
+ if (DIMENSION_ORDER.includes(normalized))
103
+ return normalized;
104
+ return null;
105
+ }
106
+ function normalizeVote(value) {
107
+ const normalized = value.trim().toLowerCase().replace(/[_\s-]+/gu, "_");
108
+ if (normalized === "ok" || normalized === "success" || normalized === "passed" || normalized === "approve") {
109
+ return "pass";
110
+ }
111
+ if (normalized === "warning" || normalized === "advisory" || normalized === "hold" || normalized === "comment") {
112
+ return "warn";
113
+ }
114
+ if (normalized === "block" || normalized === "blocked" || normalized === "failed" || normalized === "reject") {
115
+ return "fail";
116
+ }
117
+ if (["pass", "warn", "fail"].includes(normalized))
118
+ return normalized;
119
+ return null;
120
+ }
121
+ /**
122
+ * Reduce a dimension's independent votes to a consensus signal: drop unrecognized/abstention votes, tally the rest,
123
+ * pick the plurality outcome (ties broken toward the more severe outcome), and derive the agreement fraction. A
124
+ * dimension left with no definite votes is dropped.
125
+ */
126
+ function summarizeDimensionVotes(votes) {
127
+ const counts = { pass: 0, warn: 0, fail: 0 };
128
+ let voteCount = 0;
129
+ for (const raw of votes) {
130
+ const vote = normalizeVote(raw);
131
+ if (!vote)
132
+ continue;
133
+ counts[vote] += 1;
134
+ voteCount += 1;
135
+ }
136
+ if (voteCount === 0)
137
+ return null;
138
+ let majorityOutcome = "pass";
139
+ let best = -1;
140
+ for (const vote of ["fail", "warn", "pass"]) {
141
+ const count = counts[vote];
142
+ // Strictly greater wins; on a tie the earlier (more severe, per the iteration order) outcome is kept.
143
+ if (count > best || (count === best && VOTE_SEVERITY[vote] > VOTE_SEVERITY[majorityOutcome])) {
144
+ best = count;
145
+ majorityOutcome = vote;
146
+ }
147
+ }
148
+ return { majorityOutcome, voteCount, agreement: roundScore(best / voteCount) };
149
+ }
150
+ function normalizeDimensions(dimensions) {
151
+ const byDimension = new Map();
152
+ for (const item of dimensions) {
153
+ const dimension = normalizeDimension(item.dimension);
154
+ if (!dimension)
155
+ continue;
156
+ const existing = byDimension.get(dimension);
157
+ if (existing) {
158
+ existing.push(...item.votes);
159
+ }
160
+ else {
161
+ byDimension.set(dimension, [...item.votes]);
162
+ }
163
+ }
164
+ return DIMENSION_ORDER.flatMap((dimension) => {
165
+ const votes = byDimension.get(dimension);
166
+ if (!votes)
167
+ return [];
168
+ const summary = summarizeDimensionVotes(votes);
169
+ if (!summary)
170
+ return [];
171
+ return [
172
+ {
173
+ dimension,
174
+ voteCount: summary.voteCount,
175
+ majorityOutcome: summary.majorityOutcome,
176
+ agreement: summary.agreement,
177
+ score: summary.agreement,
178
+ },
179
+ ];
180
+ });
181
+ }
182
+ /**
183
+ * The per-PR consensus score: the vote-count-weighted mean of the per-dimension agreement fractions, so a dimension
184
+ * with more reviewers carries more weight than one with a single reviewer. Returns null when no dimension carries a
185
+ * definite vote (already rejected upstream).
186
+ */
187
+ function scoreDimensions(dimensions) {
188
+ let weightedAgreement = 0;
189
+ let voteSum = 0;
190
+ for (const dimension of dimensions) {
191
+ weightedAgreement += dimension.voteCount * dimension.agreement;
192
+ voteSum += dimension.voteCount;
193
+ }
194
+ if (voteSum <= 0)
195
+ return null;
196
+ return roundScore(weightedAgreement / voteSum);
197
+ }
198
+ function averageSignals(signals) {
199
+ if (signals.length === 0)
200
+ return null;
201
+ return roundScore(signals.reduce((sum, signal) => sum + signal.score, 0) / signals.length);
202
+ }
203
+ function isReviewerConsensusCalibrationIngestion(value) {
204
+ return isRecord(value) && Array.isArray(value.accepted) && Array.isArray(value.rejected);
205
+ }
206
+ function sanitizeReviewerConsensusCalibrationIngestion(ingestion) {
207
+ const accepted = [];
208
+ const rejected = [];
209
+ for (const signal of ingestion.accepted) {
210
+ if (!isRecord(signal) || !Array.isArray(signal.dimensions))
211
+ continue;
212
+ const repoFullName = typeof signal.repoFullName === "string" ? normalizeRepoFullName(signal.repoFullName) : null;
213
+ const replayRunId = typeof signal.replayRunId === "string" ? normalizeId(signal.replayRunId) : null;
214
+ const reviewRunId = typeof signal.reviewRunId === "string" ? normalizeId(signal.reviewRunId) : null;
215
+ if (!repoFullName || !replayRunId || !reviewRunId)
216
+ continue;
217
+ const dimensions = signal.dimensions.flatMap((dimension) => {
218
+ if (!isRecord(dimension) ||
219
+ typeof dimension.dimension !== "string" ||
220
+ typeof dimension.voteCount !== "number" ||
221
+ typeof dimension.majorityOutcome !== "string" ||
222
+ typeof dimension.agreement !== "number") {
223
+ return [];
224
+ }
225
+ const normalizedDimension = normalizeDimension(dimension.dimension);
226
+ const majorityOutcome = normalizeVote(dimension.majorityOutcome);
227
+ if (!normalizedDimension ||
228
+ !majorityOutcome ||
229
+ !Number.isFinite(dimension.voteCount) ||
230
+ dimension.voteCount <= 0 ||
231
+ !Number.isInteger(dimension.voteCount) ||
232
+ !Number.isFinite(dimension.agreement)) {
233
+ return [];
234
+ }
235
+ const agreement = roundScore(dimension.agreement);
236
+ return [
237
+ {
238
+ dimension: normalizedDimension,
239
+ voteCount: dimension.voteCount,
240
+ majorityOutcome,
241
+ agreement,
242
+ score: agreement,
243
+ },
244
+ ];
245
+ });
246
+ const score = scoreDimensions(dimensions);
247
+ if (dimensions.length === 0 || score === null)
248
+ continue;
249
+ accepted.push({
250
+ repoFullName,
251
+ replayRunId,
252
+ reviewRunId,
253
+ observedAt: typeof signal.observedAt === "string" ? normalizeObservedAt(signal.observedAt) : null,
254
+ dimensions,
255
+ score,
256
+ });
257
+ }
258
+ for (const row of ingestion.rejected) {
259
+ if (!isRecord(row))
260
+ continue;
261
+ const repoFullName = typeof row.repoFullName === "string"
262
+ ? (normalizeRepoFullName(row.repoFullName) ?? normalizeId(row.repoFullName))
263
+ : null;
264
+ const replayRunId = typeof row.replayRunId === "string" ? normalizeId(row.replayRunId) : null;
265
+ const reviewRunId = typeof row.reviewRunId === "string" ? normalizeId(row.reviewRunId) : null;
266
+ const reason = row.reason;
267
+ if (!repoFullName ||
268
+ !replayRunId ||
269
+ !reviewRunId ||
270
+ !["not_opted_in", "empty_dimensions", "invalid_repo", "invalid_run_id"].includes(reason)) {
271
+ continue;
272
+ }
273
+ rejected.push({ repoFullName, replayRunId, reviewRunId, reason });
274
+ }
275
+ return { accepted, rejected };
276
+ }
277
+ function normalizeCompositeWeights(weights) {
278
+ const raw = {
279
+ objectiveAnchor: finiteNonNegative(weights?.objectiveAnchor, DEFAULT_COMPOSITE_WEIGHTS.objectiveAnchor),
280
+ pairwiseJudge: finiteNonNegative(weights?.pairwiseJudge, DEFAULT_COMPOSITE_WEIGHTS.pairwiseJudge),
281
+ structuredReviewerConsensus: finiteNonNegative(weights?.structuredReviewerConsensus, DEFAULT_COMPOSITE_WEIGHTS.structuredReviewerConsensus),
282
+ };
283
+ const total = raw.objectiveAnchor + raw.pairwiseJudge + raw.structuredReviewerConsensus;
284
+ // Preserve explicitly-zeroed weights rather than substituting the defaults: a caller that zeroes every component
285
+ // must reach the objective-only fallback in the composite scorer, not silently get the default 45/35/20 blend.
286
+ if (total <= 0)
287
+ return { objectiveAnchor: 0, pairwiseJudge: 0, structuredReviewerConsensus: 0 };
288
+ return {
289
+ objectiveAnchor: raw.objectiveAnchor / total,
290
+ pairwiseJudge: raw.pairwiseJudge / total,
291
+ structuredReviewerConsensus: raw.structuredReviewerConsensus / total,
292
+ };
293
+ }
294
+ function markdownSafe(value) {
295
+ return value.replace(/[\r\n]+/gu, " ").replace(/[\\`*_[\]<>|]/gu, "\\$&");
296
+ }
297
+ function markdownList(values) {
298
+ if (values.length === 0)
299
+ return "- none";
300
+ return values.map((value) => `- ${markdownSafe(value)}`).join("\n");
301
+ }
302
+ function renderDimensionRows(dimensions) {
303
+ if (dimensions.length === 0)
304
+ return "| Dimension | Votes | Majority | Agreement |\n| --- | ---: | --- | ---: |\n";
305
+ return [
306
+ "| Dimension | Votes | Majority | Agreement |",
307
+ "| --- | ---: | --- | ---: |",
308
+ ...dimensions.map((dimension) => `| ${markdownSafe(dimension.dimension)} | ${dimension.voteCount} | ${markdownSafe(dimension.majorityOutcome)} | ${dimension.agreement.toFixed(6)} |`),
309
+ ].join("\n");
310
+ }
311
+ function renderContributingRepo(signal) {
312
+ return [
313
+ `### ${markdownSafe(signal.repoFullName)}`,
314
+ "",
315
+ `- replayRunId: ${markdownSafe(signal.replayRunId)}`,
316
+ `- reviewRunId: ${markdownSafe(signal.reviewRunId)}`,
317
+ `- observedAt: ${signal.observedAt ? markdownSafe(signal.observedAt) : "n/a"}`,
318
+ `- score: ${signal.score.toFixed(6)}`,
319
+ "",
320
+ renderDimensionRows(signal.dimensions),
321
+ ].join("\n");
322
+ }
323
+ function renderRejectedRow(row) {
324
+ return `| ${markdownSafe(row.repoFullName)} | ${markdownSafe(row.replayRunId)} | ${markdownSafe(row.reviewRunId)} | ${markdownSafe(row.reason)} |`;
325
+ }
326
+ /**
327
+ * Resolve the explicit per-repo opt-in from a parsed `.gittensory.yml`-style object. Default is opted out. The
328
+ * preferred path is `miner.calibration.shareStructuredReviewerConsensus`;
329
+ * `calibration.shareStructuredReviewerConsensus` is accepted as a narrow alias so private-config surfaces can place
330
+ * the field at top level if needed.
331
+ */
332
+ export function resolveReviewerConsensusCalibrationConfig(manifest) {
333
+ const warnings = [];
334
+ const root = isRecord(manifest) ? manifest : {};
335
+ const miner = isRecord(root.miner) ? root.miner : {};
336
+ const minerCalibration = isRecord(miner.calibration) ? miner.calibration : {};
337
+ const topCalibration = isRecord(root.calibration) ? root.calibration : {};
338
+ const optInRaw = minerCalibration.shareStructuredReviewerConsensus ?? topCalibration.shareStructuredReviewerConsensus ?? undefined;
339
+ const optIn = normalizeBoolean(optInRaw);
340
+ if (optInRaw !== undefined && optIn === undefined) {
341
+ warnings.push("miner.calibration.shareStructuredReviewerConsensus must be a boolean-like value; defaulting to false.");
342
+ }
343
+ const weightRaw = minerCalibration.structuredReviewerConsensusWeight ?? topCalibration.structuredReviewerConsensusWeight;
344
+ const weight = normalizeOptionalWeight(weightRaw);
345
+ if (weightRaw !== undefined && weight === undefined) {
346
+ warnings.push("miner.calibration.structuredReviewerConsensusWeight must be a non-negative finite number; using default.");
347
+ }
348
+ return {
349
+ shareStructuredReviewerConsensus: optIn === true,
350
+ structuredReviewerConsensusWeight: weight ?? DEFAULT_STRUCTURED_REVIEWER_CONSENSUS_WEIGHT,
351
+ warnings,
352
+ };
353
+ }
354
+ /**
355
+ * Ingest only currently opted-in structured reviewer-consensus signals. The opt-in check happens at ingestion time, so
356
+ * a maintainer opt-out immediately prevents additional calibration rows from contributing even if older collected data
357
+ * exists elsewhere.
358
+ */
359
+ export function ingestReviewerConsensusCalibrationSignals(signals) {
360
+ const accepted = [];
361
+ const rejected = [];
362
+ for (const signal of signals) {
363
+ const repoFullName = normalizeRepoFullName(signal.repoFullName);
364
+ const replayRunId = normalizeId(signal.replayRunId);
365
+ const reviewRunId = normalizeId(signal.reviewRunId);
366
+ if (!repoFullName) {
367
+ rejected.push({
368
+ repoFullName: signal.repoFullName,
369
+ replayRunId: signal.replayRunId,
370
+ reviewRunId: signal.reviewRunId,
371
+ reason: "invalid_repo",
372
+ });
373
+ continue;
374
+ }
375
+ if (!replayRunId || !reviewRunId) {
376
+ rejected.push({
377
+ repoFullName,
378
+ replayRunId: signal.replayRunId,
379
+ reviewRunId: signal.reviewRunId,
380
+ reason: "invalid_run_id",
381
+ });
382
+ continue;
383
+ }
384
+ if (!signal.optedIn) {
385
+ rejected.push({ repoFullName, replayRunId, reviewRunId, reason: "not_opted_in" });
386
+ continue;
387
+ }
388
+ const dimensions = normalizeDimensions(signal.dimensions);
389
+ const score = scoreDimensions(dimensions);
390
+ if (dimensions.length === 0 || score === null) {
391
+ rejected.push({ repoFullName, replayRunId, reviewRunId, reason: "empty_dimensions" });
392
+ continue;
393
+ }
394
+ accepted.push({
395
+ repoFullName,
396
+ replayRunId,
397
+ reviewRunId,
398
+ observedAt: normalizeObservedAt(signal.observedAt),
399
+ dimensions,
400
+ score,
401
+ });
402
+ }
403
+ return { accepted, rejected };
404
+ }
405
+ export function computeReviewerConsensusCompositeCalibrationScore(input) {
406
+ const ingestion = isReviewerConsensusCalibrationIngestion(input.reviewerConsensus)
407
+ ? sanitizeReviewerConsensusCalibrationIngestion(input.reviewerConsensus)
408
+ : ingestReviewerConsensusCalibrationSignals(input.reviewerConsensus);
409
+ const objectiveAnchorScore = typeof input.objectiveAnchor === "number" ? roundScore(input.objectiveAnchor) : input.objectiveAnchor.score;
410
+ const pairwiseJudgeScore = input.pairwise === null
411
+ ? null
412
+ : typeof input.pairwise === "number"
413
+ ? roundScore(input.pairwise)
414
+ : input.pairwise.pairwiseJudgeScore;
415
+ const structuredReviewerConsensusScore = averageSignals(ingestion.accepted);
416
+ const rawWeights = normalizeCompositeWeights(input.weights);
417
+ const usableWeights = {
418
+ objectiveAnchor: rawWeights.objectiveAnchor,
419
+ pairwiseJudge: pairwiseJudgeScore === null ? 0 : rawWeights.pairwiseJudge,
420
+ structuredReviewerConsensus: structuredReviewerConsensusScore === null ? 0 : rawWeights.structuredReviewerConsensus,
421
+ };
422
+ const total = usableWeights.objectiveAnchor + usableWeights.pairwiseJudge + usableWeights.structuredReviewerConsensus;
423
+ const weights = total <= 0
424
+ ? { objectiveAnchor: 1, pairwiseJudge: 0, structuredReviewerConsensus: 0 }
425
+ : {
426
+ objectiveAnchor: usableWeights.objectiveAnchor / total,
427
+ pairwiseJudge: usableWeights.pairwiseJudge / total,
428
+ structuredReviewerConsensus: usableWeights.structuredReviewerConsensus / total,
429
+ };
430
+ const compositeScore = roundScore(objectiveAnchorScore * weights.objectiveAnchor +
431
+ (pairwiseJudgeScore ?? 0) * weights.pairwiseJudge +
432
+ (structuredReviewerConsensusScore ?? 0) * weights.structuredReviewerConsensus);
433
+ return {
434
+ compositeScore,
435
+ objectiveAnchorScore,
436
+ pairwiseJudgeScore,
437
+ structuredReviewerConsensusScore,
438
+ weights,
439
+ audit: {
440
+ contributingRepos: ingestion.accepted.map((signal) => ({
441
+ repoFullName: signal.repoFullName,
442
+ replayRunId: signal.replayRunId,
443
+ reviewRunId: signal.reviewRunId,
444
+ observedAt: signal.observedAt,
445
+ score: signal.score,
446
+ dimensions: signal.dimensions,
447
+ })),
448
+ rejected: ingestion.rejected,
449
+ },
450
+ };
451
+ }
452
+ /**
453
+ * Render a deterministic, public-safe Markdown report for a structured reviewer-consensus calibration result. The
454
+ * report is local-run evidence: it includes aggregate scores, normalized weights, opted-in contributors, and rejected
455
+ * rows, but never accepts or emits raw review text or private scoring fields.
456
+ */
457
+ export function renderReviewerConsensusCalibrationAuditMarkdown(result) {
458
+ const lines = [
459
+ "# Structured Reviewer-Consensus Calibration",
460
+ "",
461
+ `Composite score: ${result.compositeScore.toFixed(6)}`,
462
+ "",
463
+ "## Component Scores",
464
+ "",
465
+ `- objectiveAnchor: ${result.objectiveAnchorScore.toFixed(6)}`,
466
+ `- pairwiseJudge: ${result.pairwiseJudgeScore === null ? "n/a" : result.pairwiseJudgeScore.toFixed(6)}`,
467
+ `- structuredReviewerConsensus: ${result.structuredReviewerConsensusScore === null ? "n/a" : result.structuredReviewerConsensusScore.toFixed(6)}`,
468
+ "",
469
+ "## Effective Weights",
470
+ "",
471
+ `- objectiveAnchor: ${result.weights.objectiveAnchor.toFixed(6)}`,
472
+ `- pairwiseJudge: ${result.weights.pairwiseJudge.toFixed(6)}`,
473
+ `- structuredReviewerConsensus: ${result.weights.structuredReviewerConsensus.toFixed(6)}`,
474
+ "",
475
+ "## Contributing Repos",
476
+ "",
477
+ result.audit.contributingRepos.length === 0
478
+ ? "_No opted-in structured reviewer-consensus signals contributed._"
479
+ : result.audit.contributingRepos.map(renderContributingRepo).join("\n\n"),
480
+ "",
481
+ "## Rejected Rows",
482
+ "",
483
+ ];
484
+ if (result.audit.rejected.length === 0) {
485
+ lines.push("- none");
486
+ }
487
+ else {
488
+ lines.push("| Repo | Replay run | Review run | Reason |", "| --- | --- | --- | --- |", ...result.audit.rejected.map(renderRejectedRow));
489
+ }
490
+ const contributingRepos = result.audit.contributingRepos.map((repo) => repo.repoFullName);
491
+ lines.push("", "## Contributing Repo Summary", "", markdownList(contributingRepos));
492
+ return `${lines.join("\n")}\n`;
493
+ }