@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,86 @@
1
+ // GitHub logins: 1–39 chars, alphanumeric or single hyphens (not leading/trailing). Anything else is dropped so a
2
+ // malformed entry can never widen the match or break the close path.
3
+ const GITHUB_LOGIN = /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/;
4
+ const MAX_ENTRIES = 1000;
5
+ const MAX_REASON_CHARS = 200;
6
+ const MAX_EVIDENCE = 10;
7
+ const MAX_EVIDENCE_CHARS = 500;
8
+ function isRecord(value) {
9
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10
+ }
11
+ /** Normalize a raw blacklist value (DB JSON or `.gittensory.yml`) into validated, de-duplicated entries. Never
12
+ * throws: malformed entries are dropped with a warning. De-dup is by case-insensitive login (the FIRST wins, so
13
+ * its richer metadata is kept). */
14
+ export function normalizeContributorBlacklist(input) {
15
+ const warnings = [];
16
+ if (input === undefined || input === null)
17
+ return { entries: [], warnings };
18
+ if (!Array.isArray(input)) {
19
+ warnings.push("contributorBlacklist must be a list of entries; ignoring it.");
20
+ return { entries: [], warnings };
21
+ }
22
+ const entries = [];
23
+ const seen = new Set();
24
+ for (const [index, raw] of input.entries()) {
25
+ if (entries.length >= MAX_ENTRIES) {
26
+ warnings.push(`contributorBlacklist is capped at ${MAX_ENTRIES} entries; dropping the rest.`);
27
+ break;
28
+ }
29
+ // Accept either a bare login string or a `{ login, ... }` object.
30
+ const record = typeof raw === "string" ? { login: raw } : raw;
31
+ if (!isRecord(record) || typeof record.login !== "string") {
32
+ warnings.push(`contributorBlacklist[${index}] needs a string login; ignoring it.`);
33
+ continue;
34
+ }
35
+ const login = record.login.trim();
36
+ if (!GITHUB_LOGIN.test(login)) {
37
+ warnings.push(`contributorBlacklist[${index}].login is not a valid GitHub login; ignoring it.`);
38
+ continue;
39
+ }
40
+ const key = login.toLowerCase();
41
+ if (seen.has(key))
42
+ continue; // first occurrence wins
43
+ seen.add(key);
44
+ const entry = { login };
45
+ if (typeof record.reason === "string" && record.reason.trim().length > 0)
46
+ entry.reason = record.reason.trim().slice(0, MAX_REASON_CHARS);
47
+ if (Array.isArray(record.evidence)) {
48
+ const evidence = record.evidence.filter((ref) => typeof ref === "string" && ref.trim().length > 0).map((ref) => ref.trim().slice(0, MAX_EVIDENCE_CHARS)).slice(0, MAX_EVIDENCE);
49
+ if (evidence.length > 0)
50
+ entry.evidence = evidence;
51
+ }
52
+ if (typeof record.addedAt === "string" && record.addedAt.trim().length > 0)
53
+ entry.addedAt = record.addedAt.trim();
54
+ entries.push(entry);
55
+ }
56
+ return { entries, warnings };
57
+ }
58
+ /** The blacklist entry matching `login` (case-insensitive), or null. Tolerates an absent list (treated as empty)
59
+ * so callers can pass the optional `settings.contributorBlacklist` directly. */
60
+ export function findBlacklistEntry(login, entries) {
61
+ if (!login)
62
+ return null;
63
+ const key = login.toLowerCase();
64
+ return (entries ?? []).find((entry) => entry.login.toLowerCase() === key) ?? null;
65
+ }
66
+ /** True iff `login` is on the resolved blacklist. */
67
+ export function isAuthorBlacklisted(login, entries) {
68
+ return findBlacklistEntry(login, entries) !== null;
69
+ }
70
+ /** Union multiple blacklist sources (e.g. the shared/global list + the per-repo list) by case-insensitive login.
71
+ * A login on ANY source is blocked; the FIRST source's entry wins on a duplicate so earlier (more authoritative)
72
+ * metadata is preserved. Already-normalized inputs in, de-duplicated entries out. */
73
+ export function mergeContributorBlacklists(...lists) {
74
+ const merged = [];
75
+ const seen = new Set();
76
+ for (const list of lists) {
77
+ for (const entry of list) {
78
+ const key = entry.login.toLowerCase();
79
+ if (seen.has(key))
80
+ continue;
81
+ seen.add(key);
82
+ merged.push(entry);
83
+ }
84
+ }
85
+ return merged;
86
+ }
@@ -0,0 +1,51 @@
1
+ /** The anti-abuse mechanisms this engine can count violations from -- the three ORIGINAL mechanisms
2
+ * (contributor cap, blacklist, review-nag) plus review-evasion (#review-evasion-protection: a contributor
3
+ * closing/converting-to-draft their own PR to dodge an active review). Kept as a closed union (not an open
4
+ * string) so an unrecognized value is always a normalization error, never silently accepted. */
5
+ export type ModerationRuleType = "contributor_cap" | "blacklist" | "review_nag" | "review_evasion";
6
+ /** The `audit_events.event_type` recorded for each rule's violation -- namespaced under `moderation.violation.*`
7
+ * so a cross-eventType, cross-repo count query (see `db/repositories.ts`) can scope to exactly this family. */
8
+ export declare const MODERATION_VIOLATION_EVENT_TYPE: Record<ModerationRuleType, string>;
9
+ export declare const DEFAULT_MODERATION_WARNING_LABEL = "mod:warning";
10
+ export declare const DEFAULT_MODERATION_BANNED_LABEL = "mod:banned";
11
+ export declare const DEFAULT_MODERATION_BAN_THRESHOLD = 5;
12
+ export declare const MAX_MODERATION_VIOLATION_DECAY_DAYS = 3650;
13
+ export type GlobalModerationConfig = {
14
+ enabled: boolean;
15
+ rules: ModerationRuleType[];
16
+ warningLabel: string;
17
+ bannedLabel: string;
18
+ banThreshold: number;
19
+ violationDecayDays: number | null;
20
+ autoBlacklistOnBan: boolean;
21
+ };
22
+ export declare const DEFAULT_GLOBAL_MODERATION_CONFIG: GlobalModerationConfig;
23
+ /** Normalize a raw moderation-rules list (DB JSON or `.gittensory.yml`) into a validated, de-duplicated list
24
+ * of known rule types. Never throws: an unknown/malformed entry is dropped with a warning, matching the
25
+ * normalize-with-warnings shape every other settings list in this codebase already uses. */
26
+ export declare function normalizeModerationRules(input: unknown): {
27
+ rules: ModerationRuleType[];
28
+ warnings: string[];
29
+ };
30
+ /** Normalize a raw moderation label value: empty/whitespace-only collapses to undefined (falls back to the
31
+ * caller's default), overlong is truncated. Never throws. Mirrors blacklistLabel/contributorCapLabel's
32
+ * shape, minus the explicit-null-means-"no label" case those close-coupled labels use -- a moderation label
33
+ * is always applied when the tier is reached, never suppressible to "no label at all". */
34
+ export declare function normalizeModerationLabel(input: unknown): string | undefined;
35
+ /** Effective rule set for one repo: an explicit per-repo override REPLACES the global list entirely (not a
36
+ * union) -- a repo opting out of counting review-nag toward the shared tally, for example, must be able to
37
+ * do so without also losing the ability to opt out of the others. Absent/undefined override ⇒ inherit the
38
+ * global list unchanged. */
39
+ export declare function resolveEffectiveModerationRules(globalRules: readonly ModerationRuleType[], perRepoOverride: readonly ModerationRuleType[] | null | undefined): ModerationRuleType[];
40
+ export type ModerationGateMode = "inherit" | "off" | "enabled";
41
+ /** Whether the WHOLE moderation layer runs for one repo: the global master switch is authoritative;
42
+ * `off` lets a repo opt out while the global layer is enabled, and `enabled`/`inherit` both require the
43
+ * global switch to be on. */
44
+ export declare function resolveModerationGateEnabled(globalEnabled: boolean, gateMode: ModerationGateMode): boolean;
45
+ export type ModerationTier = "none" | "warning" | "banned";
46
+ /** Pure escalation decision: given the actor's TOTAL violation count (including the one that just fired,
47
+ * already recorded by the caller) and the configured ban threshold, which tier applies. A non-positive
48
+ * threshold (malformed config) can never be reached by a real count, so it degrades to "always banned once
49
+ * any violation exists" rather than throwing -- still a safe, non-silent failure mode for a misconfigured
50
+ * threshold, not a crash. */
51
+ export declare function moderationTierForViolationCount(count: number, banThreshold: number): ModerationTier;
@@ -0,0 +1,104 @@
1
+ // Centralized moderation-rules engine (generic self-host feature, #selfhost-mod-engine). A single modular
2
+ // layer over the three EXISTING anti-abuse mechanisms (contributor cap, blacklist, review-nag) that already
3
+ // short-circuit a PR's disposition: every time one of them fires against a non-exempt contributor, it counts
4
+ // toward that login's install-wide violation tally (the shared `audit_events` ledger, keyed by actor). At
5
+ // >=1 lifetime violation the contributor is labeled with `warningLabel`; at >=`banThreshold` they are labeled
6
+ // `bannedLabel` and (when `autoBlacklistOnBan`) auto-added to the existing global contributor blacklist --
7
+ // the SAME "permanent two-strikes" enforcement an already-banned login gets.
8
+ //
9
+ // Config-as-code, layered the same as every other setting: a global default (the whole layer can be off,
10
+ // which rules count, the label text, the threshold, whether a ban auto-enforces) with a PER-REPO override
11
+ // that can turn the layer off/on for just that repo and override which rules feed IT specifically. NEVER
12
+ // hard-coded for any one repo -- a self-hoster's own `.gittensory.yml`/dashboard settings choose everything.
13
+ const ALL_MODERATION_RULE_TYPES = ["contributor_cap", "blacklist", "review_nag", "review_evasion"];
14
+ /** The `audit_events.event_type` recorded for each rule's violation -- namespaced under `moderation.violation.*`
15
+ * so a cross-eventType, cross-repo count query (see `db/repositories.ts`) can scope to exactly this family. */
16
+ export const MODERATION_VIOLATION_EVENT_TYPE = {
17
+ contributor_cap: "moderation.violation.contributor_cap",
18
+ blacklist: "moderation.violation.blacklist",
19
+ review_nag: "moderation.violation.review_nag",
20
+ review_evasion: "moderation.violation.review_evasion",
21
+ };
22
+ export const DEFAULT_MODERATION_WARNING_LABEL = "mod:warning";
23
+ export const DEFAULT_MODERATION_BANNED_LABEL = "mod:banned";
24
+ export const DEFAULT_MODERATION_BAN_THRESHOLD = 5;
25
+ // Keep the decay lookback operationally bounded, mirroring MAX_REVIEW_NAG_COOLDOWN_DAYS -- repo-controlled
26
+ // config cannot overflow Date arithmetic.
27
+ export const MAX_MODERATION_VIOLATION_DECAY_DAYS = 3650;
28
+ const MAX_LABEL_CHARS = 100;
29
+ export const DEFAULT_GLOBAL_MODERATION_CONFIG = {
30
+ enabled: false,
31
+ rules: [...ALL_MODERATION_RULE_TYPES],
32
+ warningLabel: DEFAULT_MODERATION_WARNING_LABEL,
33
+ bannedLabel: DEFAULT_MODERATION_BANNED_LABEL,
34
+ banThreshold: DEFAULT_MODERATION_BAN_THRESHOLD,
35
+ violationDecayDays: null,
36
+ autoBlacklistOnBan: true,
37
+ };
38
+ /** Normalize a raw moderation-rules list (DB JSON or `.gittensory.yml`) into a validated, de-duplicated list
39
+ * of known rule types. Never throws: an unknown/malformed entry is dropped with a warning, matching the
40
+ * normalize-with-warnings shape every other settings list in this codebase already uses. */
41
+ export function normalizeModerationRules(input) {
42
+ const warnings = [];
43
+ if (input === undefined || input === null)
44
+ return { rules: [], warnings };
45
+ if (!Array.isArray(input)) {
46
+ warnings.push("moderationRules must be a list of rule type strings; ignoring it.");
47
+ return { rules: [], warnings };
48
+ }
49
+ const rules = [];
50
+ const seen = new Set();
51
+ for (const [index, raw] of input.entries()) {
52
+ if (typeof raw !== "string" || !ALL_MODERATION_RULE_TYPES.includes(raw)) {
53
+ warnings.push(`moderationRules[${index}] is not a recognized rule type (expected one of ${ALL_MODERATION_RULE_TYPES.join(", ")}); ignoring it.`);
54
+ continue;
55
+ }
56
+ const rule = raw;
57
+ if (seen.has(rule))
58
+ continue;
59
+ seen.add(rule);
60
+ rules.push(rule);
61
+ }
62
+ return { rules, warnings };
63
+ }
64
+ /** Normalize a raw moderation label value: empty/whitespace-only collapses to undefined (falls back to the
65
+ * caller's default), overlong is truncated. Never throws. Mirrors blacklistLabel/contributorCapLabel's
66
+ * shape, minus the explicit-null-means-"no label" case those close-coupled labels use -- a moderation label
67
+ * is always applied when the tier is reached, never suppressible to "no label at all". */
68
+ export function normalizeModerationLabel(input) {
69
+ if (typeof input !== "string")
70
+ return undefined;
71
+ const trimmed = input.trim();
72
+ if (trimmed.length === 0)
73
+ return undefined;
74
+ return trimmed.slice(0, MAX_LABEL_CHARS);
75
+ }
76
+ /** Effective rule set for one repo: an explicit per-repo override REPLACES the global list entirely (not a
77
+ * union) -- a repo opting out of counting review-nag toward the shared tally, for example, must be able to
78
+ * do so without also losing the ability to opt out of the others. Absent/undefined override ⇒ inherit the
79
+ * global list unchanged. */
80
+ export function resolveEffectiveModerationRules(globalRules, perRepoOverride) {
81
+ return perRepoOverride ? [...perRepoOverride] : [...globalRules];
82
+ }
83
+ /** Whether the WHOLE moderation layer runs for one repo: the global master switch is authoritative;
84
+ * `off` lets a repo opt out while the global layer is enabled, and `enabled`/`inherit` both require the
85
+ * global switch to be on. */
86
+ export function resolveModerationGateEnabled(globalEnabled, gateMode) {
87
+ if (!globalEnabled)
88
+ return false;
89
+ if (gateMode === "off")
90
+ return false;
91
+ return true;
92
+ }
93
+ /** Pure escalation decision: given the actor's TOTAL violation count (including the one that just fired,
94
+ * already recorded by the caller) and the configured ban threshold, which tier applies. A non-positive
95
+ * threshold (malformed config) can never be reached by a real count, so it degrades to "always banned once
96
+ * any violation exists" rather than throwing -- still a safe, non-silent failure mode for a misconfigured
97
+ * threshold, not a crash. */
98
+ export function moderationTierForViolationCount(count, banThreshold) {
99
+ if (count <= 0)
100
+ return "none";
101
+ if (count >= banThreshold)
102
+ return "banned";
103
+ return "warning";
104
+ }
@@ -0,0 +1,70 @@
1
+ import type { LinkedIssueLabelPropagationConfig, PrTypeLabelSet } from "../types/manifest-deps-types.js";
2
+ export type { PrTypeLabelSet } from "../types/manifest-deps-types.js";
3
+ /** The gittensor: namespace Gittensor itself uses -- an EXAMPLE default config, not an engine
4
+ * assumption (#label-modularity): a self-hoster's `typeLabels` fully replaces the category set these
5
+ * keys are drawn from. The built-in categories are mutually exclusive by default (see
6
+ * `resolvePrTypeLabel`'s `removeLabels`) unless a propagation mapping is explicitly additive. */
7
+ export declare const DEFAULT_TYPE_LABELS: PrTypeLabelSet;
8
+ export declare const MAX_TYPE_LABEL_CATEGORIES = 32;
9
+ export declare const MAX_TYPE_LABEL_NAME_LENGTH = 50;
10
+ /** feature ONLY for substantial new functionality: a feat/feature prefix plus a concrete add/support/enable
11
+ * action, with small recognition/classification/cleanup-style work downgraded to bug/work. EVERYTHING else —
12
+ * fix, test, docs, chore, refactor, perf, ci, build, style, revert — is bug. */
13
+ export declare function deriveKindFromTitle(title: string | undefined): "bug" | "feature";
14
+ /** Defaults-fill a per-repo `typeLabels` override (config-as-code), generic over an arbitrary set of
15
+ * categories (#label-modularity): every key of `DEFAULT_TYPE_LABELS` (the built-in bug/feature/
16
+ * priority categories) is taken independently from `input` when it is a non-empty string, else falls
17
+ * back to the corresponding built-in default — so a repo can override just one built-in label name
18
+ * (e.g. only `priority`) and keep the others default. Any EXTRA key present in `input` beyond the
19
+ * built-in set (a self-hoster's own custom category, e.g. `security`) is included verbatim when
20
+ * valid, up to `MAX_TYPE_LABEL_CATEGORIES` total categories and GitHub's 50-character label-name
21
+ * limit; there is no built-in default for it to fall back to, so an invalid extra-category value is
22
+ * dropped entirely (warned, not defaulted) rather than silently defaulted. A non-object input yields
23
+ * the full default set; omitted is normal (no warning), present-but-wrong-shaped warns. An input that
24
+ * IS a valid object but has zero own keys (`{}`) also yields the full default set here — this
25
+ * function only ever defaults-fills or validates a COMPLETE settings value (the DB-persisted set, or
26
+ * a from-scratch construction); `resolveEffectiveSettings` (focus-manifest.ts) is what gives a
27
+ * manifest's *literal* `typeLabels: {}` its own distinct "deliberately zero categories" meaning,
28
+ * since collapsing that here would also flip every legacy `type_labels_json = '{}'` DB row (the SQL
29
+ * column's own default, predating any explicit customization) from full defaults to zero labels —
30
+ * the exact behavior change #priority-linked-issue-gate's migration promised existing repos would
31
+ * never see. Mirrors `normalizeCommandAuthorizationPolicy`'s defaults-fill pattern
32
+ * (`src/settings/command-authorization.ts`). */
33
+ export declare function normalizeTypeLabelSet(input: unknown, warnings: string[]): PrTypeLabelSet;
34
+ /** The pure decision `resolvePrTypeLabel` returns: which label(s) to apply, which configured
35
+ * type-label-set members to remove for mutual exclusivity, and why. */
36
+ export type PrTypeLabelDecision = {
37
+ applyLabels: string[];
38
+ removeLabels: string[];
39
+ source: "propagation_exclusive" | "propagation_additive" | "title";
40
+ };
41
+ /**
42
+ * Resolve the TYPE label decision for a PR.
43
+ * 1. Linked-issue label PROPAGATION (config-driven, #priority-linked-issue-gate): when enabled, the
44
+ * LAST configured EXCLUSIVE mapping whose `issueLabel` appears (case-insensitively) among the
45
+ * ALREADY-FETCHED `linkedIssueLabels` wins (#5385 -- declare exclusive mappings in ascending
46
+ * precedence order). This is the ONLY way a label like `gittensor:priority`
47
+ * can ever be chosen — this function does no I/O and never infers it from title, changed files,
48
+ * AI output, or PR labels; the caller must fetch `linkedIssueLabels` itself (see
49
+ * `fetchLinkedIssueLabelsForPropagation` in `review/linked-issue-label-propagation-fetch.ts`).
50
+ * - `removeOtherTypeLabels: true` (exclusive) — the mapped label REPLACES the type label,
51
+ * exactly like today's bug/feature/priority classification (used for `gittensor:priority`).
52
+ * - `removeOtherTypeLabels: false` (additive) — the mapped label is applied ALONGSIDE the
53
+ * normal title-based bug/feature label, which is left untouched (e.g. a generic
54
+ * `customer:vip` → `triage:vip` triage marker that has nothing to do with bug/feature/priority).
55
+ * 2. Otherwise, feature (feat/feature) / bug (everything else) by the conventional-commit title prefix
56
+ * -- ONLY when `labels` actually has a name registered for that built-in category; a configured set
57
+ * that omits `bug`/`feature` entirely (a self-hoster who only wants custom, propagation-driven
58
+ * categories, or an explicit `typeLabels: {}` resolved to zero categories) applies nothing for that
59
+ * branch rather than inventing a label name (#label-modularity).
60
+ * `removeLabels` is always "every member of the configured type-label set that isn't one of
61
+ * `applyLabels`" — generic and total over however many categories are configured, and safe even if a
62
+ * misconfigured additive mapping's `prLabel` happens to collide with a type-label-set name (it is
63
+ * excluded from removal since it is also being applied). Pure + total.
64
+ */
65
+ export declare function resolvePrTypeLabel(input: {
66
+ title: string | undefined;
67
+ linkedIssueLabels?: string[] | undefined;
68
+ labels?: PrTypeLabelSet | undefined;
69
+ propagation?: LinkedIssueLabelPropagationConfig | undefined;
70
+ }): PrTypeLabelDecision;
@@ -0,0 +1,149 @@
1
+ /** The gittensor: namespace Gittensor itself uses -- an EXAMPLE default config, not an engine
2
+ * assumption (#label-modularity): a self-hoster's `typeLabels` fully replaces the category set these
3
+ * keys are drawn from. The built-in categories are mutually exclusive by default (see
4
+ * `resolvePrTypeLabel`'s `removeLabels`) unless a propagation mapping is explicitly additive. */
5
+ export const DEFAULT_TYPE_LABELS = {
6
+ bug: "gittensor:bug",
7
+ feature: "gittensor:feature",
8
+ priority: "gittensor:priority",
9
+ };
10
+ export const MAX_TYPE_LABEL_CATEGORIES = 32;
11
+ export const MAX_TYPE_LABEL_NAME_LENGTH = 50;
12
+ const FEATURE_TITLE_ACTION_RE = /\b(add|adds|added|create|creates|created|enable|enables|enabled|implement|implements|implemented|integrate|integrates|integrated|introduce|introduces|introduced|launch|launches|launched|support|supports|supported|wire|wires|wired)\b/i;
13
+ const FEATURE_TITLE_DOWNGRADE_RE = /\b(avoid|block|bug|bugfix|cache|classify|classifies|classifying|cleanup|clean-up|clean up|detect|detects|detecting|docs?|fix|format|guard|lint|normalize|recognize|recognizes|recognizing|refactor|regression|rename|test|tests|testing|tighten|typo)\b/i;
14
+ /** feature ONLY for substantial new functionality: a feat/feature prefix plus a concrete add/support/enable
15
+ * action, with small recognition/classification/cleanup-style work downgraded to bug/work. EVERYTHING else —
16
+ * fix, test, docs, chore, refactor, perf, ci, build, style, revert — is bug. */
17
+ export function deriveKindFromTitle(title) {
18
+ const normalized = (title ?? "").trim();
19
+ const match = /^([a-zA-Z]+)/.exec(normalized);
20
+ const type = match?.[1]?.toLowerCase();
21
+ if (type !== "feat" && type !== "feature")
22
+ return "bug";
23
+ const subject = normalized.replace(/^[a-zA-Z]+(?:\([^)]*\))?:?\s*/, "");
24
+ if (!FEATURE_TITLE_ACTION_RE.test(subject))
25
+ return "bug";
26
+ return FEATURE_TITLE_DOWNGRADE_RE.test(subject) ? "bug" : "feature";
27
+ }
28
+ /** Defaults-fill a per-repo `typeLabels` override (config-as-code), generic over an arbitrary set of
29
+ * categories (#label-modularity): every key of `DEFAULT_TYPE_LABELS` (the built-in bug/feature/
30
+ * priority categories) is taken independently from `input` when it is a non-empty string, else falls
31
+ * back to the corresponding built-in default — so a repo can override just one built-in label name
32
+ * (e.g. only `priority`) and keep the others default. Any EXTRA key present in `input` beyond the
33
+ * built-in set (a self-hoster's own custom category, e.g. `security`) is included verbatim when
34
+ * valid, up to `MAX_TYPE_LABEL_CATEGORIES` total categories and GitHub's 50-character label-name
35
+ * limit; there is no built-in default for it to fall back to, so an invalid extra-category value is
36
+ * dropped entirely (warned, not defaulted) rather than silently defaulted. A non-object input yields
37
+ * the full default set; omitted is normal (no warning), present-but-wrong-shaped warns. An input that
38
+ * IS a valid object but has zero own keys (`{}`) also yields the full default set here — this
39
+ * function only ever defaults-fills or validates a COMPLETE settings value (the DB-persisted set, or
40
+ * a from-scratch construction); `resolveEffectiveSettings` (focus-manifest.ts) is what gives a
41
+ * manifest's *literal* `typeLabels: {}` its own distinct "deliberately zero categories" meaning,
42
+ * since collapsing that here would also flip every legacy `type_labels_json = '{}'` DB row (the SQL
43
+ * column's own default, predating any explicit customization) from full defaults to zero labels —
44
+ * the exact behavior change #priority-linked-issue-gate's migration promised existing repos would
45
+ * never see. Mirrors `normalizeCommandAuthorizationPolicy`'s defaults-fill pattern
46
+ * (`src/settings/command-authorization.ts`). */
47
+ export function normalizeTypeLabelSet(input, warnings) {
48
+ if (input === undefined)
49
+ return { ...DEFAULT_TYPE_LABELS };
50
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
51
+ warnings.push("settings.typeLabels must be an object; using default label names.");
52
+ return { ...DEFAULT_TYPE_LABELS };
53
+ }
54
+ const record = input;
55
+ const keys = new Set([...Object.keys(DEFAULT_TYPE_LABELS), ...Object.keys(record)]);
56
+ const result = {};
57
+ for (const key of keys) {
58
+ const value = record[key];
59
+ const wouldAddCategory = result[key] === undefined;
60
+ if (wouldAddCategory && Object.keys(result).length >= MAX_TYPE_LABEL_CATEGORIES) {
61
+ if (value !== undefined)
62
+ warnings.push(`settings.typeLabels has more than ${MAX_TYPE_LABEL_CATEGORIES} categories; ignoring ${key}.`);
63
+ continue;
64
+ }
65
+ const builtInDefault = DEFAULT_TYPE_LABELS[key];
66
+ if (typeof value === "string" && value.trim().length > 0 && value.trim().length <= MAX_TYPE_LABEL_NAME_LENGTH) {
67
+ result[key] = value.trim();
68
+ continue;
69
+ }
70
+ if (value !== undefined) {
71
+ const reason = typeof value === "string" && value.trim().length > MAX_TYPE_LABEL_NAME_LENGTH ? `a non-empty string no longer than ${MAX_TYPE_LABEL_NAME_LENGTH} characters` : "a non-empty string";
72
+ warnings.push(builtInDefault !== undefined
73
+ ? `settings.typeLabels.${key} must be ${reason}; using the default "${builtInDefault}".`
74
+ : `settings.typeLabels.${key} must be ${reason}; ignoring it.`);
75
+ }
76
+ // Reached for BOTH an invalid present value and an absent one -- a built-in category (bug/feature/
77
+ // priority) always has a default to fall back to; an unknown custom category does not, so it is
78
+ // dropped entirely (warned above when it was present-but-invalid, silently absent when never named).
79
+ if (builtInDefault !== undefined)
80
+ result[key] = builtInDefault;
81
+ }
82
+ return result;
83
+ }
84
+ /**
85
+ * Resolve the TYPE label decision for a PR.
86
+ * 1. Linked-issue label PROPAGATION (config-driven, #priority-linked-issue-gate): when enabled, the
87
+ * LAST configured EXCLUSIVE mapping whose `issueLabel` appears (case-insensitively) among the
88
+ * ALREADY-FETCHED `linkedIssueLabels` wins (#5385 -- declare exclusive mappings in ascending
89
+ * precedence order). This is the ONLY way a label like `gittensor:priority`
90
+ * can ever be chosen — this function does no I/O and never infers it from title, changed files,
91
+ * AI output, or PR labels; the caller must fetch `linkedIssueLabels` itself (see
92
+ * `fetchLinkedIssueLabelsForPropagation` in `review/linked-issue-label-propagation-fetch.ts`).
93
+ * - `removeOtherTypeLabels: true` (exclusive) — the mapped label REPLACES the type label,
94
+ * exactly like today's bug/feature/priority classification (used for `gittensor:priority`).
95
+ * - `removeOtherTypeLabels: false` (additive) — the mapped label is applied ALONGSIDE the
96
+ * normal title-based bug/feature label, which is left untouched (e.g. a generic
97
+ * `customer:vip` → `triage:vip` triage marker that has nothing to do with bug/feature/priority).
98
+ * 2. Otherwise, feature (feat/feature) / bug (everything else) by the conventional-commit title prefix
99
+ * -- ONLY when `labels` actually has a name registered for that built-in category; a configured set
100
+ * that omits `bug`/`feature` entirely (a self-hoster who only wants custom, propagation-driven
101
+ * categories, or an explicit `typeLabels: {}` resolved to zero categories) applies nothing for that
102
+ * branch rather than inventing a label name (#label-modularity).
103
+ * `removeLabels` is always "every member of the configured type-label set that isn't one of
104
+ * `applyLabels`" — generic and total over however many categories are configured, and safe even if a
105
+ * misconfigured additive mapping's `prLabel` happens to collide with a type-label-set name (it is
106
+ * excluded from removal since it is also being applied). Pure + total.
107
+ */
108
+ export function resolvePrTypeLabel(input) {
109
+ const labels = input.labels ?? DEFAULT_TYPE_LABELS;
110
+ const isRealLabel = (label) => typeof label === "string" && label.length > 0;
111
+ const typeLabelSet = Object.values(labels).filter(isRealLabel).filter((label) => label.length <= MAX_TYPE_LABEL_NAME_LENGTH).slice(0, MAX_TYPE_LABEL_CATEGORIES);
112
+ const titleLabel = labels[deriveKindFromTitle(input.title)];
113
+ const decide = (applyLabels, source) => {
114
+ const apply = [...new Set(applyLabels.filter(isRealLabel))];
115
+ return { applyLabels: apply, removeLabels: typeLabelSet.filter((label) => !apply.includes(label)), source };
116
+ };
117
+ if (input.propagation?.enabled) {
118
+ const wanted = new Set((input.linkedIssueLabels ?? []).map((label) => label.toLowerCase()));
119
+ // Collect EVERY mapping the linked issue's labels satisfy, not just the first. An exclusive mapping
120
+ // (removeOtherTypeLabels: true -- e.g. bug/feature, genuinely mutually-exclusive categories) lets the
121
+ // LAST-configured match win, not the first (#5385 fix -- was first-match-wins, which meant a linked issue
122
+ // carrying BOTH gittensor:bug and gittensor:feature always resolved to bug, the lower-value label, purely
123
+ // because bug is declared before feature in `.gittensory.yml`). Operators must declare exclusive mappings
124
+ // in ASCENDING precedence order (lowest-value category first, e.g. bug then feature) so the last match
125
+ // encountered while iterating is the highest-precedence one that actually applies -- this mirrors the
126
+ // repo's own default mapping order, which is already bug/feature/priority (ascending multiplier value).
127
+ // An additive mapping (e.g. priority -- a maintainer-hand-picked reward tag that coexists WITH whichever
128
+ // type already applies, not a type of its own) must compose with that winner instead of being skipped just
129
+ // because an earlier mapping in the array already matched. Before the original #priority-linked-issue-gate
130
+ // composition fix, an additive match was unreachable whenever the SAME linked issue also carried a label an
131
+ // earlier (exclusive) mapping matched -- the overwhelmingly common case for gittensor:priority, which is
132
+ // applied ALONGSIDE gittensor:bug/gittensor:feature on the issue, never instead of it.
133
+ let exclusiveMatch;
134
+ const additiveMatches = [];
135
+ for (const mapping of input.propagation.mappings) {
136
+ if (!wanted.has(mapping.issueLabel.toLowerCase()))
137
+ continue;
138
+ if (mapping.removeOtherTypeLabels)
139
+ exclusiveMatch = mapping;
140
+ else
141
+ additiveMatches.push(mapping);
142
+ }
143
+ if (exclusiveMatch || additiveMatches.length > 0) {
144
+ const applyLabels = [exclusiveMatch ? exclusiveMatch.prLabel : titleLabel, ...additiveMatches.map((mapping) => mapping.prLabel)];
145
+ return decide(applyLabels, exclusiveMatch ? "propagation_exclusive" : "propagation_additive");
146
+ }
147
+ }
148
+ return decide([titleLabel], "title");
149
+ }
@@ -0,0 +1,49 @@
1
+ export declare function canonicalize(value: string): string;
2
+ /** True if `glob` has more wildcard GROUPS than can be safely compiled to a RegExp without risking catastrophic
3
+ * backtracking (see the MAX_GLOB_WILDCARD_GROUPS rationale above). Exported so any OTHER glob-accepting config
4
+ * surface (e.g. focus-manifest.ts's contentLane.*Glob parsing) can reject an over-complex glob using the SAME
5
+ * predicate globToRegExp itself enforces — a caller with its own, independently-counted threshold could accept
6
+ * a glob globToRegExp then silently compiles to NEVER_MATCHES, configuring a lane that can never activate. */
7
+ export declare function hasUnsafeWildcardCount(glob: string): boolean;
8
+ /** Convert a path glob (`*` matches within a segment, `**` matches across `/`) to an anchored RegExp. The
9
+ * glob is canonicalized first, so matching is case-insensitive against a canonicalized path. Exported for
10
+ * reuse anywhere a maintainer-supplied path pattern needs compiling — never compile a raw regex string from
11
+ * config (ReDoS risk); this linear-time glob compiler is the one safe path pattern this codebase uses.
12
+ *
13
+ * An over-complex glob (see MAX_GLOB_WILDCARD_GROUPS) short-circuits to NEVER_MATCHES instead of being compiled —
14
+ * this function never returns a RegExp that risks catastrophic backtracking on .test(), for any input. */
15
+ export declare function globToRegExp(glob: string): RegExp;
16
+ /**
17
+ * True if `path` matches any of the globs (`*` within a segment, `**` across `/`), case-insensitively. A glob
18
+ * with more wildcards than can be safely compiled (see hasUnsafeWildcardCount) is treated as matching EVERY
19
+ * path — fail SAFE TOWARD GUARDING, mirroring isGuardrailHit's own "unknown ⇒ treat as a hit" philosophy (an
20
+ * over-complex guardrail glob still forces manual review) rather than the NEVER_MATCHES default globToRegExp
21
+ * itself falls back to, which would silently disable the maintainer's intended protection — the worse failure
22
+ * mode for a safety guardrail specifically (see globToRegExp's own docstring for why NEVER_MATCHES is still the
23
+ * right default for globToRegExp as a general-purpose compiler).
24
+ */
25
+ export declare function matchesAny(path: string, globs: string[]): boolean;
26
+ /**
27
+ * The changed paths (if any) that trip a hard guardrail. A non-empty result means the PR touches a guarded
28
+ * path and MUST fall through to a human — gittensory may neither auto-merge nor auto-close it. Pure.
29
+ */
30
+ export declare function changedPathsHittingGuardrail(changedPaths: string[], hardGuardrailGlobs: string[]): string[];
31
+ export type GuardrailPathMatch = {
32
+ path: string;
33
+ glob: string;
34
+ };
35
+ /**
36
+ * Structured guardrail match details for public review output + audit logs. Over-complex globs preserve the
37
+ * same fail-safe direction as {@link matchesAny}: they match every non-empty path rather than silently disabling
38
+ * a maintainer's guardrail. Unknown changed paths are represented by {@link isGuardrailHit}'s boolean path only,
39
+ * so callers can say "paths unavailable" without inventing a fake path.
40
+ */
41
+ export declare function guardrailPathMatches(changedPaths: string[], hardGuardrailGlobs: string[]): GuardrailPathMatch[];
42
+ /**
43
+ * Whether a PR's diff trips a hard guardrail — the BOOLEAN form shared by the disposition (held for owner
44
+ * review) and the public comment (so the headline reads "held", not "safe to merge"). FAIL-SAFE on unknown
45
+ * paths (#1062): when guardrails ARE configured but the changed-file set is empty (the cache is not yet / no
46
+ * longer populated), we cannot prove the PR avoids a guarded path, so treat it as a hit. No guardrails
47
+ * configured ⇒ never a hit. Pure.
48
+ */
49
+ export declare function isGuardrailHit(changedPaths: string[], hardGuardrailGlobs: string[]): boolean;