Skip to content

Commit 11a6dac

Browse files
feat(purge): add purgeByRepo method to portfolio queue and run state stores (#6694)
- Implemented `purgeByRepo` method in both `PortfolioQueueStore` and `RunStateStore` to allow explicit deletion of all entries associated with a specific repository. - Updated related types and specifications in `store-maintenance` to accommodate the new purge functionality. - Enhanced CLI and test coverage to ensure proper functionality and error handling for the new purge feature. Closes #5564, #6599
1 parent bb028ec commit 11a6dac

11 files changed

Lines changed: 245 additions & 22 deletions

packages/loopover-miner/lib/portfolio-queue.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export type PortfolioQueueStore = {
5050
) => Array<{ repoFullName: string; identifier: string; apiBaseUrl?: string }>,
5151
): QueueEntry[];
5252
getAttemptHistory(repoFullName: string, identifier: string, apiBaseUrl?: string): QueueAttemptHistory;
53+
purgeByRepo(repoFullName: string): number;
5354
close(): void;
5455
};
5556

packages/loopover-miner/lib/portfolio-queue.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { DEFAULT_FORGE_CONFIG } from "./forge-config.js";
22
import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js";
33
import { applySchemaMigrations } from "./schema-version.js";
4+
import { PORTFOLIO_QUEUE_PURGE_SPEC, purgeStoreByRepo } from "./store-maintenance.js";
45

56
// The miner's local portfolio/queue store (#2292): a 100% client-side, prioritized backlog of candidate work
67
// items across every repo the miner has been pointed at ("what should I look at next, across everything I'm
@@ -385,6 +386,10 @@ export function initPortfolioQueueStore(dbPath = resolvePortfolioQueueDbPath())
385386
reachedDone: row.status === "done",
386387
};
387388
},
389+
// Explicit, operator-invoked right-to-be-forgotten purge (#5564, #6599) — never runs automatically.
390+
purgeByRepo(repoFullName) {
391+
return purgeStoreByRepo(db, PORTFOLIO_QUEUE_PURGE_SPEC, normalizeRepoFullName(repoFullName));
392+
},
388393
close() {
389394
db.close();
390395
},

packages/loopover-miner/lib/purge-cli.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { ClaimLedger } from "./claim-ledger.js";
22
import type { EventLedger } from "./event-ledger.js";
33
import type { GovernorLedger } from "./governor-ledger.js";
44
import type { PredictionLedger } from "./prediction-ledger.js";
5+
import type { PortfolioQueueStore } from "./portfolio-queue.js";
6+
import type { RunStateStore } from "./run-state.js";
57

68
export const ATTEMPT_LOG_NOT_PURGEABLE_NOTE: string;
79

@@ -33,6 +35,8 @@ export type PurgeCliOptions = {
3335
initEventLedger?: () => EventLedger;
3436
initGovernorLedger?: () => GovernorLedger;
3537
initPredictionLedger?: () => PredictionLedger;
38+
initPortfolioQueueStore?: () => PortfolioQueueStore;
39+
initRunStateStore?: () => RunStateStore;
3640
resolveDbPaths?: Record<string, () => string>;
3741
};
3842

