Skip to content

feat(lifecycle): backlink reconciliation — propose missing reverse relations#359

Open
jonathanchang31 wants to merge 2 commits into
vouchdev:testfrom
jonathanchang31:feat/backlink-reconciliation-307
Open

feat(lifecycle): backlink reconciliation — propose missing reverse relations#359
jonathanchang31 wants to merge 2 commits into
vouchdev:testfrom
jonathanchang31:feat/backlink-reconciliation-307

Conversation

@jonathanchang31

@jonathanchang31 jonathanchang31 commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements #307: a read-then-propose pass over the relation graph.
For every existing Relation whose type has a configured inverse, checks whether the mirror edge already exists at the target and, if not, files one propose_relation proposal citing the originating relation id. Never writes an approved edge directly.

  • Which mirror to propose per RelationType is config (.vouch/config.yaml's backlinks.inverse_map), not hardcoded.
    Defaults to depends_onblocks plus the symmetric set similar_to / relates_to / contradicts. The other directed types (uses, supports, caused_by, owned_by, derived_from, implements, references, mentions) have no natural inverse RelationType today, so per the issue's own "unmapped types are skipped rather than guessed" rule they're left unmapped rather than invented — happy to extend the default map if maintainers want a specific pairing for any of these.
  • Proposals are attributed to a fixed reconcile actor (lifecycle.RECONCILE_ACTOR), mirroring extractors/edges.py's AUTO_EXTRACTOR_ACTOR convention for automated passes rather than whichever human/agent triggered the run.
  • Registered at all four sites: kb.reconcile_backlinks in server.py (MCP) and jsonl_server.py, capabilities.METHODS, and vouch reconcile-backlinks (--rel-types, --limit, --dry-run)
    in cli.py. Not added to trust.READ_METHODS — same bucket as propose_relation / expire / reject_extracted, since it files proposals rather than only reading durable state.

Related Issue

Closes: #307

Change Type

  • New feature
  • Bug fix
  • Breaking change
  • Documentation update

Real Behavior Proof

End-to-end against a real KB (entities auth-service, user-db, an
approved depends_on relation):

$ vouch reconcile-backlinks --dry-run
would propose 1 backlink proposal(s)
  20260704-100430-ce7af0d3  user-db --blocks--> auth-service
rerun without --dry-run to file these proposals

$ vouch reconcile-backlinks
proposed 1 backlink proposal(s)
  20260704-100450-c3d566b3  user-db --blocks--> auth-service
$ vouch pending
• 20260704-100450-c3d566b3  [relation]  by reconcile
    —
$ vouch approve 20260704-100450-c3d566b3
Approved → relation/user-db-blocks-auth-service
$ vouch reconcile-backlinks
no missing backlinks found

$ vouch doctor && vouch lint && vouch fsck
{'claims': 1, 'pages': 1, 'sources': 1, 'entities': 2, 'relations': 2, ...}
clean
clean

Checklist

  • kb.reconcile_backlinks registered at all four sites with matching signatures
  • Default inverse/symmetric map defined, overridable via .vouch/config.yaml; unmapped types skipped
  • Pass reads the graph via the store's relation surface and confirms the reverse edge is absent before proposing (see deviation note above re: kb.graph_export)
  • Each gap yields exactly one pending relation Proposal via propose_relation, with a rationale referencing the originating edge
  • Never approves or writes an approved relation — human kb.approve required
  • Symmetric types reconcile without proposing a duplicate when the mirror already exists
  • --dry-run reports the would-propose set and writes nothing
  • --limit bounds proposals per run
  • tests/test_reconcile_backlinks.py: directed gap proposed, already-mirrored skipped, symmetric edge, unmapped type skipped, dry-run — plus limit bounding, rel_types filtering, config override, and malformed-config fallback
  • Full pytest suite green, make lint clean
  • Manually validated end-to-end (see Real Behavior Proof)
  • CHANGELOG.md updated under [Unreleased]

…lations (vouchdev#307)

Adds `reconcile_backlinks` (src/vouch/lifecycle.py): a read-then-propose
pass over the relation graph. For every existing Relation whose type has
a configured inverse, checks whether the mirror edge already exists at
the target and, if not, files one `propose_relation` proposal citing the
originating relation id — never writes an approved edge directly.

Which mirror to propose per RelationType is config, not hardcoded
(`.vouch/config.yaml`'s `backlinks.inverse_map`), defaulting to
`depends_on` <-> `blocks` and the symmetric set `similar_to` /
`relates_to` / `contradicts`. Other directed types (uses, supports,
caused_by, owned_by, derived_from, implements, references, mentions) have
no natural inverse RelationType today, so per the issue's own "unmapped
types are skipped rather than guessed" rule, they're left unmapped rather
than invented.

Proposals are attributed to a fixed `reconcile` actor (RECONCILE_ACTOR),
mirroring extractors/edges.py's AUTO_EXTRACTOR_ACTOR convention for
automated passes, not whichever human or agent triggered the run.

Registered at all four surface sites: `kb.reconcile_backlinks` in
server.py (MCP) and jsonl_server.py, `METHODS` in capabilities.py, and
`vouch reconcile-backlinks` in cli.py (--rel-types, --limit, --dry-run).
Not added to trust.py's READ_METHODS, same bucket as propose_relation /
expire / reject_extracted, since it files proposals.

Deviates from the issue's suggested read path in one respect, flagged in
the docstring: vouchdev#307 suggested reading the graph via `kb.graph_export`,
but that method renders the unrelated provenance DAG (claim citations,
supersedes, approvedBy) as a dot/mermaid string and never touches
Relation objects. The real edge set is `store.list_relations()`, with
`relations_from`/`relations_to` as the existence check — the same
underlying data `kb.neighbors` merges in, minus its extra structural
(non-Relation) edges that would otherwise risk a false "already exists"
match.

tests/test_reconcile_backlinks.py covers all five acceptance-criteria
scenarios (directed gap, already-mirrored skip, symmetric type, unmapped
type skip, dry-run) plus limit bounding, rel_types filtering, config
overrides, and malformed-config fallback. Manually validated end-to-end
against a real KB: propose/approve entities and a depends_on relation,
dry-run and real reconcile-backlinks runs, approve the resulting
proposal, confirm a second run finds nothing left to reconcile, and
`vouch doctor`/`lint`/`fsck` all clean throughout.
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 95796a22-0c71-4c89-9082-e25bc20fb56f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added docs documentation, specs, examples, and repo guidance cli command line interface mcp mcp, jsonl, and http surfaces storage kb storage, migrations, schemas, and proposals tests tests and fixtures size: M 200-499 changed non-doc lines labels Jul 4, 2026
@github-actions github-actions Bot removed the docs documentation, specs, examples, and repo guidance label Jul 4, 2026
@jonathanchang31

Copy link
Copy Markdown
Contributor Author

@plind-junior Could u plz review my PR? thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli command line interface mcp mcp, jsonl, and http surfaces size: M 200-499 changed non-doc lines storage kb storage, migrations, schemas, and proposals tests tests and fixtures

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant