<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: constantine makoid</title>
    <description>The latest articles on DEV Community by constantine makoid (@constantinemakoid).</description>
    <link>https://dev.to/constantinemakoid</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4097657%2F31d09708-af1f-4bba-ba95-562a94980041.png</url>
      <title>DEV Community: constantine makoid</title>
      <link>https://dev.to/constantinemakoid</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/constantinemakoid"/>
    <language>en</language>
    <item>
      <title>The documentation is a person, and they are on holiday</title>
      <dc:creator>constantine makoid</dc:creator>
      <pubDate>Fri, 04 Sep 2026 08:39:00 +0000</pubDate>
      <link>https://dev.to/constantinemakoid/the-documentation-is-a-person-and-they-are-on-holiday-466l</link>
      <guid>https://dev.to/constantinemakoid/the-documentation-is-a-person-and-they-are-on-holiday-466l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Before publishing:&lt;/strong&gt; set &lt;code&gt;published: true&lt;/code&gt;, and check &lt;code&gt;canonical_url&lt;/code&gt; — dev.to needs the&lt;br&gt;
article to already exist at that URL, so it goes up on the site first. &lt;code&gt;cover_image&lt;/code&gt; points&lt;br&gt;
at the existing product cover; swap it for a documentation-specific one if that gets made.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is a diagram your organisation has never drawn. On it, one box holds most of what is&lt;br&gt;
known about how the system works: which service owns the settlement flow, why the retry lives&lt;br&gt;
on the consumer and not the producer. The box is a person. They are competent, generous with&lt;br&gt;
their time, and currently in Croatia.&lt;/p&gt;

&lt;p&gt;Everyone knows this. What gets missed is that the wiki does not fix it. The wiki was written&lt;br&gt;
during a documentation push eighteen months ago and has been quietly wrong since the week&lt;br&gt;
after. The README describes a deployment that no longer exists. The architecture page still&lt;br&gt;
shows the monolith that was split in March.&lt;/p&gt;

&lt;p&gt;An absent document and a false one are not the same failure. An absent one sends you to the&lt;br&gt;
code, which is annoying and correct. A false one sends you somewhere confidently, and you find&lt;br&gt;
out forty minutes later, having built a mental model on top of it. The wiki nobody trusts is not&lt;br&gt;
neutral. It is a trap already sprung on several new joiners, and the response is always the&lt;br&gt;
same: ask the person.&lt;/p&gt;

&lt;p&gt;So the person is the interface, and the wiki is a decoy pointing at them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to go stale
&lt;/h2&gt;

&lt;p&gt;Take any statement about a codebase — "requests are authenticated in the gateway". At some&lt;br&gt;
moment it stops being true. What happens then is the entire distinction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hand-written documentation goes stale silently.&lt;/strong&gt; There is no event. Somebody merges a pull&lt;br&gt;
request, and a page in Confluence becomes false. Nothing fires, no build turns red, the&lt;br&gt;
timestamp does not move. Its confidence is unchanged, which is the problem: the page is now&lt;br&gt;
more sure of itself than the code justifies. A stranger discovers it months later, at the worst&lt;br&gt;
possible time, and between falsity and discovery the document costs more than not having it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation derived from the code goes stale loudly&lt;/strong&gt;, because the generator runs again.&lt;br&gt;
The commit that made a statement false is the input to the next run. Staleness stops being an&lt;br&gt;
unobserved property of a page and becomes an event with a timestamp: a job, a diff, a&lt;br&gt;
regenerated file, a failure. You can put that on a dashboard. You cannot page on Confluence&lt;br&gt;
being wrong, because nothing knows.&lt;/p&gt;

&lt;p&gt;That is the whole argument, and it is smaller than it sounds. Not that machines write better&lt;br&gt;
prose — they very often do not. It is a claim about &lt;em&gt;observability of decay&lt;/em&gt;, and decay is what&lt;br&gt;
kills documentation, not prose quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the loud kind fails, before I sell you on it
&lt;/h2&gt;

&lt;p&gt;Four honest limits, because the sales version of this argument skips all of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A generator recovers &lt;em&gt;what&lt;/em&gt;, never &lt;em&gt;why&lt;/em&gt;.&lt;/strong&gt; It can tell you this module publishes to that&lt;br&gt;
topic. It cannot tell you the retry sits on the consumer because of an incident in 2023 and a&lt;br&gt;
vendor who would not fix their idempotency. That was never in the code and no parsing recovers&lt;br&gt;
it; it lives in the person in Croatia. Generated documentation shrinks the set of questions that&lt;br&gt;
need a person; it does not empty it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regeneration is not free.&lt;/strong&gt; Every regenerated page is model spend. A system that rewrites&lt;br&gt;
everything on every commit gets switched off, and a generator that is switched off is a&lt;br&gt;
hand-written wiki with extra steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silent success is the real bug.&lt;/strong&gt; A run that produces nothing looks identical from outside to&lt;br&gt;
a run that worked: job completed, no errors, folder empty. If a pipeline fails quietly, the&lt;br&gt;
loudness you paid for is gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Derived documentation must name what it does not cover.&lt;/strong&gt; A generated set that silently skips&lt;br&gt;
six of nine services is the same trap as the stale wiki, in a fresher timestamp.&lt;/p&gt;

&lt;p&gt;Build this yourself and those four are the specification. Below is one worked answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  One worked answer
&lt;/h2&gt;

