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
1 change: 1 addition & 0 deletions packages/loopover-miner/lib/portfolio-queue.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type PortfolioQueueStore = {
) => Array<{ repoFullName: string; identifier: string; apiBaseUrl?: string }>,
): QueueEntry[];
getAttemptHistory(repoFullName: string, identifier: string, apiBaseUrl?: string): QueueAttemptHistory;
purgeByRepo(repoFullName: string): number;
close(): void;
};

Expand Down
5 changes: 5 additions & 0 deletions packages/loopover-miner/lib/portfolio-queue.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DEFAULT_FORGE_CONFIG } from "./forge-config.js";
import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js";
import { applySchemaMigrations } from "./schema-version.js";
import { PORTFOLIO_QUEUE_PURGE_SPEC, purgeStoreByRepo } from "./store-maintenance.js";

// The miner's local portfolio/queue store (#2292): a 100% client-side, prioritized backlog of candidate work
// items across every repo the miner has been pointed at ("what should I look at next, across everything I'm
Expand Down Expand Up @@ -385,6 +386,10 @@ export function initPortfolioQueueStore(dbPath = resolvePortfolioQueueDbPath())
reachedDone: row.status === "done",
};
},
// Explicit, operator-invoked right-to-be-forgotten purge (#5564, #6599) — never runs automatically.
purgeByRepo(repoFullName) {
return purgeStoreByRepo(db, PORTFOLIO_QUEUE_PURGE_SPEC, normalizeRepoFullName(repoFullName));
},
close() {
db.close();
},
Expand Down
4 changes: 4 additions & 0 deletions packages/loopover-miner/lib/purge-cli.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { ClaimLedger } from "./claim-ledger.js";
import type { EventLedger } from "./event-ledger.js";
import type { GovernorLedger } from "./governor-ledger.js";
import type { PredictionLedger } from "./prediction-ledger.js";
import type { PortfolioQueueStore } from "./portfolio-queue.js";
import type { RunStateStore } from "./run-state.js";

export const ATTEMPT_LOG_NOT_PURGEABLE_NOTE: string;

Expand Down Expand Up @@ -33,6 +35,8 @@ export type PurgeCliOptions = {
initEventLedger?: () => EventLedger;
initGovernorLedger?: () => GovernorLedger;
initPredictionLedger?: () => PredictionLedger;
initPortfolioQueueStore?: () => PortfolioQueueStore;
initRunStateStore?: () => RunStateStore;
resolveDbPaths?: Record<string, () => string>;
};

Expand Down
20 changes: 13 additions & 7 deletions packages/loopover-miner/lib/purge-cli.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// `loopover-miner purge` (#5564): an explicit, operator-invoked right-to-be-forgotten path across the local
// ledgers. Deletes every row for one repo from the four stores that have a real `repoColumn` (claim-ledger,
// event-ledger, governor-ledger, prediction-ledger), 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.
// `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`).
// `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.
//
// Every purge is audit-observable by design (#5564's own acceptance criteria): the real (non-dry-run) path
// always prints a per-store summary, even under --json, so a purge can never be silent. A failure in one store
Expand All @@ -15,12 +15,16 @@ import { openClaimLedger, resolveClaimLedgerDbPath } from "./claim-ledger.js";
import { initEventLedger, resolveEventLedgerDbPath } from "./event-ledger.js";
import { initGovernorLedger, resolveGovernorLedgerDbPath } from "./governor-ledger.js";
import { initPredictionLedger, resolvePredictionLedgerDbPath } from "./prediction-ledger.js";
import { initPortfolioQueueStore, resolvePortfolioQueueDbPath } from "./portfolio-queue.js";
import { initRunStateStore, resolveRunStateDbPath } from "./run-state.js";
import { resolveAttemptLogDbPath } from "./attempt-log.js";
import {
CLAIM_LEDGER_PURGE_SPEC,
EVENT_LEDGER_PURGE_SPEC,
GOVERNOR_LEDGER_PURGE_SPEC,
PREDICTION_LEDGER_PURGE_SPEC,
PORTFOLIO_QUEUE_PURGE_SPEC,
RUN_STATE_PURGE_SPEC,
countStoreByRepo,
describeError,
} from "./store-maintenance.js";
Expand All @@ -36,6 +40,8 @@ const REAL_PURGE_TARGETS = [
{ name: "event-ledger", optionKey: "initEventLedger", opener: initEventLedger, resolveDbPath: resolveEventLedgerDbPath, spec: EVENT_LEDGER_PURGE_SPEC },
{ name: "governor-ledger", optionKey: "initGovernorLedger", opener: initGovernorLedger, resolveDbPath: resolveGovernorLedgerDbPath, spec: GOVERNOR_LEDGER_PURGE_SPEC },
{ 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 },
];

