Skip to documentation

Module references

Module reference / Trace it

LineageProvenance & traceability

Lineage

Append-only provenance traces source revisions through derived knowledge, answers, data calls, and agent decisions.
  • Intake

    Anchor every trail in an exact source revision.

  • Lens

    Record cited claims, retrieval context, and policy verdicts.

  • Ports

    Trace model calls, tool calls, approvals, and cost.

Lineage is the provenance-and-impact slice. It ships as one SDK entry — @eli-ai/react/documents over @eli-ai/client/documents — and one API area, /api/v1/documents. The API operation catalog files both under a single capability it labels Documents and lineage. This page is the adoption reference: what you import, what props exist, what the endpoints actually return, and which tables move. For the conceptual tour of the trail itself, read Data lineage.

What it does

Lineage answers one question about a document in a single read: where did it come from, what knowledge did it produce, and what breaks if it changes. The server assembles that from pure SQL aggregates — upstream origin and governance, an immutable revision summary, the document.* audit trail, the chunks and concepts the document derived, and the answers, cache entries, and live concepts that now depend on it. No model call, no network, no file I/O; deterministic ordering and bounded caps, so the same document yields the same payload every time.

The module also owns the document catalog itself — list, read, create, update, soft-delete — because that write path is what produces the trail: every save appends a revision row and an audit row inside the same transaction as the document update.

Use it standalone

  1. Mint a key with kb:read

    The lineage read and both catalog reads accept kb:read. The lineage read additionally accepts the legacy runs:read grant (it is an any-of guard). Add kb:write only if you also want to create, update, or delete documents. A workspace key (eli_sk_) carries its workspace, so no X-Workspace-Id header is needed; a user key (eli_uk_) that spans several workspaces must send one.
  2. Install two packages, import one module

    @eli-ai/client plus @eli-ai/contracts is enough for the headless path. Add @eli-ai/react only if you want the rendered surfaces. Contracts are imported by capability subpath, so a Lineage integration never pulls in agent, graph, or data types.
  3. Read a document, then read its trail

    createDocumentsClient(transport) exposes six methods: list, get, create, update, remove, and lineage.
minimum install (headless)bash
npm install @eli-ai/client @eli-ai/contracts
server-side: list, then tracets
import { createEliTransport } from "@eli-ai/client/core";
import { createDocumentsClient } from "@eli-ai/client/documents";
import type { DocumentLineage } from "@eli-ai/contracts/documents";

const documents = createDocumentsClient(
  createEliTransport({
    baseUrl: "https://eli.ai",
    apiKey: () => process.env.ELI_API_KEY!,
    // Only needed for eli_uk_ keys that can reach more than one workspace.
    workspaceId: process.env.ELI_WORKSPACE_ID,
  }),
);

const page = await documents.list({ limit: 20 });          // { items, total, limit, offset }
const first = page.items[0];
if (!first) throw new Error("no documents in this workspace");

const trail: DocumentLineage = await documents.lineage(first.id);

console.log(trail.origin.kind);                 // "connector" | "vault" | "upload"
console.log(trail.governance.verification);     // "unverified" | "verified" | "certified" | "deprecated"
console.log(trail.derived.chunkCount, trail.derived.relationCount);
console.log(trail.downstream.citedByAnswers, trail.downstream.impactedEntities);

What adopting Lineage does not require

No knowledge graph, no live-data connectors, no agents, no evals — none of those packages, scopes, or tables are touched by the lineage read. It also needs no AI provider or model slot configured: assembling a DocumentLineage makes zero model calls. Creating or updating a document does kick off embedding and extraction warm-up, but that is fire-and-forget — the write commits and responds whether or not enrichment succeeds.

The React package imports no Next.js router, link, image, font, or route-handler module, and EliProvider deliberately has no API-key prop — you inject a transport. Sections of the payload that belong to other modules do not fail when those modules are unused; they come back as zeros and empty arrays — see how it composes at the end of this page.

React components

@eli-ai/react/documents exports exactly two components — EliDocuments and EliDocumentLineage — plus the types EliDocumentsProps, EliDocumentLineageProps, and EliDocumentsLabels. Both are client components and both read the transport from EliProvider. Neither is re-exported from the package root, so @eli-ai/react/documents is the import path.

EliDocuments

The catalog surface. It renders a titled section containing: an optional create form (path, title, content) behind a toggle button; a filter input; a table of one row per document — title over path as a selectable button, then version, then a localized updated timestamp; and, once a row is selected, the inline lineage panel for that document. It calls GET /api/v1/documents once (query key includes limit), POST /api/v1/documents on submit, and GET /api/v1/documents/{id}/lineage for the selected row.

EliDocuments props

