Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Changelog

All notable changes to gonzalo are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. While the project is pre-1.0, the minor version is bumped for new features and the patch version for fixes.

Unreleased

Added

  • gonzalo purge removes one tombstone, discarding its restore window. It refuses a live record and an absent key (exit 1), takes no prompt, and prints the key and revision. (ADR 0030, #333)
  • Deleting a manifest is now recoverable. Record gains an additive deleted_kind field (tombstones only; absent on every existing record). A GraphManifest or VectorManifest tombstone now retains the deleted body instead of discarding it, and gonzalo gc marks through that body, so a deleted index's slices or shards survive until collect removes the tombstone. gonzalo_core::undelete, the gonzalo undelete subcommand and the facade re-export restore a manifest from its tombstone, preserving meta.created. A put that creates over a manifest tombstone is refused (see below). An older binary's gc does not read deleted_kind and frees the shards the tombstone pins, so upgrade every binary that runs gc against a store first. (ADR 0029, #327)
  • BlobStore::has_blob. A defaulted method (get_blob(..).is_some()), overridden by the filesystem store with try_exists, by S3 with HeadObject, and by the remote store with the presence route below. A failure that is not "no such key" is an error, not false. (#325, #329)
  • HEAD /v1/blobs/{hash} and the HasBlob gRPC RPC answer whether a blob exists without reading it, and ServerStore::has_blob now uses them instead of the downloading default. A writer confirming its referenced blobs survived a commit previously pulled every one of them over the daemon — 600 KB a shard for a vector index, on every interactive upsert. Additive on both surfaces. Two notes for existing callers: HEAD /v1/blobs/{hash} already answered 200/404, because axum serves HEAD from the GET handler and discards the body; it now reports content-length: 0 rather than the blob's size, since a size needs the read the route exists to avoid. GET the blob if you need its length. Over gRPC a daemon with no HasBlob RPC earns DAEMON_PREDATES_BLOB_PRESENCE ("upgrade gonzalod") rather than a silent fall back to downloading, which would keep paying the cost with nothing to show why; over HTTP no such message is needed, because axum serves HEAD from a route's GET handler and an older daemon therefore answers anyway. (ADR 0028, #329)
  • A sweep policy for blob GC. SweepPolicy, DEFAULT_MIN_AGE (one hour), sweep_blobs_with and gc_blobs_with let a caller set the minimum blob age explicitly; gc_blobs and sweep_blobs keep their signatures and use the default. GcReport gains deferred, the unreferenced blobs held back for being too young, and freed now comes back in hash order. (#325)
  • gonzalo gc --min-age <duration> (default 1h, same spellings as collect --older-than: one positive whole number and one unit of d, h, m or s). The gc and index --gc summaries print deferred. (#325)
  • Vectors now survive the process that built them. A new RecordVectorIndex persists a VectorIndex behind sharded, content-addressed blobs named by a new RecordKind::VectorManifest record (MergeClass::Opaque, since caller-supplied vectors can never be re-derived the way a code-graph manifest can). open(store, key, space, dim) declares an embedding space and dimension and errors naming both values on a mismatch, at open and again on every OCC conflict, so two handles racing to create the same missing index can't silently mix models. Shard assignment (shard_of) and the default shard count (DEFAULT_SHARDS, 256) now take std::num::NonZeroU16, so a zero shard count is unrepresentable rather than a runtime panic waiting to happen. gonzalo gc marks a vector manifest's shard blobs as live, the same way it already marks a graph manifest's slices, so running GC against a store holding a durable vector index is safe when no writer is mid-commit. A sweep that lands between a write's shard upload and its manifest commit used to delete the new shard and leave the index unopenable; see the grace-period fix under Fixed for how that is now narrowed, and what remains. A durable index's writes commit under optimistic concurrency, retrying up to 5 times on a conflict. Each commit is checked against the manifest revision the writer's in-memory state came from, so two writers on the same shard cannot silently overwrite each other; and a conflict reloads every shard that changed, not only the ones the write touched, so a writer that lost a race cannot later overwrite the winner's vectors from stale memory. The gonzalo facade's vector module (behind the vector feature) re-exports RecordVectorIndex, DEFAULT_SHARDS, shard_of and VectorManifest, so a facade consumer can reach the durable index without depending on gonzalo-vector/gonzalo-core directly. See ADR 0027. (#323)
  • KnowledgeStore::open rebuilds chunk counts from a durable index. KnowledgeStore.chunk_counts only ever lived in memory, so a re-ingest of a shrunk record after a restart used to leave its high-ordinal chunks behind forever once the index itself became durable. open(store, index, embedder) scans the index once via the new VectorIndex::keys (below) to rebuild counts at construction; KnowledgeStore::new is unchanged and still starts counts empty, which is correct for a fresh in-memory index. (#323)
  • Records carry their times. Meta.created and Meta.updated were always 0; the store now stamps both inside the same critical section that decides the write, exactly as it stamps a tombstone's deleted_at. created survives every edit, a delete counts as the record's last update (its updated equals the tombstone's deleted_at), and a recreation over a tombstone starts a new created, because that is a new record at an old key. A replication write keeps the source's times, so syncing a record is not an edit. A client's values are overwritten: times a caller can set are times a caller can lie about. The conformance suite asserts all of it, so every substrate agrees. (#293)
  • Builders for the common record paths. Record::create makes a record that doesn't exist yet, and record.update(body, meta) makes the next version of one you read: it sets the revision to follow and parent to the revision it was built from, which is the pairing OCC depends on and the easiest thing to get wrong by hand — a mismatch doesn't fail at build time, it surfaces later as a spurious conflict. Meta::new(author, origin_system) covers the provenance. Fields stay public, so nothing existing breaks. (#305)
  • Blob garbage collection, and tombstones pin their blobs. gonzalo gc now marks against every record in the store instead of a caller-supplied list of manifests: a record whose body is a blob, the blob a tombstone pins, and every code-graph slice a manifest names. A tombstone carries the deleted record's blob hash in the new Record.deleted_blob, so a delete never destroys content a peer can still sync back, and re-putting the same content doesn't re-upload it. Reclaiming a deleted record's bytes is three deliberate steps: delete, collect past the horizon, then gc. gc_blobs takes the store and does its own listing; sweep_blobs and live_blob_hashes are the halves, for a caller that already knows its live set. See ADR 0024. (#292)
  • Records over MCP, and an MCP server that can talk to a daemon. gonzalo-mcp exposed only the code graph, so an agent could ask what calls a function but could not read the records the store exists to hold — the gap both the Zep and Letta parity matrices meant by "no MCP surface", while their rows wrongly claimed there was no MCP server at all. It now also exposes record_get and record_list, with record_put and record_delete behind --allow-writes (or GONZALO_MCP_ALLOW_WRITES=1) and absent from the advertised tool list without it, so an agent never sees a tool it cannot call. GONZALO_DAEMON (or --daemon <url>, with GONZALO_TOKEN) points the server at a running gonzalod instead of a local directory, so it can share a store rather than answer from one machine's copy. An absent key is an error naming it, never an empty result, for the same reason an unknown view is. Vector and knowledge recall over MCP are tracked separately (#317). (#197)
  • Published schema artifacts, so non-Rust clients are generated rather than maintained. Client SDKs are 🔴 in all three competitor matrices, each suggesting the same fix: generate one from the daemon's schema. But gonzalo-proto was an internal workspace crate and the HTTP/JSON surface had no description at all, so the daemon's non-Rust promise (ADR 0007) was unfulfilled at the packaging layer. Every release now attaches gonzalo-vX.Y.Z.proto and gonzalo-openapi-vX.Y.Z.json to its GitHub Release, and the OpenAPI document lives in-tree at docs/api/openapi.json. They cannot drift: SERVED_OPERATIONS is checked against the router's own source and against the document, in both directions, so an operation added without describing it fails CI and so does one described but not served. The guide gains a Generating a client chapter with a worked Python example. No client code in any other language enters this repo (ADR 0020). (#198)

Changed

  • BREAKING: RecordVectorIndex::open refuses a deleted index. Opening an index whose manifest is tombstoned used to succeed, hand back a handle onto an empty in-memory index, and fail only at the first commit — so an operator who deleted an index and restarted saw open succeed, queries return nothing, and the real explanation arrive on the next upsert. open now reads raw and errors with CoreError::Invalid, naming the key and the two ways out (gonzalo undelete, gonzalo purge). The trigger is the same condition plan_put uses — the stored tombstone's deleted_kind naming a manifest kind — so open refuses exactly when the first commit would have, and a tombstone with no restore window (written before #327) still opens fresh rather than stranding the key. There is deliberately no flag to skip the refusal: a caller starting over purges first, which keeps discarding a restore window one explicit operator action. One side benefit: a refused reopen now stages no shard blob, where the commit-time refusal left one orphan for gc. (ADR 0029, ADR 0030, #340)

  • BREAKING: the raw replication write requires an admin token. PUT /v1/raw/records/{ns}/{col}/{id} and the PutRaw RPC authorized write on the namespace; they now require admin, matching POST /v1/purge/... and Purge. A replication peer configured with a non-admin token begins getting 403 / PermissionDenied and must be re-credentialed — ADR 0015 already described an admin token as the replication credential, so a correctly configured peer is unaffected. The route writes verbatim and so reaches past the plan_put guard added above, which let a principal with plain Write discard a deleted manifest's restore window by aiming a fresh record at the tombstone's revision — predictable, because tombstone_hash() is a constant. Authorship now carries across stores unconditionally, since the non-admin restamping that hardened the old path is no longer reachable. (ADR 0031, #342)

  • BREAKING: the daemon refuses to purge a live record. POST /v1/purge/... and the Purge RPC now answer 400 / InvalidArgument when the named revision matches a live record, as gonzalo purge has since #333; purging a live record leaves no tombstone, so a peer that has not synced since would resurrect it (ADR 0021). An admin purge of a live record therefore starts failing where it previously succeeded; use gonzalo delete and then purge the tombstone. Tombstone collection is unaffected: the refusal is narrow enough that a purge naming a revision that no longer matches — the key was recreated while collection held the old tombstone revision — still answers 409 Conflict and removes nothing, which collect records and carries on from. (ADR 0031, #341)

  • A put that creates over a manifest tombstone is now refused. A consumer create (expected: None) at a key holding a GraphManifest or VectorManifest tombstone used to succeed, overwriting the tombstone and discarding the retained body and the shard pin. It now fails with CoreError::Invalid (HTTP 400 / gRPC InvalidArgument), naming gonzalo undelete and gonzalo purge. This affects RecordVectorIndex commits and gonzalo index over a deleted manifest, so gonzalo reset followed by gonzalo index now needs a purge or undelete first. Replication writes and undelete are unchanged. RecordVectorIndex::open still succeeds and fails at its first commit (gonzalo#340), and the daemon's admin purge route does not check for a live record (gonzalo#341). (ADR 0030, #333)

  • BREAKING: gonzalo_core::unreferenced_slices and gonzalo_core::live_slice_hashes are removed. After #325 rewrote the sweep, neither had a caller outside its own unit tests — live_blob_hashes inlines the manifest-entry logic rather than calling live_slice_hashes, and sweep_blobs_with does its own retain loop. Both were also footguns: unreferenced_slices is age-unaware, so mapping list_blobs() down to hashes and feeding it rebuilt the unsafe delete list #325 removed, and live_slice_hashes used alone as a mark set keeps only manifest slices and sweeps every record body. Use gc_blobs_with / sweep_blobs_with to sweep and live_blob_hashes to mark. The gonzalo facade never re-exported either, so only a direct gonzalo-core dependant is affected. (#331)

  • BREAKING: the indexing knobs move into gonzalo_cli::IndexOptions. index_with_gc_filtered and index_with_gc_filtered_worker are replaced by index_with_options(root, src, repo, view, &IndexOptions), which carries {gc_after, min_age, filter, worker}. The eight-argument worker variant needed #[allow(clippy::too_many_arguments)] and respelled every argument at each fan-out layer, so a new knob cost six edit sites; it is now a field. index_with_gc is unchanged. IndexOptions::default() deliberately sets min_age to DEFAULT_MIN_AGE rather than the derived Duration::ZERO — a zero default would let a caller who sets gc_after reinstate the age-unaware sweep ADR 0028 removed. (#331)

  • BREAKING: IngestError::Conflict is a boxed tuple variant. It was Conflict { key, expected, current } with the fields inlined, which made the error path of every Result in gonzalo-ticket's ingest module 136 bytes wide — clippy 1.99's result_large_err. It is now Conflict(Box<gonzalo_core::Conflict>), the same shape PutResult::Conflict already returns, so the ingest path stops unboxing and re-inlining and callers get the whole conflicting record rather than its revision alone. Code that matches on the struct variant's fields must match the boxed value instead. (#336)

  • The pinned Rust toolchain is 1.99.0. rust-toolchain.toml moved from 1.95.0, four releases behind. Getting the gate green on it needed five fixes across four crates, each hidden behind the previous because clippy stops at the first failing crate: async-trait 0.1.89 → 0.1.92 (upstream stopped emitting the #[must_use] that made clippy's double_must_use fire on 14 desugared async fns in gonzalo-core — preferred to an #[allow], which would also have masked the lint for hand-written code, and it pulls in syn 3.0.6); as_chunks::<2>() for chunks_exact(2) in gonzalo-domain's hex parser; the IngestError boxing above; a result_large_err allow scoped to gonzalo-proto's generated v1 module, where tonic's Result<_, Status> signatures are build-script output and not editable; and slice::fill for a manual loop in gonzalo-soak's oracle tests. (#336)

  • BREAKING: BlobStore::list_blobs returns Vec<BlobEntry>. Each entry carries the blob's hash and modified_unix_ms, which the sweep needs to tell a freshly uploaded blob from an abandoned one. Any out-of-tree BlobStore implementor must change its signature. (#325)

  • BREAKING: the daemon's blob listing returns objects. HTTP GET /v1/blobs returns [{"hash": …, "modified_unix_ms": …}] instead of ["…"], and gRPC ListBlobsResponse reserves field 1 and adds repeated BlobEntry entries = 2, so an old gRPC client sees an empty list and a GC through it deletes nothing. docs/api/openapi.json gains a BlobEntry schema; note that the #198 drift check compares paths and methods, not response schemas, so this change is not guarded by it. See ADR 0028. (#325)

  • BREAKING: VectorIndex gains a required keys method. There was no way to enumerate an arbitrary index's contents, which is what KnowledgeStore::open needs to rebuild derived state — its per-record chunk counts — after a restart (gonzalo gc never calls keys; it marks a vector manifest's shard blobs directly). keys has no default implementation and must be added by any out-of-tree VectorIndex implementor. upsert_many was also added, but as a default that loops over upsert, so it is not itself breaking — RecordVectorIndex overrides it to batch a bulk load into one manifest commit instead of one per vector, and the new impl VectorIndex for Arc<T> forwards both methods (including upsert_many, explicitly, so a shared Arc handle doesn't silently fall back to the looping default). See ADR 0027. (#323)

  • gonzalo-vector now requires a tokio runtime. RecordVectorIndex::open reads shard blobs concurrently via tokio::task::JoinSet::spawn, which panics outside a tokio runtime. tokio (feature rt) moved from a dev-dependency to a real one. Every gonzalo binary already runs on tokio, so nothing breaks today, but a future non-tokio consumer of gonzalo-vector would need one to call open. (#323)

  • BREAKING: the facade's surface is grouped into modules. gonzalo re-exported 74 names flat, many of them ordinary nouns — Actor, State, Provider, Container, Link, Topic, Session, Priority, Person — which collided with a consumer's own types and said nothing about which layer they came from. The flat surface also could not express two layers using one word: gonzalo-ticket and gonzalo-graph both define a Page, and only one of them could ever be re-exported. The root now holds the record and store core and nothing else; every domain type lives under the module naming its domain, mirroring the structure gonzalo-domain already had. There is no deprecation window — the flat names are gone in this release — so the mapping is below. See ADR 0026. (#319)

    wasnow
    gonzalo::MemoryTier, Topicgonzalo::memory::{MemoryTier, Topic}
    gonzalo::Session, Turngonzalo::session::{Session, Turn}
    gonzalo::Checkpointgonzalo::checkpoint::Checkpoint
    gonzalo::RecordCodecgonzalo::codec::RecordCodec
    gonzalo::Ticket, TicketBody, TicketEvent, State, StateCategory, Actor, ActorRole, Priority, PriorityLevel, Resolution, Provider, Container, Link, LinkKind, LinkTarget, BodyFormatgonzalo::ticket::*
    gonzalo::TicketSource, Capabilities, Cursor, FieldMapping, InMemorySource, Page, SourceError, StateMapping, StateSignal, record_key, scoped_uidgonzalo::ticket::*
    gonzalo::GitHubSource, JiraSource, LinearSource, GitLabSource, AsanaSourcegonzalo::ticket::*
    gonzalo::Person, RoleGrant, IdentityBinding, ChannelConfig, LinkToken, AuditEntry, AuditResult, Authenticator, BindingOrigin, Consumption, EmptyFollowSet, FleetActor, FleetKeyError, FleetRole, Follows, GrantScope, LinkSecret, LinkSecretError, NotifyPreset, RedeemError, VerifiedEmailgonzalo::fleet::* (already available there)
    gonzalo::CodeGraph, GraphStore, InMemoryGraphStore, Located, Reference, Symbol, SymbolKind, assemble, build_rustgonzalo::graph::*
    gonzalo::Embedder, Match, MemoryVectorIndex, VectorIndexgonzalo::vector::*
    gonzalo::Hit, KnowledgeStore, knowledge_textgonzalo::knowledge::*

    Unchanged at the root: Record, RecordKey, RecordKind, Revision, Body, Meta, Identity, ContentHash, KeyPrefix, Store, BlobStore, AncestryStore, PutResult, DeleteResult, Conflict, CoreError, Result, MergeClass, MergeOutcome, sync, sync_with_ancestry, merge, collect, reset, reset_as, gc_blobs, now_ms, the reports, and FsStore / GitStore / S3Store / ServerStore.

  • S3 listings stop paying a read per record. Hiding tombstones from list cost a GetObject per key, because a tombstone lives at the deleted record's own object key and only its body says so; reset and collect inherited it. A delete now also writes a small marker beside the record, so one ListObjectsV2 pass classifies every key and only marked keys are read — one read per tombstone, which collect bounds, instead of one per record, which nothing did. Over RustFS with 500 live records and 200 tombstones, a listing went from 700 reads (216 ms) to 200 (110 ms), and to none at all (40 ms) once the tombstones were collected. The record object does not move, so every compare-and-swap is still a single conditional write. A bucket written by 0.7.0 upgrades itself: until a collection is flagged, list reads every key exactly as before and backfills the missing markers as it goes, so there is no admin step and no window where a deleted record could reappear. See ADR 0025. (#294)

Fixed

  • An S3 HeadObject fault was described as "unhandled error". A failed blob presence check surfaced HeadObjectError's own Display, which renders the unmodelled variant with no code and no status — so the most common misconfigurations, a 403 from a bucket policy or wrong credentials, were indistinguishable from each other and from anything else. The error now leads with the service's error code and the HTTP status, read off the response before the SDK discards it, and names the blob. (#330)

  • KnowledgeStore::ingest committed once per chunk on a durable index. ingest looped upsert per chunk. On MemoryVectorIndex that is cheap; on the RecordVectorIndex added in #323 every call is a full OCC commit — re-serialise the shard, upload it as a blob, write a new manifest revision — so ingesting one three-chunk record advanced the manifest three times and left two superseded shard blobs for gc. At the default 256 shards and ~100k vectors a shard is ~600 KB, so that was ~1.8 MB of blob writes for one record. It is now one upsert_many per record. One behaviour change falls out: every chunk is embedded before anything is written, so a failing embedding now writes nothing, where before the earlier chunks were already committed. Orphaned chunks (from a record that shrank) are still removed one at a time; batching them needs a new VectorIndex method, which cannot be called from gonzalo-knowledge until the workspace version is bumped, because cargo publish --dry-run verifies each packaged crate against the published gonzalo-vector. (#326)

  • A sweep could delete a blob between a writer's upload and the manifest commit naming it. Both manifest writers upload blobs before committing, so a gonzalo gc (or gonzalo index --gc under --watch, which needs no operator) landing in between deleted the blob; for a vector shard that left the index unopenable and unrecoverable. A sweep now keeps unreferenced blobs younger than the minimum age, and each writer re-checks the blobs it newly referenced after its manifest commits and re-uploads any that are gone. This narrows the race rather than closing it: a writer that crashes between its commit and that re-check, or a live writer whose old blob a sweep had already decided to delete, can still lose it, and the minimum age is also the clock-skew tolerance: a GC host running ahead of the store defeats it. See ADR 0028. (#325)

  • gonzalo gc no longer deletes live records' content. The mark set was built from graph manifests alone — the only blob references that existed when it was written — so on any store holding a record whose body is a blob, a sweep deleted that record's bytes while the record still pointed at them. The dangling reference surfaced later, as a failed fetch, with nothing to tie it back to the sweep. GC now marks from the records themselves, and an undecodable manifest is an error rather than an empty reference set. (#292)

  • S3 hardening, before any backend other than RustFS is qualified. Four fixes from the #203 review, none of which affected the pinned RustFS: an ambiguous commit (the write applies, the response times out, the retry sees 412) no longer comes back as a Conflict for the caller's own write; a GetObject that returns no ETag is now an error instead of an empty If-Match, which a permissive server could read as "no condition" and turn every compare-and-swap into a blind overwrite; a 409 retries after a jittered, bounded delay rather than immediately, so a burst cannot spend all eight attempts inside one in-flight window; and consumer list reads keys in bounded batches instead of one at a time. The soak's bucket is now created with the maintained AWS CLI image — MinIO stopped publishing mc, which had already broken CI once. (#286)

  • Git pull honours ancestor ordering, like sync. For a record changed on both sides, pull went straight to comparing kinds and bodies, so a remote that had simply advanced past the local revision — the normal state after a peer received the record through sync and wrote again — was reported as a divergence. A remote delete came back as a delete-versus-edit conflict that kept the deleted record alive, and an advance of an Opaque body, a checkpoint say, conflicted too, while sync fast-forwarded both. Pull now checks ancestry first and takes the side that is ahead, naming those keys in the new PullReport.advanced. Nothing was ever lost — the old behaviour failed safe and kept the local side — but pull and sync disagreed about the same pair of stores. (#289)

  • The git store writes record files atomically. It wrote with std::fs::write, which truncates the file before writing it, while get and list read the working tree without the repo lock. A read that landed mid-write saw an empty or half-written record and failed with a serialization error, and a crash between the truncate and the write left a zero-length record. Since 0.7.0 a delete writes a tombstone file too, so deletes had the same exposure. A write now goes to a temp file that is synced, renamed over the record and followed by a directory fsync, as the fs store already did, so a reader sees the old record or the new one and never a torn file. (#283)

Docs

  • The container guide covers scoped auth. docs/container.md listed GONZALO_AUTH_FILE but never said what goes in it. It now documents the mode precedence, the principals file with an example, the _blobs namespace for blob access, the 401/403 responses on both transports, and how writes are stamped with the principal's name. It also lists GONZALO_ANCESTOR_CAP. (#281)
  • The gonzalo crate docs have runnable examples. The docs.rs page for the crate consumers depend on was a bare list of re-exports; the only walkthrough lived in a private test that never reached the published docs. It now opens with four doctests: store and read a record, update it conditionally and handle the conflict, delete it and watch the delete survive a sync, and map a typed view through RecordCodec. They run in CI, so they cannot drift from the API. (#307)

0.7.0 - 2026-09-15

Deletion replicates. A delete used to be local: sync met a peer that still held the record and copied it straight back, so a deleted record returned on the next run and a namespace-wide reset was unusable on anything replicated. This release makes a delete a write of its own — a tombstone that travels like any record — and builds namespace reset and horizon-based collect on top of it. It also adds fleet access-control records, the identity, role, channel and audit state the Ariel chat bridge keeps in gonzalo.

Upgrade every gonzalo binary together. Deletion now replicates, and it does so with a record kind older binaries can't read. A 0.6 gonzalo, gonzalod, or any program built on gonzalo-core 0.6 that reads a store holding a tombstone fails on that key with a serialization error: loud, and no data is lost. The silent failure is worse: a 0.6 binary that runs sync against 0.7 data can't see tombstones and copies deleted records back, and nothing on the new side can tell that apart from a genuine recreation. Upgrade every binary that reads a store directly, and every binary that runs sync, at the same time. A 0.7 client against a 0.6 gonzalod keeps working for normal reads and writes, and fails replication reads with an explicit upgrade gonzalod error rather than falling back. See the guide's "Deletion, reset & collection" page and ADR 0021. (#203)

The new fleet access-control record kinds need the same all-at-once upgrade: a binary built before them fails to decode those records. Upgrade every gonzalod, CLI and embedded consumer before any writer uses them. (#278)

Added

  • Replicated deletion. A delete writes a tombstone (RecordKind::Tombstone) at the record's own path, and sync and git pull propagate it, so a delete on one store sticks everywhere instead of coming back on the next sync. Tombstones carry deleted_at (ms since the Unix epoch), and records carry a bounded ancestors list. Both are optional fields, so live records written by 0.7 still read on 0.6. See ADR 0021. (#203)

  • gonzalo delete --namespace <N> --collection <C> --id <I> [--expected <REVISION_JSON>] [--root <DIR>] [--ancestor-cap <K>]: delete one record by writing a tombstone attributed to gonzalo-cli.

    • --expected is the revision object from gonzalo get's JSON output (e.g. {"counter":3,"hash":"…"}).
    • On success, including an absent or already-deleted record, prints deleted: N/C/I and exits 0.
    • On a stale --expected, prints conflict: N/C/I and current: {…} and exits 3.
    • Malformed --expected exits 2.

    (#203)

  • gonzalo reset --namespace <N> [--collection <C>] [--root <DIR>] [--ancestor-cap <K>]: tombstone every live record in a namespace or collection. It's not atomic but it's safe to re-run. Prints X deleted, M conflicts, plus one conflict: <ns/col/id> line per conflict on stderr. When there are conflicts, it also prints a final re-run \gonzalo reset` to delete the M conflicted record(s)line on stderr. Exits0with no conflicts and3with any (re-run to finish). Omitting--namespaceis a usage error (exit2`). (#203)

  • gonzalo collect --older-than <DURATION> [--namespace <N> [--collection <C>]] [--root <DIR>] [--ancestor-cap <K>]: physically purge tombstones older than a horizon you choose.

    • <DURATION> is a positive integer plus exactly one unit of d, h, m or s (e.g. 30d), with no default. Zero, a missing, compound or unknown unit, or overflow exits 2, as does --collection without --namespace.
    • It prints horizon:, purged:, unstamped: and conflicts: lines, and exits 0 even with conflicts.
    • Purging a tombstone before every peer has synced lets the deleted record come back from that peer.
    • Tombstones with no deletion time are never collected, and clock skew can only delay collection.
    • The guide has a section on choosing a horizon.

    (#203)

  • Common to delete, reset and collect:

    • --root <DIR> (default ., expands a leading ~) and --ancestor-cap <K> (default 32). 0 exits 1 with ancestor cap must be at least 1.
    • Exit codes: 0 success, 1 error, 2 usage error, 3 conflict (delete and reset). Each command's --help lists its codes.
    • The CLI operates on a local fs store only.

    (#203)

  • Replication surface on Store: get_raw and list_raw show tombstones; put_raw(record, expected) stores a record with its revision verbatim and never re-stamps the revision (through the daemon, a non-admin caller still becomes the author, while admin and open mode keep the replicated author); purge(key, expected) is the only physical removal left. (#203)

  • Store::delete_as(key, expected, author) records who deleted a record as the tombstone's author. The daemon stamps Principal::delete_author(claimed): a non-admin is always stamped as itself; an admin keeps a named deleter, or is stamped as itself when there is none; a daemon without auth keeps a named deleter, or stamps none when there is none. The HTTP DELETE body and the gRPC DeleteRequest gain an optional author / author_json claim, which ServerStore::delete_as sends. CLI delete and reset both stamp gonzalo-cli. Library reset, and the provided delete it uses by default, keep each record's own last author instead; reset_as names a deleter, the same way delete_as does. (#203)

  • Daemon routes:

    • GET /v1/raw/records/{ns}/{col}/{id} and GET /v1/raw/keys (read on the namespace; read on * when unscoped);
    • PUT /v1/raw/records/{ns}/{col}/{id} (write on the namespace);
    • POST /v1/purge/{ns}/{col}/{id} (admin);
    • the matching GetRaw, ListRaw, PutRaw and Purge RPCs.

    (#203)

  • Ancestor cap: --ancestor-cap <n> on gonzalo delete, reset, collect and sync, and the GONZALO_ANCESTOR_CAP environment variable on gonzalod, set how many prior revisions a record remembers. The default is 32 and 0 is rejected. Peers with different caps interoperate. (#203)

  • gonzalo_core::{reset, reset_as, collect, ResetReport, CollectReport}: library reset (keeps each record's own author; reset_as names a deleter) and explicit tombstone collection. Both stop at the first store error, and re-running either afterward is safe. All five are also re-exported from the gonzalo facade crate. (#203)

  • SyncReport.fast_forwarded_to_a / fast_forwarded_to_b: the keys overwritten on each side because it was behind the other's ancestor chain. (#203)

  • Fleet access-control records. Six record kinds for people and their access to the fleet: Person, IdentityBinding, RoleGrant, ChannelConfig, LinkToken and AuditEntry. Typed views live in gonzalo-domain's fleet module and are re-exported by the gonzalo facade. Records sit in the fleet namespace, with audit entries in fleet-audit. A link token stores only a hash of its secret and is marked consumed when redeemed; audit entries are write-once. gonzalo stores these records but doesn't evaluate the roles in them. A channel config is keyed by provider, tenant and channel, and carries what the channel follows (the whole fleet, or a non-empty set of workspaces), a notify preset of all, terminal or failures, and a role ceiling. Grants scope to the fleet or to one workspace. See ADR 0022 and ADR 0023. (#278)

  • SyncReport says when sync did not converge. A new unconverged field lists the keys whose write was still losing a race when sync gave up after its pass limit, and SyncReport::converged() reports whether any pass landed cleanly. Before this, a run that never won a race returned an empty conflicts list, which read as "in sync" while the two stores still disagreed. gonzalo sync prints unconverged and warns on stderr. (#290)

Changed

  • Breaking: CoreError gains Invalid. A call the store will never accept — a consumer put of a tombstone record, which must go through delete_as — now returns CoreError::Invalid instead of Backend. The daemon answers 400 (gRPC InvalidArgument) rather than an opaque 500, and ServerStore restores it as Invalid, so a store behind a daemon fails the way a local one does. Code matching exhaustively on CoreError must add the variant. (#299)

  • gonzalo sync names the keys it could not settle. It printed counts only, so an operator learned a conflict existed but not where; it now prints a conflict: <key> or unconverged: <key> line per key on stderr, as delete, reset and collect already do, and SyncSummary's conflicts and unconverged fields carry the keys rather than counts. sync --help gained the exit-code section every other command has, and migrate now names the directory it could not read. (#299)

  • The gonzalo facade re-exports BlobStore, AncestryStore, sync_with_ancestry and now_ms. Without BlobStore, Body::Blob could not be used through the facade at all, though its own docs point at the trait; without the ancestry pair, a consumer wanting ADR 0016's three-way merge had to depend on gonzalo-core directly. (#299)

  • gonzalo sync now exits 3 on conflicts or non-convergence, instead of always exiting 0. A conflict to resolve and a run that gave up before converging are both recoverable outcomes a script should notice, which is what delete, reset and collect already signal with exit 3. A sync that converges with no conflicts still exits 0. (#290)

  • Breaking: gonzalo-core's Store trait gains required methods (get_raw, list_raw, put_raw, purge, delete_as), delete becomes a provided method over delete_as, and RecordKind gains Tombstone, so this release is 0.7.0 across the workspace. The replication methods have no default implementation on purpose: get_raw = get would compile, pass every test that doesn't delete, and resurrect records in production. Third-party Store implementations get a compile error naming exactly what to add, and must implement delete_as instead of delete. (#203)

  • Sync copies one-sided records with put_raw. A delete landing on the destination between sync's read and its copy now conflicts and is resolved on the next pass. Before, the copy became a recreation and brought the deleted record back. (#203)

  • s3 retries lost conditional writes. A write that loses an If-Match race — PreconditionFailed (412), ConditionalRequestConflict (409), or NoSuchKey (the object vanished to a concurrent purge) — re-reads, re-plans and retries for up to 8 attempts before returning a backend error, so s3 reaches the same outcomes as the lock-based fs and git stores. (#203)

  • Store::delete writes a tombstone instead of removing the record. get and list hide tombstones, so applications see no difference. Deleting an already-deleted or absent key is still an idempotent Deleted. ADR 0018's local-only decision is superseded by ADR 0021, and its conflict semantics are unchanged. (#203)

  • Writing to a deleted key re-stamps the revision. A put with no expected revision over a tombstone stores the record with a revision one past the delete, so the recreation wins over the delete when synced. put still returns the stored revision; a caller that ignores it and reuses its own will get a conflict on its next conditional write. A put with any expected revision over a deleted key returns NotFound. (#203)

  • The daemon returns 412 for a write rejected as not found. A consumer PUT /v1/records/{ns}/{col}/{id} (or raw PUT) whose expected revision the store rejects with NotFound, such as a conditional write over a deleted key, now returns HTTP 412 (gRPC FailedPrecondition) instead of an opaque 500 (Internal). ServerStore maps it back to CoreError::NotFound. 404 is deliberately not used, because on the raw routes it signals a daemon too old to serve them. (#203)

  • Raw writes through the daemon keep ADR 0015's authorship rule. A put_raw from a non-admin principal has meta.author restamped to that principal, like a consumer put. An admin principal, or a daemon without auth, keeps the replicated record's author, so an admin token is the replication credential. The revision is never re-stamped in either case. (#203)

  • Sync fast-forwards when one side is simply behind. Using each record's ancestors, sync now overwrites the stale side instead of running a merge, and reports the overwritten keys in SyncReport.fast_forwarded_to_a / fast_forwarded_to_b instead of merged. Before, an Opaque kind such as Checkpoint reported a conflict even though nothing had diverged. A delete racing an edit of the same revision is reported as a SyncConflict with neither side written. Records written before 0.7 have no ancestors and take the old merge path unchanged. gonzalo sync now prints a fast_forwarded: N line, and its output labels are re-aligned to the width of that line. (#203)

  • git pull handles tombstones on paths changed on both sides: concurrent tombstones converge on the newer one, and delete-versus-edit is reported as a PullConflict that keeps local. (#203)

  • The HA soak and s3 conformance require RustFS ≥ 1.0.0-rc.6, whose conditional DeleteObject is atomic; earlier releases can let a purge remove a concurrent recreation. docker-compose.rustfs.yml pins it. (#203)

  • The HA soak adds replicated-deletion invariants. It races deletes against edits and recreations across gonzalod replicas under replica-kill chaos, and checks that acknowledged deletes survive replica kills, that no two edits commit on the same base revision and no edit commits on the same base as a delete that actually wrote a tombstone (StaleBaseCommitted), that every run observes at least one delete conflict seeded deterministically so the check doesn't depend on scheduling, that every replica agrees on each lifecycle key's deletion state, and that consumer reads match raw reads. It now also triggers on changes to gonzalo-core and gonzalo-store-server, and its timeout is 30 minutes. The tombstone conformance cases run on fs, git, s3 and the daemon client. (#203)

Docs

  • ADR 0021, superseding ADR 0018's local-only deletion decision; the "Deletion, reset & collection" guide page, with horizon guidance; the storage, daemon and CLI guide pages updated for the replication surface and authorship rules; the tombstone replication spec aligned with what shipped; the Mem0 parity matrix Delete and reset rows, both citing ADR 0021. (#203)

0.6.0 - 2026-09-10

Resolution that uses what the syntax already says, and prebuilt binaries to run it with. The code graph stops identifying every symbol by its bare name: a call site's qualifier, a definition's owning type, a receiver's written type, and a file's own imports are all recorded and consulted, so Foo::get and Bar::get stop being one node. Each rule only ever narrows — where it cannot settle a question it declines and says so, and the counts of what it declined travel with the answer.

Much of this was found by pointing gonzalo at code it was not designed against. Seven of the sixteen tickets here came from indexing a C++ engine, a Next.js app, a Node service, three Java and Kotlin course repositories and two Python projects — .h headers parsed as C, #include ignored entirely, Java and Kotlin imports unrecorded, React's wrapper idiom yielding zero symbols per file. Two of them corrected tickets written from inside this repository.

Fixed

  • The guide's binary install example no longer pins a tag. It pointed at v0.5.0, which carries no release assets — prebuilt binaries start with this release — so the documented install 404'd from the moment it was published, and pinning any concrete tag would re-create that on every release. It now reads the latest tag, with a note that archives begin after v0.5.0. (#242)

  • A skipped file now says which file. When an isolated parse worker dies on a file, the CLI reported skipping a file — parse worker error: ... without naming it. On a 2000-file tree that left no way to find the offender but bisecting by directory, which is exactly what landing #266 cost: four rounds of re-indexing subtrees to locate one 13843-line generated header that aborts tree-sitter-cpp.

    That matters more than the inconvenience. A skipped file is silently absent from the view — search finds none of its symbols, callers misses every call it makes — so the one line saying it happened has to say where.

    The message names the path, and IndexSummary carries skipped_paths so a caller can list them rather than only count them. The summary listing is bounded at five with a count of what it left out, matching how #259 bounds its extension list. No extraction change. (#270)

  • .h headers are parsed as C++, not C. .h is the conventional header extension for C++ as much as for C, and the C grammar mis-parses C++ rather than failing: enum class Color { Red }; recorded Color as a function and invented a symbol literally named class.

    Found by indexing a real C++ project (2017 files):

    beforeafter
    symbols extracted8233188385
    symbols from .h files2295129005
    symbols named class3720
    keyword-shaped names in total47026

    Those 372 were spread across 133 files and outranked every genuine symbol in the ambiguity ranking. The 6054 new symbols are the real classes, namespaces and scoped enums those headers declare — which for a C++ project is where its types and interfaces actually live.

    Safe because the two grammars agree on plain C, which is now pinned by a test comparing symbol and reference extraction under both rather than asserted. The residual 26 keyword-shaped names come from inline namespace inside a #define body, a preprocessor limitation gonzalo already documents.

    One caveat worth stating: a 13843-line generated header in that project parses under the C grammar and aborts tree-sitter-cpp. Crash isolation handled it exactly as designed — the worker died, the pool respawned, one file of 2017 was skipped and indexing continued. .h files also now report as cpp rather than c in overview's language breakdown. EXTRACTION_VERSION is 10. (#266)

  • Indexing a tree gonzalo cannot parse now says so. A file whose extension names no known language was dropped uncounted — not in files, not in skipped, not in ignored — so a repository gonzalo could not read reported every number at zero with no explanation. Querying that view afterwards returned empty answers that looked like real ones, the same failure shape as #210 and #228.

    Found by indexing a Vanderbilt machine-learning course repository whose entire content is one Jupyter notebook. Before, it printed four zeros and nothing else. Now:

    files:    0
    ignored:  0 files, 2 dirs not descended
    unindexed: 4 files, no grammar for .csv, .ipynb, .md, .txt
    note:     nothing was indexed — gonzalo parses none of the files in this tree
    

    The count is distinct from skipped (a parse worker crashed on it) and ignored (excluded on purpose), and it names the extensions responsible so the message is actionable. A file that was going to be excluded anyway stays as invisible as it already was, so ignored is unchanged and a README inside a vendored tree does not start appearing. The extra note fires only when a run indexed nothing at all, which is the case most easily misread. No extraction change, so no re-walk. (#259)

  • A JavaScript or TypeScript function wrapped in a higher-order call is now a named function. export const Controls = React.memo(() => { ... }) is the dominant React idiom, and gonzalo extracted zero symbols from files written that way: the binding's value is a call rather than an arrow, so the component had no name in the graph and every call in its body had no caller. Object properties bound to a function ({ fetchAll: () => {} }) were missed too, because the name lives in key rather than name.

    Measured by re-indexing a real Next.js app:

    beforeafter
    symbols extracted482638
    production references with a caller17992247
    production references orphaned589141

    Test files are deliberately unchanged (4581 to 4564). Their calls sit inside anonymous Jest callbacks — it('...', () => { ... }) — which genuinely have no name, and inventing one would be worse than reporting none. The residual 141 are top-level statements like export default React.memo(ColorPicker), which correctly have no enclosing function.

    The wrapper must be called on a bare identifier (memo, forwardRef, styled) or a capitalized namespace (React.memo). That is what keeps items.map(x => f(x)) out: an iterator method returns an array, and treating its binding as a function would attribute everything inside the lambda to that binding instead of to the function actually containing it — trading a missing answer for a wrong one. Both exclusions are pinned by tests. EXTRACTION_VERSION is 8. (#257)

Changed

  • Calls in module-level declarations are attributed, and say so. A great deal of modern TypeScript lives in module-level builder objects — a Zod schema, a tRPC router — where a call sits inside a named binding but not inside a function. Those calls reached neither callers nor impact.

    They are now attributed to the nearest named binding. Since that widens what a reference's from means, the scope travels with it rather than the meaning changing silently: Reference::from_scope distinguishes Function from Module, and the MCP callers tool keeps its exact previous meaning — functions that call this — with the module-level ones in a new module_callers.

    On a real Next.js/tRPC codebase (815 files):

    beforeafter
    production references with a caller2075427524
    production references orphaned77951025
    production orphan rate27%3.6%

    Strictly additive, and measured as such: function-scope attributions came to 20754 — exactly the number that had a caller before, not one changed. The 6770 module-scope attributions are pure addition. A call inside a real function keeps today's answer, including the iterator case #257 guards.

    The nearest binding wins, so z.string() under a PORT: key reports PORT rather than the enclosing schema. Deliberately over-inclusive in the same way RefKind::Value is, which is exactly why the scope is a separate field rather than folded in. Test files are unchanged: their calls sit in anonymous Jest callbacks, which genuinely have no name. JS/TS only; Python's module-level tables and Rust's static initializers are the same shape and each needs its own evidence. EXTRACTION_VERSION is 13. (#268)

  • CommonJS require() is recorded as an import. JavaScript and TypeScript extraction handled the ES import statement only, so a Node codebase written in the older module system got no import-aware resolution at all. All three binding forms are covered: const fs = require('fs'), destructuring, and a rename, which records under its local name as the ES as alias already does. A computed require(name) records nothing, since there is no literal path to key on.

    On the one CommonJS-heavy repository available, imports went from 4 to 93 across 22 files, and relative paths resolve as expected — require('../util/aws') records util/aws.

    Worth stating plainly: that repository has no name collisions among its imported symbols, so this resolved zero ambiguous references there. The extraction is correct and the payoff is prospective — a CommonJS codebase with internal collisions gains what #252 gives every other language. It is not demonstrated on the corpus at hand. EXTRACTION_VERSION is 12. (#269)

  • C and C++ #include directives are now recorded and used to resolve. #252 and #260 gave import-aware resolution to six languages; C and C++ had none, even though an include is a stronger signal than any of them. A package import names a package that a file path is assumed to mirror; an include names the file.

    On a real C++ project (2017 files), 9860 includes recorded across 1614 files. Of the 17884 ambiguous references in files that include something, 4905 have an include pointing at a candidate and 4749 now resolve — the largest single resolution gain of any change in this cycle.

    An include names a path rather than a symbol, so it is recorded with no name and matched by path. Import::brings_whole_file says so explicitly rather than callers testing for an empty string. Dropping the extension is what makes it work: a prototype in a header is not a symbol, the definition in the translation unit is, and both sit at the same path stem, so including render/pipeline.h resolves to render/pipeline.c.

    Narrowing only, as established in #248: a system header matches nothing in the view and leaves the reference ambiguous, and an include fitting several candidates declines unless one is unambiguously nearer. EXTRACTION_VERSION is 11. (#267)

  • Imports now anchor to the file that wrote them. #252 matched an import's module as a free-floating path suffix, which fails exactly where a codebase holds two copies of the same tree — a vendored reference beside your own work, or one package tree per assignment. Every copy matched, so the reference was correctly but uselessly declined.

    Two lexical rules fix it, neither needing a project root. A Python relative import carries its dot count now, so from ..DataTypes import Action resolves by walking up from the referencing file's own package — arithmetic on a path gonzalo already has. And when an absolute import matches several candidates, the one sharing the longest path prefix with the referencing file wins, since a file means the copy in its own tree.

    Measured on four repositories, counting ambiguous references whose file imports the name:

    repocandidatesresolved beforeafter
    CS-5260 (Python)85065
    cs-395-spring-2024291419
    cs-5253-fall-20237010185
    cs-5254-summer-2023679049

    Confirmed through the server rather than only in tests: impact on a symbol defined identically in two parallel trees now reaches 18 callers, all 18 in the importing file's own tree and none in the other.

    Both rules stay narrowing, as #248 established. A tie is left ambiguous rather than broken by a coin flip, candidates sharing no prefix at all are not ranked, and a relative import claiming more levels than the path has falls through to the older rules. EXTRACTION_VERSION is 9, and the store gains an import depth column. (#261)

  • Java and Kotlin imports are now recorded. #252 shipped import-aware resolution for Rust, Python, JavaScript and TypeScript and left out the two most import-dense languages gonzalo supports — every type from another package must be imported by name, so the signal is denser there than anywhere else. Found by indexing three Vanderbilt course repositories, which between them contained thousands of import lines and recorded zero.

    repofilesimports beforeafter
    cs-395-spring-202440102657
    cs-5253-fall-202372806620
    cs-5254-summer-202375205642

    A Java static import records the member it brings into scope rather than the class holding it, and a Kotlin as alias records under its local name. A wildcard records nothing, as with Rust's glob. Kotlin needed one wrinkle: its grammar drops the *, so import a.b.* and import a.b parse identically and the source text is the only way to tell them apart.

    Extraction only — resolution already consumed imports generically. EXTRACTION_VERSION is 7, so each view re-walks once on upgrade. (#260)

  • Imports narrow an otherwise ambiguous reference. Resolution was file-scoped and consulted nothing outside the calling file, so a name defined in several places with none of them local was simply dropped — even when the file's own imports named exactly one of them.

    Each file's imports are now recorded and consulted at the point the ladder would otherwise give up. use crate::model::make prefers the make under model.rs. This is deliberately lexical, not a resolved module path: resolving an import to a file needs the crate root, which means reading the manifest and modelling the workspace, and extraction would then depend on files other than the one being parsed (ADR 0012). An import says which module a name came from, and a module's name almost always appears in its own file's path, which is enough. Three layout conventions are folded in: an entry file (mod.rs, lib.rs, main.rs, __init__.py, index.ts) names its container rather than itself, a src directory is build layout rather than a module, and - and _ compare alike so a crate written gonzalo_cli matches gonzalo-cli on disk.

    It only ever narrows, following #248: when the import fits several candidates the reference stays ambiguous, which is the honest answer, and nothing that resolves today stops resolving.

    Measured by indexing three real repositories — gonzalo itself, caliban, and a Next.js app:

    repoimports recordedambiguous refs whose file imports the namenow resolved
    gonzalo14082711
    caliban58036845
    web-viz-next7822121

    A bare @ in a JS/TS import is a tsconfig path alias rooted at the project (@/lib/utils), not a directory, and left in place it added a segment that could never match — the Next.js app resolved 0 of 21 until it was dropped, and 21 of 21 after. A scoped package (@testing-library/react) leads with @scope, which is a real name and survives.

    Rust, Python, JavaScript and TypeScript. A glob import contributes nothing, since it introduces names the file never spells out. EXTRACTION_VERSION is 6, so each view re-walks once on upgrade, and the store gains an imports table. (#252)

  • Method calls are attributed when the body says what the receiver is. A call through a receiver of unknown type is deliberately not attributed, because guessing was measurably wrong (#223) — but a large share of receivers have a type written down a few lines up. Three purely syntactic shapes are now read: a binding initialized from a constructor-style call, a binding initialized from a struct literal, and a parameter with a written type, including behind a reference. self takes the enclosing impl.

    On gonzalo's own source 1043 of 6732 method references now carry a type, 437 of which name a type the view defines; impact on insert went from 60 symbols reached to 219, and its unattributed method edges fell from 368 to 274. insert is the seed that #248 could not move at all, because it is reached almost entirely through store.insert(..).

    This is syntax, not inference, and the limits are deliberate: a receiver from a plain call, a generic parameter, or a trait object still declines and is still counted, because choosing an impl is the guess #223 removed. Bindings are scoped to the function body they appear in. Rust only for now; every other language records no receiver type and behaves exactly as before. EXTRACTION_VERSION is 5, so each view re-walks once on upgrade. (#251)

  • callers and callees now report how ambiguous the queried name is. Both are raw name matches, so a name defined in several places returns the merged answer for all of them — and a bare list said nothing about that, which is the point where a heuristic result is most easily read as a precise one. Each now answers with a definition_count alongside the list, plus defined_in while there are few enough paths to be a lead rather than noise. A count of 0 means the name is defined nowhere in the view, which is what makes an empty list readable rather than ambiguous between "nothing calls this" and "you asked the wrong question".

    This changes the shape of those two tool results: the list moved under a callers/callees key, so they return an object rather than a bare array. impact already reported the edges it declined; this brings the same honesty to the tools that do no resolution at all. (#249)

Fixed

  • unreferenced no longer reports live callbacks as dead code. A function passed as a value rather than called — and_then(helper), register(handler) — is a path expression, not a call expression, so it recorded no reference at all and the function looked unused. That was the documented main false positive of the one tool whose suggested action is deletion. On gonzalo's own source it was flagging 98 symbols that are genuinely used, among them the test fixtures handed to build_rust and constants passed to functions.

    Such a reference is now recorded with its own kind and deliberately kept out of the call graph: callers, callees and top stay call-only, because passing a function is not calling it. impact counts them in a new value_edges rather than walking them — a callback is a real dependency, so dropping it silently would under-report, but extraction is per-file and cannot tell an identifier naming a function from one naming a local, so traversing them would put false edges back into the walk #207 and #223 cleaned up. Non-zero value_edges means go look.

    The over-inclusion is deliberate and one-directional: a local variable handed to a call is recorded too, which for dead-code detection errs towards not calling something dead. EXTRACTION_VERSION is 4, so each view re-walks once on upgrade. (#250)

Added

  • Qualified symbol identity in the code graph. A call site's path qualifier and a definition's owning type are now both recorded, so Beta::get() is attributed to Beta's get instead of merging with every other get in the view. Two pieces of disambiguating information were sitting in the syntax tree and being discarded: a qualified callee was reduced to its trailing identifier, and the walk tracked only the enclosing function, so a method in an impl block was stored as a bare name with nothing recording what it hangs off. The qualifier only ever narrows — when it names something the view does not own, such as a file module or a dependency, it is ignored and the previous rules apply, so no edge that resolved before stops resolving now. Recorded where a grammar makes a path unambiguously a path (Rust, C++, PHP); Go's pkg.Func() and Java's Foo.bar() cannot be told from a receiver call at this level and stay unqualified rather than record a variable as a type. EXTRACTION_VERSION is 3, so each view re-walks once on upgrade. (#248)

  • Prebuilt macOS Apple Silicon binaries on every tagged release. A v* tag now also builds gonzalo-vX.Y.Z-aarch64-apple-darwin.tar.gz (plus a .sha256) and attaches it to the GitHub Release. The archive holds gonzalo, gonzalo-mcp and gonzalo-parse-worker together, which is the point: installed separately from crates.io those three can land at different versions, and a CLI newer than its parse worker indexes with the old parser and then marks the view current (#212, #228). aarch64-apple-darwin is the only target built; the container image covers Linux. See docs/releasing.md. (#229)

Changed

  • ParseMode and WorkerSource are #[non_exhaustive]. They ship for the first time this release, and the set of ways a worker can be located is already known to be growing, so sealing them now avoids a breaking change later. (#241)

  • Recorded the Rust-native deliverables principle as ADR 0020: gonzalo ships Rust crates, the container image and its own binaries — not client SDKs in other languages — and non-Rust consumers integrate over the daemon plus its published schema. The decision was real but written down nowhere, so the competitor parity evaluation kept ranking Python/TS SDKs as the single most convergent gap in the whole evaluation. All five SDK rows are now marked by design with the citation. (#196)

  • docs/releasing.md no longer claims version bumps are exempt from rate limiting. crates.io enforces a second limit on updates to existing crates, and a 24-crate workspace trips it on every release — v0.5.0 stopped after 23 of 24. The expected 429-and-resume flow, and a check that every crate is live before creating the Release, are now documented. (#227)

  • Documented that the gRPC decode ceiling is per-server, not per-method: raising GONZALO_MAX_BLOB_SIZE also raises the decode limit for record RPCs, which HTTP does not do. Bounded and authenticated, so documented rather than closed. (#194)

Fixed

  • The packaged gonzalo binary is portable. It linked Homebrew's libgit2 at /opt/homebrew/opt/libgit2/…, an absolute path in the Mach-O load command, so on any Mac without Homebrew it died in dyld before executing an instruction — not even --version ran. gonzalo-mcp and gonzalo-parse-worker were always clean, so the archive shipped one broken binary out of three, and it was the one that creates the view the MCP server reads. libgit2 is now vendored and statically linked, matching what the Linux container already did by accident of having no libgit2-dev installed. scripts/package-macos.sh now refuses to package a binary that links anything outside /usr/lib and /System/Library, because which way this went depended on whether the build machine happened to have a system libgit2. (#246)

  • Ticket connectors keep the provider's error message instead of discarding it. SourceError::Backend is documented as carrying the provider's message, but all five connectors reached it through reqwest's error_for_status(), which drops the response body — so an expired token, a missing scope, a malformed JQL query or a rate-limit hint all surfaced as a generic "HTTP status client error". Twenty call sites across Jira, GitHub, GitLab, Asana and Linear now report the status and the provider's own reason, bounded in length. (#240)

  • gonzalo sync expands a leading ~ in its two store roots. Those are positional arguments and were the one place #211's expansion did not reach, so gonzalo sync '~/a' '~/b' from a non-shell context silently operated on two directories that did not exist and reported success. (#238)

  • The parse-worker version note no longer claims correct data is wrong, and no longer repeats. A worker released before --extraction-version cannot report one while still producing current extraction, so that case now reads as an unconfirmed note rather than a warning to go fix your data; a worker that reports a different version still warns plainly. It is also emitted once per process rather than once per index, which under --watch meant once per file save. (#239)

  • Remote reads report failures the way remote writes do. ServerStore's read paths used reqwest's error_for_status(), which discards the response body, so a 403 on a read surfaced as a generic HTTP status line while the same refusal on a write reported the daemon's reason. All four HTTP read paths now carry status and body; 404 still means Ok(None) on get/get_blob. (#195)

  • A leading ~ in a store root is expanded to $HOME instead of being taken literally. GONZALO_ROOT=~/.gonzalo in an MCP client config reaches the process verbatim — there is no shell in that path — so it created a directory literally named ~ in whatever the working directory happened to be, and every query then answered from the wrong, empty store. The form users naturally write, and the one that works when tried in a shell, was exactly the form that silently misbehaved where it mattered. Expansion covers GONZALO_ROOT and every --root argument, which has the same exposure from a systemd unit or container spec, and status now reports the expanded path so it is verifiable. Only a leading ~ is touched: ~otheruser/… is left alone, and a ~ anywhere but the front is an ordinary directory name. (#211)

  • gonzalo index says which parse mode it is using instead of silently degrading. Crash isolation depends on finding gonzalo-parse-worker, and when it could not, indexing quietly parsed in-process — no log line, no warning, no way to tell. The two modes produce byte-identical graphs, so the loss was invisible until a tree-sitter grammar aborted and took down the whole run instead of skipping one file. A parse: line now names the worker and how it was found; the fallback prints a warning listing every location searched; and --require-parse-worker turns a missing worker into a hard error for CI and container builds. Lookup also covers PATH, so symlink and split-install layouts — where a sibling check cannot succeed but the worker is plainly available — find it. (#212)

  • A stale gonzalo-parse-worker no longer produces pre-upgrade extraction that the view records as current. EXTRACTION_VERSION guarded the CLI, but the worker is a separately installed binary and is what actually parses — so a 0.5.0 CLI driving a 0.4.0 worker emitted old-format slices and then stamped the view with the new version, leaving it wrong and marked up to date, which no later re-index would repair. The worker now answers --extraction-version, the indexer asks before parsing, and a view is credited to the version the parse path actually produced. A mismatch warns, rebuilds in full, and self-heals once the worker is upgraded. A worker too old to answer is recorded as unknown rather than assumed to agree. (#228)

0.5.0 - 2026-08-22

Code-graph correctness. impact and reference resolution stop asserting edges they cannot justify — a closure that returned half the repo for one seed, and a name-matched resolver that credited std methods to same-named project functions. Alongside that, the MCP server gains aggregate queries that answer questions about a whole view rather than about a symbol name the caller already has.

Upgrading: the first index after this release does a one-time full re-walk of every view (see EXTRACTION_VERSION below) — expected, not a fault. Reinstall the binary as well as re-indexing: the resolution fixes live in the query path, so a stale gonzalo-mcp keeps returning the old answers over freshly indexed data.

Fixed

  • A method call no longer claims a same-named free function (#223). Resolution::UniqueGlobal attributed a reference to the sole definition of that name in the view — including when the name was really a std or dependency method. In gonzalo, .chain(ours_obj.keys()) in gonzalo-core resolved to fn chain(), a test fixture in gonzalo-graph, a crate gonzalo-core does not depend on.

    References now record the shape of the call site (RefKind::{Free, Method}), and a cross-file method call resolves to the new Resolution::ReceiverUnknown rather than guessing. Measured over gonzalo's own source: of the 388 cross-file method calls that used to resolve UniqueGlobal, 294 pointed at a different crate — push, filter, send, bytes, next and friends. Same-file method calls still resolve Local, and free and path calls (foo(), a::b::foo()) are unaffected.

    Effect on impact (#207), same 122-file source both runs:

    seedbeforeafter
    build_rust185126
    assemble6023
    resolve_references_to66

    The 24 provably-false gonzalo-core nodes in the build_rust closure are now 0. Dropped edges are reported as receiver_unknown_edges, counted separately from ambiguous_edges because the cause differs: not "too many candidates" but "cannot claim any candidate".

    RefKind is omitted from the serialized slice when free, so a file of plain calls keeps its existing content hash.

  • impact no longer merges unrelated code through shared identifiers (#207). The closure walked the name-matched caller graph, so one hop into a name with several definitions absorbed every subgraph sharing that identifier. The walk now keys nodes on (name, defining path) and consults the resolver for every edge: an Ambiguous reference is counted and dropped rather than traversed.

    On the gonzalo view, seeded at build_rust: 356 → 178 reached names, with 10 ambiguous edges reported rather than followed. Seeds that were already sharp are unchanged (resolve_references_to: 5 → 5).

    The result is now a report rather than a name list — every node carries the path defining it, ambiguous_edges says how many edges could not be attributed (so a non-zero count means the true set may be larger), and truncated reports a walk stopped by the new optional max_depth. The daemon's HTTP and gRPC transports keep their existing name-list shape and so get the precision fix without the report fields.

    Of the remaining 178, 17 are still provably false and trace to a single UniqueGlobal over-attribution — std's Iterator::chain resolving to a same-named test fixture. That is a distinct defect, filed as #223.

  • An incremental re-index now prunes paths a laxer run admitted (#209 follow-up). The filter added in #218 only applied to newly walked or changed files, so an existing view kept its vendored bundles forever: a bundle never changes, so it never appears in the git diff and was never reconsidered — and once a base commit is recorded there is no full walk to clean it up. Upgrading therefore fixed new views only, which is the case least in need of fixing.

    The carried-forward set is now re-checked against the current rules, including .gitignore — necessary because docs/guide/book/ is build output excluded by ignore rules rather than by any directory-name rule, so a path-only prune left it behind.

    Re-indexing the existing caliban-ai/caliban view: 17 162 symbols → 8 549, with vendored symbols going 8 618 → 0 and the largest file becoming caliban/src/tui/events.rs (142) instead of a copy of mermaid.min.js.

  • An unknown repo/view_id is now an error, not an empty result (#210). Every graph query returned [] with isError: false when the selector named no indexed view, so a one-character typo in view_id was indistinguishable from a symbol that genuinely is not there — an agent read it as "nothing calls this" and reported a wrong answer as fact. Service::view now fails with NotFound, and the MCP layer turns that into a tool error naming the unresolved selector and listing the views that do exist, so a caller can correct itself in one round trip. A real miss inside a real view still returns [], so the two cases are finally distinguishable.

    diff gets the same check on both view_a and view_b.

  • Calls inside Rust macro arguments are now recorded as references (#216). Macro arguments parse as a token_tree of raw tokens rather than expressions, so assert_eq!(f(), 1) contained no call_expression and the call to f was never seen. Because assertions are where much of a codebase is exercised, this silently removed a large share of the call graph: callers, callees, impact and top by=fan_in all undercounted, and unreferenced reported live functions as dead.

    Re-indexing gonzalo itself, with an identical file set (1 852 symbols both runs), references go from 10 247 to 12 196 — +1 949 edges, +19.0%. Language::from_extension, the symbol that exposed the bug, goes from 0 recorded references to 28.

    Detection is token-level: an identifier whose immediate next sibling is a parenthesised token tree. A nested macro has a ! between the two and is excluded. It is deliberately over- rather than under-inclusive — a tuple-struct pattern like Some(_) reads as a call — which matches a graph that already records constructors and enum variants as calls.

    The other 17 grammars were audited for the same opaque-node hole. Only C/C++ has one: a #define body is a single opaque preproc_arg token with no child nodes to read. It is left in place and pinned by a test so the gap is discoverable rather than silent.

  • The indexer no longer walks vendored bundles or gitignored build output (#209). is_indexable skipped only target, .git, and dotted components, so half of a real repo's graph was not that repo's code. Membership now lives in one place (IndexFilter), shared by the full walk and the git-incremental driver so they cannot disagree: dependency/output directories (node_modules, vendor, dist, build, site-packages, third_party) and generated files (*.min.js, *.min.css, *.bundle.js, *-lock.json) are dropped on both paths, and the full walk additionally honours .gitignore.

    Re-indexing caliban-ai/caliban drops it from 16 986 symbols to 8 501 (-50.0%) with zero symbols from book/** or any *.min.js; the largest file in the view is now caliban/src/tui/events.rs (142 symbols) rather than a 4 231-symbol copy of mermaid.min.js. This also removes a reproducibility hole — indexing gitignored output made the graph depend on whether anyone had run a build.

    gonzalo index now reports what it excluded (ignored: N files, M dirs not descended), and --include <path> re-admits a vendored path that a built-in rule would drop. --include deliberately cannot override .gitignore, so no flag can make a view irreproducible.

Added

  • EXTRACTION_VERSION, and a full walk when it changes (#223). The incremental driver carries unchanged slices forward untouched, so a parser improvement never reached files that did not change — an existing view stayed permanently half-upgraded. gonzalo index now records the extraction format alongside the view and rebuilds in full when it differs, which is what lets #216's and #223's parsing changes actually reach an established view.

  • A guide chapter for the MCP server (#208) — docs/guide/src/mcp.md, covering install → index → register → verify → keep fresh, a tool reference grouped by the question each tool answers, and the capability boundaries. It leads with the thing nothing in the repo stated: the server only reads, gonzalo index writes, so an unindexed setup answers every query forever with no indication why. It also records the traps found while wiring the server up for real — GONZALO_ROOT not expanding ~ (#211), ~/.cargo/bin missing from the non-interactive shells an MCP client spawns, and needing to reconnect the server to pick up a newly installed binary — plus a troubleshooting table keyed by symptom.

  • views discovery tool and a view count in status (#210). views lists every indexed (repo, view_id) with its file count and the commit it was indexed at, which makes the server self-describing rather than dependent on out-of-band documentation; comparing base_commit against the checkout's HEAD also surfaces a stale view, the quieter form of the same problem. status now reports how many views are indexed, so the natural health-check call actually detects a server pointed at an empty or wrong store. The repo/view_id schema descriptions now say they must match an indexed view and point at views.

  • Aggregate code-graph queries (#214) — three MCP tools that answer questions about a view rather than about a symbol name the caller already has: overview (file/symbol/reference counts, a breakdown by kind and language, and the largest files), top (rank by fan_in, fan_out, or definitions — a definitions score above 1 marks an ambiguous name), and list (enumerate symbols filtered by path_prefix, kind, and name_contains). Backed by new default GraphStore methods, so every store implementation inherits them. Results are bounded and report total + truncated rather than silently cutting.

  • unreferenced dead-code candidates (#214) — a fourth aggregate tool listing symbols with no inbound reference, filtered by the same path_prefix/kind/name_contains and bounded the same way. exclude_tests (default on) drops members of a mod tests/mod test block by line range and anything under a tests/ directory; on gonzalo itself that is the difference between 515 hits and 40. Deliberately errs toward silence — a reference from anywhere counts, including from tests and from the symbol itself. Its false positives are documented in the tool description, the rustdoc, and a pinned test: calls inside macro arguments are not recorded at all (assert_eq!(f(), 1) registers nothing), and a function passed as a value is a path expression rather than a call, so both look uncalled.

0.4.0 - 2026-08-01

The remote-parity & backend-qualification release. Deletion and blobs — the two gaps that kept the daemon substrate behind fs/s3 — close, so a daemon-backed consumer now gets the full Store + BlobStore surface. Alongside them, an HA soak harness doubles as a conditional-write qualifier for S3 backends, and its first finding disqualifies Garage outright.

Added

  • Store::delete (#183) — OCC-aware record deletion across every substrate and the daemon, propagated by Sync. Local-only semantics; see ADR 0018. (#185)
  • Blobs over the daemon (#184) — the content-addressed BlobStore is exposed on gonzalo-server (HTTP GET|PUT|DELETE /v1/blobs/{hash}, GET /v1/blobs, plus gRPC), and ServerStore implements BlobStore over both transports, so a daemon-backed consumer gets the full Store + BlobStore surface. Blobs previously worked only on fs/s3. Adds the GONZALO_MAX_BLOB_SIZE daemon knob (default 64 MiB). (#192)

Changed

  • S3 backends are now qualified, and Garage is not among them (#52) — atomic If-Match is a hard requirement for any S3-compatible backend. Garage does not provide it: gonzalo's conditional-write conformance case expects exactly 1 of 8 concurrent racers to commit, and Garage let 8/8 through on v1.0.1 and 3–8 through non-deterministically on v2.1.0 — the signature of a check-then-set, not an atomic CAS. Deployments running gonzalo over Garage can silently lose concurrent writes. RustFS (Apache-2.0) is the qualified backend; MinIO passes the qualifier but is rejected on project sustainability. See ADR 0019. (#186, #205)

Testing: an HA soak harness for stateless gonzalod replicas over an S3-compatible store — backend-agnostic, doubling as the conditional-write qualifier above (#52, #186); cross-crate integration tests extracted into gonzalo-integration-tests (#190, #191).

Project: a crates.io publishing pipeline for the workspace, triggered on v* tags (#187, #189).

Docs: competitor capability inventories and parity-gap matrices for mem0, Zep, and Letta under docs/evaluation/ (#193); ADR 0019 recording the S3 backend qualification (#205).

0.3.0 - 2026-07-11

The hardening & language-breadth release. A broad correctness and robustness sweep — a 20-finding QA pass turned into fixes across the core merge/OCC model, every storage substrate, the daemon, the ticket connectors, and the knowledge/vector layer — lands alongside eight new code-graph grammars that take language coverage from nine to seventeen.

Added

  • Language breadth for the code graph — grammars for Ruby, PHP, Bash (#87), Kotlin (#87), Swift (#87), Lua (#87), Scala (#87), and Elixir (#87). Elixir is homoiconic (def/defp/defmacro/defmodule parse as ordinary call nodes), so it uses a value-based walk() dispatch on the call target's text rather than a node-kind mapping. Coverage is now 17 languages. (#126, #127, #128, #129, #130, #181)

Changed

  • Ticket RecordKeys are board-scoped — a board-scoped source folds its connection/board discriminator into the key, so the same issue imported from two boards no longer collides onto one thrashing record. Stored keys for board sources change shape and re-import on the next sync. (#159)

Fixed

  • core — record-key encoding is now a reversible, injective percent-style codec, so distinct keys can never collide onto one physical path (silent cross-key overwrite / OCC bypass); append-only merge preserves blank and legitimately-repeated committed lines instead of stripping/de-duping them, and the Derived/gc semantics are corrected. (#131, #133)
  • storage substrates — the git substrate locks the put critical section (no lost updates under concurrent writers) and detects non-fast-forward push rejection; filesystem writes fsync the temp file and parent directory for crash durability; S3 list-pagination terminates when the continuation token is absent; graph-sqlite view-db paths use the injective encoder. (#132, #134, #144, #145)
  • daemon — authorization runs before request deserialization, internal backend errors are returned opaquely (no path/bucket/SQLite leakage), the PUT record route validates its URL path against the body key, and the remote client surfaces daemon 403/413 responses instead of masking them as a decode error. (#146, #147)
  • code graph — JS/TS arrow-function and function-expression bindings are extracted, PHP method and static calls are recorded, and Swift/Kotlin struct/enum/interface declarations get their correct SymbolKind. (#136)
  • knowledge / vector / domain — a corrupt knowledge-bearing body surfaces an ingest error (rather than silently not indexing) and removed records are de-indexed; non-finite vectors score 0.0 and rank deterministically; the domain codec rejects a Body::Blob instead of misparsing its content hash. (#139, #149, #154)
  • cli — get/ticket get exit non-zero (message on stderr) when a record is absent, index advances the persistent SQLite graph only after the manifest commits, and --gc is honored under --watch. (#152)
  • ticket connectors — Jira routes a Canceled move to a won't-do status rather than Done; a closed GitLab issue is terminal and non-terminal moves no longer report a false success; Linear fails a mutation that returns success: false; the GitHub REST connector follows Link pagination so all issues import, not just the first 100. (#138, #140, #141, #142)

Testing: de-flaked gonzalo-parse's hung-worker timeout test, whose 300 ms budget false-timed-out the healthy recovery parse under heavy build load. (#178)

0.2.0 - 2026-07-06

The code-graph release. Gonzalo grows a full code-graph capability — tree-sitter parsing across nine languages, content-addressed slices with two-level keying, a persistent SQLite GraphStore, and structural queries over both the daemon and an MCP server — alongside real semantic search (a local Candle embedder feeding an approximate ANN index over per-kind-chunked knowledge), namespace-scoped daemon auth, and correct content-aware 3-way merges on both store-sync and git pull. Distributed as the gonzalod container image.

Added

  • Code-graph capability (EPIC A–K): tree-sitter parsing into path-agnostic, content-addressed slices with two-level keying (content+grammar hash for storage, per-view manifest for resolution); a persistent SqliteGraphStore; assembly-time name resolution; and structural queries — definitions, references, callers, callees, transitive impact, and cross-view diff — served over the daemon and a dedicated code-graph MCP server. Parsing is crash-isolated behind a worker-subprocess ParserPool. (ADR 0012; #48, #50, #54, #56, #61, #64, #66, #70, #71, #74, #77, #88, #89, #90, #10, #30)
  • Language breadth for the code graph — a Language dispatch with grammars for Rust, Python, JavaScript/TypeScript/TSX, Go, Java, C#, C, and C++. (#79, #81, #83, #84, #85, #86)
  • CLI indexing (gonzalo index): index a source tree into a code-graph view, git-diff-driven incremental re-sync, a --watch file-watcher for live re-index, and opt-in mark-sweep GC of unreferenced slices. (#74, #93, #94, #100, #104)
  • Knowledge store (gonzalo-knowledge): a "what do we know about X" surface composing Store + VectorIndex + Embedder, a vector⋈graph join (semantically similar and structurally near), and per-kind chunking so long records retrieve at turn/section granularity. (ADR 0011; #30, #29)
  • Real semantic vector search: gonzalo-embed — a local CPU sentence embedder (Candle + all-MiniLM-L6-v2) — and HnswVectorIndex, an approximate ANN backend, behind the existing Embedder/VectorIndex traits. (ADR 0013, ADR 0014; #97, #9)
  • Daemon substrate selection & health: env-driven fs|s3 store selection with a native S3 BlobStore, and unauthenticated /healthz + /readyz probes for k8s. (#62, #63)
  • Namespace-scoped daemon auth: a token→principal model with per-namespace read/write scoping enforced on both transports, plus unforgeable author stamping. (ADR 0015; #11)
  • gonzalod container image — the release artifact, published on a v* tag. (#51, #65)

Changed

  • Store sync — true 3-way merge: AncestryStore retains each version's body by revision hash so sync can merge divergent structured records against their real common ancestor instead of an empty base. (ADR 0016; #2)
  • Git pull — content-aware non-fast-forward merge: a diverged pull now reconciles per-record through gonzalo's class-aware merge() into a two-parent merge commit, surfacing unresolved records instead of erroring. (ADR 0017; #7)
  • S3 native conditional writes: If-Match/If-None-Match close the optimistic-concurrency TOCTOU window in the S3 substrate. (#5)

Fixed

  • CI: serialize GitHub Pages deploys with a concurrency group. (#107)

Internal

  • ADRs 0010–0017 added (ticket capability layer, two-level code-graph keying, local embedder, ANN backend, namespace auth, stored-ancestry 3-way merge, content-aware non-FF pull).
  • Docs: an mdBook guide publishing the ADR log, changelog, and a synthesized Guiding Principles & Invariants page; README status badges. (#103, #38)

0.1.0 - 2026-07-03

The initial development line — a generic, versioned, conflict-aware persistence layer for caliban, built milestone by milestone (M1–M6).

Added

  • Record/Store core (M1): gonzalo-core — one uniform Record model and a generic Store trait, with revisions, optimistic-concurrency parent tracking, PutResult::Conflict, per-RecordKind merge, and a feature-gated substrate conformance suite. No I/O in the core. (ADR 0002, ADR 0005, ADR 0006)
  • Filesystem substrate + domain + facade (M1): gonzalo-store-fs (mirrors caliban's on-disk layout, the zero-dependency default), gonzalo-domain (typed MemoryTier/Topic/Session/Checkpoint views), and the gonzalo facade. (ADR 0004, ADR 0008, ADR 0009)
  • Git & S3 substrates + Sync (M2): gonzalo-store-git (commit-per-write, fast-forward pull/push) and gonzalo-store-s3 (S3-compatible object store), plus the Sync engine reusing the core conflict/merge machinery. (ADR 0004, ADR 0005)
  • Daemon + remote substrate (M3): gonzalo-proto (one canonical schema), gonzalo-server (gonzalod) serving the store over both gRPC (tonic) and HTTP/JSON (axum) on one core service layer with optional bearer auth, and gonzalo-store-server as the client substrate. (ADR 0007)
  • Vector layer (M4): gonzalo-vector — Embedder + VectorIndex traits with a caller-delegating default embedder and an exact in-memory cosine index. (ADR 0008)
  • Code-graph layer (M5): gonzalo-graph — a tree-sitter Rust symbol/ref index (build_rust) behind a GraphStore trait. (ADR 0008)
  • Admin CLI (M6): gonzalo-cli (gonzalo) — list, get, status, migrate, sync.

Internal

  • Project: established docs/adr/ (MADR-lite) with the initial retrospective ADRs 0001–0009; added CI (fmt/clippy/build/test), a line-coverage gate, the Kanban label taxonomy, and board/triage automation.