@loopover/miner 0.1.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/DEPLOYMENT.md +156 -0
- package/Dockerfile +38 -0
- package/README.md +269 -0
- package/bin/gittensory-miner-mcp.d.ts +64 -0
- package/bin/gittensory-miner-mcp.js +279 -0
- package/bin/gittensory-miner.js +196 -0
- package/docs/coding-agent-driver.md +140 -0
- package/docs/config-precedence.md +96 -0
- package/docs/cross-repo-discovery-phase1.md +55 -0
- package/docs/discovery-index-contract.md +60 -0
- package/docs/discovery-plane-operator-guide.md +101 -0
- package/docs/env-reference.md +30 -0
- package/docs/fleet-run-manifest.md +50 -0
- package/docs/miner-goal-spec.md +76 -0
- package/docs/miner-selfimprove-calibration.md +105 -0
- package/docs/observability.md +60 -0
- package/docs/operations-runbook.md +209 -0
- package/docs/repo-agnostic-capability-audit.md +129 -0
- package/docs/unattended-scheduling.md +107 -0
- package/expected-engine.version +1 -0
- package/lib/ams-policy.d.ts +20 -0
- package/lib/ams-policy.js +68 -0
- package/lib/attempt-cli.d.ts +99 -0
- package/lib/attempt-cli.js +523 -0
- package/lib/attempt-input-builder.d.ts +24 -0
- package/lib/attempt-input-builder.js +91 -0
- package/lib/attempt-log.d.ts +37 -0
- package/lib/attempt-log.js +177 -0
- package/lib/attempt-runner.d.ts +59 -0
- package/lib/attempt-runner.js +164 -0
- package/lib/attempt-worktree.d.ts +31 -0
- package/lib/attempt-worktree.js +94 -0
- package/lib/calibration-cli.d.ts +1 -0
- package/lib/calibration-cli.js +92 -0
- package/lib/calibration-run.d.ts +161 -0
- package/lib/calibration-run.js +232 -0
- package/lib/calibration-types.d.ts +46 -0
- package/lib/calibration-types.js +74 -0
- package/lib/calibration.d.ts +24 -0
- package/lib/calibration.js +99 -0
- package/lib/ci-poller.d.ts +33 -0
- package/lib/ci-poller.js +234 -0
- package/lib/claim-adjudication.d.ts +21 -0
- package/lib/claim-adjudication.js +36 -0
- package/lib/claim-conflict-resolver.d.ts +29 -0
- package/lib/claim-conflict-resolver.js +101 -0
- package/lib/claim-ledger-cli.d.ts +59 -0
- package/lib/claim-ledger-cli.js +320 -0
- package/lib/claim-ledger-expiry.d.ts +20 -0
- package/lib/claim-ledger-expiry.js +41 -0
- package/lib/claim-ledger.d.ts +63 -0
- package/lib/claim-ledger.js +303 -0
- package/lib/cli-error.d.ts +3 -0
- package/lib/cli-error.js +27 -0
- package/lib/cli.d.ts +3 -0
- package/lib/cli.js +72 -0
- package/lib/coding-agent-construction.d.ts +17 -0
- package/lib/coding-agent-construction.js +103 -0
- package/lib/coding-agent-house-rules.d.ts +24 -0
- package/lib/coding-agent-house-rules.js +64 -0
- package/lib/coding-task-spec.d.ts +44 -0
- package/lib/coding-task-spec.js +183 -0
- package/lib/config-precedence.d.ts +25 -0
- package/lib/config-precedence.js +38 -0
- package/lib/deny-check.d.ts +11 -0
- package/lib/deny-check.js +76 -0
- package/lib/deny-hook-synthesis.d.ts +93 -0
- package/lib/deny-hook-synthesis.js +399 -0
- package/lib/deny-hooks.d.ts +28 -0
- package/lib/deny-hooks.js +155 -0
- package/lib/deployment-docs-audit.d.ts +41 -0
- package/lib/deployment-docs-audit.js +115 -0
- package/lib/discover-cli.d.ts +94 -0
- package/lib/discover-cli.js +284 -0
- package/lib/discovery-throttle.d.ts +8 -0
- package/lib/discovery-throttle.js +33 -0
- package/lib/env-file-indirection.d.ts +4 -0
- package/lib/env-file-indirection.js +45 -0
- package/lib/event-ledger-cli.d.ts +82 -0
- package/lib/event-ledger-cli.js +266 -0
- package/lib/event-ledger.d.ts +37 -0
- package/lib/event-ledger.js +210 -0
- package/lib/execute-local-write.d.ts +14 -0
- package/lib/execute-local-write.js +50 -0
- package/lib/feasibility-cli.d.ts +25 -0
- package/lib/feasibility-cli.js +80 -0
- package/lib/forge-config.d.ts +17 -0
- package/lib/forge-config.js +37 -0
- package/lib/governor-action-mode.d.ts +26 -0
- package/lib/governor-action-mode.js +47 -0
- package/lib/governor-chokepoint-persisted.d.ts +18 -0
- package/lib/governor-chokepoint-persisted.js +46 -0
- package/lib/governor-chokepoint.d.ts +14 -0
- package/lib/governor-chokepoint.js +52 -0
- package/lib/governor-kill-switch.d.ts +26 -0
- package/lib/governor-kill-switch.js +47 -0
- package/lib/governor-ledger-cli.d.ts +32 -0
- package/lib/governor-ledger-cli.js +155 -0
- package/lib/governor-ledger.d.ts +46 -0
- package/lib/governor-ledger.js +203 -0
- package/lib/governor-open-pr.d.ts +21 -0
- package/lib/governor-open-pr.js +27 -0
- package/lib/governor-pause-cli.d.ts +23 -0
- package/lib/governor-pause-cli.js +171 -0
- package/lib/governor-run-halt.d.ts +37 -0
- package/lib/governor-run-halt.js +58 -0
- package/lib/governor-state.d.ts +63 -0
- package/lib/governor-state.js +363 -0
- package/lib/governor-write-rate-limit.d.ts +30 -0
- package/lib/governor-write-rate-limit.js +64 -0
- package/lib/harness-submission-trigger.d.ts +69 -0
- package/lib/harness-submission-trigger.js +138 -0
- package/lib/http-retry.d.ts +12 -0
- package/lib/http-retry.js +52 -0
- package/lib/laptop-init.d.ts +53 -0
- package/lib/laptop-init.js +332 -0
- package/lib/live-issue-snapshot.d.ts +16 -0
- package/lib/live-issue-snapshot.js +117 -0
- package/lib/local-store.d.ts +18 -0
- package/lib/local-store.js +62 -0
- package/lib/logger.d.ts +58 -0
- package/lib/logger.js +167 -0
- package/lib/loop-cli.d.ts +66 -0
- package/lib/loop-cli.js +540 -0
- package/lib/loop-closure.d.ts +35 -0
- package/lib/loop-closure.js +66 -0
- package/lib/loop-reentry.d.ts +51 -0
- package/lib/loop-reentry.js +124 -0
- package/lib/manage-poll.d.ts +79 -0
- package/lib/manage-poll.js +254 -0
- package/lib/manage-status.d.ts +70 -0
- package/lib/manage-status.js +241 -0
- package/lib/metrics-cli.d.ts +9 -0
- package/lib/metrics-cli.js +50 -0
- package/lib/migrate-cli.d.ts +24 -0
- package/lib/migrate-cli.js +111 -0
- package/lib/miner-goal-spec.d.ts +12 -0
- package/lib/miner-goal-spec.js +67 -0
- package/lib/opportunity-fanout.d.ts +86 -0
- package/lib/opportunity-fanout.js +597 -0
- package/lib/opportunity-ranker.d.ts +36 -0
- package/lib/opportunity-ranker.js +118 -0
- package/lib/orb-export.d.ts +54 -0
- package/lib/orb-export.js +206 -0
- package/lib/plan-store-cli.d.ts +37 -0
- package/lib/plan-store-cli.js +151 -0
- package/lib/plan-store.d.ts +51 -0
- package/lib/plan-store.js +231 -0
- package/lib/policy-doc-cache.d.ts +25 -0
- package/lib/policy-doc-cache.js +79 -0
- package/lib/policy-verdict-cache.d.ts +35 -0
- package/lib/policy-verdict-cache.js +106 -0
- package/lib/portfolio-dashboard.d.ts +30 -0
- package/lib/portfolio-dashboard.js +105 -0
- package/lib/portfolio-discovery.d.ts +29 -0
- package/lib/portfolio-discovery.js +100 -0
- package/lib/portfolio-queue-cli.d.ts +87 -0
- package/lib/portfolio-queue-cli.js +509 -0
- package/lib/portfolio-queue-expiry.d.ts +20 -0
- package/lib/portfolio-queue-expiry.js +51 -0
- package/lib/portfolio-queue-manager.d.ts +50 -0
- package/lib/portfolio-queue-manager.js +134 -0
- package/lib/portfolio-queue.d.ts +62 -0
- package/lib/portfolio-queue.js +351 -0
- package/lib/pr-disposition-poller.d.ts +26 -0
- package/lib/pr-disposition-poller.js +163 -0
- package/lib/pr-number-parse.d.ts +4 -0
- package/lib/pr-number-parse.js +23 -0
- package/lib/pr-outcome.d.ts +41 -0
- package/lib/pr-outcome.js +90 -0
- package/lib/prediction-ledger.d.ts +47 -0
- package/lib/prediction-ledger.js +222 -0
- package/lib/pretooluse-hook.d.ts +31 -0
- package/lib/pretooluse-hook.js +93 -0
- package/lib/process-lifecycle.d.ts +31 -0
- package/lib/process-lifecycle.js +106 -0
- package/lib/purge-cli.d.ts +44 -0
- package/lib/purge-cli.js +198 -0
- package/lib/rejection-signal.d.ts +6 -0
- package/lib/rejection-signal.js +101 -0
- package/lib/rejection-state-machine.d.ts +37 -0
- package/lib/rejection-state-machine.js +81 -0
- package/lib/rejection-templates.d.ts +12 -0
- package/lib/rejection-templates.js +71 -0
- package/lib/replay-objective-anchor.d.ts +79 -0
- package/lib/replay-objective-anchor.js +179 -0
- package/lib/replay-snapshot.d.ts +41 -0
- package/lib/replay-snapshot.js +288 -0
- package/lib/replay-task-generation.d.ts +126 -0
- package/lib/replay-task-generation.js +223 -0
- package/lib/repo-clone.d.ts +19 -0
- package/lib/repo-clone.js +102 -0
- package/lib/run-state-cli.d.ts +27 -0
- package/lib/run-state-cli.js +154 -0
- package/lib/run-state.d.ts +37 -0
- package/lib/run-state.js +150 -0
- package/lib/schema-version.d.ts +17 -0
- package/lib/schema-version.js +71 -0
- package/lib/self-review-context.d.ts +28 -0
- package/lib/self-review-context.js +339 -0
- package/lib/slop-assessment.d.ts +3 -0
- package/lib/slop-assessment.js +20 -0
- package/lib/stack-detection.d.ts +41 -0
- package/lib/stack-detection.js +248 -0
- package/lib/status.d.ts +64 -0
- package/lib/status.js +441 -0
- package/lib/store-maintenance.d.ts +31 -0
- package/lib/store-maintenance.js +183 -0
- package/lib/submission-freshness-check.d.ts +32 -0
- package/lib/submission-freshness-check.js +93 -0
- package/lib/update-check.d.ts +36 -0
- package/lib/update-check.js +161 -0
- package/lib/version.d.ts +3 -0
- package/lib/version.js +10 -0
- package/lib/worktree-allocator.d.ts +39 -0
- package/lib/worktree-allocator.js +262 -0
- package/package.json +50 -0
- package/schema/miner-goal-spec.schema.json +111 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Rejection state machine (#4278): the missing detector + classifier that turns a closed-without-merge PR
|
|
2
|
+
// into a rejection-reason bucket and, for the first time, drives `renderRejectionMessage`
|
|
3
|
+
// (rejection-templates.js, which until now had zero callers outside its own test). Pure classification and
|
|
4
|
+
// content only — no GitHub calls, no network, no writes. The caller (a poller) persists the result locally.
|
|
5
|
+
//
|
|
6
|
+
// DESIGN DECISIONS (called out explicitly by #4278):
|
|
7
|
+
// • "disengaged" is a per-PR OUTCOME, not a per-repo run-state. A rejection is about one PR, so it belongs
|
|
8
|
+
// with the `manage-poll.js` outcome family (ready / needs-work / open), NOT `run-state.js`'s RUN_STATES
|
|
9
|
+
// (idle / discovering / planning / preparing). `DISENGAGED_OUTCOME` is defined HERE and left for a poller
|
|
10
|
+
// to adopt — this module deliberately does NOT mutate manage-poll.js's or run-state.js's enum as a side
|
|
11
|
+
// effect (the issue explicitly warns against silently expanding another module's vocabulary).
|
|
12
|
+
// • Zero-signal fallback: with no gate/duplicate signal, a rejection classifies as `maintainer_close_no_reason`
|
|
13
|
+
// — the courteous, non-assuming bucket — rather than being left unclassified, so a rejection ALWAYS renders
|
|
14
|
+
// a note.
|
|
15
|
+
// • This surfaces the PR's terminal fields from a payload the poller already fetches (ci-poller.js's
|
|
16
|
+
// `fetchHeadSha` GETs the full `/pulls/{n}` body, :155-163, and discards all but `head.sha`) via a pure
|
|
17
|
+
// extractor — no second API call, and no behavioral change to the existing fetch.
|
|
18
|
+
|
|
19
|
+
import { renderRejectionMessage } from "./rejection-templates.js";
|
|
20
|
+
|
|
21
|
+
/** Per-PR terminal outcome for a rejected (closed-without-merge) PR. A poller adds this to its own outcome
|
|
22
|
+
* vocabulary alongside ready / needs-work / open. */
|
|
23
|
+
export const DISENGAGED_OUTCOME = "disengaged";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Pull the terminal-outcome fields from a `GET /pulls/{n}` payload the poller already has. Pure — no API call.
|
|
27
|
+
* Missing/malformed fields normalize to null/false so a partial payload never throws here.
|
|
28
|
+
* @param {unknown} prPayload
|
|
29
|
+
* @returns {{ state: string | null, merged: boolean, mergedAt: string | null, closedAt: string | null }}
|
|
30
|
+
*/
|
|
31
|
+
export function extractPrOutcomeFields(prPayload) {
|
|
32
|
+
const p = prPayload && typeof prPayload === "object" ? prPayload : {};
|
|
33
|
+
return {
|
|
34
|
+
state: typeof p.state === "string" ? p.state : null,
|
|
35
|
+
merged: p.merged === true,
|
|
36
|
+
mergedAt: typeof p.merged_at === "string" ? p.merged_at : null,
|
|
37
|
+
closedAt: typeof p.closed_at === "string" ? p.closed_at : null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* True when a PR is closed WITHOUT a merge — the rejection this state machine acts on. A merged PR (even though
|
|
43
|
+
* GitHub also marks it `state: "closed"`) is NOT a rejection. Pure.
|
|
44
|
+
* @param {{ state?: string | null, merged?: boolean }} fields
|
|
45
|
+
*/
|
|
46
|
+
export function isRejectedPr(fields) {
|
|
47
|
+
const f = fields && typeof fields === "object" ? fields : {};
|
|
48
|
+
return f.state === "closed" && f.merged !== true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Classify a detected rejection into one of the rejection-reason buckets from the available signal.
|
|
53
|
+
* Precedence: an explicit gate close outranks a duplicate signal (the gate is the more specific, actionable
|
|
54
|
+
* cause). With neither signal, defaults to `maintainer_close_no_reason` (the documented zero-signal fallback).
|
|
55
|
+
* Pure.
|
|
56
|
+
* @param {{ gateClosed?: boolean, supersededByDuplicate?: boolean }} [signal]
|
|
57
|
+
* @returns {"gate_close" | "superseded_by_duplicate" | "maintainer_close_no_reason"}
|
|
58
|
+
*/
|
|
59
|
+
export function classifyRejectionReason(signal = {}) {
|
|
60
|
+
const s = signal && typeof signal === "object" ? signal : {};
|
|
61
|
+
if (s.gateClosed === true) return "gate_close";
|
|
62
|
+
if (s.supersededByDuplicate === true) return "superseded_by_duplicate";
|
|
63
|
+
return "maintainer_close_no_reason";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The full transition. Given a PR payload, an optional gate/duplicate signal, and the render context
|
|
68
|
+
* (`{ repoFullName, prNumber }`), decide whether the PR is a rejection and, if so, produce the disengaged
|
|
69
|
+
* transition: the classified reason and the rendered courtesy note (this is `renderRejectionMessage`'s first
|
|
70
|
+
* real caller). Returns null when the PR is not a rejection (still open, or merged) — nothing to disengage.
|
|
71
|
+
* Pure and deterministic; the caller persists `{ outcome, reason, note }` via its local event ledger.
|
|
72
|
+
* @returns {{ outcome: string, reason: string, note: string,
|
|
73
|
+
* fields: ReturnType<typeof extractPrOutcomeFields> } | null}
|
|
74
|
+
*/
|
|
75
|
+
export function resolveRejection(prPayload, signal, context) {
|
|
76
|
+
const fields = extractPrOutcomeFields(prPayload);
|
|
77
|
+
if (!isRejectedPr(fields)) return null;
|
|
78
|
+
const reason = classifyRejectionReason(signal);
|
|
79
|
+
const note = renderRejectionMessage(reason, context); // throws on malformed context — a half-note never emits
|
|
80
|
+
return { outcome: DISENGAGED_OUTCOME, reason, note, fields };
|
|
81
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type RejectionReason = "gate_close" | "maintainer_close_no_reason" | "superseded_by_duplicate";
|
|
2
|
+
|
|
3
|
+
export type RejectionContext = {
|
|
4
|
+
repoFullName: string;
|
|
5
|
+
prNumber: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const REJECTION_REASONS: readonly RejectionReason[];
|
|
9
|
+
|
|
10
|
+
export function containsPrivateLanguage(text: string): boolean;
|
|
11
|
+
|
|
12
|
+
export function renderRejectionMessage(reason: RejectionReason, context: RejectionContext): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// CoC-compliant rejection message templates (#2324). When one of the miner's PRs is closed/rejected, it may leave
|
|
2
|
+
// a single, final, human-readable local note (e.g. in a run summary or CLI output — posting anywhere is a separate
|
|
3
|
+
// write action, out of scope here). The note must be courteous, non-defensive, and never re-litigate the
|
|
4
|
+
// maintainer's decision. This module is pure content/formatting: static template strings + a deterministic
|
|
5
|
+
// renderer — no GitHub calls, no LLM, no network. Same inputs always render the same message.
|
|
6
|
+
|
|
7
|
+
// Templates keyed by rejection-reason bucket. Every placeholder is `{name}`; the renderer resolves the structured
|
|
8
|
+
// context (a PR number + a repo) and never interpolates free-form/private text.
|
|
9
|
+
const REASON_TEMPLATES = {
|
|
10
|
+
gate_close:
|
|
11
|
+
"The automated review gate closed PR #{prNumber} on {repoFullName}. Thanks for the review — I'll address the flagged points and open a fresh PR if the change still fits.",
|
|
12
|
+
maintainer_close_no_reason:
|
|
13
|
+
"PR #{prNumber} on {repoFullName} was closed by the maintainer. Thanks for taking the time to look — I'll leave it here unless you'd like me to revisit it.",
|
|
14
|
+
superseded_by_duplicate:
|
|
15
|
+
"PR #{prNumber} on {repoFullName} looks superseded by other work on the same issue, so I'm closing it on my side to avoid duplication. Thanks to whoever is carrying it forward.",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** The supported rejection-reason buckets, in declaration order. */
|
|
19
|
+
export const REJECTION_REASONS = Object.freeze(Object.keys(REASON_TEMPLATES));
|
|
20
|
+
|
|
21
|
+
// Private-language tokens that must never surface in a public-facing courtesy note (mirrors the redaction set in
|
|
22
|
+
// `sanitizePublicComment`, src/github/commands.ts). Templates are authored clean and this is asserted in tests;
|
|
23
|
+
// the structured context (a PR number + a validated `owner/repo`) carries no private scoring/reward/wallet data,
|
|
24
|
+
// so — deliberately — no value-level redaction is applied that could mangle a legitimate repo name.
|
|
25
|
+
const PRIVATE_LANGUAGE =
|
|
26
|
+
/\b(?:raw trust scores?|trust scores?|wallets?|hotkeys?|coldkeys?|seed phrases?|mnemonics?|payouts?|rewards?)\b/i;
|
|
27
|
+
|
|
28
|
+
/** True when the given text contains any banned private-language token. */
|
|
29
|
+
export function containsPrivateLanguage(text) {
|
|
30
|
+
return PRIVATE_LANGUAGE.test(text);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A GitHub `owner/repo`: owner is 1-39 chars of alphanumerics/hyphens starting alphanumeric; repo is
|
|
34
|
+
// alphanumerics/`.`/`_`/`-`. Anchored + character-class-restricted so control characters, whitespace, markup, or an
|
|
35
|
+
// extra `/` (e.g. `owner/repo\nextra`, `owner/<repo>`) are rejected — the note interpolates this text directly, so a
|
|
36
|
+
// malformed value must throw rather than leak caller-controlled display text.
|
|
37
|
+
const GITHUB_FULL_NAME = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]{1,100}$/;
|
|
38
|
+
|
|
39
|
+
function normalizeRepoFullName(repoFullName) {
|
|
40
|
+
if (typeof repoFullName !== "string") throw new Error("invalid_repo_full_name");
|
|
41
|
+
const trimmed = repoFullName.trim();
|
|
42
|
+
if (!GITHUB_FULL_NAME.test(trimmed)) throw new Error("invalid_repo_full_name");
|
|
43
|
+
return trimmed;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizePrNumber(prNumber) {
|
|
47
|
+
if (!Number.isInteger(prNumber) || prNumber < 1) throw new Error("invalid_pr_number");
|
|
48
|
+
return prNumber;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Render the courtesy note for a closed/rejected PR. `reason` must be one of {@link REJECTION_REASONS}; `context`
|
|
53
|
+
* supplies `repoFullName` (`owner/repo`) and `prNumber` (a positive integer). Throws on an unknown reason, a
|
|
54
|
+
* malformed context, or (defensively) any placeholder a template leaves unresolved — so a caller can never emit a
|
|
55
|
+
* half-rendered note. Pure and deterministic.
|
|
56
|
+
*/
|
|
57
|
+
export function renderRejectionMessage(reason, context = {}) {
|
|
58
|
+
const template = REASON_TEMPLATES[reason];
|
|
59
|
+
if (template === undefined) throw new Error("invalid_rejection_reason");
|
|
60
|
+
const values = {
|
|
61
|
+
repoFullName: normalizeRepoFullName(context.repoFullName),
|
|
62
|
+
prNumber: normalizePrNumber(context.prNumber),
|
|
63
|
+
};
|
|
64
|
+
const rendered = template.replace(/\{(\w+)\}/g, (_match, key) => {
|
|
65
|
+
const value = values[key];
|
|
66
|
+
if (value === undefined) throw new Error(`missing_placeholder:${key}`);
|
|
67
|
+
return String(value);
|
|
68
|
+
});
|
|
69
|
+
if (/\{[^}]+\}/.test(rendered)) throw new Error("unresolved_placeholder");
|
|
70
|
+
return rendered;
|
|
71
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export type ChangeKind =
|
|
2
|
+
| "feature"
|
|
3
|
+
| "fix"
|
|
4
|
+
| "refactor"
|
|
5
|
+
| "docs"
|
|
6
|
+
| "test"
|
|
7
|
+
| "chore"
|
|
8
|
+
| "perf"
|
|
9
|
+
| "build"
|
|
10
|
+
| "ci"
|
|
11
|
+
| "style"
|
|
12
|
+
| "other";
|
|
13
|
+
|
|
14
|
+
export const CHANGE_KINDS: readonly ChangeKind[];
|
|
15
|
+
export const MODULE_OVERLAP_WEIGHT: number;
|
|
16
|
+
export const CHANGE_KIND_WEIGHT: number;
|
|
17
|
+
|
|
18
|
+
export type ReplayPlanInput = {
|
|
19
|
+
pathsTouched?: unknown;
|
|
20
|
+
changeKind?: unknown;
|
|
21
|
+
title?: unknown;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type RevealedHistoryEntry = {
|
|
25
|
+
pathsTouched?: unknown;
|
|
26
|
+
changeKind?: unknown;
|
|
27
|
+
title?: unknown;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ReplayTargetFeatures = {
|
|
31
|
+
modules: string[];
|
|
32
|
+
changeKind: ChangeKind;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type RevealedFeatures = {
|
|
36
|
+
modules: string[];
|
|
37
|
+
changeKinds: ChangeKind[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ObjectiveAnchorBreakdown = {
|
|
41
|
+
score: number;
|
|
42
|
+
moduleOverlap: number;
|
|
43
|
+
changeKindMatch: 0 | 1;
|
|
44
|
+
replayChangeKind: ChangeKind;
|
|
45
|
+
revealedChangeKinds: ChangeKind[];
|
|
46
|
+
sharedModules: string[];
|
|
47
|
+
replayOnlyModules: string[];
|
|
48
|
+
revealedOnlyModules: string[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type ObjectiveAnchorResult = ObjectiveAnchorBreakdown & {
|
|
52
|
+
replayFeatures: ReplayTargetFeatures;
|
|
53
|
+
revealedFeatures: RevealedFeatures;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function classifyChangeKind(value: unknown): ChangeKind;
|
|
57
|
+
|
|
58
|
+
export function extractReplayTargetFeatures(
|
|
59
|
+
plan: ReplayPlanInput | null | undefined,
|
|
60
|
+
): ReplayTargetFeatures;
|
|
61
|
+
|
|
62
|
+
export function extractRevealedFeatures(
|
|
63
|
+
history: readonly unknown[] | RevealedHistoryEntry | null | undefined,
|
|
64
|
+
): RevealedFeatures;
|
|
65
|
+
|
|
66
|
+
export function scoreObjectiveAnchor(
|
|
67
|
+
replayFeatures: { modules?: unknown; changeKind?: unknown } | null | undefined,
|
|
68
|
+
revealedFeatures: { modules?: unknown; changeKinds?: unknown } | null | undefined,
|
|
69
|
+
): ObjectiveAnchorBreakdown;
|
|
70
|
+
|
|
71
|
+
export function computeObjectiveAnchor(
|
|
72
|
+
input:
|
|
73
|
+
| {
|
|
74
|
+
replayPlan?: ReplayPlanInput | null;
|
|
75
|
+
revealedHistory?: RevealedHistoryEntry[] | RevealedHistoryEntry | null;
|
|
76
|
+
}
|
|
77
|
+
| null
|
|
78
|
+
| undefined,
|
|
79
|
+
): ObjectiveAnchorResult;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Deterministic structural "objective-anchor" score for the historical-replay calibration harness (#3012).
|
|
2
|
+
//
|
|
3
|
+
// Once a replay run produces a plan/PR against a frozen snapshot, half of the calibration score is meant to
|
|
4
|
+
// come from a deterministic, auditable structural comparison rather than an LLM judgment. This module is that
|
|
5
|
+
// structural half: it compares what the miner's replayed output *targeted* (modules touched + change kind)
|
|
6
|
+
// against what the revealed post-T history *actually* changed, and returns a reproducible `[0, 1]` score plus
|
|
7
|
+
// a full audit breakdown. There is no model call in this path — given the same two feature sets it is
|
|
8
|
+
// byte-for-byte reproducible.
|
|
9
|
+
|
|
10
|
+
// Fixed change-kind vocabulary. Conventional-Commit types collapse onto these buckets; anything unrecognized
|
|
11
|
+
// degrades to "other" so a novel prefix lowers the signal instead of throwing.
|
|
12
|
+
export const CHANGE_KINDS = Object.freeze([
|
|
13
|
+
"feature",
|
|
14
|
+
"fix",
|
|
15
|
+
"refactor",
|
|
16
|
+
"docs",
|
|
17
|
+
"test",
|
|
18
|
+
"chore",
|
|
19
|
+
"perf",
|
|
20
|
+
"build",
|
|
21
|
+
"ci",
|
|
22
|
+
"style",
|
|
23
|
+
"other",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const CONVENTIONAL_TYPE_TO_KIND = new Map([
|
|
27
|
+
["feat", "feature"],
|
|
28
|
+
["feature", "feature"],
|
|
29
|
+
["fix", "fix"],
|
|
30
|
+
["bugfix", "fix"],
|
|
31
|
+
["refactor", "refactor"],
|
|
32
|
+
["docs", "docs"],
|
|
33
|
+
["doc", "docs"],
|
|
34
|
+
["test", "test"],
|
|
35
|
+
["tests", "test"],
|
|
36
|
+
["chore", "chore"],
|
|
37
|
+
["perf", "perf"],
|
|
38
|
+
["build", "build"],
|
|
39
|
+
["ci", "ci"],
|
|
40
|
+
["style", "style"],
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
// Fixed weights for the two structural components. They sum to 1 so the composed score stays in [0, 1].
|
|
44
|
+
export const MODULE_OVERLAP_WEIGHT = 0.7;
|
|
45
|
+
export const CHANGE_KIND_WEIGHT = 0.3;
|
|
46
|
+
|
|
47
|
+
const SCORE_PRECISION = 1e4;
|
|
48
|
+
|
|
49
|
+
function roundScore(value) {
|
|
50
|
+
return Math.round(value * SCORE_PRECISION) / SCORE_PRECISION;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// A path's "module" is its directory (everything before the final slash); a bare filename is its own module.
|
|
54
|
+
// Grouping by directory is what makes two different files in one directory a *partial* overlap, not a miss.
|
|
55
|
+
function pathToModule(path) {
|
|
56
|
+
const trimmed = path.trim().replace(/^(?:\.\/)+/, "").replace(/\/+$/, "");
|
|
57
|
+
if (!trimmed) return null;
|
|
58
|
+
const slash = trimmed.lastIndexOf("/");
|
|
59
|
+
return slash === -1 ? trimmed : trimmed.slice(0, slash);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeModules(pathsTouched) {
|
|
63
|
+
if (!Array.isArray(pathsTouched)) return [];
|
|
64
|
+
const modules = new Set();
|
|
65
|
+
for (const entry of pathsTouched) {
|
|
66
|
+
if (typeof entry !== "string") continue;
|
|
67
|
+
const module = pathToModule(entry);
|
|
68
|
+
if (module) modules.add(module);
|
|
69
|
+
}
|
|
70
|
+
return [...modules].sort();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeKindList(value) {
|
|
74
|
+
if (!Array.isArray(value)) return [];
|
|
75
|
+
const kinds = new Set();
|
|
76
|
+
for (const entry of value) {
|
|
77
|
+
if (typeof entry === "string" && CHANGE_KINDS.includes(entry)) kinds.add(entry);
|
|
78
|
+
}
|
|
79
|
+
return [...kinds].sort();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeModuleList(value) {
|
|
83
|
+
if (!Array.isArray(value)) return [];
|
|
84
|
+
const modules = new Set();
|
|
85
|
+
for (const entry of value) {
|
|
86
|
+
if (typeof entry === "string" && entry) modules.add(entry);
|
|
87
|
+
}
|
|
88
|
+
return [...modules].sort();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Deterministically map a Conventional-Commit-style subject (`feat(scope)!: …`) to a change-kind bucket.
|
|
92
|
+
// Missing prefix, unknown type, or non-string input all resolve to "other" rather than throwing.
|
|
93
|
+
export function classifyChangeKind(value) {
|
|
94
|
+
if (typeof value !== "string") return "other";
|
|
95
|
+
const match = /^\s*([A-Za-z]+)\s*(?:\([^)]*\))?\s*!?\s*:/.exec(value);
|
|
96
|
+
if (!match) return "other";
|
|
97
|
+
return CONVENTIONAL_TYPE_TO_KIND.get(match[1].toLowerCase()) ?? "other";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveChangeKind(entry) {
|
|
101
|
+
if (entry && typeof entry.changeKind === "string") {
|
|
102
|
+
const explicit = entry.changeKind.trim().toLowerCase();
|
|
103
|
+
if (CHANGE_KINDS.includes(explicit)) return explicit;
|
|
104
|
+
}
|
|
105
|
+
return classifyChangeKind(entry?.title);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Structural features of the miner's replayed plan/PR: the sorted, de-duplicated set of modules it targeted
|
|
109
|
+
// and its single change kind (explicit `changeKind` wins; otherwise classified from `title`).
|
|
110
|
+
export function extractReplayTargetFeatures(plan) {
|
|
111
|
+
return {
|
|
112
|
+
modules: normalizeModules(plan?.pathsTouched),
|
|
113
|
+
changeKind: resolveChangeKind(plan),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Structural features of the revealed post-T history. The history is a list of commits/PRs (a single object
|
|
118
|
+
// is tolerated as a one-element list); modules are unioned and change kinds collected into a set, since the
|
|
119
|
+
// revealed side legitimately spans several changes.
|
|
120
|
+
export function extractRevealedFeatures(history) {
|
|
121
|
+
const entries = Array.isArray(history) ? history : history ? [history] : [];
|
|
122
|
+
const modules = new Set();
|
|
123
|
+
const changeKinds = new Set();
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
if (!entry || typeof entry !== "object") continue;
|
|
126
|
+
for (const module of normalizeModules(entry.pathsTouched)) modules.add(module);
|
|
127
|
+
changeKinds.add(resolveChangeKind(entry));
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
modules: [...modules].sort(),
|
|
131
|
+
changeKinds: [...changeKinds].sort(),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Deterministic objective-anchor score from two already-extracted feature sets. No LLM, no clock, no
|
|
136
|
+
// randomness — identical inputs always yield an identical breakdown. A zero-overlap comparison (disjoint
|
|
137
|
+
// modules and a change kind the revealed side never shows) resolves to the score floor `0`, never an error.
|
|
138
|
+
export function scoreObjectiveAnchor(replayFeatures, revealedFeatures) {
|
|
139
|
+
const replayModules = normalizeModuleList(replayFeatures?.modules);
|
|
140
|
+
const revealedModules = normalizeModuleList(revealedFeatures?.modules);
|
|
141
|
+
const replayChangeKind =
|
|
142
|
+
typeof replayFeatures?.changeKind === "string" && CHANGE_KINDS.includes(replayFeatures.changeKind)
|
|
143
|
+
? replayFeatures.changeKind
|
|
144
|
+
: "other";
|
|
145
|
+
const revealedChangeKinds = normalizeKindList(revealedFeatures?.changeKinds);
|
|
146
|
+
|
|
147
|
+
const replaySet = new Set(replayModules);
|
|
148
|
+
const revealedSet = new Set(revealedModules);
|
|
149
|
+
const sharedModules = replayModules.filter((module) => revealedSet.has(module));
|
|
150
|
+
const replayOnlyModules = replayModules.filter((module) => !revealedSet.has(module));
|
|
151
|
+
const revealedOnlyModules = revealedModules.filter((module) => !replaySet.has(module));
|
|
152
|
+
|
|
153
|
+
const unionSize = replayModules.length + revealedModules.length - sharedModules.length;
|
|
154
|
+
const moduleOverlap = unionSize === 0 ? 0 : sharedModules.length / unionSize;
|
|
155
|
+
const changeKindMatch = revealedChangeKinds.includes(replayChangeKind) ? 1 : 0;
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
score: roundScore(MODULE_OVERLAP_WEIGHT * moduleOverlap + CHANGE_KIND_WEIGHT * changeKindMatch),
|
|
159
|
+
moduleOverlap: roundScore(moduleOverlap),
|
|
160
|
+
changeKindMatch,
|
|
161
|
+
replayChangeKind,
|
|
162
|
+
revealedChangeKinds,
|
|
163
|
+
sharedModules,
|
|
164
|
+
replayOnlyModules,
|
|
165
|
+
revealedOnlyModules,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// One-shot entry point: extract both sides, score them, and return the score together with the extracted
|
|
170
|
+
// feature sets so a low score is auditable after the fact without re-running the extraction.
|
|
171
|
+
export function computeObjectiveAnchor(input) {
|
|
172
|
+
const replayFeatures = extractReplayTargetFeatures(input?.replayPlan);
|
|
173
|
+
const revealedFeatures = extractRevealedFeatures(input?.revealedHistory);
|
|
174
|
+
return {
|
|
175
|
+
...scoreObjectiveAnchor(replayFeatures, revealedFeatures),
|
|
176
|
+
replayFeatures,
|
|
177
|
+
revealedFeatures,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { WorktreeExecFn, WorktreeRemoveResult } from "@loopover/engine";
|
|
2
|
+
|
|
3
|
+
export const REPLAY_SNAPSHOT_SUBDIR: ".gittensory-replay-snapshots";
|
|
4
|
+
|
|
5
|
+
export type ReplaySnapshotCommit = { sha: string; date: string; subject: string };
|
|
6
|
+
export type ReplaySnapshotTag = { name: string; date: string; targetSha: string };
|
|
7
|
+
export type ReplaySnapshotReadme = { filename: string; content: string };
|
|
8
|
+
|
|
9
|
+
export type ReplaySnapshot = {
|
|
10
|
+
repoFullName: string;
|
|
11
|
+
commitSha: string;
|
|
12
|
+
worktreePath: string;
|
|
13
|
+
targetDate: string;
|
|
14
|
+
commits: ReplaySnapshotCommit[];
|
|
15
|
+
tags: ReplaySnapshotTag[];
|
|
16
|
+
readme: ReplaySnapshotReadme | null;
|
|
17
|
+
exportedAt: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function resolveReplaySnapshotDbPath(env?: NodeJS.ProcessEnv): string;
|
|
21
|
+
|
|
22
|
+
export function planReplaySnapshotPath(input: { repoPath: string; commitSha: string }): string;
|
|
23
|
+
|
|
24
|
+
export function validateSnapshotFreshness(input: { targetDate: string; commits: ReplaySnapshotCommit[]; tags: ReplaySnapshotTag[] }): void;
|
|
25
|
+
|
|
26
|
+
export type ReplaySnapshotStore = {
|
|
27
|
+
dbPath: string;
|
|
28
|
+
getSnapshot(repoFullName: string, commitSha: string): ReplaySnapshot | null;
|
|
29
|
+
saveSnapshot(snapshot: Omit<ReplaySnapshot, "exportedAt">): ReplaySnapshot;
|
|
30
|
+
close(): void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function openReplaySnapshotStore(dbPath?: string): ReplaySnapshotStore;
|
|
34
|
+
export function closeDefaultReplaySnapshotStore(): void;
|
|
35
|
+
|
|
36
|
+
export function exportReplaySnapshot(
|
|
37
|
+
input: { repoPath: string; repoFullName: string; commitSha: string },
|
|
38
|
+
deps: { exec: WorktreeExecFn; store?: ReplaySnapshotStore },
|
|
39
|
+
): Promise<ReplaySnapshot>;
|
|
40
|
+
|
|
41
|
+
export function removeReplaySnapshotWorktree(exec: WorktreeExecFn, repoPath: string, worktreePath: string): Promise<WorktreeRemoveResult>;
|