labelsPartial<EliDocumentsLabels>Spread over the built-in defaults, so any subset of the sixteen keys works. See the defaults below.
limitnumberSent as the limit query parameter of GET /api/v1/documents and part of the query key, so changing it refetches. Default 50.
allowCreatebooleanRenders the create toggle and inline create form. Default true. When false, the component is read-only and never needs kb:write.
initialDocumentIdstringDocument id selected on first render; its lineage panel loads immediately without a click.
onDocumentSelect(document: DocumentMeta) => voidFired when a row button is pressed, with the full DocumentMeta for that row.
onDocumentCreated(result: DocumentSaveResult) => voidFired after a successful create, with { docId, path, contentHash, title }. The component also clears the form, selects the new document, and refetches the list.
onLineageLoaded(lineage: DocumentLineage) => voidFired whenever the embedded lineage panel finishes loading, with the full payload.
classNamestringAppended after the eli-root class on the root section element.
titlestringOverrides labels.title in the section header.
descriptionstringOverrides labels.description in the section header.
loadingLabelstringShown while the document list loads. Default "Loading documents…".
errorLabelstringShown with a retry button when the document list fails. Default "Documents could not be loaded.".
onError(error: unknown) => voidCalled on list-fetch and create failures. Every prop on this component is optional.
EliDocumentsLabels — the shipped defaultsts
// packages/react/src/documents/index.tsx — DEFAULT_LABELS.
// Pass `labels` with any subset; it is spread over these.
const DEFAULT_LABELS: EliDocumentsLabels = {
  title: "Documents",
  description: "Browse source material and inspect its provenance from intake to downstream use.",
  filter: "Filter documents",
  create: "New document",
  closeCreate: "Cancel",
  path: "Path",
  documentTitle: "Title",      // create-form field AND the first table column header
  content: "Content",
  save: "Create document",
  empty: "No documents yet",
  emptyDescription: "Create a document or sync a content connector to begin.",
  lineage: "Lineage",          // default header of <EliDocumentLineage>
  origin: "Origin",            // the four KPI tile labels
  revisions: "Revisions",
  derived: "Derived knowledge",
  downstream: "Downstream impact",
};

Three behaviours worth knowing before you wire it

The filter is client-side. It matches the loaded page only, against title and path, lowercased. It is not a server search and does not page past limit.

Two column headers are hard-coded. The first header uses labels.documentTitle; the version and updated headers are literal strings and are not label-driven.

The embedded panel has its own strings. Inside EliDocuments, the lineage panel uses fixed loading and error text rather than the loadingLabel and errorLabel you pass; those two apply to the document list. Use EliDocumentLineage directly if you need to control them.

provider wiring + catalogtsx
import { createEliTransport } from "@eli-ai/client/core";
import { EliProvider } from "@eli-ai/react/provider";
import { EliDocuments } from "@eli-ai/react/documents";
import "@eli-ai/react/styles.css";

// The injected fetch is the trust boundary: point it at a same-origin route
// that attaches the Bearer key server-side. Never ship an eli_sk_ key to a browser.
const transport = createEliTransport({ fetch: sameOriginProxyFetch });

export function DocumentCatalog({ workspaceId }: { workspaceId: string }) {
  return (
    <EliProvider transport={transport} workspaceId={workspaceId}>
      <EliDocuments
        limit={100}
        allowCreate={false}
        labels={{ title: "Source material", downstream: "Blast radius" }}
        onLineageLoaded={(lineage) => {
          console.log(lineage.origin.kind, lineage.downstream.impactedEntities);
        }}
      />
    </EliProvider>
  );
}

EliDocumentLineage

The lineage panel for one document id, in its own titled section. It renders a badge row (the verification tier, tinted as a warning when governance.reviewOverdue is true; the origin kind; the document version), four KPI tiles, and up to the first six revision items as version · change kind with a timestamp. The tiles are: Origin (origin.sourceType falling back to origin.kind, plus origin.lastSyncedAt), Revisions (revisions.total), Derived knowledge (the number of derived.entities, with chunk and relation counts underneath), and Downstream impact (downstream.citedByAnswers).

EliDocumentLineage props

documentIdrequiredstringThe document whose trail is fetched. Also the query key, so changing it refetches.
labelsPartial<EliDocumentsLabels>Shares the EliDocumentsLabels type. This component reads labels.lineage (the default header) and the four KPI labels: origin, revisions, derived, downstream.
onLoaded(lineage: DocumentLineage) => voidFired once the trail resolves, with the full payload.
classNamestringAppended after the eli-root class on the root section element.
titlestringSection header. Defaults to labels.lineage, that is "Lineage".
descriptionstringSection subheading. Defaults to the fixed string "Trace origin, governance, revisions, and downstream use.".
loadingLabelstringShown while the trail loads. Default "Loading lineage…".
errorLabelstringShown with a retry button when the trail fails. Default "Lineage could not be loaded.".
onError(error: unknown) => voidCalled when the trail request fails.
emptyLabelstringAccepted by the shared surface prop type but not read by this component — it renders no empty state. Listed for completeness only.
standalone lineage paneltsx
import { EliProvider } from "@eli-ai/react/provider";
import { EliDocumentLineage } from "@eli-ai/react/documents";

