Show HN: 개발자와 AI 에이전트를 위한 결정론적 아키텍처 그래프, Enola
요약
Enola는 소스 코드를 분석하여 모듈, 함수, API 라우트 등 모든 구성 요소와 그들 간의 의존성을 그래프 형태로 기록하는 도구입니다. 이 로컬 바이너리는 코드 기반의 시스템 아키텍처를 파악하고, 변경 사항에 따른 영향을 예측하며, 코딩 에이전트에게 정확한 구조 정보를 제공합니다.
핵심 포인트
- 코드 전체의 의존성 관계를 그래프로 시각화/분석 가능
- 변경 전 영향도를 분석하여 깨질 부분을 미리 파악할 수 있음
- 코딩 에이전트가 시스템 구조를 이해하고 작업하도록 지원
- 여러 리포지토리에 걸친 통합적인 아키텍처 관리가 가능함
enola - 코드에서 구축된 전체 소프트웨어 시스템의 하나의 그래프를 여러분의 머신에서
웹사이트 · 문서 · CLI 레퍼런스 · 벤치마크 · 릴리스 · 문제 보고하기
enola는 여러분의 소스 코드를 읽어 그 안에 무엇이 있는지 기록합니다: 모듈, 함수, API 라우트, 데이터베이스 테이블, 그리고 그것이 건드리는 메시지 토픽들. 그런 다음 이 모든 조각들이 서로 어떻게 연결되는지를 하나의 리포지토리 안에서, 여러 리포지토리에 걸쳐서도 기록합니다. 그 기록이 바로 그래프입니다: 사물들의 목록과 그 사이의 링크 목록이죠. 여러분은 이 그래프에 질문을 하거나, 코딩 에이전트에게 넘겨주거나, 자신만의 도구를 만들 수 있습니다.
로컬에서 단일 바이너리로 실행됩니다. AI 모델도, 언어 서버도, 계정도, 업로드도 필요 없습니다.
그래프는 여러분의 코드 파싱을 통해 얻어집니다. 같은 코드는 항상 같은 그래프를 생성합니다.
무엇이 깨질지 알아내세요. 함수, 라우트 또는 모듈을 수정하기 전에, 그 그래프에 무엇이 의존하는지 물어보세요. 답변은 추측이 아닌 코드 속 실제 위치 목록입니다. 빠른 시작.
아키텍처를 선언한 대로 유지하세요. 코드가 따라야 할 레이어 순서나 어떤 서비스가 어떤 서비스를 호출할 수 있는지 기록합니다. 이후 모든 변경 사항은 이 기준에 맞춰 평가되며, 오직 변경으로 인해 깨진 부분만 보고됩니다. 변경 사항 등급 매기기.
코딩 에이전트에게 시스템 구조를 제공하세요. MCP 위에서 에이전트는 자신이 건드리려는 코드에 무엇이 의존하는지 그래프에 묻고, 후크(hook)가 편집을 완료하면 그 편집을 평가합니다. 에이전트 연결하기.
여러 리포지토리를 하나의 시스템으로 보세요. 프런트엔드가 /api/orders를 호출하는 것이 해당 서비스를 제공하는 Go 핸들러와 연결되어 있습니다. 한 서비스의 Kafka 프로듀서가 토픽을 소비하는 서비스와 연결됩니다. 여러 리포지토리에 걸쳐서도요.
그래프 위에서 자신만의 도구를 만드세요. 스냅샷은 문서화된 형식을 가진 일반 파일들의 집합입니다. 필요한 곳 어디든 로드할 수 있습니다. 그래프를 기반으로 구축하세요.
| 제가 원하는 것은… | 여기서 시작하기 |
|---|---|
| 제가 가지고 있는 리포지토리에서 enola가 무엇을 찾는지 보기 | 빠른 시작 |
| ... |
이 명령어는 enola를 ~/.local/bin에 설치합니다.
같은 바이너리는 PyPI(pip install enola-cli)와 RubyGems에도 있습니다. 모든 설치 경로 및 업그레이드 방법은 docs/CLI.md에 나와 있습니다.
2. 체크아웃한 리포지토리를 대상으로 지정합니다.
enola --explain /path/to/your/repo
설정 파일도, 계정도 필요 없고, 디스크에 아무것도 쓰지 않습니다. 코드를 읽고 발견한 것을 출력해 줍니다. 당장 분석할 것이 없다면, enola 자체를 대상으로 실행해 보세요:
git clone https://github.com/enola-labs/enola
enola --explain enola
출력되는 내용 중 일부(코드가 변경됨에 따라 숫자가 움직입니다):
Overview
Languages: go, typescript, c, ruby, python
Total facts: 10814
...
읽는 방법:
- fact는 enola가 기록한 하나의 사실을 의미합니다. 예: "이 함수가 존재함", "이 함수가 저 함수를 호출함", "이 라우트가 여기에 서비스됨".
Total facts는 발견된 사실의 총 개수입니다. - cyclic dependency는 서로 필요로 하는 두 개 이상의 모듈을 의미하며, 따라서 어느 것도 단독으로 변경하거나 테스트할 수 없습니다.
- layer violation은 리포지토리가 선언한 레이어 순서 하에서, 특정 모듈이 사용해서는 안 되는 레이어로 접근하는 것을 말합니다. enola는
enola-intent.yaml에 자체적으로 이를 선언합니다.
; 0은 코드상 아무것도 이 경계를 가로지르지 않음을 의미합니다.
- hotspot은 많은 다른 모듈이 의존하는 모듈입니다.
fan-in은 몇 개의 모듈이 이것을 사용하는지를 나타내고,fan-out은 이것이 몇 개의 모듈을 사용하는지를 나타냅니다.blast radius는 여기에 변경이 생겼을 때 영향을 받을 수 있는 모듈의 개수입니다. 첫 번째 행을 읽어보면:internal/facts는 253곳에서 사용되며, 여기에 대한 변경은 96개 모듈에 영향을 미칠 수 있습니다.
3. 그래프를 유지하고 확인합니다. --explain만 출력할 뿐입니다. 그래프를 저장하려면 --generate로 빌드해야 합니다. 이는 리포지토리 내의 .enola/ 폴더 아래에 작성되며, 이 저장된 그래프가 로컬 대시보드에서 보여주는 내용입니다:
enola --generate .
enola dashboard --open
대시보드는 로컬 웹 페이지이며, 아무것도 외부로 전송되지 않습니다. [Dashboard guide](링크 없음).
enola는 각 소스 파일을 구문 분석하여 발견한 내용을 타입이 지정된 사실(typed facts)로 변환하고, 이 사실들을 그래프로 연결하며, 다음과 같은 검사들을 그래프 위에서 실행합니다: 의존성 사이클(dependency cycles), 레이어 위반(layer violations), 사용되지 않은 라우트(unused routes), 핫스팟(hotspots) 등. 각 검사는 **설명기(explainer)**라고 불리며, docs/EXPLAINERS.md에 모든 설명기가 기술되어 있습니다.
| 명령어 | 기능 | 더 알아보기 |
|---|---|---|
enola --explain <repo> | 저장소를 읽고 보고서를 출력합니다. 아무것도 기록하지 않습니다. | CLI 레퍼런스 |
enola --generate <repo> | 그래프를 구축하고 스냅샷으로 .enola/에 저장합니다. | 그래프 |
enola baseline pin | 현재 그래프 상태를 비교 지점으로 기록합니다. | 변경 사항 게이팅(Gating a change) |
enola check | 그래프를 다시 구축하고, 핀 이후 변경된 내용만 보고합니다. | 변경 사항 게이팅(Gating a change) |
enola coverage <cluster> | 여러 저장소의 경우, 어떤 크로스-저장소 호출이 연결되었고 어떤 것이 연결되지 않았는지 보여줍니다. | 클러스터 |
enola dashboard --open | 저장된 그래프를 로컬 웹 페이지에 표시합니다. | 대시보드 가이드 |
enola install --hooks | 코딩 에이전트에게 enola의 존재를 알리고, 각 세션을 평가하게 합니다. | 에이전트 연결(Connect your agent) |
이 모든 것에 대해 세 가지 속성이 적용됩니다:
결정론적(Deterministic). 동일한 코드는 항상 동일한 그래프를 생성합니다. 91개의 오픈 소스 저장소를 각각 세 번씩 인덱싱(콜드 1회, 웜 2회)한 결과, 810만 개가 넘는 사실에서 바이트 단위로 동일한 결과를 얻었습니다. 모든 스냅샷은 **영수증(receipt)**을 지니고 있습니다: 정확히 어떻게 구축되었는지에 대한 기록입니다. enola는 같은 방식으로 구축되지 않은 두 개의 스냅샷을 비교하는 것을 거부합니다.모든 커밋에 충분히 빠름(Fast enough for every commit). 변경되지 않은 트리를 재인덱싱하는 데 grafana의 경우 4.8초, Linux 커널의 경우 41.5초가 걸렸습니다.로컬(Local). 로컬 파일을 읽는 단일 바이너리입니다. 모델도 없고, 임베딩도 없고, 업로드도 없습니다.
그래프가 포함하는 내용과 디스크에 기록하는 내용은 docs/GRAPH.md를 참고하세요. 위의 수치와 이를 생성하는 스크립트는 docs/BENCHMARKS.md에 있습니다. 내부 구조는 ARCHITECTURE.md에 있습니다.
무엇이 변경되었는지 가장 간단하게 확인하는 방법은 풀(pull)과 함께 들어온 것을 확인하는 것입니다. 현재 상태를 기록하고, pull을 수행한 다음 비교해 보세요:
enola baseline pin
git pull
enola check
enola check
그래프를 다시 빌드하고, pin 이후로 변경된 내용(새로운 의존성, 새로운 호출, 새로운 발견 사항)만 보고합니다. 저장소에 이미 존재하던 문제는 보고서에서 제외됩니다. 사용자의 직접적인 수정도 동일하게 작동합니다: pin, 변경, check.
여기 storage에 새로운 헬퍼가 추가되었습니다.
이는 delivery 레이어를 가져옵니다. 컴파일되고 모든 테스트를 통과하며, 저장소에서 선언한 레이어 순서를 위반하는 경우 다음과 같이 작동합니다:
FAIL — 1 structural regression introduced.
Regressions (fail):
- [layers] 1.00 — Layer violation: storage -> delivery
...
**회귀(regression)**란 변경 사항으로 인해 pin 시점보다 나빠진 것을 의미합니다. 일부 검사는 시스템이 어떻게 보여야 하는지에 대해 점수를 매기는데, 예를 들어 레이어 순서나 어떤 서비스가 어떤 서비스를 호출할 수 있는지 등입니다. 사용자는 이를 docs/INTENT.md와 docs/CONSTRAINTS.md에 작성합니다.
기본적으로 아무것도 실패하지 않습니다. 예컨대 enola check --fail-on=layers를 통해 무엇이 실패해야 하는지 선택할 수 있습니다.
docs/GATING.md는 어떤 것이 빌드를 실패시킬 수 있고 그 이유가 무엇인지 설명하며; docs/HISTORY.md는 아키텍처가 시간이 지남에 따라 어떻게 변경되었는지 다룹니다. 전체 루프는 모듈이 작아 1분 안에 읽을 수 있습니다: docs/FIRST-CHANGE.md.
동일한 검사가 enola-action과 함께 모든 pull request에서 실행됩니다. 정확한 기본 커밋(base commit)을 해결하고, 러너(runner)에서 양쪽 모두에 점수를 매기며, 발견 사항을 도입한 라인에 주석을 달고, 아키텍처 델타를 작업 요약(job summary)에 작성합니다:
- uses: actions/checkout@v7
with:
fetch-depth: 0
...
쉘에서 enola check와 동일한 설명기와 종료 코드를 사용하며, 게시하거나 복원할 기본선(baseline)이 없습니다. 준비된 워크플로우 파일은 examples/ci/에 있습니다.
먼저 에이전트들에게 enola가 존재한다는 것을 알려주고, 각 세션마다 점수를 매기도록 합니다:
enola install --hooks
그런 다음 MCP(Model Context Protocol: 코딩 에이전트가 외부 도구를 호출하는 표준 방식)를 통해 그래프를 에이전트에게 제공합니다:
| 클라이언트 | 수행할 작업 |
|---|---|
| Claude Code | claude mcp add enola enola |
| Codex | codex mcp add enola -- enola |
| Copilot (VS Code) | <details><code>code --add-mcp '{"name":"enola","command":"enola"}'</code></details> |
| Cursor | .cursor/mcp.json 또는 모든 프로젝트의 경우 ~/.cursor/mcp.json에 아래 블록 추가 |
| opencode | 아무것도 안 함, enola install이 이미 등록했음 |
| Pi | 아무것도 안 함, enola install이 도구를 제공하는 확장 프로그램을 작성함 (Pi에는 MCP 클라이언트가 없음); Pi에서 프로젝트를 신뢰하거나 --global을 사용하세요 |
| 다른 모든 MCP 클라이언트 | 해당 MCP 설정에 아래 블록 추가 |
{ "mcpServers": { "enola": { "command": "enola" } } }
에이전트에게 변경된 내용은 다음과 같습니다: 편집하기 전에, 에이전트는 자신이 건드리려는 코드와 무엇이 의존하는지 그래프에 물어봅니다. 텍스트 검색을 통해 이를 조합해내는 것이 아니라 말이죠. 편집 후에는 훅(hook) (에이전트가 자신의 턴 끝에서 자동으로 실행하는 명령어)이 위와 동일한 enola check를 실행합니다. 에이전트는 자신이 실제로 무엇을 변경했는지 확인하고, 완료했다고 사용자에게 알리기 전에 회귀(regression)를 수정합니다.
enola install
변경하는 모든 파일을 미리 보여주고 먼저 물어봅니다; enola uninstall
모든 것을 되돌립니다. enola doctor
훅이 실제로 작동하는지 알려줍니다. Copilot의 다른 설정 키를 포함한 클라이언트별 세부 정보는 docs/CLI.md를 참조하세요.
시스템은 거의 하나의 저장소에 머무르지 않기 때문에 enola도 그렇지 않습니다. 백엔드와 이를 호출하는 것들(웹 앱, 모바일 앱, 다른 서비스)을 제공하면 이들을 하나의 그래프로 연결합니다. 그러면 다음과 같은 질문에 답할 수 있습니다: 만약 내가 이 엔드포인트를 변경한다면, 무엇이 고장 날까?
어려운 점은 두 측면이 엔드포인트를 항상 같은 방식으로 명명하지 않는다는 것입니다. examples/cross-repo/에서는 웹 서비스가 /api/v2/orders/{id}를 호출하지만, API 서비스는 그 문자열을 어디에도 작성하지 않습니다:
v2 := r.PathPrefix("/api/v2").Subrouter()
registerOrders(v2) // main()에서
r.HandleFunc("/orders/{id}", getOrder) // registerOrders()의 다른 함수에서
enola는 전달받은 함수로 접두사(prefix)를 따라가고, 실제 응답하는 주소 아래에 라우트를 파일링하여 호출이 연결되도록 합니다. 이는 Express, FastAPI, Axum, Rails, Swift 등에서도 동일하게 작동하며, 이 경우 접두사는 종종 완전히 다른 파일에 위치합니다.
만약 무언가를 연결할 수 없을 때는 추측하는 대신 그렇게 알려줍니다:
$ enola coverage cluster.yaml
service classification detected resolved unresolved
api isolated 0 0 0
...
unresolved 호출은 런타임에 URL을 구축하기 때문에 일치시킬 것이 없습니다. 이 구분이 중요합니다: 연결이 없는 서비스와 enola가 추적하는 데 실패한 연결을 가진 서비스는 절대 같아 보여서는 안 됩니다. 예제는 하나의 명령어로 실행됩니다: ./run.sh
여러 리포지토리가 설명되고 일치되는 방법은 docs/CLUSTERS.md를 참조하세요.
**스냅샷(snapshot)**이란 문서화된 형식을 가진 일반 파일들의 집합입니다. 여기에는 사실들, 그들 간의 관계, 발견 사항, 그리고 그것이 정확히 어떻게 구축되었는지를 기록한 영수증이 포함됩니다. enola를 서브프로세스로 실행하여 필요한 곳 어디에서든 이 파일들을 로드할 수 있습니다.
Cognee는 코드 그래프 검색을 이런 방식으로 구축합니다: enola-cli 릴리스 버전을 고정하고 스냅샷 파일을 로드하며, 이 검색은 LLM 키가 필요 없습니다. 바이너리를 어떻게 찾고 무엇을 작성하는지에 대한 내용은 docs/INTEGRATING.md를 참조하세요. 만약 Cognee의 일부로 enola를 받았다면, docs/COGNEE.md는 바이너리가 어디에 있고 어떻게 직접 사용하는지 보여줍니다.
docs/GRAPH.md는 그래프가 무엇을 포함하고 어떤 파일이 안정적인 계약(stable contract)인지 설명하며, docs/INTEGRATING.md는 단계별로 로드하는 방법을 보여줍니다.
20개가 넘는 언어와 형식이 자동으로 감지됩니다. 두 가지 언어를 가진 리포지토리는 알려주지 않아도 두 개의 언어로 인덱싱됩니다.
애플리케이션 코드: Go, Java, Kotlin, Scala, JavaScript, TypeScript, Vue, Svelte, Ember, Angular, Python, Ruby, PHP, Swift, Dart/Flutter, Rust, C/C++, .NET (C#, VB.NET, F#)
API 및 메시징: OpenAPI, gRPC, GraphQL, AsyncAPI
인프라스트럭처: Terraform/HCL, Ansible
이 언어 위에서 라우트(routes), 스토리지(storage), 와이어링(wiring)을 형성하는 프레임워크들, 예를 들어 Rails, Django, FastAPI, Spring, Express, Next.js, ASP.NET Core, Laravel, Axum, SwiftUI, Jetpack Compose 등을 이해합니다. 전체 표는 docs/LANGUAGES.md에 있습니다. 여기에 없는 언어라면 보고할 가치가 있는 격차입니다.
각 예제는 run.sh를 포함하는 작은 저장소로, README에 표시된 출력을 재현합니다.
- 다섯 개의 패키지에 대한 게이트: 레이어 순서로 빌드되는 모듈과 그것을 깨뜨리는 하나의 변경 사항. 이것이 이 README의 출력값이 나오는 테스트 환경(fixture)입니다.
- 두 개의 저장소, 하나의 그래프: 웹 서비스와 그 웹 서비스가 호출하는 API이며, enola가 연결할 수 있는 호출 하나와 해결되지 않은 것으로 보고하는 호출 하나를 포함합니다.
- 코드로 구현된 정책(Policy as code): 작은 Go 모듈에 적용되는 PCI DSS 및 GDPR에서 영감을 받은 제약 조건과, 이를 위반하는 세 가지 변경 사항입니다.
- 사용자 정의 HTTP 클라이언트: enola에게 사내 클라이언트를 가르쳐서 그 호출이 서버 라우트에 연결되도록 합니다.
- 복사할 준비가 된 CI 워크플로우와 프리 커밋 훅(pre-commit hook).
- Go, TypeScript, Python, Ruby, PHP, Kotlin, Swift, C++에 대한 언어별 설정 파일.
AI 자동 생성 콘텐츠
본 콘텐츠는 HN Claude Code Search의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기