@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,65 @@
|
|
|
1
|
+
import { DOCUMENTED_CALIBRATION_BASELINE, } from "./phase7-calibration-loop.js";
|
|
2
|
+
/** A whole-number percentage, or an em dash when there is no signal yet. */
|
|
3
|
+
function formatPercent(value) {
|
|
4
|
+
return value === null ? "—" : `${Math.round(value * 100)}%`;
|
|
5
|
+
}
|
|
6
|
+
/** A signed percentage-point delta (e.g. "+6pts" / "-4pts"), or an em dash when unknown. */
|
|
7
|
+
function formatDeltaPoints(value) {
|
|
8
|
+
if (value === null)
|
|
9
|
+
return "—";
|
|
10
|
+
const points = Math.round(value * 100);
|
|
11
|
+
return `${points >= 0 ? "+" : ""}${points}pts`;
|
|
12
|
+
}
|
|
13
|
+
function sourceRow(label, metric) {
|
|
14
|
+
return {
|
|
15
|
+
label,
|
|
16
|
+
value: formatPercent(metric.accuracy),
|
|
17
|
+
detail: `n=${metric.sampleSize} · ${metric.fresh ? "fresh" : "stale"}`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Classify the overall calibration state for the dashboard's headline banner. */
|
|
21
|
+
export function resolveCalibrationDashboardStatus(result) {
|
|
22
|
+
if (!result.enabled)
|
|
23
|
+
return "disabled";
|
|
24
|
+
if (result.combinedAccuracy === null)
|
|
25
|
+
return "insufficient_signal";
|
|
26
|
+
return result.combinedAccuracy >= result.baselineAccuracy ? "on_track" : "below_baseline";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Project a computed {@link Phase7CalibrationLoopResult} into a read-only dashboard view. Pure and deterministic;
|
|
30
|
+
* adds no computation of its own. `holdReasons` are surfaced verbatim so an operator can see why an autonomy
|
|
31
|
+
* increase is (or isn't) permitted.
|
|
32
|
+
*/
|
|
33
|
+
export function buildCalibrationDashboardView(result) {
|
|
34
|
+
const status = resolveCalibrationDashboardStatus(result);
|
|
35
|
+
const rows = [
|
|
36
|
+
{
|
|
37
|
+
label: "Combined accuracy",
|
|
38
|
+
value: formatPercent(result.combinedAccuracy),
|
|
39
|
+
detail: `baseline ${formatPercent(result.baselineAccuracy)}`,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: "Delta from baseline",
|
|
43
|
+
value: formatDeltaPoints(result.deltaFromBaseline),
|
|
44
|
+
detail: `documented baseline ${formatPercent(DOCUMENTED_CALIBRATION_BASELINE)}`,
|
|
45
|
+
},
|
|
46
|
+
sourceRow("Historical replay", result.bySource.historical_replay),
|
|
47
|
+
sourceRow("PR outcome", result.bySource.pr_outcome),
|
|
48
|
+
{
|
|
49
|
+
label: "Replay harness",
|
|
50
|
+
value: result.replayHarnessStatus,
|
|
51
|
+
detail: result.replayHarnessHold ? "hold" : "ok",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: "Autonomy increase",
|
|
55
|
+
value: result.autonomyIncreasePermitted ? "permitted" : "held",
|
|
56
|
+
detail: result.replayRunDue ? "replay run due" : "up to date",
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
const headline = status === "disabled"
|
|
60
|
+
? "Calibration loop disabled"
|
|
61
|
+
: status === "insufficient_signal"
|
|
62
|
+
? "Insufficient signal to score calibration yet"
|
|
63
|
+
: `${formatPercent(result.combinedAccuracy)} combined (${formatDeltaPoints(result.deltaFromBaseline)} vs baseline)`;
|
|
64
|
+
return { status, headline, rows, holdReasons: [...result.holdReasons] };
|
|
65
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type Phase7CalibrationLoopResult } from "./phase7-calibration-loop.js";
|
|
2
|
+
/** One accumulated point in the calibration history: when it was computed and its combined accuracy. */
|
|
3
|
+
export type CalibrationTrendSnapshot = {
|
|
4
|
+
observedAt: string;
|
|
5
|
+
combinedAccuracy: number | null;
|
|
6
|
+
baselineAccuracy: number;
|
|
7
|
+
};
|
|
8
|
+
export type CalibrationTrendDirection = "improving" | "degrading" | "flat" | "insufficient";
|
|
9
|
+
/** One rendered point on the trend line: a data-bearing snapshot with its delta vs the baseline. */
|
|
10
|
+
export type CalibrationTrendPoint = {
|
|
11
|
+
observedAt: string;
|
|
12
|
+
combinedAccuracy: number;
|
|
13
|
+
/** Whole percentage-point delta vs that snapshot's baseline (e.g. +6 / -4). */
|
|
14
|
+
deltaFromBaseline: number;
|
|
15
|
+
aboveBaseline: boolean;
|
|
16
|
+
};
|
|
17
|
+
/** The read-only trend projection of a calibration-snapshot series. */
|
|
18
|
+
export type CalibrationTrendView = {
|
|
19
|
+
direction: CalibrationTrendDirection;
|
|
20
|
+
headline: string;
|
|
21
|
+
/** Unicode sparkline of the data points' combined accuracy, normalized across the window. */
|
|
22
|
+
sparkline: string;
|
|
23
|
+
points: readonly CalibrationTrendPoint[];
|
|
24
|
+
latestAccuracy: number | null;
|
|
25
|
+
/** Latest minus earliest combined accuracy, in whole percentage points; null with < 2 data points. */
|
|
26
|
+
changeOverWindow: number | null;
|
|
27
|
+
/** Snapshots that carry a combined accuracy (an install still warming up contributes none). */
|
|
28
|
+
sampleCount: number;
|
|
29
|
+
baselineAccuracy: number;
|
|
30
|
+
};
|
|
31
|
+
/** Derive a trend snapshot from a computed calibration-loop result observed at a given time. */
|
|
32
|
+
export declare function calibrationSnapshotFromResult(result: Phase7CalibrationLoopResult, observedAt: string): CalibrationTrendSnapshot;
|
|
33
|
+
/**
|
|
34
|
+
* Project an accumulated series of calibration snapshots into a read-only trend view. Pure and
|
|
35
|
+
* deterministic. Snapshots without a combined accuracy (an install still warming up) are dropped from the
|
|
36
|
+
* trend line; if fewer than two data points remain, the view reports an explicit "insufficient" state
|
|
37
|
+
* instead of a misleading flat line. `baselineAccuracy` is taken from the most recent snapshot (or the
|
|
38
|
+
* documented default when there is no history yet).
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildCalibrationTrendView(snapshots: readonly CalibrationTrendSnapshot[]): CalibrationTrendView;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { DOCUMENTED_CALIBRATION_BASELINE } from "./phase7-calibration-loop.js";
|
|
2
|
+
const SPARK_TICKS = "▁▂▃▄▅▆▇█";
|
|
3
|
+
/** Derive a trend snapshot from a computed calibration-loop result observed at a given time. */
|
|
4
|
+
export function calibrationSnapshotFromResult(result, observedAt) {
|
|
5
|
+
return { observedAt, combinedAccuracy: result.combinedAccuracy, baselineAccuracy: result.baselineAccuracy };
|
|
6
|
+
}
|
|
7
|
+
function percentPoints(value) {
|
|
8
|
+
return Math.round(value * 100);
|
|
9
|
+
}
|
|
10
|
+
function formatPercent(value) {
|
|
11
|
+
return `${percentPoints(value)}%`;
|
|
12
|
+
}
|
|
13
|
+
function formatDeltaPoints(points) {
|
|
14
|
+
return `${points >= 0 ? "+" : ""}${points}pts`;
|
|
15
|
+
}
|
|
16
|
+
/** Map data-point accuracies onto sparkline ticks, normalized across the window's own min..max. */
|
|
17
|
+
function sparkline(values) {
|
|
18
|
+
if (values.length === 0)
|
|
19
|
+
return "";
|
|
20
|
+
const min = Math.min(...values);
|
|
21
|
+
const span = Math.max(...values) - min;
|
|
22
|
+
return values
|
|
23
|
+
.map((v) => SPARK_TICKS[span === 0 ? 0 : Math.round(((v - min) / span) * (SPARK_TICKS.length - 1))])
|
|
24
|
+
.join("");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Project an accumulated series of calibration snapshots into a read-only trend view. Pure and
|
|
28
|
+
* deterministic. Snapshots without a combined accuracy (an install still warming up) are dropped from the
|
|
29
|
+
* trend line; if fewer than two data points remain, the view reports an explicit "insufficient" state
|
|
30
|
+
* instead of a misleading flat line. `baselineAccuracy` is taken from the most recent snapshot (or the
|
|
31
|
+
* documented default when there is no history yet).
|
|
32
|
+
*/
|
|
33
|
+
export function buildCalibrationTrendView(snapshots) {
|
|
34
|
+
const baselineAccuracy = snapshots.length === 0 ? DOCUMENTED_CALIBRATION_BASELINE : snapshots[snapshots.length - 1].baselineAccuracy;
|
|
35
|
+
const points = snapshots
|
|
36
|
+
.filter((s) => s.combinedAccuracy !== null)
|
|
37
|
+
.map((s) => ({
|
|
38
|
+
observedAt: s.observedAt,
|
|
39
|
+
combinedAccuracy: s.combinedAccuracy,
|
|
40
|
+
deltaFromBaseline: percentPoints(s.combinedAccuracy - s.baselineAccuracy),
|
|
41
|
+
aboveBaseline: s.combinedAccuracy >= s.baselineAccuracy,
|
|
42
|
+
}));
|
|
43
|
+
const sampleCount = points.length;
|
|
44
|
+
const spark = sparkline(points.map((p) => p.combinedAccuracy));
|
|
45
|
+
const latestAccuracy = sampleCount === 0 ? null : points[sampleCount - 1].combinedAccuracy;
|
|
46
|
+
if (sampleCount < 2) {
|
|
47
|
+
const headline = sampleCount === 0
|
|
48
|
+
? "No calibration history yet"
|
|
49
|
+
: `Insufficient history: 1 snapshot (${formatPercent(points[0].combinedAccuracy)})`;
|
|
50
|
+
return { direction: "insufficient", headline, sparkline: spark, points, latestAccuracy, changeOverWindow: null, sampleCount, baselineAccuracy };
|
|
51
|
+
}
|
|
52
|
+
const earliest = points[0].combinedAccuracy;
|
|
53
|
+
const latest = points[sampleCount - 1].combinedAccuracy;
|
|
54
|
+
const changeOverWindow = percentPoints(latest - earliest);
|
|
55
|
+
const direction = changeOverWindow > 0 ? "improving" : changeOverWindow < 0 ? "degrading" : "flat";
|
|
56
|
+
const headline = direction === "flat"
|
|
57
|
+
? `Flat at ${formatPercent(latest)} over ${sampleCount} snapshots`
|
|
58
|
+
: `${direction === "improving" ? "Improving" : "Degrading"}: ${formatPercent(earliest)} → ${formatPercent(latest)} over ${sampleCount} snapshots (${formatDeltaPoints(changeOverWindow)})`;
|
|
59
|
+
return { direction, headline, sparkline: spark, points, latestAccuracy, changeOverWindow, sampleCount, baselineAccuracy };
|
|
60
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type ContributorFit = "strong" | "neutral" | "weak";
|
|
2
|
+
export type ContributorFitCheck = {
|
|
3
|
+
fit: ContributorFit;
|
|
4
|
+
reasons: string[];
|
|
5
|
+
};
|
|
6
|
+
export type ContributorFitProfile = {
|
|
7
|
+
login: string;
|
|
8
|
+
registeredRepoActivity: {
|
|
9
|
+
pullRequests: number;
|
|
10
|
+
mergedPullRequests: number;
|
|
11
|
+
reposTouched: readonly string[];
|
|
12
|
+
};
|
|
13
|
+
trustSignals: {
|
|
14
|
+
level: "new" | "emerging" | "established";
|
|
15
|
+
unlinkedOpenPullRequests: number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* A profile with no prior activity on the target repo is `neutral` (a first attempt is not evidence
|
|
20
|
+
* of poor fit). With prior activity, a personal burden signal (unlinked open PRs, or a `new` profile
|
|
21
|
+
* with a low merge ratio) reads `weak`; established trust with a clean queue and a high merge ratio
|
|
22
|
+
* reads `strong`; anything between reads `neutral`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function classifyContributorFit(profile: ContributorFitProfile, targetRepoFullName: string): ContributorFitCheck;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Contributor fit sub-check (#2312): pure classifier over an already-computed contributor profile
|
|
2
|
+
// that answers whether THIS contributor's own track record fits a target repo. No IO, no fetching.
|
|
3
|
+
/**
|
|
4
|
+
* A profile with no prior activity on the target repo is `neutral` (a first attempt is not evidence
|
|
5
|
+
* of poor fit). With prior activity, a personal burden signal (unlinked open PRs, or a `new` profile
|
|
6
|
+
* with a low merge ratio) reads `weak`; established trust with a clean queue and a high merge ratio
|
|
7
|
+
* reads `strong`; anything between reads `neutral`.
|
|
8
|
+
*/
|
|
9
|
+
export function classifyContributorFit(profile, targetRepoFullName) {
|
|
10
|
+
const { reposTouched, pullRequests, mergedPullRequests } = profile.registeredRepoActivity;
|
|
11
|
+
const target = targetRepoFullName.toLowerCase();
|
|
12
|
+
if (!reposTouched.some((repo) => repo.toLowerCase() === target)) {
|
|
13
|
+
return {
|
|
14
|
+
fit: "neutral",
|
|
15
|
+
reasons: [`No prior activity on ${targetRepoFullName}; a first attempt is not evidence of poor fit.`],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const reasons = [];
|
|
19
|
+
const mergeRatio = pullRequests > 0 ? mergedPullRequests / pullRequests : 1;
|
|
20
|
+
const unlinked = profile.trustSignals.unlinkedOpenPullRequests;
|
|
21
|
+
const level = profile.trustSignals.level;
|
|
22
|
+
const hasUnlinkedBurden = unlinked > 0;
|
|
23
|
+
const hasNewProfileBurden = level === "new" && pullRequests > 0 && mergeRatio < 0.5;
|
|
24
|
+
if (hasUnlinkedBurden)
|
|
25
|
+
reasons.push(`${unlinked} unlinked open pull request(s)`);
|
|
26
|
+
if (hasNewProfileBurden) {
|
|
27
|
+
reasons.push(`new profile with low merge ratio (${mergedPullRequests}/${pullRequests})`);
|
|
28
|
+
}
|
|
29
|
+
const cleanQueue = unlinked === 0;
|
|
30
|
+
const strongMerge = pullRequests > 0 && mergeRatio >= 0.8;
|
|
31
|
+
if (level === "established")
|
|
32
|
+
reasons.push("trust level is 'established'");
|
|
33
|
+
if (cleanQueue)
|
|
34
|
+
reasons.push("no unlinked open pull requests");
|
|
35
|
+
if (strongMerge)
|
|
36
|
+
reasons.push(`strong merge ratio (${mergedPullRequests}/${pullRequests})`);
|
|
37
|
+
const burdened = hasUnlinkedBurden || hasNewProfileBurden;
|
|
38
|
+
const proven = level === "established" && cleanQueue && strongMerge;
|
|
39
|
+
let fit;
|
|
40
|
+
if (burdened) {
|
|
41
|
+
fit = "weak";
|
|
42
|
+
}
|
|
43
|
+
else if (proven) {
|
|
44
|
+
fit = "strong";
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
fit = "neutral";
|
|
48
|
+
}
|
|
49
|
+
return { fit, reasons };
|
|
50
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export declare const DISCOVERY_INDEX_CONTRACT_VERSION = 1;
|
|
2
|
+
/** Query scope for a discovery-index request: which repos/orgs/search-terms to fan out over, plus pagination. */
|
|
3
|
+
export type DiscoveryIndexQuery = {
|
|
4
|
+
/** Canonical `owner/repo` targets. */
|
|
5
|
+
repos: readonly string[];
|
|
6
|
+
/** Bare `owner` (org/user) targets — every open issue across the owner's repos. */
|
|
7
|
+
orgs: readonly string[];
|
|
8
|
+
/** Free-text GitHub issue-search terms. */
|
|
9
|
+
searchTerms: readonly string[];
|
|
10
|
+
/** Page size, clamped to [1, 200]. Default 50. */
|
|
11
|
+
limit: number;
|
|
12
|
+
/** Opaque forward pagination cursor from a previous response's `nextCursor`, or null for the first page. */
|
|
13
|
+
cursor: string | null;
|
|
14
|
+
};
|
|
15
|
+
export type DiscoveryIndexRequest = {
|
|
16
|
+
contractVersion: number;
|
|
17
|
+
query: DiscoveryIndexQuery;
|
|
18
|
+
};
|
|
19
|
+
export type DiscoveryIndexAiPolicySource = "AI-USAGE.md" | "CONTRIBUTING.md" | "none";
|
|
20
|
+
/** One metadata-only candidate issue. Field-for-field compatible with opportunity-ranker.js `normalizeCandidate`
|
|
21
|
+
* output, so `rankCandidateIssues` consumes hosted results exactly like a local fan-out. Public-safe by contract:
|
|
22
|
+
* no scores/rewards/wallet/hotkey/source fields ever appear here (see {@link DISCOVERY_INDEX_FORBIDDEN_FIELDS}). */
|
|
23
|
+
export type DiscoveryIndexCandidate = {
|
|
24
|
+
owner: string;
|
|
25
|
+
repo: string;
|
|
26
|
+
repoFullName: string;
|
|
27
|
+
issueNumber: number;
|
|
28
|
+
title: string;
|
|
29
|
+
labels: readonly string[];
|
|
30
|
+
commentsCount: number;
|
|
31
|
+
createdAt: string | null;
|
|
32
|
+
updatedAt: string | null;
|
|
33
|
+
htmlUrl: string | null;
|
|
34
|
+
aiPolicyAllowed: boolean;
|
|
35
|
+
aiPolicySource: DiscoveryIndexAiPolicySource;
|
|
36
|
+
};
|
|
37
|
+
export type DiscoveryIndexResponse = {
|
|
38
|
+
contractVersion: number;
|
|
39
|
+
candidates: readonly DiscoveryIndexCandidate[];
|
|
40
|
+
/** Forward cursor for the next page, or null when the result set is exhausted. */
|
|
41
|
+
nextCursor: string | null;
|
|
42
|
+
};
|
|
43
|
+
export type ParsedDiscoveryIndexRequest = {
|
|
44
|
+
request: DiscoveryIndexRequest;
|
|
45
|
+
warnings: string[];
|
|
46
|
+
};
|
|
47
|
+
export type ParsedDiscoveryIndexResponse = {
|
|
48
|
+
response: DiscoveryIndexResponse;
|
|
49
|
+
warnings: string[];
|
|
50
|
+
};
|
|
51
|
+
/** Field-name fragments that must NEVER cross the public discovery boundary (Phase 1 acceptance:
|
|
52
|
+
* cross-repo-discovery-phase1.md:13-14,54). A candidate carrying any of these is rejected, not silently trimmed,
|
|
53
|
+
* so a misbehaving server can't smuggle raw economic/identity/source data past the contract. */
|
|
54
|
+
export declare const DISCOVERY_INDEX_FORBIDDEN_FIELDS: readonly string[];
|
|
55
|
+
/** Owner/repo names of forbidden-field violations present on a raw candidate object (own enumerable keys whose
|
|
56
|
+
* lower-cased name contains a forbidden fragment). Empty array = public-safe. */
|
|
57
|
+
export declare function discoveryIndexBoundaryViolations(raw: unknown): string[];
|
|
58
|
+
/**
|
|
59
|
+
* Tolerantly normalize a raw discovery-index request into a canonical {@link DiscoveryIndexRequest}. Never throws:
|
|
60
|
+
* unknown fields are ignored, malformed scope entries are skipped, and the page limit is clamped, accumulating
|
|
61
|
+
* warnings. A non-object raw yields an empty query.
|
|
62
|
+
*/
|
|
63
|
+
export declare function normalizeDiscoveryIndexRequest(raw: unknown): ParsedDiscoveryIndexRequest;
|
|
64
|
+
/**
|
|
65
|
+
* Normalize one raw candidate into a public-safe {@link DiscoveryIndexCandidate}, mirroring opportunity-ranker.js
|
|
66
|
+
* `normalizeCandidate`. Returns null when required fields are missing/invalid OR when the raw object carries any
|
|
67
|
+
* forbidden boundary field (a public-safety rejection, not a silent trim).
|
|
68
|
+
*/
|
|
69
|
+
export declare function normalizeDiscoveryIndexCandidate(raw: unknown): DiscoveryIndexCandidate | null;
|
|
70
|
+
/**
|
|
71
|
+
* Tolerantly normalize a raw discovery-index response: keep only valid, public-safe candidates (invalid or
|
|
72
|
+
* boundary-violating entries are dropped with a warning), and carry a forward cursor. Never throws.
|
|
73
|
+
*/
|
|
74
|
+
export declare function normalizeDiscoveryIndexResponse(raw: unknown): ParsedDiscoveryIndexResponse;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Discovery-index API contract (#4300). The typed request/response shape a miner uses to query the OPTIONAL hosted
|
|
2
|
+
// discovery-index service (the server side is #4250, maintainer-only, explicitly blocked on this contract). The
|
|
3
|
+
// plane exists to mitigate the rate-limit incident (#1936): one shared GitHub-metadata crawler across the fleet
|
|
4
|
+
// instead of every miner independently hammering the same repos' search/listing endpoints.
|
|
5
|
+
//
|
|
6
|
+
// This module is schema/shape ONLY — no server, no deployed endpoint, no client HTTP. It stays inside the Phase 1
|
|
7
|
+
// boundary (packages/gittensory-miner/docs/cross-repo-discovery-phase1.md): metadata-only, GET/list/search-only,
|
|
8
|
+
// and NO raw scores / rewards / wallet / hotkey data / source contents crossing the public boundary. The response
|
|
9
|
+
// candidate shape deliberately maps onto opportunity-ranker.js's `normalizeCandidate` fields so a miner can swap a
|
|
10
|
+
// local fan-out for a hosted query without the ranker changing. Tolerant-parser convention, mirroring
|
|
11
|
+
// miner-goal-spec.ts / fleet-run-manifest.ts: every field optional, malformed input degrades to a documented
|
|
12
|
+
// default with a warning rather than throwing.
|
|
13
|
+
export const DISCOVERY_INDEX_CONTRACT_VERSION = 1;
|
|
14
|
+
const MAX_QUERY_ITEMS = 200;
|
|
15
|
+
const MAX_PAGE_LIMIT = 200;
|
|
16
|
+
const DEFAULT_PAGE_LIMIT = 50;
|
|
17
|
+
/** Field-name fragments that must NEVER cross the public discovery boundary (Phase 1 acceptance:
|
|
18
|
+
* cross-repo-discovery-phase1.md:13-14,54). A candidate carrying any of these is rejected, not silently trimmed,
|
|
19
|
+
* so a misbehaving server can't smuggle raw economic/identity/source data past the contract. */
|
|
20
|
+
export const DISCOVERY_INDEX_FORBIDDEN_FIELDS = Object.freeze([
|
|
21
|
+
"score",
|
|
22
|
+
"reward",
|
|
23
|
+
"wallet",
|
|
24
|
+
"hotkey",
|
|
25
|
+
"coldkey",
|
|
26
|
+
"mnemonic",
|
|
27
|
+
"payout",
|
|
28
|
+
"ranking",
|
|
29
|
+
"rawtrust",
|
|
30
|
+
"trustscore",
|
|
31
|
+
"sourcecontent",
|
|
32
|
+
"diff",
|
|
33
|
+
"patch",
|
|
34
|
+
]);
|
|
35
|
+
/** Owner/repo names of forbidden-field violations present on a raw candidate object (own enumerable keys whose
|
|
36
|
+
* lower-cased name contains a forbidden fragment). Empty array = public-safe. */
|
|
37
|
+
export function discoveryIndexBoundaryViolations(raw) {
|
|
38
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
39
|
+
return [];
|
|
40
|
+
const violations = [];
|
|
41
|
+
for (const key of Object.keys(raw)) {
|
|
42
|
+
const lower = key.toLowerCase();
|
|
43
|
+
if (DISCOVERY_INDEX_FORBIDDEN_FIELDS.some((fragment) => lower.includes(fragment)))
|
|
44
|
+
violations.push(key);
|
|
45
|
+
}
|
|
46
|
+
return violations;
|
|
47
|
+
}
|
|
48
|
+
function normalizeStringList(value, transform) {
|
|
49
|
+
if (!Array.isArray(value))
|
|
50
|
+
return [];
|
|
51
|
+
const result = [];
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
for (const entry of value) {
|
|
54
|
+
if (typeof entry !== "string")
|
|
55
|
+
continue;
|
|
56
|
+
if (result.length >= MAX_QUERY_ITEMS)
|
|
57
|
+
break;
|
|
58
|
+
const normalized = transform(entry);
|
|
59
|
+
if (normalized === null || seen.has(normalized))
|
|
60
|
+
continue;
|
|
61
|
+
seen.add(normalized);
|
|
62
|
+
result.push(normalized);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
/** `owner/repo` with exactly one slash and non-empty halves; anything else → null (mirrors normalizeCandidate). */
|
|
67
|
+
function normalizeRepoFullName(value) {
|
|
68
|
+
const [owner, repo, extra] = value.trim().split("/");
|
|
69
|
+
if (!owner || !repo || extra !== undefined)
|
|
70
|
+
return null;
|
|
71
|
+
return `${owner}/${repo}`;
|
|
72
|
+
}
|
|
73
|
+
function normalizeOwner(value) {
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
if (!trimmed || trimmed.includes("/"))
|
|
76
|
+
return null;
|
|
77
|
+
return trimmed;
|
|
78
|
+
}
|
|
79
|
+
function normalizeSearchTerm(value) {
|
|
80
|
+
const trimmed = value.trim();
|
|
81
|
+
return trimmed || null;
|
|
82
|
+
}
|
|
83
|
+
function clampLimit(value, warnings) {
|
|
84
|
+
if (value === undefined || value === null)
|
|
85
|
+
return DEFAULT_PAGE_LIMIT;
|
|
86
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
87
|
+
warnings.push(`DiscoveryIndexRequest "limit" must be a number; falling back to ${DEFAULT_PAGE_LIMIT}.`);
|
|
88
|
+
return DEFAULT_PAGE_LIMIT;
|
|
89
|
+
}
|
|
90
|
+
const floored = Math.floor(value);
|
|
91
|
+
if (floored < 1)
|
|
92
|
+
return 1;
|
|
93
|
+
if (floored > MAX_PAGE_LIMIT)
|
|
94
|
+
return MAX_PAGE_LIMIT;
|
|
95
|
+
return floored;
|
|
96
|
+
}
|
|
97
|
+
function normalizeAiPolicySource(value) {
|
|
98
|
+
return value === "AI-USAGE.md" || value === "CONTRIBUTING.md" ? value : "none";
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Tolerantly normalize a raw discovery-index request into a canonical {@link DiscoveryIndexRequest}. Never throws:
|
|
102
|
+
* unknown fields are ignored, malformed scope entries are skipped, and the page limit is clamped, accumulating
|
|
103
|
+
* warnings. A non-object raw yields an empty query.
|
|
104
|
+
*/
|
|
105
|
+
export function normalizeDiscoveryIndexRequest(raw) {
|
|
106
|
+
const warnings = [];
|
|
107
|
+
const record = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
|
|
108
|
+
if (!record) {
|
|
109
|
+
warnings.push("DiscoveryIndexRequest must be a mapping; falling back to an empty query.");
|
|
110
|
+
}
|
|
111
|
+
const source = record ?? {};
|
|
112
|
+
const query = {
|
|
113
|
+
repos: normalizeStringList(source.repos, normalizeRepoFullName),
|
|
114
|
+
orgs: normalizeStringList(source.orgs, normalizeOwner),
|
|
115
|
+
searchTerms: normalizeStringList(source.searchTerms, normalizeSearchTerm),
|
|
116
|
+
limit: clampLimit(source.limit, warnings),
|
|
117
|
+
cursor: typeof source.cursor === "string" && source.cursor.trim() ? source.cursor : null,
|
|
118
|
+
};
|
|
119
|
+
return { request: { contractVersion: DISCOVERY_INDEX_CONTRACT_VERSION, query }, warnings };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Normalize one raw candidate into a public-safe {@link DiscoveryIndexCandidate}, mirroring opportunity-ranker.js
|
|
123
|
+
* `normalizeCandidate`. Returns null when required fields are missing/invalid OR when the raw object carries any
|
|
124
|
+
* forbidden boundary field (a public-safety rejection, not a silent trim).
|
|
125
|
+
*/
|
|
126
|
+
export function normalizeDiscoveryIndexCandidate(raw) {
|
|
127
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
128
|
+
return null;
|
|
129
|
+
if (discoveryIndexBoundaryViolations(raw).length > 0)
|
|
130
|
+
return null;
|
|
131
|
+
const candidate = raw;
|
|
132
|
+
const repoFullName = typeof candidate.repoFullName === "string" ? candidate.repoFullName : "";
|
|
133
|
+
const canonical = normalizeRepoFullName(repoFullName);
|
|
134
|
+
const issueNumber = candidate.issueNumber;
|
|
135
|
+
const title = typeof candidate.title === "string" ? candidate.title.trim() : "";
|
|
136
|
+
if (canonical === null)
|
|
137
|
+
return null;
|
|
138
|
+
if (typeof issueNumber !== "number" || !Number.isInteger(issueNumber) || issueNumber <= 0 || !title)
|
|
139
|
+
return null;
|
|
140
|
+
// `canonical` is guaranteed `owner/repo` with exactly one slash, so slice yields two non-empty strings.
|
|
141
|
+
const slashIndex = canonical.indexOf("/");
|
|
142
|
+
const owner = canonical.slice(0, slashIndex);
|
|
143
|
+
const repo = canonical.slice(slashIndex + 1);
|
|
144
|
+
const labels = Array.isArray(candidate.labels)
|
|
145
|
+
? candidate.labels.filter((label) => typeof label === "string" && label.trim() !== "").map((label) => label.trim())
|
|
146
|
+
: [];
|
|
147
|
+
return {
|
|
148
|
+
owner,
|
|
149
|
+
repo,
|
|
150
|
+
repoFullName: canonical,
|
|
151
|
+
issueNumber,
|
|
152
|
+
title,
|
|
153
|
+
labels,
|
|
154
|
+
commentsCount: typeof candidate.commentsCount === "number" && Number.isFinite(candidate.commentsCount) ? candidate.commentsCount : 0,
|
|
155
|
+
createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : null,
|
|
156
|
+
updatedAt: typeof candidate.updatedAt === "string" ? candidate.updatedAt : null,
|
|
157
|
+
htmlUrl: typeof candidate.htmlUrl === "string" ? candidate.htmlUrl : null,
|
|
158
|
+
aiPolicyAllowed: candidate.aiPolicyAllowed !== false,
|
|
159
|
+
aiPolicySource: normalizeAiPolicySource(candidate.aiPolicySource),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Tolerantly normalize a raw discovery-index response: keep only valid, public-safe candidates (invalid or
|
|
164
|
+
* boundary-violating entries are dropped with a warning), and carry a forward cursor. Never throws.
|
|
165
|
+
*/
|
|
166
|
+
export function normalizeDiscoveryIndexResponse(raw) {
|
|
167
|
+
const warnings = [];
|
|
168
|
+
const record = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
|
|
169
|
+
if (!record) {
|
|
170
|
+
warnings.push("DiscoveryIndexResponse must be a mapping; falling back to an empty candidate list.");
|
|
171
|
+
}
|
|
172
|
+
const rawCandidates = record && Array.isArray(record.candidates) ? record.candidates : [];
|
|
173
|
+
const candidates = [];
|
|
174
|
+
for (const entry of rawCandidates) {
|
|
175
|
+
const normalized = normalizeDiscoveryIndexCandidate(entry);
|
|
176
|
+
if (normalized === null) {
|
|
177
|
+
warnings.push("DiscoveryIndexResponse dropped an invalid or boundary-violating candidate.");
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
candidates.push(normalized);
|
|
181
|
+
}
|
|
182
|
+
const nextCursor = record && typeof record.nextCursor === "string" && record.nextCursor.trim() ? record.nextCursor : null;
|
|
183
|
+
return { response: { contractVersion: DISCOVERY_INDEX_CONTRACT_VERSION, candidates, nextCursor }, warnings };
|
|
184
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** The three local claim-ledger statuses (claim-ledger.js `CLAIM_STATUSES`). */
|
|
2
|
+
export type SoftClaimStatus = "active" | "released" | "expired";
|
|
3
|
+
/** Outbound coordination actions: announce a claim, or announce that a prior claim is released. */
|
|
4
|
+
export type SoftClaimAction = "claim" | "release";
|
|
5
|
+
/** The local claim-ledger record shape (claim-ledger.js `rowToClaim`) this builder reads from. */
|
|
6
|
+
export type SoftClaimRecord = {
|
|
7
|
+
repoFullName: string;
|
|
8
|
+
issueNumber: number;
|
|
9
|
+
claimedAt: string;
|
|
10
|
+
status: SoftClaimStatus;
|
|
11
|
+
note?: string | null;
|
|
12
|
+
};
|
|
13
|
+
/** Optional caller context. Reserved for future public-safe, anonymized coordination metadata. */
|
|
14
|
+
export type SoftClaimRequestContext = {
|
|
15
|
+
instanceId?: string;
|
|
16
|
+
};
|
|
17
|
+
/** The public-safe soft-claim coordination request payload targeting the discovery-index contract. */
|
|
18
|
+
export type SoftClaimRequest = {
|
|
19
|
+
contractVersion: number;
|
|
20
|
+
action: SoftClaimAction;
|
|
21
|
+
repoFullName: string;
|
|
22
|
+
issueNumber: number;
|
|
23
|
+
claimedAt: string;
|
|
24
|
+
note: string | null;
|
|
25
|
+
instanceId: string | null;
|
|
26
|
+
};
|
|
27
|
+
/** `active` announces a `claim`; `released`/`expired` announce a `release`. */
|
|
28
|
+
export declare function softClaimActionForStatus(status: SoftClaimStatus): SoftClaimAction;
|
|
29
|
+
/**
|
|
30
|
+
* Build a public-safe soft-claim coordination request from a local claim-ledger record. Pure and network-free.
|
|
31
|
+
* Returns null when the claim is missing/invalid or carries an unknown status (only the three claim-ledger
|
|
32
|
+
* statuses map to a request). Only the fixed set of known fields is copied onto the request, so the payload stays
|
|
33
|
+
* metadata-only by construction.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildSoftClaimRequest(claim: unknown, _context?: SoftClaimRequestContext): SoftClaimRequest | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { DISCOVERY_INDEX_CONTRACT_VERSION } from "./discovery-index-contract.js";
|
|
2
|
+
/** `active` announces a `claim`; `released`/`expired` announce a `release`. */
|
|
3
|
+
export function softClaimActionForStatus(status) {
|
|
4
|
+
return status === "active" ? "claim" : "release";
|
|
5
|
+
}
|
|
6
|
+
/** `owner/repo` with exactly one slash and non-empty halves; anything else → null (mirrors the discovery-index
|
|
7
|
+
* contract / claim-ledger repo validation). */
|
|
8
|
+
function normalizeRepoFullName(value) {
|
|
9
|
+
if (typeof value !== "string")
|
|
10
|
+
return null;
|
|
11
|
+
const [owner, repo, extra] = value.trim().split("/");
|
|
12
|
+
if (!owner || !repo || extra !== undefined)
|
|
13
|
+
return null;
|
|
14
|
+
return `${owner}/${repo}`;
|
|
15
|
+
}
|
|
16
|
+
function isSoftClaimStatus(value) {
|
|
17
|
+
return value === "active" || value === "released" || value === "expired";
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build a public-safe soft-claim coordination request from a local claim-ledger record. Pure and network-free.
|
|
21
|
+
* Returns null when the claim is missing/invalid or carries an unknown status (only the three claim-ledger
|
|
22
|
+
* statuses map to a request). Only the fixed set of known fields is copied onto the request, so the payload stays
|
|
23
|
+
* metadata-only by construction.
|
|
24
|
+
*/
|
|
25
|
+
export function buildSoftClaimRequest(claim, _context = {}) {
|
|
26
|
+
if (!claim || typeof claim !== "object" || Array.isArray(claim))
|
|
27
|
+
return null;
|
|
28
|
+
const record = claim;
|
|
29
|
+
const repoFullName = normalizeRepoFullName(record.repoFullName);
|
|
30
|
+
if (repoFullName === null)
|
|
31
|
+
return null;
|
|
32
|
+
const issueNumber = record.issueNumber;
|
|
33
|
+
if (typeof issueNumber !== "number" || !Number.isInteger(issueNumber) || issueNumber <= 0)
|
|
34
|
+
return null;
|
|
35
|
+
if (typeof record.claimedAt !== "string" || record.claimedAt.trim() === "")
|
|
36
|
+
return null;
|
|
37
|
+
if (!isSoftClaimStatus(record.status))
|
|
38
|
+
return null;
|
|
39
|
+
return {
|
|
40
|
+
contractVersion: DISCOVERY_INDEX_CONTRACT_VERSION,
|
|
41
|
+
action: softClaimActionForStatus(record.status),
|
|
42
|
+
repoFullName,
|
|
43
|
+
issueNumber,
|
|
44
|
+
claimedAt: record.claimedAt,
|
|
45
|
+
// Local claim-ledger notes and caller instance IDs may contain operator prose or private identifiers. The hosted
|
|
46
|
+
// discovery plane only receives fixed public metadata, so keep both fields empty.
|
|
47
|
+
note: null,
|
|
48
|
+
instanceId: null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duplicate-winner adjudication (#dup-winner). Flag-gated by GITTENSORY_DUPLICATE_WINNER.
|
|
3
|
+
*
|
|
4
|
+
* When several OPEN PRs link the same issue (a duplicate cluster), the legacy behavior gate-blocks +
|
|
5
|
+
* auto-closes EVERY sibling as a duplicate — no winner survives. With the flag ON, exactly ONE winner is
|
|
6
|
+
* spared: the earliest claimant. Sparse legacy rows that do not yet have claim timing fail closed so unknown
|
|
7
|
+
* ordering cannot arbitrarily suppress duplicate evidence. Only the LOSERS are blocked/closed; the winner
|
|
8
|
+
* still must pass CI / conflict / gate / linked-issue / slop on its OWN merits.
|
|
9
|
+
*
|
|
10
|
+
* This module is PURE — no IO, no Date, no random — so the same inputs always yield the same verdict and the
|
|
11
|
+
* caller can compute the winner ONCE per review run and thread the result boolean consistently into every
|
|
12
|
+
* surface (advisory finding, close reason, slop, panels), so they agree by construction.
|
|
13
|
+
*
|
|
14
|
+
* ELECTION ORDER: compare `linkedIssueClaimedAt`, the time gittensory first observed the PR claiming
|
|
15
|
+
* the issue. GitHub `pull_request.created_at` is intentionally not an ordering signal here: contributors can
|
|
16
|
+
* edit an old placeholder PR to add a linked issue later, so creation time would let backdated claims steal
|
|
17
|
+
* duplicate-winner credit from the PR that actually claimed the issue first. Sparse legacy rows that lack
|
|
18
|
+
* claim timing keep failing closed so unknown ordering cannot suppress duplicate evidence.
|
|
19
|
+
*
|
|
20
|
+
* INVARIANT (the caller MUST honor it): {@link openSiblingNumbers} carries OPEN-only sibling PR numbers. The
|
|
21
|
+
* existing sources already exclude closed/merged PRs. Once the winner closes (e.g. red CI), it leaves the open
|
|
22
|
+
* set and the next-earliest OPEN claimant becomes the winner on re-eval — no permanently-orphaned cluster.
|
|
23
|
+
*
|
|
24
|
+
* SECOND CONSUMER (#2278): this module is intentionally engine-hosted (not `src/`-only) because its election
|
|
25
|
+
* logic is reusable for the miner's own soft-claim adjudication — deciding which of several miners claiming
|
|
26
|
+
* the same issue proceeds. A future contributor wiring the miner's local claim ledger should import this
|
|
27
|
+
* module rather than reimplementing the election rule, so both the maintainer gate and the miner agree on
|
|
28
|
+
* exactly one winner by construction.
|
|
29
|
+
*/
|
|
30
|
+
export type DuplicateClaimMember = {
|
|
31
|
+
number: number;
|
|
32
|
+
linkedIssueClaimedAt?: string | null | undefined;
|
|
33
|
+
/** GitHub's true PR creation time. Retained for caller compatibility; not used for winner ordering. */
|
|
34
|
+
createdAt?: string | null | undefined;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* True iff `prNumber` is the cluster winner: the minimum of `{prNumber} ∪ openSiblingNumbers`. An empty
|
|
38
|
+
* sibling list ⇒ the PR is alone in (or out of) the cluster ⇒ winner. A sibling list that happens to contain
|
|
39
|
+
* `prNumber` itself is harmless — the comparison is still min-based.
|
|
40
|
+
*
|
|
41
|
+
* @deprecated Use {@link isDuplicateClusterWinnerByClaim}. PR-number election is retained only for legacy
|
|
42
|
+
* compatibility callers that do not have claim timestamps.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isDuplicateClusterWinner(prNumber: number, openSiblingNumbers: number[]): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* True iff `pr` is the earliest-elected claimant in the open duplicate cluster (see the module doc's
|
|
47
|
+
* "ELECTION ORDER" note). Sparse legacy rows fail closed; ties between equally-ordered members use PR number.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isDuplicateClusterWinnerByClaim(pr: DuplicateClaimMember, openSiblings: DuplicateClaimMember[]): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The winning PR number among `pr` and its open duplicate siblings, or `null` when the election is not
|
|
52
|
+
* determinable (mirrors {@link isDuplicateClusterWinnerByClaim}'s fail-closed semantics — this never guesses a
|
|
53
|
+
* specific winner when the ordering data is too sparse/ambiguous to be sure). Used only for DISPLAY (naming the
|
|
54
|
+
* winner in a loser's close comment, #dup-winner-credit) — the close/hold decision for any given PR is still
|
|
55
|
+
* driven directly by {@link isDuplicateClusterWinnerByClaim}, not by this function's return value.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveDuplicateClusterWinnerNumber(pr: DuplicateClaimMember, openSiblings: DuplicateClaimMember[]): number | null;
|