export function ProvenancePanel({ docId }: { docId: string }) {
  return (
    <EliProvider transport={transport}>
      <EliDocumentLineage
        documentId={docId}
        title="Provenance"
        errorLabel="We could not load this document's trail."
        onLoaded={(lineage) => {
          if (lineage.governance.reviewOverdue) flagForReview(lineage.document.id);
        }}
      />
    </EliProvider>
  );
}

Prefer your own markup?

Both components are thin wrappers over the generic hooks. Call useEliQuery from @eli-ai/react/hooks with createDocumentsClient(context.transport).lineage(id, …) and render whatever you like — the scoped eli-* classes and the CSS export are optional.

HTTP API

Six operations, all under /api/v1/documents. The workspace is resolved from the bearer key and never appears in the URL or the body. Every failure uses the shared envelope { error, message } — plus issues on body-validation failures — so a consumer can branch on error. Codes you will actually see here: unauthorized (401), insufficient_scope and insufficient_capability (403), not_found (404), invalid_pagination, invalid_json, invalid_body, and invalid_path (400).

GET/api/v1/documents/{id}/lineageBearer · kb:read or runs:read

The module's namesake read: origin provenance, governance, immutable revision summaries, the document audit trail, derived knowledge, and downstream impact — assembled in one workspace transaction. The document ACL is checked before assembly, so an unknown id, a cross-workspace id, a soft-deleted id, and a restricted id you cannot see are all the same 404.

Example requestbash
curl -s https://eli.ai/api/v1/documents/01KY10AAAAAAAAAAAAAAAAAAAA/lineage \
  -H "Authorization: Bearer $ELI_KEY" \
  | jq '{origin: .origin.kind, revisions: .revisions.total, citedBy: .downstream.citedByAnswers}'
Responsejson
{
  "document": {
    "id": "01KY10AAAAAAAAAAAAAAAAAAAA",
    "path": "ops/order-platform-runbook.md",
    "title": "Order Platform incident runbook",
    "version": 4,
    "contentHash": "9f2b1c…",
    "createdAt": "2026-06-02T14:20:00.000Z",
    "updatedAt": "2026-07-18T09:05:00.000Z",
    "createdBy": "usr_01KY0Z9M2E5Q4RS8T0V1WX2Y3Z",
    "updatedBy": "usr_01KY0Z9M2E5Q4RS8T0V1WX2Y3Z"
  },
  "governance": {
    "verification": "certified",
    "verifiedBy": "usr_01KY0Z9M2E5Q4RS8T0V1WX2Y3Z",
    "verifiedAt": "2026-07-01T00:00:00.000Z",
    "nextVerifyAt": "2026-12-28T00:00:00.000Z",
    "reviewOverdue": false
  },
  "origin": {
    "kind": "connector",
    "sourceId": "01KY0V000CONNECTOR00000001",
    "sourceType": "confluence",
    "externalId": "CONF-88213",
    "sourceVersion": "17",
    "lastSyncedAt": "2026-07-18T09:00:00.000Z",
    "recentSyncRuns": [
      { "id": "01KY18…", "kind": "incremental", "startedAt": "2026-07-18T09:00:00.000Z", "finishedAt": "2026-07-18T09:00:12.000Z", "error": null }
    ]
  },
  "revisions": {
    "total": 4,
    "items": [
      {
        "id": "01KY17…", "version": 4, "changeKind": "update",
        "changedFields": ["contentHash", "title"],
        "hasContent": true, "activity": "api",
        "changedBy": "usr_01KY0Z9M…", "note": null,
        "createdAt": "2026-07-18T09:05:00.000Z"
      }
    ]
  },
  "audit": [
    { "event": "document.verify", "actor": "usr_01KY0Z9M…", "at": "2026-07-01T00:00:00.000Z", "meta": { "from": "verified", "to": "certified" } },
    { "event": "document.update", "actor": "usr_01KY0Z9M…", "at": "2026-07-18T09:05:00.000Z", "meta": { "path": "ops/order-platform-runbook.md" } }
  ],
  "derived": {
    "chunkCount": 12,
    "entities": [
      { "id": "01KY0ZW3…", "name": "Order Platform", "type": "system", "authority": "certified", "lifecycleStatus": "published", "mentionCount": 9 }
    ],
    "relationCount": 5
  },
  "downstream": {
    "citedByAnswers": 14,
    "cachedAnswers": [
      { "entryId": "01KY19…", "question": "how do we handle stuck orders?", "invalidated": false }
    ],
    "impactedEntities": 7
  }
}
GET/api/v1/documentsBearer · kb:read

Lists live, ACL-visible documents ordered by path ascending. total is the full count for the workspace, not the page length, so it is the value to paginate against.

Query parameters

