@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,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portfolio queue primitives (#2326). Pure bookkeeping for the miner's local cross-repo work queue:
|
|
3
|
+
* bucket items by repo, respect global/per-repo WIP caps, and select the next eligible batch in a
|
|
4
|
+
* deterministic diversified order. No IO, no Date, no randomness, and no enforcement/action logic.
|
|
5
|
+
*/
|
|
6
|
+
const QUEUED_STATE = "queued";
|
|
7
|
+
const ACTIVE_STATE = "in_progress";
|
|
8
|
+
function cleanId(value) {
|
|
9
|
+
return value.trim();
|
|
10
|
+
}
|
|
11
|
+
function cleanRepoFullName(value) {
|
|
12
|
+
return value.trim().toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
function normalizeState(value) {
|
|
15
|
+
return value === ACTIVE_STATE ? ACTIVE_STATE : QUEUED_STATE;
|
|
16
|
+
}
|
|
17
|
+
function normalizeItem(item) {
|
|
18
|
+
return {
|
|
19
|
+
id: cleanId(item.id),
|
|
20
|
+
repoFullName: cleanRepoFullName(item.repoFullName),
|
|
21
|
+
state: normalizeState(item.state),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function finiteNonNegativeInt(value) {
|
|
25
|
+
if (!Number.isFinite(value))
|
|
26
|
+
return 0;
|
|
27
|
+
return Math.max(0, Math.trunc(value));
|
|
28
|
+
}
|
|
29
|
+
function normalizeCaps(caps) {
|
|
30
|
+
return {
|
|
31
|
+
globalWipCap: finiteNonNegativeInt(caps.globalWipCap),
|
|
32
|
+
perRepoWipCap: finiteNonNegativeInt(caps.perRepoWipCap),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function isActiveItem(item) {
|
|
36
|
+
return item.state === ACTIVE_STATE;
|
|
37
|
+
}
|
|
38
|
+
function isQueuedItem(item) {
|
|
39
|
+
return item.state === QUEUED_STATE;
|
|
40
|
+
}
|
|
41
|
+
function projectedLoad(bucket) {
|
|
42
|
+
return bucket.activeCount + bucket.selectedCount;
|
|
43
|
+
}
|
|
44
|
+
function pickNextBucket(buckets, lastRepoFullName) {
|
|
45
|
+
const eligible = buckets.filter((bucket) => bucket.selectedCount < bucket.queuedItems.length);
|
|
46
|
+
if (eligible.length === 0)
|
|
47
|
+
return null;
|
|
48
|
+
const alternates = lastRepoFullName === null ? eligible : eligible.filter((bucket) => bucket.repoFullName !== lastRepoFullName);
|
|
49
|
+
const candidates = alternates.length > 0 ? alternates : eligible;
|
|
50
|
+
let winner = candidates[0];
|
|
51
|
+
for (const candidate of candidates.slice(1)) {
|
|
52
|
+
const winnerLoad = projectedLoad(winner);
|
|
53
|
+
const candidateLoad = projectedLoad(candidate);
|
|
54
|
+
if (candidateLoad < winnerLoad) {
|
|
55
|
+
winner = candidate;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return winner;
|
|
59
|
+
}
|
|
60
|
+
function queueHasItem(queue, itemId) {
|
|
61
|
+
return queue.buckets.some((bucket) => bucket.items.some((item) => cleanId(item.id) === itemId));
|
|
62
|
+
}
|
|
63
|
+
/** Append one item to the queue, creating its repo bucket if needed. Duplicate/blank ids are ignored. Pure. */
|
|
64
|
+
export function enqueueItem(queue, item) {
|
|
65
|
+
const normalizedItem = normalizeItem(item);
|
|
66
|
+
if (!normalizedItem.id || !normalizedItem.repoFullName || queueHasItem(queue, normalizedItem.id))
|
|
67
|
+
return queue;
|
|
68
|
+
const bucketIndex = queue.buckets.findIndex((bucket) => cleanRepoFullName(bucket.repoFullName) === normalizedItem.repoFullName);
|
|
69
|
+
if (bucketIndex === -1) {
|
|
70
|
+
return { buckets: [...queue.buckets, { repoFullName: normalizedItem.repoFullName, items: [normalizedItem] }] };
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
buckets: queue.buckets.map((bucket, index) => index === bucketIndex
|
|
74
|
+
? {
|
|
75
|
+
repoFullName: normalizedItem.repoFullName,
|
|
76
|
+
items: [...bucket.items.map(normalizeItem), normalizedItem],
|
|
77
|
+
}
|
|
78
|
+
: bucket),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** Remove matching items by id; empty buckets disappear. Unknown/blank ids are a no-op. Pure. */
|
|
82
|
+
export function dequeueItem(queue, itemId) {
|
|
83
|
+
const targetId = cleanId(itemId);
|
|
84
|
+
if (!targetId)
|
|
85
|
+
return queue;
|
|
86
|
+
let removed = false;
|
|
87
|
+
const buckets = queue.buckets.flatMap((bucket) => {
|
|
88
|
+
const items = bucket.items.filter((item) => {
|
|
89
|
+
const keep = cleanId(item.id) !== targetId;
|
|
90
|
+
if (!keep)
|
|
91
|
+
removed = true;
|
|
92
|
+
return keep;
|
|
93
|
+
});
|
|
94
|
+
return items.length > 0 ? [{ ...bucket, items }] : [];
|
|
95
|
+
});
|
|
96
|
+
return removed ? { buckets } : queue;
|
|
97
|
+
}
|
|
98
|
+
/** Select the next batch of queued items that fit within global/per-repo WIP caps. The batch always alternates
|
|
99
|
+
* repos when another repo still has an eligible item waiting; among those eligible repos, lower current load wins
|
|
100
|
+
* and ties keep stable bucket order. Pure. */
|
|
101
|
+
export function nextEligibleItems(queue, caps) {
|
|
102
|
+
const normalizedCaps = normalizeCaps(caps);
|
|
103
|
+
if (normalizedCaps.globalWipCap === 0 || normalizedCaps.perRepoWipCap === 0)
|
|
104
|
+
return [];
|
|
105
|
+
const selectionBucketsByRepo = new Map();
|
|
106
|
+
for (const bucket of queue.buckets) {
|
|
107
|
+
for (const normalizedItem of bucket.items.map(normalizeItem)) {
|
|
108
|
+
const repoFullName = normalizedItem.repoFullName;
|
|
109
|
+
const existing = selectionBucketsByRepo.get(repoFullName);
|
|
110
|
+
if (existing) {
|
|
111
|
+
if (isActiveItem(normalizedItem)) {
|
|
112
|
+
existing.activeCount += 1;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
existing.queuedItems.push(normalizedItem);
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
selectionBucketsByRepo.set(repoFullName, {
|
|
120
|
+
repoFullName,
|
|
121
|
+
activeCount: isActiveItem(normalizedItem) ? 1 : 0,
|
|
122
|
+
queuedItems: isQueuedItem(normalizedItem) ? [normalizedItem] : [],
|
|
123
|
+
selectedCount: 0,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const selectionBuckets = Array.from(selectionBucketsByRepo.values()).map((bucket) => {
|
|
128
|
+
const remainingPerRepoCapacity = normalizedCaps.perRepoWipCap - bucket.activeCount;
|
|
129
|
+
return {
|
|
130
|
+
...bucket,
|
|
131
|
+
queuedItems: remainingPerRepoCapacity > 0 ? bucket.queuedItems.slice(0, remainingPerRepoCapacity) : [],
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
const totalActiveCount = selectionBuckets.reduce((sum, bucket) => sum + bucket.activeCount, 0);
|
|
135
|
+
const remainingGlobalSlots = normalizedCaps.globalWipCap - totalActiveCount;
|
|
136
|
+
if (remainingGlobalSlots <= 0)
|
|
137
|
+
return [];
|
|
138
|
+
const selected = [];
|
|
139
|
+
let lastRepoFullName = null;
|
|
140
|
+
while (selected.length < remainingGlobalSlots) {
|
|
141
|
+
const nextBucket = pickNextBucket(selectionBuckets, lastRepoFullName);
|
|
142
|
+
if (nextBucket === null)
|
|
143
|
+
break;
|
|
144
|
+
const nextItem = nextBucket.queuedItems[nextBucket.selectedCount];
|
|
145
|
+
selected.push(nextItem);
|
|
146
|
+
nextBucket.selectedCount += 1;
|
|
147
|
+
lastRepoFullName = nextBucket.repoFullName;
|
|
148
|
+
}
|
|
149
|
+
return selected;
|
|
150
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { type IssueQualityReport } from "./signals/predicted-gate-engine.js";
|
|
2
|
+
import { type FocusManifest } from "./focus-manifest/guidance.js";
|
|
3
|
+
import { type ContributorCalibrationSignal } from "./signals/contributor-calibration.js";
|
|
4
|
+
import type { BountyRecord, GatePolicyPack, IssueRecord, PullRequestRecord, RepositoryRecord } from "./types/predicted-gate-types.js";
|
|
5
|
+
export type { GatePolicyPack } from "./types/predicted-gate-types.js";
|
|
6
|
+
export type { GateCheckConclusion } from "./advisory/gate-advisory.js";
|
|
7
|
+
export { applyContributorCalibration, MIN_CALIBRATION_SAMPLES, MAX_READINESS_ADJUSTMENT, type ContributorCalibrationSignal } from "./signals/contributor-calibration.js";
|
|
8
|
+
import { type GateCheckConclusion } from "./advisory/gate-advisory.js";
|
|
9
|
+
/**
|
|
10
|
+
* Pre-submission "will my PR pass the gate?" prediction for a MINER, computed BEFORE a PR exists.
|
|
11
|
+
*
|
|
12
|
+
* Parity: it runs the EXACT same engine the maintainer PR pipeline runs — buildPullRequestAdvisory +
|
|
13
|
+
* evaluateGateCheck over a synthetic PR built from the contributor's local branch metadata. The verdict a
|
|
14
|
+
* miner sees pre-submission is therefore the same verdict the gate would compute post-submission.
|
|
15
|
+
*
|
|
16
|
+
* Boundary: the gate POLICY is sourced ONLY from the repo's PUBLIC `.gittensory.yml` (`manifest.gate`) +
|
|
17
|
+
* safe defaults — never the maintainer's private dashboard/DB settings. The `.gittensory.yml` is in the
|
|
18
|
+
* repo and publicly viewable, so this leaks nothing a contributor could not already read. The result is
|
|
19
|
+
* explicitly labelled "predicted" and notes that private overrides and AI-consensus blockers are not
|
|
20
|
+
* evaluated pre-submission.
|
|
21
|
+
*/
|
|
22
|
+
export type PredictedGateVerdict = {
|
|
23
|
+
predicted: true;
|
|
24
|
+
basis: "public_config";
|
|
25
|
+
/** Which policy pack the repo's public config selects (#692/#693). Under `oss-anti-slop` the predicted
|
|
26
|
+
* verdict applies to ANY author (no confirmed-contributor gate) — so an agent on a non-Gittensor repo
|
|
27
|
+
* gets a meaningful "will this pass?" answer with no Gittensor account. */
|
|
28
|
+
pack: GatePolicyPack;
|
|
29
|
+
conclusion: GateCheckConclusion;
|
|
30
|
+
title: string;
|
|
31
|
+
summary: string;
|
|
32
|
+
readinessScore: number | null;
|
|
33
|
+
confirmedContributor: boolean | undefined;
|
|
34
|
+
blockers: Array<{
|
|
35
|
+
code: string;
|
|
36
|
+
title: string;
|
|
37
|
+
detail: string;
|
|
38
|
+
action?: string | undefined;
|
|
39
|
+
}>;
|
|
40
|
+
warnings: Array<{
|
|
41
|
+
code: string;
|
|
42
|
+
title: string;
|
|
43
|
+
detail: string;
|
|
44
|
+
action?: string | undefined;
|
|
45
|
+
}>;
|
|
46
|
+
/** Opt-in conversion funnel (#694): present only under the `oss-anti-slop` pack — a non-Gittensor
|
|
47
|
+
* adopter's path to "earn on Gittensor". `null` under `gittensor` (the contributor is already there). */
|
|
48
|
+
funnel: {
|
|
49
|
+
message: string;
|
|
50
|
+
registerUrl: string;
|
|
51
|
+
} | null;
|
|
52
|
+
note: string;
|
|
53
|
+
};
|
|
54
|
+
/** Compose the predicted-gate note. Slop is always disclaimed; the path-policy/path-gated disclaimer drops once
|
|
55
|
+
* the caller supplies changed paths (#11-13/#18), replaced by the size-prediction file-count-only caveat (#2458). */
|
|
56
|
+
export declare function predictedGateNote(hasChangedPaths: boolean): string;
|
|
57
|
+
export type PredictedGateInput = {
|
|
58
|
+
repoFullName: string;
|
|
59
|
+
contributorLogin: string;
|
|
60
|
+
title: string;
|
|
61
|
+
body?: string | undefined;
|
|
62
|
+
labels?: string[] | undefined;
|
|
63
|
+
linkedIssues?: number[] | undefined;
|
|
64
|
+
authorAssociation?: string | undefined;
|
|
65
|
+
};
|
|
66
|
+
export declare function publicSafeFinding(finding: {
|
|
67
|
+
code: string;
|
|
68
|
+
title: string;
|
|
69
|
+
detail: string;
|
|
70
|
+
action?: string | undefined;
|
|
71
|
+
}, sanitize?: (value: string) => string): {
|
|
72
|
+
code: string;
|
|
73
|
+
title: string;
|
|
74
|
+
detail: string;
|
|
75
|
+
action: string | undefined;
|
|
76
|
+
};
|
|
77
|
+
export declare function buildPredictedGateVerdict(args: {
|
|
78
|
+
input: PredictedGateInput;
|
|
79
|
+
manifest: FocusManifest;
|
|
80
|
+
repo: RepositoryRecord | null;
|
|
81
|
+
issues: IssueRecord[];
|
|
82
|
+
pullRequests: PullRequestRecord[];
|
|
83
|
+
bounties?: BountyRecord[] | undefined;
|
|
84
|
+
issueQuality?: IssueQualityReport | null | undefined;
|
|
85
|
+
/** The contributor's OWN confirmed-Gittensor status (self-data). Carried through for transparency only —
|
|
86
|
+
* it no longer changes the predicted verdict (the real gate fails any author on a configured blocker;
|
|
87
|
+
* confirmed-status affects only on-chain scoring). `undefined` → not resolved. */
|
|
88
|
+
confirmedContributor?: boolean | undefined;
|
|
89
|
+
/** The PR's changed file PATHS (metadata only — file paths, never source content, so the predictor stays
|
|
90
|
+
* metadata-only). When supplied, the path-dependent gates the live gate enforces are also predicted: the
|
|
91
|
+
* focus-manifest path policy and the path-gated pre-merge checks. Absent ⇒ only path-independent pre-merge
|
|
92
|
+
* checks are predicted and the note discloses the gap (#11-13/#18). */
|
|
93
|
+
changedPaths?: string[] | undefined;
|
|
94
|
+
/** #2349: this login's own historical predict-vs-real agreement (predicted_gate_calibration_ledger),
|
|
95
|
+
* pre-aggregated by the caller -- this engine package never touches D1. Adjusts ONLY the returned
|
|
96
|
+
* readinessScore, strictly AFTER blockers/conclusion/warnings are finalized below, so personalization can
|
|
97
|
+
* never flip a hard blocker off or add/remove a finding; it can only narrow/widen the advisory confidence
|
|
98
|
+
* number within a fixed clamp (see applyContributorCalibration). `undefined`, `null`, or below the
|
|
99
|
+
* cold-start sample threshold ⇒ unweighted baseline, byte-identical to before this field existed. The raw
|
|
100
|
+
* calibration numbers are never echoed back in the returned verdict -- only their clamped, already-public
|
|
101
|
+
* downstream effect on readinessScore is. */
|
|
102
|
+
contributorCalibration?: ContributorCalibrationSignal | null | undefined;
|
|
103
|
+
}): PredictedGateVerdict;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { buildCollisionReport, buildPreflightResult, buildPublicReadinessScore, buildQueueHealth, unionScopedOverlapClusters, } from "./signals/predicted-gate-engine.js";
|
|
2
|
+
import { buildFocusManifestGuidance } from "./focus-manifest/guidance.js";
|
|
3
|
+
import { guardrailPathMatches, isGuardrailHit } from "./signals/change-guardrail.js";
|
|
4
|
+
import { resolveHardGuardrailGlobs } from "./review/guardrail-config.js";
|
|
5
|
+
import { applyContributorCalibration } from "./signals/contributor-calibration.js";
|
|
6
|
+
import { sanitizePublicComment } from "./github/sanitize-public-comment.js";
|
|
7
|
+
import { GITTENSOR_HOME_URL } from "./github/constants.js";
|
|
8
|
+
export { applyContributorCalibration, MIN_CALIBRATION_SAMPLES, MAX_READINESS_ADJUSTMENT } from "./signals/contributor-calibration.js";
|
|
9
|
+
// Opt-in funnel (#694): a non-Gittensor adopter running the `oss-anti-slop` pack learns that Gittensor pays
|
|
10
|
+
// contributors for OSS work like this. Public-safe "earn" wording only (never reward/payout/score).
|
|
11
|
+
const OSS_ANTI_SLOP_FUNNEL = {
|
|
12
|
+
message: "This repo runs the Gittensor anti-slop gate. Gittensor lets GitHub contributors earn for open-source work like this — register to start earning.",
|
|
13
|
+
registerUrl: GITTENSOR_HOME_URL,
|
|
14
|
+
};
|
|
15
|
+
import { buildPullRequestAdvisory, evaluateGateCheck } from "./advisory/gate-advisory.js";
|
|
16
|
+
import { hasValidationNote, isTestPath } from "./signals/test-evidence.js";
|
|
17
|
+
import { evaluateClaCheck } from "./review/cla-check.js";
|
|
18
|
+
import { evaluatePreMergeChecks } from "./review/pre-merge-checks.js";
|
|
19
|
+
const PREDICTED_GATE_NOTE_BASE = "Predicted from the repo's public .gittensory.yml gate config + safe defaults. The maintainer may have " +
|
|
20
|
+
"private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only " +
|
|
21
|
+
"evaluated on a real PR. ";
|
|
22
|
+
// The slop score is ALWAYS disclaimed: it needs the diff CONTENT, which the metadata-only oracle never receives.
|
|
23
|
+
const PREDICTED_GATE_NOTE_SLOP = "The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. ";
|
|
24
|
+
// Disclaimed only when the caller did NOT supply changed paths — then path-dependent gates can't be predicted.
|
|
25
|
+
const PREDICTED_GATE_NOTE_NO_PATHS = "Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and " +
|
|
26
|
+
"any pre-merge check scoped to changed paths; without them only path-independent title/description/label " +
|
|
27
|
+
"pre-merge checks are predicted. ";
|
|
28
|
+
// Shown instead of NO_PATHS once changed paths ARE supplied (#2458): the size hold can now be predicted, but only
|
|
29
|
+
// from file COUNT — line-diff stats are never sent to this metadata-only predictor, so a PR with many changed
|
|
30
|
+
// LINES across few files can still under-predict the hold the live gate would actually apply.
|
|
31
|
+
const PREDICTED_GATE_NOTE_SIZE_FILES_ONLY = "The size-hold prediction uses changed FILE count only, not changed LINE count (line-diff stats are not " +
|
|
32
|
+
"available pre-submission), so it may under-predict a hold for a PR with many changed lines across few files. ";
|
|
33
|
+
const PREDICTED_GATE_NOTE_GATE_EQUALITY = "Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor " +
|
|
34
|
+
"status (which affects only on-chain scoring).";
|
|
35
|
+
/** Compose the predicted-gate note. Slop is always disclaimed; the path-policy/path-gated disclaimer drops once
|
|
36
|
+
* the caller supplies changed paths (#11-13/#18), replaced by the size-prediction file-count-only caveat (#2458). */
|
|
37
|
+
export function predictedGateNote(hasChangedPaths) {
|
|
38
|
+
return (PREDICTED_GATE_NOTE_BASE +
|
|
39
|
+
PREDICTED_GATE_NOTE_SLOP +
|
|
40
|
+
(hasChangedPaths ? PREDICTED_GATE_NOTE_SIZE_FILES_ONLY : PREDICTED_GATE_NOTE_NO_PATHS) +
|
|
41
|
+
PREDICTED_GATE_NOTE_GATE_EQUALITY);
|
|
42
|
+
}
|
|
43
|
+
export function publicSafeFinding(finding, sanitize = sanitizePublicComment) {
|
|
44
|
+
return {
|
|
45
|
+
code: finding.code,
|
|
46
|
+
title: sanitize(finding.title),
|
|
47
|
+
detail: sanitize(finding.detail),
|
|
48
|
+
action: finding.action ? sanitize(finding.action) : undefined,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** GitHub full names are case-insensitive — mirror `sameRepo` in the live gate paths. */
|
|
52
|
+
function sameRepoFullName(left, right) {
|
|
53
|
+
return Boolean(left && right && left.toLowerCase() === right.toLowerCase());
|
|
54
|
+
}
|
|
55
|
+
export function buildPredictedGateVerdict(args) {
|
|
56
|
+
const { input, manifest, repo, issues, pullRequests } = args;
|
|
57
|
+
const gate = manifest.gate;
|
|
58
|
+
const changedPaths = (args.changedPaths ?? []).filter((path) => typeof path === "string" && path.length > 0);
|
|
59
|
+
const hasChangedPaths = changedPaths.length > 0;
|
|
60
|
+
const preflight = buildPreflightResult({
|
|
61
|
+
repoFullName: input.repoFullName,
|
|
62
|
+
contributorLogin: input.contributorLogin,
|
|
63
|
+
title: input.title,
|
|
64
|
+
body: input.body,
|
|
65
|
+
labels: input.labels,
|
|
66
|
+
linkedIssues: input.linkedIssues,
|
|
67
|
+
authorAssociation: input.authorAssociation,
|
|
68
|
+
}, repo, issues, pullRequests, args.bounties ?? [], args.issueQuality);
|
|
69
|
+
// A synthetic open PR from the local branch metadata — fed to the SAME advisory builder as a real PR.
|
|
70
|
+
// Use preflight's normalized linked issues so body references like "Closes #7" match real PR parity.
|
|
71
|
+
const syntheticPr = {
|
|
72
|
+
repoFullName: input.repoFullName,
|
|
73
|
+
number: 0,
|
|
74
|
+
title: input.title,
|
|
75
|
+
state: "open",
|
|
76
|
+
authorLogin: input.contributorLogin,
|
|
77
|
+
authorAssociation: input.authorAssociation ?? null,
|
|
78
|
+
body: input.body ?? null,
|
|
79
|
+
labels: input.labels ?? [],
|
|
80
|
+
linkedIssues: preflight.linkedIssues,
|
|
81
|
+
};
|
|
82
|
+
const collisions = buildCollisionReport(input.repoFullName, issues, pullRequests);
|
|
83
|
+
const queueHealth = buildQueueHealth(repo, issues, pullRequests, collisions);
|
|
84
|
+
const readiness = buildPublicReadinessScore({
|
|
85
|
+
pr: syntheticPr,
|
|
86
|
+
preflight,
|
|
87
|
+
queueHealth,
|
|
88
|
+
scopedOverlapCount: unionScopedOverlapClusters(collisions, syntheticPr, preflight.collisions).length,
|
|
89
|
+
});
|
|
90
|
+
// Linked-issue finding is surfaced when the repo's public policy treats it as anything but `off`, so the
|
|
91
|
+
// gate can evaluate it; evaluateGateCheck decides whether it actually blocks (block) or stays advisory.
|
|
92
|
+
// The composite mergeReadiness gate forces the linked-issue sub-gate on (applyMergeReadinessGate), and the
|
|
93
|
+
// live path collects linked-issue evidence whenever merge-readiness is enabled (shouldCollectLinkedIssueEvidence,
|
|
94
|
+
// queue/processors.ts), so the predictor must surface the finding under mergeReadiness too — otherwise a
|
|
95
|
+
// `mergeReadiness:block` repo with linkedIssue unset predicts a false success while the live gate one-shot
|
|
96
|
+
// closes the PR on the missing-linked-issue blocker. (#merge-readiness-parity)
|
|
97
|
+
const requireLinkedIssue = (gate.linkedIssue !== null && gate.linkedIssue !== "off") || (gate.mergeReadiness !== null && gate.mergeReadiness !== "off");
|
|
98
|
+
// `duplicateWinnerEnabled` is INTENTIONALLY omitted (#dup-winner): the prospective PR is synthetic #0, but a
|
|
99
|
+
// real new PR opened into an existing duplicate cluster gets the HIGHEST number ⇒ it is always a duplicate
|
|
100
|
+
// LOSER, never the winner. So the predictor must keep showing the duplicate finding (the honest pre-submit
|
|
101
|
+
// answer). Threading the flag here would let isDuplicateClusterWinner(0, …) treat #0 as the winner and
|
|
102
|
+
// falsely suppress the block — a false-optimism regression. Do NOT add it without modeling #0 as the loser.
|
|
103
|
+
// Thread linked-issue authors from the issues snapshot so the predictor surfaces the self-authored-linked-issue
|
|
104
|
+
// finding too — evaluateGateCheck below already receives gate.selfAuthoredLinkedIssue, but without this finding it
|
|
105
|
+
// had nothing to act on, so a configured self-authored gate never showed in the preview. Offline path: resolved
|
|
106
|
+
// from the snapshot, never a live fetch. (#self-authored-parity)
|
|
107
|
+
const issueAuthorByNumber = new Map(issues.filter((issue) => sameRepoFullName(issue.repoFullName, input.repoFullName)).map((issue) => [issue.number, issue.authorLogin ?? null]));
|
|
108
|
+
const linkedIssueAuthorLogins = syntheticPr.linkedIssues.map((issueNumber) => issueAuthorByNumber.get(issueNumber) ?? null);
|
|
109
|
+
// Mirror the live gate (listOtherOpenPullRequests): repo-scoped open siblings only; closed/merged PRs sharing a
|
|
110
|
+
// linked issue must not fire duplicate_pr_risk. authorHistory below still needs every state for its grace counts.
|
|
111
|
+
const openSiblings = pullRequests.filter((otherPr) => otherPr.state === "open" &&
|
|
112
|
+
sameRepoFullName(otherPr.repoFullName, input.repoFullName) &&
|
|
113
|
+
otherPr.number !== syntheticPr.number);
|
|
114
|
+
const advisory = buildPullRequestAdvisory(repo, syntheticPr, { otherOpenPullRequests: openSiblings, requireLinkedIssue, linkedIssueAuthorLogins });
|
|
115
|
+
// Deterministic pre-merge checks parity (#11/#18): the LIVE gate enforces the repo's `review.pre_merge_checks`
|
|
116
|
+
// (from the SAME public .gittensory.yml the predictor already reads). With the PR's changed paths supplied,
|
|
117
|
+
// evaluate ALL of them exactly as live (path-gated checks now have their `whenPaths` to match against); without
|
|
118
|
+
// paths, evaluate only the PATH-INDEPENDENT checks (empty `whenPaths` — title/description/label assertions),
|
|
119
|
+
// whose inputs are exactly the real PR's, and disclaim the path-gated ones in the note.
|
|
120
|
+
const predictablePreMergeChecks = hasChangedPaths ? manifest.review.preMergeChecks : manifest.review.preMergeChecks.filter((check) => check.whenPaths.length === 0);
|
|
121
|
+
advisory.findings.push(...evaluatePreMergeChecks(predictablePreMergeChecks, { title: syntheticPr.title, body: syntheticPr.body, labels: syntheticPr.labels, changedPaths, filesResolved: hasChangedPaths }));
|
|
122
|
+
// CLA / license-compatibility gate parity (#2564): this metadata-only predictor never resolves a LIVE
|
|
123
|
+
// check-run (it runs before the PR exists), so only the phrase-match detection method is predictable —
|
|
124
|
+
// checkRunConclusion stays undefined, mirroring evaluateClaCheck's "not evaluated" contract for an
|
|
125
|
+
// unresolved check-run. A repo relying solely on checkRunName (no consentPhrase configured) therefore
|
|
126
|
+
// predicts no finding either way; the note below discloses this limitation.
|
|
127
|
+
if (gate.claMode !== null && gate.claMode !== "off") {
|
|
128
|
+
advisory.findings.push(...evaluateClaCheck({ consentPhrase: gate.claConsentPhrase, checkRunName: gate.claCheckRunName }, { body: syntheticPr.body, checkRunConclusion: undefined }));
|
|
129
|
+
}
|
|
130
|
+
// Focus-manifest path policy parity (#12): the LIVE gate (manifestPolicyGateMode) pushes the three enforceable
|
|
131
|
+
// policy findings over the PR's changed paths. Mirror it when the caller supplied paths and the PUBLIC config
|
|
132
|
+
// opts in — recompute the guidance and append ONLY the policy codes, then thread manifestPolicyGateMode into
|
|
133
|
+
// evaluateGateCheck below so block-mode blocks (advisory stays a warning). Without paths, this is skipped.
|
|
134
|
+
if (hasChangedPaths && gate.manifestPolicy !== null && gate.manifestPolicy !== "off") {
|
|
135
|
+
const guidance = buildFocusManifestGuidance({
|
|
136
|
+
manifest,
|
|
137
|
+
changedPaths,
|
|
138
|
+
labels: syntheticPr.labels,
|
|
139
|
+
linkedIssueCount: syntheticPr.linkedIssues.length,
|
|
140
|
+
testFileCount: changedPaths.filter((path) => isTestPath(path)).length,
|
|
141
|
+
// Parity with the live gate (queue/processors.ts's manifestPolicyGateMode block): the predictor
|
|
142
|
+
// already has the same PR body available via input.body, so a manifest_missing_tests prediction must
|
|
143
|
+
// not stay stuck at "no validation evidence" when the real gate would already treat the body as evidence.
|
|
144
|
+
passedValidationCount: hasValidationNote(input.body ?? "") ? 1 : 0,
|
|
145
|
+
});
|
|
146
|
+
const policyCodes = new Set(["manifest_linked_issue_required", "manifest_missing_tests"]);
|
|
147
|
+
for (const finding of guidance.findings) {
|
|
148
|
+
if (!policyCodes.has(finding.code))
|
|
149
|
+
continue;
|
|
150
|
+
advisory.findings.push({
|
|
151
|
+
code: finding.code,
|
|
152
|
+
severity: finding.severity,
|
|
153
|
+
title: finding.title,
|
|
154
|
+
detail: finding.detail,
|
|
155
|
+
/* v8 ignore next -- the three policy findings always carry an action; the no-action arm is unreachable here. */
|
|
156
|
+
...(finding.action !== undefined ? { action: finding.action } : {}),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Pack-aware (#693): under `oss-anti-slop` the gate blocks ANY author, so drop the confirmed-contributor
|
|
161
|
+
// gate entirely (mirrors gateCheckPolicy). `gittensor` keeps it. Pack comes from the PUBLIC .gittensory.yml.
|
|
162
|
+
const pack = gate.pack ?? "gittensor";
|
|
163
|
+
const effectiveConfirmedContributor = pack === "oss-anti-slop" ? undefined : args.confirmedContributor;
|
|
164
|
+
// Case-insensitive author match so the PREDICTOR agrees with the live gate (which matches case-insensitively).
|
|
165
|
+
// First-time grace is retained as compatibility context, but blocker findings are no longer softened by it.
|
|
166
|
+
const contributorLoginLc = input.contributorLogin?.toLowerCase();
|
|
167
|
+
const authorHistory = pullRequests.filter((pr) => sameRepoFullName(pr.repoFullName, input.repoFullName) && pr.authorLogin?.toLowerCase() === contributorLoginLc);
|
|
168
|
+
const hardGuardrailGlobs = resolveHardGuardrailGlobs(manifest.settings);
|
|
169
|
+
const evaluation = evaluateGateCheck(advisory, {
|
|
170
|
+
linkedIssueGateMode: gate.linkedIssue ?? undefined,
|
|
171
|
+
duplicatePrGateMode: gate.duplicates ?? undefined,
|
|
172
|
+
qualityGateMode: gate.readinessMode ?? undefined,
|
|
173
|
+
qualityGateMinScore: gate.readinessMinScore ?? null,
|
|
174
|
+
aiReviewGateMode: gate.aiReviewMode ?? undefined,
|
|
175
|
+
aiReviewCloseConfidence: gate.aiReviewCloseConfidence ?? null,
|
|
176
|
+
mergeReadinessGateMode: gate.mergeReadiness ?? undefined,
|
|
177
|
+
// #12: only meaningful when changed paths were supplied (the policy findings are pushed above only then);
|
|
178
|
+
// absent paths ⇒ no manifest finding exists, so this mode has nothing to act on (byte-identical).
|
|
179
|
+
manifestPolicyGateMode: gate.manifestPolicy ?? undefined,
|
|
180
|
+
selfAuthoredLinkedIssueGateMode: gate.selfAuthoredLinkedIssue ?? undefined,
|
|
181
|
+
// #2564: only meaningful when the finding was pushed above (gate.claMode opted in); byte-identical otherwise.
|
|
182
|
+
claGateMode: gate.claMode ?? undefined,
|
|
183
|
+
readinessScore: readiness.total,
|
|
184
|
+
confirmedContributor: effectiveConfirmedContributor,
|
|
185
|
+
firstTimeContributorGrace: gate.firstTimeContributorGrace ?? undefined,
|
|
186
|
+
authorMergedPrCount: authorHistory.filter((pr) => pr.state === "merged" || pr.mergedAt).length,
|
|
187
|
+
authorClosedUnmergedPrCount: authorHistory.filter((pr) => pr.state === "closed" && !pr.mergedAt).length,
|
|
188
|
+
// Size-hold + guardrail-hold parity (#2458): only meaningful when changed paths were supplied — changedPaths
|
|
189
|
+
// is the only size/guardrail input this metadata-only predictor ever receives, so without it neither can be
|
|
190
|
+
// evaluated (byte-identical to before). changedLineCount is deliberately left unset: line-diff stats are
|
|
191
|
+
// never sent to this predictor, so the size hold can only be predicted from file count (disclosed in the
|
|
192
|
+
// note above) — never claim a line count this function has no way to know.
|
|
193
|
+
sizeGateMode: gate.sizeMode ?? undefined,
|
|
194
|
+
...(hasChangedPaths
|
|
195
|
+
? {
|
|
196
|
+
changedFileCount: changedPaths.length,
|
|
197
|
+
guardrailHit: isGuardrailHit(changedPaths, hardGuardrailGlobs),
|
|
198
|
+
guardrailMatches: guardrailPathMatches(changedPaths, hardGuardrailGlobs),
|
|
199
|
+
}
|
|
200
|
+
: {}),
|
|
201
|
+
});
|
|
202
|
+
return {
|
|
203
|
+
predicted: true,
|
|
204
|
+
basis: "public_config",
|
|
205
|
+
pack,
|
|
206
|
+
conclusion: evaluation.conclusion,
|
|
207
|
+
title: sanitizePublicComment(evaluation.title),
|
|
208
|
+
summary: sanitizePublicComment(evaluation.summary),
|
|
209
|
+
// #2349: applied strictly downstream of `evaluation` (already finalized above) -- personalization can
|
|
210
|
+
// only nudge this number, never the blockers/conclusion/warnings that were just computed.
|
|
211
|
+
readinessScore: applyContributorCalibration(readiness.total, args.contributorCalibration),
|
|
212
|
+
confirmedContributor: effectiveConfirmedContributor,
|
|
213
|
+
blockers: evaluation.blockers.map((finding) => publicSafeFinding(finding)),
|
|
214
|
+
warnings: evaluation.warnings.map((finding) => publicSafeFinding(finding)),
|
|
215
|
+
funnel: pack === "oss-anti-slop" ? { ...OSS_ANTI_SLOP_FUNNEL } : null,
|
|
216
|
+
note: predictedGateNote(hasChangedPaths),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const PROMPT_PACKET_REDACTED_TERM = "[redacted]";
|
|
2
|
+
export declare const PROMPT_PACKET_REDACTED_PATH = "<local-path>";
|
|
3
|
+
/** The four free-text fields the analyze prompt packet exposes to a coding agent. */
|
|
4
|
+
export type PromptPacketTextField = "taskBrief" | "feasibilityNotes" | "retrievalContext" | "constraints";
|
|
5
|
+
export declare const PROMPT_PACKET_TEXT_FIELDS: readonly PromptPacketTextField[];
|
|
6
|
+
export type PromptPacketInput = Record<PromptPacketTextField, string>;
|
|
7
|
+
export type PromptPacket = PromptPacketInput;
|
|
8
|
+
/** Scrub unsafe economic/identity terms and absolute local paths from one packet field. */
|
|
9
|
+
export declare function sanitizePromptPacketField(value: string): string;
|
|
10
|
+
/** Build a public-safe analyze prompt packet from metadata-only inputs. Clean fields pass through byte-identical; unsafe terms and local paths are redacted. */
|
|
11
|
+
export declare function buildPromptPacket(input: PromptPacketInput): PromptPacket;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Metadata-only prompt-packet builder (#2321): four analyze-phase text fields scrubbed with the same PUBLIC_UNSAFE_TERMS / PUBLIC_LOCAL_PATH_INLINE vocabulary as src/signals/redaction.ts (duplicated here so gittensory-engine stays standalone).
|
|
2
|
+
/** Canonical economic/identity term vocabulary (alternation source only — mirrors `PUBLIC_UNSAFE_TERMS`). */
|
|
3
|
+
const PUBLIC_UNSAFE_TERMS = String.raw `(?:reward|score|wallet|hotkey|coldkey|mnemonic|payout|ranking|cohort)\w*|miner[-_\s]?originated|human[-_\s]?originated|farming|raw[-_\s]?trust|trust[-_\s]?score|private[-_\s]?reviewability|reviewability`;
|
|
4
|
+
/** Canonical local-filesystem-root vocabulary (alternation source only — mirrors `PUBLIC_LOCAL_PATH_INLINE`). */
|
|
5
|
+
const PUBLIC_LOCAL_PATH_INLINE = String.raw `/Users/|/home/|/root/|/var/|/opt/|/tmp/|/private/|[A-Za-z]:[\\/]Users[\\/]|[A-Za-z]:[\\/]Program Files[\\/]`;
|
|
6
|
+
const UNSAFE_TERM_SCRUB = new RegExp(String.raw `\b(${PUBLIC_UNSAFE_TERMS})\b`, "gi");
|
|
7
|
+
const LOCAL_PATH_SCRUB = new RegExp(String.raw `(?:${PUBLIC_LOCAL_PATH_INLINE})[^\s"',;)]*`, "gi");
|
|
8
|
+
export const PROMPT_PACKET_REDACTED_TERM = "[redacted]";
|
|
9
|
+
export const PROMPT_PACKET_REDACTED_PATH = "<local-path>";
|
|
10
|
+
export const PROMPT_PACKET_TEXT_FIELDS = Object.freeze([
|
|
11
|
+
"taskBrief",
|
|
12
|
+
"feasibilityNotes",
|
|
13
|
+
"retrievalContext",
|
|
14
|
+
"constraints",
|
|
15
|
+
]);
|
|
16
|
+
function emptyPromptPacketInput() {
|
|
17
|
+
return {
|
|
18
|
+
taskBrief: "",
|
|
19
|
+
feasibilityNotes: "",
|
|
20
|
+
retrievalContext: "",
|
|
21
|
+
constraints: "",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Scrub unsafe economic/identity terms and absolute local paths from one packet field. */
|
|
25
|
+
export function sanitizePromptPacketField(value) {
|
|
26
|
+
return value.replace(LOCAL_PATH_SCRUB, PROMPT_PACKET_REDACTED_PATH).replace(UNSAFE_TERM_SCRUB, PROMPT_PACKET_REDACTED_TERM);
|
|
27
|
+
}
|
|
28
|
+
/** Build a public-safe analyze prompt packet from metadata-only inputs. Clean fields pass through byte-identical; unsafe terms and local paths are redacted. */
|
|
29
|
+
export function buildPromptPacket(input) {
|
|
30
|
+
const packet = emptyPromptPacketInput();
|
|
31
|
+
for (const field of PROMPT_PACKET_TEXT_FIELDS)
|
|
32
|
+
packet[field] = sanitizePromptPacketField(input[field]);
|
|
33
|
+
return packet;
|
|
34
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OpportunityRankInput } from "./opportunity-ranker.js";
|
|
2
|
+
/**
|
|
3
|
+
* Return the highest-scoring candidate at or above `minScore`, or `null` when none qualify.
|
|
4
|
+
* Non-finite thresholds return `null`. Pure — delegates to {@link rankOpportunitiesAtOrAboveScore}.
|
|
5
|
+
*/
|
|
6
|
+
export declare function bestRankedOpportunityAtOrAboveScore<T>(candidates: Array<T & OpportunityRankInput>, minScore: number): (Omit<T, "rankScore"> & OpportunityRankInput & {
|
|
7
|
+
rankScore: number;
|
|
8
|
+
}) | null;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { rankOpportunitiesAtOrAboveScore } from "./ranked-opportunity-min-score.js";
|
|
2
|
+
/**
|
|
3
|
+
* Return the highest-scoring candidate at or above `minScore`, or `null` when none qualify.
|
|
4
|
+
* Non-finite thresholds return `null`. Pure — delegates to {@link rankOpportunitiesAtOrAboveScore}.
|
|
5
|
+
*/
|
|
6
|
+
export function bestRankedOpportunityAtOrAboveScore(candidates, minScore) {
|
|
7
|
+
const survivors = rankOpportunitiesAtOrAboveScore(candidates, minScore);
|
|
8
|
+
return survivors[0] ?? null;
|
|
9
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type OpportunityRankInput } from "./opportunity-ranker.js";
|
|
2
|
+
/**
|
|
3
|
+
* Return the highest-scoring ranked candidate, or `null` when the list is empty.
|
|
4
|
+
* Pure — delegates to {@link rankOpportunities} for scoring and tie-breaking.
|
|
5
|
+
*/
|
|
6
|
+
export declare function bestRankedOpportunity<T>(candidates: Array<T & OpportunityRankInput>): (Omit<T, "rankScore"> & OpportunityRankInput & {
|
|
7
|
+
rankScore: number;
|
|
8
|
+
}) | null;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { rankOpportunities } from "./opportunity-ranker.js";
|
|
2
|
+
/**
|
|
3
|
+
* Return the highest-scoring ranked candidate, or `null` when the list is empty.
|
|
4
|
+
* Pure — delegates to {@link rankOpportunities} for scoring and tie-breaking.
|
|
5
|
+
*/
|
|
6
|
+
export function bestRankedOpportunity(candidates) {
|
|
7
|
+
const ranked = rankOpportunities(candidates);
|
|
8
|
+
return ranked[0] ?? null;
|
|
9
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type OpportunityRankInput } from "./opportunity-ranker.js";
|
|
2
|
+
/**
|
|
3
|
+
* Rank candidates and keep only those whose {@link rankOpportunityScore} is at or above `minScore`.
|
|
4
|
+
* Non-finite thresholds return an empty list. Pure — delegates ordering to {@link rankOpportunities}.
|
|
5
|
+
*/
|
|
6
|
+
export declare function rankOpportunitiesAtOrAboveScore<T>(candidates: Array<T & OpportunityRankInput>, minScore: number): Array<Omit<T, "rankScore"> & OpportunityRankInput & {
|
|
7
|
+
rankScore: number;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { rankOpportunities } from "./opportunity-ranker.js";
|
|
2
|
+
/**
|
|
3
|
+
* Rank candidates and keep only those whose {@link rankOpportunityScore} is at or above `minScore`.
|
|
4
|
+
* Non-finite thresholds return an empty list. Pure — delegates ordering to {@link rankOpportunities}.
|
|
5
|
+
*/
|
|
6
|
+
export function rankOpportunitiesAtOrAboveScore(candidates, minScore) {
|
|
7
|
+
if (!Number.isFinite(minScore))
|
|
8
|
+
return [];
|
|
9
|
+
if (candidates.length === 0)
|
|
10
|
+
return [];
|
|
11
|
+
const threshold = Math.min(1, Math.max(0, minScore));
|
|
12
|
+
return rankOpportunities(candidates).filter((entry) => entry.rankScore >= threshold);
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OpportunityRankInput } from "./opportunity-ranker.js";
|
|
2
|
+
/**
|
|
3
|
+
* Rank candidates, drop entries below `minScore`, and return the top `limit` survivors.
|
|
4
|
+
* Non-finite limits return an empty list. Pure — delegates to {@link rankOpportunitiesAtOrAboveScore}.
|
|
5
|
+
*/
|
|
6
|
+
export declare function pickTopRankedOpportunitiesAtOrAboveScore<T>(candidates: Array<T & OpportunityRankInput>, minScore: number, limit: number): Array<Omit<T, "rankScore"> & OpportunityRankInput & {
|
|
7
|
+
rankScore: number;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { rankOpportunitiesAtOrAboveScore } from "./ranked-opportunity-min-score.js";
|
|
2
|
+
/**
|
|
3
|
+
* Rank candidates, drop entries below `minScore`, and return the top `limit` survivors.
|
|
4
|
+
* Non-finite limits return an empty list. Pure — delegates to {@link rankOpportunitiesAtOrAboveScore}.
|
|
5
|
+
*/
|
|
6
|
+
export function pickTopRankedOpportunitiesAtOrAboveScore(candidates, minScore, limit) {
|
|
7
|
+
if (!Number.isFinite(limit))
|
|
8
|
+
return [];
|
|
9
|
+
const safeLimit = Math.max(0, Math.trunc(limit));
|
|
10
|
+
if (safeLimit === 0)
|
|
11
|
+
return [];
|
|
12
|
+
return rankOpportunitiesAtOrAboveScore(candidates, minScore).slice(0, safeLimit);
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AdvisoryAiRoutingConfig } from "../types/manifest-deps-types.js";
|
|
2
|
+
export declare const DEFAULT_ADVISORY_AI_ROUTING: AdvisoryAiRoutingConfig;
|
|
3
|
+
/**
|
|
4
|
+
* Normalize a raw `.gittensory.yml settings.advisoryAiRouting` value into a typed config, fail-safe: any
|
|
5
|
+
* malformed field falls back to its own (false) default and pushes a warning rather than rejecting the
|
|
6
|
+
* whole block. Mirrors `normalizeUnlinkedIssueGuardrailConfig`'s per-field discipline.
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeAdvisoryAiRoutingConfig(input: unknown, warnings: string[]): AdvisoryAiRoutingConfig;
|