46リポジトリに跨るコンテキストを、AIがセマンティックに検索できるようにした話

11 min read

目次

  1. ヒントはdb-graphにあった
  2. 同じパターンをcode-graphにも持ち込みたい
  3. ただしAPI / Event / Pageの意味付けは別途必要 ── 全関数にannotationを振るのは無理
  4. annotation graphの設計
  5. annotationの実例
  6. annotationを「現場の開発フローに干渉せずに」回す運用設計
  7. 3 graph間の整合性をSLOで守る
  8. 静的解析グラフとannotationグラフをSAME_ENTITYブリッジで接合
  9. 結果: 「会員のサブスク料金計算」でgraphに入れる
  10. 実利用の数字
  11. 上位フロントドアとしてのMCP
  12. 4-5月の試行錯誤タイムライン
  13. 4月: 拡張とブリッジ初期版
  14. 5月: 安定化と拡張
  15. このタイムラインから見える話
  16. まだ解けていない課題
  17. 1. annotationカバレッジの維持
  18. 2. ブリッジの誤接合の完全排除は構造的に未達
  19. 3. 動的解析の不在
  20. 4. 新リポ追加時の調整負荷
  21. おわりに ── 「捨てた」のではなく「進化させた」

みなさまこんにちは!エアークローゼットでCTOをしているです。

前編で、46リポジトリに跨る本番システムのコードベースを静的解析で1つのナレッジグラフに統合した話を書きました。完成はしたものの、最後に4つの課題(セマンティック検索ができない / ノード爆発 / 関数の中身はファイルを見ないと分からない / 新parser追加の運用負荷)が残っている、という話で終わりました。

今回はこのうち、最初の「セマンティック検索ができない(入口問題)」をどう解いたかという話です。残り3つは前編の状態のまま残しています ── 後半の「まだ解けていない課題」のセクションで、入口問題を解いた結果あらためて見えてきた次の課題と一緒に整理します。

入口問題から始める理由は単純で、graphができても、そこに辿り着くための入口がgrepしかないなら、結局AIは推論せざるを得ない。「事実として渡す」という本来の目的が成立しません。 だから入口問題は他の3つより先に解く必要がありました。

ヒントはdb-graphにあった

実は、同じ構造の問題を、数ヶ月前に別の領域で解いていました。db-graphの話です。

社内には複数サービスにまたがる大量のDBテーブルがあり、「どのテーブルがどの業務に使われているか」を正確にすべて把握できている人はほとんどいない状態でした。知識の広さも深さも人によってまちまちで、全体像が誰の頭の中にも収まらない構造です。そこで、ORM定義から静的解析でスキーマを抽出し、Geminiでテーブル説明文を自動生成、768次元のベクトルにしてgraphに格納、自然言語クエリで意味検索できるようにしたのがdb-graphです。

記事を書いた時点では991テーブルでしたが、今は 21スキーマ / 1,133テーブル / 10,815カラム にまで広がっていて、「テーブル名を知らなくても自然言語で目的のデータに辿り着ける」が日常的に成立しています。

ここで証明できたパターンが、

静的解析グラフ + AIで生成したコンテキスト注入 = 自然言語で意味検索が成立する

というものです。

同じパターンをcode-graphにも持ち込みたい

db-graphで効いたなら、code-graphでも効くはず。そう考えたときに気付いたことがあります。

code-graphの中には、すでに「DBテーブルノード」が境界ノードとして存在しているということです(前編で書いた境界ノードの1つ)。

つまり、code-graphとdb-graphを 接合するだけで、code-graphが自動的にDBの意味コンテキストを持つようになる。annotationを1つも振らずに、既存資産だけでgraphが一段意味豊かになる、ということです。

「graphを繋ぐ」という発想が、ここで初めて出てきました。個別のgraphで閉じない、graph同士を接合していく設計へ。

ただしAPI / Event / Pageの意味付けは別途必要 ── 全関数にannotationを振るのは無理

db-graphの接合でDBコンテキストは入りました。ただ、残りの境界(API / Event)と、graphの起点になるPageには別途意味付けが必要です。これらは静的解析だけでは意味を拾えないので、何らかの形でコンテキストを注入する必要があります。

選択肢は明快でした。annotationでコードに直接書き込むしかない(連載Part 2で書いたcortex内部のナレッジグラフと同じアプローチ)。

ただ、そのまま46リポジトリの全関数に振るのは無理です。何万関数あるか分かりません。既存組織で、既存チームが回している本番コードに、後から全関数annotationを要求するのは現実的じゃない。

ところがここでもう一つ気付いたことがあって、

重要なのは境界ノードだけ。だから境界周辺だけannotationを振れば十分意味を持つ。

ということです。

AIが「このコードを変えたら何が壊れるか」「このAPIは他のどのリポから叩かれているか」を知りたいとき、必要なのは関数の中身のロジック説明ではなく、境界の意図 (= この画面は何のためか、このAPIは何を返すのか、このEventは何の節目を表すか)です。

= 最小限のannotationで最大の意味を得る。これがこの後の設計の核心になりました。

annotation graphの設計

整理するとこうなります(annotation graphは内部ではservice-product-graph、略してSPGと呼んでいます)。

3つのgraphを並列接続して意味のあるナレッジグラフをつくる

3つのgraphが並列に存在して、互いにSAME_ENTITYで接続されている構造です。階層関係ではなく、どのgraphを起点にしても他のgraphまで辿れる形になっています。