limitinteger >= 1Page size. Default 50, capped at 200. A non-integer or out-of-range value returns 400 invalid_pagination.
offsetinteger >= 0Rows to skip. Default 0.
Example requestbash
curl -s "https://eli.ai/api/v1/documents?limit=2&offset=0" \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{
  "items": [
    {
      "id": "01KY10AAAAAAAAAAAAAAAAAAAA",
      "path": "ops/order-platform-runbook.md",
      "title": "Order Platform incident runbook",
      "contentHash": "9f2b1c…",
      "version": 4,
      "updatedAt": "2026-07-18T09:05:00.000Z"
    }
  ],
  "total": 137,
  "limit": 2,
  "offset": 0
}
GET/api/v1/documents/{id}Bearer · kb:read

One document with its raw markdown. missing is true when the metadata row exists but the stored body is gone or unservable — content is then an empty string rather than an error.

Example requestbash
curl -s https://eli.ai/api/v1/documents/01KY10AAAAAAAAAAAAAAAAAAAA \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{
  "id": "01KY10AAAAAAAAAAAAAAAAAAAA",
  "path": "ops/order-platform-runbook.md",
  "title": "Order Platform incident runbook",
  "contentHash": "9f2b1c…",
  "version": 4,
  "updatedAt": "2026-07-18T09:05:00.000Z",
  "content": "---\nid: 01KY10AAAAAAAAAAAAAAAAAAAA\n---\n\n# Order Platform incident runbook\n…",
  "missing": false
}
POST/api/v1/documentsBearer · kb:write

Creates a document through the vault: the body is stored, the id is spliced into frontmatter, the content is hashed and chunked, and a create revision plus a document.create audit row are written in the same transaction. Responds 201. Embedding and extraction are queued afterwards and never block the response. An unusable path returns 400 invalid_path.

Request body

pathstring (min 1)Vault-relative path. A missing extension gets .md appended. Omit it and the server derives a unique slug from the title it parses out of the content.
contentstringMarkdown body. When omitted, the server writes "# " followed by the title, or "# New note" when there is no title either.
titlestring (min 1)Only used to build the default content when content is omitted. The stored title is always re-extracted from the saved content, so it can differ from what you send.
Example requestbash
curl -L -X POST https://eli.ai/api/v1/documents \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"path":"ops/order-platform-runbook.md","title":"Order Platform incident runbook","content":"# Order Platform incident runbook\n\nRequeue stuck orders after 24h."}'
Responsejson
{
  "docId": "01KY10AAAAAAAAAAAAAAAAAAAA",
  "path": "ops/order-platform-runbook.md",
  "contentHash": "9f2b1c…",
  "title": "Order Platform incident runbook"
}
PATCH/api/v1/documents/{id}Bearer · kb:write

Rewrites the content in place: the version is bumped, the document is re-chunked, an update revision and a document.update audit row are appended, and dependent semantic-cache entries are invalidated. The stored path never changes here — a rename is a separate operation. Returns the same save result as create.

Request body

contentrequiredstringThe full new markdown body. This is the only accepted field.
Example requestbash
curl -L -X PATCH https://eli.ai/api/v1/documents/01KY10AAAAAAAAAAAAAAAAAAAA \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content":"# Order Platform incident runbook\n\nRequeue stuck orders after 12h."}'
Responsejson
{
  "docId": "01KY10AAAAAAAAAAAAAAAAAAAA",
  "path": "ops/order-platform-runbook.md",
  "contentHash": "4c81ae…",
  "title": "Order Platform incident runbook"
}
DELETE/api/v1/documents/{id}Bearer · kb:write

Soft-delete. The body moves to recoverable trash and the row is tombstoned, but chunks are hard-deleted and the document's graph contribution is purged, so deleted content is never retrievable. A terminal delete revision and a document.delete audit row are appended. Afterwards the lineage read returns 404 for this id — a tombstoned document has no live trail.

Example requestbash
curl -L -X DELETE https://eli.ai/api/v1/documents/01KY10AAAAAAAAAAAAAAAAAAAA \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{ "deleted": true, "id": "01KY10AAAAAAAAAAAAAAAAAAAA" }

One more route sits under this path prefix but belongs to a different module. The operation catalog files verifyDocument under the governance capability, and its handler requires the governance:write capability rather than the document one. It is documented here because it is the only writer of the governance block that Lineage reads back.

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

Warrant's document verification stamp. verified and certified set verified_by, verified_at, and a re-verify due date derived from the workspace review interval; unverified and deprecated clear all three. Audited as document.verify with meta { from, to }.

Request body

verificationrequired"unverified" | "verified" | "certified" | "deprecated"The new tier. Any other value fails validation.
Example requestbash
curl -L -X POST https://eli.ai/api/v1/documents/01KY10AAAAAAAAAAAAAAAAAAAA/verify \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"verification":"certified"}'
Responsejson
{
  "docId": "01KY10AAAAAAAAAAAAAAAAAAAA",
  "verification": "certified",
  "verifiedBy": "usr_01KY0Z9M2E5Q4RS8T0V1WX2Y3Z",
  "verifiedAt": "2026-07-01T00:00:00.000Z",
  "nextVerifyAt": "2026-12-28T00:00:00.000Z"
}

One envelope inconsistency to code against