&lt;p&gt;This part is about &lt;a href="https://celmis-labs.github.io" rel="noopener noreferrer"&gt;Celmis&lt;/a&gt;, which is mine — a self-hosted&lt;br&gt;
platform for the middle of a development cycle, AGPL-3.0-or-later, under docker compose. It&lt;br&gt;
keeps an index of the code current, and several things come out of that index: alerts you can&lt;br&gt;
act on, a dependency and vulnerability picture, questions answered across repository&lt;br&gt;
boundaries. Documentation is the one this piece is about. Every claim below is in the tree,&lt;br&gt;
including the two places where it does less than you would assume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Noticing.&lt;/strong&gt; A scheduled sweep, once a day by default, does one &lt;code&gt;git ls-remote&lt;/code&gt; per registered&lt;br&gt;
repository: one round trip, no clone, no fetch. It records four outcomes, not two — up to date,&lt;br&gt;
behind, never indexed, and &lt;em&gt;could not tell&lt;/em&gt;. The fourth matters most: a check that cannot reach&lt;br&gt;
the remote and renders as "no new changes" answers wrongly while wearing a fresh timestamp. The&lt;br&gt;
sweep is the floor under the push webhook: a webhook only fires where somebody registered one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Narrowing.&lt;/strong&gt; When the branch has moved, the re-index is incremental: it walks&lt;br&gt;
&lt;code&gt;git diff last_sha..HEAD&lt;/code&gt; and touches only what changed. A reverse index — built by reading&lt;br&gt;
which source files each generated note names in its frontmatter — resolves those changed files&lt;br&gt;
back to the documents that name them. Their vectors are dropped from the search index at once,&lt;br&gt;
so search stops returning removed content before any rewrite starts. Module notes are then&lt;br&gt;
rewritten against the current code; everything else is re-embedded from disk, which restores&lt;br&gt;
search without paying a model for prose that may not have moved.&lt;/p&gt;

&lt;p&gt;And the limit I would rather write than have you find. Feature and integration notes record&lt;br&gt;
their symbols as &lt;code&gt;file::name&lt;/code&gt; graph ids, so a changed file resolves to them. A module note&lt;br&gt;
records a &lt;em&gt;directory&lt;/em&gt; plus bare symbol names, and the lookup is an exact match on the changed&lt;br&gt;
file's full path — so editing &lt;code&gt;src/auth/login.py&lt;/code&gt; does not currently select &lt;code&gt;modules/auth.md&lt;/code&gt;.&lt;br&gt;
The note type that gets the real rewrite is the one the index is worst at selecting. Thirty&lt;br&gt;
lines in &lt;code&gt;src/vault/reverse_index.py&lt;/code&gt;, and exactly the class of bug a hand-written wiki has no&lt;br&gt;
way to even have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not paying twice.&lt;/strong&gt; Each note carries the commit it was written from, and for module notes —&lt;br&gt;
the bulk of a vault — a matching commit is skipped outright next run. Better: when the commit&lt;br&gt;
differs but none of that module's files changed between the two, the stamp is advanced and no&lt;br&gt;
model is called. If git cannot answer — a shallow clone, a missing revision — the code&lt;br&gt;
regenerates rather than skips. Feature and integration notes carry the same stamp but are not&lt;br&gt;
yet gated on it: generate twice at one commit and you pay for those twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refusing.&lt;/strong&gt; Ask it to document a repository that was never indexed and it refuses that&lt;br&gt;
repository by name and says why, rather than writing pages from filenames. Ask for everything,&lt;br&gt;
and a &lt;code&gt;missing_only&lt;/code&gt; condition means what people actually want — the services that have none —&lt;br&gt;
not a rewrite of all of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failing loudly.&lt;/strong&gt; A run in which every document failed used to report success, because the&lt;br&gt;
markdown folder existed. It now raises. So does a subtler one: three production jobs reported&lt;br&gt;
&lt;code&gt;completed&lt;/code&gt;, &lt;code&gt;attempts: 1&lt;/code&gt;, &lt;code&gt;last_error: null&lt;/code&gt; and wrote zero vectors — the documents existed,&lt;br&gt;
only the embedding half had failed. The job is not done unless both halves land.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Saying what produced it.&lt;/strong&gt; Every note a model wrote carries a provenance block: generator and&lt;br&gt;
version, commit, engine, model, timestamp, and — the useful one — the number of index lookups&lt;br&gt;
the writer made. There are two engines. One packs code and metadata into a single prompt.&lt;br&gt;
The other is an agent holding the platform's own MCP tools and nothing else — no shell, no file&lt;br&gt;
reads, no grep — so it cannot open a file and can only ask the index: what calls this, what is&lt;br&gt;
the public surface, what is deprecated. A document written after twelve lookups deserves&lt;br&gt;
different weight from one written from a single prompt: on an agent-written document that count&lt;br&gt;
is in the frontmatter, not a log, and a single-prompt document names its engine and carries no&lt;br&gt;
count. The three assembled pages — index, architecture overview, security findings — come from&lt;br&gt;
the graph and the scanner rather than a model, and carry no block yet. An awkward gap: the&lt;br&gt;
architecture page is the one most likely to be exported and forwarded.&lt;/p&gt;

