Skip to documentation

Module references

Module reference / Govern it

WarrantGovern & certify

Warrant

Authority tiers, lifecycle, and review gates make knowledge and consequential actions defensible.
  • Atlas

    Govern concepts, relations, and ontology changes.

  • Lens

    Constrain what retrieval may reveal and what answers may state.

  • Ports

    Suspend consequential agent tools for human approval.

Warrant is the module that answers who vouches for this, and who is allowed to change it. Adopt it alone if you already have concepts or documents somewhere and you need a review queue, an authority tier, an owner, and a trail you can defend.

What it does

Warrant tracks four things about a concept: its authority tier (machine_extractedasserted curatedcertified), its lifecycle (draft, pending_review, published, deprecated, superseded), its owner and stewards, and its review clock. Destructive or schema-level edits do not happen in place: they are proposed as change requests that a reviewer approves, rejects, or that the proposer withdraws. Every accepted change appends an immutable revision carrying a full snapshot and a field-level diff. Documents get a parallel, simpler treatment — a verification stamp with a re-verify due date.

Nothing in this module calls a model. All of it is transactional database work inside a workspace transaction.

Use it standalone

  1. Issue a key with the scopes Warrant actually checks

    Reads (the change-request queue, the revision trail) require kb:read. Every mutation — propose, approve, reject, withdraw, certify, review, set authority, set owner, deprecate, verify a document — requires kb:write. There is no governance:* scope; the scope taxonomy is agents:run, runs:read, kb:read, kb:write, data:read, data:run, reports:read, reports:write.

    Delegated user keys (eli_uk_…) are additionally narrowed by the owner's live workspace role: these routes name the capability governance:read or governance:write, and a 403 insufficient_capability comes back if the owner has lost it. Workspace keys (eli_sk_…) carry no role, so the capability check does not apply to them.

  2. Install two packages and import one entry

    Headless: @eli-ai/client/governance. React: @eli-ai/react/governance. Neither pulls in query, agents, data, reports, connectors or evals code.

  3. Call it

    The client is a thin typed wrapper over /api/v1. The transport takes an origin, a credential resolver and an optional workspace id; it makes no authorization decision of its own.

install (headless only)bash
npm install @eli-ai/client @eli-ai/contracts
propose → approve → read the trailts
import { createEliTransport } from "@eli-ai/client/core";
import { createGovernanceClient } from "@eli-ai/client/governance";

const transport = createEliTransport({
  baseUrl: "https://eli.ai",
  apiKey: () => process.env.ELI_API_KEY!, // eli_sk_… or eli_uk_…
  // workspaceId is only needed for a user key that can reach several workspaces
});

const governance = createGovernanceClient(transport);

// 1 — propose a gated edit instead of writing it directly
const proposal = await governance.proposeChangeRequest({
  kind: "update",
  entityId: "01KZ40AAAAAAAAAAAAAAAAAAAA",
  payload: { description: "Recurring revenue retained from existing customers over 12 months." },
  note: "Aligning the definition with the FY26 board deck",
});

// 2 — a reviewer applies it. Approve returns the request AND the entity it landed on.
const { request, appliedEntityId } = await governance.approveChangeRequest(proposal.id, {
  note: "Matches the finance definition",
});

// 3 — the append-only trail, newest first
const { revisions } = await governance.listEntityRevisions(appliedEntityId!, { limit: 10 });
console.log(request.status, revisions[0]?.changeKind, revisions[0]?.diff);

What Warrant does NOT require

No agent runtime, no retrieval or reranker configuration, no data connector, no report or eval setup, and no model provider — none of these code paths perform an LLM, network or file call. It does need somewhere for the subject to exist: a change request of kind update, deprecate, supersede or delete must name an entity that is live in the workspace (404 otherwise), and POST /documents/{id}/verify needs a live document. Kind create is the one path that needs no pre-existing row — it proposes the concept and creates it on approval.

React components

The React package exports exactly one component for this module, plus its two types. EliGovernance is a client component ("use client") that renders the open change-request queue and, optionally, a proposal form. It uses scoped eli-* class names and ships no styling of its own — import @eli-ai/react/styles.css or provide your own rules for those classes.

EliGovernance

What it renders, top to bottom:

  • a titled surface with a Propose change / Cancel toggle button (only when canPropose);
  • when toggled open, a form with a kind select (create, update, deprecate, supersede, delete), an Entity ID input (required for every kind except create), a Review note textarea, and a Proposed fields (JSON) textarea that must parse to a JSON object — an array, a scalar or invalid JSON is rejected inline before any request is sent;
  • one card per open request: a kind badge (critical tone for delete, warning otherwise), the entity name — falling back to the entity id, then to "New concept" — the created date, the request note, the names of the keys in payload, and the proposer;
  • when canReview, a per-request note input plus Approve and Reject buttons; a non-empty note is sent as the request body, an empty one is omitted;
  • loading, error (with a retry button) and empty states.

It calls four operations only: listChangeRequests({ status: "open" }) on mount and after every successful mutation, proposeChangeRequest, approveChangeRequest and rejectChangeRequest. The status filter is fixed to open and is not a prop.

EliGovernance props

labelsPartial<EliGovernanceLabels>Per-string overrides merged over the defaults. See the labels table below for the full key set.
canReviewbooleanRenders the per-request note input and the Approve/Reject buttons. Defaults to true.
canProposebooleanRenders the Propose change toggle and its form. Defaults to true.
onRequestReviewed(requestId: string, decision: "approved" | "rejected") => voidFired after an approve or reject resolves successfully, once the queue has been refetched.
onRequestProposed(request: ChangeRequest) => voidFired after a proposal resolves, with the created request. The form is reset and closed first.
titlestringOverrides the surface heading. Falls back to labels.title.
descriptionstringOverrides the sub-heading. Falls back to labels.description.
loadingLabelstringText next to the spinner. Defaults to "Loading governance queue…".
errorLabelstringText in the queue error state. Defaults to "The governance queue could not be loaded.".
onError(error: unknown) => voidCalled for the queue read and for every mutation failure.
classNamestringAppended to the root element, which always carries "eli-root".

