From d448fb79743bdaf56807b093c0dc5643b3f51479 Mon Sep 17 00:00:00 2001 From: Nehanth Date: Tue, 20 Oct 2026 21:06:37 -0400 Subject: [PATCH] fix(osv): keep fresh reachability verdicts when enabling osv on an existing INFO.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When osv was enabled on a project that already had a complete INFO.md, the preserve path overwrote the freshly generated analysis with the old threat model plus osvEnrichment.context. Since that context is now a prompt directive (instructions to emit a graded Dependency reachability block), the persisted INFO.md ended up carrying raw grading instructions and the real verdicts the analysis agent just produced were discarded — so matcher generation saw the stale threat model, not the reachability grades. Enabling osv is a deliberate "re-analyze with dependencies in view" action, so keep the fresh analysis (verdicts included) instead of restoring the preserved copy. The non-osv preserve path is unchanged and still keeps curated content. Adds a coordinator test for the osv + preserved-INFO.md path (verified to fail without the fix). Co-Authored-By: Claude Opus 4.8 --- .../src/__tests__/setup-coordinator.test.ts | 34 +++++++++++++++++++ packages/deepsec/src/setup/coordinator.ts | 11 +++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/deepsec/src/__tests__/setup-coordinator.test.ts b/packages/deepsec/src/__tests__/setup-coordinator.test.ts index f3b312c0..c772e05a 100644 --- a/packages/deepsec/src/__tests__/setup-coordinator.test.ts +++ b/packages/deepsec/src/__tests__/setup-coordinator.test.ts @@ -13,6 +13,19 @@ import { setupErrorExitCode, } from "../setup/protocol.js"; +// Stub the osv enrichment so the coordinator path can be exercised without the +// external osv-scanner/uv/syft toolchain. `runOsvEnrich` returns a sentinel +// "directive" context (what buildContext now produces); mergeInfoWithOsvContext +// stays real so the non-osv preserve path still behaves exactly as in production. +const OSV_DIRECTIVE_SENTINEL = "### Dependency reachability (required) — DIRECTIVE SENTINEL"; +vi.mock("../setup/osv-enrich.js", async (importActual) => { + const actual = await importActual(); + return { + ...actual, + runOsvEnrich: vi.fn(() => ({ context: OSV_DIRECTIVE_SENTINEL, hits: [] })), + }; +}); + const originalCwd = process.cwd(); afterEach(() => process.chdir(originalCwd)); @@ -245,6 +258,27 @@ describe("one-shot setup coordinator", () => { expect(fs.readFileSync(path.join(workspace, "data", "app", "INFO.md"), "utf8")).toBe( curatedInfo, ); + + // Enabling osv on a project that already has a complete INFO.md must NOT + // discard the freshly analyzed reachability verdicts by restoring the old + // copy with the raw grading directive appended. The fresh analysis wins. + fs.writeFileSync(path.join(workspace, "data", "app", "INFO.md"), curatedInfo); + const infoAfterOsv = await (async () => { + await runSetupWorkflow({ + workspaceDir: workspace, + projectId: "app", + projectRoot: project, + through: "threat-model", + osv: true, + services, + onLog: () => undefined, + }); + return fs.readFileSync(path.join(workspace, "data", "app", "INFO.md"), "utf8"); + })(); + expect(analyze).toHaveBeenCalledTimes(3); // osv flag invalidated the info checkpoint + expect(infoAfterOsv).not.toContain(OSV_DIRECTIVE_SENTINEL); // directive never persisted + expect(infoAfterOsv).not.toBe(curatedInfo); // old copy not restored over fresh analysis + expect(infoAfterOsv).toContain("## Threat model"); // fresh analysis content kept }); it("uses the new harness default model instead of a previous harness model", async () => { diff --git a/packages/deepsec/src/setup/coordinator.ts b/packages/deepsec/src/setup/coordinator.ts index 54f551ff..a7555f18 100644 --- a/packages/deepsec/src/setup/coordinator.ts +++ b/packages/deepsec/src/setup/coordinator.ts @@ -551,7 +551,16 @@ export async function runSetupWorkflow( // appended section first, then append the freshly generated one (or none). // Otherwise refreshes accumulate duplicate sections and keep listing // dependencies that have since been patched or removed. - if (preserveInfo) { + // + // Exception: when osv is enabled, `osvEnrichment.context` is a *prompt + // directive* (it tells the analysis agent to emit a graded Dependency + // reachability block), not a self-contained section. Appending it to the old + // INFO.md would persist raw grading instructions and discard the real + // reachability verdicts the agent just wove into the freshly generated + // INFO.md. Enabling osv is a deliberate "re-analyze with dependencies in + // view" action, so keep the fresh analysis (verdicts included) rather than + // the preserved copy. The non-osv path keeps curated content as before. + if (preserveInfo && !osvEnabled) { atomicWriteFileSync(infoPath, mergeInfoWithOsvContext(preserveInfo, osvEnrichment.context)); } state.infoDigest = digest(fs.readFileSync(infoPath, "utf8"));