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
statuschange or an archive — is not applied; it is staged as a proposal against the current version; -
a creation answers
proposed: truewith aproposalobject: the entity staysdraft, 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),valuethe position adopted; - IF —
key: "interface.<wire-name>",valuethe 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_extrasnever 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
ADRstaysproposed; to adopt the decision, write a native one.