&lt;p&gt;The block survives export, and the model name is rewritten on the way out to drop this&lt;br&gt;
installation's own workspace identifier, which would otherwise ride into an emailed Word file&lt;br&gt;
and tell a stranger nothing but an internal id. That redaction is on the export path, not the&lt;br&gt;
stored note: open the frontmatter on disk and the full deployment name is still there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Naming the gaps.&lt;/strong&gt; Exporting every repository's documentation as one archive writes a&lt;br&gt;
&lt;code&gt;MISSING.txt&lt;/code&gt; listing the repositories that have none. A download that silently covers six of&lt;br&gt;
nine services is the failure this is meant to avoid, and counting folders is not how anyone&lt;br&gt;
finds out.&lt;/p&gt;

&lt;p&gt;Underneath is a tree-sitter symbol graph — hand-written extractors for the major languages,&lt;br&gt;
generic tag-query extractors for a long tail, and Dockerfiles, compose files, Kubernetes&lt;br&gt;
manifests, Helm charts, Terraform and CI workflows in the same graph, not a separate one.&lt;br&gt;
Repositories can be grouped, and deployment references that cross a repository boundary — a&lt;br&gt;
compose or Kubernetes image, a build context — are resolved to the repository that provides them&lt;br&gt;
and stored as edges in the group's graph; cross-repository calls and imports are not&lt;br&gt;
materialised that way yet. When the platform answers in prose, the citations it renders as links&lt;br&gt;
are re-checked against the files on disk: the file must exist, the cited line must be inside it,&lt;br&gt;
and any quoted code must actually appear there — that last check exists because a model once&lt;br&gt;
invented a plausible function and hung it under a real path. Failures are reported beside the&lt;br&gt;
answer, not dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it still does not do
&lt;/h2&gt;

&lt;p&gt;It does not know why. It does not replace the person in Croatia; it shortens the queue outside&lt;br&gt;
their inbox and moves the questions in it from "how does this work" towards "why did we decide&lt;br&gt;
this". It cannot document a repository it has not indexed, and says so rather than guessing. It&lt;br&gt;
costs model spend, bounded but not zero. And two of its own mechanisms are narrower than you&lt;br&gt;
would assume — which is why they are named above rather than left for you to trip over.&lt;/p&gt;

&lt;p&gt;None of that is fixed by better prose. It is fixed by documentation being an artefact with a&lt;br&gt;
build, a provenance record, and a failure mode that shows up on a dashboard instead of in the&lt;br&gt;
face of a new joiner nine months from now.&lt;/p&gt;

&lt;p&gt;The wiki cannot fail. That is what is wrong with it.&lt;/p&gt;

</description>
      <category>documentation</category>
      <category>programming</category>
      <category>architecture</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A memory server remembers your conversation. That is not the same as knowing your code.</title>
      <dc:creator>constantine makoid</dc:creator>
      <pubDate>Tue, 01 Sep 2026 12:43:48 +0000</pubDate>
      <link>https://dev.to/constantinemakoid/a-memory-server-remembers-your-conversation-that-is-not-the-same-as-knowing-your-code-2mjc</link>
      <guid>https://dev.to/constantinemakoid/a-memory-server-remembers-your-conversation-that-is-not-the-same-as-knowing-your-code-2mjc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Before publishing:&lt;/strong&gt; set &lt;code&gt;published: true&lt;/code&gt;, and check &lt;code&gt;canonical_url&lt;/code&gt; — the article must&lt;br&gt;
exist at that URL on the site first. Without it this competes with the original in search&lt;br&gt;
instead of pointing at it. Tags are from the verified top-1,000 list; &lt;code&gt;mcp&lt;/code&gt; was not in that&lt;br&gt;
cache and is not used here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A session ends. Your agent had worked out, over forty minutes, that the retry logic lives in&lt;br&gt;
one service and the thing that gives up on it lives in another, that the queue name is spelled&lt;br&gt;
two different ways, and that the person to ask about any of it left last year. Tomorrow you&lt;br&gt;
open a new session and it knows none of that. Neither does your colleague's session. Neither&lt;br&gt;
does the agent reviewing the pull request that comes out of it.&lt;/p&gt;

&lt;p&gt;It is the same forty minutes a new engineer spends in week one, and the same forty minutes the&lt;br&gt;
README would have saved if it were still true. It is why a manager asking "where is this up to"&lt;br&gt;
has to interrupt someone who knows. The knowledge exists; it has nowhere to live but in people&lt;br&gt;
and chat logs.&lt;/p&gt;

&lt;p&gt;The reflex is to reach for memory. That reflex is worth interrogating, because there are two&lt;br&gt;
different problems hiding under one word, and only one of them is what memory servers are for.&lt;/p&gt;
&lt;h2&gt;
  
  
  What MCP actually specifies
&lt;/h2&gt;

&lt;p&gt;It helps to be exact, because "MCP memory" gets said as though it were a feature of the&lt;br&gt;
protocol. It is not — and the current revision makes that harder to miss rather than easier.&lt;/p&gt;

&lt;p&gt;Read the base protocol's own three-line summary in revision &lt;code&gt;2026-07-28&lt;/code&gt;: JSON-RPC message&lt;br&gt;
format, &lt;strong&gt;stateless, self-contained requests&lt;/strong&gt;, per-request capability negotiation. Servers&lt;br&gt;
offer three features — Resources, Prompts and Tools. Clients offer one: Elicitation. Sampling&lt;br&gt;
and Roots, which used to make that three, were deprecated in this same revision under SEP-2577,&lt;br&gt;
along with Logging and Dynamic Client Registration; the migration note against Sampling reads&lt;br&gt;
"integrate directly with LLM provider APIs".&lt;/p&gt;