そしてAIエージェントが叩く入口に MCPサーバー が立っていて、3つのgraphを横断する形で動きます。db-graphには直接接続させず、annotation graph側のMCPがプロキシとしてdb-graphも呼び出す構造にしています。

annotation graphのノード種はPage / Section / Dialog / Field / Action / Api / Taskの7種。元々は画面中心の設計で screen-graph と呼んでいましたが、backendのApi / Taskまで広げた段階で名前をservice-product-graphに変えました。

annotationの実例

具体的にはこんなふうに書きます(架空例ですが、形式は本物に近いものです):

/**
 * @graph-page /home
 * @graph-business メイン画面。 会員が現在借りているアイテムの確認、 購入、 返却ができる
 * @graph-label ホーム画面
 * @graph-has-section banners, wearing-items, wearing-return, delivery-status
 * @graph-has-dialog buying-modal, return-modal
 * @graph-navigates-to /return-procedure, /checkout, /my-karte
 * @graph-calls GET /api/v1/wearing
 * @graph-reads admin_delivery_orders, admin_rental_items
 * @graph-flow styling-loop
 * @graph-status monthly-member
 */

ポイントになるのは2つです。

これに加えて、テストケースを導出する根源になる @graph-case(条件分岐パターン)もありますが、そのあたりは別の機会に。

annotationを「現場の開発フローに干渉せずに」回す運用設計

ここからが実用性の核心です。

annotation graphを作ると決めたあとに直面する制約はこうでした:

つまり、「人とAIを同じPR内で混在させない」 ことが必要でした。

解決策はannotationを物理的に別ブランチに分離する設計です。

main branchとannotation branchを物理分離する運用

これは連載Part 6で書いた「AIに全コードを通すゲート設計」の理想を、既存組織の現実条件に合わせて分離設計したものです。cortex(社内AIプラットフォーム)は自分が1から組み立てているモノレポなので「全コード必ずAIゲート通過」が成立しますが、46リポジトリの本番系では成立しない。だから理想を諦めるのではなく、「人の開発フロー」と「AIのannotation運用」を物理的に分けて両方とも回す、という選択をしました。

3 graph間の整合性をSLOで守る

annotation運用が回るだけでは、3つのgraph(code-graph / db-graph / annotation graph)の 接合の質 は保証されません。そこで、graph全体の整合性を機械的に検査するSLOを定義しています。

主要なルールはこうなっています:

これらは要するに、「お互いに境界が繋がってなかったらおかしいよね?」という素朴な問いをSLOに落としたものです。閾値を下回ったらアラートが上がり、graph全体の信頼度を毎日守ります。

前編で書いた境界分析cron(接続率5%劣化アラート)はcode-graph単独の話でしたが、こちらは 3 graphを横断したSLO で、graph同士の接合まで守りに行く仕組みです。1リポにparserを追加した、annotationを書き足した、schemaが変わった ── 何が起きても、翌朝には接合の品質劣化が見える状態になります。

静的解析グラフとannotationグラフをSAME_ENTITYブリッジで接合

ここまで「接合」と書いてきましたが、実際の接合はそれほど素直ではありませんでした。

静的解析で抽出したAPI / Page / Taskノードと、annotation graphで書いたAPI / Page / Taskノードは、別物のノードとして作られます。同じ意味を持っているはずなのに、名前 / パス / 識別子の表現が違うので機械的には繋がらない。

これを接合するために、SAME_ENTITYという別のエッジを生成しています。3種類のブリッジ:

それから運用上の落とし穴も一つありました。初期実装は INSERT NOT EXISTS で重複を避けていたつもりだったのですが、BigQuery streaming bufferの反映遅延で重複挿入が起きて、あるリポではエッジが106 → 214と倍増しました。これはMERGE INTOに書き換えて冪等化することで解決しています。

結果: 「会員のサブスク料金計算」でgraphに入れる

ここまで作ったあと、前編の最後に書いた入口問題が解けました。

「会員のサブスク料金計算が間違っているらしい」

これを自然言語のままannotation graphに投げると、ベクトル検索で関連ノード(Page / Api / Function / DBテーブル)が事実として返ります。そこからSAME_ENTITY経由でcode-graphの関数に渡り、関連する別リポの呼び出し元 / 呼び出し先まで辿れる。さらにcode-graphのDB境界からdb-graphに渡れば、関連カラムまで取れる。

もちろん起点はどこでも良くて、「テーブル名から逆引きしたい」ならdb-graph起点、「この関数の影響を見たい」ならcode-graph起点で同じネットワークを辿れます。1つの自然言語クエリでも、特定のノード起点でも、3つのgraphを横断して関連する全コード + 全DBスキーマ が取れる状態になりました。

前編で書いた 「graphはあるのに、入口が見つからない」 が、ようやく解消した瞬間です。

実利用の数字

2026年4月16日(最初の本番投入)から執筆時点(約2.5ヶ月)で、annotation graphのMCPサーバーには 約50,000回 / 約73人の利用があります。内訳はこうです:

注目すべきは2行目です。「コードベースを自然言語で検索」は本来エンジニアの道具ですが、入口問題が解けたことで、エンジニア以外の職種でも「この機能ってどう動いてるんだっけ?」「このDBには何が入ってるんだっけ?」を自分の言葉で引けるようになりました。

これは連載Part 5で書いた「コードを書かない人がAIで仕様書を作れる時代」の隣接した動きで、graphを意味で引ける状態が組織全体に効くことを示しています。利用回数の比率では当然エンジニアが圧倒的ですが、「使い始めている職種の幅」が広がっている事実が、入口問題を解いたインパクトです。

上位フロントドアとしてのMCP

