@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,92 @@
1
+ export type TrackRecordPullRequestState = "merged" | "closed" | "open";
2
+ export type TrackRecordPullRequestOutcome = {
3
+ id?: string | number | undefined;
4
+ repoFullName: string;
5
+ authorLogin: string;
6
+ state: TrackRecordPullRequestState | string;
7
+ createdAt?: string | Date | null | undefined;
8
+ closedAt?: string | Date | null | undefined;
9
+ mergedAt?: string | Date | null | undefined;
10
+ url?: string | null | undefined;
11
+ };
12
+ export type TrackRecordIncidentKind = "ban" | "moderation" | "code_of_conduct" | "abuse" | "spam" | "unknown";
13
+ export type TrackRecordIncidentRecord = {
14
+ login: string;
15
+ kind: TrackRecordIncidentKind | string;
16
+ active?: boolean | null | undefined;
17
+ recordedAt?: string | Date | null | undefined;
18
+ publicEvidenceUrl?: string | null | undefined;
19
+ };
20
+ export type TrackRecordSummaryManifest = {
21
+ miner?: {
22
+ trackRecordSummary?: {
23
+ enabled?: unknown;
24
+ } | null;
25
+ } | null;
26
+ trackRecordSummary?: {
27
+ enabled?: unknown;
28
+ } | null;
29
+ };
30
+ export type TrackRecordSummaryConfig = {
31
+ includeTrackRecordSummary: boolean;
32
+ warnings: string[];
33
+ };
34
+ export type TrackRecordSummaryOutcomeCounts = {
35
+ merged: number;
36
+ closedWithoutMerge: number;
37
+ resolved: number;
38
+ openIgnored: number;
39
+ ignored: number;
40
+ };
41
+ export type TrackRecordTenure = {
42
+ firstObservedAt: string | null;
43
+ days: number | null;
44
+ label: string;
45
+ };
46
+ export type TrackRecordMergeRate = {
47
+ numerator: number;
48
+ denominator: number;
49
+ ratio: number | null;
50
+ percent: number | null;
51
+ label: string;
52
+ };
53
+ export type TrackRecordIncidentStatus = {
54
+ hasPublicIncident: boolean;
55
+ checkedPublicRecords: number;
56
+ activePublicRecords: number;
57
+ label: string;
58
+ evidenceUrls: string[];
59
+ };
60
+ export type TrackRecordSummaryAudit = {
61
+ normalizedLogin: string;
62
+ consideredOutcomeIds: string[];
63
+ ignoredOutcomeIds: string[];
64
+ firstObservedCandidates: string[];
65
+ };
66
+ export type TrackRecordSummary = {
67
+ enabled: boolean;
68
+ login: string;
69
+ mergeRate: TrackRecordMergeRate;
70
+ tenure: TrackRecordTenure;
71
+ incidents: TrackRecordIncidentStatus;
72
+ outcomes: TrackRecordSummaryOutcomeCounts;
73
+ audit: TrackRecordSummaryAudit;
74
+ };
75
+ /**
76
+ * Resolve the explicit miner-side opt-in. Missing and malformed values fail closed so operators must choose to include
77
+ * a public first-contact summary.
78
+ */
79
+ export declare function resolveTrackRecordSummaryConfig(manifest: TrackRecordSummaryManifest | Record<string, unknown> | null | undefined): TrackRecordSummaryConfig;
80
+ export declare function computeTrackRecordSummary(input: {
81
+ login: string;
82
+ outcomes: readonly TrackRecordPullRequestOutcome[];
83
+ incidents?: readonly TrackRecordIncidentRecord[] | undefined;
84
+ now?: string | Date | null | undefined;
85
+ config?: TrackRecordSummaryConfig | TrackRecordSummaryManifest | Record<string, unknown> | null | undefined;
86
+ }): TrackRecordSummary;
87
+ export declare function shouldIncludeTrackRecordSummary(config: TrackRecordSummaryConfig | TrackRecordSummaryManifest | Record<string, unknown> | null | undefined): boolean;
88
+ /**
89
+ * Render a deterministic Markdown block suitable for a PR body or first comment. Disabled summaries render to an empty
90
+ * string so caller code can concatenate safely without adding extra blank lines.
91
+ */
92
+ export declare function renderTrackRecordSummaryMarkdown(summary: TrackRecordSummary): string;
@@ -0,0 +1,301 @@
1
+ // Portable first-contact track-record summary (#3008).
2
+ //
3
+ // The miner runtime can render this block locally from public PR outcomes and public moderation records. The type
4
+ // surface intentionally has no score/ranking fields, and the formatter renders from computed fields only so arbitrary
5
+ // caller metadata cannot cross the public boundary.
6
+ const DEFAULT_TRACK_RECORD_CONFIG = {
7
+ includeTrackRecordSummary: false,
8
+ warnings: [],
9
+ };
10
+ const BOOLEAN_TRUE = new Set(["1", "true", "yes", "y", "on", "enabled", "include"]);
11
+ const BOOLEAN_FALSE = new Set(["0", "false", "no", "n", "off", "disabled", "exclude"]);
12
+ const RESOLVED_MERGED_STATES = new Set(["merged", "merge", "accepted"]);
13
+ const RESOLVED_CLOSED_STATES = new Set(["closed", "declined", "rejected", "closed_unmerged", "not_merged"]);
14
+ const OPEN_STATES = new Set(["open", "draft", "pending", "ready_for_review"]);
15
+ const INCIDENT_KINDS = new Set(["ban", "moderation", "code_of_conduct", "abuse", "spam"]);
16
+ const PUBLIC_FIELD_BLOCKLIST = [
17
+ /\btrust\s*score\b/iu,
18
+ /\btrustscore\b/iu,
19
+ /\bscoreability\b/iu,
20
+ /\breward\b/iu,
21
+ /\bpayout\b/iu,
22
+ /\branking\b/iu,
23
+ /\bprivate\s*scor/iu,
24
+ /\bwallet\b/iu,
25
+ /\bhotkey\b/iu,
26
+ /\bcoldkey\b/iu,
27
+ ];
28
+ function isRecord(value) {
29
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
30
+ }
31
+ function normalizeBoolean(value) {
32
+ if (typeof value === "boolean")
33
+ return value;
34
+ if (typeof value === "number") {
35
+ if (value === 1)
36
+ return true;
37
+ if (value === 0)
38
+ return false;
39
+ return undefined;
40
+ }
41
+ if (typeof value !== "string")
42
+ return undefined;
43
+ const normalized = value.trim().toLowerCase();
44
+ if (BOOLEAN_TRUE.has(normalized))
45
+ return true;
46
+ if (BOOLEAN_FALSE.has(normalized))
47
+ return false;
48
+ return undefined;
49
+ }
50
+ function normalizeLogin(value) {
51
+ return value.trim().toLowerCase();
52
+ }
53
+ function normalizeId(value, fallbackIndex) {
54
+ if (typeof value === "number" && Number.isFinite(value))
55
+ return String(value);
56
+ if (typeof value === "string") {
57
+ const trimmed = value.trim();
58
+ if (trimmed)
59
+ return collapseInline(trimmed);
60
+ }
61
+ return `row-${fallbackIndex + 1}`;
62
+ }
63
+ function normalizeState(value) {
64
+ const normalized = value.trim().toLowerCase().replace(/[\s-]+/gu, "_");
65
+ if (RESOLVED_MERGED_STATES.has(normalized))
66
+ return "merged";
67
+ if (RESOLVED_CLOSED_STATES.has(normalized))
68
+ return "closed";
69
+ if (OPEN_STATES.has(normalized))
70
+ return "open";
71
+ return "ignored";
72
+ }
73
+ function normalizeIncidentKind(value) {
74
+ const normalized = value.trim().toLowerCase().replace(/[\s-]+/gu, "_");
75
+ if (INCIDENT_KINDS.has(normalized))
76
+ return normalized;
77
+ return "unknown";
78
+ }
79
+ function parseInstant(value) {
80
+ if (value === null || value === undefined)
81
+ return null;
82
+ const ms = value instanceof Date ? value.getTime() : Date.parse(value);
83
+ if (!Number.isFinite(ms))
84
+ return null;
85
+ return new Date(ms).toISOString();
86
+ }
87
+ function parseNow(value) {
88
+ const parsed = parseInstant(value ?? undefined);
89
+ return parsed ? new Date(parsed) : new Date();
90
+ }
91
+ function clampWholeDays(startIso, now) {
92
+ const deltaMs = now.getTime() - new Date(startIso).getTime();
93
+ if (!Number.isFinite(deltaMs) || deltaMs <= 0)
94
+ return 0;
95
+ return Math.floor(deltaMs / 86_400_000);
96
+ }
97
+ function collapseInline(value) {
98
+ return value.replace(/[\r\n\t]+/gu, " ").replace(/\s{2,}/gu, " ").trim();
99
+ }
100
+ function markdownSafe(value) {
101
+ return collapseInline(value).replace(/[\\`*_[\]<>|]/gu, "\\$&");
102
+ }
103
+ function normalizeEvidenceUrl(value) {
104
+ if (!value)
105
+ return null;
106
+ const collapsed = collapseInline(value);
107
+ if (!collapsed)
108
+ return null;
109
+ if (!/^https?:\/\/[^\s<>"`|\\]+$/iu.test(collapsed))
110
+ return null;
111
+ return collapsed;
112
+ }
113
+ function firstPresentInstant(values) {
114
+ const parsed = values.flatMap((value) => {
115
+ const instant = parseInstant(value);
116
+ return instant ? [instant] : [];
117
+ });
118
+ if (parsed.length === 0)
119
+ return null;
120
+ parsed.sort();
121
+ return parsed[0];
122
+ }
123
+ function formatPercent(ratio) {
124
+ if (ratio === null)
125
+ return { percent: null, label: "not enough resolved public PR history" };
126
+ const percent = Math.round(ratio * 100);
127
+ return { percent, label: `${percent}%` };
128
+ }
129
+ function formatTenure(days) {
130
+ if (days === null)
131
+ return "not enough public history";
132
+ if (days === 0)
133
+ return "less than 1 day";
134
+ if (days === 1)
135
+ return "1 day";
136
+ if (days < 30)
137
+ return `${days} days`;
138
+ const months = Math.floor(days / 30);
139
+ if (months < 12)
140
+ return months === 1 ? "1 month" : `${months} months`;
141
+ const years = Math.floor(days / 365);
142
+ const remainderMonths = Math.floor((days % 365) / 30);
143
+ if (remainderMonths === 0)
144
+ return years === 1 ? "1 year" : `${years} years`;
145
+ return `${years}y ${remainderMonths}m`;
146
+ }
147
+ function assertPublicSummaryText(text) {
148
+ for (const pattern of PUBLIC_FIELD_BLOCKLIST) {
149
+ if (pattern.test(text)) {
150
+ throw new Error("Track-record summary attempted to render a blocked public field.");
151
+ }
152
+ }
153
+ }
154
+ function summarizeIncidents(login, incidents) {
155
+ const normalizedLogin = normalizeLogin(login);
156
+ const matching = incidents.filter((incident) => normalizeLogin(incident.login) === normalizedLogin);
157
+ const active = matching.filter((incident) => incident.active !== false);
158
+ const activeKnown = active.filter((incident) => normalizeIncidentKind(incident.kind) !== "unknown");
159
+ const evidenceUrls = Array.from(new Set(activeKnown.flatMap((incident) => {
160
+ const url = normalizeEvidenceUrl(incident.publicEvidenceUrl);
161
+ return url ? [url] : [];
162
+ }))).sort();
163
+ const hasPublicIncident = activeKnown.length > 0;
164
+ return {
165
+ hasPublicIncident,
166
+ checkedPublicRecords: matching.length,
167
+ activePublicRecords: activeKnown.length,
168
+ label: hasPublicIncident ? "public conduct incident present" : "no public conduct incidents found",
169
+ evidenceUrls,
170
+ };
171
+ }
172
+ /**
173
+ * Resolve the explicit miner-side opt-in. Missing and malformed values fail closed so operators must choose to include
174
+ * a public first-contact summary.
175
+ */
176
+ export function resolveTrackRecordSummaryConfig(manifest) {
177
+ const root = isRecord(manifest) ? manifest : {};
178
+ const miner = isRecord(root.miner) ? root.miner : {};
179
+ const minerConfig = isRecord(miner.trackRecordSummary) ? miner.trackRecordSummary : {};
180
+ const topConfig = isRecord(root.trackRecordSummary) ? root.trackRecordSummary : {};
181
+ const raw = minerConfig.enabled ?? topConfig.enabled ?? undefined;
182
+ const normalized = normalizeBoolean(raw);
183
+ const warnings = [];
184
+ if (raw !== undefined && normalized === undefined) {
185
+ warnings.push("miner.trackRecordSummary.enabled must be a boolean-like value; defaulting to false.");
186
+ }
187
+ return {
188
+ includeTrackRecordSummary: normalized ?? DEFAULT_TRACK_RECORD_CONFIG.includeTrackRecordSummary,
189
+ warnings,
190
+ };
191
+ }
192
+ export function computeTrackRecordSummary(input) {
193
+ const config = input.config && "includeTrackRecordSummary" in input.config
194
+ ? input.config
195
+ : resolveTrackRecordSummaryConfig(input.config);
196
+ const normalizedLogin = normalizeLogin(input.login);
197
+ const now = parseNow(input.now);
198
+ const outcomes = {
199
+ merged: 0,
200
+ closedWithoutMerge: 0,
201
+ resolved: 0,
202
+ openIgnored: 0,
203
+ ignored: 0,
204
+ };
205
+ const consideredOutcomeIds = [];
206
+ const ignoredOutcomeIds = [];
207
+ const firstObservedCandidates = [];
208
+ input.outcomes.forEach((outcome, index) => {
209
+ const id = normalizeId(outcome.id, index);
210
+ if (normalizeLogin(outcome.authorLogin) !== normalizedLogin) {
211
+ outcomes.ignored += 1;
212
+ ignoredOutcomeIds.push(id);
213
+ return;
214
+ }
215
+ const mergedAt = parseInstant(outcome.mergedAt);
216
+ const state = mergedAt ? "merged" : normalizeState(outcome.state);
217
+ const firstObserved = firstPresentInstant([outcome.createdAt, outcome.closedAt, outcome.mergedAt]);
218
+ if (firstObserved)
219
+ firstObservedCandidates.push(firstObserved);
220
+ if (state === "merged") {
221
+ outcomes.merged += 1;
222
+ outcomes.resolved += 1;
223
+ consideredOutcomeIds.push(id);
224
+ return;
225
+ }
226
+ if (state === "closed") {
227
+ outcomes.closedWithoutMerge += 1;
228
+ outcomes.resolved += 1;
229
+ consideredOutcomeIds.push(id);
230
+ return;
231
+ }
232
+ if (state === "open") {
233
+ outcomes.openIgnored += 1;
234
+ ignoredOutcomeIds.push(id);
235
+ return;
236
+ }
237
+ outcomes.ignored += 1;
238
+ ignoredOutcomeIds.push(id);
239
+ });
240
+ const ratio = outcomes.resolved === 0 ? null : outcomes.merged / outcomes.resolved;
241
+ const formattedRate = formatPercent(ratio);
242
+ const firstObservedAt = firstObservedCandidates.length === 0 ? null : [...firstObservedCandidates].sort()[0];
243
+ const tenureDays = firstObservedAt ? clampWholeDays(firstObservedAt, now) : null;
244
+ const incidents = summarizeIncidents(input.login, input.incidents ?? []);
245
+ return {
246
+ enabled: config.includeTrackRecordSummary,
247
+ login: normalizedLogin,
248
+ mergeRate: {
249
+ numerator: outcomes.merged,
250
+ denominator: outcomes.resolved,
251
+ ratio,
252
+ percent: formattedRate.percent,
253
+ label: formattedRate.label,
254
+ },
255
+ tenure: {
256
+ firstObservedAt,
257
+ days: tenureDays,
258
+ label: formatTenure(tenureDays),
259
+ },
260
+ incidents,
261
+ outcomes,
262
+ audit: {
263
+ normalizedLogin,
264
+ consideredOutcomeIds: consideredOutcomeIds.sort(),
265
+ ignoredOutcomeIds: ignoredOutcomeIds.sort(),
266
+ firstObservedCandidates: firstObservedCandidates.sort(),
267
+ },
268
+ };
269
+ }
270
+ export function shouldIncludeTrackRecordSummary(config) {
271
+ if (config && "includeTrackRecordSummary" in config) {
272
+ return config.includeTrackRecordSummary === true;
273
+ }
274
+ return resolveTrackRecordSummaryConfig(config).includeTrackRecordSummary;
275
+ }
276
+ /**
277
+ * Render a deterministic Markdown block suitable for a PR body or first comment. Disabled summaries render to an empty
278
+ * string so caller code can concatenate safely without adding extra blank lines.
279
+ */
280
+ export function renderTrackRecordSummaryMarkdown(summary) {
281
+ if (!summary.enabled)
282
+ return "";
283
+ const lines = [
284
+ "### Public contributor record",
285
+ "",
286
+ `- GitHub login: ${markdownSafe(summary.login)}`,
287
+ `- Resolved public PRs: ${summary.outcomes.resolved} (${summary.outcomes.merged} merged, ${summary.outcomes.closedWithoutMerge} closed without merge)`,
288
+ `- Public merge rate: ${summary.mergeRate.label}`,
289
+ `- Public tenure: ${summary.tenure.label}`,
290
+ `- Public conduct record: ${summary.incidents.label}`,
291
+ ];
292
+ if (summary.outcomes.openIgnored > 0) {
293
+ lines.push(`- Open PRs ignored for rate: ${summary.outcomes.openIgnored}`);
294
+ }
295
+ if (summary.incidents.hasPublicIncident && summary.incidents.evidenceUrls.length > 0) {
296
+ lines.push(`- Public evidence: ${summary.incidents.evidenceUrls.map((url) => markdownSafe(url)).join(", ")}`);
297
+ }
298
+ const rendered = `${lines.join("\n")}\n`;
299
+ assertPublicSummaryText(rendered);
300
+ return rendered;
301
+ }