emptyLabel is deliberately absent

EliGovernanceProps is Omit<CommonSurfaceProps, "emptyLabel"> — the empty state is driven by labels.empty instead. Passing emptyLabel is a type error.

EliGovernanceLabels — every key, with its default

titlestring"Governance queue"
descriptionstring"Review proposed knowledge changes before they alter the canonical graph."
openstring"Open"
proposedBystring"Proposed by"
approvestring"Approve"
rejectstring"Reject"
proposestring"Propose change"
cancelstring"Cancel"
kindstring"Change type"
entityIdstring"Entity ID"
notestring"Review note" — used for both the proposal note and the per-request review note
payloadstring"Proposed fields (JSON)"
submitstring"Submit proposal"
emptystring"No open change requests"
provider wiring + the componenttsx
import { createEliTransport } from "@eli-ai/client/core";
import { EliProvider } from "@eli-ai/react/provider";
import { EliGovernance } from "@eli-ai/react/governance";
import "@eli-ai/react/styles.css";

// EliProvider has no API-key prop by design. In the browser, point the transport at a
// same-origin route of your own that injects the Bearer key server-side.
const transport = createEliTransport({ fetch: sameOriginProxyFetch });

export function ReviewQueue({ workspaceId, isSteward }: { workspaceId: string; isSteward: boolean }) {
  return (
    <EliProvider transport={transport} workspaceId={workspaceId}>
      <EliGovernance
        canReview={isSteward}
        labels={{ title: "Change review", empty: "Nothing waiting on you" }}
        onRequestReviewed={(id, decision) => track("cr_reviewed", { id, decision })}
        onError={(error) => report(error)}
      />
    </EliProvider>
  );
}

Never ship a workspace key to the browser

eli_sk_ keys are workspace-wide. EliProvider takes a transport, not a credential, precisely so the key stays server-side — see the SDK page for the proxy pattern.

What the React surface does not cover yet

EliGovernance has no UI for withdraw, for certify / review / set authority / set owner / deprecate, for the revision trail, or for document verification, and it cannot list anything other than open requests. All of those are available on the headless client (createGovernanceClient) and over HTTP — build your own markup with useEliQuery / useEliMutation from @eli-ai/react/hooks if you need them in React.

HTTP API

Twelve operations. Every one authenticates with a Bearer key; the scope column below is what the route enforces, and delegated user keys additionally need the named capability. All examples use https://eli.ai with -L on non-GET calls, because the apex issues a 308 redirect that curl will not follow for a POST without it.

Warrant — change request flow

The change-request state machine. Approve is claim-then-apply across transactions: the row flips to approved first, the edit runs second, and applied_at is stamped only after that commits. A failed apply reverts the row to open; a crash between the two leaves approved with a null applied_at, which a later approve resumes idempotently.

Rendering diagram

Downloads

Concepts

  • Change request flow
  • Modules
  • Warrant

Keywords

  • applied_at still NULL
  • stamp applied_at
  • revision activity change_request:id
  • audit change_request.approve
  • POST /change-requests
  • status: open
  • owner / steward gate
  • status → approved
  • apply the edit
  • apply throws
  • status: rejected
  • status: withdrawn
  • POST /change-requests/:id/approve
  • POST /change-requests/:id/reject
  • POST /change-requests/:id/withdraw
  • reviewer
  • claim
  • commit
Source and generation provenance

Status: current

Generated at: 2026-08-17T18:41:49.318Z

Source hash: 83bc93c5a053b8d8852d08f551771b1f9ca93f0a71a3776a31052956966b4211

Metadata payload hash: 80b54e3ec8335d60685212c14f5f219f57b4ef1925459dbd05f64589c100703f

Canonical appearance

src/app/(docs)/docs/modules/warrant/page.tsx:49 route /docs/modules/warrant

All appearances

  • canonicalsrc/app/(docs)/docs/modules/warrant/page.tsx:49 route /docs/modules/warrant

No mirrored appearances.

Generation versions

App: eli-ai 0.1.0

Mermaid: 11.16.0 · Mermaid CLI: 11.16.0

Node: v26.3.1 · Yarn: 4.17.1

Renderer config hash: 68c10966fe84406ee626034d58bfabd555df9f65f691204b7c46db24038da101

Renderer theme hash: c80287a78d80ad63d27bd5ca348b2ef9a7e2f44da289e436be6484ea28a1b033

Adapter versions: diagramGenerator=2, drawioFlowchart=1, drawioGantt=1, drawioSequence=1, drawioState=1

Full sidecar JSON: warrant-change-request-flow-83bc93c5.json

Change requests

GET/api/v1/change-requestsBearer · kb:read · governance:read

The change-request queue, newest first (created_at desc, then id desc). Rows are joined to the target entity so entityName comes back with them.

Query parameters

statusstringFilter to one of open | approved | rejected | withdrawn. Any other value is a 400 invalid-params. Omit for all.
Example requestbash
curl -s https://eli.ai/api/v1/change-requests?status=open \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{
  "requests": [
    {
      "id": "01KZ41BBBBBBBBBBBBBBBBBBBB",
      "entityId": "01KZ40AAAAAAAAAAAAAAAAAAAA",
      "kind": "update",
      "payload": { "description": "Recurring revenue retained from existing customers over 12 months." },
      "status": "open",
      "proposedBy": "usr_01H8…",
      "note": "Aligning the definition with the FY26 board deck",
      "reviewedBy": null,
      "reviewNote": null,
      "createdAt": "2026-07-21T09:00:00.000Z",
      "reviewedAt": null,
      "appliedAt": null,
      "entityName": "Net Revenue Retention"
    }
  ]
}
POST/api/v1/change-requestsBearer · kb:write · governance:write

