Skip to content

Commit daf44ee

Browse files
committed
fix(miner): wire policy_verdict_cache into the per-repo purge/status/migrate store lists (#6987)
1 parent b8621fc commit daf44ee

8 files changed

Lines changed: 40 additions & 7 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { initAttemptLog, resolveAttemptLogDbPath } from "./attempt-log.js";
2323
import { openReplaySnapshotStore, resolveReplaySnapshotDbPath } from "./replay-snapshot.js";
2424
import { openWorktreeAllocator, resolveWorktreeAllocatorDbPath } from "./worktree-allocator.js";
2525
import { initContributionProfileCache, resolveContributionProfileCacheDbPath } from "./contribution-profile-cache.js";
26+
import { initPolicyVerdictCacheStore, resolvePolicyVerdictCacheDbPath } from "./policy-verdict-cache.js";
2627

2728
const MIGRATE_USAGE = "Usage: loopover-miner migrate [--json]";
2829

@@ -39,6 +40,7 @@ const STORES = [
3940
{ name: "replay-snapshot", resolveDbPath: resolveReplaySnapshotDbPath, open: openReplaySnapshotStore },
4041
{ name: "worktree-allocator", resolveDbPath: resolveWorktreeAllocatorDbPath, open: (dbPath) => openWorktreeAllocator({ dbPath }) },
4142
{ name: "contribution-profile", resolveDbPath: resolveContributionProfileCacheDbPath, open: initContributionProfileCache },
43+
{ name: "policy-verdict-cache", resolveDbPath: resolvePolicyVerdictCacheDbPath, open: initPolicyVerdictCacheStore },
4244
];
4345

4446
/** Read a store file's stamped schema version without ever creating it -- matches checkStoreIntegrity's

packages/loopover-miner/lib/policy-verdict-cache.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js";
22
import { applySchemaMigrations } from "./schema-version.js";
3+
import { POLICY_VERDICT_CACHE_PURGE_SPEC, purgeStoreByRepo } from "./store-maintenance.js";
34

45
// Local cache of resolved AI-usage-policy verdicts (#4843). Even with #4842's conditional-GET doc cache, the small
56
// but non-zero cost of resolving `resolveAiPolicyVerdict` from raw doc text was still paid on every discover run.
@@ -99,6 +100,11 @@ export function initPolicyVerdictCacheStore(dbPath = resolvePolicyVerdictCacheDb
99100
putStatement.run(normalizedRepoScope, normalizedDecisiveDoc, normalizedEtag, serializedVerdict, updatedAt);
100101
return { repoScope: normalizedRepoScope, decisiveDoc: normalizedDecisiveDoc, etag: normalizedEtag, verdict, updatedAt };
101102
},
103+
/** #6987: delete every cached verdict for one repo scope (the operator-invoked per-repo purge path),
104+
* mirroring the six sibling repo-scoped stores. */
105+
purgeByRepo(repoScope) {
106+
return purgeStoreByRepo(db, POLICY_VERDICT_CACHE_PURGE_SPEC, normalizeRepoScope(repoScope));
107+
},
102108
close() {
103109
db.close();
104110
},

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { initGovernorLedger, resolveGovernorLedgerDbPath } from "./governor-ledg
1717
import { initPredictionLedger, resolvePredictionLedgerDbPath } from "./prediction-ledger.js";
1818
import { initPortfolioQueueStore, resolvePortfolioQueueDbPath } from "./portfolio-queue.js";
1919
import { initRunStateStore, resolveRunStateDbPath } from "./run-state.js";
20+
import { initPolicyVerdictCacheStore, resolvePolicyVerdictCacheDbPath } from "./policy-verdict-cache.js";
2021
import { resolveAttemptLogDbPath } from "./attempt-log.js";
2122
import {
2223
CLAIM_LEDGER_PURGE_SPEC,
@@ -25,6 +26,7 @@ import {
2526
PREDICTION_LEDGER_PURGE_SPEC,
2627
PORTFOLIO_QUEUE_PURGE_SPEC,
2728
RUN_STATE_PURGE_SPEC,
29+
POLICY_VERDICT_CACHE_PURGE_SPEC,
2830
countStoreByRepo,
2931
describeError,
3032
} from "./store-maintenance.js";
@@ -42,6 +44,7 @@ const REAL_PURGE_TARGETS = [
4244
{ name: "prediction-ledger", optionKey: "initPredictionLedger", opener: initPredictionLedger, resolveDbPath: resolvePredictionLedgerDbPath, spec: PREDICTION_LEDGER_PURGE_SPEC },
4345
{ name: "portfolio-queue", optionKey: "initPortfolioQueueStore", opener: initPortfolioQueueStore, resolveDbPath: resolvePortfolioQueueDbPath, spec: PORTFOLIO_QUEUE_PURGE_SPEC },
4446
{ name: "run-state", optionKey: "initRunStateStore", opener: initRunStateStore, resolveDbPath: resolveRunStateDbPath, spec: RUN_STATE_PURGE_SPEC },
47+
{ name: "policy-verdict-cache", optionKey: "initPolicyVerdictCacheStore", opener: initPolicyVerdictCacheStore, resolveDbPath: resolvePolicyVerdictCacheDbPath, spec: POLICY_VERDICT_CACHE_PURGE_SPEC },
4548
];
4649

4750
function parseRepoArg(value, usage) {

packages/loopover-miner/lib/status.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { resolveAttemptLogDbPath } from "./attempt-log.js";
2626
import { resolveReplaySnapshotDbPath } from "./replay-snapshot.js";
2727
import { resolveWorktreeAllocatorDbPath } from "./worktree-allocator.js";
2828
import { resolveContributionProfileCacheDbPath } from "./contribution-profile-cache.js";
29+
import { resolvePolicyVerdictCacheDbPath } from "./policy-verdict-cache.js";
2930

3031
// Slim laptop-mode CLI commands (#2288): `status` (what's installed + where local state lives) and `doctor` (is
3132
// this laptop set up correctly). Both are read-only and 100% local — no repo-scanning, no coding-agent invocation,
@@ -318,6 +319,7 @@ function storeIntegrityChecks(env) {
318319
["replay-snapshot", resolveReplaySnapshotDbPath(env)],
319320
["worktree-allocator", resolveWorktreeAllocatorDbPath(env)],
320321
["contribution-profile", resolveContributionProfileCacheDbPath(env)],
322+
["policy-verdict-cache", resolvePolicyVerdictCacheDbPath(env)],
321323
];
322324
return stores.map(([name, dbPath]) => checkStoreIntegrity(`store-integrity:${name}`, dbPath));
323325
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ 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, #6599) for the six stores whose rows are directly scoped by a `repoColumn`. Same
27+
/** Fixed purge specs (#5564, #6599) for the seven 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. */
@@ -34,6 +34,9 @@ export const GOVERNOR_LEDGER_PURGE_SPEC = { table: "governor_events", repoColumn
3434
export const PREDICTION_LEDGER_PURGE_SPEC = { table: "predictions", repoColumn: "repo_full_name" };
3535
export const PORTFOLIO_QUEUE_PURGE_SPEC = { table: "miner_portfolio_queue", repoColumn: "repo_full_name" };
3636
export const RUN_STATE_PURGE_SPEC = { table: "miner_run_state", repoColumn: "repo_full_name" };
37+
// #6987: policy_verdict_cache is keyed by `repo_scope` (a genuine per-repo PRIMARY KEY), so it fits the same
38+
// per-repo purge pattern as the six above and was simply never wired in.
39+
export const POLICY_VERDICT_CACHE_PURGE_SPEC = { table: "policy_verdict_cache", repoColumn: "repo_scope" };
3740

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

test/unit/miner-migrate-cli.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const STORE_NAMES = [
3030
"replay-snapshot",
3131
"worktree-allocator",
3232
"contribution-profile",
33+
"policy-verdict-cache",
3334
];
3435

3536
afterEach(() => {
@@ -38,7 +39,7 @@ afterEach(() => {
3839
});
3940

4041
describe("loopover-miner migrate (#4871)", () => {
41-
it("covers the exact same twelve stores doctor's store-integrity sweep covers, in the same order, and skips every one when nothing has been created yet", () => {
42+
it("covers the exact same thirteen stores doctor's store-integrity sweep covers, in the same order, and skips every one when nothing has been created yet", () => {
4243
const env = tempEnv();
4344
const results = runMigrateChecks(env);
4445

test/unit/miner-purge-cli.test.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
closeDefaultPortfolioQueueStore,
1212
} from "../../packages/loopover-miner/lib/portfolio-queue.js";
1313
import { initRunStateStore, closeDefaultRunStateStore } from "../../packages/loopover-miner/lib/run-state.js";
14+
import { initPolicyVerdictCacheStore } from "../../packages/loopover-miner/lib/policy-verdict-cache.js";
1415
import { initAttemptLog, closeDefaultAttemptLog } from "../../packages/loopover-miner/lib/attempt-log.js";
1516
import {
1617
ATTEMPT_LOG_NOT_PURGEABLE_NOTE,
@@ -76,7 +77,7 @@ describe("parsePurgeArgs (#5564)", () => {
7677
});
7778

7879
describe("runPurge --dry-run (#5564, #6599)", () => {
79-
it("counts matching rows across the six real stores without writing anything, and reports attempt-log as not-purgeable", async () => {
80+
it("counts matching rows across the seven real stores without writing anything, and reports attempt-log as not-purgeable", async () => {
8081
const root = tempDir();
8182
const claimDbPath = join(root, "claim-ledger.sqlite3");
8283
const eventDbPath = join(root, "event-ledger.sqlite3");
@@ -128,13 +129,20 @@ describe("runPurge --dry-run (#5564, #6599)", () => {
128129
runState.setRunState("acme/other", "idle");
129130
runState.close();
130131

132+
const policyVerdictDbPath = join(root, "policy-verdict-cache.sqlite3");
133+
const policyVerdict = initPolicyVerdictCacheStore(policyVerdictDbPath);
134+
policyVerdict.put("acme/widgets", "AI-USAGE.md", '"v1"', { allowed: true, matchedPhrase: null, source: "AI-USAGE.md" });
135+
policyVerdict.put("acme/other", "AI-USAGE.md", '"v1"', { allowed: true, matchedPhrase: null, source: "AI-USAGE.md" });
136+
policyVerdict.close();
137+
131138
const resolveDbPaths = {
132139
"claim-ledger": () => claimDbPath,
133140
"event-ledger": () => eventDbPath,
134141
"governor-ledger": () => governorDbPath,
135142
"prediction-ledger": () => predictionDbPath,
136143
"portfolio-queue": () => portfolioDbPath,
137144
"run-state": () => runStateDbPath,
145+
"policy-verdict-cache": () => policyVerdictDbPath,
138146
"attempt-log": () => attemptLogDbPath,
139147
};
140148

@@ -151,6 +159,7 @@ describe("runPurge --dry-run (#5564, #6599)", () => {
151159
{ store: "prediction-ledger", wouldPurge: 0 },
152160
{ store: "portfolio-queue", wouldPurge: 2 },
153161
{ store: "run-state", wouldPurge: 1 },
162+
{ store: "policy-verdict-cache", wouldPurge: 1 },
154163
],
155164
attemptLogNote: ATTEMPT_LOG_NOT_PURGEABLE_NOTE,
156165
attemptLogTotalRows: 0,
@@ -180,12 +189,13 @@ describe("runPurge --dry-run (#5564, #6599)", () => {
180189
"prediction-ledger": () => join(root, "prediction-ledger.sqlite3"),
181190
"portfolio-queue": () => join(root, "portfolio-queue.sqlite3"),
182191
"run-state": () => join(root, "run-state.sqlite3"),
192+
"policy-verdict-cache": () => join(root, "policy-verdict-cache.sqlite3"),
183193
"attempt-log": () => join(root, "attempt-log.sqlite3"),
184194
};
185195
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
186196
expect(runPurge(["--repo", "acme/widgets", "--dry-run", "--json"], { resolveDbPaths })).toBe(0);
187197
const result = JSON.parse(String(log.mock.calls[0]?.[0]));
188-
expect(result.stores).toHaveLength(6);
198+
expect(result.stores).toHaveLength(7);
189199
expect(result.stores.every((entry: { wouldPurge: number }) => entry.wouldPurge === 0)).toBe(true);
190200
expect(result.attemptLogTotalRows).toBe(0);
191201
for (const resolve of Object.values(resolveDbPaths)) {
@@ -283,6 +293,7 @@ describe("runPurge --dry-run (#5564, #6599)", () => {
283293
LOOPOVER_MINER_PREDICTION_LEDGER_DB: process.env.LOOPOVER_MINER_PREDICTION_LEDGER_DB,
284294
LOOPOVER_MINER_PORTFOLIO_QUEUE_DB: process.env.LOOPOVER_MINER_PORTFOLIO_QUEUE_DB,
285295
LOOPOVER_MINER_RUN_STATE_DB: process.env.LOOPOVER_MINER_RUN_STATE_DB,
296+
LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB: process.env.LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB,
286297
LOOPOVER_MINER_ATTEMPT_LOG_DB: process.env.LOOPOVER_MINER_ATTEMPT_LOG_DB,
287298
};
288299
process.env.LOOPOVER_MINER_CLAIM_LEDGER_DB = join(root, "claim-ledger.sqlite3");
@@ -291,12 +302,13 @@ describe("runPurge --dry-run (#5564, #6599)", () => {
291302
process.env.LOOPOVER_MINER_PREDICTION_LEDGER_DB = join(root, "prediction-ledger.sqlite3");
292303
process.env.LOOPOVER_MINER_PORTFOLIO_QUEUE_DB = join(root, "portfolio-queue.sqlite3");
293304
process.env.LOOPOVER_MINER_RUN_STATE_DB = join(root, "run-state.sqlite3");
305+
process.env.LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB = join(root, "policy-verdict-cache.sqlite3");
294306
process.env.LOOPOVER_MINER_ATTEMPT_LOG_DB = join(root, "attempt-log.sqlite3");
295307
try {
296308
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
297309
expect(runPurge(["--repo", "acme/widgets", "--dry-run", "--json"])).toBe(0);
298310
const result = JSON.parse(String(log.mock.calls[0]?.[0]));
299-
expect(result.stores).toHaveLength(6);
311+
expect(result.stores).toHaveLength(7);
300312
expect(result.stores.every((entry: { wouldPurge: number }) => entry.wouldPurge === 0)).toBe(true);
301313
// Nothing was created — dry run against nonexistent default-path stores makes zero writes.
302314
expect(existsSync(process.env.LOOPOVER_MINER_CLAIM_LEDGER_DB)).toBe(false);
@@ -323,13 +335,15 @@ describe("runPurge (real, #5564, #6599)", () => {
323335
const prediction = fakeStore(3);
324336
const portfolio = fakeStore(4);
325337
const runState = fakeStore(1);
338+
const policyVerdict = fakeStore(1);
326339
const options = {
327340
openClaimLedger: () => claim,
328341
initEventLedger: () => event,
329342
initGovernorLedger: () => governor,
330343
initPredictionLedger: () => prediction,
331344
initPortfolioQueueStore: () => portfolio,
332345
initRunStateStore: () => runState,
346+
initPolicyVerdictCacheStore: () => policyVerdict,
333347
};
334348

335349
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
@@ -338,14 +352,15 @@ describe("runPurge (real, #5564, #6599)", () => {
338352
expect(summary).toMatchObject({
339353
outcome: "purged",
340354
repoFullName: "acme/widgets",
341-
totalPurged: 11,
355+
totalPurged: 12,
342356
stores: [
343357
{ store: "claim-ledger", purged: 2 },
344358
{ store: "event-ledger", purged: 1 },
345359
{ store: "governor-ledger", purged: 0 },
346360
{ store: "prediction-ledger", purged: 3 },
347361
{ store: "portfolio-queue", purged: 4 },
348362
{ store: "run-state", purged: 1 },
363+
{ store: "policy-verdict-cache", purged: 1 },
349364
{ store: "attempt-log", purged: null, note: ATTEMPT_LOG_NOT_PURGEABLE_NOTE },
350365
],
351366
});
@@ -361,7 +376,7 @@ describe("runPurge (real, #5564, #6599)", () => {
361376
log.mockClear();
362377
expect(runPurge(["--repo", "acme/widgets"], options as never)).toBe(0);
363378
const text = String(log.mock.calls[0]?.[0]);
364-
expect(text).toContain("Purged 11 row(s) for acme/widgets");
379+
expect(text).toContain("Purged 12 row(s) for acme/widgets");
365380
expect(text).toContain("claim-ledger=2");
366381
expect(text).toContain("portfolio-queue=4");
367382
expect(text).toContain("run-state=1");

test/unit/miner-status.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ describe("loopover-miner status/doctor (#2288)", () => {
130130
"store-integrity:replay-snapshot",
131131
"store-integrity:worktree-allocator",
132132
"store-integrity:contribution-profile",
133+
"store-integrity:policy-verdict-cache",
133134
]);
134135
// REGRESSION (#6768): doctor previously omitted these four durable local stores from the integrity sweep.
135136
expect(checks.map((check) => check.name)).toEqual(

0 commit comments

Comments
 (0)