A code-to-column change-impact knowledge graph for AI coding agents.
| English | 한국어 |
이 엔진을 붙들고 있는 생각을 모았습니다. 하나하나가 태도가 아니라 코드 안의 기제이므로, 그 생각을 구현한 파일 이름을 옆에 적어 둡니다.
그래프의 모든 엣지는 다섯 등급 중 하나를 답니다. 이 다섯 등급은 격자입니다. 요점은 그 격자를 거슬러 올라가는 일이 없다는 것입니다.
| 등급 | 뜻 | 예 |
|---|---|---|
EXACT |
구문과 심벌과 상수로 유일하게 증명됨 | 핸들러가 매퍼 메서드를 직접 호출하는 경우 |
SOUND_SET |
진짜 대상이 반드시 그 안에 있는 보수적 후보 집합 | 인터페이스 디스패치 뒤의 구현체들 |
HEURISTIC |
프로젝트 관행으로 보아 그럴듯함. 복구된 부분 바인딩 | 네이밍 전략 선언 없이 필드 이름에서 유도한 컬럼 이름. 코드 생성기의 이름 규칙만으로 라우트에 짝지은 핸들러 |
RUNTIME_ONLY |
정적으로 결정 불가. 런타임 증거가 필요함 | 외부 설정에서 고르는 빈이나 URL |
UNRESOLVED |
분석이 실패했거나 지원하지 않는 형태 | 파싱 실패, 없는 의존성 |
원소가 하나인 후보 집합도 절대 EXACT 로 승격되지 않습니다. 좁히는 것은
증명하는 것이 아닙니다. 격자는 src/core/policy.mjs 딱 한 곳에서 계산되고,
워커들은 어떤 종류의 호출인지, 바인딩 상태가 어떤지 같은 증거만 내보낼 뿐
등급을 내보내지 않습니다. 속성 테스트가 어떤 증거 조합도 격자 위로 분류되지
않음을 확인하고, 두 번째 테스트가 전체성을 확인합니다. 표가 모르는 증거 모양은
침묵이 아니라 POLICY_GAP 진단과 함께 보수적인 HEURISTIC 으로 떨어집니다.
프레임워크가 대신 하는 호출도 EXACT 일 수 있습니다. 호출 엣지는 거의 다
후보 집합입니다. 컴파일러 없이 수신자를 읽어야 하니까요. 그런데 몇몇은 애초에
“좁혀서 푼 호출” 이 아닙니다. Spring 은 컨트롤러의 @ModelAttribute 메서드를 그
컨트롤러의 핸들러마다 먼저 실행합니다. fetch 가 EAGER 인 JPA 연관은 그 연관을
가진 행과 함께 읽힙니다. 여기서는 좁힌 것이 없으니 승격한 것도 없습니다.
애너테이션과 프레임워크 자신의 규약이 사실을 진술하고, 엣지는 그 규약이
진술하는 만큼만 말합니다. 물론 오는 길에 더 약한 것이 하나라도 있으면 다른 엣지와 똑같이 등급이
내려갑니다.
걷기의 등급은 가장 약한 고리를 따릅니다. SOUND_SET 호출을 하나라도 지나는
체인은, 나머지가 아무리 exact 여도 SOUND_SET 입니다.
연결이 얼마나 확실한지는 규칙 네 개로 정합니다. 리뷰를 할 때마다 방금 고친 곳에서 모양만 조금 다른 같은 종류의 결함이 나왔습니다. 그래서 규칙을 사례가 아니라 종류에 맞춰 적었고, 모든 레인이 이 규칙을 지킵니다.
CREATE TABLE 이 아니다
같은 판단입니다. 이런 판단은 코드가 분명히 알아보는 모양에서만 성립합니다. 다른
모양은 정해지지 않은 것으로 두고, 이유를 엣지나 진단에 적습니다.SOUND_SET 은 닫힌 집합입니다. 엔진이 읽지 못한 무언가가 후보를 하나라도
더할 수 있으면 SOUND_SET 이 아닙니다. 끝까지 읽지 못한 providers 목록, 읽을 수
없는 모듈 클래스, 배포될 수 있는 패키지, 따라갈 수 없는 extends, 람다가 구현할
수 있는 인터페이스가 그런 빈틈입니다. 빈틈이 있으면 집합은 HEURISTIC 이 되고,
그 빈틈을 이름으로 말합니다.HEURISTIC 입니다. 트리가 말하지 않아서 엔진이
프레임워크의 기본값을 쓰면, 결론은 어떤 기본값을 가정했는지 말합니다. JPA 의 key
id 와 discriminator dtype, Spring Boot 의 포트 8080(url.guess: "port-default")이
그렇습니다. 데이터베이스를 선언하지 않아서 ALTER 를 MySQL 규칙으로 읽을 때도
같은 방식으로 말합니다(alter_rule_assumed, overview 의 catalog-rules-assumed
빈 곳에 셉니다). 소스가 말하지 않는 것은 프로필로 사람이 적을 수 있습니다(9절).고칠 때 틀린 결론을 감추려고 등급만 내리는 일은 없습니다. 소스가 말하지 않는 것을 말하지 않게 하거나, 무엇을 못 읽었는지 말하게 합니다.
라우트에서 핸들러로 가는 연결도 고리 하나입니다. HANDLES 엣지는 대부분
EXACT 지만 전부는 아닙니다. 룰이 코드 생성기의 이름 규칙만 보고 라우트에 짝지은
핸들러는 HEURISTIC 입니다. 엔진이 읽지 못한 exclude 가 주소를 바꿀 수 있는
NestJS 라우트도 HEURISTIC 입니다. 인터페이스 타입 필드를 거쳐 핸들러에 닿는
Spring 함수형 라우트는, 그 객체가 될 수 있는 클래스를 트리가 다 보여 줄 때
SOUND_SET 입니다. 다 보여 주지 못하면 HEURISTIC 입니다. 추상 메서드가 하나뿐인
인터페이스라 어떤 람다든 구현할 수 있을 때, 또는 이번 실행이 읽지 못한 타입이나
상위 클래스가 있을 때가 그렇습니다. OpenAPI 문서나 코드 생성기의 이름 규칙으로
놓인 연결은 HEURISTIC 입니다. 단, 그 문서가 코드와 맞는다고 프로필이 말하면
다릅니다(9절). 그래서 라우트에서 걷기는 모드가 그
연결을 받아들일 때만 핸들러에서 시작합니다. 깊이는 핸들러부터 세고, 그 아래의
행·테이블·페이지는 어느 것도 그 연결보다 높은 등급을 받지 않습니다
(src/core/walks.mjs). pack 전체 집계(overview, map, coupling, summary)와
flow 그림이 이 연결을 똑같이 읽습니다. 그래서 라우트가 얼마나 깊이 닿는지,
얼마나 확실한지를 두 쪽이 같게 말합니다. 그림은 어떤 연결로 시작했는지
적습니다(entry.link: 종류, 등급, 룰). 모드가 받아들이는 핸들러가 하나도 없으면
그림은 라우트에서 멈추고, 어떤 등급 때문에 멈췄는지 말합니다. 거꾸로 위로 걸을
때는, 연결이 모드 하한 아래인 라우트를 그 모드의 엔드포인트 행으로 내놓지
않습니다. 대신 모드가 걷지 않은 다른 연결과 함께 walk.cut.byMode 에 셉니다.
map 은 라우트에 그 모드가 실제로 걷는 핸들러 이름을 붙입니다.
라우트의 주소와 핸들러는 다른 문제입니다. 레인이 라우트 주소를 확정하지 못하면
(엔진이 읽지 못하는 exclude 목록이 있는 NestJS 전역 접두사), 그 사실을 HANDLES
증거에 적습니다(address: {grade, why}). 라우트 아래 코드는 HANDLES 연결로
등급을 받습니다. 위로 걸을 때 라우트를 부르는 쪽은 라우트로 가는 자기 연결로
등급을 받습니다. 웹 레인은 그런 라우트에 맞춘 호출을 주소 등급보다 높게 매기지
않고, 호출에도 이유를 적습니다(evidence.address). 그래서 어느 걷기든 의심을 제
자리에서 읽습니다.
문장이 닿는 곳은 어디서나 같은 규칙으로 셉니다. 문장의 테이블과 컬럼은 그 문장의
EXECUTES, READS, WRITES 엣지 가운데 모드 하한이 받아들이는 것입니다
(src/core/graph.mjs 의 sqlEdgesOf). flow 가 그렇게 그리고, 모든 집계도 그렇게
셉니다. overview, map, summary, coupling, browse, 화면 집계의 컬럼, transactions 가
모두 같습니다. 그래서 규칙이 가정하기만 한 이름의 테이블이나 컬럼(@Table 이 없고
네이밍 전략도 선언하지 않은 엔티티)은 conservative 에서 닿지 않습니다.
mode=heuristic 에서는 닿고, 전략을 선언하면 EXACT 가 됩니다.
걷기의 깊이 규칙은 하나입니다. 걷기는 그래프가 이어지는 데까지 갑니다. 집계,
map, coupling, summary, browse, 위아래 두 방향의 flow, impact 도구 모두 따로
요청하지 않으면 hop 상한(몇 단계까지 따라갈지 정한 한도)이 없습니다
(src/core/graph.mjs 의 DEFAULT_WALK_DEPTH 가 null). 도구마다 멈추는 깊이가
다르면 같은 질문에 답이 둘이 되기 때문입니다. 실제로 mall 의 pms_product.name 은
9 hop 을 가야 화면에 닿습니다. 그때 8 에서 끊던 뷰어의 걷기는 그 화면을 하나도
못 보여 줬고, screen_impact 는 12 개를 보여 줬습니다. 깊이 대신 노드 상한이
안전장치입니다. 노드를 4000 개(WALK_NODE_CAP) 기록하면 걷기를 멈춥니다. 그
걷기로 만든 답은 멈췄다고 말합니다. flow, endpoint_impact, screen_impact,
browse, summary, map, coupling 은 limits 에 적고, overview 는
node-cap 빈 곳으로 적습니다.
depth 를 직접 주면(1~8) 그만큼 좁힐 뿐이고, 답은 거기서 무엇을 잘랐는지
말합니다. impact 도구와 browse 는 depth 를 받지 않습니다. 답은 깊이를
depth 4 나 no depth cap 으로 적습니다.
한 질문, 한 답. 컬럼 하나를 바꾸면 어느 라우트와 화면에 닿는가. 이 질문을 위로
걷는 Trace, endpoint_impact 와 screen_impact, 작업 트리 오버레이의 위쪽 라우트,
다른 프로젝트로 건너가기가 모두 묻습니다. 이들은 모두 위로 걷는 걷기 하나를
읽습니다(src/core/chain.mjs 의 walkUp, src/core/walks.mjs 의 affectedBy 를
거쳐서). 너비 우선 걷기도 같고, Trace 의 엔드포인트 레인과 화면 레인을 만드는 기록
두 개도 같습니다. pack 전체 집계는 같은 규칙을 뒤집어 아래로 걷습니다.
src/core/walk_agreement.mjs 가 모든 컬럼과 테이블에서 답을 행 단위로 비교합니다.
impact 도구와 Trace 를, impact 도구와 집계를 맞춰 봅니다. 테스트 묶음은 이 비교를
손으로 만든 그래프, 골든 픽스처 트리 세 개(모든 모드), 그리고 체크아웃이 있을 때
spring-petclinic, jpetstore-6, mall pack 에서 돌립니다. 이전에는 spring-petclinic
컬럼 24 개 중 17 개, jpetstore-6 컬럼 86 개 중 49 개에서 한 답이 말한 화면을 다른
답은 말하지 않았습니다. 지금은 Java 코퍼스 pack 26 개와 TypeScript pack 4 개 어디에도
그런 컬럼이 없습니다.
질의 모드가 하한을 고릅니다. strict 는 확인된 엣지만 쓰고, conservative 는
후보 호출을 더하고, heuristic 은 추측 규칙까지 허용합니다. conservative 에서
아무것도 나오지 않다가 heuristic 에서 무언가 나오는 질문은, 그 코드에 대해
진짜 정보를 하나 알려 준 것입니다.
유효한 응답이 만들어지는 곳은 src/mcp/contract.mjs 하나뿐입니다. 필드에는
비공개 Symbol 로 도장이 찍히므로, 모양만 맞는 객체를 손으로 만들어 넘기면
직렬화 전에 assertContract() 가 거부합니다. 계약을 어긴 서버 응답은 200 이
아니라 500 contract-violation 입니다.
basis 는 답을 매어 두는 닻입니다. 프로젝트, pack 다이제스트, 만들어진
시각, 그리고 current, behind, provisional-overlay, unknown 중 하나의
최신성 판정입니다. unknown 은 진짜 답이며 절대 current 로 읽히지 않습니다.
pack 다이제스트가 없는 basis 는 아예 거부됩니다. 매어 둘 것이 없는 답은 답이
아니기 때문입니다.
trust 는 계산된 수준이며 리터럴이 아닙니다. 이름은 넷입니다. 그
문자열을 적을 수 있는 파일은 src/core/trust.mjs 하나뿐입니다. 테스트가 트리를 뒤져 그
문자열이 다른 데서 다시 나오면 빌드를 깨뜨립니다.
| 수준 | 말하는 것 | 말하지 않는 것 |
|---|---|---|
UNCERTIFIED |
점수를 매긴 것이 없음. 승인된 golden set 이 없거나 게이트가 GREEN/BOOTSTRAP 이 아님 | 이 프로젝트의 답에 대해 그 무엇도 |
GOLDEN_FAIL |
어떤 관계가 틀렸음. 이 사실이 어떤 통과보다도 위 | 나머지가 얼마나 멀쩡한지 |
RUNTIME_PASS |
실패가 없고, endpoint->tables 가 통과했고, 통과한 관계 중 적어도 하나는 실행 기록만으로 라벨이 붙었음. 즉 실제로 돈 것을 답이 덮었음 |
정밀도. 실행 기록에는 음성 사례가 없어서 보일 수 없습니다. 그리고 아무도 밟지 않은 라우트에 대해서는 아무것도 |
GOLDEN_PASS |
모든 관계가 통과. 각각 사람이 라벨을 붙였거나 전수로 측정됨 | 코퍼스가 덮지 않는 부분에 대해 엔진이 옳다는 것 |
계산은 순서대로 비관적입니다. 보정 상태가 아예 없으면 UNCERTIFIED, 게이트가
GREEN 이나 BOOTSTRAP 이 아니면 UNCERTIFIED, 승인된 골든 사례가 없으면
UNCERTIFIED 입니다. 이 모듈이 직접 밝히는 함의도 함께 읽어야 합니다. 흠 없는
30 개 사례의 Wilson 하한은 0.8865 이므로 N=30 에서는 어떤 95% 목표도 보일 수
없습니다. 30 개는 코퍼스에 점수를 매기기 시작할 수 있는 지점이지 충분해지는
지점이 아닙니다.
이 하한에는 예외가 하나 있습니다. 인심이 아니라 산수 때문입니다. 어떤 관계의 승인된
사례가 그 관계에 대해 pack 이 가진 모든 입력을 덮으면(요약의
exhaustive: true, population: n) 전수로 채점합니다. 전부 맞으면 PASS,
하나 틀리면 FAIL 입니다. Wilson 하한은 표본으로 그 표본이 나온 모집단을
추정하는 값입니다. 그런데 전수 조사에는 애초에 추정할 모집단이 남아 있지
않습니다. spring-petclinic 은 엔드포인트가 17 개뿐입니다. 하한만 있었다면 그
endpoint->tables 줄은 영영 보일 수 없었을 것입니다.
덮었다는 것은 채점했다는 뜻입니다. PASS 나 FAIL 로 돌아온 사례만 모집단에
계산되므로, UNSCORABLE 이 하나라도 있으면 그 입력이 다시 빠지고 관계는 도로
표본이 됩니다. 구멍 뚫린 전수 조사는 표본입니다. 모집단 자체는 pack 에서
읽습니다(relationPopulations). 모든 컬럼, 모든 엔드포인트, 모든 statement,
그리고 statement 를 바인딩하는 모든 심벌 — MyBatis 매퍼 메서드와 Spring Data
리포지터리 바인더 둘 다입니다.
채점할 수 없는 관계는 수준을 끌어내리지 않습니다. 대신 trust.gatesNotShown
에 이름이 적힙니다. “이것은 측정했고 통과했다” 와 “이것들은 측정하지 않았다” 는
서로 다른 사실입니다. 앞의 것을 강등하면 둘 다 잃기 때문입니다.
trust.knownGaps 는 축 선언(column-axis-degraded, code-axis-not-shipped
같은 것)도 함께 실어 나르고, 보정 게이트가 왜 이 답을 보증하지 않는지도
말합니다. no-calibration-state, calibration-gate-red, 그리고
calibration-gate-other-build 는 답하는 pack 이 아닌 다른 빌드를 판정한 통과
판정입니다(판정은 자기가 판정한 pack 의 다이제스트를 기록합니다). 다이제스트를
기록하기 전에 쓰인 통과 판정은 다음 analyze 까지 calibration-gate-unbound
를 붙인 채 유효합니다.
limits 는 엔진이 보지 못한 것을 문장으로, 각각 범위를 붙여 적은
것입니다. 물린 깊이 제한, 추측하기를 거부한 ${} 치환, 아예 만들어지지 않은
축 같은 것입니다.
truncated 는 목록마다 몇 개를 보여 주었고 전체가 몇 개이며, 어떤 순서로,
어디서부터 이어 받을지를 말합니다. 잘린 목록이 스스로를 완전하다고 말하는
사고를 막으려고 있는 필드입니다.
빈 것에도 뜻이 있습니다. not-shipped(축이 아예 만들어지지 않음),
degraded(만들어졌지만 필요한 것 없이), none(찾아봤고 없었음). 이 셋은 서로
다른 답이며, 0 results 가 “안전하다”로 읽히는 일은 없습니다.
| 인증된 베이스 | 작업 트리 오버레이 | |
|---|---|---|
| 무엇을 설명하는가 | 특정 커밋의 blob | 지금 디스크에 있는 바이트 |
| 결정성 | 바이트 단위로 재현 가능 | 명시적으로 아님. 모든 다이제스트 바깥 |
| 비용 | 초에서 분 | statement 900 개 프로젝트에서 약 0.2 초 |
| 기록되는가 | 예, pack 으로 | 아무것도. pack 도 팩트 캐시도 아님 |
| 어떻게 읽히는가 | current 또는 behind |
provisional-overlay |
베이스는 pack 입니다. 같은 커밋, 같은 카탈로그 스냅샷, 같은 엔진과 프로파일이면
어느 기계에서든 같은 다이제스트가 나옵니다. 빌드 시각, 호스트명, 절대 경로는
meta 에 실리고 다이제스트 바깥에 있습니다.
오버레이는 호출할 때마다 더러운 파일을 다시 파싱해서 나머지의 캐시된 샤드
위에 얹습니다(src/core/overlay.mjs). 베이스가 가진 적 없는 노드에는
provisional 표식이 붙습니다. 등급이 아니라 등급 옆의 표식입니다. 오버레이는
과대 근사할 수는 있어도 빠뜨릴 수는 없습니다. 말로 하는 약속이 아니라
테스트로 확인하는 약속입니다. test/overlay_integration.test.mjs 가 같은
바이트를 전체 재분석한 결과와 비교합니다. 편집을 커밋하면 오버레이는
폐기됩니다. 답은 behind 가 되고, cascade analyze 를 처방으로
알려 줍니다. 편집 이전의 답으로
조용히 되돌아가는 일은 절대 없습니다.
오버레이는 베이스 pack 을 만들 때의 입력으로 만듭니다. 분석한 루트의 diff 에
나오지 않는 변화가 여러분의 편집처럼 읽히면 안 되기 때문입니다. 그 뒤로 바뀐
입력은 조용히 다시 읽지 않습니다. 거절하거나, behind 로 답하거나, 그렇다고
말합니다. 프로필이 pack 에 기록된 것과 다르면 거절합니다. 고친 파일이 없어도
그렇습니다. 지금 파일로 계산한 카탈로그 샤드 키가 실행이 쓴 키와 다르면 거절하고
파일 이름을 댑니다(루트 밖의 DDL, 다시 받아 온 스냅샷). 따로 저장소에 있는
프런트엔드가 analyze 가 기록한 커밋보다 앞으로 갔으면 답은 behind 입니다.
따로 저장소에 있는 프런트엔드는, 분석한 루트 옆에 있든 그 안에 들어 있든 pack 이
읽은 것의 일부입니다. 그 커밋은 pack 의 핀에 들어갑니다. 그 저장소의 커밋하지 않은
파일은 pack 의 더러운 파일이 되고, 오버레이는 그 파일을 매번 다시 읽습니다. 그래서
고쳤다가 되돌린 편집도 되돌린 것으로 보입니다. 자세한 내용은
cli.md 에 있습니다.
오버레이는 베이스 pack 이 읽은 OpenAPI 문서를 지금 디스크에 있는 그대로 다시
읽고, analyze 와 똑같이 OpenAPI 브리지에 넘깁니다. 그래서 파일을 고쳐도 문서가
선언한 라우트와, 룰이 그 라우트에 이어 둔 핸들러가 사라지지 않습니다. 카탈로그도
analyze 와 같은 파일, 같은 방언이나 스냅샷으로 읽고, 실행이 읽은 카탈로그 파일을
하나라도 고쳤으면 오버레이를 거절합니다. 웹 브리지에는
베이스 pack 이 읽은 프런트엔드 패키지와 서버 포트를 analyze 와 똑같이 넘깁니다.
그래서 아무것도 고치지 않은 트리에 얹은 오버레이는 분석한 그래프와 똑같은 그래프를
만듭니다(test/overlay_equivalence.test.mjs). 다시 읽지 않는 입력도 몇 가지 있고,
그럴 때는 그렇다고 말합니다. 프런트엔드 근처의 package.json 을 고쳤으면
limits 에 적습니다. 포트를 읽은 pack 에서 Spring 설정을 고쳤거나, 다른 설정 파일이
이제 포트를 말하거나, Java 소스가 이제 포트를 정하면 그것도 적습니다. 오버레이는
베이스 pack 이 읽은 패키지와 포트를 그대로 쓰기 때문입니다. Spring XML 이 선언한
테이블 id 생성기는 트리 전체를 훑어서 찾는 것이라 다시 읽지 않습니다. 베이스
pack 이 그 생성기에 호출을 묶어 둔 적이 있으면, 답의 limits 에 그 호출이 여기서는
생성기 문장에 닿지 않는다고 적습니다. TypeScript 백엔드도 analyze 와 같은 방식으로
다시 훑습니다. 오버레이는 팩트 캐시에 아무것도 쓰지 않습니다. 바이트가 그대로라 샤드
키가 맞는 파일은 샤드에서 가져오고, 고친 파일은 다시 읽고, 어떤 파일을 읽을지는
지금의 import 를 따라 정합니다. 레인이 기대는 tsconfig, schema.prisma,
package.json 도 매 실행처럼 다시 읽습니다. 편집으로만 생긴 테이블이나 컬럼(이름을
바꾼 엔티티 컬럼, schema.prisma 에 새로 넣은 필드)은 새 statement 처럼
provisional 표식을 받습니다. git 이 바뀌지 않았다고 보는 파일인데 TypeScript 샤드가
더는 맞지 않으면 오버레이를 거절합니다(overlay-stale). 실행이 읽은 것과 다른
애플리케이션을 프로필이 가리키면(--ts-src 를 준 실행) limits 에 적습니다. 실행 트레이스와
브라우저 기록도 다시 읽지 않지만 따로 알리지는 않습니다. 이 둘은 RUNTIME_ONLY
표식만 더하고, 그 표식은 어떤 걷기도 따라가지 않기 때문입니다.
두 번째 속도는 재실행에도 해당합니다. 작은 편집 뒤의 실행은 바뀌지 않은 부분의
내용 주소 샤드를 재사용합니다. 그 결과는 같은 상태에서 돌린 cold 실행과
바이트 단위로 같아야 합니다(test/incremental.test.mjs 가 매 라운드 시드
고정 PRNG 로 파일의 무작위 부분집합을 변형합니다). 이 경로가 조용히 넘어가기를
거부하는 것이 넷 있습니다. 모른다는 것은 절대 “바뀐 것 없음”이 아닙니다. 손상된
샤드는 절대 적재되지 않습니다(SHARD_UNUSABLE 로 보고하고 그 단위만 다시
계산합니다). 더러운 트리는 meta.base.dirty 에 명시됩니다. 따로 저장소에 있는
프런트엔드의 커밋하지 않은 파일도 여기에 들어갑니다. pack 은 자신이 어떤
모드로 무엇을 다시 읽고 무엇을 재사용했는지 기록합니다.
모든 레인 입력은 선택 사항이고 모든 레인은 이름으로 끌 수 있습니다
(--no-ddl, --no-mappers, --no-java, --no-web, --no-openapi). 그래도
실행은 유효한 pack 을 만듭니다. pack 은 축별(catalog statements column
code jpa web screen)로 shipped, degraded, not-shipped 중 하나를
이유와 함께 기록합니다.
그 선언은 모든 답에 실립니다. 축 이름은 trust.knownGaps 로, 이유는 limits
로 가고, 빈 목록은 none 이 아니라 not-shipped 라고 말합니다.
답을 깔끔해 보이게 하려고 버리는 것은 없습니다. --no-ddl 로 분석한 프로젝트는
column: degraded 를 선언합니다. 그 뒤에 나오는 숫자는 실제 숫자입니다.
카탈로그 없이는
귀속할 수 없는 것을 버리지 않고 unresolved 로 기록합니다.
screen 축은 그 왕복의 먼 쪽 끝입니다. 라우터 자신의 선언을 사용자가 실제로
있는 경로로 합성하고, 각각을 그 라우트가 마운트하는 컴포넌트의 프런트엔드
함수들에 RENDERS 엣지로 잇습니다. RENDERS 는 라우트가 선언한 파일로 갈
때 EXACT 이고, 그 컴포넌트가 import 하는 파일로 갈 때 SOUND_SET 입니다.
import 된 컴포넌트의 어느 함수가 실제로 도는지는 런타임 질문이기 때문입니다.
degraded 는 입력이 빠진 경우에만 붙는 것이 아닙니다. web 축은 프런트엔드에
대해 아무것도 추측하지 않았을 때만 shipped 입니다. 엔진이 매칭 개수를 세어
알아낸 URL 접두사(룰 팩이 이름을 댄 래퍼 단계가 URL 앞에 붙이는 접두사도
포함), 프로젝트가 선언하지 않아서 가정한 경로 별칭, 어떤 .env 파일도 정하지 않은
기본값이나 이 머신이 아닌 호스트에만 기댄 base URL, 어떤 파일도 말하지 않는 포트로
이 머신에 가는 호출이 있으면 축은 degraded 가 됩니다. 축의 causes 가 그중 무엇인지
말하고, 이유는 무엇을 선언하면 되는지 알려 줍니다. 클라이언트까지
추적하지 못한 호출이 추적한 호출만큼 많아도 degraded 입니다. 그보다 적으면 축을
내리지 않습니다. 대신 몇 개가 추적되지 않았고 대부분 왜 그런지를 note 로 달아
모든 답에 실어 보냅니다.
catalog 축은 DDL, 가져온 스냅샷, schema.prisma, TypeORM 엔티티 중 무엇으로도
만들어집니다. 매핑이 테이블을 선언했으면 축의 sources 에 그 출처를
적습니다(schema.prisma, 또는 TypeORM 엔티티와 그 개수). TypeORM 테이블·컬럼
이름이 이번 실행이 확인하지 못한 것에 기대면 축은 degraded 입니다. 옵션이 네이밍
전략이나 테이블 prefix 를 실행 시점에 맡긴 경우가 그렇습니다. DDL 리더가 읽지 못했거나
가정해야 했던 것도 pack 에 들어갑니다(meta.laneStats.catalog, 종류마다 진단 하나와
개수). overview 는 이것을 빈 곳 세 가지로 셉니다. 읽지 못한 CREATE TABLE 은
catalog-tables-unread, 아무도 선언하지 않은 데이터베이스의 규칙에 기댄 결론은
catalog-rules-assumed, 파일에 적힌 대로 적용하지 못한 문장은
catalog-read-in-part 입니다. 앞의 두 가지는 축도 degraded 로 만들고, 축의
causes 가 어느 쪽인지 말합니다(tables-unread, rules-assumed. 위의 TypeORM
이름은 typeorm-names-heuristic). 일부만 읽은 테이블은 축을 내리지 않습니다.
overview 는 원인마다 해결책을 하나씩 줍니다. 가정한 데이터베이스는 sqlDialects 선언,
TypeORM 이름은 tsBackend.typeorm 선언입니다. 잃은 테이블에는 해결책이 없고, 원인이
둘이면 하나로 정할 해결책이 없습니다.
축이 보기보다 일찍 끝난다는 뜻일 수도 있습니다. 라우트가 소스가 아니라
OpenAPI 문서에서 온 pack 은 엔드포인트는 있는데 그 아래에는 아무것도 없습니다.
그래서 code 축이 degraded 가 되고, 이유는 “endpoints come from an OpenAPI
document, not from source: the routes exist, but nothing below them is walked,
so a frontend call reaches an endpoint and stops there” 라고 적힙니다. 라우트
아래 체인이 필요한 질문에는, “찾아봤다”로 읽힐 빈 목록 대신 not-shipped 로
답합니다.
화면에 라우터가 꼭 필요한 것은 아닙니다. 서버가 페이지를 그리는 애플리케이션에서는
@Controller 가 경로에 답하면서 뷰 이름을 돌려줍니다. 템플릿 엔진이 그
이름을 브라우저가 받는 페이지로 바꿉니다. screen:view:owners/findOwners 가 그
페이지입니다. symbol --RENDERS_PAGE--> screen 이 그 화면을 그리는 핸들러이고,
리터럴로 돌려준 문자열이 리졸버의 입력 그 자체이므로 EXACT 입니다. 페이지
자신의 <form> 과 링크와 인라인 <script> 는 이 pack 이 서빙하는 라우트를
향한 호출입니다. RENDERS_PAGE 는 의도적으로 걷지 않는 엣지입니다.
페이지의 링크는 이번 요청이 아니라 다음 요청입니다. 페이지를 그리는 라우트에서
그 링크를 따라가게 했더니, 모든 라우트가 자기 페이지가 링크하는 모든 라우트의
도달 범위를 물려받았습니다. 고정된 코퍼스에서 양쪽으로 재 봤습니다. 걷게 하면
엔드포인트 하나가 닿는 범위가 jpetstore-6 에서 81% 부풀고(엔드포인트-컬럼 쌍
243 → 439), xxl-job 에서는 14% 부풉니다. 그런데 합집합 개수는 하나도 움직이지
않았습니다.
fan-out 천장이 잡으려고 있는 옆으로 번지기의 정확히 그 모양입니다. 빼면 모든
프로젝트가 기준 쌍 개수로 정확히 돌아오므로, 그 엣지가 차이의 전부입니다. 대신
한 걸음만 가고, 어느 걷기든 같은 한 걸음을 갑니다. 위로 걸을 때는 걷기가 닿은
메서드가 그리는 페이지를 한 걸음 옆에서 붙입니다. 그래서 위로 걷는 Trace,
endpoint_impact 의 화면, screen_impact, 화면 집계가 같은 페이지를 말합니다.
screen_impact 는 페이지를 그리는 라우트도 그 페이지가 거치는 라우트에 넣습니다.
페이지에서 아래로 걸을 때는 그 페이지를 그리는 메서드에서 시작합니다. 시작할 때만
그렇고, 걷는 중간에는 그러지 않습니다. 라우트의 그림에는 그 핸들러가 그리는
페이지가 나옵니다.
브라우저 기록(--har)은 종류가 다른 사실입니다. 등급도 그렇게 매겨집니다.
RUNTIME_ONLY 는 모든 모드의 하한 아래이므로 기록된 screen → route 호출은
보여 주기만 하고(observed: true) 절대 걷지 않으며, 옆의 정적 엣지의
등급을 올리지도 않습니다. 기록은 요청이 한 번 일어났음을 증명할 뿐, 코드가 무엇을
할 수 있는지는 전혀 증명하지 않습니다.
cascade estimate 는 분석 전에 같은 질문에 답합니다. 이 트리가 무엇을
shipped, degraded, not-shipped 로 내놓을지입니다.
프로젝트마다 .cascade/calibration/ 에 봉인된 기준선을 둡니다
(src/core/calibration.mjs). 모든 analyze 를 그 기준선과 비교합니다. 게이트는
먼저 이 실행이 왜 다른지를 묻습니다.
| 모드 | 언제 |
|---|---|
NO_SEAL |
기준선이 아직 없음. 이번 실행이 기준선이 됩니다 (BOOTSTRAP) |
NO_CHANGE |
같은 엔진, 같은 커밋 핀 |
ENGINE_MOVED |
엔진 지문이 움직임. 업그레이드 |
REPIN |
분석 대상이 움직임: 커밋(따로 저장소에 있는 프런트엔드의 커밋 포함), 읽은 입력, 프로필, 카탈로그 |
BOTH_MOVED |
둘 다 |
ENGINE_MOVED 가 가장 엄격합니다. “분석기가 바뀌었다”는 바로 손실이 스며들면
안 되는 순간이므로, 하락을 회귀로 보고 막습니다. 게이트는 절대값이 아니라
비교 기준이며 개선도 함께 보고합니다. 분자가 올라간 비율은 답이 작아진
것이 아니라 레인이 넓어진 것입니다. 발견 문구도 바로 그렇게 적습니다.
RED 실행은 조용히 버려지지 않습니다. 그 pack 은 <packDir>-rejected/ 로 가고,
인증된 pack 은 있던 자리에 그대로 남고, 명령은 종료 코드 3 으로 끝납니다. 유일한
우회는 --accept-baseline 입니다. 기준선을 그 실행에서 다시 봉인합니다.
설계상 사람의 결정입니다.
그 옆에서 cascade golden 이 프로젝트 자신의 라벨링된 코퍼스를 관리합니다.
도구는 사례를 제안합니다. 사람이 승인합니다(--ids … 또는 명시적
--all). 해시가 어느 것을 홀드아웃으로 뺄지 정합니다. check 가 승인된
사례를 실제 배포되는 MCP 도구를 통해 채점합니다. 도구가 스스로를 승인하는
일은 없습니다. 그래서 위의 신뢰 수준이 무언가를 뜻할 수 있습니다.
pack 에서 뽑아낸 제안은 만들어진 방식 그대로 옳습니다. 바로 그래서 사람이 읽기
전까지는 아무 무게가 없습니다. 그런데 실제로 읽는 사람은 거의 없습니다.
cascade golden propose --from-otel <trace> 는 분석기가 닿을 수 없는 곳 —
돌아가는 프로그램 — 에서 라벨을 가져옵니다. 실행 기록이 밟은 라우트마다, 그
요청 아래에서 돈 모든 statement 의 테이블을 실은 endpoint->tables 사례가
됩니다. SQL 을 돌린 메서드마다는 method->statements 사례가 됩니다. pack 이
모르는 것은 빠지고, 빠졌다고 소리 내어 셉니다.
실행 기록으로 보일 수 있는 것과 없는 것은 여기까지입니다. 실행은 도달을 증명하므로,
런타임 사례에는 absent id 가 없고 재현율만 채점합니다. 실제로 돈 것을 답이
덮었다는 것은 보일 수 있고, 정밀도는 절대 보일 수 없으며, 아무도 밟지 않은
라우트에 대해서는 아무 말도 하지 않습니다. RUNTIME_PASS 는 딱 그만큼만
주장하는 수준입니다. 이것들을 승인할 때는 --all 이 정상입니다. 사람이 동의하는
것은 분석기가 옳았다는 것이 아니라 그 기록이 공정한 기록이라는 것이니까요.
statement 를 하나도 돌리지 않은 라우트가 흥미로운 사례입니다. 정직한 라벨은
빈 목록이고, 빈 라벨의 채점은 한 방향뿐입니다. pack 도 아무것도 답하지 않으면
PASS 이고(코드를 읽는 쪽과 돌려 보는 쪽, 서로 독립인 두 출처가 이 라우트는
아무것도 읽지 않는다고 합의한 것입니다), pack 이 실행에서 건드리지 않은 테이블을
답하면 UNSCORABLE 입니다. 그 요청이 단지 그 분기를 타지 않았을 수도 있기
때문입니다. 저것을 통과로 세면 데이터베이스를 전혀 건드리지 않은 실행이 인증을
해 버리게 됩니다. golden check 는 관계마다 두 개수를 다 찍습니다. propose 는 자기가
쓴 사례 중 아무것도 주장하지 않는 것이 몇 개인지 말합니다.
UNRESOLVED 로 내려갑니다. 절대 위로
반올림하지 않습니다.${} 치환, 해석 불가능한 include)은 추측하지도
버리지도 않고 진단으로 기록합니다.DDL 의 create table item (…) 과 매퍼의 FROM ITEM I 는 HSQLDB 에서도, Oracle
에서도, lower_case_table_names=1 인 MySQL 서버에서도 같은 테이블입니다.
문자열을 정확히 비교하면 그래프에 테이블이 둘 생기고 사실이 반으로 갈립니다.
언제나 접어서 비교하면, 진짜로 둘을 구분하는 데이터베이스에서 서로 다른 테이블
둘을 합쳐 버립니다. 그래서 규칙은 둘 다 아닙니다. 각 데이터베이스의 매뉴얼이
말하는 대로 방언별로 선언합니다. 그 선언은 작은 표 하나로 되어 있습니다
(src/core/identifier_case.mjs, 워커용 사본은
adapters/sql/identifier_case.py, 둘 다 돌려 보는 테스트가 교차 검증합니다).
fold-lower, Oracle 과 HSQLDB 와 H2 는
fold-upper, 이 엔진이 모르는 방언은 아무것도 접지 않는 exact 입니다."Item", `Item`)는 어디서나 정확히 비교하므로,
카탈로그가 선언한 철자와만 대조합니다.sqlIdentifierCase(fold-lower | fold-upper | exact |
null, 마지막은 방언 자신의 규칙)가 이 표를 덮어쓰며, 모든 실행이 어떤 규칙을
어디서 가져왔는지 출력합니다.접기는 비교용 키를 만들 뿐 새 이름을 만들지 않습니다. 테이블과 컬럼은
카탈로그가 준 철자를 그대로 유지하므로 pms_product 는 ERD 에서도 모든 답에서도
pms_product 입니다. 그래서 접는 방향은 사실을 바꾸지 못하고 무엇이 매칭되는지만
바꿉니다. 하나의 키로 접히는 카탈로그 이름 둘은
folded_identifier_collision 으로 보고됩니다. 먼저 선언된 쪽이 키를 가지며, 두
테이블 모두 선언된 그대로 pack 에 남습니다. 접어서 비교해도 카탈로그에 없는
테이블은 여전히 unresolved 입니다. 접기는 비교를 느슨하게 만드는 것이 아니라
올바르게 만드는 것입니다.
도구 인자도 같은 방식으로 접힙니다. pack 은 자신이 어떤 규칙으로
만들어졌는지 기록하므로(pack.meta.identifierCase), 여러분이 자기 SQL 에서
읽어 온 철자 그대로 table_usage ORDERS 라고 물어도 카탈로그의 orders 에 대해
답합니다. 답에는 그렇게 했다는 limits 한 줄이 붙습니다. column_impact,
endpoint_impact, flow, neighborhood, erd 의 focus 테이블도 같습니다.
아무것과도 맞지 않는 이름은 unknown-table 이나 unknown-column 코드를
유지하면서 가장 가까운 후보를 알려 줍니다. 선언된 이름 둘로 접히는 이름은
모호하므로 어느 쪽으로도 해석하지 않습니다.
마이크로서비스 하나에 리포지터리 하나가 보통 모양이므로, 서비스마다 자기 pack
으로 분석됩니다. 그 pack 은 자기 코드가 GET /owners/{ownerId} 를 어딘가로
보낸다는 것까지만 알고 거기서 멈춥니다. 부르는 라우트가 자기가 서빙하는 라우트가
아니니, 레인은 그래프에 바깥쪽 엔드포인트 노드를 하나 놓고 그 위에 UNRESOLVED
CALLS_HTTP 엣지를 답니다. 그리고 어떤 모드도 UNRESOLVED 엣지를 걷지 않습니다.
체인은 “pack 을 떠남” 으로 끝납니다. pack 하나만 놓고 보면 그것이 정직한 답입니다.
경계 넘기(crossing) 는 그 pack 을 들고 있는 서버가 그 라우트에 답하는
프로젝트도 함께 들고 있을 때 일어나는 일입니다. 같은 걷기가 저쪽에서 이어지고,
가져온 행에는 project 가 붙습니다. pack 은 손대지 않습니다. 이음매는 질문이
들어올 때, analyze 가 각 pack 옆에 써 두는 작은 routes.json 색인으로
만들어집니다.
동점을 가르는 것은 이름입니다. 두 프로젝트가 GET /owners 를 서빙할 수 있고,
경로로는 구분되지 않습니다. 구분해 주는 것은 부르는 쪽이 적어 놓은 이름입니다.
Java 클라이언트 URL 의 호스트, 또는 게이트웨이 라우트가 넘겨 주는 서비스입니다.
둘 다 사람이 타이핑해 넣는 것이 아니라 트리에서 읽습니다. cascade init 이
서비스 자신의 spring.application.name 을 profile.serviceNames 로, 게이트웨이의
spring.cloud.gateway 라우트 표를 profile.gatewayRoutes 로 가져옵니다. 각
항목에는 넘겨 주는 배포 단위가 실려 있습니다. 호출에 이름이 있고 프로젝트에
이름이 있으면 후보 하나를 골라 SOUND_SET 으로 넘고, 없으면 모든 후보를
HEURISTIC 으로 넘으면서 답이 모호했다고 말합니다.
경계 넘기는 절대 후보보다 나을 수 없습니다. customers-service 라는 서비스
이름에 어느 배포 단위가 답하는지는, 어느 쪽 소스 코드에도 적혀 있는 사실이
아닙니다. 배포에 관한 사실일 뿐, 그것을 진술하는 코드 줄은 하나도 없습니다.
그래서 경계 넘기는 잘해야 SOUND_SET 입니다. 어느 한쪽의 HTTP 메서드를 읽지
못했거나 등록된 프로젝트 둘 이상이 그 라우트를 서빙하면 HEURISTIC 이며,
다른 엣지와 똑같이 아래 모든 행의 경로 등급을 약화시킵니다. 경계를 넘어 닿은
테이블은 이 요청이 닿을 수도 있는 테이블입니다. 한 레인 안쪽의 MAY_CALL
이 하는 주장과 같습니다.
맞는 것이 없는 호출은 삼켜지지 않고 목록에 오릅니다. 등록된 어떤 프로젝트도
서빙하지 않는 호출은 체인을 예전 그대로 둡니다. 답은 federation.unmatched 에서
그 호출을 몇 개 프로젝트에 물어봤는지와 함께 이름 붙입니다. 프로젝트 하나만
서빙하는 서버에서도 목록은 그대로 있습니다. 해법은 “이것들을 서빙하는 프로젝트를
등록해 달라” 는 것입니다. 목록을 못 본 사람은 그럴 생각을 할 수 없기 때문입니다.
아예 물어볼 수
없었던 프로젝트도 같은 규칙입니다. pack 옆에 라우트 색인이 없는 프로젝트는
federation.skipped 에 이름이 오르고, 해법으로 cascade analyze 가 적힙니다.
요청이 들어간 라우트는 그 자체로 한 행입니다. 경계 넘기는 다른 프로젝트의
라우트에 내려앉습니다. 저쪽 걷기는 거기서 시작하므로, 그 아래에서 일어나는 일은
이쪽 그림에 전혀 나타나지 않습니다. 이 행을 빼면, 옆 프로젝트가 대신 답해 주는
호출을 가진 화면도 빈 엔드포인트 레인만 보여 주게 됩니다. 그 옆에는 이 모드가
믿지 않는 연결이라는 안내만 있고, 결국 “이 화면은 아무것도 부르지 않는다” 로
읽힙니다. 그래서 경계 넘기마다 그 대상 라우트가
project, federated, viaHttp, 넘기의 등급, 그리고 들어간 홉을 달고
endpoints 행이 됩니다. 이 프로젝트 자신의 엔드포인트 집계에는 세지 않습니다.
walk 와 layers 는 테이블에서와 똑같이 이 프로젝트의 걷기를 서술합니다.
“화면 9 개 중 0 개가 테이블에 닿는다” 는 pack 하나에 대해서는 참인 문장이고
제품에 대해서는 거짓인 문장입니다. 모든 요청을 다른 데서 받아 주는 게이트웨이
에서 그 화면 아홉 개는 HTTP 한 홉 건너 진짜 컬럼에서 끝납니다. 그래서
overview.reach 는 viaFederation: {screens, endpoints} 를 싣습니다. 이 값은
이 프로젝트의 화면과 라우트 중 몇 개가 연결된 프로젝트의 테이블에 닿는지를
나타냅니다. 페이지는 다이얼 아래에 두 숫자를 같이
적습니다(0 here, 8 in connected projects). 다이얼 자체는 이 프로젝트의
비율로 남습니다. 이 프로젝트의 몫이란 그런 뜻이기 때문입니다. 프로젝트가
하나뿐인 서버에서, 이 프로젝트가 부르는 것이 테이블에 하나도 닿지 않으면 이
블록이 없습니다. 없다는 것이 “안 찾아봤다” 가 아니라 “넘어가는 것이 없다” 로
읽히도록 하려는 것입니다.
그림도 경계를 넘습니다. 그래도 여전히 한 프로젝트의 그림입니다. pack 전체를
보여 주는 화면(map, erd)도 같은 넘기 규칙을 따릅니다. 여기에는 자기만의
규칙이 하나 더 있습니다. 이 프로젝트의 요청이 닿는 것만 더하지 저쪽 pack 을
통째로 더하지 않습니다. 두 서비스는 외래 키를 공유하지 않으므로, 그 사이에 그은
관계선은 발견이 아니라 창작입니다. 진짜 서비스 둘을 합쳐 놓은 그림은 틀리기
전에 이미 읽히지 않습니다. 그래서 이 프로젝트가 부른 라우트를 그리고, 그
라우트가 저쪽에서 닿는 것을 그 아래에 그립니다. 두 무리를 잇는 것은 HTTP 호출
그 자체뿐입니다.
전송 형태, basis.siblings, federate 인자 같은 나머지는 mcp.md
에 있습니다.
.cascade/profile.json 은 읽기 규약입니다. 파서가 알 수 없는 자리에서 이
프로젝트의 코드가 무슨 뜻인지, 그리고 소스가 말하지 않는 것을 사람이 적어 두는
곳입니다. cascade init 이 쓰고, 그다음은 여러분이 고칩니다. 모든 키는 엔진의
동작을 바꾸거나(consumed), 설정하는 순간 기록되고 그렇다고 알려집니다. 죽은 키가
없다는 것은 테스트가 지킵니다(src/core/profile.mjs 의 PROFILE_KEY_CONSUMERS).
경로는 프로필이 있는 디렉터리 .cascade/ 기준입니다. 그래서 저장소 맨 위의 파일은
../file 입니다. servers 만 분석한 루트 기준으로 이름을 씁니다. 프로필 digest 를
처음 기록한 뒤에 생긴 키(tsBackend, pathPrefixes, servers, tsBackend.typeorm
과 그 type, openapi.generatedFromCode, openapi.generatesCode, jpa.implicitNamingStrategy)는 기본값인 동안
digest 에서 빠집니다. 그래서 엔진을 올려도 보정 게이트가 대상이 바뀐 것으로 보지
않습니다. 검증기는 모양이 틀린 값은 거부하지만, 모르는 키는 거부하지 않습니다.
그래서 철자가 틀린 키는 아무것도 말하지 않습니다.
| 키 | 무엇을 말하나 | 예 |
|---|---|---|
build.tool |
빌드 도구입니다. 기록만 합니다. Gradle 이나 Maven 을 돌리지 않습니다 | "maven" |
build.javaRelease |
Java 언어 수준입니다. 기록만 합니다 | 17 |
build.profiles |
빌드 프로필입니다. 기록만 합니다. 의존성을 풀지 않습니다 | ["prod"] |
packagePrefixes |
프로젝트 자신의 최상위 패키지입니다. 그 밖의 타입은 외부 것이고, 그리로 가는 호출은 풀지 못한 호출이 아니라 외부 호출로 셉니다 | ["com.macro.mall"] |
schema.default |
스키마를 적지 않은 테이블의 스키마입니다. null 이면 모르는 것으로 둡니다 |
"public" |
schema.propertyNames |
MyBatis SQL 의 ${prop}.table 에서 raw 치환이 아니라 스키마 한정자인 속성 이름입니다 |
["db.schema"] |
schema.rewriteLayer |
SQL 재작성 계층입니다. 모델링하지 않으므로 값을 넣으면 not shipped 로 알립니다 | null |
sqlDialects |
SQL 이 어느 데이터베이스용인지 main 에 적습니다(sql-lane.md). 선언하지 않으면 MySQL 로 가정하고 그렇다고 알립니다 |
{ "main": "postgresql" } |
sqlIdentifierCase |
두 철자가 같은 테이블인지 정하는 규칙입니다: fold-lower, fold-upper, exact. null 이면 방언의 규칙을 씁니다(7절) |
"fold-lower" |
gatewayRoutes |
프런트엔드가 쓰는 접두사와 백엔드가 서빙하는 접두사입니다. "*" 는 모든 호출에 적용됩니다(web-lane.md) |
{ "/dev-api": "" } |
pathPrefixes |
설정 코드가 컨트롤러 라우트 앞에 붙이는 경로 접두사입니다(java-lane.md) | [{ "prefix": "/admin-api", "packages": "**.controller.admin.**" }] |
servers |
소스가 말하지 않는 애플리케이션 포트입니다. 키는 src/main/resources 를 가진 디렉터리이고, 분석한 루트 기준입니다(web-lane.md) |
{ "mall-admin": { "port": 8080 } } |
serviceNames |
이 프로젝트가 불리는 이름(spring.application.name)입니다. 건너가기가 이 이름으로 프로젝트를 고릅니다(8절) |
["mall-admin"] |
webRoots |
package.json 이 선언하지 않은 프런트엔드 루트입니다. {root, kind, from} 모양입니다 |
[{ "root": "../src/main/resources/static", "kind": "declared", "from": "user" }] |
templateRoots |
뷰 이름이 서버에서 그리는 페이지가 되는 곳입니다. {root, engine, suffix, from} 모양입니다 |
[{ "root": "../src/main/resources/templates", "engine": "thymeleaf", "suffix": ".html", "from": "default" }] |
screenAxis.enabled |
라우터 선언을 화면으로 만들지 정합니다. true, false, 또는 실행이 읽은 것으로 정하는 null 입니다 |
true |
screenAxis.nameSource |
화면 제목을 어디서 가져올지 정합니다: route-meta 또는 none |
"route-meta" |
screenAxis.pathRule |
화면의 짧은 라벨을 경로에서 자르는 방식입니다. 경로 자체는 바뀌지 않습니다 | "last-segment" |
screenAxis.codeRegex |
화면 코드를 읽는 패턴입니다 | "([A-Z]{2}\\d{4})" |
moduleAttribution.packageDepth |
라우트를 경로 첫 구간 대신 핸들러 패키지를 이 단계까지 자른 것으로 묶습니다 | 4 |
moduleAttribution.codeLength |
화면 코드의 앞 몇 글자가 묶음 이름인지 정합니다 | 2 |
frameworkPacks |
플래그 없는 analyze 가 돌릴 레인입니다 |
["spring-mvc", "mybatis-xml", "jpa", "web"] |
tsBackend.app |
TypeScript 레인이 읽는 NestJS 애플리케이션 루트입니다(ts-lane.md) | "../apps/api/src" |
tsBackend.prismaSchema |
Prisma 가 먼저 찾는 자리에 없을 때 읽을 schema.prisma 입니다 |
"../prisma/schema.prisma" |
tsBackend.globalPrefix |
설정에서 읽는 부트스트랩을 위한, 애플리케이션이 배포되는 접두사입니다. "" 는 접두사 없음입니다 |
"api" |
tsBackend.globalPrefixExclude |
그 접두사에서 빼는 라우트 패턴입니다. Nest 문법으로 씁니다 | ["health", "docs{/*rest}"] |
tsBackend.typeorm.namingStrategy |
TypeORM 네이밍 전략입니다. typeorm 팩의 이름(default, snake)으로 씁니다 |
"snake" |
tsBackend.typeorm.entityPrefix |
TypeORM 이 모든 테이블 이름 앞에 붙이는 접두사입니다 | "" |
tsBackend.typeorm.schema |
스키마를 적지 않은 엔티티의 스키마입니다 | "public" |
tsBackend.typeorm.type |
TypeORM 드라이버입니다. 테이블 이름 앞에 무엇이 붙는지를 드라이버가 정합니다. 통하는 값은 typeorm 팩의 드라이버 이름이고, 다른 값이면 그 이름들은 HEURISTIC 으로 남습니다 | "postgres" |
jpa.namingStrategy |
명시한 이름과 유도한 이름에 적용하는 JPA 물리 네이밍 규칙. 설정이 팩토리에 전달되는지 판단하고, 적용할 설정이 없으면 팩토리의 기본값을 가정합니다 | "spring-snake-case" |
jpa.implicitNamingStrategy |
기본 조인 테이블 이름 규칙. spring은 소유 테이블과 속성, jpa-compliant는 두 테이블의 이름을 씁니다. 선언이나 적용 가능한 설정이 없고 두 규칙의 결과가 다르면 HEURISTIC입니다 |
"spring" |
mybatisPlus.namingStrategy |
@TableName 이나 @TableField 가 없는 MyBatis-Plus 이름에 대한 같은 규칙입니다 |
"underscore" |
mybatisPlus.tablePrefix |
MyBatis-Plus 가 유도한 테이블 이름 앞에 붙이는 전역 접두사입니다 | "tb_" |
mybatisPlus.logicDeleteValue |
논리 삭제가 @TableLogic 컬럼에 쓰는 값입니다 |
"1" |
mybatisPlus.logicNotDeleteValue |
“삭제 안 됨” 값입니다. 기록만 합니다 | "0" |
generatedSources.annotations |
생성된 타입을 표시하는 애너테이션 이름입니다. 걷기는 생성 코드끼리의 엣지를 건너뛰고 그렇다고 말합니다 | ["Generated"] |
generatedSources.pathGlobs |
애너테이션을 남기지 않는 생성기를 위한, 생성된 타입의 소스 파일 glob 입니다 | ["mall-mbg/**"] |
openapi.documents |
선언된 라우트로 읽을 OpenAPI 나 Swagger 문서입니다 | ["../api/openapi.yaml"] |
openapi.generatedFromCode |
빌드가 지금 코드로 쓰는 문서입니다. 이 문서로 놓인 라우트는 핸들러를 읽은 만큼 등급을 받습니다(web-lane.md) | ["../docs/openapi.json"] |
openapi.generatesCode |
빌드가 이 코드의 인터페이스를 만들어 내는 문서입니다. 이 문서 위의 계약 연결은 EXACT 가 될 수 있습니다 | ["../src/main/resources/openapi.yml"] |
runtimeEvidence.har |
RUNTIME_ONLY 증거로 읽을 브라우저 기록입니다(runtime-evidence.md) | ["../evidence/admin-session.har"] |
runtimeEvidence.otel |
런타임 증거로 읽을 OpenTelemetry 트레이스 파일입니다 | ["../evidence/checkout-smoke.json"] |
modelPacks |
오염 분석 모델 팩입니다. 그 레인은 없으므로 값을 넣으면 not shipped 로 알립니다 | [] |
catalog.source |
카탈로그를 어디서 가져올지 정합니다: file, jdbc, none(db-catalog.md) |
"file" |
catalog.connectionFrom |
file 이면 DDL 경로이고, jdbc 나 none 이면 discovery 가 찾은 접속 정보 파일입니다 |
"../document/sql/mall.sql" |
catalog.ddl |
connectionFrom 대신 적은 순서대로 읽을 DDL 파일입니다 |
["../db/schema.sql", "../db/data.sql"] |
catalog.ddlAlternatives |
init 이 찾았지만 고르지 않은 다른 벤더의 DDL 입니다. 기록만 합니다 |
{ "oracle": ["../db/oracle/schema.sql"] } |
mappers.alternatives |
다른 벤더용 매퍼 XML 입니다. statement 레인에서 뺍니다 | { "oracle": ["../src/main/resources/mapper/oracle/UserMapper.xml"] } |
calibration.firstRun |
봉인된 기준이 없을 때: bootstrap(이번 실행이 기준이 됨) 또는 require-baseline(RED) |
"bootstrap" |
calibration.maxRelativeDrop |
엔진이 바뀐 실행이 RED 가 되기 전까지 잃어도 되는 지표의 비율입니다 | 0.05 |
calibration.maxRelativeDropOnRepin |
REPIN 일 때의 같은 값입니다 |
0.25 |
calibration.receiptTtlDays |
배포 영수증이 cascade verify 에 유효한 날 수입니다 |
30 |