From d9776452b2e29e0c4c4bd2156771aabd9ca1ba75 Mon Sep 17 00:00:00 2001 From: sb-iam <59984144+sb-iam@users.noreply.github.com> Date: Sun, 28 Jun 2026 07:47:53 -0500 Subject: [PATCH] feat(continual-learning): derive wasRealCollision from git overlap (RSI step 3) Backend becomes authoritative for the verifier signal. recordOutcome now overrides the client-supplied wasRealCollision with deriveWasRealCollision(): a flagged collision counts as REAL only if BOTH named engineers currently have the collided file in their git changedFiles (getGitStates, 120s freshness TTL). Conservative false when the collision is orphaned/missing or git state is stale. Restores the (accepted x wasReal) 2x2 the spec assumes instead of the hardcoded 107/107 true. frontend/useInterventions.ts stops sending a hardcoded `true` (now a backend-overridden placeholder). Spec: continual-learning/spec.md:98-108, policy.md:35-42. backend+frontend typecheck + eslint pass. PLAN.md P0.5 rung 3 added. Co-Authored-By: Claude Opus 4.8 --- backend/src/memory/store.ts | 41 +++++++++++++++++++++--- docs/PLAN.md | 20 +++++++++--- frontend/src/livekit/useInterventions.ts | 4 ++- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/backend/src/memory/store.ts b/backend/src/memory/store.ts index 8125385..61af96f 100644 --- a/backend/src/memory/store.ts +++ b/backend/src/memory/store.ts @@ -5,7 +5,7 @@ import type { InterventionOutcome, InterventionStatus, } from '@podman/shared'; -import { collections } from './db.js'; +import { collections, getGitStates } from './db.js'; import { enrichCollisionMemory } from './vectors.js'; function comparableFile(raw?: string): string { @@ -84,13 +84,46 @@ export async function updateInterventionStatus( ); } +/** + * Step 3 — derive whether a flagged collision was REAL from git ground truth, + * instead of trusting the client (which historically hardcoded `true`). A + * collision counts as real only if BOTH named engineers currently have the + * collided file in their git `changedFiles`. Conservative: returns false when + * the collision is orphaned/missing or git state is stale/unavailable. + * Verifier supervision per docs/continual-learning/spec.md:98-108, policy.md:35-42. + */ +export async function deriveWasRealCollision(outcome: InterventionOutcome): Promise { + try { + const c = await collections(); + const collision = await c.collisions.findOne({ id: outcome.collisionId }); + if (!collision || !Array.isArray(collision.engineers) || collision.engineers.length < 2) { + return false; + } + const target = comparableFile(collision.file); + if (!target) return false; + const gitStates = await getGitStates(outcome.podId); + const touchesTarget = (name: string): boolean => + (gitStates.get(name)?.changedFiles ?? []).some((f) => comparableFile(f) === target); + return collision.engineers.every(touchesTarget); + } catch (err) { + console.error(`[memory] wasRealCollision verifier failed: ${(err as Error).message}`); + return false; + } +} + export async function recordOutcome(outcome: InterventionOutcome): Promise { + // Backend is authoritative for wasRealCollision: derive it from git overlap + // rather than trusting the client-supplied value. (RSI Step 3) + const verified: InterventionOutcome = { + ...outcome, + wasRealCollision: await deriveWasRealCollision(outcome), + }; await persist('outcome', async () => { const c = await collections(); - await c.outcomes.insertOne({ ...outcome }); + await c.outcomes.insertOne({ ...verified }); await c.interventions.updateOne( - { id: outcome.interventionId }, - { $set: { status: outcome.accepted ? 'accepted' : 'dismissed' } }, + { id: verified.interventionId }, + { $set: { status: verified.accepted ? 'accepted' : 'dismissed' } }, ); }); } diff --git a/docs/PLAN.md b/docs/PLAN.md index eb1d4a7..bbbf83f 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -509,11 +509,21 @@ no schema change. Owner: RSI track. Independent of the MongoDB-cleanup handoff. - Spec: `docs/continual-learning/policy.md:62-63` (prefer prior accepted kind), `plan.md:66` (second similar event behaves differently). -Follow-ups (separate rungs, not in this change): Step 3 derive -`wasRealCollision` from git overlap; Step 4-5 `strategy_versions` + -Gemini-proposed `LearningProposal` slice; seed a clean demo pod with a repeated -dismissed signature (the historic 85 dismissals are orphaned — `collisionId` -resolves to no collision — so they cannot drive the demo verifier). +3. **Step 3 - derive `wasRealCollision` from git overlap (backend-authoritative)** ✅ + - `backend/src/memory/store.ts` `recordOutcome` now overrides the + client-supplied `wasRealCollision` with `deriveWasRealCollision()`: a + collision is real only if BOTH named engineers currently have the collided + file in their git `changedFiles` (`getGitStates`, 120s freshness TTL). + Conservative `false` when orphaned/stale. `frontend/.../useInterventions.ts` + stops sending a hardcoded `true` (now a backend-overridden placeholder). + - Restores the (accepted × wasReal) 2×2 the spec assumes. + - Spec: `docs/continual-learning/spec.md:98-108`, `policy.md:35-42`. + +Follow-ups (separate rungs, not in this change): Step 4-5 `strategy_versions` + +Gemini-proposed `LearningProposal` slice; Step 6 durable `owns` write; seed a +clean demo pod with a repeated dismissed signature (the historic dismissals are +orphaned — `collisionId` resolves to no collision — so they cannot drive the +demo verifier). ### P1 - polish the money moment diff --git a/frontend/src/livekit/useInterventions.ts b/frontend/src/livekit/useInterventions.ts index 48af2fe..f6b4816 100644 --- a/frontend/src/livekit/useInterventions.ts +++ b/frontend/src/livekit/useInterventions.ts @@ -66,7 +66,9 @@ export function useInterventions(room: Room | null) { interventionId: active.id, collisionId: active.collisionId, podId: active.podId, - wasRealCollision: true, + // Placeholder only — the backend derives the authoritative value from + // git overlap at outcome time (the client cannot know). (RSI Step 3) + wasRealCollision: false, accepted, recordedAt: new Date().toISOString(), });