@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.
- package/CHANGELOG.md +94 -0
- package/LICENSE +661 -0
- package/README.md +676 -0
- package/dist/advisory/gate-advisory.d.ts +151 -0
- package/dist/advisory/gate-advisory.js +512 -0
- package/dist/ai-policy-map.d.ts +97 -0
- package/dist/ai-policy-map.js +377 -0
- package/dist/ams-policy-spec.d.ts +56 -0
- package/dist/ams-policy-spec.js +165 -0
- package/dist/calibration-dashboard.d.ts +23 -0
- package/dist/calibration-dashboard.js +65 -0
- package/dist/calibration-trend.d.ts +40 -0
- package/dist/calibration-trend.js +60 -0
- package/dist/contributor-fit.d.ts +24 -0
- package/dist/contributor-fit.js +50 -0
- package/dist/discovery-index-contract.d.ts +74 -0
- package/dist/discovery-index-contract.js +184 -0
- package/dist/discovery-soft-claim.d.ts +35 -0
- package/dist/discovery-soft-claim.js +50 -0
- package/dist/duplicate-winner.d.ts +57 -0
- package/dist/duplicate-winner.js +98 -0
- package/dist/feasibility.d.ts +27 -0
- package/dist/feasibility.js +86 -0
- package/dist/finding-severity-calibration.d.ts +116 -0
- package/dist/finding-severity-calibration.js +457 -0
- package/dist/fleet-run-manifest.d.ts +41 -0
- package/dist/fleet-run-manifest.js +151 -0
- package/dist/focus-manifest/guidance.d.ts +21 -0
- package/dist/focus-manifest/guidance.js +222 -0
- package/dist/focus-manifest.d.ts +826 -0
- package/dist/focus-manifest.js +2730 -0
- package/dist/gate-verdict-calibration.d.ts +111 -0
- package/dist/gate-verdict-calibration.js +411 -0
- package/dist/github/constants.d.ts +1 -0
- package/dist/github/constants.js +1 -0
- package/dist/github/sanitize-public-comment.d.ts +1 -0
- package/dist/github/sanitize-public-comment.js +26 -0
- package/dist/goal-model.d.ts +7 -0
- package/dist/goal-model.js +87 -0
- package/dist/governor/action-mode.d.ts +55 -0
- package/dist/governor/action-mode.js +74 -0
- package/dist/governor/budget-cap.d.ts +49 -0
- package/dist/governor/budget-cap.js +34 -0
- package/dist/governor/chokepoint.d.ts +58 -0
- package/dist/governor/chokepoint.js +269 -0
- package/dist/governor/kill-switch.d.ts +38 -0
- package/dist/governor/kill-switch.js +62 -0
- package/dist/governor/rate-limit.d.ts +40 -0
- package/dist/governor/rate-limit.js +63 -0
- package/dist/governor/reputation-throttle.d.ts +50 -0
- package/dist/governor/reputation-throttle.js +94 -0
- package/dist/governor/run-halt.d.ts +39 -0
- package/dist/governor/run-halt.js +83 -0
- package/dist/governor/self-plagiarism.d.ts +47 -0
- package/dist/governor/self-plagiarism.js +200 -0
- package/dist/governor/write-rate-limit.d.ts +52 -0
- package/dist/governor/write-rate-limit.js +132 -0
- package/dist/governor-ledger.d.ts +25 -0
- package/dist/governor-ledger.js +91 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +120 -0
- package/dist/issue-plan-decomposition.d.ts +18 -0
- package/dist/issue-plan-decomposition.js +56 -0
- package/dist/issue-rag-query.d.ts +12 -0
- package/dist/issue-rag-query.js +35 -0
- package/dist/local-scorer.d.ts +34 -0
- package/dist/local-scorer.js +36 -0
- package/dist/metadata-best-min-score.d.ts +9 -0
- package/dist/metadata-best-min-score.js +9 -0
- package/dist/metadata-best-pick.d.ts +9 -0
- package/dist/metadata-best-pick.js +9 -0
- package/dist/metadata-min-score.d.ts +9 -0
- package/dist/metadata-min-score.js +13 -0
- package/dist/metadata-top-min-score.d.ts +9 -0
- package/dist/metadata-top-min-score.js +13 -0
- package/dist/metadata-top-pick.d.ts +9 -0
- package/dist/metadata-top-pick.js +13 -0
- package/dist/miner/acceptance-criteria.d.ts +43 -0
- package/dist/miner/acceptance-criteria.js +77 -0
- package/dist/miner/agent-sdk-driver.d.ts +39 -0
- package/dist/miner/agent-sdk-driver.js +162 -0
- package/dist/miner/attempt-log.d.ts +29 -0
- package/dist/miner/attempt-log.js +100 -0
- package/dist/miner/attempt-metering.d.ts +39 -0
- package/dist/miner/attempt-metering.js +67 -0
- package/dist/miner/cli-subprocess-driver.d.ts +68 -0
- package/dist/miner/cli-subprocess-driver.js +272 -0
- package/dist/miner/coding-agent-driver.d.ts +33 -0
- package/dist/miner/coding-agent-driver.js +36 -0
- package/dist/miner/coding-agent-invoke.d.ts +11 -0
- package/dist/miner/coding-agent-invoke.js +80 -0
- package/dist/miner/coding-agent-mode.d.ts +25 -0
- package/dist/miner/coding-agent-mode.js +43 -0
- package/dist/miner/driver-factory.d.ts +82 -0
- package/dist/miner/driver-factory.js +169 -0
- package/dist/miner/harness-submission-trigger.d.ts +37 -0
- package/dist/miner/harness-submission-trigger.js +46 -0
- package/dist/miner/iterate-loop.d.ts +97 -0
- package/dist/miner/iterate-loop.js +278 -0
- package/dist/miner/iterate-policy.d.ts +98 -0
- package/dist/miner/iterate-policy.js +98 -0
- package/dist/miner/lint-guard.d.ts +50 -0
- package/dist/miner/lint-guard.js +68 -0
- package/dist/miner/local-write-tools.d.ts +81 -0
- package/dist/miner/local-write-tools.js +113 -0
- package/dist/miner/loop-reentry-policy.d.ts +36 -0
- package/dist/miner/loop-reentry-policy.js +53 -0
- package/dist/miner/repo-map.d.ts +51 -0
- package/dist/miner/repo-map.js +268 -0
- package/dist/miner/self-review-adapter.d.ts +106 -0
- package/dist/miner/self-review-adapter.js +83 -0
- package/dist/miner/submission-gate.d.ts +40 -0
- package/dist/miner/submission-gate.js +89 -0
- package/dist/miner/worktree-allocator.d.ts +59 -0
- package/dist/miner/worktree-allocator.js +60 -0
- package/dist/miner/worktree-pool.d.ts +55 -0
- package/dist/miner/worktree-pool.js +65 -0
- package/dist/miner-goal-lane-fit.d.ts +18 -0
- package/dist/miner-goal-lane-fit.js +76 -0
- package/dist/miner-goal-spec.d.ts +139 -0
- package/dist/miner-goal-spec.js +281 -0
- package/dist/miner-prediction-metrics.d.ts +16 -0
- package/dist/miner-prediction-metrics.js +61 -0
- package/dist/miner-telemetry.d.ts +32 -0
- package/dist/miner-telemetry.js +87 -0
- package/dist/objective-anchor.d.ts +125 -0
- package/dist/objective-anchor.js +401 -0
- package/dist/opportunity-competition.d.ts +6 -0
- package/dist/opportunity-competition.js +29 -0
- package/dist/opportunity-freshness.d.ts +18 -0
- package/dist/opportunity-freshness.js +55 -0
- package/dist/opportunity-metadata.d.ts +53 -0
- package/dist/opportunity-metadata.js +204 -0
- package/dist/opportunity-ranker.d.ts +46 -0
- package/dist/opportunity-ranker.js +74 -0
- package/dist/pairwise-calibration.d.ts +49 -0
- package/dist/pairwise-calibration.js +106 -0
- package/dist/phase7-calibration-loop.d.ts +136 -0
- package/dist/phase7-calibration-loop.js +437 -0
- package/dist/plan-blocked.d.ts +6 -0
- package/dist/plan-blocked.js +24 -0
- package/dist/plan-completed.d.ts +5 -0
- package/dist/plan-completed.js +6 -0
- package/dist/plan-completion.d.ts +6 -0
- package/dist/plan-completion.js +7 -0
- package/dist/plan-empty.d.ts +5 -0
- package/dist/plan-empty.js +6 -0
- package/dist/plan-export.d.ts +27 -0
- package/dist/plan-export.js +86 -0
- package/dist/plan-failure.d.ts +5 -0
- package/dist/plan-failure.js +6 -0
- package/dist/plan-overall-status.d.ts +6 -0
- package/dist/plan-overall-status.js +25 -0
- package/dist/plan-pending.d.ts +5 -0
- package/dist/plan-pending.js +6 -0
- package/dist/plan-progress-complete.d.ts +7 -0
- package/dist/plan-progress-complete.js +10 -0
- package/dist/plan-ready.d.ts +6 -0
- package/dist/plan-ready.js +12 -0
- package/dist/plan-running.d.ts +5 -0
- package/dist/plan-running.js +6 -0
- package/dist/plan-skipped.d.ts +5 -0
- package/dist/plan-skipped.js +6 -0
- package/dist/plan-step-count.d.ts +5 -0
- package/dist/plan-step-count.js +6 -0
- package/dist/plan-step-stats.d.ts +5 -0
- package/dist/plan-step-stats.js +6 -0
- package/dist/plan-templates.d.ts +25 -0
- package/dist/plan-templates.js +107 -0
- package/dist/plan-terminated.d.ts +6 -0
- package/dist/plan-terminated.js +9 -0
- package/dist/portfolio/non-convergence.d.ts +37 -0
- package/dist/portfolio/non-convergence.js +57 -0
- package/dist/portfolio/queue.d.ts +30 -0
- package/dist/portfolio/queue.js +150 -0
- package/dist/predicted-gate.d.ts +103 -0
- package/dist/predicted-gate.js +218 -0
- package/dist/prompt-packet.d.ts +11 -0
- package/dist/prompt-packet.js +34 -0
- package/dist/ranked-opportunity-best-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-best-min-score.js +9 -0
- package/dist/ranked-opportunity-best-pick.d.ts +8 -0
- package/dist/ranked-opportunity-best-pick.js +9 -0
- package/dist/ranked-opportunity-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-min-score.js +13 -0
- package/dist/ranked-opportunity-top-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-top-min-score.js +13 -0
- package/dist/review/advisory-ai-routing-config.d.ts +8 -0
- package/dist/review/advisory-ai-routing-config.js +40 -0
- package/dist/review/check-names.d.ts +1 -0
- package/dist/review/check-names.js +1 -0
- package/dist/review/cla-check.d.ts +49 -0
- package/dist/review/cla-check.js +71 -0
- package/dist/review/diff-file-priority.d.ts +1 -0
- package/dist/review/diff-file-priority.js +12 -0
- package/dist/review/enrichment-analyzer-names.d.ts +3 -0
- package/dist/review/enrichment-analyzer-names.js +63 -0
- package/dist/review/guardrail-config.d.ts +10 -0
- package/dist/review/guardrail-config.js +62 -0
- package/dist/review/linked-issue-hard-rules-config.d.ts +4 -0
- package/dist/review/linked-issue-hard-rules-config.js +79 -0
- package/dist/review/linked-issue-label-propagation.d.ts +9 -0
- package/dist/review/linked-issue-label-propagation.js +114 -0
- package/dist/review/pre-merge-checks.d.ts +28 -0
- package/dist/review/pre-merge-checks.js +66 -0
- package/dist/review/review-thread-findings.d.ts +1 -0
- package/dist/review/review-thread-findings.js +1 -0
- package/dist/review/safe-url.d.ts +5 -0
- package/dist/review/safe-url.js +137 -0
- package/dist/review/screenshot-table-gate.d.ts +87 -0
- package/dist/review/screenshot-table-gate.js +340 -0
- package/dist/review/unlinked-issue-guardrail-config.d.ts +9 -0
- package/dist/review/unlinked-issue-guardrail-config.js +44 -0
- package/dist/reviewer-consensus-calibration.d.ts +113 -0
- package/dist/reviewer-consensus-calibration.js +493 -0
- package/dist/reward-risk.d.ts +223 -0
- package/dist/reward-risk.js +618 -0
- package/dist/scoring/label-match.d.ts +4 -0
- package/dist/scoring/label-match.js +145 -0
- package/dist/scoring/model.d.ts +31 -0
- package/dist/scoring/model.js +157 -0
- package/dist/scoring/pending-pr-scenarios.d.ts +44 -0
- package/dist/scoring/pending-pr-scenarios.js +145 -0
- package/dist/scoring/preview.d.ts +246 -0
- package/dist/scoring/preview.js +1108 -0
- package/dist/scoring/types.d.ts +131 -0
- package/dist/scoring/types.js +7 -0
- package/dist/settings/auto-close-exempt.d.ts +10 -0
- package/dist/settings/auto-close-exempt.js +58 -0
- package/dist/settings/autonomy.d.ts +33 -0
- package/dist/settings/autonomy.js +80 -0
- package/dist/settings/command-authorization.d.ts +49 -0
- package/dist/settings/command-authorization.js +222 -0
- package/dist/settings/contributor-blacklist.d.ts +17 -0
- package/dist/settings/contributor-blacklist.js +86 -0
- package/dist/settings/moderation-rules.d.ts +51 -0
- package/dist/settings/moderation-rules.js +104 -0
- package/dist/settings/pr-type-label.d.ts +70 -0
- package/dist/settings/pr-type-label.js +149 -0
- package/dist/signals/change-guardrail.d.ts +49 -0
- package/dist/signals/change-guardrail.js +164 -0
- package/dist/signals/check-summary.d.ts +9 -0
- package/dist/signals/check-summary.js +12 -0
- package/dist/signals/contributor-calibration.d.ts +23 -0
- package/dist/signals/contributor-calibration.js +47 -0
- package/dist/signals/duplicate-winner.d.ts +9 -0
- package/dist/signals/duplicate-winner.js +9 -0
- package/dist/signals/path-matchers.d.ts +36 -0
- package/dist/signals/path-matchers.js +371 -0
- package/dist/signals/predicted-gate-engine.d.ts +62 -0
- package/dist/signals/predicted-gate-engine.js +895 -0
- package/dist/signals/preflight-limits.d.ts +14 -0
- package/dist/signals/preflight-limits.js +14 -0
- package/dist/signals/slop.d.ts +56 -0
- package/dist/signals/slop.js +324 -0
- package/dist/signals/test-evidence.d.ts +25 -0
- package/dist/signals/test-evidence.js +108 -0
- package/dist/subprocess-env.d.ts +23 -0
- package/dist/subprocess-env.js +74 -0
- package/dist/track-record-summary.d.ts +92 -0
- package/dist/track-record-summary.js +301 -0
- package/dist/types/manifest-deps-types.d.ts +514 -0
- package/dist/types/manifest-deps-types.js +4 -0
- package/dist/types/predicted-gate-types.d.ts +338 -0
- package/dist/types/predicted-gate-types.js +3 -0
- package/dist/types/reward-risk-types.d.ts +319 -0
- package/dist/types/reward-risk-types.js +11 -0
- package/dist/utils/json.d.ts +1 -0
- package/dist/utils/json.js +3 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- 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;
|