upsert_composition
Create or republish a composition (a named, published view) at `slug`, writing the node and its `composition_focuses_node` edges in ONE atomic commit. Use this instead of create_node/update_node for any composition write, because `rev` is DERIVED: it is re-read inside the write and incremented only on a transition into `published`, so update_node({ properties: { rev: N } }) writes whatever number you hold and is silently wrong. Reads stay generic: list_nodes({ type: "composition" }) enumerates views and `status` filters them by lifecycle, and the id IS the slug so get_node({ id: slug }) returns the view and its focus edges together. Pass `rev` to make the write conditional on the revision you last read; a mismatch refuses with `stored_rev` and leaves the file byte-unchanged rather than overwriting a print you never saw. Omitting `members` PRESERVES the stored arrangement (so withdrawing or renaming a view keeps what it looked like) while `[]` clears it. A `focus_node_ids` entry that does not resolve here is dropped rather than written as a dangling edge. To withdraw a view, write lifecycle "archived" rather than deleting it, so old links resolve to something honest.
Arguments
slugstringrequiredThe slug, which IS the node id. A composition is addressed by what appears in its URL, so no surrogate id is minted. Reusing an existing slug republishes that view.
titlestringrequiredDisplay title of the view.
descriptionstringoptionalWhat this view is for. Omit to leave any stored description alone.
lifecyclestringrequiredWhere the view stands: "draft" while it is being arranged and has never been live, "published" once it resolves at its slug, "archived" when withdrawn but kept so old links resolve. Written to the node's `status`, using the phases the spec declares for a composition. "retired" is a DEPRECATED alias for "archived" and is accepted, stored as "archived". Only a write with "published" increments `rev`.
focus_node_idsarrayoptionalNodes this view is ABOUT, written as `composition_focuses_node` edges. This is what makes "which published views show this persona?" answerable to a tool that cannot parse the URLs of whichever tool published the view. An empty set is valid: a view scoped by query rather than enumeration focuses nothing in particular. Ids that do not resolve here are dropped.
membersarrayoptionalThe frozen block arrangement. OMIT to leave the stored arrangement untouched; pass [] to clear it. The two are different instructions.
member_queryobjectoptionalA declarative, portable SELECTION over the graph: which nodes the view shows. Selection only; what it looks like is `presentation`. `clauses` is authoritative and the named fields are a positive-only shorthand for it, so a reader that finds `clauses` uses it and ignores the named fields. Holds no node references except `classified_as`, and relative selections walk from the focused set via `from_focus` rather than naming ids.
presentationobjectoptionalAdvisory rendering intent. A consumer MAY ignore every field here and stay conformant, because every default it then applies is the safe one.
published_bystringoptionalPublisher handle or email. A display scalar.
revintegeroptionalThe revision you last read, as an optimistic PRECONDITION. Never the value written: the stored revision is re-derived inside the write, so two publishers racing produce N+1 then N+2. Supplying it turns "I am publishing" into "I am publishing what I saw at rev N", and a mismatch returns status "stale_revision" with `stored_rev`. Omit to publish unconditionally.
Returns
{ status: 'ok', composition }{ status: 'stale_revision', stored_rev, reason } when a supplied rev did not match, { status: 'conflict', reason } when the file moved under us or the slug belongs to a node of another type, { status: 'not_found' } when no graph is loaded.Warnings
`rev` is never written from the argument. Supplying it makes the write conditional (publish what I saw at rev N); a mismatch refuses and the file is left BYTE-UNCHANGED. Omitting `members` PRESERVES the stored arrangement, while `[]` clears it. A `focus_node_ids` entry that does not resolve in this graph is dropped rather than written as a dangling edge.