@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,26 @@
|
|
|
1
|
+
export type PrDisposition = {
|
|
2
|
+
state: "open" | "closed";
|
|
3
|
+
merged: boolean;
|
|
4
|
+
closedAt: string | null;
|
|
5
|
+
attempts: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type PollPrDispositionOptions = {
|
|
9
|
+
apiBaseUrl?: string;
|
|
10
|
+
fetchFn?: typeof fetch;
|
|
11
|
+
githubToken?: string;
|
|
12
|
+
maxAttempts?: number;
|
|
13
|
+
minIntervalMs?: number;
|
|
14
|
+
maxIntervalMs?: number;
|
|
15
|
+
sleepFn?: (delayMs: number) => Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function pollPrDisposition(
|
|
19
|
+
repoFullName: string,
|
|
20
|
+
prNumber: number,
|
|
21
|
+
options?: PollPrDispositionOptions,
|
|
22
|
+
): Promise<PrDisposition>;
|
|
23
|
+
|
|
24
|
+
export function classifyPrDisposition(
|
|
25
|
+
disposition: Pick<PrDisposition, "state" | "merged">,
|
|
26
|
+
): "merged" | "disengaged" | "other";
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Real PR-disposition poller (#5135, Wave 3.5 -- the autonomous loop). ci-poller.js already polls a PR's CI
|
|
2
|
+
// check-runs, but that answers a DIFFERENT question ("did the checks pass") from what the supervising loop
|
|
3
|
+
// needs at cycle-close time ("did the PR itself get merged or closed"). Nothing in this package answered that
|
|
4
|
+
// second question before this file: pr-outcome.js already has a real store for the classification
|
|
5
|
+
// (recordPrOutcomeSnapshot/readPrOutcomes), but every existing caller of it was a test -- this is the real
|
|
6
|
+
// GitHub fetch that produces the classification pr-outcome.js's writer expects.
|
|
7
|
+
//
|
|
8
|
+
// Deliberately its own module, not folded into ci-poller.js: the two pollers ask genuinely different
|
|
9
|
+
// questions (check-run conclusion vs. PR merge/close disposition) with different terminal conditions (a
|
|
10
|
+
// check-run poll's "pending" means "wait for the SAME head commit's checks to finish"; a disposition poll's
|
|
11
|
+
// "open" means "wait for a human to actually merge or close the PR", a potentially much longer, unbounded
|
|
12
|
+
// wait) -- composing them into one poller would conflate two different backoff/timeout policies.
|
|
13
|
+
|
|
14
|
+
const defaultApiBaseUrl = "https://api.github.com";
|
|
15
|
+
const defaultMinIntervalMs = 60_000;
|
|
16
|
+
const defaultMaxIntervalMs = 5 * 60_000;
|
|
17
|
+
const defaultMaxAttempts = 1;
|
|
18
|
+
const githubApiVersion = "2022-11-28";
|
|
19
|
+
|
|
20
|
+
function normalizeApiBaseUrl(value) {
|
|
21
|
+
if (value === undefined) return defaultApiBaseUrl;
|
|
22
|
+
if (typeof value !== "string" || !value.trim()) return defaultApiBaseUrl;
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = new URL(value.trim());
|
|
26
|
+
} catch {
|
|
27
|
+
throw new Error("invalid_api_base_url");
|
|
28
|
+
}
|
|
29
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "api.github.com") {
|
|
30
|
+
throw new Error("invalid_api_base_url");
|
|
31
|
+
}
|
|
32
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/, "");
|
|
33
|
+
parsed.search = "";
|
|
34
|
+
parsed.hash = "";
|
|
35
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizePositiveInt(value, fallback, min, max) {
|
|
39
|
+
if (!Number.isFinite(value)) return fallback;
|
|
40
|
+
return Math.min(max, Math.max(min, Math.floor(value)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeOptions(options = {}) {
|
|
44
|
+
return {
|
|
45
|
+
apiBaseUrl: normalizeApiBaseUrl(options.apiBaseUrl),
|
|
46
|
+
fetchFn: options.fetchFn ?? fetch,
|
|
47
|
+
githubToken: typeof options.githubToken === "string" ? options.githubToken.trim() : "",
|
|
48
|
+
maxAttempts: normalizePositiveInt(options.maxAttempts, defaultMaxAttempts, 1, 20),
|
|
49
|
+
minIntervalMs: normalizePositiveInt(options.minIntervalMs, defaultMinIntervalMs, 1, 60 * 60_000),
|
|
50
|
+
maxIntervalMs: normalizePositiveInt(options.maxIntervalMs, defaultMaxIntervalMs, 1, 60 * 60_000),
|
|
51
|
+
sleepFn:
|
|
52
|
+
options.sleepFn ??
|
|
53
|
+
((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs))),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseRepoFullName(repoFullName) {
|
|
58
|
+
if (typeof repoFullName !== "string") throw new Error("invalid_repo_full_name");
|
|
59
|
+
const [owner, repo, extra] = repoFullName.split("/");
|
|
60
|
+
if (!owner?.trim() || !repo?.trim() || extra !== undefined) {
|
|
61
|
+
throw new Error("invalid_repo_full_name");
|
|
62
|
+
}
|
|
63
|
+
return { owner: owner.trim(), repo: repo.trim() };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizePullNumber(value) {
|
|
67
|
+
if (!Number.isInteger(value) || value <= 0) throw new Error("invalid_pr_number");
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function githubHeaders(githubToken) {
|
|
72
|
+
const headers = {
|
|
73
|
+
accept: "application/vnd.github+json",
|
|
74
|
+
"user-agent": "loopover-miner",
|
|
75
|
+
"x-github-api-version": githubApiVersion,
|
|
76
|
+
};
|
|
77
|
+
if (githubToken) headers.authorization = `Bearer ${githubToken}`;
|
|
78
|
+
return headers;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function repoPath(target, suffix) {
|
|
82
|
+
return `/repos/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}${suffix}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function apiUrl(apiBaseUrl, path) {
|
|
86
|
+
return `${apiBaseUrl}${path}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function githubError(response, payload) {
|
|
90
|
+
const code = `github_${response.status}`;
|
|
91
|
+
const githubMessage =
|
|
92
|
+
typeof payload?.message === "string" && payload.message.trim() ? payload.message : null;
|
|
93
|
+
const message = githubMessage ? `${code}: ${githubMessage}` : code;
|
|
94
|
+
return Object.assign(new Error(message), { code, githubMessage });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function fetchPullRequest(target, prNumber, options) {
|
|
98
|
+
const response = await options.fetchFn(apiUrl(options.apiBaseUrl, repoPath(target, `/pulls/${prNumber}`)), {
|
|
99
|
+
method: "GET",
|
|
100
|
+
headers: githubHeaders(options.githubToken),
|
|
101
|
+
});
|
|
102
|
+
const payload = await response.json().catch(() => null);
|
|
103
|
+
if (!response.ok) throw githubError(response, payload);
|
|
104
|
+
return payload;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** GitHub's own vocabulary is `state: "open"|"closed"` plus a separate `merged: boolean` -- "closed and not
|
|
108
|
+
* merged" is the disengaged case. A still-open PR is never terminal for this poller's purposes. */
|
|
109
|
+
function normalizeDisposition(payload) {
|
|
110
|
+
const state = payload?.state === "closed" ? "closed" : "open";
|
|
111
|
+
const merged = Boolean(payload?.merged);
|
|
112
|
+
const closedAt = typeof payload?.closed_at === "string" ? payload.closed_at : null;
|
|
113
|
+
return { state, merged, closedAt };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function backoffDelayMs(attemptIndex, options) {
|
|
117
|
+
const exponent = Math.min(10, Math.max(0, attemptIndex));
|
|
118
|
+
return Math.min(options.maxIntervalMs, options.minIntervalMs * 2 ** exponent);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Poll a real PR's own merge/close disposition (distinct from its CI check-run conclusion, ci-poller.js's
|
|
123
|
+
* concern) with exponential backoff, until it reaches a terminal `state: "closed"` or `maxAttempts` is
|
|
124
|
+
* exhausted -- whichever comes first. A still-`"open"` PR after the last attempt is returned as-is, not an
|
|
125
|
+
* error: an unattended loop cycle should treat "still open" as "not yet resolved", not fail.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} repoFullName
|
|
128
|
+
* @param {number} prNumber
|
|
129
|
+
* @param {{
|
|
130
|
+
* apiBaseUrl?: string, fetchFn?: typeof fetch, githubToken?: string, maxAttempts?: number,
|
|
131
|
+
* minIntervalMs?: number, maxIntervalMs?: number, sleepFn?: (delayMs: number) => Promise<void>,
|
|
132
|
+
* }} [options]
|
|
133
|
+
* @returns {Promise<{ state: "open"|"closed", merged: boolean, closedAt: string|null, attempts: number }>}
|
|
134
|
+
*/
|
|
135
|
+
export async function pollPrDisposition(repoFullName, prNumber, options = {}) {
|
|
136
|
+
const target = parseRepoFullName(repoFullName);
|
|
137
|
+
const normalizedPrNumber = normalizePullNumber(prNumber);
|
|
138
|
+
const normalizedOptions = normalizeOptions(options);
|
|
139
|
+
|
|
140
|
+
let latest = { state: "open", merged: false, closedAt: null, attempts: 0 };
|
|
141
|
+
for (let attempt = 0; attempt < normalizedOptions.maxAttempts; attempt += 1) {
|
|
142
|
+
const payload = await fetchPullRequest(target, normalizedPrNumber, normalizedOptions);
|
|
143
|
+
latest = { ...normalizeDisposition(payload), attempts: attempt + 1 };
|
|
144
|
+
if (latest.state === "closed") return latest;
|
|
145
|
+
if (attempt === normalizedOptions.maxAttempts - 1) return latest;
|
|
146
|
+
await normalizedOptions.sleepFn(backoffDelayMs(attempt, normalizedOptions));
|
|
147
|
+
}
|
|
148
|
+
return latest;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Classify a real, terminal PR disposition into loop-reentry.js's own `candidate.outcome` vocabulary
|
|
153
|
+
* (`"merged"|"disengaged"|"other"`). A still-open disposition (not yet resolved) classifies as `"other"` --
|
|
154
|
+
* the same bucket a runMinerAttempt outcome that never opened a PR at all falls into (nothing to re-enter on
|
|
155
|
+
* yet, in either case).
|
|
156
|
+
*
|
|
157
|
+
* @param {{ state: "open"|"closed", merged: boolean }} disposition
|
|
158
|
+
* @returns {"merged"|"disengaged"|"other"}
|
|
159
|
+
*/
|
|
160
|
+
export function classifyPrDisposition(disposition) {
|
|
161
|
+
if (disposition.state !== "closed") return "other";
|
|
162
|
+
return disposition.merged ? "merged" : "disengaged";
|
|
163
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Shared PR-number extraction from a real `gh pr create` executeLocalWrite result (#4848). `gh pr create`
|
|
2
|
+
// prints the new PR's URL to stdout on success -- this is the one place that URL is authoritatively parsed,
|
|
3
|
+
// so loop-cli.js's CI/gate-status polling and attempt-cli.js's post-submission claim-conflict check agree on
|
|
4
|
+
// exactly how a PR number is recovered from a real command's raw output.
|
|
5
|
+
|
|
6
|
+
/** `gh pr create` (local-write-tools.ts's `buildOpenPrSpec` -- no `--json` flag) prints the created PR's own
|
|
7
|
+
* URL to stdout on success; this is `gh`'s real, documented, stable CLI behavior, not an invented contract.
|
|
8
|
+
* Scoped to the exact target repo so an unrelated URL elsewhere in stdout/stderr noise can never match.
|
|
9
|
+
*
|
|
10
|
+
* @param {{ stdout?: string, code?: number | null, timedOut?: boolean } | null | undefined} execResult
|
|
11
|
+
* @param {string} repoFullName
|
|
12
|
+
* @returns {number | null}
|
|
13
|
+
*/
|
|
14
|
+
export function parsePrNumberFromExecResult(execResult, repoFullName) {
|
|
15
|
+
if (!execResult || execResult.timedOut || execResult.code !== 0 || typeof execResult.stdout !== "string") {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const escapedRepo = repoFullName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
19
|
+
const match = execResult.stdout.match(new RegExp(`github\\.com/${escapedRepo}/pull/(\\d+)`));
|
|
20
|
+
if (!match) return null;
|
|
21
|
+
const prNumber = Number(match[1]);
|
|
22
|
+
return Number.isInteger(prNumber) && prNumber > 0 ? prNumber : null;
|
|
23
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AppendEventInput, LedgerEntry } from "./event-ledger.js";
|
|
2
|
+
|
|
3
|
+
export const MINER_PR_OUTCOME_EVENT: "pr_outcome";
|
|
4
|
+
export const MINER_PR_OUTCOME_DECISIONS: readonly ["merged", "closed"];
|
|
5
|
+
|
|
6
|
+
export type MinerPrOutcomeDecision = "merged" | "closed";
|
|
7
|
+
|
|
8
|
+
export interface NormalizedPrOutcomePayload {
|
|
9
|
+
prNumber: number;
|
|
10
|
+
decision: MinerPrOutcomeDecision;
|
|
11
|
+
closedAt: string | null;
|
|
12
|
+
reason: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PrOutcomeInput {
|
|
16
|
+
repoFullName?: unknown;
|
|
17
|
+
prNumber?: unknown;
|
|
18
|
+
decision?: unknown;
|
|
19
|
+
closedAt?: unknown;
|
|
20
|
+
reason?: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RecordPrOutcomeOptions {
|
|
24
|
+
/** Optional at the type level so a caller can pass an unusable ledger to exercise the fail-closed guard; the
|
|
25
|
+
* writer throws `invalid_event_ledger` at runtime when this is absent or lacks `appendEvent`. Reuses the
|
|
26
|
+
* real EventLedger#appendEvent signature so a genuine EventLedger (not just a same-shaped stub) type-checks. */
|
|
27
|
+
eventLedger?: { appendEvent(event: AppendEventInput): LedgerEntry };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PrOutcomeLedgerReader {
|
|
31
|
+
readEvents(filter?: { since?: number; repoFullName?: string }): unknown[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function normalizePrOutcomePayload(payload: unknown): NormalizedPrOutcomePayload | null;
|
|
35
|
+
|
|
36
|
+
export function recordPrOutcomeSnapshot(input: PrOutcomeInput, options?: RecordPrOutcomeOptions): unknown;
|
|
37
|
+
|
|
38
|
+
export function readPrOutcomes(
|
|
39
|
+
eventLedger: PrOutcomeLedgerReader,
|
|
40
|
+
filter?: { since?: number; repoFullName?: string },
|
|
41
|
+
): Map<string, NormalizedPrOutcomePayload & { repoFullName: string }>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Miner-local PR-outcome record (#4274). The miner's OWN local record of the outcomes of its OWN PRs — merged or
|
|
2
|
+
// closed — written to the miner's local SQLite via the generic append-only event-ledger.js, mirroring how
|
|
3
|
+
// manage-status.js layers a specific typed event (MANAGE_PR_UPDATE_EVENT + a payload normalizer + a thin writer)
|
|
4
|
+
// on top of that same ledger.
|
|
5
|
+
//
|
|
6
|
+
// DISTINCT from the server-side `pr_outcome` concept: src/review/outcomes-wire.ts's `recordPrOutcome` writes
|
|
7
|
+
// `pr_outcome` rows to the HOSTED backend's D1 audit tables from the GitHub App's webhook stream — that is the
|
|
8
|
+
// gittensory SERVER recording ground truth for every contributor. THIS is a laptop-mode miner's local record of
|
|
9
|
+
// its own PRs (it may have no webhook relay at all): same concept name, different codebase layer, no shared code.
|
|
10
|
+
// The distinct `MINER_PR_OUTCOME_EVENT` local constant keeps the two from being conflated.
|
|
11
|
+
|
|
12
|
+
import { REJECTION_REASONS } from "./rejection-templates.js";
|
|
13
|
+
|
|
14
|
+
/** Event-ledger vocabulary for a miner-local PR outcome. */
|
|
15
|
+
export const MINER_PR_OUTCOME_EVENT = "pr_outcome";
|
|
16
|
+
|
|
17
|
+
/** The terminal decisions a miner records for one of its own PRs. */
|
|
18
|
+
export const MINER_PR_OUTCOME_DECISIONS = Object.freeze(["merged", "closed"]);
|
|
19
|
+
|
|
20
|
+
const decisionSet = new Set(MINER_PR_OUTCOME_DECISIONS);
|
|
21
|
+
const reasonSet = new Set(REJECTION_REASONS);
|
|
22
|
+
|
|
23
|
+
function optionalString(value) {
|
|
24
|
+
if (value === undefined || value === null) return null;
|
|
25
|
+
if (typeof value !== "string") return null;
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
return trimmed || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate + normalize a PR-outcome payload; returns `null` on any malformed shape (mirrors manage-status.js's
|
|
32
|
+
* `normalizeManageUpdatePayload`, so a bad row can neither be written nor read back). A `closed` decision may carry
|
|
33
|
+
* a reason bucket drawn from {@link REJECTION_REASONS} (shared with the rejection-state-machine sibling); a `merged`
|
|
34
|
+
* decision — or an unrecognized reason — normalizes the reason to `null` (a merged PR has no rejection reason).
|
|
35
|
+
*/
|
|
36
|
+
export function normalizePrOutcomePayload(payload) {
|
|
37
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
|
|
38
|
+
if (!Number.isInteger(payload.prNumber) || payload.prNumber <= 0) return null;
|
|
39
|
+
const decision = optionalString(payload.decision);
|
|
40
|
+
if (!decision || !decisionSet.has(decision)) return null;
|
|
41
|
+
const reasonRaw = optionalString(payload.reason);
|
|
42
|
+
const reason = decision === "closed" && reasonRaw !== null && reasonSet.has(reasonRaw) ? reasonRaw : null;
|
|
43
|
+
return {
|
|
44
|
+
prNumber: payload.prNumber,
|
|
45
|
+
decision,
|
|
46
|
+
closedAt: optionalString(payload.closedAt),
|
|
47
|
+
reason,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Thin writer over an INJECTED event ledger (same dependency-injection shape as manage-poll.js's
|
|
53
|
+
* `recordManagePollSnapshot`, so it's unit-testable without a real ledger file). Appends one
|
|
54
|
+
* {@link MINER_PR_OUTCOME_EVENT} scoped to the repo and returns the appended entry. Fail-soft on a malformed
|
|
55
|
+
* snapshot: a missing repo or an invalid payload returns `null` rather than throwing (an unusable ledger is the
|
|
56
|
+
* only hard error, since that is a programmer wiring mistake).
|
|
57
|
+
*/
|
|
58
|
+
export function recordPrOutcomeSnapshot(input, options = {}) {
|
|
59
|
+
const eventLedger = options.eventLedger;
|
|
60
|
+
if (!eventLedger || typeof eventLedger.appendEvent !== "function") throw new Error("invalid_event_ledger");
|
|
61
|
+
const repoFullName = typeof input?.repoFullName === "string" ? input.repoFullName.trim() : "";
|
|
62
|
+
if (!repoFullName) return null;
|
|
63
|
+
const payload = normalizePrOutcomePayload({
|
|
64
|
+
prNumber: input?.prNumber,
|
|
65
|
+
decision: input?.decision,
|
|
66
|
+
closedAt: input?.closedAt,
|
|
67
|
+
reason: input?.reason,
|
|
68
|
+
});
|
|
69
|
+
if (!payload) return null;
|
|
70
|
+
return eventLedger.appendEvent({ type: MINER_PR_OUTCOME_EVENT, repoFullName, payload });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Reconstruct the latest outcome per repo/PR from the ledger's ascending append-only event stream (mirrors
|
|
75
|
+
* manage-status.js's `indexLatestManageUpdates`). Reads via the injected ledger's `readEvents(filter)` and reduces
|
|
76
|
+
* the pure result — a later event for the same repo/PR supersedes an earlier one. Returns a `Map` keyed by
|
|
77
|
+
* `repoFullName:prNumber`.
|
|
78
|
+
*/
|
|
79
|
+
export function readPrOutcomes(eventLedger, filter = {}) {
|
|
80
|
+
const events = eventLedger && typeof eventLedger.readEvents === "function" ? eventLedger.readEvents(filter) : [];
|
|
81
|
+
const latest = new Map();
|
|
82
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
83
|
+
if (event?.type !== MINER_PR_OUTCOME_EVENT) continue;
|
|
84
|
+
if (typeof event.repoFullName !== "string" || !event.repoFullName.trim()) continue;
|
|
85
|
+
const normalized = normalizePrOutcomePayload(event.payload);
|
|
86
|
+
if (!normalized) continue;
|
|
87
|
+
latest.set(`${event.repoFullName}:${normalized.prNumber}`, { ...normalized, repoFullName: event.repoFullName });
|
|
88
|
+
}
|
|
89
|
+
return latest;
|
|
90
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type PredictionLedgerEntry = {
|
|
2
|
+
id: number;
|
|
3
|
+
ts: string;
|
|
4
|
+
repoFullName: string;
|
|
5
|
+
targetId: number;
|
|
6
|
+
headSha: string | null;
|
|
7
|
+
conclusion: string;
|
|
8
|
+
pack: string;
|
|
9
|
+
readinessScore: number | null;
|
|
10
|
+
blockerCodes: string[];
|
|
11
|
+
warningCodes: string[];
|
|
12
|
+
engineVersion: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type AppendPredictionInput = {
|
|
16
|
+
repoFullName: string;
|
|
17
|
+
targetId: number;
|
|
18
|
+
headSha?: string | null;
|
|
19
|
+
conclusion: string;
|
|
20
|
+
pack: string;
|
|
21
|
+
readinessScore?: number | null;
|
|
22
|
+
blockerCodes?: string[];
|
|
23
|
+
warningCodes?: string[];
|
|
24
|
+
engineVersion: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ReadPredictionsFilter = {
|
|
28
|
+
repoFullName?: string | null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type PredictionLedger = {
|
|
32
|
+
dbPath: string;
|
|
33
|
+
appendPrediction(input: AppendPredictionInput): PredictionLedgerEntry;
|
|
34
|
+
readPredictions(filter?: ReadPredictionsFilter): PredictionLedgerEntry[];
|
|
35
|
+
purgeByRepo(repoFullName: string): number;
|
|
36
|
+
close(): void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function resolvePredictionLedgerDbPath(env?: Record<string, string | undefined>): string;
|
|
40
|
+
|
|
41
|
+
export function initPredictionLedger(dbPath?: string): PredictionLedger;
|
|
42
|
+
|
|
43
|
+
export function appendPrediction(input: AppendPredictionInput): PredictionLedgerEntry;
|
|
44
|
+
|
|
45
|
+
export function readPredictions(filter?: ReadPredictionsFilter): PredictionLedgerEntry[];
|
|
46
|
+
|
|
47
|
+
export function closeDefaultPredictionLedger(): void;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import {
|
|
6
|
+
PREDICTION_LEDGER_PURGE_SPEC,
|
|
7
|
+
PREDICTION_LEDGER_RETENTION_SPEC,
|
|
8
|
+
purgeStoreByRepo,
|
|
9
|
+
pruneLedgerByRetention,
|
|
10
|
+
resolveLedgerRetentionPolicy,
|
|
11
|
+
} from "./store-maintenance.js";
|
|
12
|
+
|
|
13
|
+
// Append-only prediction ledger (#4263): every predicted-gate verdict the miner computes for a target lands in
|
|
14
|
+
// a local SQLite table so a later self-improve pass can score the prediction against the realized pr_outcome.
|
|
15
|
+
// IMMUTABILITY INVARIANT: `appendPrediction`/`readPredictions` only ever issue INSERT and SELECT — never
|
|
16
|
+
// UPDATE/DELETE. Two documented exceptions, both separate maintenance operations rather than part of normal
|
|
17
|
+
// ledger operation: opt-in retention pruning (#4834, automatic) and `purgeByRepo` (#5564, always explicit and
|
|
18
|
+
// operator-invoked, never automatic). Rows are kept small and stable for later diffing: blocker/warning CODES
|
|
19
|
+
// only (no free-text detail), plus the ENGINE_VERSION that produced the call so a row self-reports which engine
|
|
20
|
+
// build made it. Mirrors governor-ledger.js's shape; normalization is local (like event-ledger.js) so the
|
|
21
|
+
// offline miner package pulls in no engine module.
|
|
22
|
+
|
|
23
|
+
const defaultDbFileName = "prediction-ledger.sqlite3";
|
|
24
|
+
let defaultPredictionLedger = null;
|
|
25
|
+
|
|
26
|
+
export function resolvePredictionLedgerDbPath(env = process.env) {
|
|
27
|
+
const explicitPath = typeof env.GITTENSORY_MINER_PREDICTION_LEDGER_DB === "string"
|
|
28
|
+
? env.GITTENSORY_MINER_PREDICTION_LEDGER_DB.trim()
|
|
29
|
+
: "";
|
|
30
|
+
if (explicitPath) return explicitPath;
|
|
31
|
+
|
|
32
|
+
const explicitConfigDir = typeof env.GITTENSORY_MINER_CONFIG_DIR === "string"
|
|
33
|
+
? env.GITTENSORY_MINER_CONFIG_DIR.trim()
|
|
34
|
+
: "";
|
|
35
|
+
if (explicitConfigDir) return join(explicitConfigDir, defaultDbFileName);
|
|
36
|
+
|
|
37
|
+
const configHome = typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim()
|
|
38
|
+
? env.XDG_CONFIG_HOME.trim()
|
|
39
|
+
: join(homedir(), ".config");
|
|
40
|
+
return join(configHome, "gittensory-miner", defaultDbFileName);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeDbPath(dbPath) {
|
|
44
|
+
const path = (dbPath ?? resolvePredictionLedgerDbPath()).trim();
|
|
45
|
+
if (!path) throw new Error("invalid_prediction_ledger_db_path");
|
|
46
|
+
return path;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeRepoFullName(repoFullName) {
|
|
50
|
+
if (typeof repoFullName !== "string") throw new Error("invalid_repo_full_name");
|
|
51
|
+
const [owner, repo, extra] = repoFullName.trim().split("/");
|
|
52
|
+
if (!owner || !repo || extra !== undefined) throw new Error("invalid_repo_full_name");
|
|
53
|
+
return `${owner}/${repo}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeOptionalRepoFullName(repoFullName) {
|
|
57
|
+
if (repoFullName === undefined || repoFullName === null) return undefined;
|
|
58
|
+
return normalizeRepoFullName(repoFullName);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function requiredNonEmptyString(value, error) {
|
|
62
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(error);
|
|
63
|
+
return value.trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function optionalString(value) {
|
|
67
|
+
if (value === undefined || value === null) return null;
|
|
68
|
+
if (typeof value !== "string") throw new Error("invalid_head_sha");
|
|
69
|
+
const trimmed = value.trim();
|
|
70
|
+
return trimmed || null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Codes are stored as a JSON array of the non-empty trimmed strings, in order — a stable, small projection of a
|
|
74
|
+
// verdict's blockers/warnings that drops all free-text detail.
|
|
75
|
+
function normalizeCodes(codes, error) {
|
|
76
|
+
if (codes === undefined || codes === null) return [];
|
|
77
|
+
if (!Array.isArray(codes)) throw new Error(error);
|
|
78
|
+
return codes.map((code) => {
|
|
79
|
+
if (typeof code !== "string" || !code.trim()) throw new Error(error);
|
|
80
|
+
return code.trim();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeReadinessScore(value) {
|
|
85
|
+
if (value === undefined || value === null) return null;
|
|
86
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error("invalid_readiness_score");
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Validate + normalize an append input, throwing on any invalid field (mirrors normalizeGovernorLedgerEvent). */
|
|
91
|
+
function normalizePredictionInput(input) {
|
|
92
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("invalid_prediction_input");
|
|
93
|
+
if (!Number.isInteger(input.targetId) || input.targetId <= 0) throw new Error("invalid_target_id");
|
|
94
|
+
return {
|
|
95
|
+
repoFullName: normalizeRepoFullName(input.repoFullName),
|
|
96
|
+
targetId: input.targetId,
|
|
97
|
+
headSha: optionalString(input.headSha),
|
|
98
|
+
conclusion: requiredNonEmptyString(input.conclusion, "invalid_conclusion"),
|
|
99
|
+
pack: requiredNonEmptyString(input.pack, "invalid_pack"),
|
|
100
|
+
readinessScore: normalizeReadinessScore(input.readinessScore),
|
|
101
|
+
blockerCodes: normalizeCodes(input.blockerCodes, "invalid_blocker_codes"),
|
|
102
|
+
warningCodes: normalizeCodes(input.warningCodes, "invalid_warning_codes"),
|
|
103
|
+
engineVersion: requiredNonEmptyString(input.engineVersion, "invalid_engine_version"),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function rowToEntry(row) {
|
|
108
|
+
let blockerCodes;
|
|
109
|
+
let warningCodes;
|
|
110
|
+
try {
|
|
111
|
+
blockerCodes = JSON.parse(row.blocker_codes_json);
|
|
112
|
+
warningCodes = JSON.parse(row.warning_codes_json);
|
|
113
|
+
if (!Array.isArray(blockerCodes) || !Array.isArray(warningCodes)) throw new Error("corrupted_prediction_row");
|
|
114
|
+
} catch {
|
|
115
|
+
throw new Error("corrupted_prediction_row");
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
id: row.id,
|
|
119
|
+
ts: row.ts,
|
|
120
|
+
repoFullName: row.repo_full_name,
|
|
121
|
+
targetId: row.target_id,
|
|
122
|
+
headSha: row.head_sha,
|
|
123
|
+
conclusion: row.conclusion,
|
|
124
|
+
pack: row.pack,
|
|
125
|
+
readinessScore: row.readiness_score,
|
|
126
|
+
blockerCodes,
|
|
127
|
+
warningCodes,
|
|
128
|
+
engineVersion: row.engine_version,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Opens the append-only prediction ledger, creating the table on first use. Rows are returned in ascending `id`
|
|
134
|
+
* order (insertion order). (#4263)
|
|
135
|
+
*/
|
|
136
|
+
export function initPredictionLedger(dbPath = resolvePredictionLedgerDbPath()) {
|
|
137
|
+
const resolvedPath = normalizeDbPath(dbPath);
|
|
138
|
+
mkdirSync(dirname(resolvedPath), { recursive: true, mode: 0o700 });
|
|
139
|
+
const db = new DatabaseSync(resolvedPath);
|
|
140
|
+
chmodSync(resolvedPath, 0o600);
|
|
141
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
142
|
+
db.exec(`
|
|
143
|
+
CREATE TABLE IF NOT EXISTS predictions (
|
|
144
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
145
|
+
ts TEXT NOT NULL,
|
|
146
|
+
repo_full_name TEXT NOT NULL,
|
|
147
|
+
target_id INTEGER NOT NULL,
|
|
148
|
+
head_sha TEXT,
|
|
149
|
+
conclusion TEXT NOT NULL,
|
|
150
|
+
pack TEXT NOT NULL,
|
|
151
|
+
readiness_score REAL,
|
|
152
|
+
blocker_codes_json TEXT NOT NULL,
|
|
153
|
+
warning_codes_json TEXT NOT NULL,
|
|
154
|
+
engine_version TEXT NOT NULL
|
|
155
|
+
)
|
|
156
|
+
`);
|
|
157
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_predictions_repo ON predictions (repo_full_name, id)");
|
|
158
|
+
// Opt-in retention (#4834): prune aged/excess rows when an operator has enabled it; a no-op by default.
|
|
159
|
+
pruneLedgerByRetention(db, PREDICTION_LEDGER_RETENTION_SPEC, resolveLedgerRetentionPolicy(), Date.now());
|
|
160
|
+
|
|
161
|
+
const appendStatement = db.prepare(`
|
|
162
|
+
INSERT INTO predictions
|
|
163
|
+
(ts, repo_full_name, target_id, head_sha, conclusion, pack, readiness_score, blocker_codes_json, warning_codes_json, engine_version)
|
|
164
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
165
|
+
`);
|
|
166
|
+
const getByIdStatement = db.prepare("SELECT * FROM predictions WHERE id = ?");
|
|
167
|
+
const readAllStatement = db.prepare("SELECT * FROM predictions ORDER BY id ASC");
|
|
168
|
+
const readByRepoStatement = db.prepare("SELECT * FROM predictions WHERE repo_full_name = ? ORDER BY id ASC");
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
dbPath: resolvedPath,
|
|
172
|
+
appendPrediction(input) {
|
|
173
|
+
const n = normalizePredictionInput(input);
|
|
174
|
+
const ts = new Date().toISOString();
|
|
175
|
+
const result = appendStatement.run(
|
|
176
|
+
ts,
|
|
177
|
+
n.repoFullName,
|
|
178
|
+
n.targetId,
|
|
179
|
+
n.headSha,
|
|
180
|
+
n.conclusion,
|
|
181
|
+
n.pack,
|
|
182
|
+
n.readinessScore,
|
|
183
|
+
JSON.stringify(n.blockerCodes),
|
|
184
|
+
JSON.stringify(n.warningCodes),
|
|
185
|
+
n.engineVersion,
|
|
186
|
+
);
|
|
187
|
+
return rowToEntry(getByIdStatement.get(Number(result.lastInsertRowid)));
|
|
188
|
+
},
|
|
189
|
+
readPredictions(filter = {}) {
|
|
190
|
+
const repoFullName = normalizeOptionalRepoFullName(filter.repoFullName);
|
|
191
|
+
const rows = repoFullName === undefined ? readAllStatement.all() : readByRepoStatement.all(repoFullName);
|
|
192
|
+
return rows.map(rowToEntry);
|
|
193
|
+
},
|
|
194
|
+
// Explicit, operator-invoked right-to-be-forgotten purge (#5564) — never runs automatically. See the
|
|
195
|
+
// IMMUTABILITY INVARIANT note above: this is a deliberate, separate exception, not a normal ledger write.
|
|
196
|
+
purgeByRepo(repoFullName) {
|
|
197
|
+
return purgeStoreByRepo(db, PREDICTION_LEDGER_PURGE_SPEC, normalizeRepoFullName(repoFullName));
|
|
198
|
+
},
|
|
199
|
+
close() {
|
|
200
|
+
db.close();
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function getDefaultPredictionLedger() {
|
|
206
|
+
defaultPredictionLedger ??= initPredictionLedger();
|
|
207
|
+
return defaultPredictionLedger;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function appendPrediction(input) {
|
|
211
|
+
return getDefaultPredictionLedger().appendPrediction(input);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function readPredictions(filter) {
|
|
215
|
+
return getDefaultPredictionLedger().readPredictions(filter);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function closeDefaultPredictionLedger() {
|
|
219
|
+
if (!defaultPredictionLedger) return;
|
|
220
|
+
defaultPredictionLedger.close();
|
|
221
|
+
defaultPredictionLedger = null;
|
|
222
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { DenyRule } from "./deny-hooks.js";
|
|
2
|
+
import type { AppendGovernorEventInput, GovernorLedgerEntry } from "./governor-ledger.js";
|
|
3
|
+
|
|
4
|
+
export type BuildHouseRulesPreToolUseHookConfig = {
|
|
5
|
+
rules?: readonly DenyRule[];
|
|
6
|
+
repoFullName?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type BuildHouseRulesPreToolUseHookOptions = {
|
|
10
|
+
append?: (event: AppendGovernorEventInput) => GovernorLedgerEntry;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Minimal shape this module reads from the real Agent SDK `PreToolUseHookInput`. */
|
|
14
|
+
export type PreToolUseHookLikeInput = {
|
|
15
|
+
tool_name?: string;
|
|
16
|
+
tool_input?: Record<string, unknown>;
|
|
17
|
+
hook_event_name?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type PreToolUseHookJSONOutput = {
|
|
21
|
+
hookSpecificOutput?: {
|
|
22
|
+
hookEventName: "PreToolUse";
|
|
23
|
+
permissionDecision: "deny";
|
|
24
|
+
permissionDecisionReason: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function buildHouseRulesPreToolUseHook(
|
|
29
|
+
config?: BuildHouseRulesPreToolUseHookConfig,
|
|
30
|
+
options?: BuildHouseRulesPreToolUseHookOptions,
|
|
31
|
+
): (input: PreToolUseHookLikeInput, toolUseId?: string, context?: unknown) => Promise<PreToolUseHookJSONOutput | Record<string, never>>;
|