Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .rustograph.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ components:
harvest: ["rustograph::harvest/**"]
graph: ["rustograph::graph/**"]
analysis: ["rustograph::analysis/**"]
traversal: ["rustograph::traversal/**"]
rules: ["rustograph::rules/**"]
export: ["rustograph::export/**"]
sarif: ["rustograph::sarif/**"]
Expand All @@ -27,14 +28,16 @@ components:
deps_report: ["rustograph::deps/**"]
deps:
app: [cli]
cli: [source, analysis, rules, export, sarif, config, graph, mcp, cli_args, deps_report]
cli: [source, analysis, traversal, rules, export, sarif, config, graph, mcp, cli_args, deps_report]
mcp: [cli_args, analysis, rules, export, config, graph, deps_report]
cli_args: [source, export, graph]
source: [graph, harvest, modtree, cargo_meta, sem, cfgeval]
# sem은 ra_ap 경계 — source만 호출하고, syn 경계·도메인 위층은 모른다.
sem: [graph, cargo_meta]
harvest: [graph, modtree]
analysis: [graph]
# 다중 root 순회는 문서만 소비한다 — 수확·입출력을 모른다.
traversal: [graph]
rules: [graph, config]
export: [graph]
sarif: [graph, rules]
Expand All @@ -49,7 +52,7 @@ deps:
config: []
deny:
# 도메인은 수확 경계와 CLI를 절대 모른다 — 순수성이 테스트 가능성이다.
graph: [cli, source, harvest, modtree, cargo_meta, analysis, rules, export, sarif, config, app, sem]
graph: [cli, source, harvest, modtree, cargo_meta, analysis, traversal, rules, export, sarif, config, app, sem]
# cli는 cargo/syn/ra_ap 경계를 직접 만지지 않는다 — source를 통한다.
cli: [cargo_meta, harvest, modtree, sem]
# mcp도 마찬가지 — 수확 경계는 source 뒤에만 있다.
Expand Down
27 changes: 27 additions & 0 deletions HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,33 @@

세션을 이어받는 에이전트가 먼저 읽는 문서입니다.

## 진행 중 — feature/schema-usr (2026-09-30, API 영향 프로그램 Phase 7c)

isthmus `trace`가 Rust 백엔드의 핸들러 도달을 관계 사용과 잇도록 두 가지를 더했다.

- `schema` 사실에 `symbol: {qualifiedName, usr}` — usr는 `impact`와 같은 정점 ID.
같은 syn 수확을 돌려 정점별 소스 범위(`harvest::OwnerSpan`)를 받고, 사실 바이트
오프셋을 감싸는 가장 안쪽 정점을 고른다(정점 집합으로 검증). 구조체 어트리뷰트·
필드 컬럼은 구조체, 메서드 밖 연관 상수는 impl self 타입. 최상위 `table!`·orphan
파일은 usr 없이 `missing-relation-usrs:`로 센다.
- `reach`·`impact --format language-traversal` — language-traversal v1, 다중 root
단일 패스(`src/traversal.rs`, pythograph 알고리즘 포팅), tentative 간선 = candidate
root별 하한, `dispatch`·`unresolvedCalls` 미신고, 사용법 오류·root-not-found 64.
- 자기 분석이 이름 충돌 거짓 순환 둘(지역명 `owner_spans`, 매개변수 `facts`)과
cli ↔ cli::reach 모듈 순환을 잡아 고쳤다 — 새 식별자는 같은 이름의 fn과 겹치지 않게.
- 남은 것: isthmus가 rust `route-decl`을 받지 않아 route 선택 trace는 불가(relation·
심볼 선택은 동작). `type` 레벨 cycles는 main부터 10개(투영 산물)이고 이번에 3개가
같은 종류로 늘었다(게이트 아님).
- 합성 e2e(axum·sqlx·sea_orm 스텁, scratch): `isthmus check --pairs` 5 matches,
relation 선택 trace는 relation-use → 역방향 순회까지 이어지고 route-decl이 없어
`non-http-entry`. isthmus를 scratch에서 고쳐 손으로 만든 route-decl로 route 선택도
핸들러 → 테이블 도달을 확인했다.
- 발견(기존 수확 결함, 이번 범위 밖): 본문의 단일 식별자 경로가 지역 변수여도 크레이트
루트 모듈 이름과 같으면 모듈 정점 references 간선이 된다(`fn f(repo: &dyn R) { repo.all() }`
→ `f → crate::repo`). 순회가 그 모듈의 `uses` 간선을 따라 모듈이 import한 아이템 전부에
닿는 과대 근사가 된다. 표현식 경로는 모듈일 수 없으므로 모듈로 해석되면 버리는 것이 후보.
syn 모드에서 메서드 호출은 이름 팬아웃(tentative)이라 구체 수신자 호출도 candidate다.