この3 graph横断の入口がMCPサーバーです。サービス検索 / サービス詳細 / API詳細 / データフロー追跡 / 影響範囲追跡 / ビジネスルール全文検索、という6つのツールを持っていて、AIエージェントが叩く唯一のエントリーポイントになっています。

特に注目すべきは、db-graphには直接接続させない設計にしていることです。annotation graph側のMCPがプロキシとしてdb-graphを呼び出す形にして、AIエージェントからは「1つのMCPに聞くだけで全部出てくる」状態を維持しています。

これで「画面 → API → コード → DB → カラム」のフルチェーン辿りが1つのMCPツールで完結する構造になりました。

4-5月の試行錯誤タイムライン

前編で1-3月のcommitを引いたのと同じ手法で、後編は4-5月の主要commitを並べます。

4月: 拡張とブリッジ初期版

5月: 安定化と拡張

このタイムラインから見える話

4月15日に「拡張 + 横断ツール + ブリッジ」がほぼ同時に入って、そこから1週間で「Redis / EventBridge / Taskブリッジ / annotation自動メンテ」と週単位で機能を積み上げていったのが分かります。

特に 4月21日のannotation自動メンテパイプラインが、前編で残した「人だけじゃ無理、AIならできる」の宿題を実装で回収した瞬間でした。ここから先は、annotationを「人が頑張って書く」から「AIが書く前提で運用設計する」にステージが変わりました。

まだ解けていない課題

ここまでで入口問題は解けましたが、もちろん全部が綺麗になったわけではありません。

1. annotationカバレッジの維持

frontend側は手厚く振られていますが、backend / Go / batch系はまだ薄めです。「振られていないノード」が存在する構造は、ゼロにはできません。これは継続的な運用課題です。

2. ブリッジの誤接合の完全排除は構造的に未達

特にPageブリッジは、1つの境界に対して複数のannotation Pageが紐付くケースが構造的に避けられません。戦略を増やすことでカバレッジは100%に持っていけましたが、「正しく繋がっている」を100%保証するのは難しい。

3. 動的解析の不在

graphに乗っているのは「edgeが静的に存在する」という事実だけで、「実際にそのedgeが本番でどれくらい使われているか」は分かりません。静的解析グラフに本番実行回数を流し込んで、dead-code edgesを別シグナルとして可視化する ── ここはまだ手をつけられていません。

4. 新リポ追加時の調整負荷

新しいリポジトリが本番系に加わるたびに、ブリッジの正規化ルールやリポ別パターンの調整が必要です。前編4番目に書いた「新しい境界パターンが出るたびに独自parserを足す運用負荷」の、annotation graph側の同型課題です。

おわりに ── 「捨てた」のではなく「進化させた」

前編の末尾の補足で、cortex(社内AIプラットフォーム)側ではcode-graphアプローチを早い段階で見送って、アノテーションベースのナレッジグラフに倒した話に触れました。「捨てた」と書いてもおかしくないくらいの早い見切りだったわけですが、この連載を通して振り返ると、正確には「捨てた」のではなく 「進化させた」 が正しい表現でした。

進化の正体は、結局3つのgraphの並列接続に集約されます:

これらをSAME_ENTITYで互いに接続して、MCPでAIに渡す。静的解析だけでは届かなかった「意味で引く」を、db-graphの成功体験を再利用 + 境界周辺だけannotationを振る最小限戦略で成立させた、という話でした。

そしてもう一つ、連載AIハーネス(Part 1-6)との対比で言うと、こうも整理できます:

= 同じ思想(AIを信頼せず設計する)の、異なる条件下での実装でした。

長文をお読みいただきありがとうございました。

comments (27)

  1. Eryc Tri Juni Svia dev.to

    The refusal architecture part of this thread really got me — coming at it from a different angle (public content/GEO, not internal code graphs), but I think I'm seeing the same thing. In my own RAG testing, when the model doesn't have enough retrieved context it almost never says so, it just... fills the gap and sounds confident about it anyway. Same root problem, just showing up in content retrieval instead of code search. What stood out is you pushing for server-side gating instead of trusting the agent to flag its own uncertainty. Makes sense — if you leave it up to the model, it'll turn "not sure" into a fluent sentence every time, because that's what it's optimized to do. Now I'm curious if something like that could work outside a controlled internal setup — like, could a public site expose some kind of confidence signal to crawlers, or does this only work because you control both sides of the pipeline?

    1. Ryosuke Tsujivia dev.to

      Glad that part landed. Your RAG observation matches exactly what pushed us to server-side gating. The model turning "not enough context" into a fluent answer isn't a bug in any one model, it's what generation is. So the "I don't know" has to be produced by something that isn't a generator. On your question, I think the honest answer is that the enforcement half only works because we control both sides. What the server-side gate actually does is refuse to hand the model ambiguous material in the first place. Below a similarity threshold it returns a typed "no confident match" instead of the top-N-whatever, so the model never gets the raw ingredients to hallucinate from. That refusal happens before generation, and it requires owning the retrieval server. A public site can't enforce that, because the gate has to sit on the consumer's side of the pipe and you don't own the crawler. What a public site can do is make the honest path cheaper than the guessing path. Structured data, explicit scope statements ("this page covers X as of date Y, it does not cover Z"), stable anchors, machine-readable freshness. None of that forces a model to admit uncertainty, but it changes what retrieval hands the model, which is the same lever we pull internally, just without the guarantee. For crawlers specifically, my guess is confidence signals end up like robots.txt. A convention that well-behaved consumers respect and nothing enforces. Publisher-supplied relevance signals also have a trust problem baked in, since they're exactly the kind of thing that gets gamed (meta keywords walked this road already). There is one path where a publisher genuinely gets the gate back, though. Expose your content as an MCP server and let agents call it directly instead of scraping. Then you own the retrieval endpoint again, and everything we do internally becomes available to you. Typed refusals below a threshold, explicit scope answers, even query logs showing what AIs are actually asking about your content. [Stripe](https://docs.stripe.com/mcp) and [Cloudflare](https://developers.cloudflare.com/agents/model-context-protocol/cloudflare/servers-for-cloudflare/) already do this for their docs, and coding agents really do call those endpoints. To be clear, this doesn't move the needle on how search engines rank or summarize you. It's a separate serving channel for the agentic side of consumption, not an SEO lever. But for the slice of traffic that comes from agents rather than crawlers, it's the one place the publisher controls both sides of the pipe.

    2. Eryc Tri Juni Svia dev.to

      Really appreciate you taking the time to lay that out — "refusal has to happen before generation, and that requires owning the retrieval server" clarified something I'd been fuzzy on. To answer my own question with what I think the honest scope is: MCP is for agents that act — bidirectional, you own both ends, you can gate before generation. What I built (AGP) is aimed upstream of that, at crawlers that only ever read, once, passively, with no request/response loop to gate at all. Mechanically: I picked Google Sites deliberately as a worst-case CMS — it can't produce clean canonical tags or structured data, and it's on a zero-authority .my.id domain, so if this works at all, it isn't explainable by domain trust or CMS quality. A Cloudflare Worker sits in front of it and uses HTMLRewriter to edit the HTML mid-flight, as it streams past — not replacing the origin, just correcting what it hands off. Crawlers get a separately-stored clean semantic payload injected in; humans get the untouched page. Same content, different container, no attempt at persuading anyone of anything. So probably not competing solutions — MCP fixes the action side, AGP (at best) fixes whether the read side is even structurally intact in the first place. Still an open question whether "structurally clean" is enough on its own, or whether a crawler needs a way to signal back "was this actually sufficient" — which sounds like exactly the gap your server-side gating closes on your end.

    3. Ryosuke Tsujivia dev.to

      The worst-case-CMS setup is good experimental design. Zero-authority domain plus a CMS that can't emit structured data means if retrieval quality improves, you've actually isolated the variable. Most GEO claims never control for domain trust, so this alone puts your test ahead of the field. The part I'd watch is the crawler/human split itself. In classic SEO that shape has a name, cloaking, and engines penalize it regardless of intent because they can't verify intent, only divergence. Your defense is semantic equivalence, same content in a cleaner container, and I think that's the right line to hold. But it's worth designing as if someone will eventually check. If AGP-like layers spread, the obvious countermeasure for engines is to render both variants and diff the semantics, and the setups that survive that check are the ones where the equivalence is real. Yours sounds like it is. The gray zone arrives when people copy the architecture and start "improving" the crawler payload beyond what the human page says. On the feedback question, you have more signal than you might think. You own the Worker, which means you own the access logs, and crawl patterns are a crude but real read on what retrieval is doing. Which paths get fetched, how often, whether a crawler comes back after the clean payload shipped. It's nowhere near a "was this sufficient" signal from the model side, but it's the same move we made internally, when you can't gate the consumer, instrument the part of the pipe you do own.

    4. Eryc Tri Juni Svia dev.to

      Went back and actually audited the code against your point rather than just agreeing with it. Two findings worth sharing. First, the element-stripping for AI bots (script/style/iframe/header/footer) turned out to be justified, but I wanted to verify rather than assume: Google Sites ships large non-semantic JS blobs inside those wrapper tags, and the bot-facing payload carries its own replacement footer with the actual contact/nav info. So the stripping is noise reduction, not content removal — checked it, the semantic substance survives in the KV payload, the JS bloat doesn't. Good distinction to have confirmed rather than assumed. Second, honestly, the audit surfaced that not every page in the architecture is at the same maturity level. The case study page holds up well on content parity. Older parts of the routing logic still have some rough edges I'm working through — exactly the kind of thing your diffing prediction would catch, which is the point. Better to find it now going page by page than have it found later. Appreciate the push to actually check instead of just asserting it.

    5. Ryosuke Tsujivia dev.to

      Glad the comment turned into an actual audit, that's the best outcome I could have hoped for from this thread. And finding the uneven pages yourself, before any crawler-side diff exists, is the whole game. Good luck with AGP, I'll be curious how the experiment reads in a few months.

    6. Eryc Tri Juni Svia dev.to

      Appreciate that, genuinely. This whole thread ended up being worth more than four months of trying to get anyone to look at it — the diffing point specifically is going to stick with me long after this conversation ends. Going to keep working through the rest of the pages the same way, on my own time now rather than in public. Good talking through this with someone who actually knows the terrain. Take care.

  2. Mudassir Khanvia dev.to

    the annotation maintenance loop has one structural risk: the AI that writes and the AI that reviews likely share model weights. a systematic generation error might look plausible to a reviewer with the same priors. the SLO catches missing coverage but not plausibly wrong coverage. the owner sampling Mike proposed closes that gap. what I am curious about is the annotation generation trigger: when a new boundary appears in a diff, the webhook fires and the AI annotates what is in the diff. but if a new route is registered in a config file and the actual handler lives in a file not touched by that diff, does the pipeline catch the handler as needing annotation, or does it slip through and stay unannotated until the SLO eventually fires?

    1. Ryosuke Tsujivia dev.to

      Great decomposition on both sides. On shared model weights, you're right, and Mike's owner sampling is exactly what closes that gap. AI review reduces the review load; it doesn't eliminate it. A small sample of ground-truth eyes stays in the loop specifically for the "plausibly wrong but covered" failure mode the SLO can't see. On the config-in-diff-but-handler-elsewhere case, in principle it should catch it, and this is where the code-graph approach diverges most clearly from diff-only SaaS AI Reviewer bots. Annotation-author and annotation-reviewer both traverse via the code graph, not the diff alone, so they can reason about code that never appears in the PR diff at all. When a new route registration lands in a config file, the graph resolves the handler that route points to (the boundary → handler edge that static analysis extracts up front). The pipeline sees "this route is new, and its handler in some-other-file.ts has no @graph-* annotation" as one connected piece of work, even when the handler file isn't in the touched set. That gets queued for annotation on the same PR. Where this would actually slip is if the config → handler resolution itself is dynamic in a way static analysis can't follow. Handler paths constructed from runtime variables, dynamic imports from computed strings, and similar patterns fall back to the SLO picking them up later. That's honestly the weakest link in that chain.

  3. Wren Callowayvia dev.to

    [@ryantsuji](https://dev.to/ryantsuji) The exact-equality-after-transformation point is the correction that matters, and I want to make sure I absorb it rather than nod past it. You're right that "the transformation pointed at a different real entity than the author intended" is a categorically different animal than "the scorer picked a similar-looking node." My original framing smuggled in the fuzzy-match failure mode without earning it. Granularity loss is the real shape here, and it's actually the more insidious one to alert on precisely because every individual bridge is defensible in isolation — the boundary does contain the component. There's no single wrong edge to point at, just a distribution that's quietly coarser than the annotation author's mental model. Which makes me think the drift alert wants a second signal beyond "% resolving via strategies 3–6." That catches the cascade sliding down the priority order, but the parent_dir absorption you're describing might not move that number much if strategy 5 was always doing the work — it'd show up instead as fan-in: multiple SPG nodes converging on one boundary. So maybe the thing to threshold is the ratio of collapsed nodes per boundary per repo, not just which strategy won. A boundary that suddenly absorbs six Pages where it used to hold two is the granularity-loss tell even when match_strategy never changed. The daily cron diffing stored bridges against a from-scratch re-resolve is the piece I'd underweighted, and it's the closest thing you have to a correctness guard that isn't a human noticing. The gap is that it catches strategy flips, not stable-but-coarse joins that never flip. Pairing that with the quarterly owner-sampling is right — owners are the only ones who actually hold the intended granularity, and no cron recovers that from the derived identifiers alone. Good thread. I learned the shape of my own critique better by being wrong about half of it.

    1. Ryosuke Tsujivia dev.to

      The fan-in signal is the piece I'd been missing. You're right that strategy-drift alone misses stable-but-coarse joins, because "strategy 5 has always won" won't move the distribution while the number of SPG nodes pointing at a single boundary quietly climbs. Ratio of collapsed nodes per boundary per repo, tracked over time with a delta threshold, is a sharper monitor. It catches the failure mode where every individual bridge is defensible but the aggregate is coarser than the author intended. One layer I'd add on top: the same signal has a runtime use, not just a monitoring use. When an agent queries a boundary that aggregates N SPG Pages, that N itself is a confidence hint the agent should surface ("this boundary corresponds to N distinct annotated pages, treat the granularity accordingly"). Same information, shifted from "we notice granularity drift once a week" to "the consumer sees the granularity every query." Neither is implemented, but the second one might actually change AI behavior in practice, since the LLM in the loop can be told to treat high-fan-in boundaries as ambiguous rather than definitive. The gap in the cron you identified is exactly right. It's a strategy- flip detector, not a granularity-drift detector. That's a good reminder that "we already have a monitor for that" and "we have a monitor for the specific failure mode you're pointing at" aren't the same thing, and I probably lean on the cron too hard as a catch-all mental model. Owner-sampling as the correctness backbone is where this lands for me too. Deterministic identifiers can encode "which entity does this transformation point at" but they can't encode "what granularity did the author actually want." Only the owner holds that. The sampling is expensive compared to a cron, but it's the only piece that closes the loop on intent-vs-derivation. Combined with fan-in ratio alerts, that's a stack that starts to look like it's guarding correctness rather than existence. "I learned the shape of my own critique better by being wrong about half of it." That's the read I aspire to when I comment on other people's technical work. Really appreciate it.

  4. Wren Callowayvia dev.to

    The SAME_ENTITY bridge coverage hitting 100% is the number I'd worry about most, precisely because you got there by adding strategies. Your issue #2 admits the Page bridge maps multiple annotation Pages to one boundary — which means "100% connected" and "100% correctly connected" have quietly diverged, and once they diverge the SLO stops measuring what it looks like it measures. A HANDLES_API handler can be 95%-connected downstream and still be bridged to the wrong annotation, and the vector search will happily return that wrong node as a "verified fact." That's a nastier failure than grep-inference, because it wears the costume of ground truth. The thing your SLOs guard is edge existence; what you can't cheaply guard is edge correctness, and the 6-strategy priority order is exactly where a silent mis-join hides — strategy 5 (parent-directory linking) firing when strategy 1 should have. If you're not already, I'd track how often the winning bridge is a low-priority fallback per repo and alert on drift there, not just on connection rate. A repo where suddenly 30% of Page joins resolve via "strip dynamic segments and match parent" is telling you the confident-but-wrong rate is climbing even while your dashboard stays green. One genuine question, not a gotcha: when a mis-join surfaces, does an agent or a human catch it, and how? That correction loop is the actual trust boundary of the whole system, and it's the one piece the post doesn't show.

    1. Ryosuke Tsujivia dev.to

      The 6-strategy priority order framing is accurate for the Page bridge. That's literally what the code does: candidates are UNION'd across six matching strategies (url_pattern / component_path / item_id / url_pascal / parent_dir / boundary_dynamic_strip), ranked by strategy priority, top-1 wins per SPG node. The Api bridge has the same shape with four stages (exact normalized match, trailing ? variance, trailing /:dynamic strip, boundary :dynamic regex against literal segments for dynamic dispatch). So the cascade isn't unique to Page. One nuance that changes the failure-mode profile though: each strategy still resolves to exact equality on a deterministically-derived identifier. No fuzzy string matching, no edit distance, no substring scoring. The "heuristic" part is the choice of transformation rule (strip this prefix, PascalCase these segments, walk up to parent directory). Once the transformation runs, the actual join is =. That constrains how mis-joins can happen: not "the algorithm picked a similar-looking wrong entity" but "this transformation rule pointed at a different real entity than the annotation author intended." The mis-join mode we actually see (issue #2) is the second kind, and it's granularity loss rather than confidently-wrong: multiple SPG Pages collapse onto one boundary when a shared parent directory catches them (strategy 5, parent_dir). Each individual bridge is "correct" in the sense that the boundary does contain that component, but the annotation author often wanted more granular boundaries than exist. You're right that this still isn't what a "% connected" SLO catches, so your critique lands, just aimed at collapsed granularity rather than plausible-but- wrong. Your alerting suggestion is the right shape for detecting that. Per-strategy tagging is present on each edge (the match_strategy field is stored on the SAME_ENTITY row), so per-repo distribution is queryable retroactively. What isn't wired up is the drift alert. Threshold on "% of joins resolving via strategies 3 through 6 per repo per week" and pager on delta would surface the parent_dir absorption pattern you're pointing at. Not implemented today. To your genuine question on correction loops. Mis-joins currently surface through three paths: An engineer notices during use (agent returns a plausibly wrong neighbor, they dig in) The daily boundary-analysis cron re-runs the strategy resolver from scratch and diffs against stored bridges; a bridge that flipped strategies shows up in the change log A downstream test fails (rare, because most consumers are LLMs that don't verify) Human is the primary catcher today. AI in the loop is good at using bridges but bad at questioning them, precisely because they look like ground truth. Making the correction loop first-class rather than an artifact of use is the actual gap you're identifying. The owner-sampling proposal from an earlier thread in these comments (a quarterly sample of ~50 bridges per relationship type, marked mismatch by entity owners) is aimed at exactly this. Combined with your strategy-drift alerting, that gets a lot closer to guarding correctness rather than just connectivity. "Confident-but-wrong wearing the costume of ground truth" is a phrase I'll be borrowing. Thanks for the read.

  5. Nazar Boykovia dev.to

    On the no dynamic analysis gap at the end, one thing that might fold in cleanly is treating production call counts as weights on the edges you already have. You're not adding a new graph, just annotating the existing SAME_ENTITY and call edges with how often each one actually fired last week, and the dead edges fall out as the ones that never lit up. It also gives your blast radius queries a way to sort by real traffic instead of treating every reachable path as equally likely. The choice to annotate only the boundaries is the part I found most convincing, since it's the same instinct as documenting a system for a new teammate. You explain the seams, not every private helper.

    1. Ryosuke Tsujivia dev.to

      Yes, that's exactly the direction I want to go. Annotating existing static edges with runtime weights (call counts, last-fired timestamps) rather than building a parallel graph fits naturally on top of the boundary-level structure we already have. At the boundary granularity (API / Event / Page entrypoints), this works cleanly: how often a boundary fires last week is a reasonable importance and freshness signal. The complication shows up when you push the weighting inside the boundary and start annotating individual call edges / nodes along the internal path. There, what the count means depends on whether the path is happy or error. On a happy path, the usual logic holds (frequent = important, rare = candidate for cleanup). For error-path code (catch blocks, retry handlers, fallback branches), frequent execution still carries value because it surfaces upstream problems worth investigating. But infrequent execution doesn't imply the handler is unimportant. A rarely-fired catch block might be the safety net that's quietly kept the system intact all year. So for internal-path weighting, you'd want to classify the execution path itself first (happy / error / recovery / one-off init) before you can interpret what the count actually means. I don't have a clean answer for how to do that classification reliably yet. Some combination of structural cues (try/catch position, branch reachability) plus runtime correlation with error logs / 5xx rates seems plausible, but it's the main open problem for me on the dynamic side.

  6. Kartik N V J Kvia dev.to

    Embedding raw function bodies tends to bury the signal under boilerplate, so fixing the entry-point problem first makes sense. Did you embed a generated summary of each node instead of the source span, and if so how do you keep those summaries from drifting as the code changes under them? Static-analysis-first seems like the only way the graph stays trustworthy across 46 repos.

    1. Ryosuke Tsujivia dev.to

      Yeah, that's basically the split. One AI writes annotations on each PR diff, another reviews that they still match the code. A few extra details worth flagging: We don't embed a runtime-generated summary, we embed the @graph-business JSDoc field that the annotation AI commits directly above each declaration. It's 1-2 sentences of business semantics co-located with the code, so the thing being indexed is small, human-readable, and survives in git history. Drift detection is mostly free: since the annotation lives in JSDoc on top of the declaration, any code change in that file pulls the annotation into the same PR diff. The reviewer AI sees both sides side-by-side. Both AIs have a hard rule that they can ONLY modify JSDoc, never application logic. The reviewer enforces it (any non-JSDoc diff line is treated as REQUEST_CHANGES). Without this constraint the two roles could cooperatively rewrite code "to keep things consistent", which is exactly the drift you'd want to prevent. Static analysis (tree-sitter) still owns the structural part of the graph (files, functions, imports, calls). The AI layer only fills business-level metadata (@graph-business, @graph-connects), so the drift surface is the annotation layer only, never the structural backbone. And yes, static-analysis-first is exactly why this holds across 46 repos. The annotation layer just adds the part static analysis can't see (e.g. "this endpoint is the Slack OAuth callback"), and the two AI roles keep that layer in sync with the code.

  7. Sloan the DEV Moderatorvia dev.to

    Hey, this article appears to have been generated with the assistance of ChatGPT or possibly some other AI tool. We allow our community members to use AI assistance when writing articles as long as they abide by our [guidelines](https://dev.to/guidelines-for-ai-assisted-articles-on-dev). Please review the guidelines and edit your post to add a disclaimer. Failure to follow these guidelines could result in DEV admin lowering the score of your post, making it less visible to the rest of the community. Or, if upon review we find this post to be particularly harmful, we may decide to unpublish it completely. We hope you understand and take care to follow our guidelines going forward!

    1. Ryosuke Tsujivia dev.to

      Thanks for flagging this — I've added an AI assistance disclosure to the top of the article. (And going forward, all my dev.to articles will carry the same disclosure automatically.)

    2. FrancisTRᴅᴇᴠ (っ◔◡◔)っvia dev.to

      Thanks for Correcting the issue. This issue is now resolved. Great post as well :D

    3. Ryosuke Tsujivia dev.to

      Thanks Francis, appreciate the quick follow-up. Glad the post landed well!

  8. Siyuvia dev.to

    The "entry-point problem" you articulate, where the knowledge graph exists but has no semantic surface for natural-language queries, maps directly onto a parallel problem in professional discovery. LinkedIn has the data, CVs have the data, but there is no way for an AI agent to ask "who in my network has led a database migration, communicates asynchronously, and prefers written briefs over meetings" and get verified results. I work on Opportunity Skill, which builds structured semantic impressions from daily work patterns that other agents can query, solving the same category of problem for professional knowledge instead of code knowledge. In both cases the cost is the same: the data already exists, but no one can reach it. What interests me most about your annotation philosophy is the deliberate minimalism: only boundary nodes get annotated, and AI handles it entirely in a parallel branch with no human review. This is not a compromise, it is a design insight. Boundary nodes carry disproportionate meaning relative to their count. A single @graph-business tag on a page explains more about what the system does than annotating every internal function ever could. The parallel-branch decision goes further: it acknowledges that maintenance cost, not annotation quality, is the bottleneck at scale. Humans cannot be relied upon to keep annotations current across 46 repos because annotation drift is invisible to them. AI can, because annotation generation is just another pipeline step. The same principle applies to maintaining professional semantic profiles: humans forget to update their bios, but an agent observing daily work patterns can refresh impressions continuously without the user lifting a finger. The non-engineer adoption data, 2,800 calls from stylists and customer support, reveals something beyond "semantic search works." A graph queried by meaning reallocates who gets to ask questions. When the access interface shifts from grep to natural language, knowledge stops belonging exclusively to the people who know where it lives and starts belonging to anyone who knows what they need. The same threshold exists in professional discovery: the barrier between needing a connection and finding one is not missing information, it is missing semantic entry points. The moment profiles become agent-queryable rather than human-readable, the entire cost structure of discovery changes. The graph was always there. The entry point was not.

    1. Ryosuke Tsujivia dev.to

      This is the parallel I was hoping someone would see — and your cross-domain framing is sharper than what I'd been holding internally. On boundary-node minimalism: yes, exactly. Once I accepted that internal helpers don't carry semantic weight (only the entry/exit surfaces do), the cost equation flipped. I'd been thinking about "how to annotate everything" when the answer was "annotate almost nothing, but pick the right almost-nothings." Boundary nodes are where intent is expressed because they're the only places where the system communicates with anything outside itself. I'd extend your maintenance-cost framing by one layer: humans don't just forget — they actively don't notice drift, because the drift is between code and meaning, not between code and code. Compilers catch code-vs-code drift. Nothing catches code-vs-meaning drift except a verifier that also reads meaning, which, pre-LLM, was a human reviewer context-switching every PR. AI verifying code-vs-meaning at PR time, every PR, asymmetric cost-wise — that's the actual unlock. The parallel-branch implementation is adaptation to the constraint we have today: 46 long-running repos where retrofitting annotations into main isn't realistic. For new environments we're building AI-native from day one, annotations sit in main alongside code, generated and reviewed in the same PR — convergent steady state, different starting point. The parallel-branch is the legacy path; main-branch annotations are the greenfield path. The "who gets to ask questions" point is where my own framing was still incomplete. I'd been describing it as "non-engineers can search now," but yours is more accurate: the entry point is where access is gatekept. grep was a competency moat masquerading as a tool. The moment the interface becomes natural language, knowledge becomes available to whoever knows what they need. The graph was always there — the entry point wasn't. Curious about Opportunity Skill. The piece I'd most want to understand: how do you handle freshness/staleness on observed-work impressions? Code annotations regenerate on PR diff, which is a discrete signal. Continuous work observation has no such discrete trigger, so I'd imagine you need a different mechanism for "this impression is current" vs "this is stale."

  9. Mike Czerwinskivia dev.to

    The recall math from Part 1 frames the accuracy ceiling at >99% per hop. Part 2's SLOs sit one layer under: they measure intra-graph connectivity (API chain >=95%, DB completeness >=80%, event fields >=70%, ambiguous = 0). Completeness, not correctness of the bridge itself. Worth a second pass for one shape the SAME_ENTITY normalizations could be hiding. Recall counted at the edge measures edges that exist. It does not catch edges that connect what they should not. A PascalCase normalization with 99% recall can also produce a 5% wrong-pair join rate if the normalization over-fits on local naming. The recall number is intact. The graph's downstream truth is not. The way to surface that error mode is an exogenous check the bridge author does not control: pull a sampled 50 SAME_ENTITY edges per quarter, hand to whoever actually owns the entities on each side (the API team, the page team, the task team), and have them mark mismatch. The cost is finite, the sample is small, and the SLO threshold becomes "% of sampled bridges confirmed correct by entity-owner >= 95%", distinct from "% of handlers reachable >= 95%". Same target, different layer of trust. The 30% non-engineer usage from this part is the more telling receipt for the system than any of the connectivity numbers. Stylists, ops, executives querying natural language through MCP and trusting the answer means refusal architecture matters as much as connectivity. The question I keep landing on for systems at this scale: what does the MCP return when the graph genuinely does not know? A confident wrong answer at 95% connectivity is more dangerous than a low-confidence answer at 70%. Curious whether SPG has explicit "I don't know" semantics, or whether it always returns the closest annotation it can find.

    1. Ryosuke Tsujivia dev.to

      Thanks for the careful read. The framing I'd push back on first: those SLOs aren't measuring bridge correctness — they're connectivity floors, set deliberately conservative. We'd rather under-recall than produce false-positive joins, so the 95/80/70 isn't a ceiling we're chasing; it's a "don't let this regress" floor we won't drop below. Each SAME_ENTITY normalization is tiered exact-match-first with eager gating on the looser fallbacks. Chasing higher recall would mean loosening those gates, and the regression alarm would trip. A confidently wrong neighbor is worse than a missing one — especially with non-engineers in the consumer loop, where the cost of a wrong-but-plausible answer changes character entirely. That said, the entity-owner sampling check you described is a layer we don't currently have, and the framing is clean: "% of sampled bridges confirmed by owner ≥ 95%" as a trust signal distinct from connectivity. Worth adding. On "what does MCP return when the graph genuinely doesn't know" — semantic search returns ranked similarity scores, so the agent sees signal it could reason about, but explicit refusal isn't first-class yet. A real gap. Coverage SLOs guarantee that high-confidence neighbors exist when they should, but they aren't refusal signals on their own.

    2. Mike Czerwinskivia dev.to

      Recasting the SLO as a floor rather than a ceiling clears most of the confusion I was carrying into the read. A floor on connectivity says "do not regress below this, because below this the consumer reasoning collapses." A ceiling on correctness says "this is the truth we will defend." Those are completely different commitments, and conflating them is what makes recall-at-95 sound like a quality claim when it is actually a non-regression contract. The owner-sampling check then sits cleanly orthogonal to the SLOs. SLOs guarantee neighbors exist; sampling tells you whether the ones that exist are the right ones. Reporting them separately keeps each one honest, because either can move while the other holds. Refusal as first-class output is the part I would push hardest on next. Ranked similarity scores are useful debugging signal, but until the consumer agent can receive an explicit "the graph does not know this" the silent failure mode is plausible-but-wrong, which is exactly the failure consumers cannot detect. The SLOs you have today can promise that high-confidence neighbors exist when they should; they cannot promise that absence is communicated. Those are different guarantees and a non-engineering consumer needs both.

    3. Ryosuke Tsujivia dev.to

      Recast nails it. Floor for non-regression, ceiling for truth, completely different commitments, and yes, "recall at 95" reads as a quality claim when the actual commitment is non-regression. The article didn't separate those carefully enough; that's on me. Owner-sampling as a separate report line is going on the roadmap. Treating SLOs (neighbors exist) and owner-confirmed correctness (they're the right ones) as independent gauges that can drift independently is the right framing. The cost of running ~50 sampled edges per relationship type quarterly is bounded, and it gives entity owners a recurring stake in the bridges that point at their domain. Refusal as first-class output is the part I have the least good answer for. Today the MCP server returns ranked similarity, which means the trust boundary sits on the consumer agent: it has to look at the scores and decide whether to back off. In practice, an LLM in the agent loop will happily turn a 0.62 cosine into a confident sentence. The shape of the fix is roughly: Server-side threshold gating. Below a per-relationship similarity floor, return an explicit no_match / empty result rather than a top-K with low scores. The trust boundary moves from agent to server. Tiered confidence in the response itself (high / low / none) rather than a single score the agent re-interprets. Removes the "is 0.7 good?" judgment from the consumer. Pair with the owner-sampling check so the thresholds stay calibrated. Otherwise the threshold becomes another opaque number we can't defend. The shape I keep landing on is that absence and uncertainty are different failure modes and want different signals: absence is just honest, uncertainty at least lets the consumer ask a follow-up, but plausible-but-wrong is invisible to a non-engineering consumer. That last one is where the server has to prevent the bad answer at source rather than rely on the agent to decline. Appreciate the read; the threads have sharpened the framing more than the original article did.