Propose a change. Returns 201 with the created request. The payload is validated strictly per kind, so an unknown field is a 400 rather than a silent drop.

Request body

kindrequired"create" | "update" | "deprecate" | "supersede" | "delete"What is being proposed.
entityIdstringTarget concept. Required for every kind except create — omitting it is a 400, and an unknown or soft-deleted id is a 404.
payloadobjectKind-specific and strict. create: {name, type, aliases?, description?, lifecycle?: draft|published} — name and type required. update: {name?, type?, aliases?, description?} — at least one. supersede: {supersededBy, note?}. deprecate: {note?}. delete: {}. Defaults to an empty object, which only satisfies deprecate and delete.
notestringWhy you are proposing this, shown to reviewers. Max 4000 characters.
Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/change-requests \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "update",
    "entityId": "01KZ40AAAAAAAAAAAAAAAAAAAA",
    "payload": { "description": "Recurring revenue retained from existing customers over 12 months." },
    "note": "Aligning the definition with the FY26 board deck"
  }'
Responsejson
{
  "id": "01KZ41BBBBBBBBBBBBBBBBBBBB",
  "entityId": "01KZ40AAAAAAAAAAAAAAAAAAAA",
  "kind": "update",
  "payload": { "description": "Recurring revenue retained from existing customers over 12 months." },
  "status": "open",
  "proposedBy": "usr_01H8…",
  "note": "Aligning the definition with the FY26 board deck",
  "reviewedBy": null,
  "reviewNote": null,
  "createdAt": "2026-07-21T09:00:00.000Z",
  "reviewedAt": null,
  "appliedAt": null,
  "entityName": "Net Revenue Retention"
}
POST/api/v1/change-requests/{id}/approveBearer · kb:write · governance:write

Approve and apply. The response carries both the closed request and the entity the change landed on — for kind create that id did not exist before the call.

Request body

notestringReview note stored on the request as reviewNote. Max 4000 characters. This field is named note, not reviewNote.
Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/change-requests/01KZ41BBBBBBBBBBBBBBBBBBBB/approve \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"note": "Matches the finance definition"}'
Responsejson
{
  "request": {
    "id": "01KZ41BBBBBBBBBBBBBBBBBBBB",
    "entityId": "01KZ40AAAAAAAAAAAAAAAAAAAA",
    "kind": "update",
    "payload": { "description": "Recurring revenue retained from existing customers over 12 months." },
    "status": "approved",
    "proposedBy": "usr_01H8…",
    "note": "Aligning the definition with the FY26 board deck",
    "reviewedBy": "usr_01J2…",
    "reviewNote": "Matches the finance definition",
    "createdAt": "2026-07-21T09:00:00.000Z",
    "reviewedAt": "2026-07-21T09:05:00.000Z",
    "appliedAt": "2026-07-21T09:05:00.000Z",
    "entityName": "Net Revenue Retention"
  },
  "appliedEntityId": "01KZ40AAAAAAAAAAAAAAAAAAAA"
}

The review-note field is `note`

Approve and reject both validate { note?: string }. Body validation strips unknown keys, so sending reviewNote is accepted with a 200 and the note is silently discarded. Send note; read it back as reviewNote on the request object.

Who may approve, and what a 409 means here

If the target entity has an ownerId or a non-empty stewardIds, only those identities may approve — anyone else gets 409 conflict with "Not a reviewer". With neither set, any caller who passes the scope check may approve.

The acting identity is the user id for a delegated eli_uk_ key, and the API key id for a shared eli_sk_ workspace key. A workspace key therefore can never match an owner or steward: owned concepts must be approved with a user key belonging to a reviewer, or through the workspace UI.

A 409 also comes back when the request is no longer open — first reviewer wins, and a double apply is impossible.

POST/api/v1/change-requests/{id}/rejectBearer · kb:write · governance:write

Close an open request as rejected without applying it. 409 when the request is not open, 404 when it does not exist.

Request body

notestringStored as reviewNote. Max 4000 characters.
Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/change-requests/01KZ41BBBBBBBBBBBBBBBBBBBB/reject \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"note": "Superseded by the ontology rework"}'
Responsejson
{
  "id": "01KZ41BBBBBBBBBBBBBBBBBBBB",
  "entityId": "01KZ40AAAAAAAAAAAAAAAAAAAA",
  "kind": "update",
  "status": "rejected",
  "reviewedBy": "usr_01J2…",
  "reviewNote": "Superseded by the ontology rework",
  "reviewedAt": "2026-07-21T09:06:00.000Z",
  "appliedAt": null,
  "entityName": "Net Revenue Retention"
}
POST/api/v1/change-requests/{id}/withdrawBearer · kb:write · governance:write

Close an open request as withdrawn. Takes no request body. 409 when the request is not open.

Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/change-requests/01KZ41BBBBBBBBBBBBBBBBBBBB/withdraw \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{
  "id": "01KZ41BBBBBBBBBBBBBBBBBBBB",
  "entityId": "01KZ40AAAAAAAAAAAAAAAAAAAA",
  "kind": "update",
  "status": "withdrawn",
  "reviewedBy": "usr_01H8…",
  "reviewNote": null,
  "reviewedAt": "2026-07-21T09:07:00.000Z",
  "appliedAt": null,
  "entityName": "Net Revenue Retention"
}

What approval actually applies

create inserts the concept at authority curated and lifecycle published (or draft when the payload asks for it); a name collision with a live concept is a 409. update writes the fields and promotes authority to at least curated in one transaction — a promotion only, never a demotion of a certified concept. deprecate and supersede route through the same code as the deprecate endpoint below, and delete soft-deletes. Every one of them records the revision with activity change_request:<id>, which is how you trace an edit back to its proposal.