The verify route returns { error: "Document not found" } on 404 — a human sentence in the error field, with no message. Every other endpoint on this page returns the machine code not_found with a separate message. If you branch on error, special-case this one route.

Data elements

Every table below is a tenant table: it carries workspace_id as the leading index column and exactly one row-level-security policy comparing it to the workspace GUC, with FORCE ROW LEVEL SECURITY applied by companion migrations. The lineage assembler runs every one of its queries inside a workspace transaction and filters workspace_id explicitly in the SQL. There is no non-tenant table in this module.

TableOne row isWhat Lineage does with itOwner
documentsOne document. Identity and content fingerprint (path, title, content_hash, version); the Warrant columns (verification, verified_by, verified_at, next_verify_at); the Intake provenance columns (source_id, external_id, source_version); actors (created_by, updated_by); the ACL columns (acl_visibility, acl_principals); and the deleted_at tombstone.Read for document, governance, and most of origin. Written by create, update, delete, and verify. A row with deleted_at set is invisible to every read here.Lineage (writes) · Warrant and Intake (columns)
document_contentsThe markdown body, keyed by (workspace_id, path) rather than by document id.Written and read only when the deployment stores bodies in Postgres instead of on disk. Not read by the lineage assembler.Lineage (vault store)
document_revisionsOne immutable revision: version, change_kind (create, update, delete), a metadata snapshot, a field-level diff, an optional full content snapshot, the PROV activity channel, the actor, and a note.Read for revisions — the uncapped total plus the newest 50 summaries. changedFields is the sorted key set of diff; hasContent reports whether a snapshot exists. Content blobs never enter the payload. Appended on every save and on delete.Lineage
audit_logOne append-only event: event name, actor, target, meta, timestamp.Read filtered to events beginning with document. and targeting this document id, newest first, capped at 50. Written as document.create, document.update, and document.delete with meta { path }, and document.verify with meta { from, to }.Lineage (writes) · Warrant (verify)
chunksOne retrievable slice of a document.Read as a count only, for derived.chunkCount. Written synchronously by the save path and hard-deleted on soft-delete.Intake
connector_sourcesOne governed ingestion source (Confluence, SharePoint, Slack, markdown upload).Left-joined on documents.source_id to fill origin.sourceType and origin.lastSyncedAt. A null source_id means origin.kind is vault; a markdown_upload type means upload; anything else means connector.Intake
sync_runsOne sync attempt for a source: kind (full, incremental, prune, permissions), start, finish, error.Read for origin.recentSyncRuns — the newest 10 for that source. Empty for vault documents.Intake
mentions + entitiesA mention links a document (and usually a chunk) to an entity; an entity is a concept with an authority tier and a lifecycle status.Mentions for this document are rolled up to canonical heads to produce derived.entities (top 50 by mention count, each with authority and lifecycle) and, as a distinct count, downstream.impactedEntities. Deleted entities are excluded.Atlas
relations + relation_evidenceA relation is a typed edge between two entities; evidence is the quote and document that grounds it.Read as one count: distinct live relations whose evidence cites this document, exposed as derived.relationCount.Atlas
messagesOne persisted conversation message; assistant messages carry a jsonb sources registry.Counted with a jsonb containment match on the document id to produce downstream.citedByAnswers.Lens
semantic_cache_entries + semantic_cache_dependenciesA cached grounded answer, plus one dependency row per document it was grounded in.Entries with a dependency on this document, newest 50, become downstream.cachedAnswers — entry id, question, and whether the entry has been tombstoned.Lens

Bounded by construction

The payload is capped so it stays a safe API response: 50 revision summaries (with the true total alongside), 50 audit events, 50 derived concepts, 10 recent sync runs, and 50 cached answers. Ordering is deterministic in every case, and the queries run sequentially on one connection.

Feature map

Five feature areas make up the module — two on the write path that produce the trail, three on the read path that assemble and project it. Each names the concrete modules and tables that deliver it, and the same names reappear in the code map and internals below.

Feature areaWhat it providesDelivered bySurfaces
Document catalog and write pathList, read, create, update, and soft-delete through the vault: path normalization, frontmatter id splicing, content hashing, and synchronous chunking.saveDocument / softDeleteDocument in src/server/vault/index.ts; chunkMarkdown in src/server/ingest/chunking.tsthe five catalog operations under /api/v1/documents · EliDocuments · documents SDK client
Revision and audit captureOne immutable revision per save or delete — metadata snapshot, field diff, optional content snapshot — plus a document.* audit event, all in the same transaction.recordDocumentRevision in src/server/vault/revisions.ts; the document_revisions and audit_log tablesrevisions and audit blocks of the lineage payload
Trail assemblyThe namesake read: one workspace transaction, sequential queries, deterministic ordering, bounded caps, zero model calls.documentLineage in src/server/lineage/index.tsGET /api/v1/documents/{id}/lineage · EliDocumentLineage · MCP tool kb_document_lineage
Origin and governance projectionWhere the document came from — connector, upload, or vault, with recent sync runs — and its verification tier with a review-overdue flag.the connector_sources left join and verification read in src/server/lineage/index.tsorigin and governance blocks · badges and the Origin tile in EliDocumentLineage
Derived and downstream projectionWhat the document produced (chunks, canonical concepts, grounded relations) and what depends on it (citing answers, cached answers, impacted entities).the mention rollup, evidence count, message containment, and cache-dependency queries in src/server/lineage/index.tsderived and downstream blocks · the Derived knowledge and Downstream impact tiles

