feat(lifecycle): backlink reconciliation — propose missing reverse relations#359
feat(lifecycle): backlink reconciliation — propose missing reverse relations#359jonathanchang31 wants to merge 2 commits into
Conversation
…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.
|
Warning Review limit reached
Next review available in: 57 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
@plind-junior Could u plz review my PR? thanks! |
|
@jonathanchang31 – That sounds very interesting! Could you please double‑check that it works correctly with Vouch v1.2.1? I'm eager to see it in action. |
|
Fix the conflict |
|
@coderabbitai review |
✅ Action performedReview finished.
|
Summary
Implements #307: a read-then-propose pass over the relation graph.
For every existing
Relationwhose type has a configured inverse, checks whether the mirror edge already exists at the target and, if not, files onepropose_relationproposal citing the originating relation id. Never writes an approved edge directly.RelationTypeis config (.vouch/config.yaml'sbacklinks.inverse_map), not hardcoded.Defaults to
depends_on↔blocksplus the symmetric setsimilar_to/relates_to/contradicts. The other directed types (uses,supports,caused_by,owned_by,derived_from,implements,references,mentions) have no natural inverseRelationTypetoday, 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.reconcileactor (lifecycle.RECONCILE_ACTOR), mirroringextractors/edges.py'sAUTO_EXTRACTOR_ACTORconvention for automated passes rather than whichever human/agent triggered the run.kb.reconcile_backlinksinserver.py(MCP) andjsonl_server.py,capabilities.METHODS, andvouch reconcile-backlinks(--rel-types,--limit,--dry-run)in
cli.py. Not added totrust.READ_METHODS— same bucket aspropose_relation/expire/reject_extracted, since it files proposals rather than only reading durable state.Related Issue
Closes: #307
Change Type
Real Behavior Proof
End-to-end against a real KB (entities
auth-service,user-db, anapproved
depends_onrelation):Checklist
kb.reconcile_backlinksregistered at all four sites with matching signatures.vouch/config.yaml; unmapped types skippedkb.graph_export)relationProposalviapropose_relation, with a rationale referencing the originating edgekb.approverequired--dry-runreports the would-propose set and writes nothing--limitbounds proposals per runtests/test_reconcile_backlinks.py: directed gap proposed, already-mirrored skipped, symmetric edge, unmapped type skipped, dry-run — plus limit bounding,rel_typesfiltering, config override, and malformed-config fallbackpytestsuite green,make lintcleanCHANGELOG.mdupdated under[Unreleased]