&lt;p&gt;There is no memory primitive and no persistence primitive. There is no memory &lt;em&gt;extension&lt;/em&gt;&lt;br&gt;
either — the official list is the two authorization extensions, MCP Apps, Skills over MCP, and&lt;br&gt;
Tasks. Tasks is the one worth pre-empting, because it advertises "durable handles" and that&lt;br&gt;
sounds adjacent: a task ID survives a disconnect so a client can resume polling a long-running&lt;br&gt;
call, it carries a TTL, and what it holds is the status and eventual result of that one call.&lt;br&gt;
It is durable in the sense a job ID is durable. It is not somewhere knowledge goes to live.&lt;/p&gt;

&lt;p&gt;So every memory you have seen over MCP is a server implementing memory with ordinary tools.&lt;br&gt;
Anthropic's own Memory MCP server keeps a knowledge graph in a JSONL file; other community&lt;br&gt;
servers do the same job over different storage. Claude Code, separately from MCP altogether,&lt;br&gt;
ships Auto Memory — a &lt;code&gt;MEMORY.md&lt;/code&gt; per project, on by default since 2.1.59.&lt;/p&gt;

&lt;p&gt;This is a compliment to those projects, not a criticism. They took a gap the protocol&lt;br&gt;
deliberately left open and filled it with plain tools — exactly what the tool primitive is for.&lt;/p&gt;
&lt;h2&gt;
  
  
  What they remember, and why that is the right thing for them to remember
&lt;/h2&gt;

&lt;p&gt;A memory server stores what was said. Your preferences. The decision you made on Tuesday and&lt;br&gt;
the reason you gave. The fact that you want British spelling and no bullet lists. Entities,&lt;br&gt;
relations and observations, accumulated from conversation.&lt;/p&gt;

&lt;p&gt;Two properties of that content matter here.&lt;/p&gt;

&lt;p&gt;It is &lt;strong&gt;personal&lt;/strong&gt;. My conversational memory is a bad thing to hand to you. It contains my&lt;br&gt;
half-formed conclusions, my shortcuts, and things I said and then revised. Sharing it is not&lt;br&gt;
an unimplemented feature; it is a category error. The value is that it is &lt;em&gt;mine&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;It is &lt;strong&gt;stated&lt;/strong&gt;. It is true because someone asserted it. Nothing in the repository can&lt;br&gt;
contradict it, because it was never derived from the repository in the first place.&lt;/p&gt;

&lt;p&gt;For remembering a conversation, both properties are correct. Now hold them against the other&lt;br&gt;
problem.&lt;/p&gt;
&lt;h2&gt;
  
  
  Knowing the code has the opposite shape
&lt;/h2&gt;

&lt;p&gt;"Where is this symbol used, in every repository, with file and line" is not personal. There is&lt;br&gt;
exactly one right answer and everybody who has read access to those repositories is entitled&lt;br&gt;
to the same one. Storing it per person means storing the same fact many times and being wrong&lt;br&gt;
in a different way in each copy.&lt;/p&gt;

&lt;p&gt;It is not stated, either. It is &lt;strong&gt;derived&lt;/strong&gt; — a function of the current commit, and therefore&lt;br&gt;
&lt;strong&gt;perishable&lt;/strong&gt; in a way conversational memory is not. If I told you on Tuesday that I prefer&lt;br&gt;
tabs, that is still true on Friday. If an agent noted on Tuesday that &lt;code&gt;apply_refund&lt;/code&gt; had three&lt;br&gt;
callers, that is a claim about a commit, and a merge on Wednesday can make it false without&lt;br&gt;
anyone touching the note. A memory store cannot know that happened, because nothing said it.&lt;/p&gt;

&lt;p&gt;That is the failure mode worth naming, and it is the same one that makes stale documentation&lt;br&gt;
worse than none. A remembered fact about code does not decay into silence. It decays into a&lt;br&gt;
confident wrong answer, indistinguishable in tone from a right one.&lt;/p&gt;

&lt;p&gt;So: conversation is personal, stated and durable. Code knowledge is shared, derived and&lt;br&gt;
perishable. Same word, opposite requirements. The fix for the second is not a better memory —&lt;br&gt;
it is not memory at all. It is an index that is rebuilt from the code, that many callers read,&lt;br&gt;
and that has a stated relationship to a commit.&lt;/p&gt;
&lt;h2&gt;
  
  
  One implementation, which you can read
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://celmis-labs.github.io" rel="noopener noreferrer"&gt;Celmis&lt;/a&gt; and it is mine, so treat the description as a&lt;br&gt;
worked example rather than a recommendation. It is a self-hosted platform for most of a&lt;br&gt;
development cycle — the alert that arrives, the fix that goes out, the dependency and SBOM&lt;br&gt;
evidence underneath — and what this article is about is one layer of it: the index. It is&lt;br&gt;
AGPL-3.0, with a carve-out for &lt;code&gt;ee/&lt;/code&gt; that today holds no product code, and every claim below&lt;br&gt;
names the file, so you can disagree with me by reading it.&lt;/p&gt;