System architecture

The module splits cleanly into a write path that produces the trail and a read path that assembles it. Rectangles are API routes and services, cylinders are tables and the job queue; there is no model node because assembling a trail makes zero model calls. The dotted edge is the fire-and-forget enrichment handoff.

Lineage — system architecture

Every save flows through the vault, which writes the document, its body, its chunks, its revision, and its audit row together. The lineage route gates on the document ACL, then the assembler joins the owned stores with the intake, graph, and answer tables it only reads.

Rendering diagram

Downloads

Concepts

  • Lineage architecture
  • REST surface — src/app/api/v1/documents
  • Write path — the trail producer
  • Read path — the trail assembler
  • Modules
  • Lineage

Keywords

  • Lineage route (API)
  • Catalog routes — list + create (API)
  • Item routes — get, update, delete (API)
  • content-acl.ts — document ACL gate (service)
  • store: queue
  • store: table
  • store: tables
  • embed + extract jobs
  • saveDocument + softDeleteDocument (service)
  • path normalization jail (service)
  • recordDocumentRevision (service)
  • synchronous chunking (service)
  • fire-and-forget warm-up (service)
  • documentLineage (service)
  • store: append-only table
  • vault/index.ts
  • vault/paths.ts
  • vault/revisions.ts
  • ingest/chunking.ts
  • ingest/enrichment.ts
  • lineage/index.ts
Source and generation provenance

Status: current

Generated at: 2026-08-17T18:35:03.816Z

Source hash: bdfd5ed9c898bae491854039be1fb760cb8e189cff771e2924301322ee0441b1

Metadata payload hash: b4ed0c487b1b8992a8af224a579c8a273d9b8db9c95b16bc473d357373fadaba

Canonical appearance

src/app/(docs)/docs/modules/lineage/page.tsx:132 route /docs/modules/lineage

All appearances

  • canonicalsrc/app/(docs)/docs/modules/lineage/page.tsx:132 route /docs/modules/lineage

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: lineage-system-architecture-bdfd5ed9.json

How to read the Lineage system architecture

  1. Start at the write path: Catalog and item routes call the vault, which normalizes the path, writes the body, chunks synchronously, and appends the revision and audit rows in the same transaction.
  2. Note what is deferred: Embedding and extraction warm-up is enqueued after the save commits and never blocks or fails the response.
  3. Cross to the read path: The lineage route checks the document ACL first, then the assembler runs its sequential queries inside one workspace transaction.
  4. Distinguish owned from joined stores: documents, document_contents, document_revisions, audit_log, and chunks are written here; connector, graph, and answer tables are read-only joins that degrade to zeros when their modules are unused.
Trust boundary
kb:read (or the legacy runs:read grant) plus the documents:read capability gate the read, and the content ACL is applied before assembly, so restricted documents 404 rather than leak a partial trail.
Durable state
documents with provenance and verification columns, bodies in the vault store, append-only document_revisions and audit_log, and chunks.

Failure paths

  • A tombstoned document has no live trail — the read returns 404, not an empty shell
  • A vault body missing on disk yields missing: true on the document read, never an error
  • Plugin lineage sections that fail or overrun are omitted with partial: true
  • Enrichment queue outages delay embeddings but never block saves

Signals

  • Lineage read latency (sequential query budget)
  • reviewOverdue share across the catalog
  • Revision totals versus the 50-item cap
  • partial: true frequency (misbehaving plugins)

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
Catalog routesAPIsrc/app/api/v1/documentsGET list (kb:read) and POST create (kb:write); create fires the enrichment warm-up after responding.
Item routesAPIsrc/app/api/v1/documents/[id]GET with raw markdown, PATCH content rewrite, DELETE soft-delete.
Lineage routeAPIsrc/app/api/v1/documents/[id]/lineageACL-checks the document through the vault before calling the assembler; kb:read or runs:read.
Trail assemblerservicesrc/server/lineage/index.tsdocumentLineage — one transaction, sequential queries, caps of 50/50/50/10/50, plus the plugin section runner.
Vault write and read pathservicesrc/server/vault/index.tssaveDocument, readDocument, listDocuments, softDeleteDocument — every catalog operation lands here.
Path normalizationservicesrc/server/vault/paths.tsJails vault-relative paths; an unusable path is a 400 invalid_path at the boundary.
Revision recorderservicesrc/server/vault/revisions.tsrecordDocumentRevision runs inside the caller's open save transaction; content snapshots cap at 256 KB.
Vault body storestoresrc/server/vault/storeFilesystem or Postgres body storage; the Postgres variant keys document_contents on workspace and path.
Synchronous chunkerservicesrc/server/ingest/chunking.tsRuns in the save path, which is why chunkCount stays accurate with every other module absent.
Enrichment warm-upservicesrc/server/ingest/enrichment.tsFire-and-forget embedding and extraction kickoff after a save; failure never blocks the write.
Content-ACL gateservicesrc/server/authz/content-acl.tsBuilds the caller's ACL context; the lineage route checks it before any assembly work.
Owned and joined tablesstoresrc/server/db/schema.tsWrites documents, document_contents, document_revisions, audit_log, chunks; reads connector_sources, sync_runs, mentions, entities, relations, relation_evidence, messages, semantic_cache_entries, semantic_cache_dependencies.
EliDocuments and EliDocumentLineageUIpackages/react/src/documents/index.tsxThe catalog table with its embedded panel, and the standalone lineage panel.
Documents clientSDKpackages/client/src/documents.tscreateDocumentsClient — list, get, create, update, remove, lineage.
Wire contractsSDKpackages/contracts/src/documents.tsDocumentMeta, DocumentSaveResult, and the full DocumentLineage shape.

