Repository navigation
Releases
Lorebase 0.3.1
A patch for the first contact with the project: the published images run on arm64, the PyPI packages carry their license and links, a Scribe worker keeps all its slots when the database fails while it records a job and gets a liveness check, the console scripts answer --help, and the quickstart commands point at the Keeper they started. No migration and no breaking change. Three new settings, all optional: scribe.worker.outcome_retry_delay_s, scribe.worker.liveness_file and scribe.worker.liveness_interval_s. The lines link the documentation of each change: the work landed directly on main, without pull requests to link.
Added
- Scribe liveness (Scribe): with
scribe.worker.liveness_fileset, the job loop rewrites that file everyscribe.worker.liveness_interval_s(10 s) with its live slots, the jobs running and the time it last reached the store, andpython -m lorebase_scribe.liveness FILEreads it, with the standard library only, and exits 1 with the reason (no file, a stale file, no live slot or, with--max-store-age, a store not reached). The composescribeservice writes/tmp/scribe-liveness.jsonand runs that command as its healthcheck; thescribevalues of the chart show the exec probe - Metric
lorebase.scribe.worker.slots(gauge,stateliveorlost) and the statusunrecordedoflorebase.scribe.jobs(metrics) lorebase-herald --helpprints the variables an MCP host sets (LOREBASE_URL,LOREBASE_API_KEY,LOREBASE_DOMAIN,LOREBASE_BRANCH,LOREBASE__HERALD__TRANSPORT) and the documentation link,--versionthe version; at start Herald logs the transport, the Keeper it calls and the default Domain to stderr, so a host pointed at the wrong Keeper sees it before the first call (MCP)make upprints theexport LOREBASE_URL=... LOREBASE_DOMAIN=supplyline for the CLI and theenventry for an MCP host over stdio, with the port set in.env;make shell-envprints the export line again, foreval "$(make shell-env)"(quickstart)make upchecks every host port the chosen profiles publish before it starts anything and names the variable to change, and refuses to start while a*_BINDvalue in.envwould publish the Keeper of the stack, which runs with authentication off, beyond this machine;KEEPER_PUBLISH_INSECURE=true, in.envor the shell, starts it anyway with a warning (quickstart)make envsetsHERALD_DOMAIN=supplywhile it is unset, so the first call of an MCP host againstmake up PROFILES=mcpneeds no Domain of its ownserver.jsonat the repository root describes Herald for the official MCP registry asio.github.aantenore/lorebase-herald, with the PyPI packagelorebase-herald(run withuvx) and the imageghcr.io/aantenore/lorebase, which now carries theio.modelcontextprotocol.server.namelabel the registry checks; the entry is submitted after a release (releasing)- MCP server: host configurations that run Herald from PyPI with
uvx lorebase-herald, as oneclaude mcp addcommand, a Cursor.cursor/mcp.jsonentry and a VS Codecode --add-mcpcommand - Comparison: DataHub, the Fabric IQ ontology MCP server, Semantica, Doltgres with the Dolt MCP server, TypeDB, mnem and RDF triple stores, each with a linked source checked on 2026-11-06, and a Standards section: LinkML import and export are supported; RDF, OWL, SHACL and JSON-LD are not native and are reached through LinkML's generators
- Images
ghcr.io/aantenore/lorebase,lorebase-mlandlorebase-webforlinux/arm64as well aslinux/amd64: each platform is built on a runner of its own architecture, and the release joins them into one multi-arch index, which it signs and attests with the SBOM (releasing) - PyPI metadata of the ten packages: license
Apache-2.0with the license text in every sdist and wheel, a README, author, keywords, classifiers and links to the repository, documentation, changelog and issues;scripts/release_tool.py checkfails when a package lacks them, and also checksserver.json(its versions, its name and the image label)
Changed
- Scribe: an outcome the store refuses to take (a dropped connection, a statement timeout) is written once more after
scribe.worker.outcome_retry_delay_s(1 s); when that fails too the run countsunrecordedand the job runs again once its lock goes stale, as a lost job does (Scribe) - README and the documentation home open with who Lorebase is for and one MCP call with the JSON answer and its Revision; the status note follows, and the 30-second demo stays the second screen
- Quickstart: the commands take the Keeper URL from the line
make upprints (eval "$(make shell-env)") instead of a fixedhttp://localhost:8080; the port table and the port troubleshooting row listGUIDE_HOST_PORT(8095),HERALD_HOST_PORT(8091) and thescale-outports; the MCP section setsHERALD_DOMAINnext toHERALD_ALLOW_ANONYMOUS; the semantic search example writesquery.jsonunder${TMPDIR:-/tmp} - CONTRIBUTING.md, the README and the validation and demo pages state the CI policy as it is: the checks run on demand and for release tags, not on every pull request
- Kubernetes and deployment say that the 0.3.0 images are
linux/amd64only and that 0.3.1 and later are published forlinux/amd64andlinux/arm64 - Releasing: a section's heading reads
X.Y.Z - unreleaseduntil the tag, whose date then replacesunreleased; the page follows the workflow's jobs and adds the MCP registry step - The Release workflow runs the CI workflow on the tagged commit first; images, the GitHub release and PyPI wait for it, so a tag whose checks fail publishes nothing. After the release it publishes the documentation site built from the tag (the
github-pagesenvironment must allowv*tags to deploy) - The compose defaults of Postgres (
pgvector/pgvector:0.8.7-pg16), Ollama, LiteLLM, the OpenTelemetry collector, Jaeger, Prometheus, Grafana and the OIDC mock, and the Helm default of Ollama, are pinned by version and digest instead oflatest,main-stableorpg16; the*_IMAGEvariables still override them, andtests/tooling/test_dockerfile_pins.pychecks the pins - CI: the integration job may run 35 minutes instead of 20, and the BM25 integration job is part of the aggregate
CIcheck - The release jobs no longer keep the checkout token on the runner
.env.examplesetsPOSTGRES_IMAGE,OLLAMA_IMAGE,LITELLM_IMAGE,OTEL_COLLECTOR_IMAGE,JAEGER_IMAGE,PROMETHEUS_IMAGEandGRAFANA_IMAGEto the version and digest pins ofdeploy/compose/docker-compose.yamlinstead of floating tags, so a.envcreated bymake envkeeps the pinned images.
Fixed
- A Scribe worker slot ended for good, and nothing reported it, when the database failed while the worker recorded a job's outcome: with the default concurrency the process stayed up with one slot, or none, while the queue grew. The slot now stays on, a slot task that ends with an error is logged and started again after
scribe.worker.poll_interval_s, andlorebase.scribe.worker.slotsshows it (Scribe) lorebasewith no server configured saidpass base_url or set LOREBASE_URL, an argument of the Python client the command line does not have; it now names the three ways a person has (--url,LOREBASE_URL, orurlin a profile of the config file) and the path of that filelorebase-herald --helpand--versionstarted the stdio server, printed nothing and exited 0 when the host closed the pipe- The audit chain heads and the merge attempts queries used a
DISTINCT ONspelling that SQLAlchemy 2.1 deprecates (anSADeprecationWarningin every integration run); they use the dialect'sdistinct_on, and the test suite fails on any such warning from now on - The 30-second demo excerpts in the README, the documentation home and manual setup quoted Revision ids that
examples/merge_demo.pyno longer prints; they are regenerated, andtests/tooling/test_merge_demo.pynow fails when a quoted line differs from the output - Stale documentation: the comparison listed LinkML import and export as roadmap work, the FAQ named the 1M-Record baseline as missing and left out the MySQL/MariaDB and SQL Server Sources, and
examples/README.mdlinked a quickstart anchor that moved;tests/tooling/test_docs_links.pynow checks the links and anchors of the README, CONTRIBUTING.md, SECURITY.md andexamples/README.md scripts/quickstart.sh upresolves the variables of its exposure and port checks the way docker compose does: a variable set in the shell wins over the environment file, then the default.KEEPER_BIND=0.0.0.0 KEEPER_ALLOW_INSECURE=true make upwith a loopback.envno longer starts an exposed Keeper with authentication off, andKEEPER_PUBLISH_INSECUREset in the shell (true or false) wins over the file.
Not done yet
- The chart wires no probe for Scribe by default; the
scribevalues show the exec probe to set. - The MCP registry entry (
server.json) is submitted by hand after the release; the registry lists nothing until then.
Upgrade notes
No migration. Rebuild or pull the images: the compose scribe service gains a healthcheck that reads the liveness file, with nothing to configure. The new settings have defaults; set scribe.worker.liveness_file and an exec probe to give another deployment the same check.
Lorebase 0.3.0
Knowledge that stays correct: a reconciliation queue for contradictions between Sources, corrections that survive Source syncs, document versions, ingestion modes and stewards, Traits and lenses in search and context packs, the merge of text values, and the resolution of words to schema and values with consolidation from use and coverage signals; the web app gains a Reconciliation screen and a passage-by-passage Conflict resolver. Pre-1.0: this minor version changes interfaces; read the breaking changes before upgrading.
Ten migrations (0038 to 0047). New configuration sections keeper.reconciliation, keeper.stewards, scribe.reconciliation, scribe.consolidation, store.values, store.resolve_misses, store.coverage, search.resolve and read.coverage, and new keys store.resolve_misses.caller_key, store.storage.hnsw_m, store.storage.hnsw_ef_construction, search.semantic.iterative_scan and search.semantic.max_scan_tuples. New endpoints POST /domains/{d}/resolve and /domains/{d}/reconciliation; responses add coverage to POST /query and POST /resolve, semantic_truncated to POST /search, can.settings to GET /domains/{d}, and source_key, version, replaces and status to documents. A Source sync no longer overwrites a value someone corrected on the target, and automatic merges into a protected branch wait for steward and role approvals (below, Breaking changes). The lines link the documentation of each change: the work landed directly on main, without pull requests to link.
Breaking changes
- Source syncs no longer put back a value someone corrected on the target: an
auto_when_cleanSource that merged silently stops at an open Proposal with aDV1orDV2Conflict when upstream changes such a value, and on its first run after migration0040. Setlocal_edits: source_winson the Source to keep the old behavior (below, Changed and Upgrade notes). - Source syncs with
auto_when_cleanorauto_with_authorityand extraction indirectmode no longer merge into a protected branch whose rules includerequired_approvals_from_stewardsorrequired_roles; the Proposal stays open (merge_blocked: approvals,direct_blocked: approvals). Installations without those rules see no change (below, Changed). - A
textAttribute edited in different sentences on two Branches merges automatically (resolverpolicy:text_merge) instead of raising a Conflict. Settext_merge: falsein the Domain'smerge_policysetting to keep the Conflict (below, Changed). - Semantic search reads the HNSW index at any size: unfiltered and broad-filter lists are approximate where they were exact (recall 0.92 to 0.99 at 300,000 vectors); a one-Kind filter stays exact (below, Changed).
Added
- Resolution of words to schema and values (design 8.9, AC-39).
POST /domains/{d}/resolve {text, kinds?, levels?, query_vector?, limit?, branch?, at?}returns ranked candidates the words of a text may stand for: Kinds, Attributes (by name or alias, with their type) and stored values with a ready-to-usewherepredicate and the count of Records holding them, each with its level (exact,synonym,trigram,embedding), score and the words matched, plus the words nothing matched. It never interprets the question, runs a query or calls a model; the embedding level only uses a caller'squery_vectorwhen no other level matched. Candidates never name a Kind or Attribute the caller may not read. Heraldlorebase_resolve, CLIlorebase resolve TEXT, Python clientrecords.resolve(sync and async). Settingssearch.resolve(agents). - Value index:
mat_valuesholds per branch the distinct short values of selected Attributes, kept by triggers onmat_recordsin the writing transaction (migration0045, additive). Enums are selected always, plain strings while their distinct values stay few (store.values); the Scribevalue_indexpass (scribe.scheduler.value_index_interval_s, default 300 s) andlorebase-scribe value-index DOMAIN [--rebuild]decide and backfill the selection. Sensitive and vault values are never indexed. A pass that changes the selection fences only its own Domain's writers (a transaction advisory lock taken shared by the triggers), for at moststore.values.pass_timeout_ms; a schema change that makes an Attribute ineligible removes its values in the same commit. At 100,000 Records resolution answers in 11.2 ms at p95 (levels 1-2); the triggers cost about 90 µs per statement where nothing is held and 0.3 ms per updated Record whose held values change (performance, operations). - Value index (design 8.9): an Attribute's distinct-value limit now holds between
value_indexpasses. The limit ismax_values:hard_max_distinctfor enums andresolvable: true,max_distinct×hysteresisfor plain strings. The write that takes a branch past it marks the Attributeoverflowedin the same transaction: resolution stops returning its values at once, later writes stop indexing it, and the next pass drops it. A schema commit or merge on the default branch applies a new limit in the same commit. Reads also skip a branch past its limit that is not yet marked. - Migration
0047addsmat_value_sizes, the per-branch count of held values kept by the value triggers, andmax_valuesandoverflowedtomat_value_attributes. Passes read the sizes instead of countingmat_values. On upgrade, existing selections gethard_max_distinctand their sizes are counted; the first pass of each Domain counts its sizes again and sets its own limits. Trigger cost: about 90 µs per statement where nothing is held, as before; 0.3 to 0.4 ms per updated Record whose held values change, against 0.29 ms before (measured withtests/perf/resolve_bench.pyon a loaded machine; performance). - Schema:
aliaseson Kinds and Attributes,value_aliases(stored value to other words) andresolvable: true|falseon Attributes; absent by default, so existing schemas keep their hashes. Validation refusesresolvableandvalue_aliaseson types other than string and enum (alone or in arrays), on sensitive and on link Attributes, and value aliases of an enum that name no value (schema.attribute_flag_invalid). The web Modeler keeps them on round trips. - Herald
lorebase_querynow documents counting with"aggregate": {"group_by": attr}and points tolorebase_resolve. - Consolidation from use (design 8.10, AC-40): with
store.resolve_misses.enabled(off by default), Keeper keeps the phrasesPOST /resolvematched to nothing per Domain, branch and phrase (counts, first and last seen, digests of distinct callers; migration0046), off the request path and only for head reads of callers holdingsearch.resolve.misses.permissionDomain-wide; requests opt out withrecord_misses: false, and searches narrowed bykindsorlevelsare not recorded. One caller adds and holds a bounded number of phrases; a full Domain evicts its least missed phrase. Phrases expire afterretention_days(30) and go with their Domain, and so do the vocabulary suggestions quoting them (their share removed, a suggestion left without phrases deleted, decided or not). A Scribevocabulary_consolidationpass (scribe.scheduler.consolidation_interval_s, CLIlorebase-scribe consolidate DOMAIN) turns frequent misses into schema suggestions of the new typesvalue_alias,attribute_alias,kind_alias(near values and names; never for sensitive, vault, derived or non-resolvable Attributes, never a phrase that already names something) and, withscribe.consolidation.llm.enabled,attribute_description(taskschema_consolidation; the model sees names and phrases only). They are reviewed like other suggestions; accepting one opens a schema change Proposal. Vocabulary suggestions have their own cap (schema.suggestions.max_vocabulary_tracked) so they cannot crowd out extraction's; they are withheld from reviewers who may not read their Kind's Records or Attribute's values; an alias that would name two things is refused at acceptance.lorebase-scribe misses purge DOMAINdeletes a Domain's misses and vocabulary suggestions. - Coverage signals (design 8.11, AC-41):
POST /queryandPOST /resolveanswer with acoverageblock (coverage: falseleaves it out; defaultread.coverage.enabled): the share of Records missing each Attribute used, Source freshness (last successful sync,stale_afterexceeded, Records unseen), the phrases unknown in the Domain, and one note per signal. It reads per-Kind statistics of the default branch head (mat_coverage, migration0046) counted by a Scribecoveragepass (scribe.scheduler.coverage_interval_s, CLIlorebase-scribe coverage DOMAIN) from one snapshot per Kind, Records unseen by Sources recounted every pass; the last successful run of a Source is one probe of the partial indexix_source_runs_last_succeeded; masked Attributes and Kinds outside the caller's scope are never described, and other branches and Revisions get no statistics. The two lookups take p95 3 ms at 100,000 Records and 600,000 Source runs. - Python client
CoverageonQueryResultandResolveResult,coverageonQueryRequestandResolveRequest,record_missesonResolveRequest, the vocabulary suggestion types andattribute,value,descriptiononSchemaSuggestion; CLIlorebase queryandlorebase resolveprint the coverage notes; Heraldlorebase_queryandlorebase_resolveend withcoverage:lines. - Web app, Review > Schema suggestions: ...
Lorebase 0.2.3
A governed bulk delete: delete_records removes many Records of one Kind in one Operation execution, one Revision and one rate-limit token. No migration, no breaking change.
Added
- Built-in bulk delete
delete_records(kind,keys, optionalskip_missing): deletes many Records of one Kind in one Operation execution, one Revision and onewritesrate-limit token, instead of onedelete_{kind}call (and Revision) per Record. It is governed like the other built-ins: protected branches and review,dry_run,expected_head,Idempotency-Key, and Kind-scoped grants as fordelete_{kind}; it needs the Kind'sdeletebuilt-in enabled. Links of the deleted Records follow each Relation'son_delete(cascade or restrict) as a single delete does. Unknown keys fail the call (record.not_foundper key) unlessskip_missingleaves them out with warnings. At mostkeeper.operations.max_bulk_delete_keyskeys per call (default 1000). Python clientoperations.delete_records, CLIlorebase record delete KIND [KEY]... [--keys-file FILE] [--skip-missing], and Herald lists it among the generic Operations (design 5.7, operations).
Lorebase 0.2.2
Kind-scoped invisibility completes full RBAC: a caller whose grants are narrowed to Kinds no longer learns of the other Kinds, and gets 404 or 422 where it got 403 (keeper.auth.authz.invisible_kinds: false keeps the earlier answers). Lexical search can rank with BM25 through ParadeDB pg_search, opt-in and off by default. Seven fixes for running Lorebase locally: compose rebuilds its images and migrate refuses a database ahead of the code, Keeper settings forwarded from .env, a clear message for reserved Attribute names, herald.http.allow_anonymous, Herald as the compose profile mcp on 127.0.0.1:8091, CORS expose_headers and an additive allow_headers, and the web app README for live mode. Migration 0037 adds functions only.
This release was built and published locally, without the release workflow: the images on GHCR, the checksums and the release assets are not signed with cosign, and the images carry no SBOM attestation (the CycloneDX SBOMs are attached to the release as files). cosign verify does not apply to 0.2.2.
Breaking changes
- Kind-scoped invisibility is on by default: principals whose grants are narrowed to Kinds get 404 or 422 instead of 403 for Kinds they may not read, and no longer see those Kinds in the schema, counts, search or context packs. Clients that told a forbidden Kind from an unknown one by the 403 must stop relying on it, or set
keeper.auth.authz.invisible_kinds: falseto keep the earlier answers (design 13.3). - Docker compose builds the backend, backend-ml and web images from the checkout on every
up; setLOREBASE_PULL_POLICY=missingto keep using prebuilt images (deployment).
Added
- Kind-scoped invisibility, the last part of full RBAC. A caller whose grants are narrowed to Kinds no longer learns of the other Kinds:
GET /schema(JSON and YAML) and schema elements return the schema without them and without the Relations, path Derivations, derived andrefAttributes, Traits, Operations and Sources that would tell about them, and every request naming one of them answers with the 404 or 422 of a name that does not exist, never a 403. Counts, Link summaries, neighborhoods, search hits and typo corrections, health reports, context packs and agent trace refs cover the visible Kinds only. Such callers can now read the Domain, its branches and tags, so the web app, Herald and the Guide work for them. On by default;keeper.auth.authz.invisible_kinds: falsekeeps the 403 naming the Kind (design 13.3, threat model). - Optional BM25 ranking of lexical search through ParadeDB
pg_search, for searches of very common terms on large Domains:search.backend: bm25ranks with a BM25 index of the existing search vectors, built bypython -m lorebase_store.storage applywithstore.storage.bm25_index: trueon a server that preloadspg_search. Matches, typo corrections, partial matching, Kind filters and masking are those of the default ranking; scores are relative to the best hit. Without the extension or a valid index search keeps the default ranking and/readyzreportssearch_backend: degraded.pg_searchis licensed under AGPL-3.0 and runs only where you choose its image: the compose overlaydeploy/compose/search-bm25.yamland the Helm values fileci/search-bm25-values.yamlset it up; the default stack, chart and images are unchanged. At 1M Records very common words are 20 to 35% faster at p50 and never truncated; rare words are about 3 ms slower, overlays slower, and scans slow down after writes untilVACUUM(search, performance). - Migration
0037:lb_search_terms,lb_bm25_query_validand the security definer functionslb_search_bm25andlb_search_bm25_overlay, granted to the application role like the other read functions. They need nothing of the extension until it is installed. make perf-up PERF_SEARCH=bm25runs the perf Postgres on the ParadeDB image, andtests/perf/bm25.pycompares both rankings, the index build and the commit cost on a seeded database.herald.http.allow_anonymous(defaultfalse): Herald over Streamable HTTP forwards calls without a credential to Keeper without one instead of refusing them, for local stacks whose Keeper runs with auth mode none. Keeper still authenticates each call, so under any other mode they keep failing with[unauthorized](MCP).- Herald in docker compose: the optional profile
mcpruns it over Streamable HTTP on port 8090 in the container, published on127.0.0.1:8091(HERALD_HOST_PORT; 8090 is the web app's), wired to the stack's Keeper, with DNS rebinding protection for127.0.0.1,localhostandherald.make up PROFILES=mcpstarts it with the default services (MCP). - Docker compose forwards documented Keeper settings from
.envto the Keeper services when they are set there:LOREBASE__KEEPER__AUTH__MODE,...__PROPOSALS__ALLOW_SELF_APPROVAL,...__PROPOSALS__REQUIRED_APPROVALS,...__OPERATIONS__DIRECT_COMMIT_ON_PROTECTEDand the...__CORS__origins, headers and credentials. Unset, the YAML file decides as before (deployment).
Changed
- Kind-narrowed callers get 404 or 422 instead of 403 for Kinds they may not read, see the cache entries and
ETags of their own projection, and no longer see a Kind whose keys reference an invisible Kind or whose titles or full-text index are computed from one (a searchable count of its Records); derived Attributes reading an invisible Kind, such assupplier_countandsingle_sourcedfor a reader of Parts only, are left out of their Records and of Derivation previews. Agent trace steps of other principals no longer carry result counts, query plans or atruncatedflag for them, Proposal extraction checks no longer count items for callers who do not see every Conflict, and resolving a Conflict outside their Kinds answers 404 like an unknown one. Branch responses leave out the progress of background copies for them. Proposal Conflicts about a reference to an invisible Kind are left out for them, and Record history, lineage, explanations and context packs leave out origins, messages and Proposal titles naming something invisible and the changes that touched only what is left out. - Documentation: the validation record gains the five-day scenario's CI figures, the rounds and findings of the PII vault and outbox security reviews; the README and the documentation landing page describe v0.2.1; the roadmap keeps open only the reference-hardware benchmark and the API stability guarantees (validation, roadmap).
- The standby of the compose profile
scale-outpasses the arguments of its service command to Postgres, so an overlay can add settings to it. - Documentation: the web app README explains live mode against a Keeper on another port or host, through the dev proxy (
LOREBASE_KEEPER_PROXY) or cross-origin (VITE_KEEPER_BASE_URL), and how Keeper allows the app's origin withLOREBASE__KEEPER__CORS__ALLOW_ORIGINSas a JSON list (apps/web).
Fixed
- Keeper CORS:
keeper.cors.expose_headers(defaultETag,Lorebase-Revision) makes conditional reads work for a web app on another origin, which could not read theETagand so never got a 304.keeper.cors.allow_headersnow adds to the headers Keeper always allows (Authorization,Content-Type,Idempotency-Key,If-None-Match, the request id, session CSRF and agent trace headers) instead of replacing them, and the trace headers are allowed even with tracing off, since clients send them anyway (configuration). - A Kind Attribute named after a reserved api name (
key,kind,ref,revision) is refused with a message that says why and proposes another name, such as"`key` is a reserved name, choose another (e.g. `code`)", instead ofapi name 'key' is reserved; the schema documentation lists the reserved names (design 5.4). - Docker compose no longer starts a stale local image: the backend, backend-ml and web images are rebuilt from the checkout on every
up(pull_policy: build;LOREBASE_PULL_POLICY=missingkeeps prebuilt images), andmigraterefuses a database that a newer Lorebase already migrated with an error naming the database revision, the newest revision of the code and the fix, instead of Alembic'sCan't locate revisionand a Keeper that restarts in a loop (deployment).
Upgrade notes
keeper.cors.allow_headerslists further headers only; its default is now empty. A configuration that repeats the earlier default keeps working, and one that listed fewer headers now allows the required ones as well.- Clients of Kind-narrowed principals that relied on 403 to tell a forbidden Kind from an unknown one now get 404 or 422 for both. Set
keeper.auth.authz.invisible_kinds: falseto keep the earlier answers. - Migration
0037creates functions only and runs on any Postgres; apply it as usual. Search is unchanged unless you setsearch.backend: bm25and build the index. - From 0.2.1: replace the images (or the packages) with 0.2.2, run the migration to
0037, then restart. - From 0.2.0: as from 0.2.1; the notes of 0.2.1 add nothing to do.
- From 0.1.0: follow the upgrade notes of 0.2.0 below, with the images of 0.2.2.
Not done yet
- The performance baseline on the 1M-Record fixture: the neighborhood on branches other than
mainis just over budget when the working set does not fit in memory ([pe...
Lorebase 0.2.1
Patch release for the storage profiles of 0.2.0. python -m lorebase_store.storage apply now converges when commits run during the lineage rewrite, the downgrade of migration 0036 no longer deadlocks against commits, and a repeated apply skips the full lineage scan once a pass is recorded complete (--verify forces it). The performance documentation replaces the 10M-Record storage profile estimate with measured numbers. No migration, configuration key or API response changes.
Added
make perf-upstarts the Postgres of the performance harness without running a benchmark;PERF_VM=16gsizes its memory for a Docker VM of 16 GiB (shared_buffers6GB,effective_cache_size12GB) instead of the defaultPERF_VM=8g. EachPERF_PG_*value can still be set on its own; these are harness settings, not product defaults (performance).python -m lorebase_store.storage apply --verifyscans every lineage map row again and resets what is recorded (performance).
Changed
storage applyno longer scans every lineage map row again once a rewrite has finished: it records the pass per partition instorage_taskswith a watermark, and a later run with the same profile only looks at the rows written since. At 10M Records the full scan took 13.5 minutes even with nothing to rewrite. Rows written while the triggers were turned off out of band, or frozen byVACUUMbefore a run looked at them, are only found by--verify(performance).- The storage profiles at 10M Records are measured instead of estimated (Docker VM of 16 GiB): the hot set goes from 57 GB to 44.1 GB in
balancedand 43.3 GB incompact, the cold 2-hop neighborhood from 122 to 100 ms at p50 (96 incompact), and a commit of 1,000 Part updates takes 2,711 ms at p50 inbalancedand 2,332 incompactunder the same load (performance, validation).
Fixed
storage applyno longer leaves lineage map rows a commit held during the rewrite in the other form: the page ranges they are in are rewritten again after the pass, and while rows stay held the step is deferred (exit status 3) and the next run finishes them (performance).- The downgrade of migration
0036takes the locks ofmat_recordsandmat_linksup front, in the order commits write them, each wait shorter thandeadlock_timeout: a commit waiting for it is never chosen as a deadlock victim, and neither is the downgrade.
Not done yet
- The performance baseline on the 1M-Record fixture: the neighborhood on branches other than
mainis just over budget when the working set does not fit in memory (performance). - The performance targets of the design at 10M Records: the 2-hop neighborhood, full-text search (searches of very common words above all), Record history and lineage are over budget, and a commit of 1,000 changes takes more than 2 s (performance).
- Full RBAC: Kind-scoped invisibility in queries (roadmap).
- The stability guarantees for
/api/v1bind from 1.0 (API stability). - Packages on PyPI: the wheels and sdists are attached to the GitHub release.
Upgrade notes
- From 0.2.0: replace the images (or the packages) with 0.2.1 and restart; there is no migration and no configuration to change. If an earlier
storage applystopped or left lineage map rows in the other form, run it again as the maintenance role: it finishes them, and it exits with status 3 while commits still hold rows, in which case run it again later. The first run of 0.2.1 makes the full lineage pass once and records it; later runs skip it. - From 0.1.0: follow the upgrade notes of 0.2.0 below, with the images of 0.2.1.
Lorebase 0.2.0
Second pre-alpha release. Records now carry their history and lineage, values tagged pii, confidential or restricted are masked and vault values can be erased, Scribe computes embeddings for semantic and hybrid search, path Derivations work, storage profiles decide what the database keeps besides the rows, and Guide, an optional exploration assistant outside Keeper, reads a Domain with the caller's own credential. The web app signs in through Keeper and its Modeler edits Operations, Derivations and Sources, and its copy no longer names the data of any one Domain. Pre-1.0: this minor version changes interfaces; read the breaking changes before upgrading. The database schema moves from migration 0012 to 0036, read.history.lineage_max_changes is removed, Scribe and Guide run on a new image ghcr.io/aantenore/lorebase-ml, and POST /search adds truncated_candidates. The lines link the documentation of each change: the work landed directly on main, without pull requests to link.
Breaking changes
- Webhook signing keys are sealed by default: Keeper refuses to create a webhook unless
webhooks.encryption_keyis set (webhooks.allow_unsealed_keysallows it for local development only),make envgeneratesLOREBASE_WEBHOOK_KEYfor the compose stack, and the Helm chart requiressecrets.values.webhookEncryptionKeyor an existing Secret entry. - The Helm chart no longer runs Keeper and Scribe as the owner of the tables. As on compose, it takes two DSNs:
db-maintenance-dsn(secrets.keys.dbMaintenanceDsn, the owner) for the migration Job and fordatabase.maintenance_dsnof Scribe and Keeper, anddb-dsnfor the application role (database.appRole.name, defaultlorebase_app) that Keeper and Scribe connect as, so row-level security, the append-onlyaudit_logand the ban on DDL apply to them. Adb-rolesinit container of the migration Job creates the role when missing (database.appRole.create, needsCREATEROLE) or waits for one created elsewhere, refusesSUPERUSER,BYPASSRLSor the owner, and applies the grants of the compose script; the migration runs withdatabase.application_role. With the in-cluster Postgres the operator manages the role (postgresql.cluster.roles) with the password ofsecrets.values.dbAppPassword. The chart refuses one user for both DSNs unlessdatabase.allowSharedRole: true(development). Upgrading: adddb-maintenance-dsnwith the old DSN and pointdb-dsnat the application role; an in-cluster release upgrades in one step (next line) (Kubernetes). - Kubernetes, in-cluster database: one
helm upgradenow adds the application role (kubernetes). Apg-rolespre-upgrade hook Job declares the roles ofpostgresql.cluster.roleson the CloudNativePG Cluster and waits for the operator before the migration Job runs; upgrading a release that used one DSN no longer takes two upgrades. It needs access to the API server when egress is restricted (networkPolicy.egress.kubernetesApi);database.setup.clusterRoles.enabled: falseturns it off. - Scribe refuses to start when
scribe.worker.heartbeat_interval_sis more than half ofstore.jobs.stale_after_seconds. Jobs run at least once: a job is run again after its worker is lost, so handlers must tolerate a second run (the built-in ones do). - Reading values of Attributes tagged
pii,confidentialorrestrictedneedspii.read,confidential.readorrestricted.read; other callers see them masked (design 10.2). Feeding vault values through imports and Sources needs the new Domain permissionpii.import(onlyadminamong the built-in roles), and vault key rotation needsvault.manage(installation administrators only). Give the read permissions through a custom role inkeeper.auth.authz.custom_roles. - Sensitive Attributes are left out of full-text search for every caller (see Changed): a search that matched a sensitive word no longer finds the Record.
read.history.lineage_max_changesis removed (lineage no longer reads a window of changes). Configuration files refuse unknown settings: delete it.lorebase admin erase --value Vrefuses a literal value, which stayed in the shell history: use--value -or--value @FILE. A value erased before is stored erased when written again;vault.erased_values: allowkeeps the old behaviour.- Lexical search ranks a different set of matches by default: within
search.common_term_docs(2,000) every match is ranked, beyond it the firstsearch.common_term_candidates(1,000) in index order, andPOST /searchanswerstruncated_candidates: truewhen that cut applies. Before, the firststore.reads.search_candidates(1,000) matches were ranked for every query.search.common_term_docs: 0keeps the old behaviour. Keeper needs migration0035. - Compose and Helm run Scribe and Guide on the new image
ghcr.io/aantenore/lorebase-ml(844 MB against 453 MB; fastembed and a baked embedding model). Deployments that mirror or pin images need it too; HelmimageMl.repository: ""keeps Scribe and Guide onimage(kubernetes). - Guide needs a writable state directory while its root file system is read-only: compose mounts the volume
guide-dataat/var/lib/lorebase/guideand Helmguide.persistence(an emptyDir by default); elsewhere the default is~/.local/state/lorebase/guide, so mount a volume there or setguide.mode_memory_file: null(guide). - Compose:
make up,make logs,make down,make clean-stackandscripts/quickstart.shadd the files listed inCOMPOSE_OVERLAYSafter their own and stop before compose runs when one does not exist;make downstops every profilemake upcan start (quickstart). - Migration
0036(shared lineage maps) leaves the lineage maps of an existing install inline untilpython -m lorebase_store.storage applyruns as the maintenance role, so processes of 0.1.0 keep reading them during a rolling upgrade; the space comes back only withapply --compact --reindex(store schema notes). A new install shares maps from the start. store.storage.profile(compact,balanced,fast; composeLOREBASE_STORAGE_PROFILE, Helmconfig.store.storage.profile) decides which secondary indexes exist. The default builds the same indexes as before;compactdrops the GIN index on Record values, the indexes ofindexed-only Attributes and the lexicon's trigram index whenstorage applyruns (performance).- Web app: the
supplypresets and dataset-specific placeholders are gone, example questions come only fromdomain.examplesof the schema, and the "planned screen" placeholders (Explorer, Modeler, Review, Sources, Settings, the Record page's Run Operation and the Canvas diff overlay tab) are removed. The Retrieval lab no longer embeds the Explore panel; it links to Explore. Every user-facing string moved toapps/web/src/messages, so local text patches must move with it.
Added
- Search ranks every match only when it can afford to (design 8.4, performance). Lexical search without filters reads the word lexicon's counts of the query's lexemes, which bound its matches: within
search.common_term_docs(2,000) every match is ranked; when every term is more common, only the firstsearch.common_term_candidates(1,000) matches in index order are, andPOST /searchanswerstruncated_candidates: trueso that callers narrow the query (Herald and Guide say so to the agent). Before, at moststore.reads.search_candidates(1,000) matches were ranked for every query, without saying when that cut the ranking.search.common_term_docs: 0keeps that behaviour. Measured at 1M and 10M Records with realistic text, 20,000 and 5,000 raised the p95 of title-word searches 3.4 times at 1M and 4.4 times at 10M, hence the lower defaults. Migration0035adds the security definer functionslb_search_boundedandlb_search_bounded_overlay, which report the cap. values: falseonPOST /neighborhood(agents): nodes come back as Record headers (kind,key,title) and edges as Link endpoints, for callers that need the structure only. The Python client takesvaluesonNeighborhoodRequest; Herald'slorebase_neighborhoodasks Keeper for it unless itsvaluesis true (its default leaves values out, as before, now Link values too); Guide'sneighborhoodtool takesvalues(default true).--vocabulary zipfforexamples/supply/scripts/make_supply_data.py: names, titles, summaries and reasons drawn from 40,000 terms with Zipf frequencies plus product codes, the rest of the dataset unchanged. The 1M and 10M benchmarks use it (seed databasesperf_seed_1m_zipfandperf_seed_10m_zipf;PERF_VOCABULARY=demomeasures the earlier ones);tests/perf/focus.pycompares search with and without the lexicon-sized ranking and the neighborhood with and without values on a seeded database.- Benchmarks at 1M and 10M Records (performance):
make perf-1mandmake perf-10mstart a Postgres of their own (tests/perf/compose.yaml, compose projectlorebase-perf), seed the dataset once into a kept database and measure it in place;make perf-downdeletes it. The 10M fixture loads a tenth through the commit pipeline and copies its materialized rows with renumbered keys (--replicas); an interrupted load resumes. The harness also times the agent trace through Keeper (--trace-iterations) and can bound the measured branch copy (--copy-batches,--no-pair-copy). COMPOSE_OVERLAYS(in.envor the environment): extra compose files, space separated, thatmake up,make logs,make down, `make c...
Lorebase 0.1.0
First release. Lorebase keeps a typed ontology in Postgres and lets teams change it on branches, review the change in a Proposal and merge schema and data together, with Conflicts resolved per Attribute. Agents read through a REST API that never calls a model, or through the MCP server, and write only through Operations; a write that needs review becomes a task branch and a Proposal. Documents are extracted into Proposals whose items reviewers accept or reject. Pre-alpha and pre-1.0: /api/v1 is not frozen and interfaces may change in a minor version. The database schema is at migration 0012. The lines of this first section link the documentation of each feature; from the next release they link the pull request or issue.
Added
Schema and versioning (design, schema as code)
- A Domain is a YAML schema document (
format: lorebase.schema/v1) with Kinds, Attributes, Relations, Traits, Operations, Derivations and Sources, checked statically and applied as one schema change set:POST /api/v1/domains/{domain}/branches/{branch}/schema/changes,lorebase schema validate|diff|apply. Attribute ids are stable, so a rename keeps its history. - Content-addressed Revisions, branches, tags, diff and time-travel reads (
?at=<revision>,lorebase diff,lorebase record get --at). - Three-way merge of schema and data with typed Conflicts (attribute value, delete or modify, add or add, missing value, type coercion, cardinality, dangling Link and more), resolved with
ours,theirs,baseor a custom value, one by one or from a file (lorebase proposal conflicts|resolve). Merge policiesprefer, cascade and defaults. The merged data is validated against the merged schema before the commit. - Proposals with reviews, required approvals (
keeper.proposals), Conflict resolution and merge over REST and CLI (lorebase proposal create|review|merge|close). The default branch of a Domain is protected (keeper.domains.protect_default_branch): changes reach it through a Proposal. - Expressions in CEL for Operations (preconditions, steps, review policies) and for
expressionandaggregateDerivations, type checked when a schema is applied (schema.expressions_enabled) and recomputed in the materialized head on every commit.pathDerivations are not computed.
Reads for people and agents (agents, performance)
- Records, Links, the JSON query DSL, traversal, neighborhoods and lexical search over REST (
lorebase record,query,search), with ETags and the serving Revision in every response. Full-text search reads GIN indexes on(domain_id, branch, search)and(domain_id, branch, title gin_trgm_ops)(migration0011,btree_gin), so a search reads only its own branch's matches, however many branches share the partition. Semantic search takes vectors supplied by the caller; nothing computes embeddings. - Context packs within a token budget (
POST .../context,lorebase context), Record explanations with the origin of each value (GET .../records/{key}/explain,lorebase explain) and data health reports (GET .../health,lorebase health), all computed without a model. - A change feed as Server-Sent Events and as polling (
GET .../branches/{branch}/events,.../changes) and signed webhooks (POST .../webhooks,Lorebase-SignatureHMAC). - Overlay branches (store schema notes, migration
0010): a branch created withmode: overlay(POST .../branches, defaultstore.materialize.branch_mode, always for names underagent/,extract/andsource/, and for agent task branches perkeeper.agent_writes.branch_mode) stores only the rows it changes and reads its base as of the fork point. Creating one does not depend on the size of the Domain; it is promoted to a full copy abovestore.materialize.overlay_max_rowsrows oroverlay_max_base_lag_revisionsgenerations of base lag, before a schema change and before its base is deleted.lorebase branch create --mode copy|overlaychooses the mode from the command line. Overlay reads go through_overlayvariants of the security definer functions with the same argument and scope checks. An overlay gets the per-Attribute indexes of its schema on its own rows, built asstore.indexjobs when it is created, and its neighborhood looks Links and Records up with one index scan over both branches (migration0012); on the 100k-Record baseline search, attribute queries, Links of a Record and the neighborhood stay within 30% of a copy at p50 (performance). - Per-Attribute indexes and security definer read functions that keep Domain isolation under row-level security. At 99,900 Records and 302,940 Links on one laptop, the measured p95 of every agent read stays within its budget (performance); the 1M-Record baseline is not measured.
Writes and ingestion (quickstart, scribe, inference)
- Built-in Operations
import_records,import_linksandimport_batchwith dry run and idempotency keys, and custom Operations declared in the schema (POST .../operations/{operation}/execute). lorebase import records|links|batchload CSV, TSV, JSON Lines or JSON (Parquet with theparquetextra): types are converted from the schema, a Record travels with its Links, files are cut into batches of one Revision each.- Schema inference from CSV, TSV, JSON, JSON Lines (
.jsonl,.ndjson) and Parquet uploads, chosen by suffix (keeper.schema_inference.file_formats), and from Postgres tables (POST .../schema:infer): Kinds, typed Attributes, keys, Relations with cardinality, a confidence and the evidence for every decision. It proposes and writes nothing. Parquet uploads need thelorebase-keeper[parquet]extra; without it they are refused with a 422 naming it. A record profiler (lorebase_store.profiling.profile_records,RecordProfiler) andprofile_sourcein Scribe profile any Source through its connector; limits are instore.profiling.records. - CSV and Postgres Sources with snapshot and incremental syncs run by Scribe, the worker and scheduler on the Postgres job queue. A sync commits on a Source branch, opens a Proposal and applies the Source's merge policy (
proposal,auto_when_clean,auto_with_authority); every changed value keeps its row as Lineage (lorebase sources sync|runs). - JSON, JSON Lines, Parquet and HTTP JSON Sources (scribe): the
jsonandjsonltypes and therecords_path,flatten_separatorandhttpoptions (headers as secret references, cursor, page, offset or Link header pagination, a watermark parameter and a page cap). Files are opened under the allowed roots without following links, URLs are checked against private addresses, JSON Lines and Parquet are streamed, and an HTTP JSON run that passesmax_pagesfails instead of committing part of a snapshot. Parquet needs thelorebase-scribe[parquet]extra. - Document intake and tiered extraction into Proposals (extraction):
POST .../documentstakes Markdown, plain text, PDF and DOCX (PDF and DOCX with the scribedocumentsextra); Scribe parses and chunks them and proposes Records, Links and values with verified evidence, from rules (tier 0, no model), local GLiNER and GLiREL (tier 1) or any OpenAI-compatible model with JSON schema output (tier 2, cached, token budget per job). Reviewers accept, edit or reject items one by one or by filter (GET .../proposals/{p}/items,POST .../items:decide,lorebase documents upload,lorebase proposal items|decide); nothing reaches the default branch without a merge. Derived and sensitive Attributes are never extracted.GET .../documents/{id}/text(lorebase documents text) serves the parsed text the evidence offsets index. The web app has a Documents screen (intake, library, a viewer that highlights the evidence in the parsed text) and an Items tab on extraction Proposals to accept, edit or reject items one by one or by filter. - Inference providers behind one OpenAI-compatible interface for chat and embeddings, plus local fastembed, GLiNER and GLiREL (optional extras), and compose profiles for Ollama and a LiteLLM proxy. Extraction uses them; nothing computes embeddings yet.
Agents and access (MCP, operations)
- Herald, the MCP server, over stdio and Streamable HTTP with 11 tools (
lorebase_context,lorebase_query,lorebase_executewith dry run and more), two resources and one prompt. It is a thin layer over Keeper and holds no database credentials.lorebase_executepassesintent,auto_proposeandauto_mergeto Keeper also when the call names notask, and returns Keeper's problem text unchanged;auto_propose: truewithout ataskis refused asvalidation-failed. - Agent writes that need review (agents): an Operation refused with
409 review-required(protected branch or review policy) runs, when the call names atask, on a task branchagent/{principal}/{task}and opens or updates one Proposal, merged at once only whenkeeper.agent_writes.allowed_roles_for_auto_mergeallows the caller's role, nothing conflicts and the required approvals are in. Without ataskthe409carriesnexthints. Settingskeeper.agent_writes, overridable per Domain. - API keys (hashed with a server-side pepper), OIDC bearer tokens, role grants per Domain, Kind, Operation and branch, and a hash-chained audit log (
lorebase apikey,GET .../audit).python -m lorebase_keeper bootstrapcreates the first installation administrator key from the database settings and prints it once; it refuses when an administrator already holds a usable key, unless--force. - Rate limits per principal and request class (
RateLimit-*h...