Concept authority & lifecycle

These five mutations all return the same CurationMeta object and all append a revision. They are the direct path — use them for the states a change request does not cover (authority, owner, review stamps), and use change requests for edits you want reviewed.

POST/api/v1/entities/{id}/certifyBearer · kb:write · governance:write

Owner/admin sign-off. Sets authority to 'certified', refreshes lastReviewedAt and nextReviewAt (now + the workspace reviewIntervalDays, default 180), bumps version, and writes a revision with changeKind 'certify'. Lifecycle is untouched.

Request body

notestringRecorded on the revision. Max 4000 characters.
Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/entities/01KZ40AAAAAAAAAAAAAAAAAAAA/certify \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"note": "Signed off by the finance data owner"}'
Responsejson
{
  "authority": "certified",
  "lifecycleStatus": "published",
  "ownerId": "usr_01J2…",
  "stewardIds": ["usr_01K4…"],
  "version": 8,
  "lastReviewedAt": "2026-07-21T09:10:00.000Z",
  "nextReviewAt": "2027-01-17T09:10:00.000Z",
  "supersededBy": null,
  "sourceRefs": [{ "label": "FY26 board deck", "url": "https://example.com/fy26" }],
  "editorialNote": null
}
POST/api/v1/entities/{id}/reviewBearer · kb:write · governance:write

Stamp a review without certifying. Refreshes lastReviewedAt and nextReviewAt; a machine_extracted concept is promoted to 'curated', higher tiers are left alone. Takes no request body.

Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/entities/01KZ40AAAAAAAAAAAAAAAAAAAA/review \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{
  "authority": "curated",
  "lifecycleStatus": "published",
  "ownerId": null,
  "stewardIds": [],
  "version": 4,
  "lastReviewedAt": "2026-07-21T09:12:00.000Z",
  "nextReviewAt": "2027-01-17T09:12:00.000Z",
  "supersededBy": null,
  "sourceRefs": [],
  "editorialNote": null
}
POST/api/v1/entities/{id}/authorityBearer · kb:write · governance:write

Set the authority tier explicitly, including a demotion. Setting the tier it already has is an idempotent no-op: no version bump, no revision, no audit row.

Request body

authorityrequired"machine_extracted" | "asserted" | "curated" | "certified"The new tier. Any other value is a 400.
Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/entities/01KZ40AAAAAAAAAAAAAAAAAAAA/authority \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"authority": "curated"}'
Responsejson
{
  "authority": "curated",
  "lifecycleStatus": "published",
  "ownerId": "usr_01J2…",
  "stewardIds": [],
  "version": 9,
  "lastReviewedAt": "2026-07-21T09:10:00.000Z",
  "nextReviewAt": "2027-01-17T09:10:00.000Z",
  "supersededBy": null,
  "sourceRefs": [],
  "editorialNote": null
}
POST/api/v1/entities/{id}/ownerBearer · kb:write · governance:write

Assign or clear the owner and optionally replace the whole steward list. This is what arms the reviewer gate on change requests for this concept.

Request body

ownerIdrequiredstring | nullThe owning identity, 1–200 characters, or null to clear. The key must be present; the reviewer gate compares this value against the acting identity, so use the same id space your user keys resolve to.
stewardIdsstring[]Replaces the steward list wholesale when present, de-duplicated. Up to 50 entries, each 1–200 characters. Omit to leave the existing list untouched.
Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/entities/01KZ40AAAAAAAAAAAAAAAAAAAA/owner \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ownerId": "usr_01J2…", "stewardIds": ["usr_01K4…"]}'
Responsejson
{
  "authority": "curated",
  "lifecycleStatus": "published",
  "ownerId": "usr_01J2…",
  "stewardIds": ["usr_01K4…"],
  "version": 10,
  "lastReviewedAt": "2026-07-21T09:10:00.000Z",
  "nextReviewAt": "2027-01-17T09:10:00.000Z",
  "supersededBy": null,
  "sourceRefs": [],
  "editorialNote": null
}
POST/api/v1/entities/{id}/deprecateBearer · kb:write · governance:write

Retire a concept. Without supersededBy the lifecycle becomes 'deprecated'; with it the lifecycle becomes 'superseded' and supersededBy points at the successor.

Request body

supersededBystringSuccessor concept id. It must be live, unmerged and published: 404 when it does not exist, 400 when it was merged away, is not published, or equals the target itself.
notestringRecorded on the revision. Max 4000 characters.
Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/entities/01KZ40AAAAAAAAAAAAAAAAAAAA/deprecate \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"supersededBy": "01KZ44CCCCCCCCCCCCCCCCCCCC", "note": "Replaced by the FY26 definition"}'
Responsejson
{
  "authority": "curated",
  "lifecycleStatus": "superseded",
  "ownerId": "usr_01J2…",
  "stewardIds": [],
  "version": 11,
  "lastReviewedAt": "2026-07-21T09:10:00.000Z",
  "nextReviewAt": "2027-01-17T09:10:00.000Z",
  "supersededBy": "01KZ44CCCCCCCCCCCCCCCCCCCC",
  "sourceRefs": [],
  "editorialNote": null
}

Only a published concept can be retired

Deprecate and supersede both require the target to be in lifecycle published. A draft or pending-review concept gets a 400 telling you to publish or delete it instead; a concept that is already deprecated or superseded gets a 409. There is no draft → deprecated edge in the state machine.

Revision trail

GET/api/v1/entities/{id}/revisionsBearer · kb:read · governance:read

The append-only history for one concept, newest first. Returns 404 for an unknown or deleted entity, so revisions of purged ids stay private.

Query parameters

