@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
package/README.md
ADDED
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
# @loopover/engine
|
|
2
|
+
|
|
3
|
+
Shared, deterministic engine logic for the Gittensory review stack and the `gittensory-miner`.
|
|
4
|
+
|
|
5
|
+
This package houses pure, side-effect-free logic (scoring preview/model, predicted-gate types, reward-risk,
|
|
6
|
+
slop signals, focus-manifest parse/compile core, duplicate-winner adjudication, and their engine-parity
|
|
7
|
+
fixtures) so the exact same code runs identically in the hosted review backend and in a local miner. It is
|
|
8
|
+
versioned independently of the app and published to npm as `@loopover/engine`.
|
|
9
|
+
|
|
10
|
+
The logic is extracted from the app's `src/` in follow-up issues; this skeleton keeps the package buildable in
|
|
11
|
+
the meantime. The root `package.json` already globs `packages/*` in its `workspaces` field, so `npm ci`
|
|
12
|
+
discovers this package with no additional wiring.
|
|
13
|
+
|
|
14
|
+
## Version pin
|
|
15
|
+
|
|
16
|
+
`ENGINE_VERSION` mirrors `package.json`'s `version` field and is exported from the package barrel so consumers can
|
|
17
|
+
log or assert which engine build produced a deterministic result.
|
|
18
|
+
|
|
19
|
+
## Build
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
npm run build --workspace @loopover/engine
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This runs `tsc -p tsconfig.json`, emitting `dist/` (the only published output alongside `CHANGELOG.md`).
|
|
26
|
+
|
|
27
|
+
## Test
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
npm test --workspace @loopover/engine
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Compiles the package and the `test/` suite (`node:test`) to plain JS and runs it — no experimental runtime
|
|
34
|
+
flags, so it works on the whole declared `engines` range.
|
|
35
|
+
|
|
36
|
+
## `opportunity-ranker`
|
|
37
|
+
|
|
38
|
+
The Phase-1 miner-discovery ranker. It composes five already-normalized `[0, 1]` signals into one ordinal score:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
score = potential * feasibility * laneFit * freshness * (1 - dupRisk)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Every field is normalized before use, so a malformed upstream signal always degrades the score toward `0` rather
|
|
45
|
+
than inverting or overflowing it — but the two directions are handled asymmetrically:
|
|
46
|
+
|
|
47
|
+
- The four **positive** factors (`potential`, `feasibility`, `laneFit`, `freshness`) clamp into `[0, 1]`; a
|
|
48
|
+
non-finite value (`NaN`/`±Infinity`) maps to `0`.
|
|
49
|
+
- **`dupRisk`** is clamped into `[0, 1]` like the others (below-range → `0`, above-range → `1`), so `-0.1` reads as
|
|
50
|
+
no contention. The one exception: a **non-finite** `dupRisk` (`NaN`/`±Infinity`) can't be clamped, so it **fails
|
|
51
|
+
closed** to `1` (maximum risk) rather than `0` — a broken contention signal must never masquerade as safe.
|
|
52
|
+
|
|
53
|
+
Any single factor at `0` (or a `dupRisk` of `1`) collapses the whole score to `0`.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { rankOpportunities, rankOpportunityScore } from "@loopover/engine";
|
|
57
|
+
|
|
58
|
+
rankOpportunityScore({ potential: 0.9, feasibility: 0.8, laneFit: 1, freshness: 0.7, dupRisk: 0.1 }); // → 0.4536
|
|
59
|
+
|
|
60
|
+
rankOpportunities(candidates); // sorted by descending score, each annotated with `rankScore`
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`rankOpportunities` is a stable sort with an explicit index tie-break: candidates with an equal score keep their
|
|
64
|
+
input order.
|
|
65
|
+
|
|
66
|
+
## Objective-anchor calibration
|
|
67
|
+
|
|
68
|
+
`scoreObjectiveAnchor()` provides the deterministic half of historical replay calibration. It compares the structural
|
|
69
|
+
features of a miner replay against the revealed post-snapshot history without any model call, network call, wall-clock
|
|
70
|
+
read, or random input.
|
|
71
|
+
|
|
72
|
+
The score is intended for replay harnesses that need an auditable floor before a pairwise judge runs. Callers pass
|
|
73
|
+
the replayed plan or PR target data and the revealed history target data:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { scoreObjectiveAnchor } from "@loopover/engine";
|
|
77
|
+
|
|
78
|
+
const result = scoreObjectiveAnchor({
|
|
79
|
+
replayed: {
|
|
80
|
+
paths: ["packages/gittensory-engine/src/opportunity-ranker.ts"],
|
|
81
|
+
labels: ["feature"],
|
|
82
|
+
titles: ["feat(miner): add deterministic opportunity ranking"],
|
|
83
|
+
},
|
|
84
|
+
revealed: {
|
|
85
|
+
paths: ["packages/gittensory-engine/src/objective-anchor.ts"],
|
|
86
|
+
labels: ["feature"],
|
|
87
|
+
titles: ["feat(miner): add objective-anchor calibration scoring"],
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The returned object includes:
|
|
93
|
+
|
|
94
|
+
- `score`: a composite value in `[0, 1]`.
|
|
95
|
+
- `dimensions.paths`: exact/tight path overlap.
|
|
96
|
+
- `dimensions.modules`: coarser module overlap, so a replay that targets the right package but the wrong file receives
|
|
97
|
+
visible partial credit.
|
|
98
|
+
- `dimensions.changeKinds`: overlap between caller-supplied or inferred change classes.
|
|
99
|
+
- `audit`: normalized replayed/revealed feature sets, intersections, misses, and normalized weights.
|
|
100
|
+
|
|
101
|
+
The default weight split is path-heavy but still gives module-level and kind-level signal:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
{
|
|
105
|
+
paths: 0.45,
|
|
106
|
+
modules: 0.4,
|
|
107
|
+
changeKinds: 0.15
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Custom weights are normalized to sum to `1`. Negative, non-finite, or otherwise invalid weights are treated as `0`;
|
|
112
|
+
if every provided weight is unusable, the defaults are restored.
|
|
113
|
+
|
|
114
|
+
Feature extraction is intentionally conservative:
|
|
115
|
+
|
|
116
|
+
- Paths are normalized to lowercase slash paths, deduplicated, and sorted.
|
|
117
|
+
- Modules are derived only from paths, never guessed from free text.
|
|
118
|
+
- Change kinds can come from explicit `changeKinds`, issue/PR labels, titles, notes, and path conventions.
|
|
119
|
+
- If no change-kind signal exists, the kind is `unknown` so an opaque replay and opaque revealed history can still be
|
|
120
|
+
compared deterministically.
|
|
121
|
+
|
|
122
|
+
Given the same inputs, `JSON.stringify(scoreObjectiveAnchor(input))` is byte-stable across runs.
|
|
123
|
+
|
|
124
|
+
Replay harnesses that already represent the two sides as arrays of plans, PRs, or commits can use the history
|
|
125
|
+
helpers instead:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { scoreObjectiveAnchorHistory } from "@loopover/engine";
|
|
129
|
+
|
|
130
|
+
const result = scoreObjectiveAnchorHistory({
|
|
131
|
+
replayed: [
|
|
132
|
+
{
|
|
133
|
+
id: "plan:objective-anchor",
|
|
134
|
+
source: "plan",
|
|
135
|
+
paths: ["packages/gittensory-engine/src/objective-anchor.ts"],
|
|
136
|
+
labels: ["feature"],
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
revealed: [
|
|
140
|
+
{
|
|
141
|
+
id: "pr:3142",
|
|
142
|
+
source: "pull_request",
|
|
143
|
+
paths: ["packages/gittensory-engine/src/objective-anchor.ts"],
|
|
144
|
+
labels: ["feature"],
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`result.history.replayed.items` and `result.history.revealed.items` preserve the per-record normalized features, while
|
|
151
|
+
`result.audit` shows the aggregate intersections and misses used for the score. Empty histories remain valid inputs:
|
|
152
|
+
they produce empty path/module sets and an `unknown` change kind rather than throwing, so a replay batch can record a
|
|
153
|
+
low-information calibration row without special casing.
|
|
154
|
+
|
|
155
|
+
For local replay artifacts, `renderObjectiveAnchorAuditMarkdown(result)` turns either score shape into a deterministic
|
|
156
|
+
Markdown report. It includes dimensions, weights, normalized feature sets, intersections, misses, and per-item history
|
|
157
|
+
evidence when present. Report values are escaped and collapsed to one line so caller-supplied ids or paths cannot
|
|
158
|
+
reshape the artifact.
|
|
159
|
+
|
|
160
|
+
## Pairwise calibration
|
|
161
|
+
|
|
162
|
+
`computePairwiseCalibrationScore()` is the deterministic half of the order-swapped pairwise judge layer. The miner
|
|
163
|
+
runtime owns the model calls; the engine package owns the stable post-processing contract:
|
|
164
|
+
|
|
165
|
+
- run a judge attempt in both presentation orders,
|
|
166
|
+
- accept only outcomes that agree after inverting the swapped-order verdict,
|
|
167
|
+
- discard `incomparable` and order-flipping attempts,
|
|
168
|
+
- cap retries,
|
|
169
|
+
- track order-instability rate,
|
|
170
|
+
- combine the surviving pairwise average with the objective-anchor score.
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { computePairwiseCalibrationScore } from "@loopover/engine";
|
|
174
|
+
|
|
175
|
+
const result = computePairwiseCalibrationScore({
|
|
176
|
+
objectiveAnchor: 0.55,
|
|
177
|
+
samples: [
|
|
178
|
+
{
|
|
179
|
+
attempts: [
|
|
180
|
+
{
|
|
181
|
+
replayFirst: "replay_better",
|
|
182
|
+
revealedFirst: "revealed_better",
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
If every pairwise sample is unstable, the composite falls back to the objective-anchor score and records the failed
|
|
191
|
+
samples in `metrics` rather than averaging noise into the calibration signal.
|
|
192
|
+
|
|
193
|
+
## Structured gate-verdict calibration
|
|
194
|
+
|
|
195
|
+
`resolveGateVerdictCalibrationConfig()`, `ingestGateVerdictCalibrationSignals()`, and
|
|
196
|
+
`computeGateVerdictCompositeCalibrationScore()` provide the pure engine contract for opt-in cross-product calibration.
|
|
197
|
+
The hosted review stack remains responsible for loading the repo's current `.loopover.yml` (or legacy
|
|
198
|
+
`.gittensory.yml`, dual-read indefinitely — #4773) or private config; the engine contract is
|
|
199
|
+
deliberately default-off and safe to call at ingestion time.
|
|
200
|
+
|
|
201
|
+
The preferred config-as-code surface is:
|
|
202
|
+
|
|
203
|
+
```yaml
|
|
204
|
+
miner:
|
|
205
|
+
calibration:
|
|
206
|
+
shareStructuredGateVerdicts: true
|
|
207
|
+
structuredGateVerdictWeight: 0.2
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Only `shareStructuredGateVerdicts: true` enables ingestion. Missing, malformed, or falsey values all fail closed to no
|
|
211
|
+
sharing. The optional weight is non-negative and finite; malformed values fall back to the default.
|
|
212
|
+
|
|
213
|
+
The accepted signal is intentionally narrow. It contains repo/run ids plus structured dimension outcomes such as
|
|
214
|
+
`correctness`, `tests`, `security`, `scope`, `freshness`, `ci`, and `policy`. It has no fields for raw review text,
|
|
215
|
+
secrets, trust scores, reward values, private rankings, or maintainer evidence.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import {
|
|
219
|
+
computeGateVerdictCompositeCalibrationScore,
|
|
220
|
+
ingestGateVerdictCalibrationSignals,
|
|
221
|
+
} from "@loopover/engine";
|
|
222
|
+
|
|
223
|
+
const gateVerdicts = ingestGateVerdictCalibrationSignals([
|
|
224
|
+
{
|
|
225
|
+
repoFullName: "jsonbored/gittensory",
|
|
226
|
+
replayRunId: "replay-2026-07-04",
|
|
227
|
+
gateRunId: "gate-123",
|
|
228
|
+
optedIn: true,
|
|
229
|
+
dimensions: [
|
|
230
|
+
{ dimension: "correctness", outcome: "pass" },
|
|
231
|
+
{ dimension: "tests", outcome: "warn" },
|
|
232
|
+
{ dimension: "security", outcome: "pass" },
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
const score = computeGateVerdictCompositeCalibrationScore({
|
|
238
|
+
objectiveAnchor: 0.65,
|
|
239
|
+
pairwise: 0.8,
|
|
240
|
+
gateVerdicts,
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The composite scorer renormalizes weights when a signal is absent. For example, if a repo opts out or no valid
|
|
245
|
+
structured dimensions remain, the structured gate-verdict weight drops to zero and the objective/pairwise signals are
|
|
246
|
+
renormalized. The returned audit trail records which opted-in repos contributed to the replay run and which rows were
|
|
247
|
+
rejected because the repo was not opted in, had invalid ids, or exposed no recognized structured dimensions.
|
|
248
|
+
|
|
249
|
+
`renderGateVerdictCalibrationAuditMarkdown(result)` turns the composite result into a deterministic local artifact with
|
|
250
|
+
component scores, effective weights, contributing repos, dimension tables, rejected rows, and a contributing-repo
|
|
251
|
+
summary. All caller-supplied ids and repo names are Markdown-escaped and newline-collapsed before rendering.
|
|
252
|
+
|
|
253
|
+
## Phase 7 calibration loop
|
|
254
|
+
|
|
255
|
+
`computePhase7CalibrationLoop()` wires the historical-replay composite score into the live Phase 7 calibration loop
|
|
256
|
+
alongside the passive pr_outcome signal. The module tracks a combined calibration-accuracy metric against the
|
|
257
|
+
documented 62% baseline, records provenance per source, recommends replay-run cadence, and fail-closes autonomy-level
|
|
258
|
+
increases when the replay harness is missing, stale, degraded, or below the configured threshold.
|
|
259
|
+
|
|
260
|
+
The loop is default-off and must be enabled explicitly:
|
|
261
|
+
|
|
262
|
+
```yaml
|
|
263
|
+
miner:
|
|
264
|
+
calibration:
|
|
265
|
+
phase7LoopEnabled: true
|
|
266
|
+
autonomyIncreaseMinAccuracy: 0.70
|
|
267
|
+
replayFreshnessMaxAgeHours: 168
|
|
268
|
+
historicalReplayWeight: 0.5
|
|
269
|
+
prOutcomeWeight: 0.5
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
When enabled, autonomy-level increases require a fresh healthy historical-replay run plus enough live pr_outcome samples.
|
|
273
|
+
If the replay harness is degraded or unavailable, the loop sets an explicit hold flag instead of silently falling back
|
|
274
|
+
to pr_outcome-only gating.
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import {
|
|
278
|
+
computePhase7CalibrationLoop,
|
|
279
|
+
shouldScheduleHistoricalReplayRun,
|
|
280
|
+
} from "@loopover/engine";
|
|
281
|
+
|
|
282
|
+
const prOutcome = {
|
|
283
|
+
mergeConfirmed: 74,
|
|
284
|
+
mergeFalse: 26,
|
|
285
|
+
closeConfirmed: 0,
|
|
286
|
+
closeFalse: 0,
|
|
287
|
+
observedAt: "2026-07-04T18:00:00Z",
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const loop = computePhase7CalibrationLoop({
|
|
291
|
+
config: {
|
|
292
|
+
phase7LoopEnabled: true,
|
|
293
|
+
autonomyIncreaseMinAccuracy: 0.7,
|
|
294
|
+
replayFreshnessMaxAgeHours: 168,
|
|
295
|
+
historicalReplayWeight: 0.5,
|
|
296
|
+
prOutcomeWeight: 0.5,
|
|
297
|
+
prOutcomeMinDecided: 10,
|
|
298
|
+
warnings: [],
|
|
299
|
+
},
|
|
300
|
+
prOutcome,
|
|
301
|
+
historicalReplay: {
|
|
302
|
+
compositeScore: 0.82,
|
|
303
|
+
replayRunId: "replay-2026-07-04",
|
|
304
|
+
observedAt: "2026-07-04T12:00:00Z",
|
|
305
|
+
harnessStatus: "healthy",
|
|
306
|
+
},
|
|
307
|
+
now: "2026-07-04T18:00:00Z",
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const schedule = shouldScheduleHistoricalReplayRun({
|
|
311
|
+
config: {
|
|
312
|
+
phase7LoopEnabled: true,
|
|
313
|
+
autonomyIncreaseMinAccuracy: 0.7,
|
|
314
|
+
replayFreshnessMaxAgeHours: 168,
|
|
315
|
+
historicalReplayWeight: 0.5,
|
|
316
|
+
prOutcomeWeight: 0.5,
|
|
317
|
+
prOutcomeMinDecided: 10,
|
|
318
|
+
warnings: [],
|
|
319
|
+
},
|
|
320
|
+
lastReplayObservedAt: loop.bySource.historical_replay.observedAt,
|
|
321
|
+
harnessStatus: loop.replayHarnessStatus,
|
|
322
|
+
now: "2026-07-04T18:00:00Z",
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`renderPhase7CalibrationAuditMarkdown(loop)` turns the result into a deterministic local artifact with the combined
|
|
327
|
+
metric, baseline delta, per-source breakdown, hold reasons, and replay cadence state.
|
|
328
|
+
|
|
329
|
+
`computePrOutcomeCalibrationAccuracy()` is a read-only helper for inspecting derived accuracy from raw gate-eval
|
|
330
|
+
counters; pass the counters themselves into `computePhase7CalibrationLoop()`, not the helper result.
|
|
331
|
+
|
|
332
|
+
## Track-record summary
|
|
333
|
+
|
|
334
|
+
`computeTrackRecordSummary()` and `renderTrackRecordSummaryMarkdown()` provide a portable first-contact summary for a
|
|
335
|
+
miner identity. The summary is computed client-side from already-public PR outcomes plus public conduct/moderation
|
|
336
|
+
records, then rendered as a short Markdown block for a PR body or first comment.
|
|
337
|
+
|
|
338
|
+
The feature is default-off and must be enabled explicitly:
|
|
339
|
+
|
|
340
|
+
```yaml
|
|
341
|
+
miner:
|
|
342
|
+
trackRecordSummary:
|
|
343
|
+
enabled: true
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
The computation only counts resolved PR outcomes attributable to the requested login. Merged PRs contribute to the
|
|
347
|
+
numerator, closed-without-merge PRs contribute to the denominator, and open PRs are reported as ignored so in-flight
|
|
348
|
+
work cannot inflate or deflate the public rate. Tenure is derived from the earliest observed public PR timestamp, and a
|
|
349
|
+
clean conduct line is emitted only when no active public incident record is present for the login.
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import {
|
|
353
|
+
computeTrackRecordSummary,
|
|
354
|
+
renderTrackRecordSummaryMarkdown,
|
|
355
|
+
resolveTrackRecordSummaryConfig,
|
|
356
|
+
} from "@loopover/engine";
|
|
357
|
+
|
|
358
|
+
const config = resolveTrackRecordSummaryConfig({
|
|
359
|
+
miner: { trackRecordSummary: { enabled: true } },
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const summary = computeTrackRecordSummary({
|
|
363
|
+
login: "octo-miner",
|
|
364
|
+
config,
|
|
365
|
+
now: "2026-07-04T18:00:00Z",
|
|
366
|
+
outcomes: [
|
|
367
|
+
{
|
|
368
|
+
repoFullName: "JSONbored/gittensory",
|
|
369
|
+
authorLogin: "octo-miner",
|
|
370
|
+
state: "merged",
|
|
371
|
+
createdAt: "2026-06-01T00:00:00Z",
|
|
372
|
+
mergedAt: "2026-06-02T00:00:00Z",
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
incidents: [],
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const markdown = renderTrackRecordSummaryMarkdown(summary);
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The rendered block is intentionally narrow: login, resolved public PR counts, public merge rate, public tenure, conduct
|
|
382
|
+
status, and optional public evidence URLs for active incidents. Caller-provided ids, PR URLs, and arbitrary metadata are
|
|
383
|
+
never copied into the Markdown, and the renderer fails closed if a blocked private-field name is introduced.
|
|
384
|
+
|
|
385
|
+
## Structured finding-severity calibration
|
|
386
|
+
|
|
387
|
+
`resolveFindingSeverityCalibrationConfig()`, `ingestFindingSeverityCalibrationSignals()`, and
|
|
388
|
+
`computeFindingSeverityCompositeCalibrationScore()` provide the pure engine contract for the opt-in finding-severity
|
|
389
|
+
calibration signal. It sits in the same family as objective-anchor and pairwise-judge: the hosted review stack decides
|
|
390
|
+
whether a repo is opted in from its resolved `.loopover.yml`/private config (legacy `.gittensory.yml` also still
|
|
391
|
+
works, #4773), and the engine contract is deliberately default-off and safe to call at ingestion time.
|
|
392
|
+
|
|
393
|
+
The preferred config-as-code surface is:
|
|
394
|
+
|
|
395
|
+
```yaml
|
|
396
|
+
miner:
|
|
397
|
+
calibration:
|
|
398
|
+
shareStructuredFindingSeverity: true
|
|
399
|
+
structuredFindingSeverityWeight: 0.2
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Only `shareStructuredFindingSeverity: true` enables ingestion. Missing, malformed, or falsey values all fail closed to
|
|
403
|
+
no sharing. `calibration.shareStructuredFindingSeverity` is accepted as a narrow top-level alias. The optional weight is
|
|
404
|
+
non-negative and finite; malformed values fall back to the default.
|
|
405
|
+
|
|
406
|
+
The accepted signal is intentionally narrow: repo/run ids plus, per severity tier (`blocker`, `warning`, `advisory`,
|
|
407
|
+
`nit`), how many findings the review raised and how many were subsequently CONFIRMED (true positives). It has no fields
|
|
408
|
+
for raw review text, secrets, trust scores, reward values, private rankings, or maintainer evidence.
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
import {
|
|
412
|
+
computeFindingSeverityCompositeCalibrationScore,
|
|
413
|
+
ingestFindingSeverityCalibrationSignals,
|
|
414
|
+
} from "@loopover/engine";
|
|
415
|
+
|
|
416
|
+
const findingSeverity = ingestFindingSeverityCalibrationSignals([
|
|
417
|
+
{
|
|
418
|
+
repoFullName: "jsonbored/gittensory",
|
|
419
|
+
replayRunId: "replay-2026-07-04",
|
|
420
|
+
reviewRunId: "review-123",
|
|
421
|
+
optedIn: true,
|
|
422
|
+
tiers: [
|
|
423
|
+
{ tier: "blocker", total: 2, confirmed: 2 },
|
|
424
|
+
{ tier: "warning", total: 5, confirmed: 3 },
|
|
425
|
+
{ tier: "nit", total: 9, confirmed: 1 },
|
|
426
|
+
],
|
|
427
|
+
},
|
|
428
|
+
]);
|
|
429
|
+
|
|
430
|
+
const score = computeFindingSeverityCompositeCalibrationScore({
|
|
431
|
+
objectiveAnchor: 0.65,
|
|
432
|
+
pairwise: 0.8,
|
|
433
|
+
findingSeverity,
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
The per-PR score is the severity-and-volume-weighted mean of the per-tier confirmation rates, so a confirmed blocker
|
|
438
|
+
moves calibration far more than a confirmed nit, and a review that raises blockers which are then dismissed (false
|
|
439
|
+
positives at the most disruptive tier) calibrates poorly. Each tier's `confirmed` count is clamped to its `total` and
|
|
440
|
+
discounted by an optional per-tier `confidence`, so an unverified "all confirmed" claim cannot inflate the score.
|
|
441
|
+
|
|
442
|
+
The composite scorer renormalizes weights when a signal is absent: if a repo opts out or no tier survives
|
|
443
|
+
normalization, the structured finding-severity weight drops to zero and the objective/pairwise signals are
|
|
444
|
+
renormalized. The returned audit trail records which opted-in repos contributed and which rows were rejected because the
|
|
445
|
+
repo was not opted in, had invalid ids, or exposed no recognized non-empty tiers.
|
|
446
|
+
|
|
447
|
+
`renderFindingSeverityCalibrationAuditMarkdown(result)` turns the composite result into a deterministic local artifact
|
|
448
|
+
with component scores, effective weights, contributing repos, per-tier tables, rejected rows, and a contributing-repo
|
|
449
|
+
summary. All caller-supplied ids and repo names are Markdown-escaped and newline-collapsed before rendering.
|
|
450
|
+
|
|
451
|
+
## Structured reviewer-consensus calibration
|
|
452
|
+
|
|
453
|
+
`resolveReviewerConsensusCalibrationConfig()`, `ingestReviewerConsensusCalibrationSignals()`, and
|
|
454
|
+
`computeReviewerConsensusCompositeCalibrationScore()` provide the pure engine contract for the opt-in
|
|
455
|
+
reviewer-consensus calibration signal. When a review runs more than one independent reviewer (multiple models, or the
|
|
456
|
+
same model sampled multiple times), each reviewer casts a per-dimension verdict; this signal measures how much they
|
|
457
|
+
**agree**. It is a companion to the pairwise judge (which measures order-stability of a single judge) at the level of
|
|
458
|
+
independent reviewers, and — like the rest of the family — the engine contract is deliberately default-off and safe to
|
|
459
|
+
call at ingestion time.
|
|
460
|
+
|
|
461
|
+
The preferred config-as-code surface is:
|
|
462
|
+
|
|
463
|
+
```yaml
|
|
464
|
+
miner:
|
|
465
|
+
calibration:
|
|
466
|
+
shareStructuredReviewerConsensus: true
|
|
467
|
+
structuredReviewerConsensusWeight: 0.2
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Only `shareStructuredReviewerConsensus: true` enables ingestion. Missing, malformed, or falsey values all fail closed to
|
|
471
|
+
no sharing. `calibration.shareStructuredReviewerConsensus` is accepted as a narrow top-level alias. The optional weight
|
|
472
|
+
is non-negative and finite; malformed values fall back to the default.
|
|
473
|
+
|
|
474
|
+
The accepted signal is intentionally narrow: repo/run ids plus, per dimension (`correctness`, `tests`, `security`,
|
|
475
|
+
`maintainability`, `scope`, `freshness`, `ci`, `policy`), the set of independent reviewer votes (`pass`/`warn`/`fail`).
|
|
476
|
+
It has no fields for raw review text, secrets, trust scores, reward values, private rankings, or maintainer evidence.
|
|
477
|
+
|
|
478
|
+
```ts
|
|
479
|
+
import {
|
|
480
|
+
computeReviewerConsensusCompositeCalibrationScore,
|
|
481
|
+
ingestReviewerConsensusCalibrationSignals,
|
|
482
|
+
} from "@loopover/engine";
|
|
483
|
+
|
|
484
|
+
const reviewerConsensus = ingestReviewerConsensusCalibrationSignals([
|
|
485
|
+
{
|
|
486
|
+
repoFullName: "jsonbored/gittensory",
|
|
487
|
+
replayRunId: "replay-2026-07-05",
|
|
488
|
+
reviewRunId: "review-123",
|
|
489
|
+
optedIn: true,
|
|
490
|
+
dimensions: [
|
|
491
|
+
{ dimension: "correctness", votes: ["pass", "pass", "pass"] },
|
|
492
|
+
{ dimension: "security", votes: ["fail", "warn", "fail"] },
|
|
493
|
+
],
|
|
494
|
+
},
|
|
495
|
+
]);
|
|
496
|
+
|
|
497
|
+
const score = computeReviewerConsensusCompositeCalibrationScore({
|
|
498
|
+
objectiveAnchor: 0.65,
|
|
499
|
+
pairwise: 0.8,
|
|
500
|
+
reviewerConsensus,
|
|
501
|
+
});
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Per dimension, unrecognized and abstention votes are dropped, the remaining votes are tallied, the plurality outcome is
|
|
505
|
+
chosen (ties broken toward the more severe outcome so a genuine split never rounds a real `fail`/`warn` down to `pass`),
|
|
506
|
+
and the **agreement** fraction is the plurality's share of the definite votes. The per-PR score is the
|
|
507
|
+
**vote-count-weighted** mean of the per-dimension agreements, so a dimension reviewed by more reviewers carries more
|
|
508
|
+
weight than one seen by a single reviewer.
|
|
509
|
+
|
|
510
|
+
The composite scorer renormalizes weights when a signal is absent: if a repo opts out or no dimension carries a definite
|
|
511
|
+
vote, the structured reviewer-consensus weight drops to zero and the objective/pairwise signals are renormalized. The
|
|
512
|
+
returned audit trail records which opted-in repos contributed and which rows were rejected because the repo was not
|
|
513
|
+
opted in, had invalid ids, or exposed no definite per-dimension votes.
|
|
514
|
+
|
|
515
|
+
`renderReviewerConsensusCalibrationAuditMarkdown(result)` turns the composite result into a deterministic local artifact
|
|
516
|
+
with component scores, effective weights, contributing repos, per-dimension agreement tables, rejected rows, and a
|
|
517
|
+
contributing-repo summary. All caller-supplied ids and repo names are Markdown-escaped and newline-collapsed before
|
|
518
|
+
rendering.
|
|
519
|
+
|
|
520
|
+
## Plan templates
|
|
521
|
+
|
|
522
|
+
`plan-templates.ts` exports one builder per miner lifecycle stage (`analyze`, `plan`, `prepare`, `create`, `manage`).
|
|
523
|
+
Each builder returns `RawPlanStep[]` in the shape accepted by `gittensory_build_plan`. Templates are pure data — they
|
|
524
|
+
describe step ordering via `dependsOn` but never actuate anything.
|
|
525
|
+
|
|
526
|
+
## Plan DAG status helpers
|
|
527
|
+
|
|
528
|
+
`plan-export.ts` renders a validated `PlanDag`; the helpers below are pure predicates over that shape for miner and
|
|
529
|
+
dashboard progress summaries:
|
|
530
|
+
|
|
531
|
+
- `countPlanSteps(plan)` — total step count
|
|
532
|
+
- `countPlanStepsByStatus(plan, status)` — steps matching a `PlanStepStatus`
|
|
533
|
+
- `isPlanEmpty(plan)` — whether the plan has no steps
|
|
534
|
+
- `isPlanFullyCompleted(plan)` — every step is `completed` (empty plans are not complete)
|
|
535
|
+
- `hasPlanFailedSteps(plan)` — any step is `failed`
|
|
536
|
+
- `hasPlanPendingSteps(plan)` — any step is `pending`
|
|
537
|
+
- `hasPlanRunningSteps(plan)` — any step is `running`
|
|
538
|
+
- `hasPlanSkippedSteps(plan)` — any step is `skipped`
|
|
539
|
+
- `hasPlanCompletedSteps(plan)` — any step is `completed`
|
|
540
|
+
- `isPlanBlocked(plan)` — pending steps remain but none are runnable (deadlock; mirrors `planProgress`'s `blocked` status)
|
|
541
|
+
- `isPlanProgressComplete(plan)` — every step is `completed` or `skipped` (empty plans are not complete; mirrors `planProgress`'s `completed` status)
|
|
542
|
+
- `resolvePlanOverallStatus(plan)` — coarse status (`pending` | `running` | `completed` | `failed` | `blocked`); mirrors `planProgress`'s `status`
|
|
543
|
+
- `hasPlanReadySteps(plan)` — any step is runnable now (`pending` with satisfied dependencies; mirrors `nextReadySteps(plan).length > 0`)
|
|
544
|
+
- `isPlanTerminated(plan)` — plan reached a terminal outcome (`failed` step or every step `completed`/`skipped`; empty plans are not terminated)
|
|
545
|
+
|
|
546
|
+
## Opportunity competition
|
|
547
|
+
|
|
548
|
+
`computeOpportunityCompetition(highRiskDuplicateClusters, openPullRequests)` mirrors the hosted
|
|
549
|
+
`opportunityCompetitionFactor` in `src/signals/reward-risk.ts`, producing a `[0, 1]` signal suitable for the ranker's
|
|
550
|
+
`dupRisk` input.
|
|
551
|
+
|
|
552
|
+
## Metadata opportunity signals
|
|
553
|
+
|
|
554
|
+
`opportunity-metadata.ts` turns fan-out issue metadata into the five normalized ranker inputs:
|
|
555
|
+
|
|
556
|
+
- `computeMetadataPotential` — label-based upside estimate
|
|
557
|
+
- `computeMetadataFeasibility` — comment load + issue age + title quality
|
|
558
|
+
- `computeMetadataDupRisk` — same-repo title overlap inside a candidate batch
|
|
559
|
+
- `computeMetadataLaneFit` — label-only lane fit by default; honors optional `candidatePaths` via `computeLaneFit`
|
|
560
|
+
- `buildMetadataRankInput` — composes freshness, competition, lane fit, and the metadata heuristics
|
|
561
|
+
- `rankMetadataOpportunities` — sorts candidates with `rankOpportunities`
|
|
562
|
+
|
|
563
|
+
`computeOpportunityFreshness` and `computeOpportunityCompetition` mirror the hosted reward-risk helpers with pure,
|
|
564
|
+
injected-clock semantics for local miners.
|
|
565
|
+
|
|
566
|
+
## AI Policy Map
|
|
567
|
+
|
|
568
|
+
`scanAiPolicyText` and `resolveAiPolicyVerdict` provide the deterministic policy gate used by miner discovery.
|
|
569
|
+
They only deny on small, explicit AI-contribution ban phrases in `AI-USAGE.md` or `CONTRIBUTING.md`; ambiguous,
|
|
570
|
+
missing, or empty policy text stays allowed so discovery does not invent a ban.
|
|
571
|
+
|
|
572
|
+
`resolveAiPolicyFatigueVerdict()` adds a softer metadata-only tier for repos that show signs of AI-contribution
|
|
573
|
+
fatigue before a formal ban lands. It keeps the hard-ban verdict authoritative and attaches a separate `fatigue`
|
|
574
|
+
object instead of changing `allowed`:
|
|
575
|
+
|
|
576
|
+
```ts
|
|
577
|
+
import { resolveAiPolicyFatigueVerdict } from "@loopover/engine";
|
|
578
|
+
|
|
579
|
+
const verdict = resolveAiPolicyFatigueVerdict({
|
|
580
|
+
now: "2026-07-05T00:00:00Z",
|
|
581
|
+
docs: {
|
|
582
|
+
aiUsage: null,
|
|
583
|
+
contributing: "Please keep pull requests focused.",
|
|
584
|
+
},
|
|
585
|
+
pullRequests: [
|
|
586
|
+
{
|
|
587
|
+
state: "closed",
|
|
588
|
+
title: "AI-assisted parser cleanup",
|
|
589
|
+
labels: ["ai-generated"],
|
|
590
|
+
closedAt: "2026-07-04T00:00:00Z",
|
|
591
|
+
reviewDecision: "changes_requested",
|
|
592
|
+
maintainerResponse: "terse_rejection",
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
docChanges: [
|
|
596
|
+
{
|
|
597
|
+
path: "CONTRIBUTING.md",
|
|
598
|
+
changedAt: "2026-07-04T12:00:00Z",
|
|
599
|
+
addedPhrases: ["Disclose AI or automation assistance."],
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
});
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
The fatigue verdict has four levels:
|
|
606
|
+
|
|
607
|
+
- `none` leaves ranking unchanged and uses a long recheck interval.
|
|
608
|
+
- `watch` and `deprioritize` return `priorityAdjustment: "deprioritize"` so miners can rank the repo lower without
|
|
609
|
+
treating it as banned.
|
|
610
|
+
- `defer` returns `priorityAdjustment: "defer"` with a short recheck interval for stronger but still reversible
|
|
611
|
+
signals.
|
|
612
|
+
|
|
613
|
+
Evidence is deliberately metadata-only: AI-attributed closed PR rows, terse/template rejection metadata, and recent
|
|
614
|
+
AI/automation language added to policy docs that does not match a formal ban phrase. `renderAiPolicyFatigueMarkdown()`
|
|
615
|
+
turns the verdict into a deterministic observability artifact. Fresh cache entries can be passed back through
|
|
616
|
+
`cache`; expired entries are recomputed.
|
|
617
|
+
|
|
618
|
+
Miner ranking/fanout code can pass the verdict to `applyAiPolicyFatigueToRankInput()`. Formal bans still zero the
|
|
619
|
+
candidate, but fatigue-only verdicts merely reduce `potential` (`watch` = 0.7x, `deprioritize` = 0.35x, `defer` =
|
|
620
|
+
0.05x) and carry a `deferUntilHours` hint when the repo should be revisited later.
|
|
621
|
+
|
|
622
|
+
`createAiPolicyFatigueCacheEntry()` and `describeAiPolicyFatigueCache()` provide the cache surface for the miner's
|
|
623
|
+
shorter fatigue recheck interval. Cache keys normalize repo names to lowercase `owner/name`, record an ISO
|
|
624
|
+
`computedAt`, and are considered fresh for 24 hours.
|
|
625
|
+
|
|
626
|
+
## Governor ledger
|
|
627
|
+
|
|
628
|
+
`normalizeGovernorLedgerEvent` validates append-only governor decision rows before the local miner persists them.
|
|
629
|
+
The vocabulary is fixed (`allowed`, `denied`, `throttled`, `kill_switch`) and unknown event types fail closed. This
|
|
630
|
+
module defines the storage contract only — it does not wire into live governor enforcement yet. (#2328)
|
|
631
|
+
|
|
632
|
+
## MinerGoalSpec
|
|
633
|
+
|
|
634
|
+
`MinerGoalSpec` is the type surface for a repo's `.gittensory-miner.yml` (miner-side analogue of `.gittensory.yml`).
|
|
635
|
+
`DEFAULT_MINER_GOAL_SPEC` is the safe default a repo with no file behaves as — minable (`minerEnabled: true`, an
|
|
636
|
+
explicit opt-out), no path/label preferences, one concurrent claim, `neutral` discovery.
|
|
637
|
+
|
|
638
|
+
`parseMinerGoalSpec(raw)` and `parseMinerGoalSpecContent(content)` are the tolerant parser pair for that file. They
|
|
639
|
+
never throw on malformed JSON/YAML; instead they return `{ present, spec, warnings }`, where `spec` is normalized to
|
|
640
|
+
safe defaults and `warnings` explains any dropped or invalid fields.
|
|
641
|
+
|
|
642
|
+
`discoverMinerGoalSpecPath(exists)` returns the first present file in the documented order (`MINER_GOAL_SPEC_FILENAMES`:
|
|
643
|
+
`.gittensory-miner.yml` → `.github/gittensory-miner.yml` → the `.json` variants). It is IO-free — the caller injects
|
|
644
|
+
the existence check — so a caller reads the returned path and feeds its content to `parseMinerGoalSpecContent`. See
|
|
645
|
+
`.gittensory-miner.yml.example` for the documented fields.
|
|
646
|
+
|
|
647
|
+
## AmsPolicySpec
|
|
648
|
+
|
|
649
|
+
`AmsPolicySpec` is the type surface for `.gittensory-ams.yml` — the OPERATOR's own execution-risk policy for
|
|
650
|
+
their miner (`submissionMode`, `slopThreshold`, `capLimits`, `convergenceThresholds`, `maxIterations`,
|
|
651
|
+
`maxTurnsPerIteration`), a deliberate structural sibling to `MinerGoalSpec` but answering a different question: `MinerGoalSpec` is what the target repo wants
|
|
652
|
+
from being mined; `AmsPolicySpec` is how aggressive the operator wants their own agent to be. No field on this
|
|
653
|
+
type lets a target repo's own file loosen what an operator's agent is willing to do — see the type's own header
|
|
654
|
+
comment for why that boundary is load-bearing. `DEFAULT_AMS_POLICY_SPEC` is deny-by-default: `"observe"`
|
|
655
|
+
submission mode (computes real decisions but never actually submits) and a `"low"` (strict) slop threshold.
|
|
656
|
+
|
|
657
|
+
`parseAmsPolicySpec(raw)` / `parseAmsPolicySpecContent(content)` are the same tolerant-parser pair shape as
|
|
658
|
+
`MinerGoalSpec`'s — never throw, return `{ present, spec, warnings }`.
|
|
659
|
+
|
|
660
|
+
Unlike `MinerGoalSpec`, this package does not resolve `.gittensory-ams.yml` from the filesystem itself (this
|
|
661
|
+
package is IO-free) — `packages/gittensory-miner/lib/ams-policy.js`'s `resolveAmsPolicy` is the real caller.
|
|
662
|
+
That resolver reads only the operator's local policy and otherwise uses safe defaults; it intentionally does not
|
|
663
|
+
fetch a target repo's checked-in AMS policy, because untrusted repo content must not loosen operator-side budget,
|
|
664
|
+
turn, slop, or submission controls. See `.gittensory-ams.yml.example`.
|
|
665
|
+
|
|
666
|
+
## Repo map builder
|
|
667
|
+
|
|
668
|
+
`buildRepoMap(files)` gives a coding-agent driver (or the acceptance-criteria/prompt-packet builders upstream of
|
|
669
|
+
it) a compact, structural view of a target repository — function/class/method/interface/type signatures — without
|
|
670
|
+
paying the token cost of dumping full file contents into a prompt. It parses with `web-tree-sitter` (the WASM
|
|
671
|
+
binding, not a native addon, since this package also ships a Cloudflare Workers deployment target) using prebuilt
|
|
672
|
+
grammars from `tree-sitter-wasms`. Supported today: JavaScript/TypeScript/TSX. A file with an unsupported extension
|
|
673
|
+
or a grammar that fails to load/parse is reported via `skipped` on its `RepoMapFileEntry`, never thrown — this
|
|
674
|
+
module's contract is "extract what it safely can," not "block the whole driver invocation." `renderRepoMap(entries)`
|
|
675
|
+
renders a bounded plain-text outline, truncating (with a marker) once a configurable char budget is exceeded so it
|
|
676
|
+
can't blow out a prompt budget on a large repo. (#4280)
|