rustograph가 Rust 클라이언트 코드에서 isthmus http route-call 사실을 만드는 규칙과 근거를 적는다. 계약 정본은
isthmus GRAPH-EXCHANGE "HTTP 경계"와
HTTP-WRAPPERS(url-compose 규칙·래퍼 선언)다.
서버 선언 규칙은 HTTP-ROUTES.md에 있다.
rustograph routes --role client [--dir DIR] [--out FILE] [--wrappers http-wrappers.json] [--service NAME]- 문서: bridge-facts v1,
platform: "rust",target: "http",roles: ["client"],sourceSets.tests: "excluded", 선택service(--service). 사실이 0건이어도 target은http다(스캔했으나 호출 없음).dispatch는 없다. - 스캔: 모든 워크스페이스 멤버의 lib·bin 타깃,
impact와 같은 syn 수확(모듈 트리·AST·정점 집합).#[cfg(test)]모듈과#[test]함수는 뺀다. 컴파일·실행·네트워크 접근이 없다. symbol.usr: 호출을 감싸는 가장 안쪽 그래프 정점(함수·메서드·트레이트 기본 메서드, 상수·static 초기식) —impact·reach의 정점 ID와 같다. 중첩 함수·클로저 안의 호출은 바깥 정점이다. 정점이 없으면 usr 없이 내고missing-route-usrs:로 센다.verify-cli-contract.sh가impact --roots-from으로 모든 usr가 정점임을 확인한다.location: 호출식이 시작하는 줄(wrapper.location— 메서드 체인은 수신자 시작)과 UTF-8 바이트 열 + 1.- dynamic 사실은
channel: null이다 — 원문 식은 URL의 userinfo·query를 담을 수 있어 싣지 않는다. 증명한 리터럴 접두사만 마스킹한channelPrefix로 싣는다. authority: base가 리터럴이고 root 앵커일 때만 싣는 소문자host[:port](userinfo 제거, 포트는 적힌 대로).baseRef: base URL이 구조체 필드(app::api::ApiClient::base_url)나 상수·static(app::HOST)에서 오면 그 id다. 필드 id는 그래프 정점이 아니다 — workspace 매니페스트match.baseRefs에 쓰는 안정 이름이다.
| 라이브러리(확인 버전) | 인식 | URL 해석 | 근거 |
|---|---|---|---|
| reqwest 0.13.5(0.12.28도 오라클 실행) | reqwest::get·reqwest::blocking::get, Client·blocking::Client의 get·post·put·patch·delete·head·request(Method::X, url), Request::new(Method::X, url) |
문자열은 url::Url::parse(WHATWG), Url은 그대로 |
src/into_url.rs IntoUrlSealed for &str(Url::parse(self) 뒤 has_host() 검사), src/async_impl/client.rs execute_request(http·https 외 scheme 거부) |
| ureq 3.4.2 | ureq::get·post·put·delete·patch·head·options·trace, Agent의 같은 메서드 |
http::Uri — 점 세그먼트를 지우지 않는다 |
src/lib.rs pub fn get<T>(uri: T) where Uri: TryFrom<T>, 오라클 ureq_dots |
| ureq 2.x | 위와 같은 자유 함수·Agent, request(method, url)·request_url |
url::Url::parse(소스 기준, 실행 미확인 — 버전 한계를 낸다) |
— |
| url 2.5.8 | Url::parse(s)(reqwest 재수출 reqwest::Url 포함), url.join(p) 체인 |
WHATWG URL Standard | url 크레이트 실행 결과(아래) |
결합 방식 이름. isthmus 3a45450(HTTP-WRAPPERS "Go, Rust, Python 클라이언트")이 정했다. url::Url::join은
rfc3986이다. reqwest·ureq에는 base URL 설정이 없으므로 호출 식의 URL이 곧 요청 URL이고, 전체 URL 리터럴은
compose.strip, 문자열 조립은 보간 규칙을 쓴다. 조립한 문자열의 앞머리가 모르는 값(format!("{base}/x"))이면 계약의
"그 밖" 규칙대로 /로 시작하는 뒤 리터럴만 base 앵커 꼬리이고, 아니면 dynamic + ambiguous-base-join:이다.
| 쓰는 곳 | /x(미상 base) |
x(미상 base) |
base 리터럴 |
|---|---|---|---|
url::Url::join(rfc3986) |
root(점 세그먼트 제거) | base, ./는 지움, ..·빈 참조는 dynamic + ambiguous-base-join: |
WHATWG 상대 해석, root |
이은 문자열을 reqwest·ureq 2에 넘김(Url::parse) |
base | dynamic + ambiguous-base-join: |
전체 URL을 WHATWG로 해석(점 세그먼트 제거, // 보존), root |
이은 문자열을 ureq 3에 넘김(http::Uri) |
base | dynamic + ambiguous-base-join: |
전체 URL 그대로(점 세그먼트 보존), root |
코드 안의 이름은 Join::WhatwgJoin(=rfc3986), Join::WhatwgConcat, Join::HttpUriConcat이다. 벡터에는 뒤의 둘을
가리키는 결합 이름이 없다(전체 URL 규칙이라 필요 없다).
WHATWG 결합은 http(s)에서 RFC 3986과 같다. 다른 점은 셋이고 모두 구현했다: \를 /로 읽고, 같은 scheme의
http:x는 상대 참조이며, 앞뒤 C0·공백과 탭·줄바꿈을 지운다. url 2.5.8로 실행해 확인한 값(단위 테스트
whatwg_join_matches_url_crate가 고정): http://h/api + x → /x, http://h/api/ + x → /api/x,
http://h/a/b/c + ../x → /a/x, http://h/api + \x → /x, http://h/api/ + http:x → /api/x,
http://h/api + //other/x → host other의 /x, http://h/api + ?q=1 → /api.
문자열 연결의 base 미상 행은 dio 단순 연결과 결과가 같아 공유 벡터의 공통(producer) dio-concat 사례를
Join::WhatwgConcat으로 실행한다.
slash-join 사례는 벡터용 Join::SlashJoin으로 실행한다(그 방식을 쓰는 Rust 라이브러리는 모델링하지 않았다).
- 문자열: 리터럴,
format!/format_args!(위치·이름·인라인 캡처 인자,{{·}}),concat!,a + b,to_string·to_owned·into·as_str·clone·String::from같은 전달, 워크스페이스const·static·연관 상수 (Self::BASE)를 따라간다.{:?}처럼 서식 지정이 있으면 그 값은 모르는 값이고,{:>1$}처럼 인자 순서를 바꾸는 서식은 문자열 전체를 모른다. - 지역 변수: 스코프 스택(블록·클로저·
for·match팔·if let/while let·let-else)으로 그림자를 따르고, 함수 안에서 대입·&mut·push_str같은 수정 메서드가 닿는 이름은 믿지 않는다. 매개변수는 출처(Origin::Param)를 단 값이다. - query 꼬리(
compose.suffix): 끝에 붙은 지역 변수의if/match모든 가지가?리터럴로 시작하거나 빈 문자열 (String::new()포함)이면 떼고queryTailStripped를 단다.else없는if는 증명하지 못한다. - 구조체 필드 base: 크레이트의 모든 구조체 리터럴(
Self { .. }포함)이 그 필드를 같은 리터럴·상수 값으로 채우면 그 값이다(인스턴스와 무관하게self.f·x.f모두). 생성 위치가 매개변수로 채우거나, 값이 둘 이상이거나,..rest생략,#[derive(Default)], 필드 대입·&mut·수정 메서드가 있으면 값을 버리고 base 앵커 +baseRef다.Url필드도 같다(Url::parse(리터럴).unwrap()). - 수신자 판정: 식의 타입을 구문으로 추론한다 — 생성자(
Client::new()·Client::builder()…build()?·ureq::agent()·Agent::new_with_defaults()·AgentBuilder·config_builder()…new_agent()), 타입 주석이 있는 지역 변수·매개변수, 구조체 필드 타입, 워크스페이스 함수·메서드의 반환 타입,static의 선언 타입.&·Arc·Rc·Box·Lazy·LazyLock·OnceLock·Result는 벗긴다. 증명하지 못한 수신자에서 보낸 요청 모양 호출(x.get(u).send())은 사실로 내지 않고 센다. - 동사: 메서드 이름,
reqwest::Method::X·http::Method::X경로, ureqrequest("GET", ..)의 정확한 대문자 리터럴만 동사다. 그 밖은methodDynamic: true다. - base 없는 상대 URL(
client.get("/x"))과 http(s)가 아닌 scheme은 클라이언트가 보내기 전에 거부하므로 사실을 내지 않고 센다(오라클relative_url: 요청 없음). - 모델링하지 않는 클라이언트(hyper·hyper-util의 client 경로, surf, awc, isahc, attohttpc, minreq, curl, ehttp, gloo-net, reqwasm, http-client, reqwest-middleware)는 멤버가 직접 의존할 때 경로 사용 위치를 크레이트별로 센다.
"language": "rust" 항목만 적용한다(다른 언어 항목은 공백으로도 세지 않는다). 선언 오류는 종료 코드 2다.
function:owner::name이 rustograph 정점 ID다. 자유 함수는 모듈 경로(app::net+send), 메서드·연관 함수는 타입 ID(app::api::ApiClient+request), 트레이트 impl 메서드는app::api::ApiClient::<Trait>+ 이름.constructor:owner는 타입 ID.name이 타입 이름과 같으면 구조체 리터럴(Endpoint { verb, path }—label은 필드 이름)이나 튜플 구조체 생성(Ep(m, p)—index)이고, 다르면 연관 함수(Endpoint::new)다.- 인자: Rust에는 이름 붙은 인자가 없으므로 함수·메서드는
index(메서드는 수신자를 빼고 0부터, UFCS 호출도 같다)로 묶는다.label은 구조체 리터럴 필드에만 맞는다.methodEnum은 enum case·연관 상수 경로의 마지막 이름 (Verb::Post→Post)과 문자열 리터럴을 case로 본다. - 경로 인자: 전체 URL이면 절대 해석,
/로 시작하면 선언pathAnchor, 상대 경로는 래퍼 안 결합을 모르므로 dynamic (base 앵커면ambiguous-base-join:). 래퍼 경로에는baseRef를 싣지 않는다. - 선언된 래퍼 본문의 dynamic 호출은 내지 않는다(래퍼 호출 사실이 대신한다). 선언되지 않은 함수가 매개변수를 URL
전체·base 뒤 꼬리로 흘려보내거나 동사 매개변수를 넘기면
http-wrapper-undeclared:로 센다(세그먼트 일부를 채우는 매개변수는 래퍼 선언으로 풀리지 않아 세지 않는다). - 정점·구조체에 닿지 않는 선언과 호출 0건 선언은
http-wrapper-unresolved:다.--service와 다른service를 선언한 래퍼가 있으면 isthmus가 문서를 거부하므로 먼저 사용법 오류로 막는다.
| 접두사 | 뜻 |
|---|---|
route-call-coverage: |
모델링하지 않는 클라이언트 사용 수(크레이트별), 증명하지 못한 수신자의 요청 모양 호출 수, 보내기 전 거부되는 URL 수, 확인한 버전 밖(reqwest 0.12·0.13, ureq 3 밖)의 의존, 위치를 못 찾은 호출, cargo metadata 한계 |
ambiguous-base-join: |
미상 base 뒤 상대 경로 수 |
http-wrapper-undeclared: |
선언되지 않은 래퍼 싱크 수 |
http-wrapper-unresolved: |
정점에 닿지 않거나 호출이 0건인 rust 래퍼 선언(wrappers[n]) |
missing-route-usrs: |
usr가 없는 사실 수 |
호출 측 공백은 증명할 수 있는 요청 범위가 없어 limitationScopes를 내지 않는다(문서 전체 효과).
conformance/url-compose.json(isthmus 3a45450)의 producer 41건과 producer:rustograph 7건(base-join/rust-url-join-*·
rfc3986-unknown-*)을 tests/client_routes.rs가 제품 함수로 실행한다(compose::compose_path·join·UrlVal::parse·mask,
wrappers::bind_method) — 48/48 통과. wrapper.location은 실제 스캐너로 여러 줄 호출의 시작 줄과 UTF-8 열을 확인한다.
다른 생산자 전용 사례(producer:kartograph·gartograph·pythograph)는 적용하지 않는다. 오라클과 어긋난 벡터는 없다.
벡터를 받으며 고친 것: 미상 base의 rfc3986 상대 참조에 ..가 있거나 참조가 비면 dynamic + ambiguous-base-join:이다
(이전에는 ..가 알려진 세그먼트 안에서 끝나면 템플릿을 냈다).
tests/fixture-client/app/은 각 규칙을 부르는 합성 클라이언트이고, experiments/client-oracle/은 그 lib.rs를
crates.io의 진짜 reqwest 0.13.5·ureq 3.4.2·url 2.5.8로 컴파일해 시나리오를 실행한다. 모든 http 요청은 환경 프록시
(HTTP_PROXY)로 지정한 127.0.0.1 임시 포트의 기록 서버로 간다(ureq 3은 http 요청도 CONNECT 터널을 연다 — 서버가
터널을 수락하고 안의 평문 요청을 읽는다). 외부 네트워크 요청은 없고, 서버는 오라클 프로세스 안의 스레드라 함께
끝난다. 판정은 root는 전체 경로, base는 세그먼트 경계 꼬리, {}는 비어 있지 않은 세그먼트, authority는 요청 host이고,
정적 사실은 모두 어떤 요청과 맞아야 한다(정밀도). 재기록은 experiments/client-oracle/run.sh, 기본 테스트는 커밋된
기록(recorded/client.json)을 스텁 의존(tests/fixture-client/stubs, 이름·버전만)으로 해석한 사실과 오프라인 대조한다.
2026-09-30 기록: 41개 시나리오, 일치 37 · dynamic 3 · 요청 없음 1 · 불일치 0, 귀속되지 않은 사실 0. 같은 소스를 reqwest 0.12.28로 바꾼 scratch 실행도 같은 결과였다.
| 시나리오 | 기록 요청 | route-call 사실 | 결과 |
|---|---|---|---|
literal_query |
GET /v1/items | GET /v1/items root |
일치 |
async_user |
GET /v1/users/7 | GET /v1/users/{} root |
일치 |
concat_post |
POST /v1/orders | POST /v1/orders root |
일치 |
put_positional |
PUT /v1/items/5 | PUT /v1/items/{} root |
일치 |
delete_plus |
DELETE /v1/items/9 | DELETE /v1/items/{} root |
일치 |
patch_local |
PATCH /v1/profile | PATCH /v1/profile root |
일치 |
head_health |
HEAD /v1/health | HEAD /v1/health root |
일치 |
request_options |
OPTIONS /v1/items | OPTIONS /v1/items root |
일치 |
request_dynamic |
PATCH /v1/verbs | 동사 동적 /v1/verbs root |
일치 |
join_relative |
GET /v2/users/5 | GET /v2/users/{} root |
일치 |
join_replaces_last(RFC 3986 병합) |
GET /v2/tags | GET /v2/tags root |
일치 |
join_absolute_path |
GET /root/ping | GET /root/ping root |
일치 |
dot_segments |
GET /v1/b | GET /v1/b root |
일치 |
double_slash(// 보존) |
GET /v3//items | GET /v3//items root |
일치 |
query_tail |
GET /v1/search | GET /v1/search root |
일치 |
query_tail_empty |
GET /v1/search | GET /v1/search root |
일치 |
partial_segment |
GET /v1/files/report.json | GET dynamic, prefix /v1/files/ |
dynamic |
unknown_base_rooted |
GET /prefix/v1/status | GET /v1/status base |
일치 |
unknown_base_glued |
GET /prefixstatus | GET dynamic(ambiguous-base-join:) |
dynamic |
relative_url |
없음 — 보내기 전 실패 | 없음 | 요청 없음 |
masked_token |
GET /v1/tokens/a1b2c3d4e5f6a7b8c9d0 | GET /v1/tokens/{} root |
일치 |
non_ascii |
GET /v1/caf%C3%A9 | GET /v1/caf%C3%A9 root |
일치 |
userinfo_fragment |
GET /v1/secure | GET /v1/secure root |
일치 |
with_port |
GET /v1/port | GET /v1/port root(authority api.example.com:8080) |
일치 |
backslashes |
GET /v1/bs | GET /v1/bs root |
일치 |
builder_client |
GET /v1/built | GET /v1/built root |
일치 |
struct_field_base |
GET /api/items | GET /api/items root |
일치 |
struct_field_item |
GET /api/items/42 | GET /api/items/{} root |
일치 |
struct_field_plus |
POST /api/items | POST /api/items root |
일치 |
struct_field_query |
GET /api/search | GET /api/search root |
일치 |
struct_param_base |
GET /remote/users/3 | GET /users/{} base |
일치 |
url_field |
GET /cat/products | GET /cat/products root |
일치 |
ureq_get |
GET /v1/ureq/items | GET /v1/ureq/items root |
일치 |
ureq_delete |
DELETE /v1/ureq/items/4 | DELETE /v1/ureq/items/{} root |
일치 |
ureq_dots(점 세그먼트 보존) |
GET /v1/x/../ureq-dots | GET /v1/x/../ureq-dots root |
일치 |
ureq_agent_head |
HEAD /v1/ureq/health | HEAD /v1/ureq/health root |
일치 |
ureq_post |
POST /v1/ureq/orders | POST /v1/ureq/orders root |
일치 |
wrapper_function |
GET /wb/w/items | GET /w/items base |
일치 |
wrapper_method |
POST /api/w/orders | POST /w/orders base |
일치 |
wrapper_constructor |
DELETE /api/e/items/6 | DELETE /e/items/{} base(+ execute의 동사 동적 dynamic) |
일치 |
undeclared_sink |
GET /v1/undeclared | GET dynamic(http-wrapper-undeclared:) |
dynamic |
isthmus 3a45450(#133)부터 rust route-call을 받는다(그 전 main c395c59는 "Fact kind is not valid for platform"으로
거부했다). 무패치 3a45450 빌드로 fixture 문서와 axum08 서버 문서를 workspace trace에 넣어 확인했다(HANDOFF의 e2e 절).