Repository navigation
Releases
0.0.41
Added
!MergeEntity {absorbed, into, reason, evidence}applies (story:merge-entity-applies, design
§ 111, ADR 0018): it joins a duplicate node to the node it duplicates — of one type, or of types
related throughparents— in a revision of its own. The absorbed node leaves the head into a
supersession record naminginto, as aSupersedeNoderetires a node, and every reference to it
is rewritten in place: each edge at it keeps its id and runs tointo, an edge joining the two is
retired into the record, and one duplicating an edgeintoholds, with equal properties,
collapses onto it; each assertion about it, naming it as its object or in aNodeRefinside its
value, namesinto, every other byte unchanged; eachNodeRefnaming it in a node's or an edge's
properties namesinto; and a property only it holds moves tointo. The record'smerge
lineage — undersupersededinekr snapshot— says what moved. The absorbed node reads as a
superseded node does;/changeslists the merge as aNodeMergedchange, counted in
nodes_merged, withintointarget. Applied under every validation profile. Refused by name:
merge-into-itself,unresolved-node,node-superseded,merge-without-reason,
merge-without-evidence,merge-evidence-unknown,merge-types-unrelated,
merge-undeclared-property,merge-property-conflict,merge-duplicate-edge-conflict,
merge-self-relation(an active relation assertion joining the two nodes, either way: retract
it first),edge-endpoint-type,wrong-type,edge-cardinality,property-cardinalityand
superseded-node-named-in-transaction. The bare{absorbed, into}shape is refused as before,
unsupported-operation, under every profile, so retained rejections replay; a store that never
merges keeps its roots and checkpoint bytes.ekr comparecompares a merged node's lineage in
itssupersededcategory. The SDK builds the bare shape withEntityMerge::bare, reads
SnapshotSupersededNode::mergeandChangeKind::NodeMerged, andResolver::observedrops every
cached answer naming a node a committed merge absorbs.
Changed
- Breaking: the SDK's
EntityMerge::new(absorbed, into, reason, evidence)takes the reason
and the evidence, no longer only the two nodes, and builds the shape that applies; a caller that
wroteEntityMerge::new(absorbed, into)for the bare P1 shape writesEntityMerge::bare(absorbed, into), which every profile still refuses. - Breaking:
EntityMergeis no longerCopy, in the SDK (ekr_sdk) and in the kernel
(ekr_kernel): each gainedreason: Option<String>andevidence: Option<BTreeSet<EvidenceId>>.
A caller that copied one clones it.
0.0.40
Fixed
SupersedeNodereview fixes (story:supersede-node-review-fixes).describe_node/1 of a live
node no longer lists or counts, underreferencing, an assertion whose subject is a superseded
node or an edge retired with one, and the overview's roles date no node from one, as its
timeline already did.Resolver::observedrops every cached answer naming a node a committed
SupersedeNoderetires.ekr apply-extractionandekr_sdk::extraction::applycheck a fact
against the type of the node its key resolves to: a key superseded by a node of another type
takes the kept node's properties and edge ends, a fact the kept type does not admit is refused
before anything is written — no node is created for it — and a refusal names the supersession
only where that end caused it. A validation and a sample look retired edges up in one index of
the supersession records instead of scanning them per lookup; no verdict changes. Documents:
design § 110 andgraph.yamlsay an edge between two nodes superseded
together is held in its source's record;docs/cli.mdsays onlyekr viewanswers the refusal
with 404;views.yamlsays overview totals count every held assertion while projections and the
timeline omit those about superseded or retired subjects.
0.0.39
Added
!SupersedeNode {node, by, reason, evidence}retires a duplicate node in favour of the node
that replaces it, in a revision of its own (story:supersede-node, design § 110). The node and
every edge at it leave the head and are kept in a supersession record — the node and its edges
as they were,by, the revision, the reason and the evidence — whichekr snapshotlists under
superseded; assertions about it are not changed, and every earlier revision still reads it.
Every head read omits it.ekr resolveof its type and one of its aliases answersby, even when
byis of another type, and those aliases stay taken (alias-already-exists). A read of its id
at the head —/node/<id>in either format,/expand, the MCPdescribe_nodeandexpand, the
session's — is refused asekr.views.NodeSuperseded, namingby;/changeslists the
supersession as aNodeSupersededchange, with the node kept intarget. Refused by name:
supersede-unknown-node,node-already-superseded,supersede-into-itself,
supersede-by-unknown-node,supersede-by-superseded-node,supersession-without-reason,
supersession-without-evidence,supersession-evidence-unknownand
superseded-node-named-in-transaction; a later operation naming a superseded node or a retired
edge isnode-superseded, namingby. Applied under every validation profile. The SDK builds
it (NodeSupersession) and readsSnapshotGraph::supersededandChangeKind::NodeSuperseded.
A store that never supersedes keeps its roots and checkpoint bytes. A read that reaches an
assertion naming a superseded node names its pointer:subject_superseded_byand
object_superseded_byinekr sampleandekr explain,object_superseded_byon every rendered
assertion (SDKSampledFact,ViewAssertion,SnapshotAssertion)./timelineof a superseded
subjectisekr.views.NodeSuperseded;ekr comparecompares the records as asuperseded
category, listed only where either store holds one.ekr apply-extractionreplaces a value
under a superseded key on the node kept, and a fact refused after resolving through a
supersession to a node of another type names it.
0.0.38
Added
ekr mcp-httpandekr viewtake--metrics, which servesGET /metricsin the Prometheus
text exposition format, and--request-log <path>(-for standard error), which writes one
ekr.request-log/1JSON line per request. Both count each read at their dispatch by its wire
name and outcome under one set of names:ekr_read_requests_total{tool,outcome},
ekr_read_duration_seconds{tool},ekr_read_result_bytes{tool},ekr_read_in_flight,
ekr_store_head_revisionandekr_store_query_errors_total{tool}, so one read through either
server moves the same series. A log line carries the request'sX-Request-Idand
traceparentwhen they are well formed, generated ones otherwise, and never an argument, a
result, document content or a credential./metricsis admitted as every other path is and
answers 404 without the option (ekr.views.ReaderMetricName,ekr.views.RequestLogV1,
ekr.cli.ReaderTelemetryOptions).ekr.node-matches/3, a search that ranks a whole name or alias first whatever its case
(story:search-match-kinds). Underekr.node-matches/2a case-exact match comes before a
case-folded one, so a node whose whole name is the text in another case ranked below every node
holding the text case-exactly inside a longer name or alias, and a reader of the first few
matches missed it. /3 matches the nodes /2 matches, with the sametype,fromandto, and
names each match'skind—WholeName,WholeAlias,Prefix,SubstringorWords— and
folded, whether it matched only after lowercasing. Matches rank by kind, then case-exact
before case-folded, then (with a window) newest first, degree and id;meta.whole_total
counts the whole matches beforelimit.ekr view's/searchand the session verbsearch
answer it withformat=ekr.node-matches/3(--format), and the SDK reads it as
Reader::search_v3,NodeMatchesV3.ekr.node-matches/1and/2answer the bytes they did.
Changed
- The
searchtool ofekr mcpanswersekr.node-matches/3when noformatis named (it
answered/2); nameekr.node-matches/2or/1for the earlier documents.
0.0.37
Fixed
ekr stage beginstages a store whose seed or commit carries thousands of evidence payloads on
PostgreSQL (#93). The begin's copy published each occurrence as one provider group, and
PostgreSQL takes one advisory lock per stream of a group, so a seed of 8,192 payloads was
refusedstage-copy-failed, "out of shared memory", on a stock-sized lock table. A copied
occurrence that would carry more than 32 streams now has its payloads stored ahead of it in
groups of at most 32 (ekr.store.CopyGroupStreams), with their class andstored_at, and its
own group binds only the objects it appends, so no transaction of the copy takes more than 34
advisory locks (32 streams, the publication gate and the group's own).ekr seed,
ekr commitandekr migrateare unchanged: a migration still writes each copied seed or
commit as one transaction, into a destination another migration may also be writing, and
docs/cli.mdnames the lock-table setting that bounds all three.ekr stage liston a PostgreSQL store answered[], or left out a stage, while any other
transaction on the server was open, although the stage's begin had committed and
ekr stage showfound it (#94). A begin now records the stage in the store's stage index,
ekr.store.StageIndexed, in the same atomic write as itsStageBegun, and the listing reads
that index as a stream; stages begun before this release are still found through the log.
0.0.36
Added
-
ekr.node-detail/2, one node bounded for an agent's context (ekr-mcp-fixes-20261015 #1, #10,
#11).ekr.node-detail/1lists every assertion and edge of the node, so a node with thousands
of edges answered megabytes./2answers the record with its type's name;same_name, the
other nodes sharing its name or an alias, compared lowercased; its property values grouped by
property and named, newest first, a String over 2,000 bytes cut on a character boundary and
markedtruncated; and its neighbours grouped by edge type and direction, each group named
and counted, listing at mostlimit(10 by default, up to 500) by degree, each with the newest
Relation assertion between the two.edge_type(an id or a name) andafterpage one edge
type's groups. Anekr.node-detail/2request for a node type or edge type id is refused as
ekr.views.NodeIsAType, naming the type;/1still answersekr.views.NodeNotFoundfor it.
ekr viewserves it at/node/<id>?format=ekr.node-detail/2[&limit=L][&edge_type=E][&after=A]
and the session'sdescribewith--format ekr.node-detail/2; both keep answering/1by
default, byte for byte as before. -
A search format,
ekr.node-matches/2, served byekr view's/searchand the session verb
searchwhen asked for (format=ekr.node-matches/2,--format ekr.node-matches/2) and by the
MCP toolsearchby default. A text of two or more words also matches a name or an alias
holding every word in any order, as the new tierWords(widget routerfinds
Widget-Router); within a tier a node whose whole name or alias is the text comes first (eu
before a busierprod-eu-central-9); every match carriestype_nameand, when the node has
one, its timelinetime.type(a node type's id or name) lists only that type's nodes, an
empty text listing them all, andfromandto(milliseconds orYYYY-MM-DD) only nodes
whose time falls in the window, newest first. Atypenaming no node type is refused as
ekr.views.NodeTypeNotFound.ekr.node-matches/1is unchanged and stays the default of
ekr viewand the session verb; the SDK'sNodeMatcheskeeps reading /1. -
The MCP tool
searchtakestextasqueryorqas well; naming two of them is refused. -
Named formats, so an answer names the types and properties it cites and needs no ontology to
read:ekr.graph-overview/3(eachnode_typesandedge_typesentry with itsname, and
meta.committed_at, when the revision read was committed),ekr.graph-slice/2(each node and
edge with itstype_name),ekr.graph-timeline/2andekr.graph-changes/2(withnames, an
object mapping every node type, edge type and property id the document holds to its name),
ekr.explanation/3(ekr explain's document withnames) andekr.resolution/1(ekr resolve's outcome underoutcome, withnames).ekr viewserves them when asked for
(format=on/overview,/expand,/timelineand/changes), so do the session verbs
(--format),ekr explain --format ekr.explanation/3andekr resolve --format ekr.resolution/1. Without a format each answers what it answered before; the SDK keeps
reading the earlier formats. -
ekr.explanation/3answers the evidence the chain cites, within a whole-answer cap.--scope cited(its default) keeps only theEvidencelinks the chain's assertions and attachments cite
and counts the rest asomitted_evidence, whereekr.explanation/2lists every evidence entry
of the transactions on the chain (247 for an assertion citing one, as measured);--scope all
keeps them all. With--documentsit carries no proposal record and no base64payloadbeside
atext, and the printed answer is at most 48 KiB: the longest evidence text is halved, and
markedtruncatedwithoffsetandrecord_length, until it fits.--scopeneeds--format ekr.explanation/3;ekr explainwithout a format printsekr.explanation/2unchanged. -
The MCP tools take a few arguments under a second name:
describe_node'snodeasidor
node_idand itsedge_typeasedge,expand'sseedsasseed,nodeornodes(and one id where a list was
required),explain'sassertionasidorassertion_id, andtimeline'ssubjectas
node. Each tool'sinputSchemalists them. An alias answers exactly what the argument's own
name answers; naming one argument twice is-32602.ekr viewis unchanged.
Changed
- The MCP tool
describe_nodeanswersekr.node-detail/2by default, byte for byte what
ekr viewserves for the same request with the format named, and takeslimit,edge_type
andafter;full: trueanswers the wholeekr.node-detail/1as before. - The MCP tool
searchanswersekr.node-matches/2unlessformatnames
ekr.node-matches/1: an agent readingtierseesWords, and each match gainstype_name.
For one request with the format named, its answer is byte for byte whatekr viewserves. - The MCP tools
overview,expand,timeline,changes_since,explainandresolveanswer
the named formats unlessformatnames the earlier one (ekr.graph-overview/2,
ekr.graph-slice/1,ekr.graph-timeline/1,ekr.graph-changes/1,ekr.explanation/2,
outcome);explainanswers scopecitedunlessscopenamesall. An agent reading
resolvereads the outcome underoutcome. For one request with the format (and for
explainthe scope) named, each answer is byte for byte whatekr view, the session verb,
ekr explainorekr resolveanswers. - The MCP tools answer smaller default pages, since an agent reads a tool result whole:
expand
takesedgesas 200 (was 5,000) andchanges_sincetakeslimitas 100 (was 500) when the
call names none. The answer is whatekr viewand the session verb answer with that bound
named; their own defaults are unchanged. A page ends at whichever of its limits comes first,
and thedescribe_nodeandexpanddescriptions say how to ask for the next page. changes_since,GET /changesand the session'schangesderive each revision's changes
from its transaction document once while the server keeps its indexes: a second request over
the same revisions parses no transaction document, and one after a commit parses only the
revisions it added. Answers are unchanged.ekr_views::Index::transaction_documents_parsed
counts the documents parsed.ekr mcp-httpanswersinitialize,ping,tools/list, notifications and every transport
refusal on the request's own connection, never behind the store's queue: a client's handshake
no longer waits for another client's tool call or for the store's first open, and is answered
(with the same bytes) before the store is admitted. Onlytools/calland/readyzwait for
the store.
0.0.35
Fixed
ekr stage beginwhose copy into the stage's tenant does not land no longer answers
publication outcome is unknown, which named no stage although the begin had recorded one. It
abandons the stage and empties its tenant as far as each lands and refuses with the new named
refusalstage-copy-failed(exit 2), naming the stage, whether it isAbandonedor still
Begun, whether its tenant may hold part of the copy, and the copy's own error;
ekr stage abandon <stage id>finishes what the begin could not. The store's head and history
do not move.- On PostgreSQL a write whose outcome the provider cannot tell is tried again only while less
thanpool.transaction_timeout_mshas passed since its first attempt, at most 16 times as
before, so a write that runs into that deadline answers within about two deadlines instead of
sixteen. A stage begin of a large store needspool.transaction_timeout_ms(with
statement_timeout_msandlock_timeout_ms) long enough for its largest copy group; SQLite and
File stores retry as before.docs/cli.mddescribes both.
0.0.34
Added
-
ekr compare --other <store>says whether two stores hold the same knowledge, on any providers,
each at its head or at--atand--other-at: theekr.store-comparison/1document, equal or
not, and per category (types, properties, nodes, edges, assertions, evidence) the counts on each
side and the first differing ids with the fields that differ. Commit times and transaction ids
are not compared, so two stores built from the same documents at different times compare equal.
It writes to neither store.docs/cli.mddescribes it. -
A schema change can cite the evidence that introduced it.
!CiteEvidence <evidence id>beside
the schema changes of a transaction names evidence the store retains, or evidence an
!AddEvidenceof the same transaction adds, and that!AddEvidenceno longer makes the
transactionmixed-schema-transaction; any other data operation still does. The schema version
the commit makes records every cited id, andekr ontology(with or without--at) prints them
asschema_version_evidence. Under validation profiles v2 and v3 only; v1 refuses the kind as
unsupported-operation. A citation is refused asunresolved-evidence,
citation-without-schema-change,duplicate-citationorevidence-set-mismatch(the cited id
belongs intransaction.evidence).ekr operationslists seventeen kinds. A version that cites
nothing encodes as before, so no existing ontology root moves; a store holding a citing version
is refused by earlier binaries. The projection (ekr.graph-projection/1, whatekr view
serves at/projection) carries a version's citations asevidenceon itsschema.versions
entry, omitted when it cites none, so a projection without a citation is unchanged; the viewer
page does not display them yet. Inekr-sdk,read::Ontologycarries
schema_version_evidence, andTransactionBuilder::buildrefuses a citation without a schema
change asDocumentError::CitationWithoutSchemaChange, the kernel's
citation-without-schema-change, rather thanMixedSchemaTransaction.
Changed
-
One-shot
ekr snapshotandekr ontologyanswer from the retained replay checkpoint, which
serves as a persisted read model, when the kernel verifies it for the revision read. They then
load the revision's record, the seed envelope and every evidence payload, and decode the
record with the checkpoint. They replay nothing and no longer read, hash or decode every record of the history
(design § 108). Answers are byte for byte what a complete read prints. A model that does not
verify is ignored, and the read answers as before.- Still served by a complete read, as before: a snapshot of a head past the checkpoint's
revision (up to four commits made one process each, § 99.5.1), a snapshot--atany revision
other than the checkpoint's, a schema changed since the checkpoint, any head read after a
proposal or validation no checkpoint pointer covers yet,--full-replay, and every read after
the first on a long-lived handle (ekr session). - A retained record damaged after the checkpoint pointer that covers it was written is no longer
refused by anekr snapshotorekr ontologyserved from the model, which does not load it.
This is whatekr headalready did since § 99.2. The next read that loads the record refuses
it: any write verb,ekr explain,ekr transactions, or any verb under--full-replay.
A damaged or absent seed envelope or evidence payload is still refused by both verbs: the
model loads and verifies each one a complete read requires (design § 108.2 item 7).
- Still served by a complete read, as before: a snapshot of a head past the checkpoint's
-
overviewanswersekr.graph-overview/2and takessections, any ofontology,schema,
rolesandtimeline, onGET /overview(sections=ontology,schema), the MCP tool
("sections": ["ontology"]) and the session verb (--sections ontology,schema). Named none, the
answer is the counts, the type counts and the top nodes only, so its size no longer grows with
the ontology's properties, the schema's versions or the revisions committed. Named all four, it
isekr.graph-overview/1's content under the new format name. A section not named is absent,
never empty. A client that reads the ontology, schema, roles or timeline fromoverviewnow
names them; the viewer page and the SDK'sReader::overviewname all four. The SDK's
Overviewholdsontology,schema,rolesandtimelineasOptions, and
Reader::overview_sectionsnames the sections to read.
0.0.33
Added
- A run can be staged:
ekr stage begincopies the store at its head into a stage of its own and
prints the stage id;--stage <id>orEKR_STAGE=<id>joins every verb that opens an existing
store to that stage;ekr stage publish <id> --expect-head <revision>appends the stage's
revisions to the store in one append group, or refuses by name and changes nothing;ekr stage abandon <id>drops the stage;ekr stage listlists a store's stages. After a publication or
an abandonment the stage's tenant holds nothing. A retried publish returns the first one's
result and appends nothing twice. Stages run on SQLite and PostgreSQL; the File provider
refuses them.docs/cli.mdlists the verbs and their refusals. - A PostgreSQL store's inventory is read under one provider capture, so a stage can be begun from
a PostgreSQL store.ekr migratestill refuses a PostgreSQL source.
Changed
- Every store opener refuses a tenant containing
ekr.stage:asstage-tenant-reserved; that
marker names stage tenants only. - A stage publication is elected through a new retained format,
ekr.publication-preparation/4. A store holding one is refused by earlier binaries as
inventory-preparation-unreadable. - The Eventlog crates move from 0.5.0 to 0.8.1. The File provider's appends and in-window reads
no longer grow with the journal's size.
0.0.32
Changed
ekr explain --documentsand the MCPexplaintool withdocuments: trueanswer at most
64 KiB of each evidence record, centred on the cited text (the first string value of an
assertion on the chain that the record holds), instead of the whole record. A link that answers
less than its whole record carriesoffset,record_lengthandtruncated: true; a record
within the bound is answered whole and unchanged. Proposal records and commit receipts are
still answered whole.
Added
ekr explain --documents --offset N --limit N, andoffsetandlimiton the MCPexplain
tool, read any byte range of each evidence record. Offsets are raw byte offsets: a range that
cuts a character answerspayloadwithouttext. Steps oflimitfrom offset 0 to
record_lengthreassemble the record byte for byte. Both refuse withoutdocuments, and a
limitof 0 is refused.- An
ekr.postgres/1configuration takes an optionalpassword_fileholding exactly
{"password": "..."}, the document a saved PostgreSQL connection hands a launched program, so
the connection file and the configuration directory hold no secret. An absolute path such as
/proc/self/fd/3is used as written. A connection file that already carries a password, a
document with another field or a non-string password, and a document over 64 KiB are refused
aspostgres-configurationwithout echoing the value.
Fixed
- A running
ekr mcp,ekr vieworekr sessionno longer answers a discarded revision after
its SQLite store is restored to an older snapshot through SQLite's online backup and a new
commit reuses the revision number. Their indexes and/projectionand/rolesanswers are
kept under the revision's identity — its revision id and root — and each read drops one the
store no longer holds, sosearch,overview,describe_node,expand,timelineand
changes_sinceanswer the new revision. While anything is kept, a read also reads the store's
retained transaction records.ekr_views::LoadedRevisionEntrycarries each revision's
revision_idandroot;ekr_views::RevisionIdentityandekr_views::Lineageare new.