@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,40 @@
|
|
|
1
|
+
export const DEFAULT_ADVISORY_AI_ROUTING = {
|
|
2
|
+
slop: false,
|
|
3
|
+
e2eTestGen: false,
|
|
4
|
+
planner: false,
|
|
5
|
+
summaries: false,
|
|
6
|
+
chatQa: false,
|
|
7
|
+
chatQaFrontierFallback: false,
|
|
8
|
+
intentRouting: false,
|
|
9
|
+
};
|
|
10
|
+
function normalizeField(value, field, warnings) {
|
|
11
|
+
if (value === undefined)
|
|
12
|
+
return DEFAULT_ADVISORY_AI_ROUTING[field];
|
|
13
|
+
if (typeof value === "boolean")
|
|
14
|
+
return value;
|
|
15
|
+
warnings.push(`settings.advisoryAiRouting.${field} must be a boolean; using the default "${DEFAULT_ADVISORY_AI_ROUTING[field]}".`);
|
|
16
|
+
return DEFAULT_ADVISORY_AI_ROUTING[field];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Normalize a raw `.gittensory.yml settings.advisoryAiRouting` value into a typed config, fail-safe: any
|
|
20
|
+
* malformed field falls back to its own (false) default and pushes a warning rather than rejecting the
|
|
21
|
+
* whole block. Mirrors `normalizeUnlinkedIssueGuardrailConfig`'s per-field discipline.
|
|
22
|
+
*/
|
|
23
|
+
export function normalizeAdvisoryAiRoutingConfig(input, warnings) {
|
|
24
|
+
if (input === undefined)
|
|
25
|
+
return { ...DEFAULT_ADVISORY_AI_ROUTING };
|
|
26
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
27
|
+
warnings.push("settings.advisoryAiRouting must be an object; using the default (every capability off).");
|
|
28
|
+
return { ...DEFAULT_ADVISORY_AI_ROUTING };
|
|
29
|
+
}
|
|
30
|
+
const record = input;
|
|
31
|
+
return {
|
|
32
|
+
slop: normalizeField(record.slop, "slop", warnings),
|
|
33
|
+
e2eTestGen: normalizeField(record.e2eTestGen, "e2eTestGen", warnings),
|
|
34
|
+
planner: normalizeField(record.planner, "planner", warnings),
|
|
35
|
+
summaries: normalizeField(record.summaries, "summaries", warnings),
|
|
36
|
+
chatQa: normalizeField(record.chatQa, "chatQa", warnings),
|
|
37
|
+
chatQaFrontierFallback: normalizeField(record.chatQaFrontierFallback, "chatQaFrontierFallback", warnings),
|
|
38
|
+
intentRouting: normalizeField(record.intentRouting, "intentRouting", warnings),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const LOOPOVER_GATE_CHECK_NAME = "LoopOver Orb Review Agent";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const LOOPOVER_GATE_CHECK_NAME = "LoopOver Orb Review Agent";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AdvisoryFinding } from "../types/predicted-gate-types.js";
|
|
2
|
+
/** Finding code raised when `gate.claMode` is opted in (advisory/block) and neither configured detection method
|
|
3
|
+
* (the PR body consent phrase, or the named CLA-bot check-run) confirms consent. ALWAYS severity "warning" at
|
|
4
|
+
* generation time — mirrors `manifest_missing_tests`/`manifest_linked_issue_required` (focus-manifest.ts): a
|
|
5
|
+
* single finding code whose escalation to a hard blocker is decided entirely by the configured gate MODE
|
|
6
|
+
* (isConfiguredGateBlocker, src/rules/advisory.ts), not by this evaluator. */
|
|
7
|
+
export declare const CLA_CONSENT_MISSING_CODE = "cla_consent_missing";
|
|
8
|
+
/** Finding code emitted when check-run detection is the ONLY configured method and its conclusion could not be
|
|
9
|
+
* resolved (a transient fetch failure, not a resolved "no such check-run"). Mirrors `pre_merge_check_unresolved`
|
|
10
|
+
* (review/pre-merge-checks.ts): isEvaluationBlocker (advisory.ts) treats this as a NEUTRAL gate (HELD,
|
|
11
|
+
* re-evaluates automatically) — never silently skipping a hard requirement and never hard-closing the
|
|
12
|
+
* contributor on a transient resolution miss. */
|
|
13
|
+
export declare const CLA_CHECK_UNRESOLVED_CODE = "cla_check_unresolved";
|
|
14
|
+
export type ClaCheckConfig = {
|
|
15
|
+
/** Public-safe-filtered consent phrase a maintainer requires somewhere in the PR body (case-insensitive
|
|
16
|
+
* substring match), e.g. "I have read and agree to the CLA". `null` ⇒ phrase-match detection is not configured. */
|
|
17
|
+
consentPhrase: string | null;
|
|
18
|
+
/** Name of a separate CLA-bot check-run this repo also runs (e.g. "CLA Assistant Lite"). When set, a
|
|
19
|
+
* `success`/`neutral` conclusion for a check-run with this exact name (case-insensitive) also satisfies
|
|
20
|
+
* consent. `null` ⇒ check-run detection is not configured. */
|
|
21
|
+
checkRunName: string | null;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Evaluate `.gittensory.yml gate.claMode` + `gate.cla` (consentPhrase / checkRunName) against a PR —
|
|
25
|
+
* DETERMINISTICALLY, mirroring the pre-merge-checks title/description phrase-match pattern (review/pre-merge-checks.ts)
|
|
26
|
+
* exactly: a case-insensitive substring match against already-resolved PR data, no AI judgment. Consent is
|
|
27
|
+
* satisfied when EITHER configured method holds (an "either" contract, not "all", because a repo may only be able
|
|
28
|
+
* to detect ONE method for a given PR — e.g. no check-run data was resolved): the PR body contains
|
|
29
|
+
* `consentPhrase`, OR a check-run named `checkRunName` concluded `success`/`neutral`. When NEITHER method is
|
|
30
|
+
* configured (both null), there is nothing to evaluate — no finding (byte-identical, matches `pre_merge_checks`'
|
|
31
|
+
* empty-checks behavior).
|
|
32
|
+
*
|
|
33
|
+
* `checkRunConclusion` is `undefined` when the caller could not resolve check-run data at all (a transient
|
|
34
|
+
* fetch failure, or the predicted-gate metadata-only path, which never sees live check-runs) — that is NOT the
|
|
35
|
+
* same as a resolved-but-absent check-run (`null`, "no check-run with this name exists"). When check-run
|
|
36
|
+
* detection is configured and its conclusion is unresolved (a transient fetch failure, or "not yet run"), this
|
|
37
|
+
* HOLDS (`cla_check_unresolved`) instead of failing closed — exactly like an unresolved changed-file set HOLDS
|
|
38
|
+
* a path-gated pre-merge check rather than silently skipping (auto-merge bypass) or hard-closing on a
|
|
39
|
+
* transient miss. This applies EVEN WHEN `consentPhrase` is ALSO configured but not (yet) satisfied: per the
|
|
40
|
+
* "either method holds ⇒ satisfied" contract above, an unresolved check-run might still satisfy consent, so
|
|
41
|
+
* deciding purely from a not-yet-satisfied phrase would hard-fail a PR the check-run could have saved (#2564
|
|
42
|
+
* gate-review finding). A hold only degrades to a hard `cla_consent_missing` once EVERY configured method has
|
|
43
|
+
* been definitively resolved and none of them is satisfied. Pure + side-effect-free; the caller pushes the
|
|
44
|
+
* finding into the advisory before the gate evaluates.
|
|
45
|
+
*/
|
|
46
|
+
export declare function evaluateClaCheck(config: ClaCheckConfig, ctx: {
|
|
47
|
+
body?: string | null | undefined;
|
|
48
|
+
checkRunConclusion?: string | null | undefined;
|
|
49
|
+
}): AdvisoryFinding[];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Finding code raised when `gate.claMode` is opted in (advisory/block) and neither configured detection method
|
|
2
|
+
* (the PR body consent phrase, or the named CLA-bot check-run) confirms consent. ALWAYS severity "warning" at
|
|
3
|
+
* generation time — mirrors `manifest_missing_tests`/`manifest_linked_issue_required` (focus-manifest.ts): a
|
|
4
|
+
* single finding code whose escalation to a hard blocker is decided entirely by the configured gate MODE
|
|
5
|
+
* (isConfiguredGateBlocker, src/rules/advisory.ts), not by this evaluator. */
|
|
6
|
+
export const CLA_CONSENT_MISSING_CODE = "cla_consent_missing";
|
|
7
|
+
/** Finding code emitted when check-run detection is the ONLY configured method and its conclusion could not be
|
|
8
|
+
* resolved (a transient fetch failure, not a resolved "no such check-run"). Mirrors `pre_merge_check_unresolved`
|
|
9
|
+
* (review/pre-merge-checks.ts): isEvaluationBlocker (advisory.ts) treats this as a NEUTRAL gate (HELD,
|
|
10
|
+
* re-evaluates automatically) — never silently skipping a hard requirement and never hard-closing the
|
|
11
|
+
* contributor on a transient resolution miss. */
|
|
12
|
+
export const CLA_CHECK_UNRESOLVED_CODE = "cla_check_unresolved";
|
|
13
|
+
/**
|
|
14
|
+
* Evaluate `.gittensory.yml gate.claMode` + `gate.cla` (consentPhrase / checkRunName) against a PR —
|
|
15
|
+
* DETERMINISTICALLY, mirroring the pre-merge-checks title/description phrase-match pattern (review/pre-merge-checks.ts)
|
|
16
|
+
* exactly: a case-insensitive substring match against already-resolved PR data, no AI judgment. Consent is
|
|
17
|
+
* satisfied when EITHER configured method holds (an "either" contract, not "all", because a repo may only be able
|
|
18
|
+
* to detect ONE method for a given PR — e.g. no check-run data was resolved): the PR body contains
|
|
19
|
+
* `consentPhrase`, OR a check-run named `checkRunName` concluded `success`/`neutral`. When NEITHER method is
|
|
20
|
+
* configured (both null), there is nothing to evaluate — no finding (byte-identical, matches `pre_merge_checks`'
|
|
21
|
+
* empty-checks behavior).
|
|
22
|
+
*
|
|
23
|
+
* `checkRunConclusion` is `undefined` when the caller could not resolve check-run data at all (a transient
|
|
24
|
+
* fetch failure, or the predicted-gate metadata-only path, which never sees live check-runs) — that is NOT the
|
|
25
|
+
* same as a resolved-but-absent check-run (`null`, "no check-run with this name exists"). When check-run
|
|
26
|
+
* detection is configured and its conclusion is unresolved (a transient fetch failure, or "not yet run"), this
|
|
27
|
+
* HOLDS (`cla_check_unresolved`) instead of failing closed — exactly like an unresolved changed-file set HOLDS
|
|
28
|
+
* a path-gated pre-merge check rather than silently skipping (auto-merge bypass) or hard-closing on a
|
|
29
|
+
* transient miss. This applies EVEN WHEN `consentPhrase` is ALSO configured but not (yet) satisfied: per the
|
|
30
|
+
* "either method holds ⇒ satisfied" contract above, an unresolved check-run might still satisfy consent, so
|
|
31
|
+
* deciding purely from a not-yet-satisfied phrase would hard-fail a PR the check-run could have saved (#2564
|
|
32
|
+
* gate-review finding). A hold only degrades to a hard `cla_consent_missing` once EVERY configured method has
|
|
33
|
+
* been definitively resolved and none of them is satisfied. Pure + side-effect-free; the caller pushes the
|
|
34
|
+
* finding into the advisory before the gate evaluates.
|
|
35
|
+
*/
|
|
36
|
+
export function evaluateClaCheck(config, ctx) {
|
|
37
|
+
if (config.consentPhrase === null && config.checkRunName === null)
|
|
38
|
+
return []; // nothing configured ⇒ no finding
|
|
39
|
+
const phraseSatisfied = config.consentPhrase !== null && (ctx.body ?? "").toLowerCase().includes(config.consentPhrase.toLowerCase());
|
|
40
|
+
const checkRunSatisfied = config.checkRunName !== null && (ctx.checkRunConclusion === "success" || ctx.checkRunConclusion === "neutral");
|
|
41
|
+
if (phraseSatisfied || checkRunSatisfied)
|
|
42
|
+
return [];
|
|
43
|
+
// A configured check-run whose conclusion is unresolved: cannot confirm OR deny consent via that method, so
|
|
44
|
+
// HOLD rather than fail closed — regardless of whether consentPhrase is ALSO configured (a not-yet-satisfied
|
|
45
|
+
// phrase does not mean consent is definitively absent while the check-run could still satisfy it).
|
|
46
|
+
if (config.checkRunName !== null && ctx.checkRunConclusion === undefined) {
|
|
47
|
+
return [
|
|
48
|
+
{
|
|
49
|
+
code: CLA_CHECK_UNRESOLVED_CODE,
|
|
50
|
+
severity: "warning",
|
|
51
|
+
title: `CLA check held — "${config.checkRunName}" not resolved`,
|
|
52
|
+
detail: `Gittensory could not resolve the "${config.checkRunName}" check-run's conclusion for this PR; the gate is held and re-evaluates automatically.`,
|
|
53
|
+
action: "No action needed — the gate re-evaluates once the check-run's conclusion is available.",
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
const missing = [];
|
|
58
|
+
if (config.consentPhrase !== null)
|
|
59
|
+
missing.push(`the PR description must contain "${config.consentPhrase}"`);
|
|
60
|
+
if (config.checkRunName !== null)
|
|
61
|
+
missing.push(`the "${config.checkRunName}" check must pass`);
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
code: CLA_CONSENT_MISSING_CODE,
|
|
65
|
+
severity: "warning",
|
|
66
|
+
title: "CLA consent not confirmed",
|
|
67
|
+
detail: `This PR does not confirm contributor license agreement consent: ${missing.join(" or ")}.`,
|
|
68
|
+
action: "Add the required CLA consent phrase to the PR description, or complete the CLA check, then re-run the gate.",
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function diffFilePriority(path: string): number;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isTestPath } from "../signals/test-evidence.js";
|
|
2
|
+
export function diffFilePriority(path) {
|
|
3
|
+
if (/(^|\/)(package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lock|bun\.lockb|cargo\.lock|poetry\.lock|pipfile\.lock|composer\.lock|gemfile\.lock|go\.sum|go\.work\.sum|uv\.lock|packages\.lock\.json|flake\.lock|deno\.lock|pubspec\.lock|podfile\.lock|mix\.lock|package\.resolved|gradle\.lockfile|pdm\.lock|conan\.lock|pixi\.lock|cartfile\.resolved|gopkg\.lock|shard\.lock|rebar\.lock|renv\.lock|chart\.lock)$|\.(min\.(js|css)|map|snap)$/i.test(path))
|
|
4
|
+
return 4;
|
|
5
|
+
if (/(^|\/)(dist|build|out|coverage|vendor|node_modules)\//i.test(path))
|
|
6
|
+
return 4;
|
|
7
|
+
if (/\.(md|mdx|markdown|rst|adoc|asciidoc|txt)$/i.test(path))
|
|
8
|
+
return 2;
|
|
9
|
+
if (isTestPath(path))
|
|
10
|
+
return 1;
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const REES_ANALYZER_NAMES: readonly ["dependency", "dependencyDiff", "lockfileDrift", "secret", "license", "installScript", "heavyDependency", "hardcodedUrl", "actionPin", "eol", "redos", "provenance", "codeowners", "secretLog", "assetWeight", "typosquat", "commitSignature", "iacMisconfig", "nativeBuild", "history", "docCommentDrift", "duplication", "churnHotspot", "blameLink", "approvalIntegrity", "ciCheckSignals", "undocumentedExport", "staleBranch", "commitHygiene", "pendingReviewRequests", "testRatio", "migrationSafety", "looseRange", "terminology", "todoMarker", "magicNumber", "conflictMarker", "debugLeftover", "sizeSmell", "floatingPromise", "deepNesting", "errorSwallow", "complexity", "complexityDelta", "unsafeAny", "a11y", "i18n", "unusedExport", "exhaustiveness", "flakyTest", "commitLint", "apiBreak", "deprecatedDep", "revertRecurrence", "coverageDelta", "callerImpact"];
|
|
2
|
+
export type ReesAnalyzerName = (typeof REES_ANALYZER_NAMES)[number];
|
|
3
|
+
export declare const REES_ANALYZER_NAME_SET: ReadonlySet<string>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Canonical REES enrichment-analyzer name registry (#2050). The single source of truth for the analyzer keys that
|
|
2
|
+
// both the operator `REES_ANALYZERS` env list and the per-repo `.gittensory.yml` `review.enrichment` toggles are
|
|
3
|
+
// validated against. A leaf module with no imports, so the review wiring and the signals-layer manifest parser can
|
|
4
|
+
// share it without a heavy or circular dependency.
|
|
5
|
+
export const REES_ANALYZER_NAMES = [
|
|
6
|
+
"dependency",
|
|
7
|
+
"dependencyDiff",
|
|
8
|
+
"lockfileDrift",
|
|
9
|
+
"secret",
|
|
10
|
+
"license",
|
|
11
|
+
"installScript",
|
|
12
|
+
"heavyDependency",
|
|
13
|
+
"hardcodedUrl",
|
|
14
|
+
"actionPin",
|
|
15
|
+
"eol",
|
|
16
|
+
"redos",
|
|
17
|
+
"provenance",
|
|
18
|
+
"codeowners",
|
|
19
|
+
"secretLog",
|
|
20
|
+
"assetWeight",
|
|
21
|
+
"typosquat",
|
|
22
|
+
"commitSignature",
|
|
23
|
+
"iacMisconfig",
|
|
24
|
+
"nativeBuild",
|
|
25
|
+
"history",
|
|
26
|
+
"docCommentDrift",
|
|
27
|
+
"duplication",
|
|
28
|
+
"churnHotspot",
|
|
29
|
+
"blameLink",
|
|
30
|
+
"approvalIntegrity",
|
|
31
|
+
"ciCheckSignals",
|
|
32
|
+
"undocumentedExport",
|
|
33
|
+
"staleBranch",
|
|
34
|
+
"commitHygiene",
|
|
35
|
+
"pendingReviewRequests",
|
|
36
|
+
"testRatio",
|
|
37
|
+
"migrationSafety",
|
|
38
|
+
"looseRange",
|
|
39
|
+
"terminology",
|
|
40
|
+
"todoMarker",
|
|
41
|
+
"magicNumber",
|
|
42
|
+
"conflictMarker",
|
|
43
|
+
"debugLeftover",
|
|
44
|
+
"sizeSmell",
|
|
45
|
+
"floatingPromise",
|
|
46
|
+
"deepNesting",
|
|
47
|
+
"errorSwallow",
|
|
48
|
+
"complexity",
|
|
49
|
+
"complexityDelta",
|
|
50
|
+
"unsafeAny",
|
|
51
|
+
"a11y",
|
|
52
|
+
"i18n",
|
|
53
|
+
"unusedExport",
|
|
54
|
+
"exhaustiveness",
|
|
55
|
+
"flakyTest",
|
|
56
|
+
"commitLint",
|
|
57
|
+
"apiBreak",
|
|
58
|
+
"deprecatedDep",
|
|
59
|
+
"revertRecurrence",
|
|
60
|
+
"coverageDelta",
|
|
61
|
+
"callerImpact",
|
|
62
|
+
];
|
|
63
|
+
export const REES_ANALYZER_NAME_SET = new Set(REES_ANALYZER_NAMES);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RepositorySettings } from "../types/predicted-gate-types.js";
|
|
2
|
+
export declare const CONFIG_AS_CODE_GUARDRAIL_GLOBS: string[];
|
|
3
|
+
export declare const WORKFLOW_AND_RUNTIME_GUARDRAIL_GLOBS: string[];
|
|
4
|
+
export declare const ENGINE_DECISION_GUARDRAIL_GLOBS: string[];
|
|
5
|
+
export declare const DEFAULT_HARD_GUARDRAIL_GLOBS: string[];
|
|
6
|
+
/**
|
|
7
|
+
* Resolve hard-guardrail path globs from the already-effective repo settings. Built-in config-as-code,
|
|
8
|
+
* workflow/runtime, and engine decision guardrails are invariants; repo settings may only add globs.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveHardGuardrailGlobs(settings: Pick<RepositorySettings, "hardGuardrailGlobs"> | null | undefined): string[];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// New-brand (`loopover`) and legacy (`gittensory`) entries are both listed — this is a Set-membership guardrail
|
|
2
|
+
// list (order doesn't matter, unlike the loaders' priority-ordered candidate lists), so a contributor PR touching
|
|
3
|
+
// EITHER the new canonical `.loopover.*` config file or a still-supported legacy `.gittensory.*` one gets the same
|
|
4
|
+
// hard-guardrail protection (#4773 — dual-read, additive; the legacy globs are never removed).
|
|
5
|
+
export const CONFIG_AS_CODE_GUARDRAIL_GLOBS = [
|
|
6
|
+
".loopover.yml",
|
|
7
|
+
".loopover.yaml",
|
|
8
|
+
".loopover.json",
|
|
9
|
+
".github/loopover.yml",
|
|
10
|
+
".github/loopover.yaml",
|
|
11
|
+
".github/loopover.json",
|
|
12
|
+
".gittensory.yml",
|
|
13
|
+
".gittensory.yaml",
|
|
14
|
+
".gittensory.json",
|
|
15
|
+
".github/gittensory.yml",
|
|
16
|
+
".github/gittensory.yaml",
|
|
17
|
+
".github/gittensory.json",
|
|
18
|
+
"**/codecov.yml",
|
|
19
|
+
"**/codecov.yaml",
|
|
20
|
+
"**/.codecov.yml",
|
|
21
|
+
];
|
|
22
|
+
export const WORKFLOW_AND_RUNTIME_GUARDRAIL_GLOBS = [
|
|
23
|
+
".github/workflows/**",
|
|
24
|
+
"scripts/**",
|
|
25
|
+
"wrangler.jsonc",
|
|
26
|
+
"src/selfhost/**",
|
|
27
|
+
];
|
|
28
|
+
export const ENGINE_DECISION_GUARDRAIL_GLOBS = [
|
|
29
|
+
"src/rules/**",
|
|
30
|
+
"src/services/**",
|
|
31
|
+
"src/settings/agent-actions.ts",
|
|
32
|
+
"src/settings/agent-execution.ts",
|
|
33
|
+
"src/settings/agent-sweep.ts",
|
|
34
|
+
"src/settings/autonomy.ts",
|
|
35
|
+
"src/queue/**",
|
|
36
|
+
"src/github/pr-actions.ts",
|
|
37
|
+
"src/github/app.ts",
|
|
38
|
+
"src/github/backfill.ts",
|
|
39
|
+
// #4197: writes a real commit onto a CONTRIBUTOR's own PR branch (not a branch gittensory owns) — the same
|
|
40
|
+
// guardrail tier as pr-actions.ts/app.ts for the same reason, a new GitHub-write surface.
|
|
41
|
+
"src/github/e2e-test-commit.ts",
|
|
42
|
+
"src/scoring/**",
|
|
43
|
+
"src/auth/**",
|
|
44
|
+
"src/review/safety.ts",
|
|
45
|
+
"src/review/guardrail-config.ts",
|
|
46
|
+
"src/review/cutover-gate.ts",
|
|
47
|
+
"src/review/linked-issue-hard-rules.ts",
|
|
48
|
+
"src/review/outcomes-wire.ts",
|
|
49
|
+
];
|
|
50
|
+
export const DEFAULT_HARD_GUARDRAIL_GLOBS = [
|
|
51
|
+
...CONFIG_AS_CODE_GUARDRAIL_GLOBS,
|
|
52
|
+
...WORKFLOW_AND_RUNTIME_GUARDRAIL_GLOBS,
|
|
53
|
+
...ENGINE_DECISION_GUARDRAIL_GLOBS,
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* Resolve hard-guardrail path globs from the already-effective repo settings. Built-in config-as-code,
|
|
57
|
+
* workflow/runtime, and engine decision guardrails are invariants; repo settings may only add globs.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveHardGuardrailGlobs(settings) {
|
|
60
|
+
const configured = settings?.hardGuardrailGlobs;
|
|
61
|
+
return Array.from(new Set([...DEFAULT_HARD_GUARDRAIL_GLOBS, ...(Array.isArray(configured) ? configured : [])]));
|
|
62
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { LinkedIssueHardRulesConfig, LinkedIssueHardRulesMode } from "../types/manifest-deps-types.js";
|
|
2
|
+
export declare const DEFAULT_LINKED_ISSUE_HARD_RULES: LinkedIssueHardRulesConfig;
|
|
3
|
+
export declare function isLinkedIssueHardRuleMode(value: unknown): value is LinkedIssueHardRulesMode;
|
|
4
|
+
export declare function normalizeLinkedIssueHardRulesConfig(input: unknown, warnings: string[]): LinkedIssueHardRulesConfig;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const VALID_LINKED_ISSUE_HARD_RULE_MODES = ["block", "off"];
|
|
2
|
+
const DEFAULT_CLOSE_DELAY_SECONDS = 30;
|
|
3
|
+
const MAX_CLOSE_DELAY_SECONDS = 300;
|
|
4
|
+
export const DEFAULT_LINKED_ISSUE_HARD_RULES = {
|
|
5
|
+
ownerAssignedClose: "off",
|
|
6
|
+
assignedIssueClose: "off",
|
|
7
|
+
missingPointLabelClose: "off",
|
|
8
|
+
maintainerOnlyLabelClose: "off",
|
|
9
|
+
pointBearingLabels: [],
|
|
10
|
+
maintainerOnlyLabels: [],
|
|
11
|
+
defaultLabelRepo: false,
|
|
12
|
+
verifyBeforeClose: true,
|
|
13
|
+
closeDelaySeconds: DEFAULT_CLOSE_DELAY_SECONDS,
|
|
14
|
+
};
|
|
15
|
+
export function isLinkedIssueHardRuleMode(value) {
|
|
16
|
+
return typeof value === "string" && VALID_LINKED_ISSUE_HARD_RULE_MODES.includes(value);
|
|
17
|
+
}
|
|
18
|
+
function normalizeStringList(value, field, warnings) {
|
|
19
|
+
if (value === undefined)
|
|
20
|
+
return [];
|
|
21
|
+
if (!Array.isArray(value)) {
|
|
22
|
+
warnings.push(`settings.linkedIssueHardRules.${field} must be an array; using no labels.`);
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const labels = [];
|
|
26
|
+
for (const [index, item] of value.entries()) {
|
|
27
|
+
if (typeof item !== "string" || item.trim().length === 0) {
|
|
28
|
+
warnings.push(`settings.linkedIssueHardRules.${field}[${index}] must be a non-empty string; ignoring it.`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
labels.push(item.trim());
|
|
32
|
+
}
|
|
33
|
+
return labels;
|
|
34
|
+
}
|
|
35
|
+
function normalizeMode(value, field, warnings) {
|
|
36
|
+
if (value === undefined)
|
|
37
|
+
return DEFAULT_LINKED_ISSUE_HARD_RULES[field];
|
|
38
|
+
if (isLinkedIssueHardRuleMode(value))
|
|
39
|
+
return value;
|
|
40
|
+
warnings.push(`settings.linkedIssueHardRules.${field} must be one of block, off; using the default "${DEFAULT_LINKED_ISSUE_HARD_RULES[field]}".`);
|
|
41
|
+
return DEFAULT_LINKED_ISSUE_HARD_RULES[field];
|
|
42
|
+
}
|
|
43
|
+
function normalizeBoolean(value, field, warnings) {
|
|
44
|
+
if (value === undefined)
|
|
45
|
+
return DEFAULT_LINKED_ISSUE_HARD_RULES[field];
|
|
46
|
+
if (typeof value === "boolean")
|
|
47
|
+
return value;
|
|
48
|
+
warnings.push(`settings.linkedIssueHardRules.${field} must be a boolean; using the default "${DEFAULT_LINKED_ISSUE_HARD_RULES[field]}".`);
|
|
49
|
+
return DEFAULT_LINKED_ISSUE_HARD_RULES[field];
|
|
50
|
+
}
|
|
51
|
+
function normalizeCloseDelaySeconds(value, warnings) {
|
|
52
|
+
if (value === undefined)
|
|
53
|
+
return DEFAULT_LINKED_ISSUE_HARD_RULES.closeDelaySeconds;
|
|
54
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
55
|
+
warnings.push(`settings.linkedIssueHardRules.closeDelaySeconds must be a non-negative number; using the default "${DEFAULT_CLOSE_DELAY_SECONDS}".`);
|
|
56
|
+
return DEFAULT_CLOSE_DELAY_SECONDS;
|
|
57
|
+
}
|
|
58
|
+
return Math.min(MAX_CLOSE_DELAY_SECONDS, Math.floor(value));
|
|
59
|
+
}
|
|
60
|
+
export function normalizeLinkedIssueHardRulesConfig(input, warnings) {
|
|
61
|
+
if (input === undefined)
|
|
62
|
+
return { ...DEFAULT_LINKED_ISSUE_HARD_RULES, pointBearingLabels: [], maintainerOnlyLabels: [] };
|
|
63
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
64
|
+
warnings.push("settings.linkedIssueHardRules must be an object; using the default all-off policy.");
|
|
65
|
+
return { ...DEFAULT_LINKED_ISSUE_HARD_RULES, pointBearingLabels: [], maintainerOnlyLabels: [] };
|
|
66
|
+
}
|
|
67
|
+
const record = input;
|
|
68
|
+
return {
|
|
69
|
+
ownerAssignedClose: normalizeMode(record.ownerAssignedClose, "ownerAssignedClose", warnings),
|
|
70
|
+
assignedIssueClose: normalizeMode(record.assignedIssueClose, "assignedIssueClose", warnings),
|
|
71
|
+
missingPointLabelClose: normalizeMode(record.missingPointLabelClose, "missingPointLabelClose", warnings),
|
|
72
|
+
maintainerOnlyLabelClose: normalizeMode(record.maintainerOnlyLabelClose, "maintainerOnlyLabelClose", warnings),
|
|
73
|
+
pointBearingLabels: normalizeStringList(record.pointBearingLabels, "pointBearingLabels", warnings),
|
|
74
|
+
maintainerOnlyLabels: normalizeStringList(record.maintainerOnlyLabels, "maintainerOnlyLabels", warnings),
|
|
75
|
+
defaultLabelRepo: normalizeBoolean(record.defaultLabelRepo, "defaultLabelRepo", warnings),
|
|
76
|
+
verifyBeforeClose: normalizeBoolean(record.verifyBeforeClose, "verifyBeforeClose", warnings),
|
|
77
|
+
closeDelaySeconds: normalizeCloseDelaySeconds(record.closeDelaySeconds, warnings),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LinkedIssueLabelPropagationConfig, LinkedIssueLabelPropagationMode } from "../types/manifest-deps-types.js";
|
|
2
|
+
export type { LinkedIssueLabelPropagationConfig, LinkedIssueLabelPropagationMapping, LinkedIssueLabelPropagationMode } from "../types/manifest-deps-types.js";
|
|
3
|
+
export declare const DEFAULT_LINKED_ISSUE_LABEL_PROPAGATION: LinkedIssueLabelPropagationConfig;
|
|
4
|
+
export declare const VALID_LINKED_ISSUE_LABEL_PROPAGATION_MODES: readonly LinkedIssueLabelPropagationMode[];
|
|
5
|
+
/** Defaults-fill a per-repo `linkedIssueLabelPropagation` override into an always-complete, safe config —
|
|
6
|
+
* mirrors `normalizeCommandAuthorizationPolicy`'s defaults-fill pattern
|
|
7
|
+
* (`src/settings/command-authorization.ts`). Malformed mapping entries are dropped with a warning; valid
|
|
8
|
+
* entries in the same array are kept (matches `commandAuthorization`'s per-entry `commands` validation). */
|
|
9
|
+
export declare function normalizeLinkedIssueLabelPropagationConfig(input: unknown, warnings: string[]): LinkedIssueLabelPropagationConfig;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Linked-issue label PROPAGATION (#priority-linked-issue-gate). Generic, config-driven mechanism: when a
|
|
2
|
+
// linked/closing issue already carries a configured label, copy a mapped label onto the PR. Built specifically
|
|
3
|
+
// so a maintainer-reward/bonus label (e.g. `gittensor:priority`) can NEVER be inferred from a PR's title,
|
|
4
|
+
// changed files, AI output, or existing PR labels — only ever from a linked issue that ALREADY carries it.
|
|
5
|
+
// Generic beyond that one use case: any self-hoster can map any issue label to any PR label, exclusive
|
|
6
|
+
// (replaces the normal bug/feature type label, like priority does) or additive (applied alongside it).
|
|
7
|
+
//
|
|
8
|
+
// PURE config types + normalizer only — no GitHub/fetch/Env-dependent imports. `focus-manifest.ts`'s YAML
|
|
9
|
+
// parser imports this module directly, and `focus-manifest.ts` is itself pulled into the gittensory-ui
|
|
10
|
+
// workspace's isolated typecheck (via `apps/gittensory-ui/src/lib/registration-workspace.ts`), which has no
|
|
11
|
+
// visibility into the Worker's ambient `Env` type. The actual GitHub fetch orchestrator
|
|
12
|
+
// (`fetchLinkedIssueLabelsForPropagation`) lives in `linked-issue-label-propagation-fetch.ts` instead, kept
|
|
13
|
+
// out of this file specifically so the UI workspace's typecheck never has to resolve `Env`.
|
|
14
|
+
// Fail-SAFE default: propagation OFF, no mappings. A self-hoster must explicitly opt in per repo.
|
|
15
|
+
export const DEFAULT_LINKED_ISSUE_LABEL_PROPAGATION = {
|
|
16
|
+
enabled: false,
|
|
17
|
+
mode: "exclusive_type_label",
|
|
18
|
+
mappings: [],
|
|
19
|
+
};
|
|
20
|
+
// Exported so `focus-manifest.ts`'s sparse-override parser can check whether a raw `mode` value is
|
|
21
|
+
// actually valid before deciding to copy the normalizer's (possibly defaults-filled-on-invalid) result.
|
|
22
|
+
export const VALID_LINKED_ISSUE_LABEL_PROPAGATION_MODES = ["exclusive_type_label"];
|
|
23
|
+
function normalizeMapping(input, index, warnings) {
|
|
24
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
25
|
+
warnings.push(`settings.linkedIssueLabelPropagation.mappings[${index}] must be an object; ignoring it.`);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const record = input;
|
|
29
|
+
const issueLabel = typeof record.issueLabel === "string" ? record.issueLabel.trim() : "";
|
|
30
|
+
const prLabel = typeof record.prLabel === "string" ? record.prLabel.trim() : "";
|
|
31
|
+
if (issueLabel.length === 0 || prLabel.length === 0) {
|
|
32
|
+
warnings.push(`settings.linkedIssueLabelPropagation.mappings[${index}] must have non-empty "issueLabel" and "prLabel" strings; ignoring it.`);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
// `removeOtherTypeLabels` picks exclusive (replaces the type label, the gittensor:priority case) vs.
|
|
36
|
+
// additive (applied alongside it) -- silently coercing a present-but-wrong-shaped value (e.g. a quoted
|
|
37
|
+
// `"true"` string) to `false` could flip an intended-exclusive mapping to additive without any signal,
|
|
38
|
+
// so a present, non-boolean value drops the whole entry with a warning instead (omitted is still a
|
|
39
|
+
// normal, unwarned default of `false`).
|
|
40
|
+
if (record.removeOtherTypeLabels !== undefined && typeof record.removeOtherTypeLabels !== "boolean") {
|
|
41
|
+
warnings.push(`settings.linkedIssueLabelPropagation.mappings[${index}].removeOtherTypeLabels must be a boolean; ignoring this mapping.`);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
// Unlike `removeOtherTypeLabels`, a malformed value here can only ever be warned-and-defaulted (never
|
|
45
|
+
// dropped) -- defaulting to `undefined`/strict is always the SAFE direction (no mapping accidentally
|
|
46
|
+
// starts trusting maintainer-authored issues), so there is no silent-flip risk that would justify
|
|
47
|
+
// discarding an otherwise-valid mapping over it.
|
|
48
|
+
let trustMaintainerAuthoredIssue;
|
|
49
|
+
if (record.trustMaintainerAuthoredIssue !== undefined) {
|
|
50
|
+
if (typeof record.trustMaintainerAuthoredIssue === "boolean") {
|
|
51
|
+
trustMaintainerAuthoredIssue = record.trustMaintainerAuthoredIssue;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
warnings.push(`settings.linkedIssueLabelPropagation.mappings[${index}].trustMaintainerAuthoredIssue must be a boolean; ignoring it.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Same parse contract as trustMaintainerAuthoredIssue just above (#priority-reward-maintainer-trust):
|
|
58
|
+
// malformed is warned-and-defaulted to undefined/strict, never silently coerced, never a reason to drop
|
|
59
|
+
// an otherwise-valid mapping.
|
|
60
|
+
let trustMaintainerAuthoredIssueForReward;
|
|
61
|
+
if (record.trustMaintainerAuthoredIssueForReward !== undefined) {
|
|
62
|
+
if (typeof record.trustMaintainerAuthoredIssueForReward === "boolean") {
|
|
63
|
+
trustMaintainerAuthoredIssueForReward = record.trustMaintainerAuthoredIssueForReward;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
warnings.push(`settings.linkedIssueLabelPropagation.mappings[${index}].trustMaintainerAuthoredIssueForReward must be a boolean; ignoring it.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { issueLabel, prLabel, removeOtherTypeLabels: record.removeOtherTypeLabels === true, trustMaintainerAuthoredIssue, trustMaintainerAuthoredIssueForReward };
|
|
70
|
+
}
|
|
71
|
+
/** Defaults-fill a per-repo `linkedIssueLabelPropagation` override into an always-complete, safe config —
|
|
72
|
+
* mirrors `normalizeCommandAuthorizationPolicy`'s defaults-fill pattern
|
|
73
|
+
* (`src/settings/command-authorization.ts`). Malformed mapping entries are dropped with a warning; valid
|
|
74
|
+
* entries in the same array are kept (matches `commandAuthorization`'s per-entry `commands` validation). */
|
|
75
|
+
export function normalizeLinkedIssueLabelPropagationConfig(input, warnings) {
|
|
76
|
+
if (input === undefined)
|
|
77
|
+
return { ...DEFAULT_LINKED_ISSUE_LABEL_PROPAGATION, mappings: [] };
|
|
78
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
79
|
+
warnings.push("settings.linkedIssueLabelPropagation must be an object; propagation stays disabled.");
|
|
80
|
+
return { ...DEFAULT_LINKED_ISSUE_LABEL_PROPAGATION, mappings: [] };
|
|
81
|
+
}
|
|
82
|
+
const record = input;
|
|
83
|
+
let enabled = DEFAULT_LINKED_ISSUE_LABEL_PROPAGATION.enabled;
|
|
84
|
+
if (record.enabled !== undefined) {
|
|
85
|
+
if (typeof record.enabled === "boolean") {
|
|
86
|
+
enabled = record.enabled;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
warnings.push(`settings.linkedIssueLabelPropagation.enabled must be a boolean; using the default "${DEFAULT_LINKED_ISSUE_LABEL_PROPAGATION.enabled}".`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
let mode = DEFAULT_LINKED_ISSUE_LABEL_PROPAGATION.mode;
|
|
93
|
+
if (record.mode !== undefined) {
|
|
94
|
+
if (typeof record.mode === "string" && VALID_LINKED_ISSUE_LABEL_PROPAGATION_MODES.includes(record.mode)) {
|
|
95
|
+
mode = record.mode;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
warnings.push(`settings.linkedIssueLabelPropagation.mode must be one of ${VALID_LINKED_ISSUE_LABEL_PROPAGATION_MODES.join(", ")}; using the default "${DEFAULT_LINKED_ISSUE_LABEL_PROPAGATION.mode}".`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
let mappings = [];
|
|
102
|
+
if (record.mappings !== undefined) {
|
|
103
|
+
if (Array.isArray(record.mappings)) {
|
|
104
|
+
mappings = record.mappings.flatMap((entry, index) => {
|
|
105
|
+
const normalized = normalizeMapping(entry, index, warnings);
|
|
106
|
+
return normalized ? [normalized] : [];
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
warnings.push("settings.linkedIssueLabelPropagation.mappings must be an array; using no mappings.");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { enabled, mode, mappings };
|
|
114
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type PreMergeCheck } from "../focus-manifest/guidance.js";
|
|
2
|
+
import type { AdvisoryFinding } from "../types/predicted-gate-types.js";
|
|
3
|
+
/** Finding code for a FAILED advisory (default) pre-merge check — surfaced but NEVER blocks. */
|
|
4
|
+
export declare const PRE_MERGE_CHECK_ADVISORY_CODE = "pre_merge_check_failed";
|
|
5
|
+
/** Finding code for a FAILED pre-merge check the maintainer marked `enforce: true` — a hard gate blocker
|
|
6
|
+
* (isConfiguredGateBlocker treats this code as blocking, like secret_leak). */
|
|
7
|
+
export declare const PRE_MERGE_CHECK_BLOCKING_CODE = "pre_merge_check_required";
|
|
8
|
+
/** Finding code emitted when an ENFORCED `whenPaths`-gated check cannot be evaluated because the PR's changed-file
|
|
9
|
+
* set could not be resolved. isEvaluationBlocker (advisory.ts) treats this as a NEUTRAL gate (HELD, re-evaluates
|
|
10
|
+
* automatically) — never silently skipping a hard requirement (auto-merge bypass) and never hard-closing the
|
|
11
|
+
* contributor on a transient resolution miss. (#review-audit) */
|
|
12
|
+
export declare const PRE_MERGE_CHECK_UNRESOLVED_CODE = "pre_merge_check_unresolved";
|
|
13
|
+
/**
|
|
14
|
+
* Evaluate the maintainer's `.gittensory.yml review.pre_merge_checks` against a PR — DETERMINISTICALLY, with no AI
|
|
15
|
+
* judgment. A check with `whenPaths` applies only when a changed path matches; it PASSES only when EVERY configured
|
|
16
|
+
* assertion holds (the title contains `titleContains`, the body contains `descriptionContains`, and the
|
|
17
|
+
* `requireLabel` label is present — all case-insensitive). Each FAILED check yields ONE finding:
|
|
18
|
+
* `pre_merge_check_required` (severity critical → the gate blocks under enforce) or `pre_merge_check_failed`
|
|
19
|
+
* (severity warning → advisory). Pure + side-effect-free; the caller pushes the findings into the advisory before
|
|
20
|
+
* the gate evaluates. Empty `checks` ⇒ no findings (byte-identical).
|
|
21
|
+
*/
|
|
22
|
+
export declare function evaluatePreMergeChecks(checks: PreMergeCheck[], ctx: {
|
|
23
|
+
title?: string | null | undefined;
|
|
24
|
+
body?: string | null | undefined;
|
|
25
|
+
labels?: string[] | null | undefined;
|
|
26
|
+
changedPaths: string[];
|
|
27
|
+
filesResolved?: boolean | undefined;
|
|
28
|
+
}): AdvisoryFinding[];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { matchesManifestPath } from "../focus-manifest/guidance.js";
|
|
2
|
+
/** Finding code for a FAILED advisory (default) pre-merge check — surfaced but NEVER blocks. */
|
|
3
|
+
export const PRE_MERGE_CHECK_ADVISORY_CODE = "pre_merge_check_failed";
|
|
4
|
+
/** Finding code for a FAILED pre-merge check the maintainer marked `enforce: true` — a hard gate blocker
|
|
5
|
+
* (isConfiguredGateBlocker treats this code as blocking, like secret_leak). */
|
|
6
|
+
export const PRE_MERGE_CHECK_BLOCKING_CODE = "pre_merge_check_required";
|
|
7
|
+
/** Finding code emitted when an ENFORCED `whenPaths`-gated check cannot be evaluated because the PR's changed-file
|
|
8
|
+
* set could not be resolved. isEvaluationBlocker (advisory.ts) treats this as a NEUTRAL gate (HELD, re-evaluates
|
|
9
|
+
* automatically) — never silently skipping a hard requirement (auto-merge bypass) and never hard-closing the
|
|
10
|
+
* contributor on a transient resolution miss. (#review-audit) */
|
|
11
|
+
export const PRE_MERGE_CHECK_UNRESOLVED_CODE = "pre_merge_check_unresolved";
|
|
12
|
+
/**
|
|
13
|
+
* Evaluate the maintainer's `.gittensory.yml review.pre_merge_checks` against a PR — DETERMINISTICALLY, with no AI
|
|
14
|
+
* judgment. A check with `whenPaths` applies only when a changed path matches; it PASSES only when EVERY configured
|
|
15
|
+
* assertion holds (the title contains `titleContains`, the body contains `descriptionContains`, and the
|
|
16
|
+
* `requireLabel` label is present — all case-insensitive). Each FAILED check yields ONE finding:
|
|
17
|
+
* `pre_merge_check_required` (severity critical → the gate blocks under enforce) or `pre_merge_check_failed`
|
|
18
|
+
* (severity warning → advisory). Pure + side-effect-free; the caller pushes the findings into the advisory before
|
|
19
|
+
* the gate evaluates. Empty `checks` ⇒ no findings (byte-identical).
|
|
20
|
+
*/
|
|
21
|
+
export function evaluatePreMergeChecks(checks, ctx) {
|
|
22
|
+
const title = (ctx.title ?? "").toLowerCase();
|
|
23
|
+
const body = (ctx.body ?? "").toLowerCase();
|
|
24
|
+
const labels = (ctx.labels ?? []).map((label) => label.toLowerCase());
|
|
25
|
+
const filesResolved = ctx.filesResolved ?? true; // absent ⇒ caller asserts a trustworthy changedPaths set
|
|
26
|
+
const findings = [];
|
|
27
|
+
for (const check of checks) {
|
|
28
|
+
// when_paths gate: a check with whenPaths applies ONLY to PRs that touch a matching path; an unmatched check
|
|
29
|
+
// is N/A (no finding). Empty whenPaths ⇒ the check always applies (title/description/label only).
|
|
30
|
+
if (check.whenPaths.length > 0) {
|
|
31
|
+
if (!filesResolved) {
|
|
32
|
+
// The changed-file set could not be resolved, so we cannot evaluate this path gate. HOLD the gate for an
|
|
33
|
+
// ENFORCED check (re-evaluates when files resolve) instead of silently skipping a hard requirement (which
|
|
34
|
+
// would let a guarded PR auto-merge). An advisory check is just dropped (no noise on a transient miss).
|
|
35
|
+
if (check.enforce)
|
|
36
|
+
findings.push({
|
|
37
|
+
code: PRE_MERGE_CHECK_UNRESOLVED_CODE,
|
|
38
|
+
severity: "warning",
|
|
39
|
+
title: `Pre-merge check held — changed files not resolved: ${check.name}`,
|
|
40
|
+
detail: `Gittensory could not resolve this PR's changed files to evaluate the path-gated check "${check.name}"; the gate is held and re-evaluates automatically.`,
|
|
41
|
+
action: "No action needed — the gate re-evaluates once the PR's files are available.",
|
|
42
|
+
});
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!ctx.changedPaths.some((path) => check.whenPaths.some((glob) => matchesManifestPath(path, glob))))
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const unmet = [];
|
|
49
|
+
if (check.titleContains !== null && !title.includes(check.titleContains.toLowerCase()))
|
|
50
|
+
unmet.push(`the title must contain "${check.titleContains}"`);
|
|
51
|
+
if (check.descriptionContains !== null && !body.includes(check.descriptionContains.toLowerCase()))
|
|
52
|
+
unmet.push(`the description must contain "${check.descriptionContains}"`);
|
|
53
|
+
if (check.requireLabel !== null && !labels.includes(check.requireLabel.toLowerCase()))
|
|
54
|
+
unmet.push(`the "${check.requireLabel}" label must be applied`);
|
|
55
|
+
if (unmet.length === 0)
|
|
56
|
+
continue; // every configured assertion held → the check passed
|
|
57
|
+
findings.push({
|
|
58
|
+
code: check.enforce ? PRE_MERGE_CHECK_BLOCKING_CODE : PRE_MERGE_CHECK_ADVISORY_CODE,
|
|
59
|
+
severity: check.enforce ? "critical" : "warning",
|
|
60
|
+
title: `Pre-merge check not satisfied: ${check.name}`,
|
|
61
|
+
detail: `This PR does not satisfy the maintainer pre-merge check "${check.name}": ${unmet.join("; ")}.`,
|
|
62
|
+
action: "Update the PR to satisfy the check, then re-run the gate.",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return findings;
|
|
66
|
+
}
|