limitinteger1 to 200, default 50. Anything else is a 400 invalid-params.
Example requestbash
curl -s "https://eli.ai/api/v1/entities/01KZ40AAAAAAAAAAAAAAAAAAAA/revisions?limit=2" \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{
  "entityId": "01KZ40AAAAAAAAAAAAAAAAAAAA",
  "revisions": [
    {
      "id": "01KZ45DDDDDDDDDDDDDDDDDDDD",
      "workspaceId": "ws_01H7…",
      "entityId": "01KZ40AAAAAAAAAAAAAAAAAAAA",
      "version": 8,
      "changeKind": "certify",
      "snapshot": {
        "name": "Net Revenue Retention",
        "type": "metric",
        "attrs": {},
        "authority": "certified",
        "lifecycleStatus": "published",
        "ownerId": "usr_01J2…",
        "stewardIds": ["usr_01K4…"],
        "aliases": ["NRR"],
        "sourceRefs": [],
        "supersededBy": null
      },
      "diff": { "authority": { "from": "curated", "to": "certified" } },
      "activity": null,
      "changedBy": "usr_01J2…",
      "note": "Signed off by the finance data owner",
      "createdAt": "2026-07-21T09:10:00.000Z"
    }
  ]
}

changeKind is one of create, update, review, certify, set_authority, set_owner, deprecate, supersede, delete, change_request, verify. activity records where the change came from — api for the authority and deprecate endpoints, and change_request:<id> for anything applied by an approval.

Document verification

POST/api/v1/documents/{id}/verifyBearer · kb:write · governance:write

Stamp a document. verified and certified record who verified it, when, and a re-verify due date (now + the workspace reviewIntervalDays); unverified and deprecated clear all three stamps. Audited as document.verify.

Request body

verificationrequired"unverified" | "verified" | "certified" | "deprecated"The new state. Any other value is a 400.
Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/documents/01KZ50EEEEEEEEEEEEEEEEEEEE/verify \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"verification": "certified"}'
Responsejson
{
  "docId": "01KZ50EEEEEEEEEEEEEEEEEEEE",
  "verification": "certified",
  "verifiedBy": "usr_01J2…",
  "verifiedAt": "2026-07-21T09:20:00.000Z",
  "nextVerifyAt": "2027-01-17T09:20:00.000Z"
}

Error envelope

Failures come back as { error, message } — plus issues on a body-validation 400. Across this module: 401 unauthorized (missing, invalid or revoked key), 403 insufficient_scope or insufficient_capability, 400 invalid_body / invalid_json / invalid-params / invalid, 404 not_found, and 409 conflict. One exception worth coding around: the document-verify route's own not-found reply is { "error": "Document not found" } with no message key. That branch also applies the caller's content ACL, so a document restricted away from the key looks identical to one that does not exist. There is no pagination envelope, idempotency key or rate limiter on these routes.

Data elements

Warrant owns two tables outright, owns a column group on two more, and appends to the audit log. Every one of the five carries a workspace_id and a <table>_tenant_isolation row-level-security policy whose USING and WITH CHECK clauses both require workspace_id to equal the workspace of the current transaction. There is no cross-workspace read path: a wrong-workspace id is simply invisible, which is why several routes return 404 where you might expect 403.

TableWarrant's roleColumns that matterWhat a row means
concept_change_requestsownsid · workspace_id · entity_id · kind · payload · status · proposed_by · note · reviewed_by · review_note · created_at · reviewed_at · applied_atOne proposed change. entity_id is null when kind is 'create' (a brand-new concept). status is open | approved | rejected | withdrawn. applied_at is stamped only after the apply commits, so approved + null applied_at is a detectable, resumable wedge.
entity_revisionsowns (append-only)id · workspace_id · entity_id · version · change_kind · snapshot · diff · activity · changed_by · note · created_atOne immutable revision per accepted change. snapshot holds name, type, attrs, authority, lifecycleStatus, ownerId, stewardIds, aliases, sourceRefs, supersededBy; diff is {field: {from, to}} against the previous snapshot. entity_id deliberately carries NO foreign key so history survives hard removal of the entity. Never UPDATEd or DELETEd.
entitiesreads + writes governance columnsauthority · lifecycle_status · owner_id · steward_ids · version · last_reviewed_at · next_review_at · superseded_by · source_refs · editorial_noteThe row itself belongs to the graph module; Warrant owns this column group. authority defaults to machine_extracted, lifecycle_status defaults to published, version defaults to 1. editorial_note is steward-only and is excluded from revision snapshots.
documentsreads + writes verification columnsverification · verified_by · verified_at · next_verify_atDocument-level verification. verification defaults to 'unverified'; setting verified or certified stamps the other three, setting unverified or deprecated clears them.
audit_logwritesid · workspace_id · actor_id · event · target · meta · created_atWarrant appends change_request.create / .approve / .reject / .withdraw, entity.certify / .review / .authority / .owner / .deprecate / .supersede / .lifecycle / .refs / .editorial_note, and document.verify.

Two details worth knowing before you build on these rows. First, editorial_note is steward-only: it is stored on the entity but deliberately excluded from the revision snapshot and diff, so it never travels into a history view. Second, entity_revisions.version is bumped with an atomic UPDATE … RETURNING that takes the entity row lock, so two concurrent mutations can never mint the same version number.

Feature map

Five feature areas make up the module. Each names the concrete modules and tables that deliver it, and the same names reappear in the code map and internals below, so every behavioural claim on this page is traceable to the code that enforces it.