&lt;p&gt;Here is the wider loop, so the layer this article is about has something to be a layer&lt;br&gt;
of. Thirty-four seconds, one real incident, every frame a screenshot of it:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/jwgbt8vM7XQ" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;It builds a tree-sitter symbol graph per repository and serves that same graph over MCP. Eight&lt;br&gt;
languages have hand-written extractors — TypeScript, Vue, Python, Go, PHP, Java, C#, C++ — and&lt;br&gt;
sixteen more (Ruby, Rust, Kotlin, Swift, Scala, Elixir, Dart, Lua, R, Solidity, OCaml, F#, Elm,&lt;br&gt;
Gleam, Racket, Fortran) come from the grammar authors' own tags queries, registered at the&lt;br&gt;
lowest priority so a real extractor always wins. Dockerfiles, Compose files, Helm charts and CI&lt;br&gt;
workflows are matched by filename or path rather than suffix, and Kubernetes manifests by&lt;br&gt;
sniffing the first few kilobytes for &lt;code&gt;apiVersion&lt;/code&gt; and &lt;code&gt;kind&lt;/code&gt;. Terraform is the ordinary case —&lt;br&gt;
&lt;code&gt;.tf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The HTTP mount registers 23 tools, eighteen of which read and five of which write. A sample of&lt;br&gt;
what the read half returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;search_symbols&lt;/code&gt; — name, kind, file, line, signature and repo slug, for definitions matching
a name across every repository in a project.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;find_consumers&lt;/code&gt; — who calls a symbol, with repo, file and line. The list of what breaks.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_api_surface&lt;/code&gt; — functions whose &lt;em&gt;names&lt;/em&gt; look like HTTP handlers, with a route path guessed
from the name by turning underscores into slashes, plus method, file and line. A name-convention
heuristic over the symbol index and nothing more; it does not read route decorators, and the
source says so.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_architecture&lt;/code&gt; — the cached orientation summary for a repository, with the model that
produced it and the timestamp, so you can see how old it is.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_owner&lt;/code&gt; — top git-blame authors plus matched CODEOWNERS entries for a path. This is the
one that answers "who do I ask" without asking.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;list_accessible_repos&lt;/code&gt; and &lt;code&gt;get_my_access&lt;/code&gt; — the agent asking what it is allowed to look at,
and being told which path globs are denied.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;start_integration_walk&lt;/code&gt; — an ordered checklist of the other tool calls, returned as data, for
a client that would otherwise fire ten guesses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three things about that list are load-bearing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is the same index, not a copy.&lt;/strong&gt; The MCP tools open &lt;code&gt;settings.repo_graph_path(slug)&lt;/code&gt;. So&lt;br&gt;
does the retrieval layer answering a human's question in the web UI. So does the pull-request&lt;br&gt;
reviewer. One graph file per repository, three readers. Celmis does hold its own clone and index&lt;br&gt;
it — one copy, on your infrastructure, not one per agent or per session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is the same access rules, with one exception I will name.&lt;/strong&gt; Every tool that returns code,&lt;br&gt;
ownership or review content calls &lt;code&gt;caller_access&lt;/code&gt;, which calls &lt;code&gt;resolve_access&lt;/code&gt; from&lt;br&gt;
&lt;code&gt;src/access&lt;/code&gt; — the identical function behind the human REST endpoint and behind multi-repository&lt;br&gt;
question answering. A repository you may not research is omitted and named in &lt;code&gt;blocked_repos&lt;/code&gt;;&lt;br&gt;
a denied path is filtered out of the matches, and &lt;code&gt;search_symbols&lt;/code&gt; also reports how many it hid,&lt;br&gt;
in &lt;code&gt;hidden_symbol_count&lt;/code&gt;, though not every tool yet returns that count. The exception:&lt;br&gt;
&lt;code&gt;list_deprecations&lt;/code&gt; reads its table today without a workspace or access predicate. That is a&lt;br&gt;
bug on my side, not a design, and it is being fixed. While I am being exact: the scope filter on&lt;br&gt;
&lt;code&gt;tools/list&lt;/code&gt; is listing hygiene, not authorisation — a scoped read client is not &lt;em&gt;shown&lt;/em&gt; the&lt;br&gt;
write tools, but the HTTP mount carries no per-call scope check, and a token with no scopes sees&lt;br&gt;
everything. Writes are gated on the token resolving to a workspace, not on its scopes. Do not&lt;br&gt;
treat a read scope as a boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It has a stated relationship to a commit.&lt;/strong&gt; A daily sweep runs &lt;code&gt;git ls-remote&lt;/code&gt; — one network&lt;br&gt;
round trip, no clone, no fetch — and compares the branch head with &lt;code&gt;last_indexed_sha&lt;/code&gt;. It&lt;br&gt;
reports three outcomes, not two: up to date, behind, and &lt;em&gt;could not tell&lt;/em&gt;, because a check that&lt;br&gt;
cannot reach the remote and renders as "no new changes" is worse than no check. Behind enqueues&lt;br&gt;
an incremental pass that diffs &lt;code&gt;last_sha..HEAD&lt;/code&gt;, drops the symbols for every touched file and&lt;br&gt;
re-extracts them, rather than re-parsing the whole repository because one file moved.&lt;/p&gt;

&lt;p&gt;The stdio server carries a raw-Cypher escape hatch too: it tokenises the query and rejects any&lt;br&gt;
of eleven write keywords before running it — a denylist, not a parser, as its own docstring says.&lt;br&gt;
It is not one of the HTTP mount's 23.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not do
&lt;/h2&gt;