Primary runtime flow

The highest-value flow is the namesake read. Message labels use the operation names from the HTTP API section above.

Lineage — primary runtime flow

getDocumentLineage gates on the document ACL first, then assembles origin, revisions, audit, derived knowledge, and downstream impact in one workspace transaction with deterministic ordering and fixed caps.

Rendering diagram

Downloads

Concepts

  • Lineage runtime flow
  • Modules
  • Lineage

Keywords

  • GET :id/lineage (API)
  • API consumer (SDK or curl)
  • documents + connector_sources (store)
  • document_revisions + audit_log (store)
  • newest 50 document.* audit events
  • vault readDocument (service)
  • chunks + mentions + relations (store)
  • messages + semantic cache (store)
  • getDocumentLineage for one document id
  • documentLineage in one workspace transaction
  • the assembled DocumentLineage
  • document row left-joined to its connector source
  • newest 50 revision summaries with the uncapped total
  • citedByAnswers count and newest 50 dependent cache entries
  • lineage/index.ts
  • /api/v1/documents
  • service
  • newest 10 sync_runs when the origin is a connector
  • the document is live and visible to this key
  • ACL-checked read — unknown, deleted, restricted are the same 404
  • chunk count, top 50 canonical concepts, grounded relation count
Source and generation provenance

Status: current

Generated at: 2026-08-17T18:34:59.423Z

Source hash: dca4f3d8c20f01ed3cdcc565ef9afe5a2b689d3e98b5484ff5534f213e8eb216

Metadata payload hash: 140400113ca35e5bba76046ac49060da97aba7b15dbb5123732bb368f7ff7b1d

Canonical appearance

src/app/(docs)/docs/modules/lineage/page.tsx:189 route /docs/modules/lineage

All appearances

  • canonicalsrc/app/(docs)/docs/modules/lineage/page.tsx:189 route /docs/modules/lineage

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: lineage-primary-runtime-flow-dca4f3d8.json

How to read the Lineage primary runtime flow

  1. The ACL gate is first and absolute: An unknown id, a cross-workspace id, a soft-deleted id, and a restricted id the key cannot see are all the same 404 before any assembly runs.
  2. One transaction, sequential queries: All queries run in order on the single transaction connection — a pg connection is not safe for concurrent statements, and determinism is part of the contract.
  3. Caps keep the payload an API response: Fifty revision summaries with the true total alongside, fifty audit events, fifty concepts, ten sync runs, fifty cached answers.
  4. Foreign sections degrade to zeros: With no connector the origin is vault; with no graph the derived counts are zero; with no answers the downstream block is empty — never an error.
Trust boundary
Scope kb:read or the legacy runs:read grant, the documents:read capability for delegated keys, and the content ACL applied through the vault read before assembly.
Durable state
Nothing is written — the flow is a pure read over the owned tables and the joined intake, graph, and answer tables.

Failure paths

  • Deleted documents return 404 — a tombstone has no live trail
  • A dangling source_id (source deleted) degrades origin to a best-effort connector kind
  • Plugin sections that fail or overrun are dropped with partial: true
  • Cross-workspace reads are impossible under forced RLS

Signals

  • Lineage read latency and its slowest sub-query
  • 404 rate (stale ids in callers)
  • citedByAnswers growth per document
  • invalidated share among dependent cache entries

Internals and invariants

Document catalog and write path

Every write lands through the vault: the path is normalized and jailed, the id is spliced into frontmatter, the title is re-extracted from the saved content, the body is hashed, and chunks are written synchronously in the same operation. Deletes are soft for the document and hard for its chunks and graph contribution.

  • Invariant — Vault-relative paths cannot escape the vault: normalization and the path jail reject traversal at the boundary, enforced in src/server/vault/paths.ts.
  • Invariant — Chunks are written in the synchronous save path, never a deferred job, so chunkCount is accurate the moment a save returns, enforced through src/server/vault/index.ts and src/server/ingest/chunking.ts.

