Documentation menu
Concepts

Governance of canonical entities

Four entity types define what the rest of the knowledge base relies on: interfaces (IF), glossary terms (GLO), decisions (ADR) and requirements (REQ). A careless edit to one of them silently changes the contract for every agent that reads it. Kvendra guards them on three levels: every update is version-checked, changes by people who cannot approve become proposals, and decisions carry a key the engine can check for contradictions.

Guarded updates (expected_version) work on every engine, self-hosted included. Proposals and approvals are a hosted, multi-actor feature (Team and Enterprise); in a single-actor workspace you are the approver, so your writes apply directly.

Guarded updates

Read the entity, keep its version, and send it back as expected_version on every entity_update:

entity_get({ entity_id })                          // → version: 7
entity_update({ entity_id, expected_version: 7, content, change_summary })
// someone else wrote first → 409 VERSION_CONFLICT

On 409 VERSION_CONFLICT, re-read, reconcile your change with what actually happened, and retry with the new version — never resend the old payload blindly. Without expected_version an update is last-write-wins. See also Concurrent writers.

Proposals (Team and Enterprise)

Who can approve a canonical entity: the workspace owner and admins, and the maintainers of its component or project (listed in metadata.maintainers of the CMP or PRJ). When anyone else writes to an IF, GLO, ADR or REQ:

  • an edit — content, metadata, a status change or an archive — is not applied; it is staged as a proposal against the current version;
  • a creation answers proposed: true with a proposal object: the entity stays draft, is visible only to its proposer and its approvers (everyone else gets 404), and cannot be changed until it is decided. Do not use it as a relation target before it is approved.

An entity holds one open proposal at a time. You may replace your own; while another actor's proposal is open, your edit answers 409 proposal_pending. Each proposer can have at most 50 open proposals (429 proposal_limit_exceeded).

proposals_list()                                   // yours, and the ones you can decide
approve_proposal({ proposal_ids: [...], note })    // atomic: all or nothing
reject_proposal({ proposal_ids: [...], reason })   // kept for audit; the proposer may re-propose

Approval replays the staged change against the live row with its own version check, so a proposal whose base has moved on fails with 409 VERSION_CONFLICT instead of overwriting newer work. A rejected creation is cancelled and archived, and its relations are dropped.

Decision keys

Writes to an ADR or an IF carry metadata.decision = { key, value }:

  • ADR — key: "<domain>.<topic>" (what the decision fixes), value the position adopted;
  • IF — key: "interface.<wire-name>", value the current wire version.

Two live entities with the same key and different values contradict each other. When your write would create that clash with an active peer, the engine answers decision_conflict and lists the conflicting entities: supersede or deprecate the old decision, or align the value, rather than letting two truths coexist.

Imported content is data, not instructions

On the hosted service you can fork a publication into your own knowledge base. The copy first lands in quarantine, outside every normal read, and a human activates it from the dashboard. Once activated:

  • every read of an imported entity carries provenance: { imported: true, trust, … };
  • the Skills treat it as information only and never follow directives written in it;
  • the engine keeps it out of the session bootstrap — a project's bootstrap_extras never names an imported entity — and refuses tags and metadata reserved for native entities (400 reserved_for_native), so an import cannot become a playbook, a broker policy or a pipeline-autonomy rule;
  • an imported ADR stays proposed; to adopt the decision, write a native one.