&lt;p&gt;Celmis's MCP server has no memory. Zero hits for memory, persist, recall or remember across&lt;br&gt;
&lt;code&gt;src/mcp_server/&lt;/code&gt;. It will not remember that you decided to deprecate the old endpoint, or why.&lt;br&gt;
That is conversational, it is yours, and a memory server is the right tool for it — run one&lt;br&gt;
alongside, and let it keep the decisions while the index keeps the code.&lt;/p&gt;

&lt;p&gt;The honest claim is narrower than "your agent finally has long-term memory", and better. The&lt;br&gt;
next engineer's agent starts out knowing what yours knew about the code, because that knowledge&lt;br&gt;
was never in your chat log to begin with. It is in an index, derived from the commit, readable&lt;br&gt;
by every session under the same rules, and rebuilt when the commit moves.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://celmis-labs.github.io" rel="noopener noreferrer"&gt;Celmis&lt;/a&gt; — self-hosted, AGPL-3.0 (&lt;code&gt;ee/&lt;/code&gt; carve-out, empty&lt;br&gt;
today), &lt;code&gt;docker compose&lt;/code&gt;. Its pull-request reviewer placed 17th of 50 on the Martian Code Review&lt;br&gt;
Bench offline set — seventeenth under all three judges, F1 between 42.7% and 47.5% depending on&lt;br&gt;
who is judging. That is a deliberately unflattering number about one surface of the product and&lt;br&gt;
it stays. The cross-repository capability described above contributed nothing to it: the&lt;br&gt;
benchmark set is isolated single-repository pull requests and the graph came back empty on all&lt;br&gt;
50, so nothing in that table is evidence for or against this path. Check it the way I would want&lt;br&gt;
it checked — register two services, put them in one project, call &lt;code&gt;find_consumers&lt;/code&gt; with a symbol&lt;br&gt;
you know one calls in the other, and see whether the repo, file and line are right.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Introducing Celmis: self-hosted code intelligence over a symbol graph</title>
      <dc:creator>constantine makoid</dc:creator>
      <pubDate>Thu, 27 Aug 2026 16:37:48 +0000</pubDate>
      <link>https://dev.to/constantinemakoid/introducing-celmis-self-hosted-code-intelligence-over-a-symbol-graph-23eb</link>
      <guid>https://dev.to/constantinemakoid/introducing-celmis-self-hosted-code-intelligence-over-a-symbol-graph-23eb</guid>
      <description>&lt;p&gt;&lt;strong&gt;Celmis is self-hosted code intelligence.&lt;/strong&gt; It reads a set of repositories once and&lt;br&gt;
keeps a symbol graph of them; asking questions, reviewing pull requests, auditing&lt;br&gt;
dependencies, routing alerts and serving an MCP endpoint are then all different ways of&lt;br&gt;
reading that one index, rather than five products each holding their own copy of your&lt;br&gt;
code. It runs on one machine under &lt;code&gt;docker compose&lt;/code&gt;, with whichever model provider you&lt;br&gt;
already pay for behind it.&lt;/p&gt;

&lt;p&gt;This is the introduction — what it does, what it refuses to do, and how to run it. The&lt;br&gt;
quickest way to say why it exists is to show the thing a diff-only tool structurally&lt;br&gt;
cannot do.&lt;/p&gt;
&lt;h2&gt;
  
  
  One question, two repositories
&lt;/h2&gt;

&lt;p&gt;I asked a question that spanned two repositories, and the answer quoted both.&lt;/p&gt;

&lt;p&gt;The question was ordinary: &lt;em&gt;"How does the gateway talk to the payments service? Name the&lt;br&gt;
function on each side."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The answer found a Python publisher:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Emit one settlement event on the published topic.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;SETTLEMENT_TOPIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;batch_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and a TypeScript listener in a different repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SETTLEMENT_TOPIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;settlement&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sockets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They never call each other. They meet on a Kafka topic. Then the answer added something&lt;br&gt;
nobody asked for:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Duplicated contract.&lt;/strong&gt; The topic name (&lt;code&gt;payments.settlement.v2&lt;/code&gt;) and the event payload&lt;br&gt;
structure are hardcoded in two separate repositories — &lt;code&gt;src/config.py&lt;/code&gt; in payments and&lt;br&gt;
&lt;code&gt;src/contract.ts&lt;/code&gt; in the gateway. Changing the topic name or the payload schema in one&lt;br&gt;
repository without updating the other will silently break the integration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;A reviewer that reads only the diff cannot say that.&lt;/strong&gt; It never had the other repository&lt;br&gt;
open. That is not a model-quality problem — no amount of reasoning recovers a file that&lt;br&gt;
was never in the context.&lt;/p&gt;
&lt;h2&gt;
  
  
  The shape of the idea
&lt;/h2&gt;

&lt;p&gt;Read the repositories &lt;strong&gt;once&lt;/strong&gt;. Build a symbol graph — deterministically, with tree-sitter,&lt;br&gt;
no model involved. Then everything else is a different way of reading that one index&lt;br&gt;
rather than a separate product with its own copy of your code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ask questions&lt;/strong&gt;, answered with &lt;code&gt;file:line&lt;/code&gt; citations, across repository and language
boundaries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review pull requests&lt;/strong&gt;, with knowledge of who else calls the function in the diff —
including from a repository not in the pull request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit dependencies&lt;/strong&gt;, and produce the artefacts a buyer or an auditor asks for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route what your running services are shouting about&lt;/strong&gt; — alerts arrive, and the index
already knows which repository the failing service is, and who owns it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serve it over MCP&lt;/strong&gt;, so your own editor or agent reads the same index under the same
access rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The index is the product. The rest are surfaces.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the review actually catches
&lt;/h2&gt;