packages/loopover-miner/lib/purge-cli.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
// `loopover-miner purge` (#5564): an explicit, operator-invoked right-to-be-forgotten path across the local
2-
// ledgers. Deletes every row for one repo from the four stores that have a real `repoColumn` (claim-ledger,
3-
// event-ledger, governor-ledger, prediction-ledger), via each store's own `purgeByRepo` method (which reuses
4-
// `store-maintenance.js`'s shared, identifier-guarded `purgeStoreByRepo`). `attempt-log.js` is deliberately
5-
// reported as not-purgeable rather than silently skipped or approximated: its payload is a free-form
6-
// `Record<string, unknown>` with no dedicated repo column, so a precise per-repo match isn't possible there
7-
// without risking false matches -- see store-maintenance.js's own purge-spec doc comment.
1+
// `loopover-miner purge` (#5564, #6599): an explicit, operator-invoked right-to-be-forgotten path across the local
2+
// ledgers. Deletes every row for one repo from the six stores that have a real `repoColumn` (claim-ledger,
3+
// event-ledger, governor-ledger, prediction-ledger, portfolio-queue, run-state), via each store's own
4+
// `purgeByRepo` method (which reuses `store-maintenance.js`'s shared, identifier-guarded `purgeStoreByRepo`).
5+
// `attempt-log.js` is deliberately reported as not-purgeable rather than silently skipped or approximated: its
6+
// payload is a free-form `Record<string, unknown>` with no dedicated repo column, so a precise per-repo match
7+
// isn't possible there without risking false matches -- see store-maintenance.js's own purge-spec doc comment.
88
//
99
// Every purge is audit-observable by design (#5564's own acceptance criteria): the real (non-dry-run) path
1010
// always prints a per-store summary, even under --json, so a purge can never be silent. A failure in one store
@@ -15,12 +15,16 @@ import { openClaimLedger, resolveClaimLedgerDbPath } from "./claim-ledger.js";
1515
import { initEventLedger, resolveEventLedgerDbPath } from "./event-ledger.js";
1616
import { initGovernorLedger, resolveGovernorLedgerDbPath } from "./governor-ledger.js";
1717
import { initPredictionLedger, resolvePredictionLedgerDbPath } from "./prediction-ledger.js";
18+
import { initPortfolioQueueStore, resolvePortfolioQueueDbPath } from "./portfolio-queue.js";
19+
import { initRunStateStore, resolveRunStateDbPath } from "./run-state.js";
1820
import { resolveAttemptLogDbPath } from "./attempt-log.js";
1921
import {
2022
CLAIM_LEDGER_PURGE_SPEC,
2123
EVENT_LEDGER_PURGE_SPEC,
2224
GOVERNOR_LEDGER_PURGE_SPEC,
2325
PREDICTION_LEDGER_PURGE_SPEC,
26+
PORTFOLIO_QUEUE_PURGE_SPEC,
27+
RUN_STATE_PURGE_SPEC,
2428
countStoreByRepo,
2529
describeError,
2630
} from "./store-maintenance.js";
@@ -36,6 +40,8 @@ const REAL_PURGE_TARGETS = [
3640
{ name: "event-ledger", optionKey: "initEventLedger", opener: initEventLedger, resolveDbPath: resolveEventLedgerDbPath, spec: EVENT_LEDGER_PURGE_SPEC },
3741
{ name: "governor-ledger", optionKey: "initGovernorLedger", opener: initGovernorLedger, resolveDbPath: resolveGovernorLedgerDbPath, spec: GOVERNOR_LEDGER_PURGE_SPEC },
3842
{ name: "prediction-ledger", optionKey: "initPredictionLedger", opener: initPredictionLedger, resolveDbPath: resolvePredictionLedgerDbPath, spec: PREDICTION_LEDGER_PURGE_SPEC },
43+
{ name: "portfolio-queue", optionKey: "initPortfolioQueueStore", opener: initPortfolioQueueStore, resolveDbPath: resolvePortfolioQueueDbPath, spec: PORTFOLIO_QUEUE_PURGE_SPEC },
44+
{ name: "run-state", optionKey: "initRunStateStore", opener: initRunStateStore, resolveDbPath: resolveRunStateDbPath, spec: RUN_STATE_PURGE_SPEC },
3945
];
4046

