@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,76 @@
|
|
|
1
|
+
import { computeLaneFit } from "./goal-model.js";
|
|
2
|
+
/** Whether a repo's miner goal spec permits autonomous targeting (explicit opt-out only). */
|
|
3
|
+
export function isMinerRepoTargetable(spec) {
|
|
4
|
+
return spec.minerEnabled;
|
|
5
|
+
}
|
|
6
|
+
function clamp01(value) {
|
|
7
|
+
if (!Number.isFinite(value))
|
|
8
|
+
return 0;
|
|
9
|
+
return Math.min(1, Math.max(0, value));
|
|
10
|
+
}
|
|
11
|
+
function normalizeLabels(labels) {
|
|
12
|
+
return labels
|
|
13
|
+
.filter((label) => typeof label === "string")
|
|
14
|
+
.map((label) => label.trim().toLowerCase())
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Compute a [0, 1] lane-fit score from issue labels and a parsed {@link MinerGoalSpec}. Path-based fit is
|
|
19
|
+
* intentionally omitted — discovery metadata has labels only; path gating belongs in the analyze phase.
|
|
20
|
+
*/
|
|
21
|
+
export function computeMinerGoalLaneFit(issue, spec) {
|
|
22
|
+
const issueLabels = normalizeLabels(issue.labels);
|
|
23
|
+
const blocked = normalizeLabels(spec.blockedLabels);
|
|
24
|
+
if (blocked.length > 0 && blocked.some((label) => issueLabels.includes(label))) {
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
const preferred = normalizeLabels(spec.preferredLabels);
|
|
28
|
+
let score;
|
|
29
|
+
if (preferred.length === 0) {
|
|
30
|
+
score = 1;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const preferredMatch = preferred.some((want) => issueLabels.includes(want));
|
|
34
|
+
if (preferredMatch) {
|
|
35
|
+
score = 1;
|
|
36
|
+
}
|
|
37
|
+
else if (spec.issueDiscoveryPolicy === "discouraged") {
|
|
38
|
+
score = 0.6;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
score = 0.25;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (spec.issueDiscoveryPolicy === "encouraged") {
|
|
45
|
+
score = Math.max(score, 0.85);
|
|
46
|
+
}
|
|
47
|
+
return clamp01(score);
|
|
48
|
+
}
|
|
49
|
+
function normalizeCandidatePaths(paths) {
|
|
50
|
+
if (!Array.isArray(paths))
|
|
51
|
+
return [];
|
|
52
|
+
const normalized = [];
|
|
53
|
+
for (const path of paths) {
|
|
54
|
+
if (typeof path !== "string")
|
|
55
|
+
continue;
|
|
56
|
+
const trimmed = path.trim();
|
|
57
|
+
if (trimmed)
|
|
58
|
+
normalized.push(trimmed);
|
|
59
|
+
}
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Lane-fit for metadata-ranked issues. Uses full path+label {@link computeLaneFit} when
|
|
64
|
+
* `candidatePaths` are present; otherwise falls back to label-only {@link computeMinerGoalLaneFit}.
|
|
65
|
+
*/
|
|
66
|
+
export function computeMetadataLaneFit(issue, spec) {
|
|
67
|
+
const candidatePaths = normalizeCandidatePaths(issue.candidatePaths);
|
|
68
|
+
if (candidatePaths.length > 0) {
|
|
69
|
+
return computeLaneFit({
|
|
70
|
+
candidatePaths,
|
|
71
|
+
candidateLabels: [...issue.labels],
|
|
72
|
+
goalSpec: spec,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return computeMinerGoalLaneFit(issue, spec);
|
|
76
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { MINER_LIVE_MODE_OPT_IN } from "./governor/action-mode.js";
|
|
2
|
+
/** How strongly opening discovery issues is encouraged for this repo. Mirrors the review-side policy vocabulary. */
|
|
3
|
+
export type MinerIssueDiscoveryPolicy = "encouraged" | "neutral" | "discouraged";
|
|
4
|
+
/** Per-repo tuning for the feasibility gate (`buildFeasibilityVerdict`, see `feasibility.ts`) a miner consults
|
|
5
|
+
* before starting work. This is config-parsing surface only — it does not itself change the composer's
|
|
6
|
+
* behavior; a caller wiring the gate into a decision flow reads this policy and applies it. */
|
|
7
|
+
export type FeasibilityGatePolicy = {
|
|
8
|
+
/** Whether this repo wants the feasibility gate consulted at all before a miner starts work. Setting this
|
|
9
|
+
* `false` lets a repo opt out of the gate entirely rather than tuning it. Default: true. */
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
/** Specific `buildFeasibilityVerdict` avoid/raise reason codes (e.g. `"duplicate_cluster_high"`) this repo
|
|
12
|
+
* wants ignored — for a repo that doesn't want duplicate-cluster signals to affect feasibility, for example.
|
|
13
|
+
* String list. Default: [] (nothing suppressed). */
|
|
14
|
+
suppressedReasons: readonly string[];
|
|
15
|
+
};
|
|
16
|
+
/** Per-repo self-plagiarism throttle tuning for Governor open_pr (#2345). */
|
|
17
|
+
export type SelfPlagiarismPolicy = {
|
|
18
|
+
/** Jaccard similarity threshold in [0, 1] for near-duplicate diff fingerprints. Default: 0.85. */
|
|
19
|
+
similarityThreshold: number;
|
|
20
|
+
};
|
|
21
|
+
/** Per-repo kill-switch tuning consulted by the Governor chokepoint's kill-switch primitive (#2341). */
|
|
22
|
+
export type MinerKillSwitchPolicy = {
|
|
23
|
+
/**
|
|
24
|
+
* Per-repo runtime halt: stops all miner WRITE actions for this repo without deregistering it from
|
|
25
|
+
* targeting/discovery. Distinct from `minerEnabled` (a discovery-time opt-out) — pausing preserves in-flight
|
|
26
|
+
* queue state so un-pausing resumes exactly where the queue left off. Default: false (not paused).
|
|
27
|
+
*/
|
|
28
|
+
paused: boolean;
|
|
29
|
+
};
|
|
30
|
+
/** Per-repo dry-run/live execution tuning consulted by the Governor chokepoint's action-mode primitive (#2342). */
|
|
31
|
+
export type MinerExecutionPolicy = {
|
|
32
|
+
/**
|
|
33
|
+
* Explicit opt-in to LIVE write execution for this repo. Must equal EXACTLY the literal string `"live"` — any
|
|
34
|
+
* other value (a typo, `"yes"`, `"on"`, or a boolean `true` from a malformed file) is treated as not opted
|
|
35
|
+
* in, so a fat-fingered config can never accidentally enable live writes. A miner also stays in dry-run
|
|
36
|
+
* unless its own operator separately opts in globally (`GITTENSORY_MINER_LIVE_MODE=live`) — this field alone
|
|
37
|
+
* cannot force a stranger's miner instance live. Default: null (dry-run).
|
|
38
|
+
*/
|
|
39
|
+
liveModeOptIn: typeof MINER_LIVE_MODE_OPT_IN | null;
|
|
40
|
+
};
|
|
41
|
+
/** Per-repo miner configuration parsed from `.gittensory-miner.yml`. See {@link DEFAULT_MINER_GOAL_SPEC}. */
|
|
42
|
+
export type MinerGoalSpec = {
|
|
43
|
+
/**
|
|
44
|
+
* Whether this repo permits autonomous miners at all. Explicit OPT-OUT, not opt-in: a public repo with no
|
|
45
|
+
* `.gittensory-miner.yml` is still minable, mirroring `.gittensory.yml`'s "safe by default" stance. Set `false`
|
|
46
|
+
* to halt all miner targeting of this repo. Default: true.
|
|
47
|
+
*/
|
|
48
|
+
minerEnabled: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Work areas the maintainer wants a miner to focus on; a candidate touching these is preferred. Glob list.
|
|
51
|
+
* Default: [] (no preference).
|
|
52
|
+
*/
|
|
53
|
+
wantedPaths: readonly string[];
|
|
54
|
+
/**
|
|
55
|
+
* Paths off-limits to a miner. A candidate touching one of these should be skipped. Glob list.
|
|
56
|
+
* Default: [] (nothing blocked).
|
|
57
|
+
*/
|
|
58
|
+
blockedPaths: readonly string[];
|
|
59
|
+
/**
|
|
60
|
+
* Issue/PR labels the maintainer prefers a miner to target; a candidate carrying one is favored. String list.
|
|
61
|
+
* Default: [] (no preference).
|
|
62
|
+
*/
|
|
63
|
+
preferredLabels: readonly string[];
|
|
64
|
+
/**
|
|
65
|
+
* Issue/PR labels a miner must not target; a candidate carrying one should be skipped. String list.
|
|
66
|
+
* Default: [] (nothing blocked).
|
|
67
|
+
*/
|
|
68
|
+
blockedLabels: readonly string[];
|
|
69
|
+
/**
|
|
70
|
+
* Maximum number of issues a single miner may hold claimed on this repo at once, so one miner cannot monopolize
|
|
71
|
+
* a repo's queue. A positive integer (`>= 1`); the parser is expected to floor a non-integer toward zero
|
|
72
|
+
* (`Math.floor`) and reject any value below 1. Default: 1.
|
|
73
|
+
*/
|
|
74
|
+
maxConcurrentClaims: number;
|
|
75
|
+
/**
|
|
76
|
+
* How strongly this repo encourages a miner to open discovery issues. Values: encouraged | neutral | discouraged.
|
|
77
|
+
* Default: neutral.
|
|
78
|
+
*/
|
|
79
|
+
issueDiscoveryPolicy: MinerIssueDiscoveryPolicy;
|
|
80
|
+
/**
|
|
81
|
+
* Per-repo tuning for the feasibility gate a miner consults before starting work. See {@link FeasibilityGatePolicy}.
|
|
82
|
+
* Default: { enabled: true, suppressedReasons: [] }.
|
|
83
|
+
*/
|
|
84
|
+
feasibilityGate: FeasibilityGatePolicy;
|
|
85
|
+
/**
|
|
86
|
+
* Self-plagiarism throttle consulted before open_pr (#2345). Default: { similarityThreshold: 0.85 }.
|
|
87
|
+
*/
|
|
88
|
+
selfPlagiarism: SelfPlagiarismPolicy;
|
|
89
|
+
/**
|
|
90
|
+
* Per-repo kill-switch consulted by the Governor chokepoint before every write action (#2341).
|
|
91
|
+
* Default: { paused: false }.
|
|
92
|
+
*/
|
|
93
|
+
killSwitch: MinerKillSwitchPolicy;
|
|
94
|
+
/**
|
|
95
|
+
* Per-repo dry-run/live execution opt-in consulted by the Governor chokepoint (#2342).
|
|
96
|
+
* Default: { liveModeOptIn: null }.
|
|
97
|
+
*/
|
|
98
|
+
execution: MinerExecutionPolicy;
|
|
99
|
+
};
|
|
100
|
+
/** The tolerant parser result for `.gittensory-miner.yml`: the normalized spec plus parse warnings and whether the
|
|
101
|
+
* file actually expressed any non-default goal fields. Mirrors `parseFocusManifest`'s present/warnings pattern
|
|
102
|
+
* without forcing metadata onto downstream consumers that only need the config itself. */
|
|
103
|
+
export type ParsedMinerGoalSpec = {
|
|
104
|
+
present: boolean;
|
|
105
|
+
spec: MinerGoalSpec;
|
|
106
|
+
warnings: string[];
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* The safe defaults applied when a field is absent from `.gittensory-miner.yml` (or the file itself is missing).
|
|
110
|
+
* Every value here matches the "Default: X" documented on its field above. Analogous to the defaults constant that
|
|
111
|
+
* accompanies `FocusManifest` in `src/signals/focus-manifest.ts` — a repo with no file behaves as if it declared
|
|
112
|
+
* this: minable, with no path/label preferences, one concurrent claim, and neutral discovery.
|
|
113
|
+
*
|
|
114
|
+
* Deep-frozen: this is a shared singleton, so runtime code can read it freely but must not mutate it — clone before
|
|
115
|
+
* layering repo-specific overrides on top.
|
|
116
|
+
*/
|
|
117
|
+
export declare const DEFAULT_MINER_GOAL_SPEC: Readonly<MinerGoalSpec>;
|
|
118
|
+
/**
|
|
119
|
+
* Tolerantly normalize an already-parsed `.gittensory-miner.yml` object into a {@link ParsedMinerGoalSpec}.
|
|
120
|
+
* Never throws: malformed shapes degrade to safe defaults and accumulate warnings so callers can surface
|
|
121
|
+
* "your miner goal spec had problems" without hard-failing a run.
|
|
122
|
+
*/
|
|
123
|
+
export declare function parseMinerGoalSpec(raw: unknown): ParsedMinerGoalSpec;
|
|
124
|
+
/**
|
|
125
|
+
* Parse raw `.gittensory-miner.yml` file content (JSON or YAML). Malformed content degrades to an absent
|
|
126
|
+
* goal spec with a warning rather than throwing, mirroring `parseFocusManifestContent`.
|
|
127
|
+
*/
|
|
128
|
+
export declare function parseMinerGoalSpecContent(content: string | null | undefined): ParsedMinerGoalSpec;
|
|
129
|
+
/**
|
|
130
|
+
* The documented `.gittensory-miner` file-discovery order (first match wins), mirroring how `.gittensory.yml` is
|
|
131
|
+
* discovered: repo-root YAML, then `.github/` YAML, then the JSON variants.
|
|
132
|
+
*/
|
|
133
|
+
export declare const MINER_GOAL_SPEC_FILENAMES: readonly [".gittensory-miner.yml", ".github/gittensory-miner.yml", ".gittensory-miner.json", ".github/gittensory-miner.json"];
|
|
134
|
+
/**
|
|
135
|
+
* The first {@link MINER_GOAL_SPEC_FILENAMES} candidate that exists, or null. Pure: the caller injects the existence
|
|
136
|
+
* check (e.g. `fs.existsSync`) so this module stays IO-free and unit-testable. A caller reads the returned path and
|
|
137
|
+
* feeds its content to {@link parseMinerGoalSpecContent}.
|
|
138
|
+
*/
|
|
139
|
+
export declare function discoverMinerGoalSpecPath(exists: (path: string) => boolean): string | null;
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { parse as parseYaml } from "yaml";
|
|
2
|
+
import { MINER_LIVE_MODE_OPT_IN } from "./governor/action-mode.js";
|
|
3
|
+
import { DEFAULT_SELF_PLAGIARISM_SIMILARITY_THRESHOLD, resolveSelfPlagiarismConfig, } from "./governor/self-plagiarism.js";
|
|
4
|
+
/**
|
|
5
|
+
* The safe defaults applied when a field is absent from `.gittensory-miner.yml` (or the file itself is missing).
|
|
6
|
+
* Every value here matches the "Default: X" documented on its field above. Analogous to the defaults constant that
|
|
7
|
+
* accompanies `FocusManifest` in `src/signals/focus-manifest.ts` — a repo with no file behaves as if it declared
|
|
8
|
+
* this: minable, with no path/label preferences, one concurrent claim, and neutral discovery.
|
|
9
|
+
*
|
|
10
|
+
* Deep-frozen: this is a shared singleton, so runtime code can read it freely but must not mutate it — clone before
|
|
11
|
+
* layering repo-specific overrides on top.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_MINER_GOAL_SPEC = Object.freeze({
|
|
14
|
+
minerEnabled: true,
|
|
15
|
+
wantedPaths: Object.freeze([]),
|
|
16
|
+
blockedPaths: Object.freeze([]),
|
|
17
|
+
preferredLabels: Object.freeze([]),
|
|
18
|
+
blockedLabels: Object.freeze([]),
|
|
19
|
+
maxConcurrentClaims: 1,
|
|
20
|
+
issueDiscoveryPolicy: "neutral",
|
|
21
|
+
feasibilityGate: Object.freeze({ enabled: true, suppressedReasons: Object.freeze([]) }),
|
|
22
|
+
selfPlagiarism: Object.freeze({ similarityThreshold: DEFAULT_SELF_PLAGIARISM_SIMILARITY_THRESHOLD }),
|
|
23
|
+
killSwitch: Object.freeze({ paused: false }),
|
|
24
|
+
execution: Object.freeze({ liveModeOptIn: null }),
|
|
25
|
+
});
|
|
26
|
+
const MAX_MINER_GOAL_SPEC_BYTES = 32_768;
|
|
27
|
+
const MAX_LIST_ITEMS = 100;
|
|
28
|
+
const MAX_ITEM_LENGTH = 256;
|
|
29
|
+
function cloneDefaultMinerGoalSpec() {
|
|
30
|
+
return {
|
|
31
|
+
...DEFAULT_MINER_GOAL_SPEC,
|
|
32
|
+
wantedPaths: [...DEFAULT_MINER_GOAL_SPEC.wantedPaths],
|
|
33
|
+
blockedPaths: [...DEFAULT_MINER_GOAL_SPEC.blockedPaths],
|
|
34
|
+
preferredLabels: [...DEFAULT_MINER_GOAL_SPEC.preferredLabels],
|
|
35
|
+
blockedLabels: [...DEFAULT_MINER_GOAL_SPEC.blockedLabels],
|
|
36
|
+
feasibilityGate: {
|
|
37
|
+
enabled: DEFAULT_MINER_GOAL_SPEC.feasibilityGate.enabled,
|
|
38
|
+
suppressedReasons: [...DEFAULT_MINER_GOAL_SPEC.feasibilityGate.suppressedReasons],
|
|
39
|
+
},
|
|
40
|
+
selfPlagiarism: { ...DEFAULT_MINER_GOAL_SPEC.selfPlagiarism },
|
|
41
|
+
killSwitch: { ...DEFAULT_MINER_GOAL_SPEC.killSwitch },
|
|
42
|
+
execution: { ...DEFAULT_MINER_GOAL_SPEC.execution },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function emptyMinerGoalSpec(warnings = []) {
|
|
46
|
+
return { present: false, spec: cloneDefaultMinerGoalSpec(), warnings };
|
|
47
|
+
}
|
|
48
|
+
function normalizeStringList(value, field, warnings) {
|
|
49
|
+
if (value === undefined || value === null)
|
|
50
|
+
return [];
|
|
51
|
+
if (!Array.isArray(value)) {
|
|
52
|
+
warnings.push(`MinerGoalSpec field "${field}" must be a list; ignoring a ${typeof value} value.`);
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const result = [];
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
for (const [index, entry] of value.entries()) {
|
|
58
|
+
if (index >= MAX_LIST_ITEMS) {
|
|
59
|
+
warnings.push(`MinerGoalSpec field "${field}" exceeded ${MAX_LIST_ITEMS} entries; extra entries ignored.`);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
if (typeof entry !== "string") {
|
|
63
|
+
warnings.push(`MinerGoalSpec field "${field}" skipped a non-string entry.`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const trimmed = entry.trim();
|
|
67
|
+
if (!trimmed)
|
|
68
|
+
continue;
|
|
69
|
+
let normalized = trimmed;
|
|
70
|
+
if (normalized.length > MAX_ITEM_LENGTH) {
|
|
71
|
+
warnings.push(`MinerGoalSpec field "${field}" truncated an over-long entry.`);
|
|
72
|
+
normalized = normalized.slice(0, MAX_ITEM_LENGTH);
|
|
73
|
+
}
|
|
74
|
+
if (seen.has(normalized))
|
|
75
|
+
continue;
|
|
76
|
+
result.push(normalized);
|
|
77
|
+
seen.add(normalized);
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
function normalizeBoolean(value, field, fallback, warnings) {
|
|
82
|
+
if (value === undefined || value === null)
|
|
83
|
+
return fallback;
|
|
84
|
+
if (typeof value === "boolean")
|
|
85
|
+
return value;
|
|
86
|
+
warnings.push(`MinerGoalSpec field "${field}" must be a boolean; falling back to ${String(fallback)}.`);
|
|
87
|
+
return fallback;
|
|
88
|
+
}
|
|
89
|
+
function normalizeIssueDiscoveryPolicy(value, field, fallback, warnings) {
|
|
90
|
+
if (value === undefined || value === null)
|
|
91
|
+
return fallback;
|
|
92
|
+
if (value === "encouraged" || value === "neutral" || value === "discouraged")
|
|
93
|
+
return value;
|
|
94
|
+
warnings.push(`MinerGoalSpec field "${field}" must be one of encouraged, neutral, discouraged; falling back to "${fallback}".`);
|
|
95
|
+
return fallback;
|
|
96
|
+
}
|
|
97
|
+
function normalizeFeasibilityGatePolicy(value, field, fallback, warnings) {
|
|
98
|
+
if (value === undefined || value === null)
|
|
99
|
+
return fallback;
|
|
100
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
101
|
+
warnings.push(`MinerGoalSpec field "${field}" must be a mapping; falling back to defaults.`);
|
|
102
|
+
return fallback;
|
|
103
|
+
}
|
|
104
|
+
const record = value;
|
|
105
|
+
return {
|
|
106
|
+
enabled: normalizeBoolean(record.enabled, `${field}.enabled`, fallback.enabled, warnings),
|
|
107
|
+
suppressedReasons: normalizeStringList(record.suppressedReasons, `${field}.suppressedReasons`, warnings),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function normalizeSelfPlagiarismPolicy(value, field, fallback, warnings) {
|
|
111
|
+
if (value === undefined || value === null)
|
|
112
|
+
return fallback;
|
|
113
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
114
|
+
warnings.push(`MinerGoalSpec field "${field}" must be a mapping; falling back to defaults.`);
|
|
115
|
+
return fallback;
|
|
116
|
+
}
|
|
117
|
+
const resolved = resolveSelfPlagiarismConfig(value);
|
|
118
|
+
const record = value;
|
|
119
|
+
if (record.similarityThreshold !== undefined &&
|
|
120
|
+
typeof record.similarityThreshold !== "number") {
|
|
121
|
+
warnings.push(`MinerGoalSpec field "${field}.similarityThreshold" must be a number; falling back to ${fallback.similarityThreshold}.`);
|
|
122
|
+
return fallback;
|
|
123
|
+
}
|
|
124
|
+
return resolved;
|
|
125
|
+
}
|
|
126
|
+
function normalizeKillSwitchPolicy(value, field, fallback, warnings) {
|
|
127
|
+
if (value === undefined || value === null)
|
|
128
|
+
return fallback;
|
|
129
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
130
|
+
warnings.push(`MinerGoalSpec field "${field}" must be a mapping; falling back to defaults.`);
|
|
131
|
+
return fallback;
|
|
132
|
+
}
|
|
133
|
+
const record = value;
|
|
134
|
+
return {
|
|
135
|
+
paused: normalizeBoolean(record.paused, `${field}.paused`, fallback.paused, warnings),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function normalizeExecutionPolicy(value, field, fallback, warnings) {
|
|
139
|
+
if (value === undefined || value === null)
|
|
140
|
+
return fallback;
|
|
141
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
142
|
+
warnings.push(`MinerGoalSpec field "${field}" must be a mapping; falling back to defaults.`);
|
|
143
|
+
return fallback;
|
|
144
|
+
}
|
|
145
|
+
const record = value;
|
|
146
|
+
const raw = record.liveModeOptIn;
|
|
147
|
+
if (raw === undefined || raw === null)
|
|
148
|
+
return { liveModeOptIn: fallback.liveModeOptIn };
|
|
149
|
+
if (typeof raw !== "string") {
|
|
150
|
+
warnings.push(`MinerGoalSpec field "${field}.liveModeOptIn" must be a string; falling back to dry-run.`);
|
|
151
|
+
return { liveModeOptIn: fallback.liveModeOptIn };
|
|
152
|
+
}
|
|
153
|
+
// A string that isn't the exact opt-in literal is NOT malformed (it's a valid string, just not the one value
|
|
154
|
+
// that grants live mode) -- no warning, just a silent, safe fall-through to dry-run. Only a wrong TYPE above
|
|
155
|
+
// warns, matching every other field's tolerant-parse convention.
|
|
156
|
+
return { liveModeOptIn: raw === MINER_LIVE_MODE_OPT_IN ? raw : null };
|
|
157
|
+
}
|
|
158
|
+
function normalizePositiveInteger(value, field, fallback, warnings) {
|
|
159
|
+
if (value === undefined || value === null)
|
|
160
|
+
return fallback;
|
|
161
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
162
|
+
warnings.push(`MinerGoalSpec field "${field}" must be a positive whole number; falling back to ${fallback}.`);
|
|
163
|
+
return fallback;
|
|
164
|
+
}
|
|
165
|
+
const normalized = Math.floor(value);
|
|
166
|
+
if (normalized >= 1)
|
|
167
|
+
return normalized;
|
|
168
|
+
warnings.push(`MinerGoalSpec field "${field}" must be >= 1 after flooring; falling back to ${fallback}.`);
|
|
169
|
+
return fallback;
|
|
170
|
+
}
|
|
171
|
+
function utf8ByteLength(value) {
|
|
172
|
+
let bytes = 0;
|
|
173
|
+
for (const char of value) {
|
|
174
|
+
const codePoint = char.codePointAt(0);
|
|
175
|
+
if (codePoint <= 0x7f)
|
|
176
|
+
bytes += 1;
|
|
177
|
+
else if (codePoint <= 0x7ff)
|
|
178
|
+
bytes += 2;
|
|
179
|
+
else if (codePoint <= 0xffff)
|
|
180
|
+
bytes += 3;
|
|
181
|
+
else
|
|
182
|
+
bytes += 4;
|
|
183
|
+
}
|
|
184
|
+
return bytes;
|
|
185
|
+
}
|
|
186
|
+
function hasConfiguredGoalFields(spec) {
|
|
187
|
+
return (spec.minerEnabled !== DEFAULT_MINER_GOAL_SPEC.minerEnabled ||
|
|
188
|
+
spec.wantedPaths.length > 0 ||
|
|
189
|
+
spec.blockedPaths.length > 0 ||
|
|
190
|
+
spec.preferredLabels.length > 0 ||
|
|
191
|
+
spec.blockedLabels.length > 0 ||
|
|
192
|
+
spec.maxConcurrentClaims !== DEFAULT_MINER_GOAL_SPEC.maxConcurrentClaims ||
|
|
193
|
+
spec.issueDiscoveryPolicy !== DEFAULT_MINER_GOAL_SPEC.issueDiscoveryPolicy ||
|
|
194
|
+
spec.feasibilityGate.enabled !== DEFAULT_MINER_GOAL_SPEC.feasibilityGate.enabled ||
|
|
195
|
+
spec.feasibilityGate.suppressedReasons.length > 0 ||
|
|
196
|
+
spec.selfPlagiarism.similarityThreshold !== DEFAULT_MINER_GOAL_SPEC.selfPlagiarism.similarityThreshold ||
|
|
197
|
+
spec.killSwitch.paused !== DEFAULT_MINER_GOAL_SPEC.killSwitch.paused ||
|
|
198
|
+
spec.execution.liveModeOptIn !== DEFAULT_MINER_GOAL_SPEC.execution.liveModeOptIn);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Tolerantly normalize an already-parsed `.gittensory-miner.yml` object into a {@link ParsedMinerGoalSpec}.
|
|
202
|
+
* Never throws: malformed shapes degrade to safe defaults and accumulate warnings so callers can surface
|
|
203
|
+
* "your miner goal spec had problems" without hard-failing a run.
|
|
204
|
+
*/
|
|
205
|
+
export function parseMinerGoalSpec(raw) {
|
|
206
|
+
if (raw === undefined || raw === null)
|
|
207
|
+
return emptyMinerGoalSpec();
|
|
208
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
209
|
+
return emptyMinerGoalSpec([
|
|
210
|
+
"MinerGoalSpec must be a mapping of fields; ignoring malformed config and falling back to safe defaults.",
|
|
211
|
+
]);
|
|
212
|
+
}
|
|
213
|
+
const record = raw;
|
|
214
|
+
const warnings = [];
|
|
215
|
+
const spec = {
|
|
216
|
+
minerEnabled: normalizeBoolean(record.minerEnabled, "minerEnabled", DEFAULT_MINER_GOAL_SPEC.minerEnabled, warnings),
|
|
217
|
+
wantedPaths: normalizeStringList(record.wantedPaths, "wantedPaths", warnings),
|
|
218
|
+
blockedPaths: normalizeStringList(record.blockedPaths, "blockedPaths", warnings),
|
|
219
|
+
preferredLabels: normalizeStringList(record.preferredLabels, "preferredLabels", warnings),
|
|
220
|
+
blockedLabels: normalizeStringList(record.blockedLabels, "blockedLabels", warnings),
|
|
221
|
+
maxConcurrentClaims: normalizePositiveInteger(record.maxConcurrentClaims, "maxConcurrentClaims", DEFAULT_MINER_GOAL_SPEC.maxConcurrentClaims, warnings),
|
|
222
|
+
issueDiscoveryPolicy: normalizeIssueDiscoveryPolicy(record.issueDiscoveryPolicy, "issueDiscoveryPolicy", DEFAULT_MINER_GOAL_SPEC.issueDiscoveryPolicy, warnings),
|
|
223
|
+
feasibilityGate: normalizeFeasibilityGatePolicy(record.feasibilityGate, "feasibilityGate", DEFAULT_MINER_GOAL_SPEC.feasibilityGate, warnings),
|
|
224
|
+
selfPlagiarism: normalizeSelfPlagiarismPolicy(record.selfPlagiarism, "selfPlagiarism", DEFAULT_MINER_GOAL_SPEC.selfPlagiarism, warnings),
|
|
225
|
+
killSwitch: normalizeKillSwitchPolicy(record.killSwitch, "killSwitch", DEFAULT_MINER_GOAL_SPEC.killSwitch, warnings),
|
|
226
|
+
execution: normalizeExecutionPolicy(record.execution, "execution", DEFAULT_MINER_GOAL_SPEC.execution, warnings),
|
|
227
|
+
};
|
|
228
|
+
if (!hasConfiguredGoalFields(spec)) {
|
|
229
|
+
warnings.push("MinerGoalSpec contained no recognized non-default goal fields; falling back to safe defaults.");
|
|
230
|
+
return { present: false, spec: cloneDefaultMinerGoalSpec(), warnings };
|
|
231
|
+
}
|
|
232
|
+
return { present: true, spec, warnings };
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Parse raw `.gittensory-miner.yml` file content (JSON or YAML). Malformed content degrades to an absent
|
|
236
|
+
* goal spec with a warning rather than throwing, mirroring `parseFocusManifestContent`.
|
|
237
|
+
*/
|
|
238
|
+
export function parseMinerGoalSpecContent(content) {
|
|
239
|
+
if (content === undefined || content === null || content.trim() === "")
|
|
240
|
+
return emptyMinerGoalSpec();
|
|
241
|
+
if (utf8ByteLength(content) > MAX_MINER_GOAL_SPEC_BYTES) {
|
|
242
|
+
return emptyMinerGoalSpec([
|
|
243
|
+
`MinerGoalSpec content exceeded ${MAX_MINER_GOAL_SPEC_BYTES} bytes; ignoring it and falling back to safe defaults.`,
|
|
244
|
+
]);
|
|
245
|
+
}
|
|
246
|
+
const trimmed = content.trim();
|
|
247
|
+
const looksLikeJson = trimmed.startsWith("{") || trimmed.startsWith("[");
|
|
248
|
+
let parsed;
|
|
249
|
+
try {
|
|
250
|
+
parsed = looksLikeJson ? JSON.parse(trimmed) : parseYaml(trimmed);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return emptyMinerGoalSpec([
|
|
254
|
+
looksLikeJson
|
|
255
|
+
? "MinerGoalSpec content was not valid JSON; ignoring it and falling back to safe defaults."
|
|
256
|
+
: "MinerGoalSpec content was not valid YAML; ignoring it and falling back to safe defaults.",
|
|
257
|
+
]);
|
|
258
|
+
}
|
|
259
|
+
return parseMinerGoalSpec(parsed);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The documented `.gittensory-miner` file-discovery order (first match wins), mirroring how `.gittensory.yml` is
|
|
263
|
+
* discovered: repo-root YAML, then `.github/` YAML, then the JSON variants.
|
|
264
|
+
*/
|
|
265
|
+
export const MINER_GOAL_SPEC_FILENAMES = [
|
|
266
|
+
".gittensory-miner.yml",
|
|
267
|
+
".github/gittensory-miner.yml",
|
|
268
|
+
".gittensory-miner.json",
|
|
269
|
+
".github/gittensory-miner.json",
|
|
270
|
+
];
|
|
271
|
+
/**
|
|
272
|
+
* The first {@link MINER_GOAL_SPEC_FILENAMES} candidate that exists, or null. Pure: the caller injects the existence
|
|
273
|
+
* check (e.g. `fs.existsSync`) so this module stays IO-free and unit-testable. A caller reads the returned path and
|
|
274
|
+
* feeds its content to {@link parseMinerGoalSpecContent}.
|
|
275
|
+
*/
|
|
276
|
+
export function discoverMinerGoalSpecPath(exists) {
|
|
277
|
+
for (const name of MINER_GOAL_SPEC_FILENAMES)
|
|
278
|
+
if (exists(name))
|
|
279
|
+
return name;
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const MINER_PREDICTIONS_TOTAL = "gittensory_miner_predictions_total";
|
|
2
|
+
export declare const MINER_PREDICTION_CORRECT_TOTAL = "gittensory_miner_prediction_correct_total";
|
|
3
|
+
export declare const MINER_PREDICTION_INCORRECT_TOTAL = "gittensory_miner_prediction_incorrect_total";
|
|
4
|
+
/** One prediction-ledger row for metrics: its predicted `conclusion`, plus an optional realized-outcome pairing
|
|
5
|
+
* (`correct`: true = matched, false = differed, null/undefined = not yet resolved). */
|
|
6
|
+
export type MinerPredictionMetricRow = {
|
|
7
|
+
conclusion: string;
|
|
8
|
+
correct?: boolean | null;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Render prediction-calibration counters as Prometheus text-exposition format. Pure and side-effect-free: a caller
|
|
12
|
+
* supplies the ledger rows (joined with any resolved outcomes) and prints the result. Deterministic — conclusion
|
|
13
|
+
* series are emitted in sorted order. Always emits HELP/TYPE for every counter, so the surface is well-formed even
|
|
14
|
+
* for an empty ledger.
|
|
15
|
+
*/
|
|
16
|
+
export declare function renderMinerPredictionMetrics(rows: readonly MinerPredictionMetricRow[]): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Miner prediction-calibration metrics (#4264). A pure Prometheus text-exposition renderer for the miner's own
|
|
2
|
+
// predicted-gate accuracy, the miner-side counterpart to the server's src/selfhost/metrics.ts registry. It turns
|
|
3
|
+
// prediction-ledger rows (packages/gittensory-miner/lib/prediction-ledger.js `readPredictions`) — optionally
|
|
4
|
+
// joined with their realized outcome — into counters a future dashboard can scrape.
|
|
5
|
+
//
|
|
6
|
+
// Scoped as an on-demand RENDERER, not a live HTTP registry: gittensory-miner is a local CLI, not a daemon, so a
|
|
7
|
+
// caller renders this to stdout for its own scrape/cron setup and reads the ledger itself (no data collection of
|
|
8
|
+
// its own lives here — this stays a pure, side-effect-free function like the rest of gittensory-engine). It mirrors
|
|
9
|
+
// the metric-naming (`gittensory_miner_*_total`) and HELP/TYPE/label conventions of src/selfhost/metrics.ts rather
|
|
10
|
+
// than importing across the package boundary.
|
|
11
|
+
//
|
|
12
|
+
// Counters emitted:
|
|
13
|
+
// - `gittensory_miner_predictions_total{conclusion="..."}` — predictions recorded, one series per predicted
|
|
14
|
+
// conclusion (e.g. merge/close/hold).
|
|
15
|
+
// - `gittensory_miner_prediction_correct_total` — predictions whose realized outcome matched the prediction.
|
|
16
|
+
// - `gittensory_miner_prediction_incorrect_total` — predictions whose realized outcome differed.
|
|
17
|
+
// The correct/incorrect counters only move for rows carrying a resolved outcome; unresolved rows count toward
|
|
18
|
+
// `predictions_total` only, so the surface is meaningful before outcome-pairing exists and grows once it does.
|
|
19
|
+
export const MINER_PREDICTIONS_TOTAL = "gittensory_miner_predictions_total";
|
|
20
|
+
export const MINER_PREDICTION_CORRECT_TOTAL = "gittensory_miner_prediction_correct_total";
|
|
21
|
+
export const MINER_PREDICTION_INCORRECT_TOTAL = "gittensory_miner_prediction_incorrect_total";
|
|
22
|
+
/** Mirror src/selfhost/metrics.ts:204 — HELP text escapes backslash and newline. */
|
|
23
|
+
function escapeHelpText(help) {
|
|
24
|
+
return help.replace(/\\/g, "\\\\").replace(/\n/g, "\\n");
|
|
25
|
+
}
|
|
26
|
+
/** Prometheus label-value escaping (backslash, double-quote, newline), a correctness-complete superset of
|
|
27
|
+
* src/selfhost/metrics.ts:193's `"`-only escape so an arbitrary conclusion string can never break the line. */
|
|
28
|
+
function escapeLabelValue(value) {
|
|
29
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Render prediction-calibration counters as Prometheus text-exposition format. Pure and side-effect-free: a caller
|
|
33
|
+
* supplies the ledger rows (joined with any resolved outcomes) and prints the result. Deterministic — conclusion
|
|
34
|
+
* series are emitted in sorted order. Always emits HELP/TYPE for every counter, so the surface is well-formed even
|
|
35
|
+
* for an empty ledger.
|
|
36
|
+
*/
|
|
37
|
+
export function renderMinerPredictionMetrics(rows) {
|
|
38
|
+
const totalByConclusion = new Map();
|
|
39
|
+
let correct = 0;
|
|
40
|
+
let incorrect = 0;
|
|
41
|
+
for (const row of rows) {
|
|
42
|
+
totalByConclusion.set(row.conclusion, (totalByConclusion.get(row.conclusion) ?? 0) + 1);
|
|
43
|
+
if (row.correct === true)
|
|
44
|
+
correct += 1;
|
|
45
|
+
else if (row.correct === false)
|
|
46
|
+
incorrect += 1;
|
|
47
|
+
}
|
|
48
|
+
const lines = [];
|
|
49
|
+
lines.push(`# HELP ${MINER_PREDICTIONS_TOTAL} ${escapeHelpText("Gate-outcome predictions the miner has recorded, by predicted conclusion.")}`);
|
|
50
|
+
lines.push(`# TYPE ${MINER_PREDICTIONS_TOTAL} counter`);
|
|
51
|
+
for (const [conclusion, count] of [...totalByConclusion.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
52
|
+
lines.push(`${MINER_PREDICTIONS_TOTAL}{conclusion="${escapeLabelValue(conclusion)}"} ${count}`);
|
|
53
|
+
}
|
|
54
|
+
lines.push(`# HELP ${MINER_PREDICTION_CORRECT_TOTAL} ${escapeHelpText("Predictions whose realized outcome matched the predicted conclusion.")}`);
|
|
55
|
+
lines.push(`# TYPE ${MINER_PREDICTION_CORRECT_TOTAL} counter`);
|
|
56
|
+
lines.push(`${MINER_PREDICTION_CORRECT_TOTAL} ${correct}`);
|
|
57
|
+
lines.push(`# HELP ${MINER_PREDICTION_INCORRECT_TOTAL} ${escapeHelpText("Predictions whose realized outcome differed from the predicted conclusion.")}`);
|
|
58
|
+
lines.push(`# TYPE ${MINER_PREDICTION_INCORRECT_TOTAL} counter`);
|
|
59
|
+
lines.push(`${MINER_PREDICTION_INCORRECT_TOTAL} ${incorrect}`);
|
|
60
|
+
return `${lines.join("\n")}\n`;
|
|
61
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Immutable discovery-plane telemetry event vocabulary — an unknown value fails closed before it is recorded. */
|
|
2
|
+
export declare const MINER_TELEMETRY_EVENT_TYPES: readonly ["query_issued", "candidates_returned", "soft_claim_attempted", "soft_claim_succeeded", "soft_claim_collided"];
|
|
3
|
+
export type MinerTelemetryEventType = (typeof MINER_TELEMETRY_EVENT_TYPES)[number];
|
|
4
|
+
/** Fixed low-cardinality outcome buckets — the discovery-plane analogue of orb-collector's `bucketReasonCode`, so a
|
|
5
|
+
* free-text reason can never leak through the telemetry surface. */
|
|
6
|
+
export declare const MINER_TELEMETRY_OUTCOME_BUCKETS: readonly ["ok", "empty", "collision", "rate_limited", "error", "other"];
|
|
7
|
+
export type MinerTelemetryOutcomeBucket = (typeof MINER_TELEMETRY_OUTCOME_BUCKETS)[number];
|
|
8
|
+
/** A single discovery-plane telemetry event, pre-anonymization-checked. `repoHash`/`issueHash` are the exporter's
|
|
9
|
+
* per-instance HMAC hashes (orb-collector's `getOrCreateAnonSecret`/`hmacField` posture) — NEVER a raw
|
|
10
|
+
* `owner/repo` or issue number. `metrics` is count-only quantitative data (e.g. `candidatesReturned`), never text. */
|
|
11
|
+
export type MinerTelemetryEvent = {
|
|
12
|
+
eventType: MinerTelemetryEventType;
|
|
13
|
+
repoHash?: string | null | undefined;
|
|
14
|
+
issueHash?: string | null | undefined;
|
|
15
|
+
outcome: MinerTelemetryOutcomeBucket;
|
|
16
|
+
metrics?: Record<string, number> | undefined;
|
|
17
|
+
};
|
|
18
|
+
/** The normalized, storage/transport-ready form: hashes coerced to `string | null`, metrics serialized to JSON. */
|
|
19
|
+
export type NormalizedMinerTelemetryEvent = {
|
|
20
|
+
eventType: MinerTelemetryEventType;
|
|
21
|
+
repoHash: string | null;
|
|
22
|
+
issueHash: string | null;
|
|
23
|
+
outcome: MinerTelemetryOutcomeBucket;
|
|
24
|
+
metricsJson: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Validate and normalize a discovery-plane telemetry event before it is recorded/exported. Fail-closed, mirroring
|
|
28
|
+
* {@link normalizeGovernorLedgerEvent}: an unknown `eventType`/`outcome`, a non-hash identifier, or a non-numeric
|
|
29
|
+
* metric throws rather than silently shipping malformed or de-anonymizing data. Defines the contract only — it does
|
|
30
|
+
* NOT perform the HMAC hashing (that is the exporter's job at #4250's boundary) or send anything.
|
|
31
|
+
*/
|
|
32
|
+
export declare function normalizeMinerTelemetryEvent(input: unknown): NormalizedMinerTelemetryEvent;
|