@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,102 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
// Per-repo base-clone cache (#5132, Wave 3.5 follow-up). packages/gittensory-engine/src/miner/
|
|
8
|
+
// worktree-allocator.ts's real `addWorktree` primitive (git worktree add -b <branch> <path> <baseBranch>)
|
|
9
|
+
// requires an EXISTING git clone to branch off -- it has never been wired into this package because that
|
|
10
|
+
// clone-management step didn't exist yet. This module is that step: clone a target repo once, then keep it
|
|
11
|
+
// current (fetch + hard-reset to the base branch) on every subsequent attempt, so `addWorktree` always
|
|
12
|
+
// branches off real, fresh content. Relies entirely on whatever git/gh credentials are already configured
|
|
13
|
+
// on this machine -- same assumption execute-local-write.js's `gh pr create` already makes; this module
|
|
14
|
+
// never embeds a token in a clone URL.
|
|
15
|
+
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
17
|
+
const DEFAULT_CLONE_DIR_NAME = "repos";
|
|
18
|
+
const DEFAULT_BASE_BRANCH = "main";
|
|
19
|
+
|
|
20
|
+
export function resolveRepoCloneBaseDir(env = process.env) {
|
|
21
|
+
const explicitPath = typeof env.GITTENSORY_MINER_REPO_CLONE_DIR === "string" ? env.GITTENSORY_MINER_REPO_CLONE_DIR.trim() : "";
|
|
22
|
+
if (explicitPath) return explicitPath;
|
|
23
|
+
|
|
24
|
+
const explicitConfigDir = typeof env.GITTENSORY_MINER_CONFIG_DIR === "string" ? env.GITTENSORY_MINER_CONFIG_DIR.trim() : "";
|
|
25
|
+
if (explicitConfigDir) return join(explicitConfigDir, DEFAULT_CLONE_DIR_NAME);
|
|
26
|
+
|
|
27
|
+
const configHome = typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim() ? env.XDG_CONFIG_HOME.trim() : join(homedir(), ".config");
|
|
28
|
+
return join(configHome, "gittensory-miner", DEFAULT_CLONE_DIR_NAME);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// GitHub owner/repo names are restricted to alphanumerics, hyphens, underscores, and periods, and are never
|
|
32
|
+
// exactly "." or ".." -- both are rejected here so a value like "../foo" can't make resolveRepoCloneDir's
|
|
33
|
+
// join(cloneBaseDir, owner, repo) escape the intended clone directory (a real path-traversal finding).
|
|
34
|
+
const REPO_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
35
|
+
|
|
36
|
+
function isPathTraversalSegment(segment) {
|
|
37
|
+
return segment === "." || segment === "..";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeRepoFullName(repoFullName) {
|
|
41
|
+
if (typeof repoFullName !== "string") throw new Error("invalid_repo_full_name");
|
|
42
|
+
const [owner, repo, extra] = repoFullName.trim().split("/");
|
|
43
|
+
if (!owner || !repo || extra !== undefined) throw new Error("invalid_repo_full_name");
|
|
44
|
+
if (!REPO_SEGMENT_PATTERN.test(owner) || !REPO_SEGMENT_PATTERN.test(repo)) throw new Error("invalid_repo_full_name");
|
|
45
|
+
if (isPathTraversalSegment(owner) || isPathTraversalSegment(repo)) throw new Error("invalid_repo_full_name");
|
|
46
|
+
return { owner, repo, repoFullName: `${owner}/${repo}` };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolveRepoCloneDir(repoFullName, env = process.env) {
|
|
50
|
+
const target = normalizeRepoFullName(repoFullName);
|
|
51
|
+
return join(resolveRepoCloneBaseDir(env), target.owner, target.repo);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function defaultRunGit(args, cwd, timeoutMs) {
|
|
55
|
+
try {
|
|
56
|
+
const { stdout, stderr } = await execFileAsync("git", args, { cwd, timeout: timeoutMs });
|
|
57
|
+
return { ok: true, stdout, stderr };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const stderr = typeof error?.stderr === "string" ? error.stderr : "";
|
|
60
|
+
return { ok: false, stdout: "", stderr: stderr || (error instanceof Error ? error.message : String(error)) };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Ensure a real, current local clone of `repoFullName` exists at the deterministic per-repo cache path.
|
|
66
|
+
* First use: `git clone`. Subsequent use: `git fetch origin` + hard-reset the base branch to
|
|
67
|
+
* `origin/<baseBranch>`, so every attempt branches off fresh content, not a stale prior checkout.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} repoFullName
|
|
70
|
+
* @param {{
|
|
71
|
+
* baseBranch?: string, cloneBaseDir?: string, env?: Record<string, string | undefined>, timeoutMs?: number,
|
|
72
|
+
* remoteUrl?: string, runGit?: (args: string[], cwd: string, timeoutMs: number) => Promise<{ ok: boolean, stdout: string, stderr: string }>,
|
|
73
|
+
* }} [options]
|
|
74
|
+
* @returns {Promise<{ ok: boolean, repoPath: string, error?: string }>}
|
|
75
|
+
*/
|
|
76
|
+
export async function ensureRepoCloned(repoFullName, options = {}) {
|
|
77
|
+
const target = normalizeRepoFullName(repoFullName);
|
|
78
|
+
const baseBranch = typeof options.baseBranch === "string" && options.baseBranch.trim() ? options.baseBranch.trim() : DEFAULT_BASE_BRANCH;
|
|
79
|
+
const cloneBaseDir = typeof options.cloneBaseDir === "string" && options.cloneBaseDir.trim() ? options.cloneBaseDir.trim() : resolveRepoCloneBaseDir(options.env);
|
|
80
|
+
const repoPath = join(cloneBaseDir, target.owner, target.repo);
|
|
81
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 120_000;
|
|
82
|
+
const runGit = options.runGit ?? defaultRunGit;
|
|
83
|
+
|
|
84
|
+
if (!existsSync(repoPath)) {
|
|
85
|
+
mkdirSync(join(cloneBaseDir, target.owner), { recursive: true, mode: 0o700 });
|
|
86
|
+
const cloneUrl = typeof options.remoteUrl === "string" && options.remoteUrl.trim() ? options.remoteUrl.trim() : `https://github.com/${target.owner}/${target.repo}.git`;
|
|
87
|
+
const cloned = await runGit(["clone", cloneUrl, repoPath], cloneBaseDir, timeoutMs);
|
|
88
|
+
if (!cloned.ok) return { ok: false, repoPath, error: cloned.stderr || "git_clone_failed" };
|
|
89
|
+
return { ok: true, repoPath };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const fetched = await runGit(["fetch", "origin"], repoPath, timeoutMs);
|
|
93
|
+
if (!fetched.ok) return { ok: false, repoPath, error: fetched.stderr || "git_fetch_failed" };
|
|
94
|
+
|
|
95
|
+
const checkedOut = await runGit(["checkout", baseBranch], repoPath, timeoutMs);
|
|
96
|
+
if (!checkedOut.ok) return { ok: false, repoPath, error: checkedOut.stderr || "git_checkout_failed" };
|
|
97
|
+
|
|
98
|
+
const reset = await runGit(["reset", "--hard", `origin/${baseBranch}`], repoPath, timeoutMs);
|
|
99
|
+
if (!reset.ok) return { ok: false, repoPath, error: reset.stderr || "git_reset_failed" };
|
|
100
|
+
|
|
101
|
+
return { ok: true, repoPath };
|
|
102
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type ParsedStateGetArgs =
|
|
2
|
+
| {
|
|
3
|
+
repoFullName: string;
|
|
4
|
+
json: boolean;
|
|
5
|
+
apiBaseUrl: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
| { error: string };
|
|
8
|
+
|
|
9
|
+
export type ParsedStateSetArgs =
|
|
10
|
+
| {
|
|
11
|
+
repoFullName: string;
|
|
12
|
+
state: "idle" | "discovering" | "planning" | "preparing";
|
|
13
|
+
dryRun: boolean;
|
|
14
|
+
json: boolean;
|
|
15
|
+
apiBaseUrl: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
| { error: string };
|
|
18
|
+
|
|
19
|
+
export function parseStateGetArgs(args: string[]): ParsedStateGetArgs;
|
|
20
|
+
|
|
21
|
+
export function parseStateSetArgs(args: string[]): ParsedStateSetArgs;
|
|
22
|
+
|
|
23
|
+
export function runStateGet(args: string[]): number;
|
|
24
|
+
|
|
25
|
+
export function runStateSet(args: string[]): number;
|
|
26
|
+
|
|
27
|
+
export function runStateCli(subcommand: string | undefined, args: string[]): number;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { RUN_STATES, getRunState, setRunState } from "./run-state.js";
|
|
2
|
+
import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";
|
|
3
|
+
|
|
4
|
+
const STATE_GET_USAGE = "Usage: gittensory-miner state get <owner/repo> [--api-base-url <url>] [--json]";
|
|
5
|
+
const STATE_SET_USAGE =
|
|
6
|
+
"Usage: gittensory-miner state set <owner/repo> <idle|discovering|planning|preparing> [--api-base-url <url>] [--dry-run] [--json]";
|
|
7
|
+
|
|
8
|
+
const allowedRunStates = new Set(RUN_STATES);
|
|
9
|
+
|
|
10
|
+
function parseRepoArg(value, usage) {
|
|
11
|
+
if (!value) return { error: usage };
|
|
12
|
+
const trimmed = value.trim();
|
|
13
|
+
const [owner, repo, extra] = trimmed.split("/");
|
|
14
|
+
if (!owner || !repo || extra !== undefined) {
|
|
15
|
+
return { error: "Repository must be in owner/repo form." };
|
|
16
|
+
}
|
|
17
|
+
return { repoFullName: `${owner}/${repo}` };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseStateGetArgs(args) {
|
|
21
|
+
const options = { json: false, apiBaseUrl: undefined };
|
|
22
|
+
const positional = [];
|
|
23
|
+
|
|
24
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
25
|
+
const token = args[index];
|
|
26
|
+
if (token === "--json") {
|
|
27
|
+
options.json = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
// #5563: scope the lookup to a non-default forge host, so it doesn't collide with (or get confused for) a
|
|
31
|
+
// same-named repo on the default github.com host.
|
|
32
|
+
if (token === "--api-base-url") {
|
|
33
|
+
const value = args[index + 1];
|
|
34
|
+
if (!value || value.startsWith("-")) {
|
|
35
|
+
return { error: STATE_GET_USAGE };
|
|
36
|
+
}
|
|
37
|
+
options.apiBaseUrl = value;
|
|
38
|
+
index += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (token.startsWith("-")) {
|
|
42
|
+
return { error: `Unknown option: ${token}` };
|
|
43
|
+
}
|
|
44
|
+
positional.push(token);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (positional.length !== 1) {
|
|
48
|
+
return { error: STATE_GET_USAGE };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const repo = parseRepoArg(positional[0], STATE_GET_USAGE);
|
|
52
|
+
if ("error" in repo) return repo;
|
|
53
|
+
|
|
54
|
+
return { repoFullName: repo.repoFullName, ...options };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseStateSetArgs(args) {
|
|
58
|
+
const options = { json: false, dryRun: false, apiBaseUrl: undefined };
|
|
59
|
+
const positional = [];
|
|
60
|
+
|
|
61
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
62
|
+
const token = args[index];
|
|
63
|
+
if (token === "--json") {
|
|
64
|
+
options.json = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// #4847: reports what a real state set would do and returns before writing to the run-state store.
|
|
68
|
+
if (token === "--dry-run") {
|
|
69
|
+
options.dryRun = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (token === "--api-base-url") {
|
|
73
|
+
const value = args[index + 1];
|
|
74
|
+
if (!value || value.startsWith("-")) {
|
|
75
|
+
return { error: STATE_SET_USAGE };
|
|
76
|
+
}
|
|
77
|
+
options.apiBaseUrl = value;
|
|
78
|
+
index += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (token.startsWith("-")) {
|
|
82
|
+
return { error: `Unknown option: ${token}` };
|
|
83
|
+
}
|
|
84
|
+
positional.push(token);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (positional.length !== 2) {
|
|
88
|
+
return { error: STATE_SET_USAGE };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const repo = parseRepoArg(positional[0], STATE_SET_USAGE);
|
|
92
|
+
if ("error" in repo) return repo;
|
|
93
|
+
|
|
94
|
+
const state = positional[1];
|
|
95
|
+
if (!allowedRunStates.has(state)) {
|
|
96
|
+
return { error: `Invalid state: ${state}. Expected one of ${RUN_STATES.join(", ")}.` };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { repoFullName: repo.repoFullName, state, ...options };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function runStateGet(args) {
|
|
103
|
+
const parsed = parseStateGetArgs(args);
|
|
104
|
+
if ("error" in parsed) {
|
|
105
|
+
return reportCliFailure(argsWantJson(args), parsed.error);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const state = getRunState(parsed.repoFullName, parsed.apiBaseUrl);
|
|
110
|
+
if (parsed.json) {
|
|
111
|
+
console.log(JSON.stringify({ repoFullName: parsed.repoFullName, state }));
|
|
112
|
+
} else {
|
|
113
|
+
console.log(state ?? "none");
|
|
114
|
+
}
|
|
115
|
+
return 0;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return reportCliFailure(parsed.json, describeCliError(error));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function runStateSet(args) {
|
|
122
|
+
const parsed = parseStateSetArgs(args);
|
|
123
|
+
if ("error" in parsed) {
|
|
124
|
+
return reportCliFailure(argsWantJson(args), parsed.error);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (parsed.dryRun) {
|
|
128
|
+
const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, state: parsed.state };
|
|
129
|
+
if (parsed.json) {
|
|
130
|
+
console.log(JSON.stringify(dryRunResult));
|
|
131
|
+
} else {
|
|
132
|
+
console.log(`DRY RUN: would set ${parsed.repoFullName}'s run state to "${parsed.state}". No run-state write was made.`);
|
|
133
|
+
}
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const write = setRunState(parsed.repoFullName, parsed.state, parsed.apiBaseUrl);
|
|
139
|
+
if (parsed.json) {
|
|
140
|
+
console.log(JSON.stringify(write));
|
|
141
|
+
} else {
|
|
142
|
+
console.log(write.state);
|
|
143
|
+
}
|
|
144
|
+
return 0;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return reportCliFailure(parsed.json, describeCliError(error));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function runStateCli(subcommand, args) {
|
|
151
|
+
if (subcommand === "get") return runStateGet(args);
|
|
152
|
+
if (subcommand === "set") return runStateSet(args);
|
|
153
|
+
return reportCliFailure(argsWantJson(args), `Unknown state subcommand: ${subcommand ?? ""}. ${STATE_GET_USAGE}`);
|
|
154
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type RunState = "idle" | "discovering" | "planning" | "preparing";
|
|
2
|
+
|
|
3
|
+
export type RunStateWrite = {
|
|
4
|
+
apiBaseUrl: string;
|
|
5
|
+
repoFullName: string;
|
|
6
|
+
state: RunState;
|
|
7
|
+
updatedAt: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type RunStateRow = {
|
|
11
|
+
apiBaseUrl: string;
|
|
12
|
+
repoFullName: string;
|
|
13
|
+
state: RunState;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type RunStateStore = {
|
|
18
|
+
dbPath: string;
|
|
19
|
+
getRunState(repoFullName: string, apiBaseUrl?: string): RunState | null;
|
|
20
|
+
setRunState(repoFullName: string, state: RunState, apiBaseUrl?: string): RunStateWrite;
|
|
21
|
+
listRunStates(): RunStateRow[];
|
|
22
|
+
close(): void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const RUN_STATES: readonly RunState[];
|
|
26
|
+
|
|
27
|
+
export function resolveRunStateDbPath(env?: Record<string, string | undefined>): string;
|
|
28
|
+
|
|
29
|
+
export function initRunStateStore(dbPath?: string): RunStateStore;
|
|
30
|
+
|
|
31
|
+
export function getRunState(repoFullName: string, apiBaseUrl?: string): RunState | null;
|
|
32
|
+
|
|
33
|
+
export function setRunState(repoFullName: string, state: RunState, apiBaseUrl?: string): RunStateWrite;
|
|
34
|
+
|
|
35
|
+
export function listRunStates(): RunStateRow[];
|
|
36
|
+
|
|
37
|
+
export function closeDefaultRunStateStore(): void;
|
package/lib/run-state.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { DEFAULT_FORGE_CONFIG } from "./forge-config.js";
|
|
2
|
+
import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js";
|
|
3
|
+
import { applySchemaMigrations } from "./schema-version.js";
|
|
4
|
+
|
|
5
|
+
export const RUN_STATES = Object.freeze(["idle", "discovering", "planning", "preparing"]);
|
|
6
|
+
|
|
7
|
+
const runStateSet = new Set(RUN_STATES);
|
|
8
|
+
const defaultDbFileName = "run-state.sqlite3";
|
|
9
|
+
let defaultRunStateStore = null;
|
|
10
|
+
|
|
11
|
+
export function resolveRunStateDbPath(env = process.env) {
|
|
12
|
+
return resolveLocalStoreDbPath(defaultDbFileName, "GITTENSORY_MINER_RUN_STATE_DB", env);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeDbPath(dbPath) {
|
|
16
|
+
return normalizeLocalStoreDbPath(dbPath, resolveRunStateDbPath(), "invalid_run_state_db_path");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeRepoFullName(repoFullName) {
|
|
20
|
+
if (typeof repoFullName !== "string") throw new Error("invalid_repo_full_name");
|
|
21
|
+
const trimmed = repoFullName.trim();
|
|
22
|
+
const [owner, repo, extra] = trimmed.split("/");
|
|
23
|
+
if (!owner || !repo || extra !== undefined) throw new Error("invalid_repo_full_name");
|
|
24
|
+
return `${owner}/${repo}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeRunState(state) {
|
|
28
|
+
if (runStateSet.has(state)) return state;
|
|
29
|
+
throw new Error("invalid_run_state");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Optional forge host, scoping rows so two hosts serving the same owner/repo name never collide (#5563).
|
|
33
|
+
* Omitted/nullish → the github.com default, so every pre-existing single-forge caller is unaffected. */
|
|
34
|
+
function normalizeApiBaseUrl(apiBaseUrl) {
|
|
35
|
+
if (apiBaseUrl === undefined || apiBaseUrl === null) return DEFAULT_FORGE_CONFIG.apiBaseUrl;
|
|
36
|
+
if (typeof apiBaseUrl !== "string" || !apiBaseUrl.trim()) throw new Error("invalid_api_base_url");
|
|
37
|
+
return apiBaseUrl.trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// v1 -> v2 (#5563): rebuild the bare `repo_full_name` PRIMARY KEY into a (api_base_url, repo_full_name) composite
|
|
41
|
+
// -- two forge hosts serving a same-named owner/repo must not share one "current state" row. SQLite cannot ALTER
|
|
42
|
+
// a PRIMARY KEY in place, so this rebuilds the table: create the new shape, copy every existing row with the
|
|
43
|
+
// pre-#4784 implicit single-forge default backfilled, drop the old table, rename the new one in.
|
|
44
|
+
function addApiBaseUrlScope(db) {
|
|
45
|
+
db.exec(`
|
|
46
|
+
CREATE TABLE miner_run_state_v2 (
|
|
47
|
+
api_base_url TEXT NOT NULL,
|
|
48
|
+
repo_full_name TEXT NOT NULL,
|
|
49
|
+
state TEXT NOT NULL CHECK (state IN ('idle', 'discovering', 'planning', 'preparing')),
|
|
50
|
+
updated_at TEXT NOT NULL,
|
|
51
|
+
PRIMARY KEY (api_base_url, repo_full_name)
|
|
52
|
+
)
|
|
53
|
+
`);
|
|
54
|
+
// OR IGNORE: a row this store's own read path already treats as unusable garbage (an unrecognized `state`,
|
|
55
|
+
// e.g. from a hand-edited or otherwise corrupted file -- getRunState/listRunStates fail closed on it too)
|
|
56
|
+
// would violate the CHECK constraint above and abort the whole migration. Skipping it here is consistent with
|
|
57
|
+
// that same fail-closed posture, rather than turning one bad row into a permanently unmigratable file.
|
|
58
|
+
db.prepare(
|
|
59
|
+
`INSERT OR IGNORE INTO miner_run_state_v2 (api_base_url, repo_full_name, state, updated_at)
|
|
60
|
+
SELECT ?, repo_full_name, state, updated_at FROM miner_run_state`,
|
|
61
|
+
).run(DEFAULT_FORGE_CONFIG.apiBaseUrl);
|
|
62
|
+
db.exec("DROP TABLE miner_run_state");
|
|
63
|
+
db.exec("ALTER TABLE miner_run_state_v2 RENAME TO miner_run_state");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Opens the 100% local/client-side miner run-state store. The database only lives on this machine;
|
|
68
|
+
* this module never uploads, syncs, or phones home with its contents. (#2289, #5563)
|
|
69
|
+
*/
|
|
70
|
+
export function initRunStateStore(dbPath = resolveRunStateDbPath()) {
|
|
71
|
+
const resolvedPath = normalizeDbPath(dbPath);
|
|
72
|
+
const db = openLocalStoreDb(resolvedPath);
|
|
73
|
+
db.exec(`
|
|
74
|
+
CREATE TABLE IF NOT EXISTS miner_run_state (
|
|
75
|
+
repo_full_name TEXT PRIMARY KEY,
|
|
76
|
+
state TEXT NOT NULL CHECK (state IN ('idle', 'discovering', 'planning', 'preparing')),
|
|
77
|
+
updated_at TEXT NOT NULL
|
|
78
|
+
)
|
|
79
|
+
`);
|
|
80
|
+
// Schema-version convention (#4832): stamp the baseline and run any post-baseline migrations.
|
|
81
|
+
applySchemaMigrations(db, [addApiBaseUrlScope]);
|
|
82
|
+
|
|
83
|
+
const getStatement = db.prepare(
|
|
84
|
+
"SELECT state FROM miner_run_state WHERE api_base_url = ? AND repo_full_name = ?",
|
|
85
|
+
);
|
|
86
|
+
const setStatement = db.prepare(`
|
|
87
|
+
INSERT INTO miner_run_state (api_base_url, repo_full_name, state, updated_at)
|
|
88
|
+
VALUES (?, ?, ?, ?)
|
|
89
|
+
ON CONFLICT(api_base_url, repo_full_name) DO UPDATE SET
|
|
90
|
+
state = excluded.state,
|
|
91
|
+
updated_at = excluded.updated_at
|
|
92
|
+
`);
|
|
93
|
+
const listStatement = db.prepare(
|
|
94
|
+
"SELECT api_base_url, repo_full_name, state, updated_at FROM miner_run_state ORDER BY repo_full_name",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
dbPath: resolvedPath,
|
|
99
|
+
getRunState(repoFullName, apiBaseUrl) {
|
|
100
|
+
const row = getStatement.get(normalizeApiBaseUrl(apiBaseUrl), normalizeRepoFullName(repoFullName));
|
|
101
|
+
return runStateSet.has(row?.state) ? row.state : null;
|
|
102
|
+
},
|
|
103
|
+
setRunState(repoFullName, state, apiBaseUrl) {
|
|
104
|
+
const normalizedForge = normalizeApiBaseUrl(apiBaseUrl);
|
|
105
|
+
const normalizedRepo = normalizeRepoFullName(repoFullName);
|
|
106
|
+
const normalizedState = normalizeRunState(state);
|
|
107
|
+
const updatedAt = new Date().toISOString();
|
|
108
|
+
setStatement.run(normalizedForge, normalizedRepo, normalizedState, updatedAt);
|
|
109
|
+
return { apiBaseUrl: normalizedForge, repoFullName: normalizedRepo, state: normalizedState, updatedAt };
|
|
110
|
+
},
|
|
111
|
+
/** Every repo with a recorded run state, across the whole store — the per-repo discover/plan/prepare
|
|
112
|
+
* signal a "run portfolio" view folds alongside managed PR rows (#4279). */
|
|
113
|
+
listRunStates() {
|
|
114
|
+
return listStatement.all()
|
|
115
|
+
.filter((row) => runStateSet.has(row.state))
|
|
116
|
+
.map((row) => ({
|
|
117
|
+
apiBaseUrl: row.api_base_url,
|
|
118
|
+
repoFullName: row.repo_full_name,
|
|
119
|
+
state: row.state,
|
|
120
|
+
updatedAt: row.updated_at,
|
|
121
|
+
}));
|
|
122
|
+
},
|
|
123
|
+
close() {
|
|
124
|
+
db.close();
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getDefaultRunStateStore() {
|
|
130
|
+
defaultRunStateStore ??= initRunStateStore();
|
|
131
|
+
return defaultRunStateStore;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getRunState(repoFullName, apiBaseUrl) {
|
|
135
|
+
return getDefaultRunStateStore().getRunState(repoFullName, apiBaseUrl);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function setRunState(repoFullName, state, apiBaseUrl) {
|
|
139
|
+
return getDefaultRunStateStore().setRunState(repoFullName, state, apiBaseUrl);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function listRunStates() {
|
|
143
|
+
return getDefaultRunStateStore().listRunStates();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function closeDefaultRunStateStore() {
|
|
147
|
+
if (!defaultRunStateStore) return;
|
|
148
|
+
defaultRunStateStore.close();
|
|
149
|
+
defaultRunStateStore = null;
|
|
150
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
|
|
3
|
+
/** A single post-baseline schema migration: mutate the store in place to advance it exactly one version. */
|
|
4
|
+
export type SchemaMigration = (db: DatabaseSync) => void;
|
|
5
|
+
|
|
6
|
+
/** The bootstrap schema every store creates inline is, by convention, schema version 1. */
|
|
7
|
+
export const BASELINE_SCHEMA_VERSION: number;
|
|
8
|
+
|
|
9
|
+
/** Read a store's current `PRAGMA user_version`, coercing any absent/invalid value to 0 (pre-versioning). */
|
|
10
|
+
export function readSchemaVersion(db: DatabaseSync): number;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Run pending post-baseline migrations in order and stamp the resulting `user_version`. `migrations[i]` upgrades
|
|
14
|
+
* from version i+1 to i+2, so the target version is `BASELINE_SCHEMA_VERSION + migrations.length`. Returns the
|
|
15
|
+
* resulting version.
|
|
16
|
+
*/
|
|
17
|
+
export function applySchemaMigrations(db: DatabaseSync, migrations?: SchemaMigration[]): number;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Lightweight schema-versioning convention shared across the miner's local SQLite stores (#4832).
|
|
2
|
+
//
|
|
3
|
+
// Every store bootstraps its tables with `CREATE TABLE IF NOT EXISTS ...` but, until now, carried no
|
|
4
|
+
// `user_version`/migration mechanism at all — so an older on-disk file was silently reused with a stale shape.
|
|
5
|
+
// This module adds the missing convention without the weight of the main product's `migrations/` runner: each
|
|
6
|
+
// store's bootstrap schema is treated as version 1 (BASELINE_SCHEMA_VERSION), and a store's `migrations` array
|
|
7
|
+
// describes ONLY the changes AFTER that baseline (`migrations[i]` upgrades the schema from version i+1 to i+2).
|
|
8
|
+
// `applySchemaMigrations` reads the file's current `PRAGMA user_version`, runs exactly the pending migrations in
|
|
9
|
+
// order, and stamps the new version — so opening an older-schema file runs its outstanding migrations instead of
|
|
10
|
+
// silently continuing on an incompatible shape. A pre-versioning file (user_version 0) already carries the
|
|
11
|
+
// baseline tables (the idempotent `CREATE TABLE IF NOT EXISTS` ran), so it is treated as the baseline before any
|
|
12
|
+
// post-baseline migration is applied. Pure control flow over an injected `DatabaseSync` handle: no IO of its own
|
|
13
|
+
// beyond the PRAGMA read/write and the caller-supplied migration functions, and deterministic given the same
|
|
14
|
+
// handle + migration list.
|
|
15
|
+
|
|
16
|
+
/** The bootstrap schema every store creates inline is, by convention, schema version 1. */
|
|
17
|
+
export const BASELINE_SCHEMA_VERSION = 1;
|
|
18
|
+
|
|
19
|
+
/** Read a store's current `PRAGMA user_version`, coercing any absent/invalid value to 0 (pre-versioning). */
|
|
20
|
+
export function readSchemaVersion(db) {
|
|
21
|
+
const row = db.prepare("PRAGMA user_version").get();
|
|
22
|
+
const raw = row ? Number(row.user_version) : 0;
|
|
23
|
+
return Number.isInteger(raw) && raw >= 0 ? raw : 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Bring a store's on-disk schema up to date, then stamp its `user_version`. `migrations[i]` upgrades from
|
|
28
|
+
* version i+1 to i+2, so the target version is `BASELINE_SCHEMA_VERSION + migrations.length`. Every migration
|
|
29
|
+
* whose resulting version is above the file's current version runs, in order; a file already at (or past) the
|
|
30
|
+
* target runs none. Returns the resulting version. Never runs a migration twice (the stamped `user_version`
|
|
31
|
+
* gates re-runs on the next open) and never DOWNGRADES: a file written by newer code with more migrations is
|
|
32
|
+
* left at its higher version rather than stamped back down. Each migration and its version stamp are applied in
|
|
33
|
+
* one transaction, so a failure part-way through the sequence leaves the file at the last fully-applied version
|
|
34
|
+
* and re-opening resumes at the failed migration (a throwing migration rethrows after its changes roll back).
|
|
35
|
+
*
|
|
36
|
+
* @param {import("node:sqlite").DatabaseSync} db - an open store handle whose baseline tables already exist.
|
|
37
|
+
* @param {Array<(db: import("node:sqlite").DatabaseSync) => void>} [migrations] - post-baseline migrations.
|
|
38
|
+
* @returns {number} the schema version after applying any pending migrations.
|
|
39
|
+
*/
|
|
40
|
+
export function applySchemaMigrations(db, migrations = []) {
|
|
41
|
+
const target = BASELINE_SCHEMA_VERSION + migrations.length;
|
|
42
|
+
const current = readSchemaVersion(db);
|
|
43
|
+
// A pre-versioning file (0) already holds the baseline schema, so advance from the baseline, not from 0.
|
|
44
|
+
const effective = current < BASELINE_SCHEMA_VERSION ? BASELINE_SCHEMA_VERSION : current;
|
|
45
|
+
// Stamp a pre-versioning file up to the baseline first, so a store with NO post-baseline migrations still
|
|
46
|
+
// records a version. Only ever stamp UPWARD: a file already at or past the baseline (including one written by
|
|
47
|
+
// newer code with more migrations) is never downgraded. `user_version` is an integer PRAGMA that cannot be
|
|
48
|
+
// parameterized; every stamped value here is a computed integer, never caller text, so interpolating is safe.
|
|
49
|
+
if (current < BASELINE_SCHEMA_VERSION) {
|
|
50
|
+
db.exec(`PRAGMA user_version = ${BASELINE_SCHEMA_VERSION}`);
|
|
51
|
+
}
|
|
52
|
+
for (let version = effective; version < target; version += 1) {
|
|
53
|
+
// Apply each migration AND stamp its resulting version in ONE transaction, so a failure part-way through the
|
|
54
|
+
// sequence leaves the file at the LAST fully-applied version: the next open resumes at the failed migration
|
|
55
|
+
// rather than re-running the ones that already succeeded (which, for a non-idempotent ALTER, would be a hard
|
|
56
|
+
// duplicate-column error). PRAGMA user_version is transactional in SQLite, so ROLLBACK undoes the migration's
|
|
57
|
+
// partial changes and its version stamp together.
|
|
58
|
+
db.exec("BEGIN");
|
|
59
|
+
try {
|
|
60
|
+
migrations[version - BASELINE_SCHEMA_VERSION](db);
|
|
61
|
+
db.exec(`PRAGMA user_version = ${version + 1}`);
|
|
62
|
+
db.exec("COMMIT");
|
|
63
|
+
} catch (error) {
|
|
64
|
+
db.exec("ROLLBACK");
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// The resulting on-disk version: `target` after an upgrade, or the file's own higher version when it was
|
|
69
|
+
// written by newer code (never downgraded).
|
|
70
|
+
return Math.max(current, target);
|
|
71
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SelfReviewContext } from "@loopover/engine";
|
|
2
|
+
|
|
3
|
+
// bounties/issueQuality are always omitted (see this file's own header comment for why), so the result is
|
|
4
|
+
// SelfReviewContext minus those two optional fields rather than the full type.
|
|
5
|
+
export type SelfReviewContextResult = Omit<SelfReviewContext, "bounties" | "issueQuality">;
|
|
6
|
+
|
|
7
|
+
// A narrower shape than `typeof fetch` on purpose: this module only ever calls it with a string URL and a
|
|
8
|
+
// plain GET init, and the ambient `fetch` type in this repo's TS program is Cloudflare-Workers-flavored
|
|
9
|
+
// (RequestInfo<CfProperties> | URL), which is both irrelevant here (this package runs under plain Node) and
|
|
10
|
+
// stricter than any real caller needs -- same rationale as live-issue-snapshot.js's own LiveIssueSnapshotFetch.
|
|
11
|
+
export type SelfReviewContextFetch = (
|
|
12
|
+
url: string,
|
|
13
|
+
init?: { method?: string; headers?: Record<string, string> },
|
|
14
|
+
) => Promise<{ ok: boolean; status: number; json: () => Promise<unknown>; text: () => Promise<string> }>;
|
|
15
|
+
|
|
16
|
+
export type FetchSelfReviewContextOptions = {
|
|
17
|
+
githubToken?: string;
|
|
18
|
+
contributorLogin?: string;
|
|
19
|
+
linkedIssues?: number[];
|
|
20
|
+
apiBaseUrl?: string;
|
|
21
|
+
rawContentBaseUrl?: string;
|
|
22
|
+
gittensorApiBase?: string;
|
|
23
|
+
fetchImpl?: SelfReviewContextFetch;
|
|
24
|
+
perPage?: number;
|
|
25
|
+
maxPages?: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function fetchSelfReviewContext(repoFullName: string, options?: FetchSelfReviewContextOptions): Promise<SelfReviewContextResult>;
|