4147
function parseRepoArg(value, usage) {

packages/loopover-miner/lib/run-state.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type RunStateStore = {
1919
getRunState(repoFullName: string, apiBaseUrl?: string): RunState | null;
2020
setRunState(repoFullName: string, state: RunState, apiBaseUrl?: string): RunStateWrite;
2121
listRunStates(): RunStateRow[];
22+
purgeByRepo(repoFullName: string): number;
2223
close(): void;
2324
};
2425

packages/loopover-miner/lib/run-state.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { DEFAULT_FORGE_CONFIG } from "./forge-config.js";
22
import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js";
33
import { applySchemaMigrations } from "./schema-version.js";
4+
import { RUN_STATE_PURGE_SPEC, purgeStoreByRepo } from "./store-maintenance.js";
45

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

@@ -133,6 +134,10 @@ export function initRunStateStore(dbPath = resolveRunStateDbPath()) {
133134
updatedAt: row.updated_at,
134135
}));
135136
},
137+
// Explicit, operator-invoked right-to-be-forgotten purge (#5564, #6599) — never runs automatically.
138+
purgeByRepo(repoFullName) {
139+
return purgeStoreByRepo(db, RUN_STATE_PURGE_SPEC, normalizeRepoFullName(repoFullName));
140+
},
136141
close() {
137142
db.close();
138143
},

packages/loopover-miner/lib/store-maintenance.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const CLAIM_LEDGER_PURGE_SPEC: LedgerPurgeSpec;
1313
export const EVENT_LEDGER_PURGE_SPEC: LedgerPurgeSpec;
1414
export const GOVERNOR_LEDGER_PURGE_SPEC: LedgerPurgeSpec;
1515
export const PREDICTION_LEDGER_PURGE_SPEC: LedgerPurgeSpec;
16+
export const PORTFOLIO_QUEUE_PURGE_SPEC: LedgerPurgeSpec;
17+
export const RUN_STATE_PURGE_SPEC: LedgerPurgeSpec;
1618

1719
export type StoreIntegrityResult = { name: string; ok: boolean; detail: string };
1820
export type LedgerRetentionPolicy = { maxAgeMs?: number; maxRows?: number };

packages/loopover-miner/lib/store-maintenance.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,16 @@ export const EVENT_LEDGER_RETENTION_SPEC = { table: "miner_event_ledger", timest
2424
export const GOVERNOR_LEDGER_RETENTION_SPEC = { table: "governor_events", timestampColumn: "ts", orderColumn: "id" };
2525
export const PREDICTION_LEDGER_RETENTION_SPEC = { table: "predictions", timestampColumn: "ts", orderColumn: "id" };
2626

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

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

test/unit/miner-portfolio-queue.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,4 +644,30 @@ describe("loopover-miner portfolio/queue store (#2292)", () => {
644644
}).not.toThrow();
645645
});
646646
});
647+
648+
describe("purgeByRepo (#5564, #6599)", () => {
649+
it("deletes every queue row for one repo and leaves other repos untouched", () => {
650+
const store = tempStore();
651+
store.enqueue({ repoFullName: "owner/repo-a", identifier: "1" });
652+
store.enqueue({ repoFullName: "owner/repo-a", identifier: "2" });
653+
store.enqueue({ repoFullName: "owner/repo-b", identifier: "3" });
654+
655+
expect(store.purgeByRepo("owner/repo-a")).toBe(2);
656+
expect(store.listQueue("owner/repo-a")).toEqual([]);
657+
expect(store.listQueue()).toHaveLength(1);
658+
});
659+
660+
it("returns 0 when nothing matches the repo", () => {
661+
const store = tempStore();
662+
store.enqueue({ repoFullName: "owner/repo-b", identifier: "1" });
663+
expect(store.purgeByRepo("owner/repo-a")).toBe(0);
664+
expect(store.listQueue()).toHaveLength(1);
665+
});
666+
667+
it("rejects a missing/malformed repoFullName rather than silently no-opping", () => {
668+
const store = tempStore();
669+
expect(() => store.purgeByRepo(undefined as never)).toThrow("invalid_repo_full_name");
670+
expect(() => store.purgeByRepo("no-slash")).toThrow("invalid_repo_full_name");
671+
});
672+
});
647673
});

0 commit comments

Comments
 (0)