Feature areaWhat it providesDelivered bySurfaces
Gated change through change requestsPropose, approve, reject, withdraw; strict per-kind payload validation; claim-then-apply approval that survives crashes.proposeChangeRequest / approveChangeRequest in src/server/graph/change-requests.ts; the concept_change_requests table/api/v1/change-requests routes · EliGovernance queue · governance SDK client
Direct curation mutationsCertify, review-stamp, set authority (including demotion), assign owner and stewards, deprecate or supersede — each one transaction returning CurationMeta.certifyEntity, reviewEntity, setEntityAuthority, setEntityOwner, deprecateEntity in src/server/graph/curation.tsthe five /api/v1/entities/{id}/* endpoints · headless governance client
Append-only revision trailOne immutable row per accepted change: full snapshot, field-level diff, actor, and the PROV activity channel that links an edit to its proposal.recordRevision in curation.ts; the entity_revisions table (no foreign key, so history survives entity removal)GET /api/v1/entities/{id}/revisions · listEntityRevisions on the SDK
Document verificationA human stamp on a document — verified or certified with a re-verify due date, unverified or deprecated clearing the stamps.verifyDocument in src/server/vault/verification.ts; the verification column group on documentsPOST /api/v1/documents/{id}/verify · verification distribution in the manifest
Reviewer gating, audit, and accessOwner/steward-only approval on owned concepts, scope plus capability checks on every route, and an audit event for every mutation.the reviewer gate in change-requests.ts; requireApiKey in src/app/api/v1/_lib; the audit_log table409 "Not a reviewer" on the wire · audit_log rows per event

System architecture

The module is three route groups over four services and five stores — and no model anywhere. Rectangles are API routes and services, cylinders are tables; the two append-only cylinders (entity_revisions, audit_log) are what make the trail defensible.

Warrant — system architecture

Change requests claim in concept_change_requests and apply through the same manual.ts and curation.ts writers the direct endpoints use, so every accepted change lands in entity_revisions and audit_log regardless of which track it took. Document verification is the parallel, simpler lane on the right.

Rendering diagram

Downloads

Concepts

  • Warrant architecture
  • Governance services
  • REST surface — src/app/api/v1
  • Modules
  • Warrant

Keywords

  • change-requests routes (API)
  • documents/:id/verify route (API)
  • change-requests.ts — propose, claim, apply (service)
  • curation.ts — authority, lifecycle, recordRevision (service)
  • store: table
  • document stamps (service)
  • store: append-only table
  • workspace-settings — reviewIntervalDays (service)
  • vault/verification.ts
  • manual.ts — the create, update, delete apply path (service)
Source and generation provenance

Status: current

Generated at: 2026-08-17T18:41:55.969Z

Source hash: 45a113726a4cbf5440690b372fec5c435b498e2596d7735a829f59d47641a2fc

Metadata payload hash: dbea75e50953c0942da6849fb9f84cd07b68b2946c5ba7a9235ff4f0c7b27fd7

Canonical appearance

src/app/(docs)/docs/modules/warrant/page.tsx:58 route /docs/modules/warrant

All appearances

  • canonicalsrc/app/(docs)/docs/modules/warrant/page.tsx:58 route /docs/modules/warrant

No mirrored appearances.

Generation versions

App: eli-ai 0.1.0

Mermaid: 11.16.0 · Mermaid CLI: 11.16.0

Node: v26.3.1 · Yarn: 4.17.1

Renderer config hash: 68c10966fe84406ee626034d58bfabd555df9f65f691204b7c46db24038da101

Renderer theme hash: c80287a78d80ad63d27bd5ca348b2ef9a7e2f44da289e436be6484ea28a1b033

Adapter versions: diagramGenerator=2, drawioFlowchart=1, drawioGantt=1, drawioSequence=1, drawioState=1

Full sidecar JSON: warrant-system-architecture-45a11372.json

How to read the Warrant system architecture

  1. Two tracks, one writer set: Change-request approvals and the direct governance endpoints both mutate entities through manual.ts and curation.ts, so the revision and audit guarantees hold on both paths.
  2. Follow the settings dependency: Review and verification due dates derive from the workspace reviewIntervalDays, read before any transaction opens.
  3. Note the column-group ownership: The entities and documents rows belong to other modules; this module owns their governance and verification column groups and is their only writer.
  4. Check the append-only pair: entity_revisions and audit_log are written inside the same transaction as the mutation they record and are never updated or deleted.
Trust boundary
Every route guards on kb:read or kb:write plus the governance:read or governance:write capability for delegated keys; owned concepts additionally gate approval on the owner and steward identities.
Durable state
concept_change_requests with review metadata, append-only entity_revisions and audit_log, and the governance and verification column groups on entities and documents.

Failure paths

  • An apply that throws reverts the claimed request to open
  • A crash between claim and apply leaves approved with null appliedAt — resumable, not wedged
  • A second concurrent approval loses the claim and returns 409
  • Deprecating a non-published concept is rejected with a 400

Signals

  • Open change-request queue depth and age
  • approved rows with null appliedAt (crash wedges)
  • Authority-tier distribution over time
  • Documents past their next_verify_at date

Where the logic lives

The code map is the module boundary in file terms. Every path is real; table names are stated in the notes because tables are defined in the schema module, not in files of their own.

ComponentKindLives atNotes
Change-request queue routesAPIsrc/app/api/v1/change-requestsGET the queue (status filterable), POST a proposal; kb:read / kb:write plus governance capabilities.
Review action routesAPIsrc/app/api/v1/change-requests/[id]approve, reject, and withdraw live as subroutes; approve returns the request plus appliedEntityId.
Certify endpoint (and its four siblings)APIsrc/app/api/v1/entities/[id]/certifyreview, authority, owner, and deprecate sit beside it under the same id segment; all return CurationMeta.
Revision trail routeAPIsrc/app/api/v1/entities/[id]/revisionsNewest-first history read; 404 for unknown or deleted entities so purged ids stay private.
Document verify routeAPIsrc/app/api/v1/documents/[id]/verifyApplies the caller's content ACL before stamping, so a restricted document 404s.
Change-request engineservicesrc/server/graph/change-requests.tsStrict zod payload schemas per kind, the reviewer gate, and the claim-then-apply state machine.
Curation engineservicesrc/server/graph/curation.tsAuthority rank order, lifecycle transitions, review stamps, and recordRevision — the single writer of entity_revisions.
Apply-path graph writesservicesrc/server/graph/manual.tsApproved create, update, and delete payloads land through the same functions the graph module's own API uses.
Document verificationservicesrc/server/vault/verification.tsValidates the tier enum, stamps or clears verified_by / verified_at / next_verify_at, audits as document.verify.
Workspace review settingsservicesrc/server/workspace-settingsreviewIntervalDays (default 180) drives nextReviewAt and nextVerifyAt; read before transactions open.
Governance tables and column groupsstoresrc/server/db/schema.tsconcept_change_requests and entity_revisions owned outright; governance columns on entities, verification columns on documents, plus audit_log appends.
API guardservicesrc/app/api/v1/_librequireApiKey resolves the key, checks scope membership, and re-checks live capabilities for delegated user keys.
EliGovernance surfaceUIpackages/react/src/governance/index.tsxThe open-queue review surface documented above; four operations only.
Governance clientSDKpackages/client/src/governance.tscreateGovernanceClient — the queue, the five curation mutations, revisions, and verify.
Wire contractsSDKpackages/contracts/src/governance.tsChangeRequest, CurationMeta, EntityRevision, DocumentVerification, and the tier unions.

Primary runtime flow

The highest-value flow is an approval, because it exercises the reviewer gate, the claim, the apply, and the trail in one call. Message labels use the operation names from the HTTP API section above.

Warrant — primary runtime flow

approveChangeRequest claims the row before applying and stamps appliedAt only after the apply commits. The apply itself runs through manual.ts and curation.ts, so the revision and audit rows are identical to what a direct edit would have produced — plus the change_request activity link.

Rendering diagram

Downloads

Concepts

  • Warrant runtime flow
  • Modules
  • Warrant

Keywords

  • POST :id/approve (API)
  • audit_log (store)
  • change-requests.ts (service)
  • concept_change_requests (store)
  • manual.ts + curation.ts (service)
  • entity_revisions (store)
  • append change_request.approve
  • Reviewer (SDK or curl)
  • entities (store)
  • append a revision with activity change_request:id
  • approveChangeRequest with an optional note
  • load the request, must be open
  • claim the row, status open to approved
  • apply the payload by kind
  • stamp appliedAt only after the apply commits
  • closed request plus appliedEntityId
  • guard kb:write and the governance:write capability
  • /api/v1/change-requests
  • reviewer gate — owner or steward only, when either is set
  • write the fields, promote authority to at least curated
Source and generation provenance

Status: current

Generated at: 2026-08-17T18:41:53.943Z

Source hash: eb6432fde77d55f982c084e22ea0fb29b05ef24382ab24d98c65b036bfaa6d0b

Metadata payload hash: ccaf6fc341f8c4abac579f2ac598923d3912bff32602a0ca2231a003b2e9e453

Canonical appearance

src/app/(docs)/docs/modules/warrant/page.tsx:97 route /docs/modules/warrant

All appearances

  • canonicalsrc/app/(docs)/docs/modules/warrant/page.tsx:97 route /docs/modules/warrant

No mirrored appearances.

Generation versions

App: eli-ai 0.1.0

Mermaid: 11.16.0 · Mermaid CLI: 11.16.0

Node: v26.3.1 · Yarn: 4.17.1

Renderer config hash: 68c10966fe84406ee626034d58bfabd555df9f65f691204b7c46db24038da101

Renderer theme hash: c80287a78d80ad63d27bd5ca348b2ef9a7e2f44da289e436be6484ea28a1b033

Adapter versions: diagramGenerator=2, drawioFlowchart=1, drawioGantt=1, drawioSequence=1, drawioState=1

Full sidecar JSON: warrant-primary-runtime-flow-eb6432fd.json

How to read the Warrant primary runtime flow

  1. The gate comes before the claim: When the target concept has an owner or stewards, only those identities may approve; a workspace key can never match one, so owned concepts need a user key.
  2. The claim is the concurrency control: Flipping open to approved before applying means a concurrent second approval loses the claim and gets a 409 — a double apply is impossible.
  3. The apply is ordinary graph code: Kind create inserts at curated authority, update writes fields with a promote-only floor, deprecate and supersede route through the same code as the direct endpoint, delete soft-deletes.
  4. appliedAt is the commit marker: It is stamped in a separate step after the apply transaction commits; approved with null appliedAt is a detectable crash wedge a later approve resumes.
Trust boundary
Scope kb:write plus the governance:write capability gate the route; the owner and steward identity check gates the decision itself.
Durable state
The closed request row with reviewer and notes, the applied entity state, one entity_revisions row with activity change_request:id, and the audit event.

Failure paths

  • Reviewer mismatch returns 409 with no state change
  • Payload that no longer applies (for example a name collision) reverts the claim to open
  • A crash after claim leaves approved plus null appliedAt for idempotent resume
  • Reject and withdraw close the row without touching the entity

Signals

  • Time from proposal to review decision
  • Approve versus reject ratio by kind
  • Revisions with change_request activity per week
  • 409 rate on approve (contention or gate misses)

Internals and invariants

Gated change through change requests

A proposal validates its payload against a strict per-kind schema — unknown fields are a 400, not a silent drop — and lands as an open row. Approval is claim-then-apply across transactions: the claim serializes reviewers, the apply runs through the ordinary graph writers, and the appliedAt stamp is deliberately last.

  • Invariant — A change request is applied at most once: the open-to-approved claim is the lock, a failed apply reverts to open, and appliedAt is stamped only after the apply commits, enforced in src/server/graph/change-requests.ts.
  • Invariant — When a concept has an owner or stewards, only those identities may approve or reject its requests (409 otherwise), enforced by the reviewer gate in src/server/graph/change-requests.ts.

Direct curation mutations

The five direct endpoints are one workspace transaction each: load the live entity, mutate the governance columns, record the revision, append the audit row. Review stamps derive their due date from the workspace review interval, which is read before the transaction opens.

  • Invariant — Applied updates promote authority to at least curated and never demote a certified concept; explicit demotion exists only on the authority endpoint, enforced in src/server/graph/change-requests.ts and src/server/graph/curation.ts.
  • Invariant — Only a published concept can be deprecated or superseded, and a supersession target must be live, unmerged, and published, enforced in src/server/graph/curation.ts.

Append-only revision trail

Every accepted change appends one revision carrying a full snapshot and a field-level diff against the previous one. The version number is minted with an atomic bump that takes the entity row lock, and the table deliberately has no foreign key to entities.

  • Invariant — Revision rows are never updated or deleted, and two concurrent mutations can never mint the same version number, enforced by recordRevision in src/server/graph/curation.ts.
  • Invariant — The steward-only editorial note is excluded from revision snapshots and diffs, so it never travels into a history view, enforced in src/server/graph/curation.ts.

Document verification

Verification is a single-row state machine on the document: verified and certified stamp who, when, and a re-verify due date; unverified and deprecated clear all three. The route applies the caller's content ACL first, so a restricted document is indistinguishable from a missing one.

  • Invariant — Setting verified or certified always stamps verified_by, verified_at, and next_verify_at together, and the other two states always clear them, enforced by verifyDocument in src/server/vault/verification.ts.

Reviewer gating, audit, and access

Every route resolves the bearer key to a workspace, checks scope by exact membership, and re-checks the live capability for delegated user keys. Every mutation appends an audit event named for what happened, and all reads and writes run under row-level security.

  • Invariant — Every governance mutation appends an audit_log event in the same transaction as the change it records, enforced across src/server/graph/curation.ts, src/server/graph/change-requests.ts, and src/server/vault/verification.ts.
  • Invariant — A cross-workspace id is invisible rather than forbidden: tenant RLS policies on every table turn it into a 404, enforced through src/server/db/tenant.ts.

For a self-hosting team the safe extension points are observation and policy, not the state machine: attach read-only policy observers, read stale-concept and curation summaries through the plugin facade, and drive markReviewed / setAuthority / deprecateEntity through it so every change still appends the same revisions. Adjusting the review cadence is a workspace setting. The claim-then-apply protocol, the promote-only floor, and the append-only trail are load-bearing and not configurable.

How it composes

Warrant is usable on its own — the state it records is meaningful whether or not anything reads it. What follows is what each neighbour adds, and what genuinely breaks without it.

With Atlas — the concepts being governed

Atlas owns the entities row; Warrant owns its governance columns. This is the one real dependency: a change request of kind update, deprecate, supersede or delete, and every one of the five authority/lifecycle endpoints, needs a live entity to point at. Approving a create request is how you get one without touching Atlas's write API at all — the entity is created by the approval, at authority curated.

With Lens — where the state changes answers

This is where governance stops being paperwork. Retrieval's graph-fusion stage excludes entities whose lifecycle_status is draft or pending_review, so drafting a concept keeps it out of answer context. On the document side, documents.verification is a direct input to the answer policy's confidence features — ranked unverified 0, verified 1, certified 2, and deprecated −1, so a deprecated source counts against the answer — and when no cited document is verified or certified the policy raises a no_verified_source caveat. Entity authority participates too: the feature set tracks whether any detected concept is certified. Without Lens, all of this is recorded and simply not consumed.

With Lineage — the trail, assembled

entity_revisions and the audit rows are Warrant's, and GET /api/v1/entities/{id}/revisions reads them without any Lineage code. Lineage is what stitches them together with document revisions, extraction runs and citations into a single provenance view. Note that createGovernanceClient also exposes documentLineage(id), which calls GET /api/v1/documents/{id}/lineage— that endpoint is Lineage's, not Warrant's, and is documented there.

With Intake — verification on ingested content

Connector-ingested documents arrive at the default verification of unverified. Warrant's verify endpoint is the human stamp on top of that. No connector is required to use it — any document row will do.

With Crucible — the policy decision log

GET /api/v1/policy/decisions and GET /api/v1/policy/stats are guarded by the same governance:read capability as this module's reads (accepting either kb:read or runs:read), and their wire types PolicyDecision / PolicyStats live in @eli-ai/contracts/governance. Their client methods, however, are on the quality entry — createQualityClient(transport).listPolicyDecisions() — so they are documented with Crucible. Adopt Warrant alone and you still get the write-side governance state; you do not get the decision log unless you also take that entry.

With Ports — two approvals that are not the same thing

Agent runs have their own human-in-the-loop approval at POST /api/v1/runs/{id}/approve, gated on agents:run. It has no relationship to the change-request queue: different table, different scope, different lifecycle. Do not wire one to the other expecting shared state. Separately, the SKOS/PROV-O export at GET /api/v1/export/skos is reachable from createGovernanceClient(transport).exportSkos() but guards on graph:read — it is a graph export whose contents are lifecycle-shaped (published, deprecated and superseded concepts only; drafts never export).

With Conduit

No coupling. Live-data connectors and governed queries neither read nor write any Warrant table, and Warrant needs nothing from them.

Going deeper

The reasoning behind the tiers, the two-track gating model and the review SLA is in Concept governance. For a curl-only walkthrough of the same loop see Curate & govern concepts, and for the in-product review queue see Curation & change management.

Extending Warrant

A plugin can attach a read-only policy observer policyObservers runs post-hoc on a recorded decision and can never change a verdict — and can read and write governance through the facade: staleConcepts, getCurationMeta and curationSummary under governance:read, and markReviewed, setAuthority and deprecateEntity under governance:write, each appending the same revision rows a human action would. Warrant emits entity.certified, entity.authority_changed, change_request.* and policy.decision.recorded. See the extension model for the contract and the trust boundary, and Build a plugin for a worked example.