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.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ 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! |
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]