@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,87 @@
1
+ // Anonymized discovery-plane telemetry event schema (pure) — #4301, Wave 2 tracker #2353 Phase 6.
2
+ //
3
+ // Typed event shapes for the OPTIONAL hosted discovery-index service (#4250) — which candidates a miner
4
+ // fetched/ranked and whether a soft-claim succeeded or collided — so that shared service can be operated and
5
+ // debugged WITHOUT ever holding source, diffs, or credentials. This mirrors governor-ledger.ts's pure
6
+ // validate/normalize shape (fixed fail-closed vocabulary, JSON-round-trip-verified payload) and copies the
7
+ // anonymization POSTURE of src/selfhost/orb-collector.ts (the one shipped precedent for "anonymized telemetry
8
+ // leaving an instance"): repo/issue identifiers are HMAC hashes keyed by a per-instance secret the collector
9
+ // never holds, and free-text-adjacent fields are collapsed to a fixed low-cardinality bucket rather than raw text.
10
+ //
11
+ // NEVER INCLUDED in a telemetry event (the discovery-plane analogue of orb-collector.ts:1-18's inventory): no
12
+ // source contents, no diffs, no GitHub tokens or credentials, no full issue bodies or titles, no commit SHAs, and
13
+ // no RAW repo/issue identifiers — only the exporter's per-instance HMAC hashes reach this shape. This module is
14
+ // SCHEMA/TYPES ONLY: it does not export events, hash anything itself (the exporter does that at #4250's boundary),
15
+ // or wire into an endpoint. It only defines and validates the on-the-wire contract.
16
+ /** Immutable discovery-plane telemetry event vocabulary — an unknown value fails closed before it is recorded. */
17
+ export const MINER_TELEMETRY_EVENT_TYPES = Object.freeze([
18
+ "query_issued",
19
+ "candidates_returned",
20
+ "soft_claim_attempted",
21
+ "soft_claim_succeeded",
22
+ "soft_claim_collided",
23
+ ]);
24
+ /** Fixed low-cardinality outcome buckets — the discovery-plane analogue of orb-collector's `bucketReasonCode`, so a
25
+ * free-text reason can never leak through the telemetry surface. */
26
+ export const MINER_TELEMETRY_OUTCOME_BUCKETS = Object.freeze([
27
+ "ok",
28
+ "empty",
29
+ "collision",
30
+ "rate_limited",
31
+ "error",
32
+ "other",
33
+ ]);
34
+ const telemetryEventTypeSet = new Set(MINER_TELEMETRY_EVENT_TYPES);
35
+ const telemetryOutcomeSet = new Set(MINER_TELEMETRY_OUTCOME_BUCKETS);
36
+ const metricNamePattern = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
37
+ /** Coerce an optional anonymized identifier. Present values must be a non-empty opaque hash — an anti-leak guard
38
+ * rejects anything that looks like a RAW identifier (contains `/`, i.e. `owner/repo`, or any whitespace), so a
39
+ * caller cannot accidentally ship an un-hashed `repoFullName` through the anonymized surface. */
40
+ function normalizeOptionalHash(value, code) {
41
+ if (value === undefined || value === null)
42
+ return null;
43
+ if (typeof value !== "string")
44
+ throw new Error(code);
45
+ const trimmed = value.trim();
46
+ if (!trimmed || trimmed.includes("/") || /\s/.test(trimmed))
47
+ throw new Error(code);
48
+ return trimmed;
49
+ }
50
+ /** Serialize the count-only metrics map, rejecting any free-text metric name or non-finite-number value (so no raw identifiers,
51
+ * free text, or NaN/Infinity can slip into the telemetry payload). Absent metrics normalize to an empty object. */
52
+ function normalizeMetrics(metrics) {
53
+ if (metrics === undefined)
54
+ return "{}";
55
+ if (metrics === null || typeof metrics !== "object" || Array.isArray(metrics))
56
+ throw new Error("invalid_metrics");
57
+ for (const [name, value] of Object.entries(metrics)) {
58
+ if (!metricNamePattern.test(name) || typeof value !== "number" || !Number.isFinite(value)) {
59
+ throw new Error("invalid_metrics");
60
+ }
61
+ }
62
+ return JSON.stringify(metrics);
63
+ }
64
+ /**
65
+ * Validate and normalize a discovery-plane telemetry event before it is recorded/exported. Fail-closed, mirroring
66
+ * {@link normalizeGovernorLedgerEvent}: an unknown `eventType`/`outcome`, a non-hash identifier, or a non-numeric
67
+ * metric throws rather than silently shipping malformed or de-anonymizing data. Defines the contract only — it does
68
+ * NOT perform the HMAC hashing (that is the exporter's job at #4250's boundary) or send anything.
69
+ */
70
+ export function normalizeMinerTelemetryEvent(input) {
71
+ if (!input || typeof input !== "object")
72
+ throw new Error("invalid_event");
73
+ const event = input;
74
+ const eventType = typeof event.eventType === "string" ? event.eventType.trim() : "";
75
+ if (!telemetryEventTypeSet.has(eventType))
76
+ throw new Error("invalid_event_type");
77
+ const outcome = typeof event.outcome === "string" ? event.outcome.trim() : "";
78
+ if (!telemetryOutcomeSet.has(outcome))
79
+ throw new Error("invalid_outcome");
80
+ return {
81
+ eventType: eventType,
82
+ repoHash: normalizeOptionalHash(event.repoHash, "invalid_repo_hash"),
83
+ issueHash: normalizeOptionalHash(event.issueHash, "invalid_issue_hash"),
84
+ outcome: outcome,
85
+ metricsJson: normalizeMetrics(event.metrics),
86
+ };
87
+ }
@@ -0,0 +1,125 @@
1
+ export type ObjectiveAnchorChangeKind = "feature" | "fix" | "test" | "docs" | "refactor" | "config" | "ci" | "security" | "dependency" | "unknown";
2
+ export type ObjectiveAnchorInput = {
3
+ /** Paths touched or explicitly targeted by a replayed plan/PR or by revealed history. */
4
+ paths?: readonly string[] | undefined;
5
+ /** Labels from an issue, PR, or local candidate record. Used only for change-kind extraction. */
6
+ labels?: readonly string[] | undefined;
7
+ /** Titles or short subjects. Used only for change-kind extraction. */
8
+ titles?: readonly string[] | undefined;
9
+ /** Longer plan/review/commit notes. Used only for change-kind extraction. */
10
+ notes?: readonly string[] | undefined;
11
+ /** Optional already-classified kinds from an upstream caller. */
12
+ changeKinds?: readonly ObjectiveAnchorChangeKind[] | undefined;
13
+ };
14
+ export type ObjectiveAnchorHistoryItem = ObjectiveAnchorInput & {
15
+ /** Stable caller-side identifier, e.g. `plan:abc`, `pr:123`, or `commit:deadbeef`. */
16
+ id?: string | undefined;
17
+ /** Human-readable source bucket for audit reports. */
18
+ source?: "plan" | "pull_request" | "commit" | "issue" | "manual" | "unknown" | undefined;
19
+ };
20
+ export type ObjectiveAnchorFeatures = {
21
+ /** Stable, normalized file paths with duplicates removed. */
22
+ paths: string[];
23
+ /** Coarse module buckets derived from paths, e.g. `src/review`, `packages/gittensory-engine`, `docs`. */
24
+ modules: string[];
25
+ /** Inferred or caller-supplied change kinds with duplicates removed. */
26
+ changeKinds: ObjectiveAnchorChangeKind[];
27
+ };
28
+ export type ObjectiveAnchorHistoryItemAudit = {
29
+ id: string;
30
+ source: NonNullable<ObjectiveAnchorHistoryItem["source"]>;
31
+ features: ObjectiveAnchorFeatures;
32
+ };
33
+ export type ObjectiveAnchorHistoryExtraction = {
34
+ features: ObjectiveAnchorFeatures;
35
+ items: ObjectiveAnchorHistoryItemAudit[];
36
+ };
37
+ export type ObjectiveAnchorWeights = {
38
+ /** Weight for exact/tight path overlap. Default: 0.45. */
39
+ paths?: number | undefined;
40
+ /** Weight for coarser module overlap. Default: 0.4. */
41
+ modules?: number | undefined;
42
+ /** Weight for change-kind overlap. Default: 0.15. */
43
+ changeKinds?: number | undefined;
44
+ };
45
+ type NormalizedObjectiveAnchorWeights = {
46
+ paths: number;
47
+ modules: number;
48
+ changeKinds: number;
49
+ };
50
+ export type ObjectiveAnchorDimensionScores = {
51
+ paths: number;
52
+ modules: number;
53
+ changeKinds: number;
54
+ };
55
+ export type ObjectiveAnchorAudit = {
56
+ replayed: ObjectiveAnchorFeatures;
57
+ revealed: ObjectiveAnchorFeatures;
58
+ weights: NormalizedObjectiveAnchorWeights;
59
+ dimensions: ObjectiveAnchorDimensionScores;
60
+ intersections: {
61
+ paths: string[];
62
+ modules: string[];
63
+ changeKinds: ObjectiveAnchorChangeKind[];
64
+ };
65
+ misses: {
66
+ replayedOnlyPaths: string[];
67
+ revealedOnlyPaths: string[];
68
+ replayedOnlyModules: string[];
69
+ revealedOnlyModules: string[];
70
+ replayedOnlyChangeKinds: ObjectiveAnchorChangeKind[];
71
+ revealedOnlyChangeKinds: ObjectiveAnchorChangeKind[];
72
+ };
73
+ };
74
+ export type ObjectiveAnchorScore = {
75
+ /** Composite score in [0, 1]. */
76
+ score: number;
77
+ dimensions: ObjectiveAnchorDimensionScores;
78
+ audit: ObjectiveAnchorAudit;
79
+ };
80
+ export type ObjectiveAnchorHistoryScore = ObjectiveAnchorScore & {
81
+ history: {
82
+ replayed: ObjectiveAnchorHistoryExtraction;
83
+ revealed: ObjectiveAnchorHistoryExtraction;
84
+ };
85
+ };
86
+ /**
87
+ * Extract normalized structural features from replayed or revealed history input. The extractor is intentionally
88
+ * conservative: paths determine modules, and labels/titles/notes only classify change kind. It never guesses a
89
+ * module from free text, which keeps the path/module score auditable and reproducible.
90
+ */
91
+ export declare function extractObjectiveAnchorFeatures(input: ObjectiveAnchorInput): ObjectiveAnchorFeatures;
92
+ /**
93
+ * Extract and aggregate structural features from a replay/revealed history list. Each item keeps its own normalized
94
+ * feature set in `items` for auditability, while `features` is the deduplicated union used for scoring. Empty history
95
+ * is valid and produces empty path/module sets with an `unknown` change kind, matching single-input extraction.
96
+ */
97
+ export declare function extractObjectiveAnchorHistory(items: readonly ObjectiveAnchorHistoryItem[]): ObjectiveAnchorHistoryExtraction;
98
+ /**
99
+ * Score replayed structural features against revealed history. Path and module dimensions use Dice overlap so a
100
+ * partial module match gets visible credit without pretending it is exact. Change-kind overlap is the same metric
101
+ * over inferred/caller-supplied kinds. The revealed side may have zero overlapping modules; that is a valid low
102
+ * score, never an error, and the misses section explains what diverged.
103
+ */
104
+ export declare function scoreObjectiveAnchor(input: {
105
+ replayed: ObjectiveAnchorInput | ObjectiveAnchorFeatures;
106
+ revealed: ObjectiveAnchorInput | ObjectiveAnchorFeatures;
107
+ weights?: ObjectiveAnchorWeights | undefined;
108
+ }): ObjectiveAnchorScore;
109
+ /**
110
+ * Score arrays of replayed and revealed history records, preserving the per-record extraction evidence alongside the
111
+ * normal score/audit payload. This is the ergonomic entrypoint for replay harnesses that compare a generated plan/PR
112
+ * bundle with multiple revealed commits or merged PRs after the snapshot timestamp.
113
+ */
114
+ export declare function scoreObjectiveAnchorHistory(input: {
115
+ replayed: readonly ObjectiveAnchorHistoryItem[];
116
+ revealed: readonly ObjectiveAnchorHistoryItem[];
117
+ weights?: ObjectiveAnchorWeights | undefined;
118
+ }): ObjectiveAnchorHistoryScore;
119
+ /**
120
+ * Render the score audit as deterministic Markdown for local replay artifacts. The renderer escapes Markdown control
121
+ * characters and collapses newlines in untrusted ids/paths so a caller can persist the output next to a replay run
122
+ * without letting a path or caller-supplied id reshape the report.
123
+ */
124
+ export declare function renderObjectiveAnchorAuditMarkdown(result: ObjectiveAnchorScore | ObjectiveAnchorHistoryScore): string;
125
+ export {};
@@ -0,0 +1,401 @@
1
+ // Deterministic objective-anchor scoring for historical replay calibration (#3012).
2
+ //
3
+ // The replay harness needs a stable, auditable score before any LLM judge is involved: compare what the miner
4
+ // planned or changed against what the revealed post-T history actually changed. This module is deliberately pure:
5
+ // no IO, no Date, no random, no model calls. Given the same replayed and revealed records, it returns the same
6
+ // normalized features, dimension scores, composite score, and audit payload byte-for-byte.
7
+ const DEFAULT_WEIGHTS = {
8
+ paths: 0.45,
9
+ modules: 0.4,
10
+ changeKinds: 0.15,
11
+ };
12
+ const CHANGE_KIND_ORDER = [
13
+ "feature",
14
+ "fix",
15
+ "test",
16
+ "docs",
17
+ "refactor",
18
+ "config",
19
+ "ci",
20
+ "security",
21
+ "dependency",
22
+ "unknown",
23
+ ];
24
+ const KIND_SYNONYMS = [
25
+ ["feature", /\b(feat|feature|enhancement|add|adds|introduce|support|capability)\b/iu],
26
+ ["fix", /\b(fix|bug|bugfix|regression|repair|broken|incorrect|failure|fails?)\b/iu],
27
+ ["test", /\b(test|tests|coverage|regression-test|vitest|unit|integration)\b/iu],
28
+ ["docs", /\b(doc|docs|readme|documentation|guide|quickstart|manual)\b/iu],
29
+ ["refactor", /\b(refactor|cleanup|simplify|extract|rename|restructure)\b/iu],
30
+ ["config", /\b(config|configuration|settings|env|schema|yaml|jsonc|wrangler|toml)\b/iu],
31
+ ["ci", /\b(ci|workflow|github-actions|actionlint|codecov|pipeline|build)\b/iu],
32
+ ["security", /\b(security|secret|token|credential|auth|permission|vulnerability|cve)\b/iu],
33
+ ["dependency", /\b(dependency|dependencies|deps|package-lock|npm|pnpm|yarn|version|upgrade|pin)\b/iu],
34
+ ];
35
+ const DOC_EXTENSIONS = new Set([".md", ".mdx", ".rst", ".adoc", ".txt"]);
36
+ const TEST_SEGMENTS = new Set(["test", "tests", "__tests__", "spec", "specs"]);
37
+ const CI_SEGMENTS = new Set([".github", "workflows"]);
38
+ const CONFIG_FILENAMES = new Set([
39
+ ".env",
40
+ ".env.example",
41
+ ".env.selfhost.example",
42
+ ".loopover.yml", // new-brand manifest name (legacy .gittensory.yml kept below, dual-read indefinitely, #4773)
43
+ ".gittensory.yml",
44
+ "package.json",
45
+ "package-lock.json",
46
+ "tsconfig.json",
47
+ "tsconfig.test.json",
48
+ "wrangler.jsonc",
49
+ "vite.config.ts",
50
+ "vitest.config.ts",
51
+ ]);
52
+ function normalizePath(path) {
53
+ const normalized = path.trim().replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\.\//u, "");
54
+ if (!normalized || normalized === "." || normalized.includes("\0"))
55
+ return undefined;
56
+ return normalized.toLowerCase();
57
+ }
58
+ function extensionOf(path) {
59
+ const last = path.split("/").pop() ?? "";
60
+ const dot = last.lastIndexOf(".");
61
+ return dot <= 0 ? "" : last.slice(dot);
62
+ }
63
+ function uniqueSorted(values) {
64
+ return [...new Set(values)].sort((a, b) => a.localeCompare(b));
65
+ }
66
+ function uniqueKinds(kinds) {
67
+ const seen = new Set(kinds);
68
+ return CHANGE_KIND_ORDER.filter((kind) => seen.has(kind));
69
+ }
70
+ function combineFeatures(features) {
71
+ return {
72
+ paths: uniqueSorted(features.flatMap((feature) => feature.paths)),
73
+ modules: uniqueSorted(features.flatMap((feature) => feature.modules)),
74
+ changeKinds: uniqueKinds(features.flatMap((feature) => feature.changeKinds)),
75
+ };
76
+ }
77
+ function isKnownKind(kind) {
78
+ return CHANGE_KIND_ORDER.includes(kind);
79
+ }
80
+ function normalizeKind(value) {
81
+ const normalized = value.trim().toLowerCase().replace(/[_\s]+/gu, "-");
82
+ if (isKnownKind(normalized))
83
+ return normalized;
84
+ if (normalized === "feat" || normalized === "enhancement")
85
+ return "feature";
86
+ if (normalized === "bug" || normalized === "bugfix" || normalized === "regression")
87
+ return "fix";
88
+ if (normalized === "documentation" || normalized === "readme")
89
+ return "docs";
90
+ if (normalized === "build" || normalized === "workflow")
91
+ return "ci";
92
+ if (normalized === "deps" || normalized === "package")
93
+ return "dependency";
94
+ return undefined;
95
+ }
96
+ function pathModule(path) {
97
+ const segments = path.split("/").filter(Boolean);
98
+ if (segments.length === 0)
99
+ return "root";
100
+ const [first, second] = segments;
101
+ if (first === "packages" || first === "apps") {
102
+ return second ? `${first}/${second}` : first;
103
+ }
104
+ if (first === "src" || first === "test" || first === "tests") {
105
+ return second ? `${first}/${second}` : first;
106
+ }
107
+ if (first === ".github")
108
+ return segments[1] === "workflows" ? ".github/workflows" : ".github";
109
+ return first;
110
+ }
111
+ function kindsFromPath(path) {
112
+ const segments = path.split("/");
113
+ const filename = segments[segments.length - 1] ?? path;
114
+ const kinds = [];
115
+ if (segments.some((segment) => TEST_SEGMENTS.has(segment)) || /\.test\.|\.spec\./u.test(filename)) {
116
+ kinds.push("test");
117
+ }
118
+ if (DOC_EXTENSIONS.has(extensionOf(path)) || segments.includes("docs") || filename.toLowerCase() === "readme.md") {
119
+ kinds.push("docs");
120
+ }
121
+ if (segments.some((segment) => CI_SEGMENTS.has(segment)) || filename.endsWith(".yml") || filename.endsWith(".yaml")) {
122
+ kinds.push("ci");
123
+ }
124
+ if (CONFIG_FILENAMES.has(filename) || filename.endsWith(".jsonc") || filename.endsWith(".toml")) {
125
+ kinds.push("config");
126
+ }
127
+ if (/package(?:-lock)?\.json$/u.test(filename)) {
128
+ kinds.push("dependency");
129
+ }
130
+ return kinds;
131
+ }
132
+ function kindsFromText(values) {
133
+ if (!values)
134
+ return [];
135
+ const kinds = [];
136
+ for (const value of values) {
137
+ for (const explicit of value.split(/[,\s/()[\]{}:;]+/u)) {
138
+ const normalized = normalizeKind(explicit);
139
+ if (normalized)
140
+ kinds.push(normalized);
141
+ }
142
+ for (const [kind, pattern] of KIND_SYNONYMS) {
143
+ if (pattern.test(value))
144
+ kinds.push(kind);
145
+ }
146
+ }
147
+ return kinds;
148
+ }
149
+ function normalizeWeights(weights) {
150
+ const raw = {
151
+ paths: finiteNonNegative(weights?.paths, DEFAULT_WEIGHTS.paths),
152
+ modules: finiteNonNegative(weights?.modules, DEFAULT_WEIGHTS.modules),
153
+ changeKinds: finiteNonNegative(weights?.changeKinds, DEFAULT_WEIGHTS.changeKinds),
154
+ };
155
+ const total = raw.paths + raw.modules + raw.changeKinds;
156
+ if (total <= 0)
157
+ return DEFAULT_WEIGHTS;
158
+ return {
159
+ paths: raw.paths / total,
160
+ modules: raw.modules / total,
161
+ changeKinds: raw.changeKinds / total,
162
+ };
163
+ }
164
+ function finiteNonNegative(value, fallback) {
165
+ if (value === undefined)
166
+ return fallback;
167
+ if (!Number.isFinite(value) || value < 0)
168
+ return 0;
169
+ return value;
170
+ }
171
+ function diceOverlap(left, right) {
172
+ if (left.length === 0 && right.length === 0)
173
+ return 1;
174
+ if (left.length === 0 || right.length === 0)
175
+ return 0;
176
+ const rightSet = new Set(right);
177
+ const intersection = left.filter((value) => rightSet.has(value)).length;
178
+ return (2 * intersection) / (left.length + right.length);
179
+ }
180
+ function intersectStrings(left, right) {
181
+ const rightSet = new Set(right);
182
+ return left.filter((value) => rightSet.has(value));
183
+ }
184
+ function differenceStrings(left, right) {
185
+ const rightSet = new Set(right);
186
+ return left.filter((value) => !rightSet.has(value));
187
+ }
188
+ function intersectKinds(left, right) {
189
+ const rightSet = new Set(right);
190
+ return CHANGE_KIND_ORDER.filter((kind) => left.includes(kind) && rightSet.has(kind));
191
+ }
192
+ function differenceKinds(left, right) {
193
+ const rightSet = new Set(right);
194
+ return CHANGE_KIND_ORDER.filter((kind) => left.includes(kind) && !rightSet.has(kind));
195
+ }
196
+ function roundScore(value) {
197
+ return Math.round(Math.min(1, Math.max(0, value)) * 1_000_000) / 1_000_000;
198
+ }
199
+ function auditItemId(item, index) {
200
+ const trimmed = item.id?.trim();
201
+ return trimmed ? trimmed : `item:${index + 1}`;
202
+ }
203
+ function auditItemSource(item) {
204
+ return item.source ?? "unknown";
205
+ }
206
+ function markdownSafe(value) {
207
+ return value.replace(/[\r\n]+/gu, " ").replace(/[\\`*_[\]<>|]/gu, "\\$&");
208
+ }
209
+ function markdownList(values) {
210
+ if (values.length === 0)
211
+ return "- none";
212
+ return values.map((value) => `- ${markdownSafe(value)}`).join("\n");
213
+ }
214
+ function markdownKindList(values) {
215
+ return markdownList(values);
216
+ }
217
+ function markdownFeatureBlock(features) {
218
+ return [
219
+ "Paths:",
220
+ markdownList(features.paths),
221
+ "",
222
+ "Modules:",
223
+ markdownList(features.modules),
224
+ "",
225
+ "Change kinds:",
226
+ markdownKindList(features.changeKinds),
227
+ ].join("\n");
228
+ }
229
+ function markdownHistoryBlock(extraction) {
230
+ if (extraction.items.length === 0)
231
+ return "_No history items._";
232
+ return extraction.items
233
+ .map((item) => [
234
+ `### ${markdownSafe(item.id)} (${markdownSafe(item.source)})`,
235
+ "",
236
+ markdownFeatureBlock(item.features),
237
+ ].join("\n"))
238
+ .join("\n\n");
239
+ }
240
+ /**
241
+ * Extract normalized structural features from replayed or revealed history input. The extractor is intentionally
242
+ * conservative: paths determine modules, and labels/titles/notes only classify change kind. It never guesses a
243
+ * module from free text, which keeps the path/module score auditable and reproducible.
244
+ */
245
+ export function extractObjectiveAnchorFeatures(input) {
246
+ const paths = uniqueSorted((input.paths ?? []).map(normalizePath).filter((path) => Boolean(path)));
247
+ const modules = uniqueSorted(paths.map(pathModule));
248
+ const directKinds = (input.changeKinds ?? []).filter((kind) => isKnownKind(kind));
249
+ const textKinds = kindsFromText([...(input.labels ?? []), ...(input.titles ?? []), ...(input.notes ?? [])]);
250
+ const pathKinds = paths.flatMap(kindsFromPath);
251
+ const changeKinds = uniqueKinds([...directKinds, ...textKinds, ...pathKinds]);
252
+ return {
253
+ paths,
254
+ modules,
255
+ changeKinds: changeKinds.length > 0 ? changeKinds : ["unknown"],
256
+ };
257
+ }
258
+ /**
259
+ * Extract and aggregate structural features from a replay/revealed history list. Each item keeps its own normalized
260
+ * feature set in `items` for auditability, while `features` is the deduplicated union used for scoring. Empty history
261
+ * is valid and produces empty path/module sets with an `unknown` change kind, matching single-input extraction.
262
+ */
263
+ export function extractObjectiveAnchorHistory(items) {
264
+ const itemAudits = items.map((item, index) => ({
265
+ id: auditItemId(item, index),
266
+ source: auditItemSource(item),
267
+ features: extractObjectiveAnchorFeatures(item),
268
+ }));
269
+ const features = itemAudits.length > 0 ? combineFeatures(itemAudits.map((item) => item.features)) : extractObjectiveAnchorFeatures({});
270
+ return { features, items: itemAudits };
271
+ }
272
+ /**
273
+ * Score replayed structural features against revealed history. Path and module dimensions use Dice overlap so a
274
+ * partial module match gets visible credit without pretending it is exact. Change-kind overlap is the same metric
275
+ * over inferred/caller-supplied kinds. The revealed side may have zero overlapping modules; that is a valid low
276
+ * score, never an error, and the misses section explains what diverged.
277
+ */
278
+ export function scoreObjectiveAnchor(input) {
279
+ const replayed = isFeatures(input.replayed) ? input.replayed : extractObjectiveAnchorFeatures(input.replayed);
280
+ const revealed = isFeatures(input.revealed) ? input.revealed : extractObjectiveAnchorFeatures(input.revealed);
281
+ const weights = normalizeWeights(input.weights);
282
+ const dimensions = {
283
+ paths: roundScore(diceOverlap(replayed.paths, revealed.paths)),
284
+ modules: roundScore(diceOverlap(replayed.modules, revealed.modules)),
285
+ changeKinds: roundScore(diceOverlap(replayed.changeKinds, revealed.changeKinds)),
286
+ };
287
+ const score = roundScore(dimensions.paths * weights.paths + dimensions.modules * weights.modules + dimensions.changeKinds * weights.changeKinds);
288
+ return {
289
+ score,
290
+ dimensions,
291
+ audit: {
292
+ replayed,
293
+ revealed,
294
+ weights,
295
+ dimensions,
296
+ intersections: {
297
+ paths: intersectStrings(replayed.paths, revealed.paths),
298
+ modules: intersectStrings(replayed.modules, revealed.modules),
299
+ changeKinds: intersectKinds(replayed.changeKinds, revealed.changeKinds),
300
+ },
301
+ misses: {
302
+ replayedOnlyPaths: differenceStrings(replayed.paths, revealed.paths),
303
+ revealedOnlyPaths: differenceStrings(revealed.paths, replayed.paths),
304
+ replayedOnlyModules: differenceStrings(replayed.modules, revealed.modules),
305
+ revealedOnlyModules: differenceStrings(revealed.modules, replayed.modules),
306
+ replayedOnlyChangeKinds: differenceKinds(replayed.changeKinds, revealed.changeKinds),
307
+ revealedOnlyChangeKinds: differenceKinds(revealed.changeKinds, replayed.changeKinds),
308
+ },
309
+ },
310
+ };
311
+ }
312
+ /**
313
+ * Score arrays of replayed and revealed history records, preserving the per-record extraction evidence alongside the
314
+ * normal score/audit payload. This is the ergonomic entrypoint for replay harnesses that compare a generated plan/PR
315
+ * bundle with multiple revealed commits or merged PRs after the snapshot timestamp.
316
+ */
317
+ export function scoreObjectiveAnchorHistory(input) {
318
+ const replayed = extractObjectiveAnchorHistory(input.replayed);
319
+ const revealed = extractObjectiveAnchorHistory(input.revealed);
320
+ const score = scoreObjectiveAnchor({
321
+ replayed: replayed.features,
322
+ revealed: revealed.features,
323
+ weights: input.weights,
324
+ });
325
+ return {
326
+ ...score,
327
+ history: { replayed, revealed },
328
+ };
329
+ }
330
+ /**
331
+ * Render the score audit as deterministic Markdown for local replay artifacts. The renderer escapes Markdown control
332
+ * characters and collapses newlines in untrusted ids/paths so a caller can persist the output next to a replay run
333
+ * without letting a path or caller-supplied id reshape the report.
334
+ */
335
+ export function renderObjectiveAnchorAuditMarkdown(result) {
336
+ const lines = [
337
+ "# Objective-Anchor Score",
338
+ "",
339
+ `Score: ${result.score.toFixed(6)}`,
340
+ "",
341
+ "## Dimensions",
342
+ "",
343
+ `- paths: ${result.dimensions.paths.toFixed(6)}`,
344
+ `- modules: ${result.dimensions.modules.toFixed(6)}`,
345
+ `- changeKinds: ${result.dimensions.changeKinds.toFixed(6)}`,
346
+ "",
347
+ "## Weights",
348
+ "",
349
+ `- paths: ${result.audit.weights.paths.toFixed(6)}`,
350
+ `- modules: ${result.audit.weights.modules.toFixed(6)}`,
351
+ `- changeKinds: ${result.audit.weights.changeKinds.toFixed(6)}`,
352
+ "",
353
+ "## Replayed Features",
354
+ "",
355
+ markdownFeatureBlock(result.audit.replayed),
356
+ "",
357
+ "## Revealed Features",
358
+ "",
359
+ markdownFeatureBlock(result.audit.revealed),
360
+ "",
361
+ "## Intersections",
362
+ "",
363
+ "Paths:",
364
+ markdownList(result.audit.intersections.paths),
365
+ "",
366
+ "Modules:",
367
+ markdownList(result.audit.intersections.modules),
368
+ "",
369
+ "Change kinds:",
370
+ markdownKindList(result.audit.intersections.changeKinds),
371
+ "",
372
+ "## Misses",
373
+ "",
374
+ "Replayed-only paths:",
375
+ markdownList(result.audit.misses.replayedOnlyPaths),
376
+ "",
377
+ "Revealed-only paths:",
378
+ markdownList(result.audit.misses.revealedOnlyPaths),
379
+ "",
380
+ "Replayed-only modules:",
381
+ markdownList(result.audit.misses.replayedOnlyModules),
382
+ "",
383
+ "Revealed-only modules:",
384
+ markdownList(result.audit.misses.revealedOnlyModules),
385
+ "",
386
+ "Replayed-only change kinds:",
387
+ markdownKindList(result.audit.misses.replayedOnlyChangeKinds),
388
+ "",
389
+ "Revealed-only change kinds:",
390
+ markdownKindList(result.audit.misses.revealedOnlyChangeKinds),
391
+ ];
392
+ if ("history" in result) {
393
+ lines.push("", "## Replayed History Items", "", markdownHistoryBlock(result.history.replayed), "", "## Revealed History Items", "", markdownHistoryBlock(result.history.revealed));
394
+ }
395
+ return `${lines.join("\n")}\n`;
396
+ }
397
+ function isFeatures(value) {
398
+ return (Array.isArray(value.paths) &&
399
+ Array.isArray(value.modules) &&
400
+ Array.isArray(value.changeKinds));
401
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Compute a [0, 1] competition factor from duplicate-cluster pressure and open PR volume, mirroring
3
+ * `opportunityCompetitionFactor` in `src/signals/reward-risk.ts` so the miner engine can derive `dupRisk`
4
+ * inputs without importing hosted signal code.
5
+ */
6
+ export declare function computeOpportunityCompetition(highRiskDuplicateClusters: number, openPullRequests: number): number;
@@ -0,0 +1,29 @@
1
+ function round4(value) {
2
+ return Math.round(value * 10000) / 10000;
3
+ }
4
+ function clamp(value, min, max) {
5
+ return Math.max(min, Math.min(max, value));
6
+ }
7
+ function finiteNonNegative(value) {
8
+ if (!Number.isFinite(value))
9
+ return 0;
10
+ return Math.max(0, value);
11
+ }
12
+ function failClosedClusterPressure(value) {
13
+ // Non-finite input (NaN/±Infinity) means the duplicate-cluster signal is broken, not absent, so
14
+ // treat it as maximal pressure instead of `finiteNonNegative`'s fail-open 0 — dividing by
15
+ // `Math.max(1, openPrs)` and clamping below then yields the maximum competition factor of 1.
16
+ if (!Number.isFinite(value))
17
+ return Number.POSITIVE_INFINITY;
18
+ return Math.max(0, value);
19
+ }
20
+ /**
21
+ * Compute a [0, 1] competition factor from duplicate-cluster pressure and open PR volume, mirroring
22
+ * `opportunityCompetitionFactor` in `src/signals/reward-risk.ts` so the miner engine can derive `dupRisk`
23
+ * inputs without importing hosted signal code.
24
+ */
25
+ export function computeOpportunityCompetition(highRiskDuplicateClusters, openPullRequests) {
26
+ const clusters = failClosedClusterPressure(highRiskDuplicateClusters);
27
+ const openPrs = finiteNonNegative(openPullRequests);
28
+ return round4(clamp(clusters / Math.max(1, openPrs), 0, 1));
29
+ }
@@ -0,0 +1,18 @@
1
+ export type FreshnessIssue = {
2
+ state: string;
3
+ updatedAt?: string | null;
4
+ createdAt?: string | null;
5
+ };
6
+ declare function pickTimestamp(issue: FreshnessIssue): string | null;
7
+ declare function issueAgeDays(value: string | null, nowMs: number): number;
8
+ export declare const opportunityFreshnessInternals: {
9
+ pickTimestamp: typeof pickTimestamp;
10
+ issueAgeDays: typeof issueAgeDays;
11
+ };
12
+ /**
13
+ * Compute a [0.05, 1] freshness factor from open issue timestamps, mirroring
14
+ * `opportunityFreshnessFactor` in `src/signals/reward-risk.ts` with an injected clock so the miner engine
15
+ * stays pure and testable.
16
+ */
17
+ export declare function computeOpportunityFreshness(issues: readonly FreshnessIssue[], nowMs: number): number;
18
+ export {};