## 직전 세션 요약 (2026-09-25)

**목표.** HANDOFF의 보류 항목을 정리하고 main에 쌓인 기능을 배포한다.
Expand Down
41 changes: 41 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ rustograph dead --exclude-tests # #[cfg(test)] 서브트리 제외
rustograph graph --target x86_64-pc-windows-msvc # cfg(트리플) 평가
rustograph mcp # MCP stdio 서버 — 에이전트가 되묻는 통로
rustograph schema --dir . --out schema-facts.json # isthmus persistence 사실
rustograph reach mycrate::api::list_users # isthmus language-traversal(정방향)
rustograph impact --format language-traversal --roots-from schema-facts.json # 역방향
```

`#[cfg]` 조건은 메타데이터로 그래프에 실립니다 — 정점의 `cfg`는 자기
Expand All @@ -90,6 +92,9 @@ rustograph schema --dir . --out schema-facts.json # isthmus persistence 사실
조용히 새 수확으로 돌아갑니다. `--no-cache`로 끌 수 있습니다.

종료 코드: `0` 정상 · `1` strict 위반/발견 · `2` 사용법/분석 오류.
순회 명령(`reach`, `impact --format language-traversal`)은 isthmus 계열
계약을 따릅니다 — 사용법 오류는 표준 출력을 비운 채 `64`, 그래프 정점이
아닌 root가 섞이면 문서를 쓴 뒤 `64`입니다.

## persistence 사실 — `schema`

Expand All @@ -112,13 +117,49 @@ rustograph schema --dir . --out schema-facts.json # isthmus persistence 사실
`users::columns::name` — 워크스페이스에 선언된 `table!` 이름과
맞물릴 때만 정적으로 인정하고, 안 맞는 같은 모양 경로는 `dynamic`

사실마다 `symbol: {qualifiedName, usr}`을 싣고 `usr`는 감싸는 그래프
정점 ID입니다 — `impact`/`reach`와 같은 ID라 isthmus `trace`가 핸들러
도달과 관계 사용을 잇습니다. fn·impl 메서드(`Type::method`, 트레이트 impl은
`Type::<Trait>::method`)·트레이트 기본 메서드·const/static 초기화식은 그
정점, 구조체 어트리뷰트·필드 컬럼은 구조체, 메서드 밖 연관 상수는 impl의
self 타입입니다. 감싸는 정점이 없는 사실(최상위 `table!` 호출, 모듈 트리
밖 파일)은 `symbol` 없이 내고 `missing-relation-usrs:`로 셉니다. ID는
`impact`와 같은 syn 수확에서 받아 오고(다시 유도하지 않음) 그래프 정점
집합으로 확인합니다.

비한정 이름(`query!`, `sql_query`, `table!`)은 그 파일이 sqlx/diesel에서
import할 때만 인정합니다. 파싱 실패 파일·문법이 다른 `table!`·테이블
바인딩 없는
컬럼 어트리뷰트는 조용히 넘기지 않고 `limitations`로 셉니다. 이름 기반
스캔은 추측하지 않습니다 — 정적으로 해석할 수 없는 것은 지어내지 않고
센 것입니다.

## 순회 문서 — `reach` / `impact --format language-traversal`