function parseRepoArg(value, usage) {
Expand Down
1 change: 1 addition & 0 deletions packages/loopover-miner/lib/run-state.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type RunStateStore = {
getRunState(repoFullName: string, apiBaseUrl?: string): RunState | null;
setRunState(repoFullName: string, state: RunState, apiBaseUrl?: string): RunStateWrite;
listRunStates(): RunStateRow[];
purgeByRepo(repoFullName: string): number;
close(): void;
};

Expand Down
5 changes: 5 additions & 0 deletions packages/loopover-miner/lib/run-state.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DEFAULT_FORGE_CONFIG } from "./forge-config.js";
import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js";
import { applySchemaMigrations } from "./schema-version.js";
import { RUN_STATE_PURGE_SPEC, purgeStoreByRepo } from "./store-maintenance.js";

export const RUN_STATES = Object.freeze(["idle", "discovering", "planning", "preparing"]);

Expand Down Expand Up @@ -133,6 +134,10 @@ export function initRunStateStore(dbPath = resolveRunStateDbPath()) {
updatedAt: row.updated_at,
}));
},
// Explicit, operator-invoked right-to-be-forgotten purge (#5564, #6599) — never runs automatically.
purgeByRepo(repoFullName) {
return purgeStoreByRepo(db, RUN_STATE_PURGE_SPEC, normalizeRepoFullName(repoFullName));
},
close() {
db.close();
},
Expand Down
2 changes: 2 additions & 0 deletions packages/loopover-miner/lib/store-maintenance.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const CLAIM_LEDGER_PURGE_SPEC: LedgerPurgeSpec;
export const EVENT_LEDGER_PURGE_SPEC: LedgerPurgeSpec;
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 type StoreIntegrityResult = { name: string; ok: boolean; detail: string };
export type LedgerRetentionPolicy = { maxAgeMs?: number; maxRows?: number };
Expand Down
4 changes: 3 additions & 1 deletion packages/loopover-miner/lib/store-maintenance.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@ export const EVENT_LEDGER_RETENTION_SPEC = { table: "miner_event_ledger", timest
export const GOVERNOR_LEDGER_RETENTION_SPEC = { table: "governor_events", timestampColumn: "ts", orderColumn: "id" };
export const PREDICTION_LEDGER_RETENTION_SPEC = { table: "predictions", timestampColumn: "ts", orderColumn: "id" };

/** Fixed purge specs (#5564) for the four stores whose rows are directly scoped by a `repoColumn`. Same
/** Fixed purge specs (#5564, #6599) for the six stores whose rows are directly scoped by a `repoColumn`. Same
* internal-constant-only discipline as the retention specs above. `attempt-log.js` is deliberately absent: its
* payload is a free-form `Record<string, unknown>` with no dedicated repo column, so a precise per-repo purge
* isn't possible there without risking false matches — `purge-cli.js` reports it as not-purgeable instead. */
export const CLAIM_LEDGER_PURGE_SPEC = { table: "miner_claims", repoColumn: "repo_full_name" };
export const EVENT_LEDGER_PURGE_SPEC = { table: "miner_event_ledger", repoColumn: "repo_full_name" };
export const GOVERNOR_LEDGER_PURGE_SPEC = { table: "governor_events", repoColumn: "repo_full_name" };
export const PREDICTION_LEDGER_PURGE_SPEC = { table: "predictions", repoColumn: "repo_full_name" };
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" };

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

Expand Down
26 changes: 26 additions & 0 deletions test/unit/miner-portfolio-queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -644,4 +644,30 @@ describe("loopover-miner portfolio/queue store (#2292)", () => {
}).not.toThrow();
});
});

describe("purgeByRepo (#5564, #6599)", () => {
it("deletes every queue row for one repo and leaves other repos untouched", () => {
const store = tempStore();
store.enqueue({ repoFullName: "owner/repo-a", identifier: "1" });
store.enqueue({ repoFullName: "owner/repo-a", identifier: "2" });
store.enqueue({ repoFullName: "owner/repo-b", identifier: "3" });

expect(store.purgeByRepo("owner/repo-a")).toBe(2);
expect(store.listQueue("owner/repo-a")).toEqual([]);
expect(store.listQueue()).toHaveLength(1);
});

it("returns 0 when nothing matches the repo", () => {
const store = tempStore();
store.enqueue({ repoFullName: "owner/repo-b", identifier: "1" });
expect(store.purgeByRepo("owner/repo-a")).toBe(0);
expect(store.listQueue()).toHaveLength(1);
});

it("rejects a missing/malformed repoFullName rather than silently no-opping", () => {
const store = tempStore();
expect(() => store.purgeByRepo(undefined as never)).toThrow("invalid_repo_full_name");
expect(() => store.purgeByRepo("no-slash")).toThrow("invalid_repo_full_name");
});
});
});
Loading