@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,457 @@
|
|
|
1
|
+
// Opt-in structured finding-severity calibration signal (#1955 calibration family).
|
|
2
|
+
//
|
|
3
|
+
// This module is the pure engine half of finding-severity calibration. The hosted review stack decides whether a
|
|
4
|
+
// repo is currently opted in from its resolved `.gittensory.yml`/private config; the miner replay harness can then
|
|
5
|
+
// ingest only the structured per-severity-tier finding fields exposed here — how many findings the review raised at
|
|
6
|
+
// each severity tier (blocker/warning/advisory/nit) and how many of those were subsequently CONFIRMED (a true
|
|
7
|
+
// positive that pointed at a real, acted-on issue). No raw review text, secrets, trust values, rewards, rankings, or
|
|
8
|
+
// maintainer evidence are represented in this type surface.
|
|
9
|
+
//
|
|
10
|
+
// The score measures how well-CALIBRATED a review's severity assignments are: a confirmed blocker is worth much more
|
|
11
|
+
// than a confirmed nit, and a review that raises blockers which are then dismissed (false positives at the tier that
|
|
12
|
+
// most interrupts a maintainer) calibrates poorly. It composes with the objective-anchor and pairwise-judge signals
|
|
13
|
+
// exactly like the other calibration signals in this family.
|
|
14
|
+
const TIER_ORDER = ["blocker", "warning", "advisory", "nit"];
|
|
15
|
+
// Severity weight for scoring: a confirmed blocker is worth far more than a confirmed nit, and a dismissed blocker
|
|
16
|
+
// (false positive at the most disruptive tier) is penalized far more than a dismissed nit.
|
|
17
|
+
const TIER_WEIGHT = {
|
|
18
|
+
blocker: 1,
|
|
19
|
+
warning: 0.6,
|
|
20
|
+
advisory: 0.3,
|
|
21
|
+
nit: 0.1,
|
|
22
|
+
};
|
|
23
|
+
const DEFAULT_STRUCTURED_FINDING_SEVERITY_WEIGHT = 0.2;
|
|
24
|
+
const DEFAULT_COMPOSITE_WEIGHTS = {
|
|
25
|
+
objectiveAnchor: 0.45,
|
|
26
|
+
pairwiseJudge: 0.35,
|
|
27
|
+
structuredFindingSeverity: 0.2,
|
|
28
|
+
};
|
|
29
|
+
function isRecord(value) {
|
|
30
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
31
|
+
}
|
|
32
|
+
function finiteNonNegative(value, fallback) {
|
|
33
|
+
if (value === undefined)
|
|
34
|
+
return fallback;
|
|
35
|
+
if (!Number.isFinite(value) || value < 0)
|
|
36
|
+
return 0;
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function finiteNonNegativeInt(value) {
|
|
40
|
+
if (value === undefined || !Number.isFinite(value) || value < 0)
|
|
41
|
+
return 0;
|
|
42
|
+
return Math.floor(value);
|
|
43
|
+
}
|
|
44
|
+
function roundScore(value) {
|
|
45
|
+
return Math.round(Math.min(1, Math.max(0, value)) * 1_000_000) / 1_000_000;
|
|
46
|
+
}
|
|
47
|
+
function normalizeRepoFullName(value) {
|
|
48
|
+
const trimmed = value.trim().toLowerCase();
|
|
49
|
+
if (!/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/u.test(trimmed))
|
|
50
|
+
return null;
|
|
51
|
+
return trimmed;
|
|
52
|
+
}
|
|
53
|
+
function normalizeId(value) {
|
|
54
|
+
const trimmed = value.trim();
|
|
55
|
+
if (!trimmed || trimmed.length > 160 || /[\r\n\0]/u.test(trimmed))
|
|
56
|
+
return null;
|
|
57
|
+
return trimmed;
|
|
58
|
+
}
|
|
59
|
+
function normalizeObservedAt(value) {
|
|
60
|
+
if (!value)
|
|
61
|
+
return null;
|
|
62
|
+
const ms = Date.parse(value);
|
|
63
|
+
if (!Number.isFinite(ms))
|
|
64
|
+
return null;
|
|
65
|
+
return new Date(ms).toISOString();
|
|
66
|
+
}
|
|
67
|
+
function normalizeBoolean(value) {
|
|
68
|
+
if (typeof value === "boolean")
|
|
69
|
+
return value;
|
|
70
|
+
if (typeof value !== "string")
|
|
71
|
+
return undefined;
|
|
72
|
+
const normalized = value.trim().toLowerCase();
|
|
73
|
+
if (["true", "1", "yes", "on"].includes(normalized))
|
|
74
|
+
return true;
|
|
75
|
+
if (["false", "0", "no", "off"].includes(normalized))
|
|
76
|
+
return false;
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
function normalizeOptionalWeight(value) {
|
|
80
|
+
if (value === undefined || value === null)
|
|
81
|
+
return undefined;
|
|
82
|
+
const number = typeof value === "number" ? value : typeof value === "string" ? Number(value.trim()) : Number.NaN;
|
|
83
|
+
if (!Number.isFinite(number) || number < 0)
|
|
84
|
+
return undefined;
|
|
85
|
+
return number;
|
|
86
|
+
}
|
|
87
|
+
function clampConfidence(value) {
|
|
88
|
+
if (value === undefined)
|
|
89
|
+
return 1;
|
|
90
|
+
if (!Number.isFinite(value))
|
|
91
|
+
return 0;
|
|
92
|
+
return Math.min(1, Math.max(0, value));
|
|
93
|
+
}
|
|
94
|
+
function normalizeTier(value) {
|
|
95
|
+
const normalized = value.trim().toLowerCase().replace(/[_\s-]+/gu, "_");
|
|
96
|
+
if (normalized === "block" || normalized === "blocked" || normalized === "blocking" || normalized === "critical") {
|
|
97
|
+
return "blocker";
|
|
98
|
+
}
|
|
99
|
+
if (normalized === "warn" || normalized === "warnings" || normalized === "major")
|
|
100
|
+
return "warning";
|
|
101
|
+
if (normalized === "info" ||
|
|
102
|
+
normalized === "informational" ||
|
|
103
|
+
normalized === "suggestion" ||
|
|
104
|
+
normalized === "advice") {
|
|
105
|
+
return "advisory";
|
|
106
|
+
}
|
|
107
|
+
if (normalized === "nitpick" || normalized === "minor" || normalized === "trivial" || normalized === "style") {
|
|
108
|
+
return "nit";
|
|
109
|
+
}
|
|
110
|
+
if (TIER_ORDER.includes(normalized))
|
|
111
|
+
return normalized;
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Aggregate raw per-tier inputs into one normalized signal per tier: sum totals, sum confirmed (each entry's
|
|
116
|
+
* confirmed is clamped to its own total), drop tiers whose total is zero, and derive the confirmation rate and the
|
|
117
|
+
* severity-weighted per-tier score. Confidence discounts the effective confirmation the same way it does for the
|
|
118
|
+
* gate-verdict signal. Deterministic and returned in TIER_ORDER.
|
|
119
|
+
*/
|
|
120
|
+
function normalizeTiers(tiers) {
|
|
121
|
+
const byTier = new Map();
|
|
122
|
+
for (const item of tiers) {
|
|
123
|
+
const tier = normalizeTier(item.tier);
|
|
124
|
+
if (!tier)
|
|
125
|
+
continue;
|
|
126
|
+
const total = finiteNonNegativeInt(item.total);
|
|
127
|
+
if (total <= 0)
|
|
128
|
+
continue;
|
|
129
|
+
const confirmedRaw = Math.min(total, finiteNonNegativeInt(item.confirmed));
|
|
130
|
+
// A low confidence in the confirmation labelling shrinks the credited confirmations toward zero (never above the
|
|
131
|
+
// raw count), so an unverified "all confirmed" claim cannot inflate the calibration score.
|
|
132
|
+
const confirmed = Math.min(total, Math.round(confirmedRaw * clampConfidence(item.confidence)));
|
|
133
|
+
const existing = byTier.get(tier);
|
|
134
|
+
if (existing) {
|
|
135
|
+
existing.total += total;
|
|
136
|
+
existing.confirmed += confirmed;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
byTier.set(tier, { total, confirmed });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return TIER_ORDER.flatMap((tier) => {
|
|
143
|
+
const bucket = byTier.get(tier);
|
|
144
|
+
if (!bucket)
|
|
145
|
+
return [];
|
|
146
|
+
const confirmed = Math.min(bucket.total, bucket.confirmed);
|
|
147
|
+
const confirmationRate = roundScore(confirmed / bucket.total);
|
|
148
|
+
return [
|
|
149
|
+
{
|
|
150
|
+
tier,
|
|
151
|
+
total: bucket.total,
|
|
152
|
+
confirmed,
|
|
153
|
+
confirmationRate,
|
|
154
|
+
weight: TIER_WEIGHT[tier],
|
|
155
|
+
score: confirmationRate,
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The per-PR calibration score: the severity-and-volume-weighted mean of the per-tier confirmation rates, so a
|
|
162
|
+
* confirmed blocker moves the score far more than a confirmed nit, and a tier with more findings carries more weight
|
|
163
|
+
* than a tier with a single finding. Returns null when there is no weighted volume (which only happens for an empty
|
|
164
|
+
* tier list, already rejected upstream).
|
|
165
|
+
*/
|
|
166
|
+
function scoreTiers(tiers) {
|
|
167
|
+
let weightedRate = 0;
|
|
168
|
+
let weightSum = 0;
|
|
169
|
+
for (const tier of tiers) {
|
|
170
|
+
const weight = tier.weight * tier.total;
|
|
171
|
+
weightedRate += weight * tier.confirmationRate;
|
|
172
|
+
weightSum += weight;
|
|
173
|
+
}
|
|
174
|
+
if (weightSum <= 0)
|
|
175
|
+
return null;
|
|
176
|
+
return roundScore(weightedRate / weightSum);
|
|
177
|
+
}
|
|
178
|
+
function averageSignals(signals) {
|
|
179
|
+
if (signals.length === 0)
|
|
180
|
+
return null;
|
|
181
|
+
return roundScore(signals.reduce((sum, signal) => sum + signal.score, 0) / signals.length);
|
|
182
|
+
}
|
|
183
|
+
function isFindingSeverityCalibrationIngestion(value) {
|
|
184
|
+
return isRecord(value) && Array.isArray(value.accepted) && Array.isArray(value.rejected);
|
|
185
|
+
}
|
|
186
|
+
function sanitizeFindingSeverityCalibrationIngestion(ingestion) {
|
|
187
|
+
const accepted = [];
|
|
188
|
+
const rejected = [];
|
|
189
|
+
for (const signal of ingestion.accepted) {
|
|
190
|
+
if (!isRecord(signal) || !Array.isArray(signal.tiers))
|
|
191
|
+
continue;
|
|
192
|
+
const repoFullName = typeof signal.repoFullName === "string" ? normalizeRepoFullName(signal.repoFullName) : null;
|
|
193
|
+
const replayRunId = typeof signal.replayRunId === "string" ? normalizeId(signal.replayRunId) : null;
|
|
194
|
+
const reviewRunId = typeof signal.reviewRunId === "string" ? normalizeId(signal.reviewRunId) : null;
|
|
195
|
+
if (!repoFullName || !replayRunId || !reviewRunId)
|
|
196
|
+
continue;
|
|
197
|
+
const tierInputs = signal.tiers.flatMap((tier) => {
|
|
198
|
+
if (!isRecord(tier) ||
|
|
199
|
+
typeof tier.tier !== "string" ||
|
|
200
|
+
typeof tier.total !== "number" ||
|
|
201
|
+
typeof tier.confirmed !== "number") {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
return [
|
|
205
|
+
{
|
|
206
|
+
tier: tier.tier,
|
|
207
|
+
total: tier.total,
|
|
208
|
+
confirmed: tier.confirmed,
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
});
|
|
212
|
+
const tiers = normalizeTiers(tierInputs);
|
|
213
|
+
const score = scoreTiers(tiers);
|
|
214
|
+
if (tiers.length === 0 || score === null)
|
|
215
|
+
continue;
|
|
216
|
+
accepted.push({
|
|
217
|
+
repoFullName,
|
|
218
|
+
replayRunId,
|
|
219
|
+
reviewRunId,
|
|
220
|
+
observedAt: typeof signal.observedAt === "string" ? normalizeObservedAt(signal.observedAt) : null,
|
|
221
|
+
tiers,
|
|
222
|
+
score,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
for (const row of ingestion.rejected) {
|
|
226
|
+
if (!isRecord(row))
|
|
227
|
+
continue;
|
|
228
|
+
const repoFullName = typeof row.repoFullName === "string"
|
|
229
|
+
? (normalizeRepoFullName(row.repoFullName) ?? normalizeId(row.repoFullName))
|
|
230
|
+
: null;
|
|
231
|
+
const replayRunId = typeof row.replayRunId === "string" ? normalizeId(row.replayRunId) : null;
|
|
232
|
+
const reviewRunId = typeof row.reviewRunId === "string" ? normalizeId(row.reviewRunId) : null;
|
|
233
|
+
const reason = row.reason;
|
|
234
|
+
if (!repoFullName ||
|
|
235
|
+
!replayRunId ||
|
|
236
|
+
!reviewRunId ||
|
|
237
|
+
!["not_opted_in", "empty_tiers", "invalid_repo", "invalid_run_id"].includes(reason)) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
rejected.push({ repoFullName, replayRunId, reviewRunId, reason });
|
|
241
|
+
}
|
|
242
|
+
return { accepted, rejected };
|
|
243
|
+
}
|
|
244
|
+
function normalizeCompositeWeights(weights) {
|
|
245
|
+
const raw = {
|
|
246
|
+
objectiveAnchor: finiteNonNegative(weights?.objectiveAnchor, DEFAULT_COMPOSITE_WEIGHTS.objectiveAnchor),
|
|
247
|
+
pairwiseJudge: finiteNonNegative(weights?.pairwiseJudge, DEFAULT_COMPOSITE_WEIGHTS.pairwiseJudge),
|
|
248
|
+
structuredFindingSeverity: finiteNonNegative(weights?.structuredFindingSeverity, DEFAULT_COMPOSITE_WEIGHTS.structuredFindingSeverity),
|
|
249
|
+
};
|
|
250
|
+
const total = raw.objectiveAnchor + raw.pairwiseJudge + raw.structuredFindingSeverity;
|
|
251
|
+
if (total <= 0)
|
|
252
|
+
return DEFAULT_COMPOSITE_WEIGHTS;
|
|
253
|
+
return {
|
|
254
|
+
objectiveAnchor: raw.objectiveAnchor / total,
|
|
255
|
+
pairwiseJudge: raw.pairwiseJudge / total,
|
|
256
|
+
structuredFindingSeverity: raw.structuredFindingSeverity / total,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function markdownSafe(value) {
|
|
260
|
+
return value.replace(/[\r\n]+/gu, " ").replace(/[\\`*_[\]<>|]/gu, "\\$&");
|
|
261
|
+
}
|
|
262
|
+
function markdownList(values) {
|
|
263
|
+
if (values.length === 0)
|
|
264
|
+
return "- none";
|
|
265
|
+
return values.map((value) => `- ${markdownSafe(value)}`).join("\n");
|
|
266
|
+
}
|
|
267
|
+
function renderTierRows(tiers) {
|
|
268
|
+
if (tiers.length === 0)
|
|
269
|
+
return "| Tier | Total | Confirmed | Rate | Weight |\n| --- | ---: | ---: | ---: | ---: |\n";
|
|
270
|
+
return [
|
|
271
|
+
"| Tier | Total | Confirmed | Rate | Weight |",
|
|
272
|
+
"| --- | ---: | ---: | ---: | ---: |",
|
|
273
|
+
...tiers.map((tier) => `| ${markdownSafe(tier.tier)} | ${tier.total} | ${tier.confirmed} | ${tier.confirmationRate.toFixed(6)} | ${tier.weight.toFixed(6)} |`),
|
|
274
|
+
].join("\n");
|
|
275
|
+
}
|
|
276
|
+
function renderContributingRepo(signal) {
|
|
277
|
+
return [
|
|
278
|
+
`### ${markdownSafe(signal.repoFullName)}`,
|
|
279
|
+
"",
|
|
280
|
+
`- replayRunId: ${markdownSafe(signal.replayRunId)}`,
|
|
281
|
+
`- reviewRunId: ${markdownSafe(signal.reviewRunId)}`,
|
|
282
|
+
`- observedAt: ${signal.observedAt ? markdownSafe(signal.observedAt) : "n/a"}`,
|
|
283
|
+
`- score: ${signal.score.toFixed(6)}`,
|
|
284
|
+
"",
|
|
285
|
+
renderTierRows(signal.tiers),
|
|
286
|
+
].join("\n");
|
|
287
|
+
}
|
|
288
|
+
function renderRejectedRow(row) {
|
|
289
|
+
return `| ${markdownSafe(row.repoFullName)} | ${markdownSafe(row.replayRunId)} | ${markdownSafe(row.reviewRunId)} | ${markdownSafe(row.reason)} |`;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Resolve the explicit per-repo opt-in from a parsed `.gittensory.yml`-style object. Default is opted out. The
|
|
293
|
+
* preferred path is `miner.calibration.shareStructuredFindingSeverity`; `calibration.shareStructuredFindingSeverity`
|
|
294
|
+
* is accepted as a narrow alias so private-config surfaces can place the field at top level if needed.
|
|
295
|
+
*/
|
|
296
|
+
export function resolveFindingSeverityCalibrationConfig(manifest) {
|
|
297
|
+
const warnings = [];
|
|
298
|
+
const root = isRecord(manifest) ? manifest : {};
|
|
299
|
+
const miner = isRecord(root.miner) ? root.miner : {};
|
|
300
|
+
const minerCalibration = isRecord(miner.calibration) ? miner.calibration : {};
|
|
301
|
+
const topCalibration = isRecord(root.calibration) ? root.calibration : {};
|
|
302
|
+
const optInRaw = minerCalibration.shareStructuredFindingSeverity ?? topCalibration.shareStructuredFindingSeverity ?? undefined;
|
|
303
|
+
const optIn = normalizeBoolean(optInRaw);
|
|
304
|
+
if (optInRaw !== undefined && optIn === undefined) {
|
|
305
|
+
warnings.push("miner.calibration.shareStructuredFindingSeverity must be a boolean-like value; defaulting to false.");
|
|
306
|
+
}
|
|
307
|
+
const weightRaw = minerCalibration.structuredFindingSeverityWeight ?? topCalibration.structuredFindingSeverityWeight;
|
|
308
|
+
const weight = normalizeOptionalWeight(weightRaw);
|
|
309
|
+
if (weightRaw !== undefined && weight === undefined) {
|
|
310
|
+
warnings.push("miner.calibration.structuredFindingSeverityWeight must be a non-negative finite number; using default.");
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
shareStructuredFindingSeverity: optIn === true,
|
|
314
|
+
structuredFindingSeverityWeight: weight ?? DEFAULT_STRUCTURED_FINDING_SEVERITY_WEIGHT,
|
|
315
|
+
warnings,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Ingest only currently opted-in structured finding-severity signals. The opt-in check happens at ingestion time, so
|
|
320
|
+
* a maintainer opt-out immediately prevents additional calibration rows from contributing even if older collected
|
|
321
|
+
* data exists elsewhere.
|
|
322
|
+
*/
|
|
323
|
+
export function ingestFindingSeverityCalibrationSignals(signals) {
|
|
324
|
+
const accepted = [];
|
|
325
|
+
const rejected = [];
|
|
326
|
+
for (const signal of signals) {
|
|
327
|
+
const repoFullName = normalizeRepoFullName(signal.repoFullName);
|
|
328
|
+
const replayRunId = normalizeId(signal.replayRunId);
|
|
329
|
+
const reviewRunId = normalizeId(signal.reviewRunId);
|
|
330
|
+
if (!repoFullName) {
|
|
331
|
+
rejected.push({
|
|
332
|
+
repoFullName: signal.repoFullName,
|
|
333
|
+
replayRunId: signal.replayRunId,
|
|
334
|
+
reviewRunId: signal.reviewRunId,
|
|
335
|
+
reason: "invalid_repo",
|
|
336
|
+
});
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (!replayRunId || !reviewRunId) {
|
|
340
|
+
rejected.push({
|
|
341
|
+
repoFullName,
|
|
342
|
+
replayRunId: signal.replayRunId,
|
|
343
|
+
reviewRunId: signal.reviewRunId,
|
|
344
|
+
reason: "invalid_run_id",
|
|
345
|
+
});
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (!signal.optedIn) {
|
|
349
|
+
rejected.push({ repoFullName, replayRunId, reviewRunId, reason: "not_opted_in" });
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const tiers = normalizeTiers(signal.tiers);
|
|
353
|
+
const score = scoreTiers(tiers);
|
|
354
|
+
if (tiers.length === 0 || score === null) {
|
|
355
|
+
rejected.push({ repoFullName, replayRunId, reviewRunId, reason: "empty_tiers" });
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
accepted.push({
|
|
359
|
+
repoFullName,
|
|
360
|
+
replayRunId,
|
|
361
|
+
reviewRunId,
|
|
362
|
+
observedAt: normalizeObservedAt(signal.observedAt),
|
|
363
|
+
tiers,
|
|
364
|
+
score,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return { accepted, rejected };
|
|
368
|
+
}
|
|
369
|
+
export function computeFindingSeverityCompositeCalibrationScore(input) {
|
|
370
|
+
const ingestion = isFindingSeverityCalibrationIngestion(input.findingSeverity)
|
|
371
|
+
? sanitizeFindingSeverityCalibrationIngestion(input.findingSeverity)
|
|
372
|
+
: ingestFindingSeverityCalibrationSignals(input.findingSeverity);
|
|
373
|
+
const objectiveAnchorScore = typeof input.objectiveAnchor === "number" ? roundScore(input.objectiveAnchor) : input.objectiveAnchor.score;
|
|
374
|
+
const pairwiseJudgeScore = input.pairwise === null
|
|
375
|
+
? null
|
|
376
|
+
: typeof input.pairwise === "number"
|
|
377
|
+
? roundScore(input.pairwise)
|
|
378
|
+
: input.pairwise.pairwiseJudgeScore;
|
|
379
|
+
const structuredFindingSeverityScore = averageSignals(ingestion.accepted);
|
|
380
|
+
const rawWeights = normalizeCompositeWeights(input.weights);
|
|
381
|
+
const usableWeights = {
|
|
382
|
+
objectiveAnchor: rawWeights.objectiveAnchor,
|
|
383
|
+
pairwiseJudge: pairwiseJudgeScore === null ? 0 : rawWeights.pairwiseJudge,
|
|
384
|
+
structuredFindingSeverity: structuredFindingSeverityScore === null ? 0 : rawWeights.structuredFindingSeverity,
|
|
385
|
+
};
|
|
386
|
+
const total = usableWeights.objectiveAnchor + usableWeights.pairwiseJudge + usableWeights.structuredFindingSeverity;
|
|
387
|
+
const weights = total <= 0
|
|
388
|
+
? { objectiveAnchor: 1, pairwiseJudge: 0, structuredFindingSeverity: 0 }
|
|
389
|
+
: {
|
|
390
|
+
objectiveAnchor: usableWeights.objectiveAnchor / total,
|
|
391
|
+
pairwiseJudge: usableWeights.pairwiseJudge / total,
|
|
392
|
+
structuredFindingSeverity: usableWeights.structuredFindingSeverity / total,
|
|
393
|
+
};
|
|
394
|
+
const compositeScore = roundScore(objectiveAnchorScore * weights.objectiveAnchor +
|
|
395
|
+
(pairwiseJudgeScore ?? 0) * weights.pairwiseJudge +
|
|
396
|
+
(structuredFindingSeverityScore ?? 0) * weights.structuredFindingSeverity);
|
|
397
|
+
return {
|
|
398
|
+
compositeScore,
|
|
399
|
+
objectiveAnchorScore,
|
|
400
|
+
pairwiseJudgeScore,
|
|
401
|
+
structuredFindingSeverityScore,
|
|
402
|
+
weights,
|
|
403
|
+
audit: {
|
|
404
|
+
contributingRepos: ingestion.accepted.map((signal) => ({
|
|
405
|
+
repoFullName: signal.repoFullName,
|
|
406
|
+
replayRunId: signal.replayRunId,
|
|
407
|
+
reviewRunId: signal.reviewRunId,
|
|
408
|
+
observedAt: signal.observedAt,
|
|
409
|
+
score: signal.score,
|
|
410
|
+
tiers: signal.tiers,
|
|
411
|
+
})),
|
|
412
|
+
rejected: ingestion.rejected,
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Render a deterministic, public-safe Markdown report for a structured finding-severity calibration result. The
|
|
418
|
+
* report is local-run evidence: it includes aggregate scores, normalized weights, opted-in contributors, and rejected
|
|
419
|
+
* rows, but never accepts or emits raw review text or private scoring fields.
|
|
420
|
+
*/
|
|
421
|
+
export function renderFindingSeverityCalibrationAuditMarkdown(result) {
|
|
422
|
+
const lines = [
|
|
423
|
+
"# Structured Finding-Severity Calibration",
|
|
424
|
+
"",
|
|
425
|
+
`Composite score: ${result.compositeScore.toFixed(6)}`,
|
|
426
|
+
"",
|
|
427
|
+
"## Component Scores",
|
|
428
|
+
"",
|
|
429
|
+
`- objectiveAnchor: ${result.objectiveAnchorScore.toFixed(6)}`,
|
|
430
|
+
`- pairwiseJudge: ${result.pairwiseJudgeScore === null ? "n/a" : result.pairwiseJudgeScore.toFixed(6)}`,
|
|
431
|
+
`- structuredFindingSeverity: ${result.structuredFindingSeverityScore === null ? "n/a" : result.structuredFindingSeverityScore.toFixed(6)}`,
|
|
432
|
+
"",
|
|
433
|
+
"## Effective Weights",
|
|
434
|
+
"",
|
|
435
|
+
`- objectiveAnchor: ${result.weights.objectiveAnchor.toFixed(6)}`,
|
|
436
|
+
`- pairwiseJudge: ${result.weights.pairwiseJudge.toFixed(6)}`,
|
|
437
|
+
`- structuredFindingSeverity: ${result.weights.structuredFindingSeverity.toFixed(6)}`,
|
|
438
|
+
"",
|
|
439
|
+
"## Contributing Repos",
|
|
440
|
+
"",
|
|
441
|
+
result.audit.contributingRepos.length === 0
|
|
442
|
+
? "_No opted-in structured finding-severity signals contributed._"
|
|
443
|
+
: result.audit.contributingRepos.map(renderContributingRepo).join("\n\n"),
|
|
444
|
+
"",
|
|
445
|
+
"## Rejected Rows",
|
|
446
|
+
"",
|
|
447
|
+
];
|
|
448
|
+
if (result.audit.rejected.length === 0) {
|
|
449
|
+
lines.push("- none");
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
lines.push("| Repo | Replay run | Review run | Reason |", "| --- | --- | --- | --- |", ...result.audit.rejected.map(renderRejectedRow));
|
|
453
|
+
}
|
|
454
|
+
const contributingRepos = result.audit.contributingRepos.map((repo) => repo.repoFullName);
|
|
455
|
+
lines.push("", "## Contributing Repo Summary", "", markdownList(contributingRepos));
|
|
456
|
+
return `${lines.join("\n")}\n`;
|
|
457
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** One target repo in a fleet run, with its own concurrent-worktree budget. */
|
|
2
|
+
export type FleetRunManifestRepo = {
|
|
3
|
+
/** Canonical `owner/repo`. Compatible with opportunity-fanout's target normalization (a splittable pair). */
|
|
4
|
+
repoFullName: string;
|
|
5
|
+
/**
|
|
6
|
+
* Max concurrent worktrees (in-flight attempts) this repo may hold at once. A positive integer (`>= 1`); a
|
|
7
|
+
* non-integer is floored, a value below 1 falls back to the default. Default: 1.
|
|
8
|
+
*/
|
|
9
|
+
maxConcurrentWorktrees: number;
|
|
10
|
+
};
|
|
11
|
+
/** Fleet run-manifest: the repos to work across and how to split the concurrency budget. See {@link DEFAULT_FLEET_RUN_MANIFEST}. */
|
|
12
|
+
export type FleetRunManifest = {
|
|
13
|
+
/** Target repos, de-duplicated by `repoFullName` (first entry wins). Default: [] (no repos in scope). */
|
|
14
|
+
repos: readonly FleetRunManifestRepo[];
|
|
15
|
+
/**
|
|
16
|
+
* Total concurrent worktrees across the whole fleet, regardless of per-repo budgets. A positive integer
|
|
17
|
+
* (`>= 1`); floored, sub-1 falls back to the default. Default: 1.
|
|
18
|
+
*/
|
|
19
|
+
totalConcurrentWorktrees: number;
|
|
20
|
+
};
|
|
21
|
+
/** Tolerant parser result: the normalized manifest plus warnings and whether the file expressed any non-default
|
|
22
|
+
* field. Mirrors {@link ParsedMinerGoalSpec}'s present/warnings shape. */
|
|
23
|
+
export type ParsedFleetRunManifest = {
|
|
24
|
+
present: boolean;
|
|
25
|
+
manifest: FleetRunManifest;
|
|
26
|
+
warnings: string[];
|
|
27
|
+
};
|
|
28
|
+
/** Safe defaults applied when a field is absent (or the file is missing): no repos in scope, one worktree total.
|
|
29
|
+
* Deep-frozen shared singleton — clone before layering overrides. */
|
|
30
|
+
export declare const DEFAULT_FLEET_RUN_MANIFEST: FleetRunManifest;
|
|
31
|
+
/**
|
|
32
|
+
* Tolerantly normalize an already-parsed run-manifest object into a {@link ParsedFleetRunManifest}. Never throws:
|
|
33
|
+
* malformed shapes degrade to safe defaults and accumulate warnings so a fleet run can surface "your run-manifest
|
|
34
|
+
* had problems" without hard-failing. Mirrors {@link parseMinerGoalSpec}.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseFleetRunManifest(raw: unknown): ParsedFleetRunManifest;
|
|
37
|
+
/**
|
|
38
|
+
* Parse raw run-manifest file content (JSON or YAML). Malformed content degrades to an absent manifest with a
|
|
39
|
+
* warning rather than throwing, mirroring {@link parseMinerGoalSpecContent}.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseFleetRunManifestContent(content: string | null | undefined): ParsedFleetRunManifest;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { parse as parseYaml } from "yaml";
|
|
2
|
+
/** Safe defaults applied when a field is absent (or the file is missing): no repos in scope, one worktree total.
|
|
3
|
+
* Deep-frozen shared singleton — clone before layering overrides. */
|
|
4
|
+
export const DEFAULT_FLEET_RUN_MANIFEST = Object.freeze({
|
|
5
|
+
repos: Object.freeze([]),
|
|
6
|
+
totalConcurrentWorktrees: 1,
|
|
7
|
+
});
|
|
8
|
+
const MAX_FLEET_RUN_MANIFEST_BYTES = 65_536;
|
|
9
|
+
const MAX_MANIFEST_REPOS = 500;
|
|
10
|
+
function cloneDefaultFleetRunManifest() {
|
|
11
|
+
return { ...DEFAULT_FLEET_RUN_MANIFEST, repos: [...DEFAULT_FLEET_RUN_MANIFEST.repos] };
|
|
12
|
+
}
|
|
13
|
+
function emptyFleetRunManifest(warnings = []) {
|
|
14
|
+
return { present: false, manifest: cloneDefaultFleetRunManifest(), warnings };
|
|
15
|
+
}
|
|
16
|
+
/** `owner/repo` with exactly one slash and non-empty halves; anything else → null. Same shape the goal-spec /
|
|
17
|
+
* portfolio-queue validators use, so a manifest repo is directly compatible with opportunity-fanout targets. */
|
|
18
|
+
function normalizeRepoFullName(value) {
|
|
19
|
+
if (typeof value !== "string")
|
|
20
|
+
return null;
|
|
21
|
+
const [owner, repo, extra] = value.trim().split("/");
|
|
22
|
+
if (!owner || !repo || extra !== undefined)
|
|
23
|
+
return null;
|
|
24
|
+
return `${owner}/${repo}`;
|
|
25
|
+
}
|
|
26
|
+
function normalizePositiveInteger(value, field, fallback, warnings) {
|
|
27
|
+
if (value === undefined || value === null)
|
|
28
|
+
return fallback;
|
|
29
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
30
|
+
warnings.push(`FleetRunManifest field "${field}" must be a positive whole number; falling back to ${fallback}.`);
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
const normalized = Math.floor(value);
|
|
34
|
+
if (normalized >= 1)
|
|
35
|
+
return normalized;
|
|
36
|
+
warnings.push(`FleetRunManifest field "${field}" must be >= 1 after flooring; falling back to ${fallback}.`);
|
|
37
|
+
return fallback;
|
|
38
|
+
}
|
|
39
|
+
// A repo entry may be a bare `"owner/repo"` string (uses the default per-repo budget) or a `{ repoFullName,
|
|
40
|
+
// maxConcurrentWorktrees? }` mapping. Anything else, or an unparseable repo name, is skipped with a warning.
|
|
41
|
+
function normalizeRepoList(value, warnings) {
|
|
42
|
+
if (value === undefined || value === null)
|
|
43
|
+
return [];
|
|
44
|
+
if (!Array.isArray(value)) {
|
|
45
|
+
warnings.push(`FleetRunManifest field "repos" must be a list; ignoring a ${typeof value} value.`);
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const result = [];
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
for (const [index, entry] of value.entries()) {
|
|
51
|
+
if (index >= MAX_MANIFEST_REPOS) {
|
|
52
|
+
warnings.push(`FleetRunManifest field "repos" exceeded ${MAX_MANIFEST_REPOS} entries; extra entries ignored.`);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
let repoFullName;
|
|
56
|
+
let maxConcurrentWorktrees = DEFAULT_FLEET_RUN_MANIFEST.totalConcurrentWorktrees;
|
|
57
|
+
if (typeof entry === "string") {
|
|
58
|
+
repoFullName = normalizeRepoFullName(entry);
|
|
59
|
+
}
|
|
60
|
+
else if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
61
|
+
const record = entry;
|
|
62
|
+
repoFullName = normalizeRepoFullName(record.repoFullName);
|
|
63
|
+
maxConcurrentWorktrees = normalizePositiveInteger(record.maxConcurrentWorktrees, "maxConcurrentWorktrees", 1, warnings);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
warnings.push(`FleetRunManifest "repos" skipped a non-string, non-mapping entry.`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (repoFullName === null) {
|
|
70
|
+
warnings.push(`FleetRunManifest "repos" skipped an entry with an invalid "owner/repo" name.`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (seen.has(repoFullName)) {
|
|
74
|
+
warnings.push(`FleetRunManifest "repos" skipped a duplicate entry for ${repoFullName}.`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
seen.add(repoFullName);
|
|
78
|
+
result.push({ repoFullName, maxConcurrentWorktrees });
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
function utf8ByteLength(value) {
|
|
83
|
+
let bytes = 0;
|
|
84
|
+
for (const char of value) {
|
|
85
|
+
const codePoint = char.codePointAt(0);
|
|
86
|
+
if (codePoint <= 0x7f)
|
|
87
|
+
bytes += 1;
|
|
88
|
+
else if (codePoint <= 0x7ff)
|
|
89
|
+
bytes += 2;
|
|
90
|
+
else if (codePoint <= 0xffff)
|
|
91
|
+
bytes += 3;
|
|
92
|
+
else
|
|
93
|
+
bytes += 4;
|
|
94
|
+
}
|
|
95
|
+
return bytes;
|
|
96
|
+
}
|
|
97
|
+
function hasConfiguredManifestFields(manifest) {
|
|
98
|
+
return manifest.repos.length > 0 || manifest.totalConcurrentWorktrees !== DEFAULT_FLEET_RUN_MANIFEST.totalConcurrentWorktrees;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Tolerantly normalize an already-parsed run-manifest object into a {@link ParsedFleetRunManifest}. Never throws:
|
|
102
|
+
* malformed shapes degrade to safe defaults and accumulate warnings so a fleet run can surface "your run-manifest
|
|
103
|
+
* had problems" without hard-failing. Mirrors {@link parseMinerGoalSpec}.
|
|
104
|
+
*/
|
|
105
|
+
export function parseFleetRunManifest(raw) {
|
|
106
|
+
if (raw === undefined || raw === null)
|
|
107
|
+
return emptyFleetRunManifest();
|
|
108
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
109
|
+
return emptyFleetRunManifest([
|
|
110
|
+
"FleetRunManifest must be a mapping of fields; ignoring malformed config and falling back to safe defaults.",
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
const record = raw;
|
|
114
|
+
const warnings = [];
|
|
115
|
+
const manifest = {
|
|
116
|
+
repos: normalizeRepoList(record.repos, warnings),
|
|
117
|
+
totalConcurrentWorktrees: normalizePositiveInteger(record.totalConcurrentWorktrees, "totalConcurrentWorktrees", DEFAULT_FLEET_RUN_MANIFEST.totalConcurrentWorktrees, warnings),
|
|
118
|
+
};
|
|
119
|
+
if (!hasConfiguredManifestFields(manifest)) {
|
|
120
|
+
warnings.push("FleetRunManifest contained no recognized non-default fields; falling back to safe defaults.");
|
|
121
|
+
return { present: false, manifest: cloneDefaultFleetRunManifest(), warnings };
|
|
122
|
+
}
|
|
123
|
+
return { present: true, manifest, warnings };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Parse raw run-manifest file content (JSON or YAML). Malformed content degrades to an absent manifest with a
|
|
127
|
+
* warning rather than throwing, mirroring {@link parseMinerGoalSpecContent}.
|
|
128
|
+
*/
|
|
129
|
+
export function parseFleetRunManifestContent(content) {
|
|
130
|
+
if (content === undefined || content === null || content.trim() === "")
|
|
131
|
+
return emptyFleetRunManifest();
|
|
132
|
+
if (utf8ByteLength(content) > MAX_FLEET_RUN_MANIFEST_BYTES) {
|
|
133
|
+
return emptyFleetRunManifest([
|
|
134
|
+
`FleetRunManifest content exceeded ${MAX_FLEET_RUN_MANIFEST_BYTES} bytes; ignoring it and falling back to safe defaults.`,
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
const trimmed = content.trim();
|
|
138
|
+
const looksLikeJson = trimmed.startsWith("{") || trimmed.startsWith("[");
|
|
139
|
+
let parsed;
|
|
140
|
+
try {
|
|
141
|
+
parsed = looksLikeJson ? JSON.parse(trimmed) : parseYaml(trimmed);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return emptyFleetRunManifest([
|
|
145
|
+
looksLikeJson
|
|
146
|
+
? "FleetRunManifest content was not valid JSON; ignoring it and falling back to safe defaults."
|
|
147
|
+
: "FleetRunManifest content was not valid YAML; ignoring it and falling back to safe defaults.",
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
150
|
+
return parseFleetRunManifest(parsed);
|
|
151
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FocusManifest, FocusManifestGuidance } from "../types/predicted-gate-types.js";
|
|
2
|
+
export declare function isFocusManifestPublicSafe(text: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Match a changed path against a manifest path pattern. Supports exact paths, directory
|
|
5
|
+
* prefixes (`src/` or `src`), and `*` wildcards (`**` collapses to `*`).
|
|
6
|
+
*/
|
|
7
|
+
export declare function matchesManifestPath(path: string, pattern: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Build deterministic, public-safe guidance from a focus manifest for a concrete change set.
|
|
10
|
+
* Explains why changed paths are preferred or discouraged and surfaces manifest-driven blockers
|
|
11
|
+
* without leaking maintainer-private notes into public next steps.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildFocusManifestGuidance(args: {
|
|
14
|
+
manifest: FocusManifest;
|
|
15
|
+
changedPaths: string[];
|
|
16
|
+
labels?: string[] | undefined;
|
|
17
|
+
linkedIssueCount?: number | undefined;
|
|
18
|
+
testFileCount?: number | undefined;
|
|
19
|
+
passedValidationCount?: number | undefined;
|
|
20
|
+
}): FocusManifestGuidance;
|
|
21
|
+
export type { FocusManifest, FocusManifestGuidance, PreMergeCheck } from "../types/predicted-gate-types.js";
|