Skip to content

Redesign relationship updates to be non-destructive (only touch fields present in payload) #48

Description

@NoisyFlowers

Background

Mutation updates in Resolvers.js handleUpdate use a destructive "replace" pattern: for every updatable relationship type on a node, the handler unconditionally deletes all existing edges (Phase 1, archiving them into ENTERED_BY), then recreates only the edges whose field is present and non-empty in the payload (Phase 2). The only guard against leaving a relationship deleted is required: true, which throws when a required field is absent/empty — before any Cypher runs.

This pattern was the root cause of the Specimen↔Description severance fixed in #<this-fix-PR/commit f9ab84a>. That fix corrected the immediate bug (a descriptionID vs descriptionIDs field-name mismatch) and hardened the Description side with required: true, but it did not address the underlying destructive-update behavior.

Problems that remain

  • Partial severance. A caller that submits an incomplete relationship list (e.g. 2 of 3 specimenIDs) silently drops the omitted edges. required: true only enforces length > 0, not completeness, so it cannot catch this.
  • No backstop on asymmetric/optional relationships. Where a relationship is intentionally optional (e.g. Specimen → DESCRIBED_BY, required: false, because a Specimen may exist before any Description), required: true is not available as a guard. Any UpdateSpecimen that omits descriptionIDs will sever the link, and by design nothing stops it. The current fix relies on the client always sending the specimen's full current Description list.
  • Implicit "send everything every time" contract. Because absent ≡ delete, every relationship field is effectively mandatory on every update. This is fragile and easy to regress in any new client or direct API caller.

Proposed direction

Make relationship updates non-destructive: only modify a relationship when its field is explicitly present in the mutation payload (distinguish "absent" from "empty"). When a field is absent, leave its existing edges untouched; when present, reconcile to the submitted set. This removes the structural cause of accidental severance and makes partial updates safe.

This is a cross-cutting change affecting handleUpdate (and likely handleCreate) for all node types in SchemaMaps.js, so it warrants its own design before implementation. Consider how it interacts with the ENTERED_BY audit-archiving logic.

Related follow-up (separate)

An audit/remediation may be needed for Specimen→Description links severed by the original bug before commit f9ab84a. Recommend a read-only audit (e.g. Specimens edited after their Description was created but now lacking a DESCRIBED_BY edge, cross-referenced with archived _DESCRIBED_BY / ENTERED_BY audit edges) to scope whether data repair is warranted.

References

  • Fix commit: f9ab84a
  • Archived OpenSpec change: openspec/changes/archive/2026-06-09-fix-specimen-description-severance/ (see follow-up.md, design.md)
  • Capability spec: openspec/specs/specimen-description-integrity/spec.md

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions