Skip to content

Slice B: update_node_avps/2 (merge/upsert + mutate/1 grammar kind)#49

Merged
david-w-t merged 8 commits into
davidwt-com:mainfrom
david-w-t:develop
Jun 26, 2026
Merged

Slice B: update_node_avps/2 (merge/upsert + mutate/1 grammar kind)#49
david-w-t merged 8 commits into
davidwt-com:mainfrom
david-w-t:develop

Conversation

@david-w-t

Copy link
Copy Markdown
Contributor

Slice B — update_node_avps/2

Implements graphdb_mgr:update_node_avps/2 (previously a not_implemented
stub) as an atomic merge over a node's attribute_value_pairs, through the
existing three-tier write-path transaction seam, and exposes it as a fourth
mutate/1 batch mutation kind.

The last not_implemented stub of its pair in graphdb_mgr (only
delete_node remains). Unblocks the AVP-edit halves of slice C (template
attribute list) and slice E (relationship-AVP edit).

Semantics

  • Upsert — an update map carrying a value key replaces the matching
    attribute in place (preserving list position), or appends if new.
  • Delete — an update map lacking the value key removes that attribute
    (no-op if absent). No new sentinel: the value-less map shape was unused
    anywhere in the system.
  • value => undefined is a real upsert (a declared-but-unbound entry, slice
    C's case), never a delete.
  • Order-preserving upsert honors the codebase's name-AVP-at-head convention.

Layering (3-tier seam)

  • Tier-1 update_node_avps_in_txn/3 — bare-mnesia, runs inside a caller's
    txn, mnesia:abort/1 on failure, exported for composition.
  • Tier-2 update_node_avps/2 — owns one transaction/1; runs the
    client-side + pre-txn guards outside the txn (RetAttr resolved outside —
    the load-bearing no-gen_server-call-in-txn invariant).
  • Tier-3 mutate/1{update_node_avps, Nref, AVPs} composes the tier-1
    primitive directly into the batch transaction.

Guards

Category-immutable, permanent-tier, well-formedness (client-side),
node-existence, attribute-existence (upserts only), and retired-marker (the
retired state stays behind retire_node/unretire_node). Bare abort
reasons; whole-batch rollback in mutate/1.

Tests

480 CT + 122 EUnit, zero warnings. New: 17 pure/client-side EUnit + an
update_avps CT group (11 cases incl. round-trip, delete, undefined-retained,
every guard, single-call + whole-batch atomicity, head-position preservation)

  • 5 new mutate-group cases.

Docs

docs/designs/slice-b-update-node-avps-design.md,
docs/superpowers/plans/2026-06-25-slice-b-update-node-avps.md; TASKS.md
marked IMPLEMENTED; both CLAUDE.md guides and the module header updated.

Note (pre-existing, not introduced here)

A category node rejects via update_node_avps/2 with
category_nodes_are_immutable on the solo path but permanent_node_immutable
through mutate/1 (same divergence already present for
retire_node/unretire_node in mutate/1). Left as-is; can be normalized in
a follow-up if desired.

🤖 Generated with Claude Code

david-w-t and others added 8 commits June 25, 2026 21:13
Brainstormed design for graphdb_mgr:update_node_avps/2 (the last
not_implemented stub of its pair): merge/upsert semantics with a
value-less-map delete signal (no new symbol; value => undefined stays a
real declared-but-unbound entry, preserving slice C), order-preserving
in-place upsert to honor the name-AVP-at-head convention, the three-tier
seam layering (tier-1 in-txn primitive + tier-2 wrapper + mutate/1
grammar entry), guard placement under the no-gen_server-call-in-txn
invariant, and the test plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
Four-task subagent-driven plan: pure AVP merge/validate helpers, tier-1
in-txn primitive + tier-2 wrapper (replacing the not_implemented stub),
the {update_node_avps, Nref, AVPs} mutate/1 grammar kind, and docs/TASKS
status. Grounded in the real mutate/1 machinery and the set_retired
pattern; all four CT registration points and the clause-terminator flips
are spelled out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
apply_avp_updates/2 (order-preserving upsert + value-less-map delete) and
validate_avp_updates/1, with EUnit coverage. Pure functions only; no
Mnesia. Foundation for the tier-1 primitive and mutate/1 grammar entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
Replace the not_implemented stub with the merge/upsert write op: tier-1
update_node_avps_in_txn/3 (node-existence, attribute-existence on upserts,
retired-marker guards; order-preserving merge) and the tier-2 wrapper
owning one transaction/1, mirroring set_retired. Client-side
well-formedness short-circuits before the gen_server call. Updates the
pre-existing category-guard test (nref 6 now hits the permanent-tier
guard). New update_avps CT group covers round-trip, delete, undefined-
retained, every guard, and single-call atomicity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
Add {update_node_avps, Nref, AVPs} as a fourth batch mutation kind:
validate_mutation runs the pure well-formedness + permanent-tier guards in
phase 1; dispatch composes update_node_avps_in_txn/3 with the RetAttr
already resolved by run_mutations. CT covers single, mixed-with-add_rel,
whole-batch rollback, and static malformed rejection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
Narrow the not_implemented note to delete_node, document update_node_avps
in the graphdb_mgr guides, add the fourth mutate/1 grammar kind, and mark
slice B IMPLEMENTED in TASKS.md (trimming it from the mutate/1 deferred
grammar-extension list).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
Add mutate_update_avps_not_found CT case exercising the tier-1
mnesia:abort(not_found) branch via the batch path (the solo path
short-circuits at the category guard). Reword the delete_node handle_call
comment so it no longer implies update_node_avps is unimplemented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
Record the pre-existing low-priority follow-up surfaced by slice B's final
review: a category node rejects with category_nodes_are_immutable on the
solo update_node_avps path but permanent_node_immutable through mutate/1
(same as retire/unretire). Both refuse; only the reason atom differs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF

@david-w-t david-w-t left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

@david-w-t david-w-t merged commit ea9853e into davidwt-com:main Jun 26, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant