Skip to content

fix: Body-ledger @annotation reification + update-clause annotation minting#1383

Open
bplatz wants to merge 4 commits into
mainfrom
fix/body-ledger-annotation-reification
Open

fix: Body-ledger @annotation reification + update-clause annotation minting#1383
bplatz wants to merge 4 commits into
mainfrom
fix/body-ledger-annotation-reification

Conversation

@bplatz

@bplatz bplatz commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes silent @annotation data-model corruption and unblocks minting annotations through update transactions. Three related issues, surfaced from a body-ledger bug report and a follow-on audit:

  1. Body-ledger annotation drop (reported bug). Supplying the target ledger in the request body instead of the URL path silently dropped @annotation reification — the annotation expanded into a dangling literal predicate with no reification bundle and no error. The annotation-lowering pass had its own envelope detector (is_envelope) that only tolerated @context/@graph/opts, so a stray top-level ledger key made it misclassify the envelope as a data node and skip @graph.

  2. Reserved-key drift. Four independent stages each hard-coded their own notion of "reserved top-level transactor key" and had diverged. Consolidated into a single canonical RESERVED_TXN_KEYS / CLAUSE_KEYS in parse/mod.rs, sourced by all four sites, with unit sync-guards that fail if any detector drifts. This also fixed a latent single-object-form leak (a body-form ledger leaking as a stray predicate).

  3. Update-clause annotations no-opped. Inline @annotation in an UPDATE insert clause silently dropped the entire clause: synthesized f:reifies* sibling nodes were flushed onto the top-level wrapper, rewrapping {where, insert, …} into {@graph: […]} so parse_update then found no clauses. Fixed by scoping sibling attachment per clause.

Changes

  • parse/mod.rs — canonical RESERVED_TXN_KEYS / CLAUSE_KEYS.
  • parse/jsonld.rsstrip_opts_for_expansion strips the full reserved set (fixes single-object leak).
  • parse/edge_annotations.rsis_envelope / is_transaction_wrapper source the shared set; attach_siblings scopes synthesized siblings per clause and lifts reserved keys out of the rewrapped node.
  • Docs: worked update-mint example (concepts) + per-clause sibling invariant (design).

Coverage

An audit of the annotation surface (operation × surface × value-type × identity × cardinality) found no functional holes in the JSON-LD add/update/delete lifecycle. New tests pin the high-value, previously-unpinned capabilities:

  • body-ledger annotation: envelope + single-object forms (no stray routing-key predicate)
  • update insert clause: constant, WHERE-bound, anonymous, explicit-@id, parallel, literal/typed
  • editing an anonymous annotation's metadata via inline @annotation WHERE binding
  • selector delete of an anonymous annotation
  • unit sync-guards for the reserved-key set

Scope boundaries (pre-existing, unchanged)

  • Inline named-graph annotation queries remain outside the read-side correctness envelope (the write works).
  • SPARQL UPDATE annotations are default-graph only; @reifies minting is deferred; upsert is not a parse_update clause.

Validation

fluree-db-transact (263) · grp_misc edge annotations (78) · grp_transact (159) · grp_query_sparql (233) all green. Clippy clean on changed targets; cargo fmt --all applied.

bplatz added 4 commits June 26, 2026 16:21
…eify

@annotation reification was silently dropped when the target ledger was
supplied in the request body (vs the URL path). The annotation lowering
pass has its own envelope detector (is_envelope) which only tolerated
@context/@graph/opts, so a stray top-level `ledger` key misclassified the
document as a data node-map, skipped @graph during lowering, and let
@annotation expand into a dangling literal predicate with no reification.

Three independent stages each hard-coded their own notion of "reserved
top-level transactor key" and had drifted apart:
- strip_opts_for_expansion (jsonld) stripped only opts/txn-meta
- is_envelope / is_transaction_wrapper (edge_annotations) each carried a
  different ad-hoc key list

Introduce a single canonical RESERVED_TXN_KEYS / CLAUSE_KEYS in parse/mod
and source all four sites from it. Also strip the routing/dataset keys
(ledger, from, ...) before expansion so the single-object body form no
longer leaks them as stray predicates, and lift reserved keys out of the
node in attach_siblings' rewrap path (where lowering otherwise buries a
body-form `ledger` inside @graph, out of the strip's reach).

Tests: two unit sync-guards that fail if any detector drifts from the
canonical set, plus integration regressions for the envelope and
single-object body-ledger forms.
Inline @annotation in an UPDATE insert/upsert clause silently no-opped the
entire clause: attach_siblings flushed the synthesized f:reifies* nodes onto
the top-level wrapper, rewrapping the whole {where, insert, ...} document
into {@graph: [...]} so parse_update then found no insert clause at all.

Scope the sibling accumulator to each clause: take it empty before recursing,
attach whatever that clause synthesized back onto the clause's own value, and
restore the outer accumulator. next_anon_id stays shared so blank-node ids
don't collide across clauses. No parser change is needed — the resulting
{@graph: [base, ...siblings]} clause value flows through the same
parse_expanded_triples_with_ctx path the insert/upsert envelope already uses.
Add integration coverage for minting annotations through an UPDATE insert
clause now that per-clause sibling scoping makes it work:
- constant template gated by WHERE
- WHERE-bound subject/object (one annotation per solution, no blank-node
  collision)
- anonymous (blank-node) annotation
- combined delete-clause retract + insert-clause mint in one transaction

Document the WHERE/INSERT mint form in the concepts doc (the surface was
already claimed but had no worked example) and record the per-clause
sibling-attachment invariant in the design doc so the top-level-flush
no-op isn't reintroduced.

Scope notes for reviewers: an `upsert` clause inside an update is not a
parse_update feature (only where/delete/insert/values), and inline
named-graph annotation *queries* remain outside the read-side correctness
envelope — both are pre-existing and orthogonal to this change.
A coverage audit of the annotation surface found the behavior complete but
several capabilities unpinned by tests. Lock in the high-value ones, all
verified working:

- update insert clause: explicit-@id annotation
- update insert clause: parallel annotations (repeated nodes)
- update insert clause: literal-valued (typed) edge
- editing an ANONYMOUS annotation's metadata by binding its subject through
  the inline @annotation WHERE form, then delete/insert by that variable —
  the only route to mutate an annotation with no user-assigned @id
- selector delete (no @id) retracting an anonymous annotation

No functional holes were found in add/update/delete across anonymous vs
explicit-@id, ref vs literal objects, single vs parallel, default graph.
@bplatz bplatz requested review from aaj3f and zonotope June 26, 2026 21:18
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