Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/loopover-miner/lib/contribution-profile-cache.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export type ContributionProfileCache = {
profile: ContributionProfile,
nowMs?: number,
): { repoFullName: string; fetchedAt: string };
/** Delete the cached profile for one repo (#7091); returns rows removed (0 or 1). */
purgeByRepo(repoFullName: string): number;
close(): void;
};

Expand Down
15 changes: 15 additions & 0 deletions packages/loopover-miner/lib/contribution-profile-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import {
resolveLocalStoreDbPath,
} from "./local-store.js";
import { applySchemaMigrations } from "./schema-version.js";
import {
CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC,
purgeStoreByRepo,
} from "./store-maintenance.js";

const defaultDbFileName = "contribution-profile-cache.sqlite3";
let defaultContributionProfileCache = null;
Expand Down Expand Up @@ -111,6 +115,17 @@ export function initContributionProfileCache(
putStatement.run(repoFullName, JSON.stringify(profile), fetchedAt);
return { repoFullName, fetchedAt };
},
/**
* Delete the cached profile for one repo (#7091) — the right-to-be-forgotten path `loopover-miner purge`
* invokes. Returns the number of rows removed (0 or 1, since repo_full_name is the primary key). Reuses
* store-maintenance.js's identifier-guarded purgeStoreByRepo, exactly like the other repo-scoped stores.
*
* @param {string} repoFullName
* @returns {number} rows deleted
*/
purgeByRepo(repoFullName) {
return purgeStoreByRepo(db, CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC, normalizeRepoFullName(repoFullName));
},
close() {
db.close();
},
Expand Down
2 changes: 2 additions & 0 deletions packages/loopover-miner/lib/governor-state.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export type GovernorState = {
saveReputationHistory(repoFullName: string, history: RepoOutcomeHistory, apiBaseUrl?: string): RepoOutcomeHistory;
recordOwnSubmission(record: OwnSubmissionRecord): OwnSubmissionRecord;
listRecentOwnSubmissions(filter?: ListRecentOwnSubmissionsFilter): OwnSubmissionRecord[];
/** Delete every repo-scoped row for one repo across both governor tables (#7091); returns total rows removed. */
purgeByRepo(repoFullName: string): number;
close(): void;
};

Expand Down
22 changes: 22 additions & 0 deletions packages/loopover-miner/lib/governor-state.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { DEFAULT_FORGE_CONFIG } from "./forge-config.js";
import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js";
import {
GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC,
GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC,
purgeStoreByRepo,
} from "./store-maintenance.js";

// Governor cross-attempt state persistence (#5134, Wave 3.5). Every governor-*.js wrapper
// (governor-chokepoint.js) is a pure in/out transform: it computes and RETURNS
Expand Down Expand Up @@ -304,6 +309,23 @@ export function openGovernorState(dbPath = resolveGovernorStateDbPath()) {
: listSubmissionsByRepoStatement.all(normalizeRepoFullName(filter.repoFullName), limit);
return rows.map(rowToSubmission);
},
/**
* Delete every repo-scoped row for one repo across BOTH governor tables against this single open handle
* (#7091) — the right-to-be-forgotten path `loopover-miner purge` invokes. `governor_reputation_history` is
* purged on `repo_full_name` alone (its key is composite with `api_base_url`), so nothing survives on any
* forge host. `governor_scalar_state` is deliberately untouched — it has no repo dimension. Returns the
* total rows removed across both tables.
*
* @param {string} repoFullName
* @returns {number} rows deleted across both repo-scoped tables
*/
purgeByRepo(repoFullName) {
const normalized = normalizeRepoFullName(repoFullName);
return (
purgeStoreByRepo(db, GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC, normalized) +
purgeStoreByRepo(db, GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC, normalized)
);
},
close() {
db.close();
},
Expand Down
23 changes: 19 additions & 4 deletions packages/loopover-miner/lib/purge-cli.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// `loopover-miner purge` (#5564, #6599): an explicit, operator-invoked right-to-be-forgotten path across the local
// ledgers. Deletes every row for one repo from the six stores that have a real `repoColumn` (claim-ledger,
// event-ledger, governor-ledger, prediction-ledger, portfolio-queue, run-state), via each store's own
// `purgeByRepo` method (which reuses `store-maintenance.js`'s shared, identifier-guarded `purgeStoreByRepo`).
// ledgers. Deletes every row for one repo from the stores that have a real `repoColumn` (claim-ledger,
// event-ledger, governor-ledger, prediction-ledger, portfolio-queue, run-state, contribution-profile-cache, and
// governor-state's two repo-scoped tables — #7091), via each store's own `purgeByRepo` method (which reuses
// `store-maintenance.js`'s shared, identifier-guarded `purgeStoreByRepo`).
// `attempt-log.js` is deliberately reported as not-purgeable rather than silently skipped or approximated: its
// payload is a free-form `Record<string, unknown>` with no dedicated repo column, so a precise per-repo match
// isn't possible there without risking false matches -- see store-maintenance.js's own purge-spec doc comment.
Expand All @@ -17,6 +18,8 @@ import { initGovernorLedger, resolveGovernorLedgerDbPath } from "./governor-ledg
import { initPredictionLedger, resolvePredictionLedgerDbPath } from "./prediction-ledger.js";
import { initPortfolioQueueStore, resolvePortfolioQueueDbPath } from "./portfolio-queue.js";
import { initRunStateStore, resolveRunStateDbPath } from "./run-state.js";
import { initContributionProfileCache, resolveContributionProfileCacheDbPath } from "./contribution-profile-cache.js";
import { openGovernorState, resolveGovernorStateDbPath } from "./governor-state.js";
import { resolveAttemptLogDbPath } from "./attempt-log.js";
import {
CLAIM_LEDGER_PURGE_SPEC,
Expand All @@ -25,6 +28,9 @@ import {
PREDICTION_LEDGER_PURGE_SPEC,
PORTFOLIO_QUEUE_PURGE_SPEC,
RUN_STATE_PURGE_SPEC,
CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC,
GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC,
GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC,
countStoreByRepo,
describeError,
} from "./store-maintenance.js";
Expand All @@ -42,6 +48,10 @@ const REAL_PURGE_TARGETS = [
{ name: "prediction-ledger", optionKey: "initPredictionLedger", opener: initPredictionLedger, resolveDbPath: resolvePredictionLedgerDbPath, spec: PREDICTION_LEDGER_PURGE_SPEC },
{ name: "portfolio-queue", optionKey: "initPortfolioQueueStore", opener: initPortfolioQueueStore, resolveDbPath: resolvePortfolioQueueDbPath, spec: PORTFOLIO_QUEUE_PURGE_SPEC },
{ name: "run-state", optionKey: "initRunStateStore", opener: initRunStateStore, resolveDbPath: resolveRunStateDbPath, spec: RUN_STATE_PURGE_SPEC },
{ name: "contribution-profile-cache", optionKey: "initContributionProfileCache", opener: initContributionProfileCache, resolveDbPath: resolveContributionProfileCacheDbPath, spec: CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC },
// governor-state holds TWO repo-scoped tables in one DB file; its store.purgeByRepo deletes both against a
// single handle (never reopening the file), and its dry-run count sums both via `specs` (#7091).
{ name: "governor-state", optionKey: "openGovernorState", opener: openGovernorState, resolveDbPath: resolveGovernorStateDbPath, specs: [GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC, GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC] },
];

function parseRepoArg(value, usage) {
Expand Down Expand Up @@ -113,8 +123,13 @@ export function runPurgeDryRun(parsed, options = {}) {
const resolveDbPaths = options.resolveDbPaths ?? {};
const stores = REAL_PURGE_TARGETS.map((target) => {
const dbPath = (resolveDbPaths[target.name] ?? target.resolveDbPath)();
// A target scopes one table (`spec`) or -- for governor-state -- several in one file (`specs`); sum the
// per-table counts against the single read-only handle so the preview matches what a real purge removes.
const specs = target.specs ?? [target.spec];
try {
const wouldPurge = countExistingRows(dbPath, (db) => countStoreByRepo(db, target.spec, parsed.repoFullName));
const wouldPurge = countExistingRows(dbPath, (db) =>
specs.reduce((sum, spec) => sum + countStoreByRepo(db, spec, parsed.repoFullName), 0),
);
return { store: target.name, wouldPurge };
} catch (error) {
return { store: target.name, wouldPurge: null, error: describeError(error) };
Expand Down
3 changes: 3 additions & 0 deletions packages/loopover-miner/lib/store-maintenance.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export const GOVERNOR_LEDGER_PURGE_SPEC: LedgerPurgeSpec;
export const PREDICTION_LEDGER_PURGE_SPEC: LedgerPurgeSpec;
export const PORTFOLIO_QUEUE_PURGE_SPEC: LedgerPurgeSpec;
export const RUN_STATE_PURGE_SPEC: LedgerPurgeSpec;
export const CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC: LedgerPurgeSpec;
export const GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC: LedgerPurgeSpec;
export const GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC: LedgerPurgeSpec;

export type StoreIntegrityResult = { name: string; ok: boolean; detail: string };
export type LedgerRetentionPolicy = { maxAgeMs?: number; maxRows?: number };
Expand Down
13 changes: 13 additions & 0 deletions packages/loopover-miner/lib/store-maintenance.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// no internal clock read in the prune path so it stays deterministic and unit-testable.
import { existsSync } from "node:fs";
import { DatabaseSync } from "node:sqlite";
import { CONTRIBUTION_PROFILE_STORE_TABLE } from "./contribution-profile.js";

/** Env opt-ins for ledger retention (unset ⇒ retention disabled). */
export const LEDGER_RETENTION_DAYS_ENV = "LOOPOVER_MINER_LEDGER_RETENTION_DAYS";
Expand All @@ -35,6 +36,18 @@ export const PREDICTION_LEDGER_PURGE_SPEC = { table: "predictions", repoColumn:
export const PORTFOLIO_QUEUE_PURGE_SPEC = { table: "miner_portfolio_queue", repoColumn: "repo_full_name" };
export const RUN_STATE_PURGE_SPEC = { table: "miner_run_state", repoColumn: "repo_full_name" };

/** Three more repo-scoped stores the original six missed (#7091), same `repoColumn` shape and same internal-
* constant-only discipline. The contribution-profile-cache table name comes from its schema module's own
* `CONTRIBUTION_PROFILE_STORE_TABLE` constant so this spec can't drift from a second hardcoded literal.
* governor-state holds two genuinely repo-scoped tables (reputation history + own submissions);
* `governor_scalar_state` is intentionally excluded — it is a single whole-run scalar row with no repo
* dimension. `governor_reputation_history` is purged on `repo_full_name` alone (its key is composite with
* `api_base_url`), so a right-to-be-forgotten sweep clears the repo across every forge host it was recorded
* against, not just the default one. */
export const CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC = { table: CONTRIBUTION_PROFILE_STORE_TABLE, repoColumn: "repo_full_name" };
export const GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC = { table: "governor_reputation_history", repoColumn: "repo_full_name" };
export const GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC = { table: "governor_own_submissions", repoColumn: "repo_full_name" };

const SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;

/** A readable message for a caught value, whether or not it is an Error. */
Expand Down
Loading