&lt;p&gt;On a real pull request in a demo repository, three findings, all on real lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;              &lt;span class="c1"&gt;// vals is {} — undefined, not a TypeError&lt;/span&gt;
&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// off by one&lt;/span&gt;
&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// never throws, so the catch below is dead&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three inline comments with &lt;code&gt;suggestion&lt;/code&gt; blocks, one summary comment, &lt;strong&gt;zero false&lt;br&gt;
positives on that run&lt;/strong&gt;. I verified all three against the file in the PR branch by hand,&lt;br&gt;
because a review tool that is right four times out of five is a tool you stop reading.&lt;/p&gt;
&lt;h2&gt;
  
  
  The MCP server
&lt;/h2&gt;

&lt;p&gt;Eighteen tools over the same index. Here is a real session, trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;initialize&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;--&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;serverInfo:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"celmis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.29.1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tools/list&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;--&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tools&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;list_projects&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;get_api_surface&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="err"&gt;bootstrap_client&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;search_symbols&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="err"&gt;list_accessible_repos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;start_integration_walk&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;find_consumers&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="err"&gt;get_review&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="err"&gt;route_incident&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;get_owner&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="err"&gt;get_review_policy&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;get_dep_audit&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;get_architecture&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;list_deprecations&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;list_dep_findings&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tools/call&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;search_symbols&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"project_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"083bd97a-…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SETTLEMENT_TOPIC"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;--&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"matches"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"repo_slug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…celmis-demo-gateway"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"variable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/contract.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"repo_slug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…celmis-demo-payments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"constant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/config.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One query. Two repositories, two languages, the same contract symbol — from a client that&lt;br&gt;
has never checked either of them out. &lt;code&gt;find_consumers&lt;/code&gt; is the one I use most: it answers&lt;br&gt;
"what breaks if I change this" across the whole set.&lt;/p&gt;
&lt;h2&gt;
  
  
  Alerts, and why they belong on the same index
&lt;/h2&gt;

&lt;p&gt;This is the surface I nearly left out of this article, which was a mistake, because it is&lt;br&gt;
the one that closes the loop.&lt;/p&gt;

&lt;p&gt;Your services are already producing alerts. They land in a channel where somebody has to&lt;br&gt;
work out which repository the failing service actually is, who owns it, and whether the&lt;br&gt;
thing that broke was touched recently. That lookup is the expensive part — not the alert.&lt;/p&gt;

&lt;p&gt;So the same index answers it. An ingest endpoint takes the alert, a binding routes it, and&lt;br&gt;
the card arrives in chat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /webhook/alerts/{token}
  { "severity": "critical",
    "repo_hint": "celmis-codereviewer/celmis-demo-gateway",
    "title": "checkout: unhandled exception in settle()" }

→ notif_delivered event=alert_received
  repo=celmis-codereviewer/celmis-demo-gateway severity=critical
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Review results ride the same rails — a finished pull-request review posts its own card:&lt;br&gt;
&lt;code&gt;Review CHANGES · PR #4 — 0 critical · 3 error · 0 warn · 0 info&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two things worth stealing from how this is wired, both of which I got wrong first:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The webhook signature is checked before anything else.&lt;/strong&gt; Wrong signature → &lt;code&gt;401&lt;/code&gt;.&lt;br&gt;
Correct → &lt;code&gt;202&lt;/code&gt;. Replay the exact same delivery → &lt;code&gt;{"status": "duplicate"}&lt;/code&gt; rather than a&lt;br&gt;
second review and a second bill. Delivery IDs are cheap; duplicated model calls are not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A failed channel test must not echo the URL it tested.&lt;/strong&gt; Google Chat webhook URLs carry&lt;br&gt;
&lt;code&gt;key&lt;/code&gt; and &lt;code&gt;token&lt;/code&gt; in the query string — &lt;strong&gt;the URL is a credential&lt;/strong&gt;. &lt;code&gt;httpx&lt;/code&gt; puts the&lt;br&gt;
request URL in the exception text, and an early version of the endpoint returned&lt;br&gt;
&lt;code&gt;str(exc)&lt;/code&gt; verbatim, which meant a failed test handed the caller back the secret it was&lt;br&gt;
testing. If you are building anything that tests a user-supplied webhook, go and check&lt;br&gt;
that path in your own code right now.&lt;/p&gt;
&lt;h2&gt;
  
  
  The surface that quietly became urgent
&lt;/h2&gt;

&lt;p&gt;The dependency audit is the one part that is &lt;strong&gt;deterministic end to end&lt;/strong&gt; — native&lt;br&gt;
auditors and OSV.dev, no LLM in the loop. It produces two things.&lt;/p&gt;

