Git の開発履歴を「迷路」として可視化する Electron アプリ兼 MCP サーバー。
コミット履歴には、完成したコードには残らないものが埋まっている——どこで巻き戻したか、 どのファイルを何度も直したか、どこで手が止まったか。DevMaze はそれを捨てずに取り出す。
迷路ビュー 暦ビュー 沼 / ホットスポット
体感時間の道のり 実時間の濃淡 詰まった箇所・荒れた場所
| 何を出すか | |
|---|---|
| 迷路グラフ | メインラインを横軸に固定し、ブランチを上下のスイムレーンに展開した力学グラフ |
| 暦ビュー | 同じ履歴を実時間で並べた面。迷路が対数で圧縮した時間を検算できる。曜日・時間帯の分布つき |
| 試行錯誤スコア | revert×4 + fix×2 + WIP×1 + merge×1 + branch×2 の集計値(0-∞、4段階) |
| 沼(Struggle) | どこで詰まったか を個別のエピソードとして抽出。根拠つき |
| ホットスポット | どこが荒れているか をファイル単位で risk 0-100 に |
| 開発フェーズ | コミット種別の推移から「機能開発期/バグ修正期」等の区間を推定 |
| マイルストーン | タグ・バージョン・大規模変更を迷路のゲートとして表示 |
| 働き方 | 時間帯・曜日の分布、夜間率、稼働日数、連続稼働と最長の空白、著者統計 |
| 検索 | ⌘F でメッセージ・ハッシュ・著者・変更ファイルを横断検索 |
| レポート出力 | ヘッダーの「レポート」から、開発過程を Markdown 1本にして保存 |
| MCP サーバー | 上記すべてを他のエージェントから引ける形で公開 |
GitHub の user/repo 表記を入れると bare clone して解析する(--filter=blob:none --depth=1000)。
ローカルリポジトリは chokidar で監視し、新しいコミットを検出するとバナーで知らせる。
リポジトリを開くと、リモートに新着が無いかを自分から見に行く(git ls-remote のみ。
取り込みはしない)。新着があれば上部にバーが出て、「取り込む」で取り込める。
5分ごとに再確認するので、開いたまま放置していても気づける。
再スキャン(↺)は、まずリモートから取り込んでから解析し直す。 手元の履歴だけを読み直しても、GitHub 側に積まれた新着は永久に反映されないため。 取り込めた件数はトースト(「新着 3 件を取り込みました」)で返す。認証やネットワークで 取り込めなかった場合も、その旨を出したうえで手元の履歴で解析を続ける。
npm install
# GUI(開発モード)
npm run dev
# 特定のリポジトリを開いた状態で起動
DEVMAZE_REPO=/path/to/repo npm run dev
# テスト
npm test
# 配布物のビルド(macOS)
npm run package:macnpm run build:mcp # dist-mcp/server.js を生成~/.claude.json などに登録する:
{
"mcpServers": {
"devmaze": {
"command": "node",
"args": ["/absolute/path/to/DevMaze/dist-mcp/server.js"]
}
}
}| ツール | 返すもの |
|---|---|
scan_repo |
統計・スコア・ノード数の概要 |
get_summary |
開発サマリー(Markdown) |
get_score |
試行錯誤スコアと内訳 |
get_struggles |
沼エピソード(format, min_severity, limit, kind で絞り込み) |
get_struggle_diff |
その沼で実際に書かれたコードの差分。get_struggles は「どこで何回詰まったか」まで、こちらは「何を試して何が効いたか」 |
get_hotspots |
荒れているファイル(risk 順) |
export_report |
サマリー+沼+ホットスポット+働き方を1本の Markdown に |
get_maze_graph |
ノードとエッジの JSON |
export_report は、開発過程を外部(ノート・記憶・レポート)に資産として取り込むための出力口。
get_struggle_diff は、そこから一段踏み込んで技術的な中身を渡す口。
解析層はこれまで --numstat(ファイル名と増減行数)しか読んでおらず、
「どのファイルを何行いじったか」は持っていても「何を書いたか」は捨てていた。
沼のコミット群の実際のパッチを、ノイズ(lock ファイル・dist/)を除いて返すので、
受け取った側が「何を試して何が効いたか」まで蒸留できる。
量の歯止めは既定で入っている(コミット12件・1ファイル200行・文脈1行・沼の関与ファイルのみ)。
削ったときは削ったことを、取れなかったときは理由を必ず返す ——
GitHub キャッシュは --filter=blob:limit=100k なので 100KB 以上のファイルは中身が手元に無い。
「差分が無い」と「差分が取れない」を取り違えないため。
沼はスコアと違い、個別のエピソードとして出る。判定はコミット履歴だけから決まり、 根拠(evidence)を数値で持つので、読んだ側が再判断できる。
| 種別 | 判定 |
|---|---|
revert_loop — やり直しの輪 |
revert とその前後で同じファイルに触れたコミットが3件以上 |
fix_chain — 修正の連鎖 |
fix 系コミットが24時間以内の間隔で3件以上連続 |
file_churn — 同じファイルの往復 |
同一ファイルを7日以内に4回以上変更、かつ fix/revert/WIP を含む |
wip_drift — WIP の漂流 |
WIP 系コミットが48時間以内の間隔で3件以上連続 |
stall_burst — 停滞のあとの再開 |
中央値の8倍かつ3日以上の空白の直後に、修正または大規模変更 |
各エピソードは escape(沼を抜けた印 = 同じファイルへの次の前進コミット)を持つ。
escape が無い沼は「まだ抜けていない可能性がある場所」として読める。
再発: 同じファイルで14日以上あけて繰り返し詰まっている場合、recurrence が付く。
一度きりの沼と、半年に3回同じ場所で溺れている沼は意味がまったく違う。
後者は「そのとき苦労した」ではなく「その場所に問題がある」。
夜間率: 各エピソードは nightRatio(22-5時に打たれたコミットの割合)を持つ。
詰まった時間帯が生活を侵食していたかどうかが残る。
迷路上では、沼が廊下に敷かれた帯として出る。期間が帯の長さ、深刻度が下端の線の太さ。
終端の形が「抜けたかどうか」 —— 閉じていれば抜けた、開いていればまだ抜けていない。
頭に種別の記号が付き、再発しているものは 3/4 のように何回目かが出る。
既定ではごく薄い地面にとどめ、選んだ1件だけ強く出す(30件を常時強調すると道全体が赤い壁になる)。
サイドバーの沼をクリックすると、そのエピソードのコミットだけが浮かび上がり、
他は沈む。コミット詳細パネルには、所属する沼と変更ファイル(ホットスポットの risk つき)が出る。
- なぎ払いコミット(26ファイル以上を一度に変更)は、初回インポートや一括整形なので ファイル単位の判定から外す。どのファイルにも等しく現れて全体を汚すため。
- 自動生成物(
package-lock.json、dist/、node_modules/等)は除外する。 - リリースコミットは版番号の更新でファイルに触るだけなので往復に数えない。
- 同じファイルの往復は、荒れ(fix/revert/WIP)が2割以上あることを求める。 件数で1件以上とすると、毎コミット追記される台帳やログが「30回書き直した沼」に化ける。
- コミット分類は件名を主・本文を従として行う。本文の箇条書き("- fix ...")に 引きずられると、初回リリースが「バグ修正」に化けて全体が狂う。
shallow clone やオブジェクト欠損のリポジトリでは git log --numstat が途中で失敗する。
このときファイル差分がまったく取れず、沼が0件に見えてしまう。
git log --all --numstat は最初に見つからないオブジェクトで止まるので、
blob が1つ欠けるだけで残り全部の差分を道連れにする。
例外から部分出力を拾う手も持っているが当てにならない ——
simple-git はこの失敗で stdOut を持たず、実測(912コミット)では救出できずに
取得率が0%になっていた。そこで分割して取り直す: 200件ずつ → 失敗したら半分に割る →
1件まで割って駄目ならその1件だけ諦める。実測で 0% → 100%(893/894)。
そのうえで stats.fileStatsCoverage(0-1)として取得率を持ち、
50%を下回るときはレポートとサイドバーに警告を出す。
0件が「沼が無い」なのか「見えていない」なのかを取り違えないため。
risk = 100 × (0.35·log正規化した変更回数 + 0.45·平滑化した修正率 + 0.12·著者数 + 0.08·直近性)
- 変更回数は対数で正規化する。台帳やログのように1ファイルだけ極端に多い場合、 線形正規化では他のファイルが全部潰れるため。
- 修正率はリポジトリ全体の修正比率を事前分布として平滑化する。 「2回変更して2回とも修正 = 100%」を最悪値として扱わないため。
コミットが200件を超えると、自動で作業のまとまり単位に集約する。 900件を1件ずつ描いても点が900個あるだけで全体は読めない。 連続して書いた一続き(間隔2時間以内・同じレーン)を1つの塊として、 件数を大きさに、支配的な種別を色に、タグや版番号をラベルにして並べる。
まとまりをクリックすると、その中のコミットに降りる(他は沈む)。 下部の「まとまり / コミット」で手動でも切り替えられる。
行の長さは固定せず、いちばん大きく映せる折り返し方を選ぶ。 画面幅に合わせて1行=1画面にすると行数が増え、全体表示の縮尺が縦で頭打ちになる (実測: 9行で48% → 折り返しを選び直して6行69%)。
| 操作 | 動き |
|---|---|
| スクロール | 拡大・縮小 |
| Shift + スクロール | 横に移動 |
| ドラッグ | 移動(パン) |
| 左下の − / + / ⤢ | 縮小 / 拡大 / 全体表示 |
| ミニマップをクリック | その位置へ飛ぶ |
- 時間軸を画面幅で折り返して蛇行させる(偶数行は左→右、奇数行は右→左)。 一直線の履歴を一直線に描くと画面の縦がまるごと余り、横に細長い帯にしかならない。 折り返すと画面を使い切れて、道として読める。行の変わり目は短い縦移動になる。
- 各行の端にその行が始まった日付と、その期間の開発フェーズが出る。
- 1件ごとの前進はごく小さく、時間が近いコミットは重なる。 重なりを衝突判定が押し広げるので、集中して書いた時期が「塊」として見える。 長い空白は間隔として残るので、塊と塊のあいだが休止期間になる。
- 破線の縦棒は3日以上コミットが無かった期間(拡大すると日数が出る)。
- 縦のレーンはブランチ。行の中心がメインライン。
- 薄い赤の地面は沼。選ぶとその1件だけ帯が立つ(終端が閉じていれば抜けた)。
- 種別は色4種+形で持つ。11種を色だけで持つと色覚多様性で26組が同色に潰れるため、 色は物語に効く4種(機能追加・通常・リバート・バグ修正)の輝度ラダーに絞り、 マージ=◇ / リリース=⬡ / WIP=破線の円 / 周辺作業=□+1文字(C・D・T・R)にした。
- 右下のミニマップは全体のどこを見ているかを示す。クリックでその位置へ飛ぶ。
- 左下にズーム操作(− / 倍率 / + / 全体表示)。
- ミニマップは全体が収まっているあいだ出ない。 収まっているときは何も足さないうえ、 最終行の点に重なるだけなので。拡大して画面から溢れたときだけ現れる。
shared/analyzer/ Electron と MCP で共用する解析層(純粋関数)
git.ts git log の取得と解析(分類・numstat・欠損時の分割取得)
diff.ts コミットの実際のパッチ(get_struggle_diff の中身)
graph.ts レーン割り当て・ゾーン・マイルストーン
score.ts 試行錯誤スコアとサマリー
struggle.ts 沼の検出(時間の軸)
hotspot.ts ホットスポットの検出(場所の軸)
report.ts Markdown レポートの組み立て(アプリと MCP で共用)
activity.ts 働き方(時間帯・稼働日・著者)
paths.ts ノイズファイル・なぎ払いコミットの判定
electron/ メインプロセス、IPC、キャッシュ、GitHub clone、ファイル監視
src/ React レンダラー(Header / Sidebar / MazeGraph / CalendarView / NodeDetail / SearchPanel)
shared/theme.ts コミット種別の色・形・ラベルの唯一の出どころ(Electron と MCP の両方から読む)
mcp-server/ MCP stdio サーバー
tests/ vitest(解析層の単体テスト)
解析結果は ~/Library/Application Support/devmaze/repo-cache/ にキャッシュされ、
HEAD が変わったときだけ再スキャンする。
Electron 33 / React 18 / Vite 5 / D3.js v7 / simple-git / @modelcontextprotocol/sdk / vitest