Repository navigation
Normalize relationship writes through an in-direction name to the forward edge - #2951
Conversation
…ward edge (closes #2941) create_relationship accepted a write through an `in` declaration's own name and stored it with that name and the caller's endpoint order, giving one logical edge two storage shapes. Every write path (create/remove in tx, delete, update properties) now rewrites such a call to the forward spelling — endpoints swapped, stored under the declaration's reverseName — before validating, so target type, cardinality on both ends, edge-field validation and required last-edge protection apply unchanged. - check_node_completeness drops the in-name own-end lookup; an `in` declaration is satisfied only by the inbound forward edge. - rel_ops::resolve_relationship_name resolves a node's own `in` declaration as a Reverse traversal of its forward name, so reads through the `in` name find the normalized edge. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pragmatic Code Review — PR #2951Review Type: Initial Review Requirements Check
Focus areas checked, no finding
Code Review Findings 🟡 Important: the viewer and inbound readers still assume an
|
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: REQUEST CHANGES (posted as a comment — GitHub blocks self-review verdicts). Full findings in the review comment above.
- get_inbound_relationships returns only forward (out) declarations. An `in` declaration never has instance edges of its own, so reporting it as inbound gave the panel a permanently empty group, let name resolution accept a spelling that always returned nothing, and advertised it in the unknown-relationship error. - get_node_relationships no longer renders an own `in` declaration as an outbound group; its edges render once, in the forward declaration's inbound group. - create rejects a write through an `in` name whose far type does not declare the forward name as `out` (unpaired, or paired with another `in`), naming the caller's own spelling instead of the rewritten one. - update's missing-edge error names the edge as the caller wrote it. - Cross-type tests (doc.approved_by in <-> person.approves out, plus a memo sharing the forward name): forward storage, wrong-type rejection, read narrowing, unpaired rejection, panel rendering. Skipped: validating the in/out pairing at schema save (changes schema-authoring rules, beyond this PR; tracked as follow-up), and folding the four normalization call sites into one helper (each uses a different source lookup — tx vs virtual-date vs plain get_node — so a shared helper would need the node passed in and the saving is a few lines). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An `in` declaration is only meaningful as the exact mirror of a forward declaration on its targetType, since every write through it is stored as that forward edge. create_schema and update_schema's add_relationships now reject an `in` declaration unless: - it has a targetType; - that type's extends chain declares its reverseName as `direction: out` (a forward that is itself `in` does not count); - the forward's reverseName names it back, and the forward's targetType is this schema or an ancestor; - the cardinalities mirror each other from both ends; - it declares no edgeFields (they are validated on the forward side and would be silently ignored here). A self-referential pair declared in one payload still validates against the payload itself, since the schema cannot be looked up yet. The runtime rejection in create_relationship_in_tx stays: a forward half removed after the `in` declaration was saved is still reachable, and is now what the runtime test exercises. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Address-review summaryAddressed (4 of 5):
Skipped (1):
Not covered by a test: a Play Verification: core suite and clippy clean; Re-review needed: yes. Reader behavior changed and schema-save validation is new. |
Pragmatic Code Review: Re-Review (commits
|
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (re-review): APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
- Editing the forward side is now checked: when update_schema changes a schema's relationships or extends, every other schema's `in` declaration whose forward resolves through it is re-validated against the result. Removing a mirrored forward, or re-adding it with another reverseName or cardinality, is rejected naming the pair it would break. - The forward lookup walks the far type's whole extends chain, with the schema being saved standing in for its stored state wherever it appears. A subtype's `in` declaration can now mirror a forward it inherits, the same chain the write path resolves by (ADR-078). - update_schema validates pairing after a same-call `extends` re-target, against the resulting chain rather than the one being replaced. - Runtime guard: a write through an `in` name also requires the forward to name it back. set_schema_relationships (called by Play actions) writes declarations below schema save, so a pair can still break there. - Tests: forward edit rejection, inherited forward, extends re-target, the below-save runtime path, the missing forward-cardinality mirror case, and message assertions on the wrong-type test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Address-review summary (re-review round), commit
|
Pragmatic Code Review: Re-Review (commit
|
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (re-review 2): APPROVE with a 🟡 to fix before merge (posted as a comment — GitHub blocks self-approval). Findings in the comment above.
…ved schema - type_chain now splices: a descendant's stored chain runs through the schema being saved into its OLD ancestors, so everything from that schema onward is replaced with the chain this call leaves it with. Re-parenting a middle schema away from the ancestor that declares a forward a grandchild's mirror depends on is now rejected (the owner chain for such mirrors is spliced the same way). - Mirror-check errors are only re-framed when they are pairing rejections (InvalidParams, unwrapped so the kind prefix is not repeated); storage failures pass through with their own kind. - Comment on the update gate: an extends re-target counts as a declaration change (it bumps relationships_added). Skipped: returning early from the mirror scan when no schema declares an `in` relationship — knowing that takes the same get_all_schemas read. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Address-review summary (round 3), commit
|
Pragmatic Code Review: Re-Review (commit
|
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (re-review 3): APPROVE, no findings (posted as a comment — GitHub blocks self-approval).
Address-review (final)The re-review of |
Closes #2941
Closes #3026
Problem
create_relationshipaccepted a write through anin-direction declaration's own name (e.g.old --superseded_by--> new) and stored it under that name with the caller's endpoint order. One logical edge then had two storage shapes: forward-name reads missed it, cardinality was bypassed on both ends, andcheck_node_completenesshad to check both shapes.Change
relationship.rs):create_relationship_in_tx,remove_relationship_in_tx,delete_relationshipandupdate_relationship_propertiesrewrite anin-name call to its forward spelling first. The endpoints are swapped and the name becomes the declaration'sreverseName. The existing forward validation (target type,cardinality+reverse_cardinalityreplace, edge-field enums, required last-edge guard) then applies unchanged. Helpers:in_declaration_forward_name,forward_endpoints.schema.rs): the in-name own-end lookup is removed. Anoutdeclaration counts edges from its own end; anindeclaration counts only the inbound forward edge.rel_ops.rs):resolve_relationship_nameresolves a node's ownindeclaration as aReversetraversal of its forward name. Without this, a read through theinname would find nothing once writes are normalized.No backfill: reset the database to drop any in-name edges already stored.
Tests
New
packages/core/tests/relationship_in_name_normalization_test.rs:in-name write stores the same row as the forward write.inname.inname, including on an edge written the forward way.inname finds the forward edge.The existing
required_inbound_relationship_written_through_in_name_reports_completestill passes, now on the normalized shape.Follow-up (not in this PR)
The relationships panel (
get_node_relationships) still draws an ownindeclaration as an outgoing group, andget_inbound_relationshipsreturns it as an incoming group as well. Both groups are now always empty. The forward declaration's incoming group already shows these edges, so nothing regresses, but the panel should renderindeclarations once.🤖 Generated with Claude Code