Revision and audit capture

The revision recorder runs inside the caller's already-open save transaction: a metadata snapshot, a field-level diff against the previous revision, an optional content snapshot for diffing, and the actor and activity channel. The matching audit event is appended in the same transaction.

  • Invariant — A revision is written atomically with the document mutation it records — never a partial history — enforced by recordDocumentRevision in src/server/vault/revisions.ts.
  • Invariant — Revision content snapshots are bounded: bodies over 256 KB store metadata only, and oversized diffs collapse to a coarse summary, enforced in src/server/vault/revisions.ts.

Trail assembly

The assembler is deliberately boring: one workspace transaction, each sub-query filtered by workspace id on top of row-level security, deterministic ordering everywhere, and fixed caps. Plugin-contributed sections run after the core projection with a strict budget and can only append.

  • Invariant — The lineage read performs no model, network, or file I/O and returns the same payload for the same document state, enforced in src/server/lineage/index.ts.
  • Invariant — Contributed sections are purely additive under the src/server/plugins/limits.ts budget (three-second default, eight-second cap); a failing or overrunning plugin costs its section and sets partial, never the read, enforced in src/server/lineage/index.ts.

Origin and governance projection

Origin classification is a left join: a null source id means vault, a markdown_upload source means upload, anything else means connector, with the newest sync runs attached. Governance reads the verification column group and computes reviewOverdue from the stored due date.

  • Invariant — The ACL is checked before assembly, so unknown, cross-workspace, deleted, and restricted ids are indistinguishable 404s, enforced in src/app/api/v1/documents/[id]/lineage via src/server/authz/content-acl.ts.

Derived and downstream projection

Derived knowledge rolls document mentions up to canonical heads with authority and lifecycle labels and counts live relations whose evidence cites the document. Downstream impact counts persisted assistant messages whose source registry contains the document and lists dependent semantic-cache entries with their invalidation state.

  • Invariant — Derived concepts exclude deleted entities and resolve merges to canonical heads before counting, enforced by the rollup queries in src/server/lineage/index.ts.

For a self-hosting team the safe extension points are the seams the module already exposes: append a titled section to the trail with a lineageSections plugin contribution (budgeted, additive, never load-bearing), swap the vault body store in src/server/vault/storebetween filesystem and Postgres, and tune the enrichment queue's consumers. The transactional revision capture, the ACL-before-assembly rule, and the payload caps are load-bearing and not configurable.

How it composes

Lineage is the module that reads everyone else's trail. Adopted alone it works and is useful — you get identity, revisions, audit, and chunk counts for every document you write — but four of its sections are genuinely filled in by other modules, and this is what they look like without them.

  • With Intake origin becomes real provenance: kind of connector or upload, the source type, the external id, the source version, the last sync time, and the recent sync runs. Without it, kind is vault, every other origin field is null, and recentSyncRuns is empty. This one genuinely needs Intake — there is no other writer of those columns.
  • With Warrant governance carries a real tier plus reviewOverdue, and the badge in EliDocumentLineage turns amber when a review is past due. Without it, verification stays unverified and reviewOverdue is always false. Note the package-level relationship too: the documents capability declares a dependency on governance, but that is a type dependency only — the verification, authority, and lifecycle unions are imported from @eli-ai/contracts/governance. The wire read needs no governance scope.
  • With Atlas derived.entities, derived.relationCount, and downstream.impactedEntities become the real knowledge the document produced and the concepts a deprecation would touch. Without it, entities is empty and both counts are zero — chunkCount is still accurate, because chunking happens in the synchronous save path, not in extraction.
  • With Lens downstream.citedByAnswers counts persisted messages whose [Sn] source registry includes this document, and cachedAnswers lists the semantic-cache entries that depend on it. Without it, both are zero and empty: nothing has cited the document yet. This is the section that most rewards adopting Lens alongside Lineage — impact analysis is only interesting once answers exist.
  • With Ports — the identical trail is exposed as the MCP tool kb_document_lineage, so an agent can trace provenance mid-run without your application brokering the call. Without it, the REST endpoint and the SDK are the surfaces.
  • Conduit and Crucible — honestly, nothing. No field of DocumentLineage is populated by live-data connectors or by evals. They compose at the product level, not in this payload.

Where to go next

The conceptual walkthrough of the trail, with the full DocumentLineage TypeScript shape and the upstream/downstream diagram, lives at Data lineage. For the package architecture behind these imports — transports, workspace resolution, and error classes — see the JavaScript and React SDK, and confirm scopes in Authentication.

Extending Lineage

A plugin can append a titled section to this trail — lineageSections returns a label/value table that lands in DocumentLineage.contributed, alongside (never replacing) the core projection. It runs under documents:read with a three-second default budget; an overrun or a throw omits that section and sets partial: true rather than failing the read, so a misbehaving plugin costs you a section and never the lineage page. See the extension model for the contract and the trust boundary, and Build a plugin for a worked example.