&lt;p&gt;A CycloneDX SBOM, and an evidence pack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;sbom/&amp;lt;repo&amp;gt;.cdx.json&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;CycloneDX,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;one&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;per&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;repository&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;findings.json&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="err"&gt;what&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;was&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;found&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;against&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;those&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;components&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;timeline.jsonl&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="err"&gt;when&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;each&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fact&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;entered&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;record&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;MANIFEST.json&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="err"&gt;sha&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;every&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;above&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last file is the point. An archive of files is not evidence — nothing in it stops&lt;br&gt;
the contents from having been edited afterwards. A manifest of digests means &lt;strong&gt;a third&lt;br&gt;
party can verify the pack without trusting the machine that produced it&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"algorithm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"findings.json"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"921412d4bf97eb32fa4b3e8ad09447dab762f19d31ffec45bddb6d7962bf08e5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sbom/gateway.cdx.json"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6eadeafad0043b85a51b65ddddba84ef7b43081064ed9e1b320c62838ddc3d8e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timeline.jsonl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b1815a8e3607712aa9332cd1f8d2d0ec6f93cd8dce53dd92844b8edcde790b3d"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"generated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-26T19:09:41Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why now:&lt;/strong&gt; from &lt;strong&gt;11 September 2026&lt;/strong&gt;, under the EU Cyber Resilience Act, manufacturers&lt;br&gt;
must report an actively exploited vulnerability to ENISA and their national CSIRT within&lt;br&gt;
&lt;strong&gt;24 hours&lt;/strong&gt;. The SBOM itself is not mandated until December 2027 — which is the trap,&lt;br&gt;
because on a 24-hour clock the first question is not how to word the notification. It is&lt;br&gt;
whether you ship the component at all, in which service, at which version.&lt;/p&gt;

&lt;p&gt;The document is due in 2027. The visibility it describes is needed fifteen months earlier.&lt;/p&gt;
&lt;h3&gt;
  
  
  The part I am most attached to
&lt;/h3&gt;

&lt;p&gt;The audit reports &lt;strong&gt;what it could not check&lt;/strong&gt;, as prominently as what it found:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Not fully checked (4). Treat a zero here as unknown, not as safe.

- …demo-gateway — npm via npm-audit: no lock file
  (package-lock.json / pnpm-lock.yaml / yarn.lock) — cannot resolve the tree
- …demo-gateway — all via osv-scanner: recognised no manifest or lock file here
- …e2e-probe/requirements.txt — PyPI via pip-audit: dependency resolution failed
  — audited 4 pinned requirements directly, without the transitive tree
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;An unchecked ecosystem reports zero vulnerabilities exactly like a clean one.&lt;/strong&gt; If your&lt;br&gt;
tooling cannot tell you which of the two you are looking at, that gap &lt;em&gt;is&lt;/em&gt; the finding.&lt;br&gt;
Steal this behaviour regardless of what you use.&lt;/p&gt;
&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;

&lt;p&gt;One machine. Postgres and Qdrant bundled — no external cluster to provision.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Celmis-labs/Celmis.git celmis
&lt;span class="nb"&gt;cd &lt;/span&gt;celmis
./scripts/init-env.sh          &lt;span class="c"&gt;# generates .env, every secret in the format it needs&lt;/span&gt;
docker compose &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a clean server that took &lt;strong&gt;197 seconds&lt;/strong&gt; from &lt;code&gt;git clone&lt;/code&gt; to six healthy services —&lt;br&gt;
measured, not estimated. About 1.1 GB of RAM at peak during indexing, 565 MB at rest.&lt;/p&gt;

&lt;p&gt;Bring your own model key: Gemini, Anthropic, OpenAI, OpenRouter, Groq or Mistral. A free&lt;br&gt;
Gemini key is enough to evaluate it. &lt;strong&gt;No telemetry, no licence check&lt;/strong&gt; — the only&lt;br&gt;
outbound calls are the ones you configure. AGPLv3, the whole thing, not open core.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number I am not hiding
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;17th of 50&lt;/strong&gt; on the Martian Code Review Bench offline set, stable under all three&lt;br&gt;
judges.&lt;/p&gt;

&lt;p&gt;It measures one of the surfaces above — pull-request review on isolated&lt;br&gt;
single-repository PRs. That set has no sibling service for a symbol to have consumers in,&lt;br&gt;
so the cross-repository work this is built around contributes nothing to the score. It is&lt;br&gt;
on the front page of the site with that explanation next to it rather than instead of it.&lt;/p&gt;

&lt;p&gt;I also audited every one of the 79 findings the benchmark counted against us. &lt;strong&gt;33 were&lt;br&gt;
real defects the reference set was silent about.&lt;/strong&gt; That audit is published in full, with&lt;br&gt;
the code for each one — partly because it is the honest thing to do, and partly because&lt;br&gt;
"our precision is actually higher" is a claim nobody should accept without the evidence&lt;br&gt;
attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known rough edges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Semantic search needs a generated vault. Until you make one, the UI says so in an
orange banner rather than quietly returning worse answers.&lt;/li&gt;
&lt;li&gt;The execution sandbox has deliberate internet egress, because &lt;code&gt;pip install&lt;/code&gt; and
&lt;code&gt;npm ci&lt;/code&gt; need it.&lt;/li&gt;
&lt;li&gt;No long-term support branch. Fixes land on the latest release.&lt;/li&gt;
&lt;li&gt;It does not make anyone compliant with anything, and it says so in its own output.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Source, docs and the benchmark audit: &lt;strong&gt;&lt;a href="https://celmis-labs.github.io" rel="noopener noreferrer"&gt;https://celmis-labs.github.io&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer anything — especially about the benchmark methodology, which is the part&lt;br&gt;
that deserves an argument.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>devtools</category>
      <category>selfhosted</category>
    </item>
  </channel>
</rss>