isthmus [`language-traversal` v1](https://github.com/ictechgy/isthmus/blob/main/docs/LANGUAGE-TRAVERSAL.md)
문서를 냅니다. `reach`는 root가 기대는 쪽(`dependencies`), `impact`는 root에
기대는 쪽(`dependents`)입니다.

- root는 위치 인자 다음 `--roots-from`(JSON 문자열 배열 또는 bridge-facts
문서의 `symbol.usr`, `-`는 표준 입력) 순서로, 처음 나온 자리에만 남깁니다.
빈 id·제어 문자 id·10,000개 초과는 사용법 오류(64, 표준 출력 비움)입니다.
- 모든 root를 한 번에 훑어 정점마다 닿는 root 목록(64개까지 + `rootsTruncated`),
가장 가까운 깊이, 최단 경로 목격(`via`)을 싣습니다. root별 BFS 오라클과
무작위 그래프로 대조합니다.
- 근거 등급: `tentative` 간선(이름 팬아웃·`dyn`/제네릭 트레이트 impl 후보)은
`candidate`, 나머지는 `direct`이고 정점마다 root별 하한을 싣습니다.
미해석 호출 수가 완전하다고 말할 수 없어 `dispatch`·`unresolvedCalls`는
싣지 않습니다.
- `--max-depth` 1~128(기본 128, `--depth 0`은 128), `--max-reached` 1~100,000.
- `project`는 `schema`와 같은 realpath, `revision`은 `--revision` 또는 작업
트리가 깨끗할 때의 git HEAD, `graphRevision`은 그래프 JSON의 SHA-256입니다.
- 그래프 정점이 아닌 root는 `symbol` 없이 싣고 `root-not-found:` limitation을
더한 문서를 쓴 뒤 64로 끝납니다.

isthmus는 `platform: "rust"` 문서의 `route-decl`을 받지 않아 Rust 핸들러에서
시작하는 route 선택 `trace`는 아직 불가능합니다. relation·심볼 선택(역방향
순회)은 지금 동작합니다.

## 개발

```bash
Expand Down
67 changes: 66 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,17 @@ rustograph mcp --graph .rustograph/graph.json

# Emit bridge-facts for isthmus' persistence join (SQL relation uses)
rustograph schema --dir . --out schema-facts.json

# isthmus language-traversal v1 for `isthmus trace` (many roots, one pass)
rustograph reach mycrate::api::list_users mycrate::api::create_user
rustograph impact --format language-traversal --roots-from schema-facts.json
```

Exit codes: `0` ok · `1` strict violation/finding · `2` usage or analysis
error.
error. The traversal commands (`reach`, `impact --format
language-traversal`) follow the isthmus family contract instead: `64` for a
usage error (stdout stays empty) or when some roots are not graph vertices
(the document is still written).

## Graph model

Expand Down Expand Up @@ -199,13 +206,71 @@ Extracted references:
`users::columns::name` — matched against the workspace's declared
`table!` names; same-shaped paths that match nothing stay `dynamic`

Each fact carries `symbol: {qualifiedName, usr}` where `usr` is the id of
the enclosing graph vertex — the same id `impact`/`reach` use, so isthmus
`trace` can join handler reach to relation uses:

- `fn`, impl method (`crate::m::Type::method`, trait impls
`crate::m::Type::<Trait>::method`), trait default method, and
`const`/`static` initializer → that vertex
- struct-level facts (`table_name` attributes, field columns) → the struct
- impl-level facts outside any method (associated consts) → the impl's
self type
- no enclosing vertex (top-level `table!` invocations, files outside the
module tree) → no `symbol`; counted as `missing-relation-usrs:`

The ids come from the same syn harvest `impact` uses (not re-derived), and
every attached `usr` is checked against the graph's vertex set.

Unqualified names (`query!`, `sql_query`, `table!`) count only when the
file imports them from `sqlx`/`diesel`. Unparseable files, off-grammar
`table!` bodies,
and column attributes without a table binding surface as `limitations`,
not silence. The name-based scan never guesses: what cannot be resolved
statically is counted, not invented.

## Traversal documents — `reach` / `impact --format language-traversal`

```bash
rustograph reach ID... [--roots-from FILE|-] [--max-depth N] [--max-reached N]
[--revision REV] [--generated-at TIMESTAMP] [-- ID...]
rustograph impact --format language-traversal ID... (same options)
```

Both write an isthmus
[`language-traversal` v1](https://github.com/ictechgy/isthmus/blob/main/docs/LANGUAGE-TRAVERSAL.md)
document: `reach` the symbols the roots depend on (`dependencies`),
`impact` the symbols that depend on them (`dependents`). Ids are the same
strings as `symbol.usr` in `schema`.

- **Roots**: positional ids, then `--roots-from` (a JSON array of strings or
a bridge-facts document — its facts' `symbol.usr`; `-` reads stdin),
deduplicated in first-seen order (that order is the meaning of
`reached[].roots`). Empty or control-character ids and more than 10,000
roots are usage errors (`64`, empty stdout).
- **One pass over all roots**: every reached symbol lists every root that
reaches it (`roots`, first 64 plus `rootsTruncated`), the nearest depth,
and a shortest-path witness (`via`). A root reached from another root is
listed without its own index. Checked against a per-root BFS oracle on
random graphs.
- **Evidence**: `tentative` edges (name fan-out, `dyn`/generic trait impl
candidates) are `candidate`, all other edges `direct`; each symbol carries
the per-root lower bound. `dispatch` and `unresolvedCalls` are not emitted
— rustograph cannot claim its unresolved-call counts are complete.
- **Limits**: `--max-depth` 1–128 (default 128; `--depth 0` means 128),
`--max-reached` 1–100,000; cuts set `truncated` with `depth` /
`max-reached`.
- **Identity**: `project` is the same realpath `schema` writes; `revision`
is `--revision` or git `HEAD` when the work tree is clean; `graphRevision`
is the SHA-256 of the graph JSON (with `root` normalized to the project).
- **root-not-found**: ids that are not graph vertices are listed without
`symbol`, a `root-not-found:` limitation is added, and the command exits
`64` after writing the document.

isthmus rejects `route-decl` facts from `platform: "rust"` documents, so
Rust handlers cannot yet start a route-selection `trace`; relation and
symbol selections (reverse traversal) work today.

## Agent output contract

`query`/`impact` JSON reports `level`, `depth`, `truncated`, all edge kinds
Expand Down
30 changes: 30 additions & 0 deletions scripts/verify-cli-contract.sh
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,36 @@ if [ "$got" -ne 0 ] || ! grep -q '"target": "persistence"' "$FIX/facts.json" 2>/
fails=$((fails+1))
fi

# reach·impact --format language-traversal — isthmus 순회 문서. 계열 계약대로
# 사용법 오류는 표준 출력을 비운 채 64, 정점이 아닌 root가 섞이면 문서를
# 쓰고 64다. schema 사실의 usr는 전부 root로 받아져야 한다(0).
check 0 "reach" reach fixture_app::main
check 0 "impact traversal" impact --format language-traversal fixture_core::Used fixture_core::entry
check 64 "reach root-not-found" reach fixture_app::main no_such::root
check 64 "reach no roots" reach
check 64 "reach control char" reach "$(printf 'a\007b')"
check 64 "reach bad depth" reach fixture_app::main --max-depth 129
check 64 "reach bad flag" reach fixture_app::main --max 3
check 64 "impact trav no roots" impact --format language-traversal
trav_out="$("$BIN" reach --dir "$FIX/fixture" 2>/dev/null)" || true
if [ -n "$trav_out" ]; then
echo "FAIL reach usage error: stdout must be empty" >&2
fails=$((fails+1))
fi
# pipefail이라 64를 파이프로 넘기면 grep 결과와 무관하게 실패한다 — 먼저 담는다.
rnf_out="$("$BIN" reach --dir "$FIX/fixture" fixture_app::main no_such::root 2>/dev/null)" || true
echo "$rnf_out" | grep -q '"root-not-found"' || {
echo "FAIL reach root-not-found: document not written" >&2
fails=$((fails+1))
}
got=0
"$BIN" impact --format language-traversal --dir "$FIX/fixture-schema" \
--roots-from "$FIX/facts.json" >/dev/null 2>&1 || got=$?
if [ "$got" -ne 0 ]; then
echo "FAIL schema usr roots: expected 0 (every usr is a vertex), got $got" >&2
fails=$((fails+1))
fi

# --dir를 붙이지 않는 검사 — check()는 항상 fixture dir을 뒤에 붙이므로
# 나쁜 --dir 검증은 마지막 인자가 이기는(last-wins) 구조상 여기서 따로 한다.
for c in "graph --dir /nonexistent-xyz" "schema --dir /nonexistent-xyz"; do
Expand Down
54 changes: 51 additions & 3 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
//! 종료 코드: 0 정상, 1 strict 위반 발견, 2 사용법/분석 오류.
//! 플래그 파서는 외부 크레이트 없이 직접 만든다 — 명령이 적고 계약이 단순해서다.

mod reach;

use crate::cli_args::{self, Args};
use crate::{analysis, config, deps, export, mcp, rules, sarif, source};
use crate::{analysis, config, deps, export, mcp, rules, sarif, source, traversal};
use std::io::Write;
use std::path::{Path, PathBuf};

Expand All @@ -24,6 +26,9 @@ usage:
rustograph deps [--strict] [--format text|json]
rustograph query ID [--depth N] [--max N]
rustograph impact ID [--depth N] [--max N]
rustograph reach ID... [--roots-from FILE|-] [--max-depth N] [--max-reached N]
[--revision REV] [--generated-at TIMESTAMP]
rustograph impact --format language-traversal ID... (same options as reach)
rustograph paths FROM TO [--max N] [--budget N]
rustograph search QUERY [--max N]
rustograph mcp [--dir DIR] [--graph FILE] [--config FILE] [--deps] [--tests]
Expand All @@ -36,7 +41,12 @@ shared flags: --deps --tests --retain-public --semantic --no-cache
semantics (requires a build with `--features semantic`; results cached
in .rustograph/semantic-cache.json unless --no-cache)

exit codes: 0 ok · 1 strict violation found · 2 usage/analysis error";
reach / impact --format language-traversal emit an isthmus language-traversal
v1 document (dependencies / dependents) for many roots in one pass; ids that
are not graph vertices are listed without symbol and the command exits 64.

exit codes: 0 ok · 1 strict violation found · 2 usage/analysis error
(traversal output: 64 usage error with empty stdout, or root-not-found)";

/// 명령을 실행하고 종료 코드를 돌려준다.
/// os::exit 대신 반환값을 쓰는 것은 종료 코드 계약을 테스트하기 위함이다.
Expand All @@ -51,7 +61,15 @@ pub fn run(args: &[String], stdout: &mut dyn Write, stderr: &mut dyn Write) -> i
}

fn run_inner(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> Result<i32, String> {
let a = cli_args::parse(args).map_err(|e| format!("{e}\n{USAGE}"))?;
let a = match cli_args::parse(args) {
Ok(a) => a,
// 순회 명령은 계열 계약대로 사용법 오류가 64다 — 전역 파서 실패도 마찬가지.
Err(e) if reach::is_traversal_argv(args) => {
let _ = writeln!(err, "error: {e}");
return Ok(reach::EXIT_USAGE);
}
Err(e) => return Err(format!("{e}\n{USAGE}")),
};
match a.cmd.as_str() {
"version" => {
writeln!(out, "rustograph {VERSION}").ok();
Expand All @@ -63,7 +81,11 @@ fn run_inner(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> Resul
"rules" => cmd_rules(&a, out),
"deps" => cmd_deps(&a, out),
"query" => cmd_query(&a, out, false),
"impact" if a.get("format") == Some("language-traversal") => {
cmd_traversal(&a, traversal::Direction::Dependents, out, err)
}
"impact" => cmd_query(&a, out, true),
"reach" => cmd_traversal(&a, traversal::Direction::Dependencies, out, err),
"paths" => cmd_paths(&a, out),
"search" => cmd_search(&a, out),
"schema" => cmd_schema(&a, out),
Expand All @@ -76,6 +98,32 @@ fn run_inner(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> Resul
}
}

/// `reach`·`impact --format language-traversal` — 순회 문서를 낸다.
/// 사용법 오류는 표준 출력을 비운 채 64, 수확 실패는 2다.
fn cmd_traversal(
a: &Args,
direction: traversal::Direction,
out: &mut dyn Write,
err: &mut dyn Write,
) -> Result<i32, String> {
match reach::cmd(a, VERSION, direction, &mut std::io::stdin().lock(), out) {
Ok(code) => {
if code == reach::EXIT_USAGE {
let _ = writeln!(
err,
"error: some roots are not rustograph graph vertices (root-not-found); the document lists them without symbol"
);
}
Ok(code)
}
Err(reach::Failure::Usage(m)) => {
let _ = writeln!(err, "error: {m}");
Ok(reach::EXIT_USAGE)
}
Err(reach::Failure::Analysis(m)) => Err(m),
}
}

/// `schema` — isthmus persistence 도메인의 bridge-facts 문서를 낸다.
/// 그래프 문서가 아니라 교환 문서라 export 직렬화만 재사용한다.
fn cmd_schema(a: &Args, out: &mut dyn Write) -> Result<i32, String> {
Expand Down
Loading
Loading