Repository navigation
Store SchemaRelationship declarations as relationship-table edges (#2008) - #2010
Conversation
…oses #2008) Schema relationship declarations move out of the schema node's properties.relationships JSON into relationship-table rows between the declaring and target schema nodes (self-edge when untyped), with the full SchemaRelationship serialized in the row's properties. Migration v004 converts any existing JSON declarations and strips the key. - Consolidated read path: SqliteStore::get_schema_declarations / get_all_schema_declarations hydrate SchemaNode.relationships inside get_schema_node/get_all_schemas — one caller-facing fetch; every former independent JSON parse (create_relationship validation, delete_relationship's required-guard, rel_ops outbound branch, get_inbound_relationships, agent context) now agrees by construction - Single write path: NodeService::set_schema_relationships (used by create_schema/update_schema and core-schema seeding) with reserved builtin-name rejection and block-by-default protection for removing/retargeting declarations that live instance edges depend on - delete_node/bulk_delete/delete_subtree_atomic refuse to delete a schema node with declaration edges (closes the FK-CASCADE silent-destroy gap) - create_relationship refuses schema-node endpoints on instance writes - Daemon assembles the wire view via SchemaNode::into_wire_node, so the desktop client contract (properties.relationships on the wire) is unchanged; proto untouched - Deleted: InboundRelationshipCache (dead in production) + its test, SchemaRelationship.edge_table + compute_edge_table_name (SurrealDB holdovers), handle_add/remove_schema_relationship (tool-unreachable; update_schema covers the capability), SqliteStore::update_schema (no callers) - Test fixtures that hand-wrote relationships JSON now route through the real declaration write path; new integration suite covers storage shape, read-path agreement, reserved names, guards, and migration Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pragmatic Code Review — PR #2010 (issue #2008)Review Type: Initial Review (no prior reviews on this PR; full diff Code Review SummaryThis is a well-executed structural migration: schema relationship declarations move from One genuine gap keeps this from an immediate approve: the "single write path" invariant has a bypass through two public instance-edge mutation APIs (details below). It is a small, contained fix that mirrors the guard this PR already added to Requirements Check (issue #2008 acceptance criteria)
Open Question 1 (disambiguation) is resolved as endpoint- Findings🔴 Critical
🟡 Improvements
🟢 Nitpicks
Recommendation: REQUEST CHANGESThe PR is a clear net improvement to code health and closes issue #2008's structural goals with strong test coverage. The single blocker is the 🔴 declaration-mutation bypass in |
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: REQUEST CHANGES (posted as a comment — GitHub blocks review actions on one's own pull request). Full findings in the review comment above.
…nvariants - delete_relationship and update_relationship_properties now refuse a non-builtin edge whose source is a schema node — a declaration edge — mirroring create_relationship's guard, so the instance-edge API surface can no longer delete a declaration (bypassing live-edge protection) or overwrite its authoritative SchemaRelationship JSON. Two negative tests. - set_schema_declarations enforces the name invariants at the store layer (reserved builtin names, per-schema uniqueness) so direct callers like core-schema seeding cannot bypass them; service doc corrected to match reality about seeding's path. - Migration v004 drops (with a warning) a legacy declaration named after a builtin structural relationship instead of converting it into a live has_child/mentions/... edge between schema nodes; migration test extended. - DRY: both hand-rolled builtin matches! lists replaced with is_builtin_relationship. - Stale InboundRelationshipCache doc references removed from local_agent_service.rs; daemon get_schema_definition distinguishes a corrupt schema node (internal) from a non-schema node (failed_precondition); _unchecked delete paths document that schema-node ids must not be routed through them; unrelated bun.lock churn reverted. Addresses reviewer recommendations from PR #2010 review (1 critical, 2 important, all nits — none skipped). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Address-review summary (commit 2)All findings from the initial review addressed — none skipped: 🔴 Critical — declaration-mutation bypass (FIXED) 🟡 Migration builtin-name hazard (FIXED) — v004 drops (with warning) a legacy declaration named after a builtin structural relationship instead of converting it into a live structural edge between schema nodes; migration test extended with a 🟡 Seeding bypass (FIXED) — the name invariants (reserved builtin names + per-schema uniqueness) moved into 🟢 Nits (ALL FIXED) — builtin Tests: declarations suite 11/11, migrations 7/7, daemon green, Re-Review DecisionDecision: RE-REVIEW NEEDED Rationale: The critical finding was addressed with real logic changes to two public API methods plus store-layer validation changes — not style-only edits. A delta review of the new commit should verify the guards match the intended scoping (non-builtin + schema-source only). 🤖 Generated with Claude Code |
Pragmatic Code Review — PR #2010 (issue #2008)Review Type: Re-Review (delta Previous Review SummaryPrior findings: 1 Critical, 2 Improvements, 7 Nits. Addressed: 10/10. Not addressed: 0. Each verified against the delta, not just the summary comment:
Findings (delta)Critical IssuesNone. Suggested ImprovementsNone. Nitpicks
Verification Evidence
Recommendation: APPROVEThe single blocker is closed with correctly-scoped guards (non-builtin + schema-source, unreachable-around via rel_ops), both improvements landed at the right layers (migration-time drop; store-layer invariant backstop), and every nit was taken. The delta introduces no new issues. Net positive; ready to merge. |
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
Address-review (round 2)Re-review verdict was APPROVE with zero actionable findings. One nit was noted and is deliberately skipped: the guard in Re-Review DecisionDecision: NO RE-REVIEW NEEDED Rationale: No code changed in this round. The PR is ready for merge (pending approvals). 🤖 Generated with Claude Code |
Closes #2008.
What changed
Schema relationship declarations (
project → tasks → task) are no longer JSON inside the declaring schema node'sproperties.relationships— each declaration is a row in the existingrelationshiptable between the declaring and target schema nodes, finishing the design the SQLite migration dropped:in_node= declaring schema node,out_node= target schema node (self-edge whentarget_type: None),relationship_type= declared name, fullSchemaRelationshipJSON in the row'sproperties(authoritative).v004converts any existing JSON declarations to rows (self-edge fallback for dangling targets) and strips therelationshipskey from every schema node. Covered by a migration test.Consolidated read path (single round-trip preserved)
SqliteStore::get_schema_declarations/get_all_schema_declarationsare the only declaration readers;get_schema_node/get_all_schemashydrateSchemaNode.relationshipsfrom them, so callers still get fields + relationships in one fetch. All five former independent JSON parses now agree by construction (the issue counted four —delete_relationship's required-last-edge guard atrelationship.rs:813was a fifth):create_relationshipvalidationdelete_relationshiprequired-guardget_inbound_relationships(viaget_all_schemas)rel_ops::get_node_relationshipsoutbound branchcontext_opsagent relationship-hop context (search hits re-resolved from the hydrated corpus)A new integration test (
all_read_paths_agree_on_the_declaration_set) locks the agreement in.Single write path + guards
NodeService::set_schema_relationships(used bycreate_schema/update_schemaand core-schema seeding) is the only declaration writer. It enforces:has_child,mentions,member_of,has_role) —BUILTIN_RELATIONSHIP_NAMESmoved tomodels::schemaand is shared by every consumer. This check did not exist before under any storage model.Additional guards:
delete_node/bulk_delete/delete_subtree_atomicrefuse to delete a schema node that declaration edges point at or from (closes theON DELETE CASCADEsilent-destroy gap).create_relationshiprefuses a schema node on either end of an instance write.Wire contract unchanged
Correction to the issue's analysis:
nodespace-types::SchemaNodeis not dead — the desktop app parses the daemon's schema responses through it, readingproperties.relationshipsoff the wire. The daemon'sget_all_schemas/get_schema_definitionnow assemble the wire node viaSchemaNode::into_wire_node()(relationships embedded in wire properties only), so no frontend or proto change was needed and none was made.into_node()is the storage shape and never carries relationships.Deletions
InboundRelationshipCache+relationship_cache_test.rs(confirmed dead in production; no replacement cache per issue scope)SchemaRelationship.edge_table+compute_edge_table_name(SurrealDB holdovers, zero readers)handle_add_schema_relationship/handle_remove_schema_relationship— recorded decision for the issue's open item: these were tool-unreachable dead code andupdate_schema'sadd_relationships/remove_relationshipscovers the capability, so they are deleted rather than wired upSqliteStore::update_schema(no callers; raw-properties writer, now a footgun)Known limitations (noted deliberately)
create_node_with_parent). Declarations are written first inupdate_schemaso a live-edge rejection leaves the schema fully untouched.relationshipstable sync + relationship events as instance edges; if they ever need different treatment, ADR-045's filter-by-type mechanism applies. Flagging for whoever owns sync design, per the issue's courtesy-check note.Test evidence
schema_relationship_declarations_test.rs: 9 tests (storage shape, read-path agreement, reserved names, live-edge blocks, schema-delete guard, endpoint scoping)🤖 Generated with Claude Code