Skip to content

validation gap: every write path lands Relations/Pages with dangling foreign-id references #123

Description

@galuis116

Problem

Every write path that lands a Relation or a Page accepts dangling foreign-id references. relation.source, relation.target, relation.evidence, page.entities, and page.sources can all reference ids that resolve to nothing in the KB. health.lint / doctor reports the resulting dangling_relation finding after the fact (src/vouch/health.py:135-145), but no write path enforces it, so the corrupt state is reachable through every approval / import / sync path the system advertises.

This is the same structural shape as #81 (uncited claims): the invariant lives in one observer (health.lint) but is enforced by zero writers, so every write path silently produces what the reviewer is told the KB never contains.

End-to-end repro on test HEAD (6047ddc):

  • Layer 1 propose_relation -> approve : source/target/evidence all dangling, landed
  • Layer 2 storage.put_relation : dangling endpoints landed
  • Layer 3 storage.put_page : dangling entities/sources landed
  • Layer 4 bundle.import_apply : ok=True, dangling relation + page written
  • Layer 5 sync.sync_apply : ok=True, dangling relation + page written

Affected paths:

  • proposals.propose_relation (src/vouch/proposals.py:179-207) - no existence checks for src, target, or evidence ids. Compare propose_claim:91-98 which validates every evidence id resolves.
  • proposals.propose_page (src/vouch/proposals.py:115-147) - no existence checks for claim_ids, entity_ids, source_ids.
  • storage.put_relation / put_relation_idempotent (src/vouch/storage.py:399-440) - write the YAML with zero validation. Reached by proposals.approve and by lifecycle.supersede / lifecycle.contradict.
  • storage.put_page (src/vouch/storage.py:342-354) - validates claims but not entities or sources.
  • bundle.import_apply (src/vouch/bundle.py) - writes member bytes directly, bypassing the storage layer; _validate_content only checks field shapes, not whether referenced ids exist.
  • sync.sync_apply (src/vouch/sync.py) - same pattern via bundle._validate_content.

The audit log records each landing as a normal approve / import event with the legitimate id, so an operator reading the trail has no signal the artifact references nothing.

Why this matters

  • README "Object model" describes Relations as a "typed edge between entities / claims / pages" - the implicit invariant that source/target are such artifacts is what makes graph traversal (relations_from, relations_to, kb.context) meaningful.
  • health.lint already calls out dangling_relation as an error-severity finding, so the team already treats this as a defect - it just isnt prevented at write time.
  • The bundle / sync paths defeat the portable-bundle integrity story: a manifest-consistent bundle can ship a fully dangling knowledge graph and import_check.ok reports True.

This is the prevention counterpart to #96 / #112 (vouch fsck): fsck reports existing damage; this is about not landing new damage.

Suggested fix

Mirror the Claim.evidence + put_claim enforcement to the Relation / Page surface:

  1. storage.put_relation / put_relation_idempotent - verify source and target each resolve to a known claim / page / entity / source; verify every evidence id resolves to a source / evidence.
  2. storage.put_page - extend the existing claims loop to cover entities and sources.
  3. proposals.propose_relation / propose_page - surface the same checks earlier as ProposalError.
  4. bundle.import_check - after the per-file schema + sha256 pass, run a cross-artifact graph-integrity pass against the post-merge id set (destination KB plus incoming bundle); import_apply already raises on the first issue.
  5. sync.sync_check - same cross-artifact pass.

No on-disk-layout, schema, or bundle-format change - only data the model + observers already say should never land starts raising.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions