Skip to content

prospect verify passes on a graph with broken evidence trails #49

Description

@elecnix

Problem

verifyNodes only recomputes each node's output_key from its stored (input_key, content):

// src/commands/verify.ts
const recomputed = computeOutputKey(n.input_key, content);
if (recomputed !== n.output_key) {  }

It never reads analysis_edges. So a graph whose evidence trails are broken verifies as clean.

Observed concretely: after nodes were removed out of band, the database held 43,094 dangling consumes edges across 689 session-overview nodes — every one pointing at an output_key that no longer exists — and verification still reported:

✓ 32562 node(s) verified: every output_key is consistent with its content.

Why this matters

DESIGN.md makes verification the mechanism for catching exactly this class of damage:

Verification — recomputing every node's output key from its stored content and confirming it matches. Because identities are content-addressed, any drift reveals out-of-band tampering or corruption.

But the invariant most worth protecting is a relationship invariant, not a content one:

A proposal can always be traced, via edges, back to the conversation evidence that justifies it.

A dangling consumes edge breaks precisely that, and it is currently invisible. prospect show will fail to walk the trail, but nothing reports the graph as damaged — the one tool whose stated job is detecting corruption says everything is fine.

Note the asymmetry that makes this subtle: a node whose consumers disappear is harmless (append-only lineage does this routinely), whereas a node whose targets disappear is a broken trail. Only the second is a defect.

How it arose

Manual DELETE against the DB, not a code path — grep confirms nothing in src/ deletes nodes or edges, and the graph is append-only by design. That is not a mitigation though: out-of-band modification is the stated reason verify exists.

Proposed change

Extend verifyNodes (or add a sibling check) to validate referential integrity per edge kind, using the vocabulary already in edge-kinds.ts:

edge kind target must exist as
consumes, revises analysis_nodes.output_key
anchors sessions.id or messages.id
uses_prompt prompt_versions.hash
uses_config analyzer_configs.id
produces proposals.id
contrasts_with sessions.id

Report dangling edges grouped by owning analyzer and edge kind, so the output points at what to recompute. validateEdge already checks that a kind pairing is legal; this extends the same idea from "is this edge well-typed" to "does its target exist".

Worth considering alongside: a prospect gc <analyzer> that deletes an analyzer's nodes and the edges referencing them, so the manual cleanup that caused this has a safe supported path instead of raw SQL.

Repro

  1. Delete any analyzer's nodes directly (DELETE FROM analysis_nodes WHERE analyzer_id = …) without deleting edges pointing at their output_key.
  2. Run prospect verify — it reports a clean graph.
  3. Query for edges whose to_ref_kind='analysis_node' and whose to_ref_id matches no output_key — they exist.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions