isthmus-cli 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +170 -45
- package/README.md +178 -36
- package/Skills/isthmus/SKILL.md +104 -5
- package/compatibility.json +10 -0
- package/dist/cli/check-command.d.ts +2 -2
- package/dist/cli/check-command.js +22 -9
- package/dist/cli/check-command.js.map +1 -1
- package/dist/cli/command-support.d.ts +20 -2
- package/dist/cli/command-support.js +38 -9
- package/dist/cli/command-support.js.map +1 -1
- package/dist/cli/diff-command.js +4 -3
- package/dist/cli/diff-command.js.map +1 -1
- package/dist/cli/doctor-command.d.ts +11 -0
- package/dist/cli/doctor-command.js +128 -0
- package/dist/cli/doctor-command.js.map +1 -0
- package/dist/cli/extract-js-command.d.ts +33 -0
- package/dist/cli/extract-js-command.js +233 -0
- package/dist/cli/extract-js-command.js.map +1 -0
- package/dist/cli/graph-command.js +13 -6
- package/dist/cli/graph-command.js.map +1 -1
- package/dist/cli/impact-command.d.ts +5 -0
- package/dist/cli/impact-command.js +111 -0
- package/dist/cli/impact-command.js.map +1 -0
- package/dist/cli/init-command.d.ts +11 -0
- package/dist/cli/init-command.js +115 -0
- package/dist/cli/init-command.js.map +1 -0
- package/dist/cli/main.js +67 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/mcp-server.d.ts +16 -0
- package/dist/cli/mcp-server.js +534 -0
- package/dist/cli/mcp-server.js.map +1 -0
- package/dist/cli/preflight-command.d.ts +5 -0
- package/dist/cli/preflight-command.js +83 -0
- package/dist/cli/preflight-command.js.map +1 -0
- package/dist/cli/query-command.js +13 -6
- package/dist/cli/query-command.js.map +1 -1
- package/dist/cli/retentions-command.js +14 -7
- package/dist/cli/retentions-command.js.map +1 -1
- package/dist/cli/runtime-command.d.ts +5 -0
- package/dist/cli/runtime-command.js +42 -0
- package/dist/cli/runtime-command.js.map +1 -0
- package/dist/cli/runtime-json-reader.d.ts +12 -0
- package/dist/cli/runtime-json-reader.js +37 -0
- package/dist/cli/runtime-json-reader.js.map +1 -0
- package/dist/cli/serve-command.d.ts +13 -0
- package/dist/cli/serve-command.js +43 -0
- package/dist/cli/serve-command.js.map +1 -0
- package/dist/exchange/capture-config.d.ts +18 -0
- package/dist/exchange/capture-config.js +100 -0
- package/dist/exchange/capture-config.js.map +1 -0
- package/dist/exchange/impact-selection.d.ts +13 -0
- package/dist/exchange/impact-selection.js +35 -0
- package/dist/exchange/impact-selection.js.map +1 -0
- package/dist/exchange/kartograph-impact.d.ts +4 -0
- package/dist/exchange/kartograph-impact.js +169 -0
- package/dist/exchange/kartograph-impact.js.map +1 -0
- package/dist/exchange/messages.d.ts +36 -0
- package/dist/exchange/messages.js +96 -0
- package/dist/exchange/messages.js.map +1 -0
- package/dist/exchange/parse.d.ts +69 -0
- package/dist/exchange/parse.js +127 -20
- package/dist/exchange/parse.js.map +1 -1
- package/dist/exchange/preflight-context.d.ts +71 -0
- package/dist/exchange/preflight-context.js +317 -0
- package/dist/exchange/preflight-context.js.map +1 -0
- package/dist/exchange/producer-impact.d.ts +17 -0
- package/dist/exchange/producer-impact.js +226 -0
- package/dist/exchange/producer-impact.js.map +1 -0
- package/dist/exchange/runtime.d.ts +69 -0
- package/dist/exchange/runtime.js +160 -0
- package/dist/exchange/runtime.js.map +1 -0
- package/dist/extract/js-document.d.ts +22 -0
- package/dist/extract/js-document.js +230 -0
- package/dist/extract/js-document.js.map +1 -0
- package/dist/extract/js-scan.d.ts +93 -0
- package/dist/extract/js-scan.js +1205 -0
- package/dist/extract/js-scan.js.map +1 -0
- package/dist/extract/lexer.d.ts +26 -0
- package/dist/extract/lexer.js +316 -0
- package/dist/extract/lexer.js.map +1 -0
- package/dist/join/join.d.ts +66 -1
- package/dist/join/join.js +171 -1
- package/dist/join/join.js.map +1 -1
- package/dist/join/message-address.d.ts +7 -0
- package/dist/join/message-address.js +37 -0
- package/dist/join/message-address.js.map +1 -0
- package/dist/join/messages.d.ts +26 -0
- package/dist/join/messages.js +87 -0
- package/dist/join/messages.js.map +1 -0
- package/dist/report/check-report.d.ts +20 -3
- package/dist/report/check-report.js +240 -8
- package/dist/report/check-report.js.map +1 -1
- package/dist/report/codequality.d.ts +45 -0
- package/dist/report/codequality.js +74 -0
- package/dist/report/codequality.js.map +1 -0
- package/dist/report/diff.d.ts +25 -3
- package/dist/report/diff.js +91 -10
- package/dist/report/diff.js.map +1 -1
- package/dist/report/graph.d.ts +10 -4
- package/dist/report/graph.js +60 -6
- package/dist/report/graph.js.map +1 -1
- package/dist/report/impact.d.ts +62 -0
- package/dist/report/impact.js +168 -0
- package/dist/report/impact.js.map +1 -0
- package/dist/report/preflight-runtime.d.ts +42 -0
- package/dist/report/preflight-runtime.js +193 -0
- package/dist/report/preflight-runtime.js.map +1 -0
- package/dist/report/preflight-view.d.ts +146 -0
- package/dist/report/preflight-view.js +216 -0
- package/dist/report/preflight-view.js.map +1 -0
- package/dist/report/preflight.d.ts +104 -0
- package/dist/report/preflight.js +331 -0
- package/dist/report/preflight.js.map +1 -0
- package/dist/report/query.d.ts +11 -4
- package/dist/report/query.js +84 -17
- package/dist/report/query.js.map +1 -1
- package/dist/report/retentions.d.ts +17 -5
- package/dist/report/retentions.js +85 -8
- package/dist/report/retentions.js.map +1 -1
- package/dist/report/rules.d.ts +16 -0
- package/dist/report/rules.js +30 -0
- package/dist/report/rules.js.map +1 -0
- package/dist/report/runtime-impact.d.ts +38 -0
- package/dist/report/runtime-impact.js +94 -0
- package/dist/report/runtime-impact.js.map +1 -0
- package/dist/report/runtime.d.ts +54 -0
- package/dist/report/runtime.js +137 -0
- package/dist/report/runtime.js.map +1 -0
- package/dist/report/sarif.d.ts +2 -8
- package/dist/report/sarif.js +2 -10
- package/dist/report/sarif.js.map +1 -1
- package/dist/report/sorted-json.d.ts +1 -1
- package/dist/report/sorted-json.js +2 -2
- package/dist/report/sorted-json.js.map +1 -1
- package/docs/BRIDGE-EVENTS.md +87 -0
- package/docs/BRIDGE-MESSAGES.md +117 -0
- package/docs/GRAPH-EXCHANGE.md +382 -0
- package/docs/IMPACT.md +96 -0
- package/docs/MCP.md +62 -0
- package/docs/PREFLIGHT.md +311 -0
- package/docs/RUNTIME.md +172 -0
- package/docs/TOOLCHAIN.md +101 -0
- package/package.json +14 -2
- package/scripts/build-preflight-toolchain.mjs +186 -0
- package/scripts/capture-preflight.mjs +380 -0
- package/scripts/run-child.mjs +17 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# 브리지 사실 교환 형식 (버전 1)
|
|
2
|
+
|
|
3
|
+
isthmus 소유의 추가 입력/보고 계약은 [변경 사전 점검](IMPACT.md),
|
|
4
|
+
[언어 간 전이 분석과 수집](PREFLIGHT.md),
|
|
5
|
+
[런타임 통신 검증](RUNTIME.md)에 있다. 이들은 기존 bridge-facts v1 생산자 필드를
|
|
6
|
+
변경하지 않는다. 런타임에서 지원하는 transport를 정적 producer 지원으로 해석하지 않는다.
|
|
7
|
+
|
|
8
|
+
개발 중인 [BasicMessageChannel v2](BRIDGE-MESSAGES.md)와
|
|
9
|
+
[EventChannel v2](BRIDGE-EVENTS.md)는 별도 transport 문서다.
|
|
10
|
+
`check`는 v2 문서를 직접 소비해 transport별 진단 코드로 보고한다. `query`는 v2 경계를
|
|
11
|
+
`message`·`stream` kind 주체로, `graph`는 literal v2 경계를 `message`·`stream` 간선으로,
|
|
12
|
+
`diff`는 literal v2 경계의 추가·삭제와 v2 진단의 introduced/resolved를 싣는다.
|
|
13
|
+
`retentions`는 literal v2 경계의 Swift 핸들러를 method 없는 보존 근거로 다.
|
|
14
|
+
`preflight`는 선택적 context.messages로 소비한다. `impact`는 v1 전용으로 version 2를
|
|
15
|
+
명시적으로 거부한다 — 모르는 facts를 무시하고 초록 결과를 내지 않는다.
|
|
16
|
+
|
|
17
|
+
cartograph · kartograph · dartograph · isthmus 의 JS/TS 추출기가 **내보내고**, isthmus 가 **읽는** 형식. 이 문서가 바뀌면 네 저장소가 같이 바뀐다. 버전 1은 `experiments/phase-0/`의 Dart ↔ Swift 코퍼스를 양방향으로 조인해 검증했다.
|
|
18
|
+
|
|
19
|
+
## 원칙
|
|
20
|
+
|
|
21
|
+
- 각 도구는 **자기 언어에서 본 사실만** 낸다. 판정하지 않는다
|
|
22
|
+
- 위치는 항상 파일 · 줄 · 열. isthmus 의 모든 보고가 양쪽 위치를 가리켜야 한다
|
|
23
|
+
- 리터럴이 아닌 이름은 `dynamic: true` 로 표시하고 **버리지 않는다.** 한계를 세는 데 필요하다
|
|
24
|
+
- 키 순서는 정렬, 파일은 diff 가능해야 한다 (cartograph `GraphDocument` 와 같은 이유)
|
|
25
|
+
|
|
26
|
+
## 문서
|
|
27
|
+
|
|
28
|
+
```jsonc
|
|
29
|
+
{
|
|
30
|
+
"format": "bridge-facts",
|
|
31
|
+
"version": 1,
|
|
32
|
+
"tool": { "name": "dartograph", "version": "0.1.0" },
|
|
33
|
+
"generatedAt": "2026-09-04T12:00:00Z", // 신선도 판단용
|
|
34
|
+
"platform": "dart" | "swift" | "kotlin" | "js",
|
|
35
|
+
"target": "flutter" | "react-native" | "capacitor" | null, // 브리지 메커니즘
|
|
36
|
+
"project": "/abs/path", // POSIX realpath로 정규화한 절대 경로
|
|
37
|
+
"facts": [ Fact, ... ],
|
|
38
|
+
"limitations": [ "dynamic-channel-names: 3 channel constructors use a non-literal name", ... ]
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 선택적 limitation 스코프 (v1 확장)
|
|
43
|
+
|
|
44
|
+
기존 `limitations: string[]`는 유지한다. 생산자는 그 중 특정 항목의 **공백 전체**를
|
|
45
|
+
포함하는 채널 집합을 증명할 수 있을 때만 선택적 `limitationScopes`를 추가한다.
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
"limitations": ["opaque-handler-bodies: 1 handler body could not be inspected"],
|
|
49
|
+
"limitationScopes": [{"limitationIndex": 0, "channels": ["dev.example/camera"]}]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- `limitationIndex`는 같은 문서의 `limitations` 배열에 대한 0부터 시작하는 인덱스다.
|
|
53
|
+
항목별로 하나만 허용한다. 같은 접두사의 다른 항목과 다른 문서의 공백을 덮어쓰지 않는다.
|
|
54
|
+
- `channels`는 제어 문자가 없는 비어 있지 않은 채널 문자열의 비어 있지 않은 배열이다.
|
|
55
|
+
글롭·대소문자 접기·부분 문자열 매칭을 하지 않는다. 중복은 제거하고 문자열 순으로 정규화한다.
|
|
56
|
+
- 채널 이름을 일부 발견한 것만으로 스코프를 만들지 않는다. 해당 한계가 가릴 수 있는 모든
|
|
57
|
+
채널을 포함하는 **보수적 상한**이어야 한다. 동적 이름·미해석 위임·읽기 실패 때문에
|
|
58
|
+
상한을 증명할 수 없으면 그 항목의 스코프를 생략한다. ObjC 파일에서 채널 리터럴을
|
|
59
|
+
몇 개 읽었다는 것만으로 `objective-c-sources`의 범위를 좁히지 않는다.
|
|
60
|
+
- 스코프가 없는 항목은 기존 target 범위 전체에 적용한다. 스코프 있는 항목과 공존하면
|
|
61
|
+
범위 없는 공백이 우선하며, 다른 target과 호출 측 한계는 기존 규칙대로 처리한다.
|
|
62
|
+
`target: null`은 target을 추측하지 않고 모든 target의 해당 채널에 적용한다.
|
|
63
|
+
- 잘못된 인덱스, 중복 인덱스, 빈 채널 집합, 잘못된 타입은 입력 오류로 거부한다. 잘못된
|
|
64
|
+
스코프를 빈 공백으로 읽고 error를 만들지 않는다. 문서당 최대 1,000 스코프, 정규화 전
|
|
65
|
+
채널 원소 합계 최대 10,000개다. 파일 위치는 채널 집합의 대체물이 아니다.
|
|
66
|
+
- 문자열 끝의 `[channels: …]`는 문장일 뿐 파싱하지 않는다. JSON 문자열 인코딩이 쉼표·
|
|
67
|
+
대괄호·따옴표 이스케이프를 맡는다. 같은 채널 안의 플랫폼 조건은 이 범위로 구분하지 않는다.
|
|
68
|
+
- 옛 v1 소비자는 모르는 필드를 버리고 기존 문자열을 target 전체로 적용한다. 새 소비자는
|
|
69
|
+
조인 결과의 해당 `JoinLimitation.channels`에 범위를 보존하며 check/query/graph/diff에
|
|
70
|
+
전달한다. 스코프 배열 자체가 비었으면 추가 범위가 없다는 뜻이며 기존 전체 적용이다.
|
|
71
|
+
범위만 바뀌어도 diff에서 한계 변화로 보인다. isthmus의 직접 계수에는 선택적
|
|
72
|
+
`origin: "consumer"`를 붙이고, 이 필드는 생산 문서에서 복사하지 않는다. `unjoined-*`를 생산자가 신고해
|
|
73
|
+
소비자의 자체 계수를 덮어쓰는 것은 여전히 허용하지 않는다.
|
|
74
|
+
|
|
75
|
+
## Fact
|
|
76
|
+
|
|
77
|
+
공통 필드:
|
|
78
|
+
|
|
79
|
+
```jsonc
|
|
80
|
+
{
|
|
81
|
+
"kind": "channel-create" | "channel-register" | "method-invoke" | "method-handle"
|
|
82
|
+
| "module-export" | "module-import" | "component-export" | "component-require",
|
|
83
|
+
"channel": "com.example/camera", // 귀속할 수 없으면 null. dynamic 이면 원문 표현식
|
|
84
|
+
"method": "takePhoto", // method-* 에만
|
|
85
|
+
"mechanism": "expo", // module-*/component-* 에만(method-*와
|
|
86
|
+
// 상호 배타). 생략은 "core"
|
|
87
|
+
"optional": true, // module-import 에만 — 호출 API가
|
|
88
|
+
// 부재 시 null 반환을 허용한다는 증거
|
|
89
|
+
"dynamic": false,
|
|
90
|
+
"location": { "path": "lib/camera.dart", "line": 42, "column": 5 },
|
|
91
|
+
"symbol": { // 이 사실을 담고 있는 선언 (있으면)
|
|
92
|
+
"qualifiedName": "CameraPlugin.register",
|
|
93
|
+
"usr": "s:…" // 생산 도구의 안정 식별자. Phase 0 구문 실험에서는 생략 가능
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
선택적 `sourceLanguage: "objective-c"`는 `platform: "swift"` 문서에 담긴 `.m`/`.mm`의
|
|
99
|
+
**Objective-C 구현 사실**을 구분한다. 실제 Clang 인덱스에서 확인한 `c:` USR이 있으면
|
|
100
|
+
`usr`까지 함께 싣는다. 인덱스가 없거나 선언을 유일하게 확인하지 못하면 `usr`를 생략하고
|
|
101
|
+
구문이 아는 `qualifiedName`만 둘 수 있다 — Swift의 `missing-handler-usrs`와 같은
|
|
102
|
+
대칭이다. 합성 안정 식별자(SCIP 문법 등)를 지어 진짜 신원처럼 싣지 않는다.
|
|
103
|
+
`usr`가 있는데 `c:`로 시작하지 않으면 입력 오류다.
|
|
104
|
+
그 외 값·플랫폼·확장자 조합은 입력 오류다.
|
|
105
|
+
필드가 없으면 기존 플랫폼 의미를 유지한다. 위치 확장자만으로 Objective-C라고 추측하지 않는다.
|
|
106
|
+
`sourceLanguage`는 Swift 분석 그래프 밖의 Objective-C 구현을 선언하는 생산자의 자가 선언 필드이며, 소비자는 이를 신뢰한다(생산자 신뢰 전제). 생산자가 이 라벨을 오선언해 Swift 보존 fail-closed를 우회하는 것은 소비자의 정적 분석 범위 밖이다.
|
|
107
|
+
|
|
108
|
+
`method-handle`의 `symbol`은 문자열 `case` 자체가 아니라 그것을 감싸는 타입·함수 선언이다. Swift 클로저에는 USR이 없으므로 `qualifiedName`은 `CameraPlugin.register`처럼 감싸는 선언을 가리키고, `location`은 실제 `case` 문자열을 가리킨다. cartograph의 생산 구현은 인덱스와 결합해 `usr`까지 채워야 한다. 구문 실험처럼 `usr`을 채우지 못하면 `missing-handler-usrs`를 `limitations`에 싣는다.
|
|
109
|
+
|
|
110
|
+
### `method-handle`의 선택적 분기 근거 (v1 확장)
|
|
111
|
+
|
|
112
|
+
`method-handle` 사실은 선택적 `handlerScope`와 `dependencies`를 함께 실을 수 있다.
|
|
113
|
+
필드 형태·상한·완전성 의미는 [BRIDGE-MESSAGES](BRIDGE-MESSAGES.md)의 "handler별 의존
|
|
114
|
+
근거" 절과 같으며, 차이는 범위가 가리키는 것뿐이다. `handlerScope`는 감싸는 핸들러
|
|
115
|
+
선언 안에서 이 메서드로 귀속한 분기(예: `switch`의 `case "m"` 절, `if call.method == "m"`
|
|
116
|
+
의 참 분기)의 소스 범위다. `scope: "handler"` 의존은 그 분기 안의 관찰된 사용 관계이고,
|
|
117
|
+
`scope: "registration"` 의존은 감싸는 핸들러 선언 안에서 어떤 메서드 분기 범위에도
|
|
118
|
+
속하지 않는 공유 부분이다. Objective-C 사실(`sourceLanguage: "objective-c"`)은 분기
|
|
119
|
+
근거를 싣지 않는다.
|
|
120
|
+
|
|
121
|
+
소비자는 **한 `(channel, method)` 경로의 언어 심볼로 귀속 가능한 수신 사실이 전부
|
|
122
|
+
완전한 분기 근거를 가질 때만** 범위별 전파를 적용한다. 그때는 도달한
|
|
123
|
+
dependency/dispatch 후보에서만 해당 경계로 전파하고, 수신 선언과 등록
|
|
124
|
+
(`channel-register`) 위치는 직접 변경 대상으로 선택된 경우에만 경계를 연다.
|
|
125
|
+
귀속 불가능한 Objective-C 사실은 분기 근거를 가질 수 없으므로 이 판정에서 제외하고
|
|
126
|
+
기존처럼 공백 증거로 남는다. 근거가 없거나 불완전한 수신 사실이 하나라도 있으면
|
|
127
|
+
기존 넓은 후보를 보존하고 정밀도 공백을 알린다 — 등록 선언이 도달되면 그 안의
|
|
128
|
+
모든 경계를 보고하는 기존 동작이다. 옛 v1 소비자는 이 두 필드를 모르는 추가 필드로
|
|
129
|
+
제거하므로, 이 확장을 내는 생산자와 읽는 소비자의 배포 순서는 자유다.
|
|
130
|
+
|
|
131
|
+
`location.path`는 프로젝트 루트 기준 상대 경로다. 절대 경로, `..` 상위 이동, 제어 문자를 넣지 않는다.
|
|
132
|
+
`location.line`과 `location.column`은 1부터 시작하며, `column`은 해당 줄의 UTF-8 바이트
|
|
133
|
+
오프셋에 1을 더한 값이다. 생산자는 언어 런타임의 UTF-16 또는 Unicode scalar 열을 그대로
|
|
134
|
+
내보내지 않는다.
|
|
135
|
+
소비자는 이 조건을 어긴 문서를 거부해 로컬 경로 노출과 후속 출력 문법 오염을 막는다.
|
|
136
|
+
프로젝트 경로와 채널·메서드·심볼 이름에도 제어 문자를 넣지 않는다.
|
|
137
|
+
NEL(U+0085)과 Unicode 줄·문단 구분자(U+2028/U+2029)도 허용하지 않는다.
|
|
138
|
+
식별자(프로젝트 경로·위치 경로·채널·메서드·심볼 이름·도구 이름·스코프 채널)에 유효하지 않은 유니코드 코드 포인트인 짝 없는 서러게이트(lone surrogate, U+D800~U+DFFF)도 허용하지 않으며(소비자는 `toWellFormed()`로 검증), 이를 어긴 문서는 URI 인코딩 등 하류 출력 파이프라인의 오작동을 막기 위해 파싱 단계에서 거부한다.
|
|
139
|
+
`limitations` 문자열은 원인 설명이라 문장을 자유롭게 쓸 수 있고 소비자가 내용을
|
|
140
|
+
검증하지 않는다. 소비자의 텍스트 출력(DOT·Mermaid 주석 등)에는 제어 문자를
|
|
141
|
+
제거해 넣고, JSON 출력은 인코딩이 이스케이프를 맡는다.
|
|
142
|
+
`generatedAt`은 timezone이 명시된 ISO 8601 날짜·시각이어야 한다. 생산자는 입력 offset을
|
|
143
|
+
UTC로 변환하고 밀리초 세 자리의 `YYYY-MM-DDTHH:mm:ss.SSSZ` 형식으로 정규화한다.
|
|
144
|
+
소비자는 버전 1에 정의되지 않은 추가 필드를 검증 경계에서
|
|
145
|
+
제거하고, 위치의 줄·열은 1 이상의 안전한 정수만 허용한다.
|
|
146
|
+
|
|
147
|
+
### 이름 경계 사실의 선택적 `mechanism` 필드 (v1 확장)
|
|
148
|
+
|
|
149
|
+
`target: "react-native"` 문서 안에는 코어 RN 경로와 Expo Modules 경로가
|
|
150
|
+
공존한다 — Expo는 별도 target이 아니다. `requireNativeModule`의 해석 순서가
|
|
151
|
+
`expo.modules` → `NativeModulesProxy` → **`TurboModuleRegistry` 폴백**이라
|
|
152
|
+
코어 RN 모듈도 만족시키는데, 별도 target으로 나누면 이 폴백이 거짓 미수출
|
|
153
|
+
오류를 만든다. 대신 이름 경계 사실 네 종류(`module-import`·`module-export`·
|
|
154
|
+
`component-require`·`component-export`)에 선택적 `mechanism: "core" | "expo"`
|
|
155
|
+
를 둔다. 생략은 `core`다 — 이 필드가 없던 문서는 모두 코어 RN만 기술했다.
|
|
156
|
+
|
|
157
|
+
- 생산자는 관찰한 API가 어느 경로로 해석되는지 알 때만 실는다. 어떤 경로인지
|
|
158
|
+
알 수 없는 사실에는 필드를 생략해(=`core`) 추측을 싣지 않는다.
|
|
159
|
+
- 허용 값은 `"core"`·`"expo"`뿐이고 다른 target 문서의 사실에는 실을 수 없다.
|
|
160
|
+
소비자는 잘못 놓인 mechanism을 모르는 필드로 버리지 않고 문서를 거부한다 —
|
|
161
|
+
추가 필드 제거는 정의되지 않은 필드에만 적용된다.
|
|
162
|
+
- 옛 소비자는 모르는 추가 필드로 버린다 — 기존 `(target, 이름)` 조인은 유지되고
|
|
163
|
+
mechanism 불일치 구분만 사라진다.
|
|
164
|
+
|
|
165
|
+
### `module-import`의 선택적 `optional` 필드 (v1 확장)
|
|
166
|
+
|
|
167
|
+
호출 측 API마다 모듈 부재 의미가 다르다. `requireOptionalNativeModule`·
|
|
168
|
+
`TurboModuleRegistry.get`·`getNullable`은 부재 시 던지지 않고 `null`을
|
|
169
|
+
돌려주지만, `requireNativeModule`·`getEnforcing`·`NativeModules.X` 접근은
|
|
170
|
+
부재를 호출자가 감당한다는 신호가 아니거나 그대로 크래시다. 부재를 허용하는
|
|
171
|
+
API로 관찰한 `module-import`에만 `optional: true`를 실을 수 있다.
|
|
172
|
+
|
|
173
|
+
- 미수출 그룹의 호출자가 **전부** `optional`이면 `module-import-without-export`
|
|
174
|
+
error 대신 `module-import-without-export-optional` warning으로 내린다 —
|
|
175
|
+
부재가 호출자에게 관찰 가능한 정상 경로다. 던지는 호출자가 하나라도
|
|
176
|
+
섞이면 그 호출 지점은 부재 시 크래시하므로 error를 유지한다.
|
|
177
|
+
- 다른 종류의 사실에는 실을 수 없고 `true`가 아닌 값은 문서 거부다.
|
|
178
|
+
옛 소비자는 모르는 필드로 버린다 — 미수출은 종전대로 error로 읽힌다.
|
|
179
|
+
|
|
180
|
+
`channel: null`은 `method-handle`에서만 허용하며, "채널이 없다"가 아니라 생산자가
|
|
181
|
+
핸들러를 어느 채널에 귀속할지 **모른다**는 뜻이다. 소비자는 이 사실을 조인하지 않고,
|
|
182
|
+
호출 없는 핸들러 같은 불일치에도 포함하지 않는다. 생산자는 그 수와 원인을 정확히
|
|
183
|
+
`unattributed-method-handles:`로 시작하는 limitation으로 알려야 하며, 없으면 소비자는
|
|
184
|
+
문서를 거부한다.
|
|
185
|
+
|
|
186
|
+
FFI·JNI 등 채널 계약 밖의 네이티브 interop은 fact로 만들지 않는다 — 심볼 이름 조인은
|
|
187
|
+
런타임 결정 구조라 정적 채널 키로 귀속할 수 없다. 대신 생산자는 소스에서 interop
|
|
188
|
+
근거(dart:ffi 계열 import, `@_cdecl`·Dart C API·dlsym, `external fun`·`System.loadLibrary`·
|
|
189
|
+
`native` 메서드·JNI export 이름)를 관측하면 `unscanned-ffi-interop:`로 시작하는
|
|
190
|
+
limitation에 파일 수를 실어 알린다. 이 라벨은 정보성이다 — 파일 수준 표식만으로는
|
|
191
|
+
어느 채널의 호출·핸들러가 interop으로 가려졌는지 귀속할 수 없으므로 소비자의 공백
|
|
192
|
+
심각도를 바꾸지 않고 그대로 전달한다. 어느 문서에나 실을 수 있다.
|
|
193
|
+
|
|
194
|
+
### 종류별 의미
|
|
195
|
+
|
|
196
|
+
| kind | 누가 내는가 | 뜻 |
|
|
197
|
+
|---|---|---|
|
|
198
|
+
| `channel-create` | Dart / JS | 호출하는 쪽이 채널 객체를 만들었다 |
|
|
199
|
+
| `channel-register` | Swift / Kotlin | 받는 쪽이 채널에 핸들러를 달았다 (`setMethodCallHandler`). 위치도 생성자가 아니라 이 호출을 가리킨다 |
|
|
200
|
+
| `method-invoke` | Dart / JS | `invokeMethod('m')` 호출 |
|
|
201
|
+
| `method-handle` | Swift / Kotlin | 핸들러 안에서 `case "m":` 또는 동등한 분기 |
|
|
202
|
+
| `module-export` | Swift / Kotlin | RN `RCT_EXPORT_MODULE(Name)`, `@ReactModule(name=)`; Expo `Module` DSL `Name("N")` |
|
|
203
|
+
| `module-import` | JS | `NativeModules.Name`, `TurboModuleRegistry.get('Name')`; Expo `requireNativeModule`·`requireOptionalNativeModule` |
|
|
204
|
+
| `component-export` | Swift / Kotlin | RN `RCT_EXPORT_VIEW_PROPERTY` 등 뷰 매니저; Expo `View(V.self)` DSL |
|
|
205
|
+
| `component-require` | JS | `requireNativeComponent('Name')`; Expo `requireNativeViewManager('Name')` |
|
|
206
|
+
|
|
207
|
+
RN 의 메서드는 `method-invoke`(JS: `NativeModules.Name.method()`) / `method-handle`(네이티브: `RCT_EXPORT_METHOD(method:)`, `@ReactMethod fun method`) 로 같은 종류를 쓴다. `channel` 자리에 모듈 이름이 들어간다.
|
|
208
|
+
|
|
209
|
+
`module-*`과 `component-*`는 이름 기반으로 조인된다. isthmus는 JS 호출 측과
|
|
210
|
+
Swift/Kotlin 수신 측의 같은 이름을 (target, `channel`=모듈·컴포넌트 이름) 키로
|
|
211
|
+
연결한다. JS 측은 내장 추출기 `isthmus extract-js`가 무의존 토큰 스캔으로 낸다 —
|
|
212
|
+
`NativeModules.X`·`NativeModules['X']`·`TurboModuleRegistry.get*('X')`·
|
|
213
|
+
`requireNativeModule`/`requireOptionalNativeModule`·`requireNativeComponent`·
|
|
214
|
+
`codegenNativeComponent`·`requireNativeViewManager`와 같은 파일·상대 import·
|
|
215
|
+
`export { A as B } from` 형태의 배럴 재수출(4홉 상한) 범위의 바인딩 해석,
|
|
216
|
+
그리고 확정된 모듈 식의 멤버 호출(`method-invoke`)까지 읽는다.
|
|
217
|
+
비리터럴 이름·메서드는 원문 표현식을 실은 `dynamic: true` 사실로 보존하고,
|
|
218
|
+
계약이 허용하지 않는 리터럴(빈 이름·제어 문자 포함)도 정적 이름이 아니라
|
|
219
|
+
동적 사실로 내린다. Expo 전용 API 이름(`requireNativeModule`·
|
|
220
|
+
`requireOptionalNativeModule`·`requireNativeViewManager`)은 Expo 패키지
|
|
221
|
+
specifier의 import·`import { api as alias }` 별칭·CJS
|
|
222
|
+
`require('expo…')` 바인딩으로 확인되거나, 어떤 가져오기·로컬 선언도 없이
|
|
223
|
+
호출되면 `mechanism: "expo"`를 싣는다 — 코어 RN에는 같은 이름의 진입점이
|
|
224
|
+
없다. 반대로 같은 이름이 로컬에 선언됐거나(같은 파일 래퍼·쉼) Expo가 아닌
|
|
225
|
+
specifier에서 가져온 동명 래퍼면 해석 경로를 알 수 없어 mechanism을
|
|
226
|
+
생략하고, 같은 이름의 매개변수가 가리는 호출도 생략한다.
|
|
227
|
+
`function NAME(...)` 선언부는 호출로 읽지 않는다. 부재를 허용하는 조회
|
|
228
|
+
(`requireOptionalNativeModule`, `TurboModuleRegistry.get`·`getNullable`)로
|
|
229
|
+
관찰한 `module-import`에는 `optional: true`를 싣고, 던지는 조회
|
|
230
|
+
(`requireNativeModule`, `getEnforcing`)·`NativeModules.X` 접근·컴포넌트
|
|
231
|
+
require에는 싣지 않는다.
|
|
232
|
+
스캔 집합을 벗어난 바인딩(패키지 import, 함수 결과,
|
|
233
|
+
인스턴스 상태)은 `limitations`로만 보고한다 — 정적 이름을 추측해 연결하지
|
|
234
|
+
않는다. 함수·메서드·`{…}` 본문을 가진 화살표의 매개변수는 그 본문 안에서
|
|
235
|
+
파일 바인딩을 가리는 것으로 처리하지만, 식 본문 화살표(`M => M.x()`)·
|
|
236
|
+
`for`/`catch` 등 선언문 밖의 바인딩·`export * from` 재수출은 추적하지 않는다.
|
|
237
|
+
토큰 스캔은 완전한 JS 의미 해석이 아니므로 이 추출기의 출력은 관찰 범위의
|
|
238
|
+
근거다.
|
|
239
|
+
|
|
240
|
+
## 조인 규칙 (isthmus 가 적용)
|
|
241
|
+
|
|
242
|
+
- `channel-create` ↔ `channel-register`: `channel` 이 같다. 플랫폼별로 따로 맞춘다 (Swift 와 Kotlin 이 각각 등록하는 것이 정상)
|
|
243
|
+
- 생성 없는 `channel-register`는 호출 측 사용을 찾지 못한 경고로 보존한다
|
|
244
|
+
- `method-invoke` ↔ `method-handle`: `(channel, method)` 가 같다
|
|
245
|
+
- `module-import` ↔ `module-export`: `(target, channel=모듈 이름)`이 같고
|
|
246
|
+
mechanism이 도달 가능해야 한다. `mechanism: "expo"`인 import는
|
|
247
|
+
TurboModuleRegistry 폴백이 있어 core·expo export 모두와 잇고,
|
|
248
|
+
core(생략 포함) import는 core export만 만족시킨다. export를 찾지 못한
|
|
249
|
+
import는 error, import를 찾지 못한 export는 warning이다. 다만 미수출
|
|
250
|
+
그룹의 호출자가 전부 `optional`이면(부재 시 `null`을 돌려주는 API로만
|
|
251
|
+
관찰) error 대신 `module-import-without-export-optional` warning이다.
|
|
252
|
+
같은 이름의
|
|
253
|
+
export가 mechanism만 다르게 관찰된 호출은 error가 아니라
|
|
254
|
+
`module-import-mechanism-mismatch` warning이다 — 코어 호출이 Expo export에
|
|
255
|
+
실제로 도달하는지의 상호운용은 아직 미해결이다. 반대 방향도 같다 —
|
|
256
|
+
호출이 mechanism만 다르게 관찰된 export는 `module-export-mechanism-mismatch`
|
|
257
|
+
warning이다. 불일치 진단은 양쪽 증거 위치를 함께 실는다
|
|
258
|
+
- `component-require` ↔ `component-export`: `(target, channel=컴포넌트 이름)`이
|
|
259
|
+
같고 mechanism이 같아야 한다 — `requireNativeViewManager`에는 모듈과 같은
|
|
260
|
+
폴백이 없다. export를 찾지 못한 require는 error, require를 찾지 못한
|
|
261
|
+
export는 warning이다. 코어 require×expo export처럼 상호운용이 미해결인
|
|
262
|
+
불일치만 `component-require-mechanism-mismatch` warning이다; expo
|
|
263
|
+
require에 코어 export만 관찰된 경우는 확정된 미수출로 error를 유지한다.
|
|
264
|
+
export 쪽의 대칭 불일치는 `component-export-mechanism-mismatch` warning이다
|
|
265
|
+
- 한 이름 아래 mechanism이 섞이면 그룹 전체가 아니라 호출·수신 증거 쌍 단위로
|
|
266
|
+
판정한다. 만족한 호출자와 도달한 수신자만 매치로 고정하고 나머지는 각각
|
|
267
|
+
미수출·미호출 증거로 남기므로, 한 이름이 매치·미수출·미호출 결과 둘 이상에
|
|
268
|
+
동시에 나타날 수 있다
|
|
269
|
+
- `dynamic: true`이거나 `channel: null`인 사실은 조인하지 않고 `limitations`로 센다. 조인할 수 없다는 이유로 불일치라고 판정하지 않는다.
|
|
270
|
+
세는 주체는 소비자다. isthmus는 조인에서 제외한 dynamic 사실을 직접 세어 자신을 출처(`tool: "isthmus"`)로 밝힌 limitation으로 내보내며, 같은 위치의 중복 사실은 한 번만 센다. 생산자의 `dynamic-*` limitation은 원인을 설명하는 추가 정보이지 소비자가 신뢰의 근거로 삼는 값이 아니다. `channel: null` 핸들러도 같다. 생산자의 `unattributed-method-handles:` 신고가 없으면 문서를 거부하지만, 신고한 개수는 검증하지 않고 소비자가 실제 사실 수를 다시 센다
|
|
271
|
+
- 수신 측이 스스로 신고한 분석 공백은 심각도에 반영한다. 소비자는 `objective-c-sources:`·`shadowed-flutter-method-channel:`(등록과 핸들러를 모두 가림), `opaque-handler-bodies:`(핸들러를 가림)를 수신 측 플랫폼 문서에서 발견하면 "핸들러 없는 호출"과 "등록 없는 채널 생성"을 error가 아니라 판정 불가(`-unverified` 경고)로 보고한다. 소비자가 직접 센 `unjoined-dynamic-methods`·`unjoined-unattributed-handlers`는 핸들러를, `unjoined-dynamic-channels`는 등록을, `unjoined-dynamic-exports`는 모듈·컴포넌트 export를 가리는 공백으로 본다 — 이 경우 "export 없는 import·require"도 error가 아니라 판정 불가(`-unverified` 경고)다. 알려진 접두사만 인정한다. `unjoined-` 접두사는 isthmus가 직접 세고 `origin: "consumer"`를 붙인 한계에만 유효하다. 이 출처는 입력 문서에서 복사하지 않는다. 생산자가 tool 이름을 isthmus로 적거나 같은 접두사를 차용해도 자체 계수의 근거가 되지 않는다. 모르는 한계를 공백으로 넓게 해석하면 진짜 불일치가 경고로 묻힌다. 호출 측 플랫폼의 한계는 네이티브 코드를 가리지 않으므로 심각도를 바꾸지 않는다.
|
|
272
|
+
이 접두사들은 계약이다. 생산자는 문구를 바꿀 때 접두사를 유지하고, 새 공백 종류를 추가하면 소비자의 목록도 함께 갱신한다. 목록이 닫혀 있으므로 갱신 전까지는 그 공백이 error로 보고된다(안전한 방향).
|
|
273
|
+
완화 단위는 진단의 target이다. 사실은 target별로만 조인되므로 target을 가진 수신 문서가 신고한 공백은 그 target 진단의 심각도만 낮춘다. `unjoined-dynamic-exports`는 소비자 계수라 채널 범위를 갖지 않아 같은 target의 미수출 진단 전체를 완화한다 — "어떤 수신 문서에도 export가 없다"는 판정은 한 수신 플랫폼의 동적 export 사실 하나로도 반증될 수 있으므로 플랫폼을 가르지 않는 것이 맞다. 단 특정 플랫폼에서만 export가 빠진 경우와 "어디에도 없다"를 이 진단은 구분하지 못한다. 사실이 없는(`target: null`) 수신 문서의 공백은 어느 target의 분석을 가리는지 귀속 근거가 없어 모든 target에 적용한다. 같은 target에 귀속된 수신 문서가 사실과 함께 공존해도 마찬가지다. 수신 문서 여러 개가 소스 트리를 나누어 가졌을 수 있어, 귀속 없는 문서가 본 소스가 해당 target의 핸들러를 가릴 가능성을 배제할 수 없기 때문이다. mixed-targets 문서의 한계도 선언한 target을 신뢰할 수 없어 귀속 없이 남긴다. 선택적 limitationScopes가 있으면 같은 target 안에서도 그 채널에만 적용한다. 범위가 없으면 기존 전체 적용을 유지한다. 같은 이유로 `objective-c-sources:`처럼 소비자가 직접 셀 수 없는 공백은 생산자의 신고를 그대로 믿는다. 과다 신고는 진짜 불일치를 경고로 묻고, 과소 신고는 거짓 error를 남긴다
|
|
274
|
+
- 위치는 증거이지 조인 키가 아니다. 같은 `(channel, method)` 사실이 여러 위치에 있어도 존재 여부는 키 집합으로 판단하고, 위치는 모두 증거로 보존한다
|
|
275
|
+
- 한 번의 조인에 넣는 모든 문서는 정확히 같은 `project` 문자열을 가져야 한다. 다른 프로젝트의 같은 채널 이름을 연결하지 않기 위해 불일치는 입력 오류로 거부한다
|
|
276
|
+
- 생산자는 `project`를 내보내기 전에 **POSIX realpath**(`realpath(3)`)로 정규화한다. 결과는 항상 symlink·`..`·중복 슬래시가 접힌 절대 경로다. 프로젝트 경로를 해결할 수 없거나 결과가 이 계약이 금지하는 제어 문자(NEL과 U+2028/U+2029 포함)를 포함하면 생산자는 문서를 내보내지 않고 실패한다 — 소비자에게 거부될 문서를 내보내지 않는다. 버전 1은 POSIX를 가정하며, Windows 정규화(드라이브 문자 대소문자, `\\?\` 접두사)는 Windows 지원 시 별도 합의한다. kartograph의 목표 기준은 JVM `Path.toRealPath()`다
|
|
277
|
+
- `project`는 생산자가 선언한 **조인 루트**다. 모든 사실의 `location.path`는 이 루트 기준 상대 경로이며, 모노레포에서 분석 루트와 조인 루트가 다르면 생산자가 위치를 조인 루트 기준으로 재기준화해 내보낸다. 생산 후에 문서의 `project`만 손으로 고쳐 쓰는 것은 조인 루트 선언이 아니다 — `location.path`가 다른 트리를 가리키게 되어 계약 위반이다. 선언 방법은 생산자 옵션이고 우선순위는 명시 옵션 > 자동 감지 > 분석 루트다. dartograph(0.5.0): `--project <shared-root>`는 스캔 범위를 위치 인자로 둔 채 `project`와 `location.path`를 공유 루트 기준으로 재기준화하고, 공유 루트는 realpath 정규화 후 package root를 포함하거나 동일해야 하며(위반은 사용 오류), pub workspace 자동 감지는 스캔 루트 pubspec의 `resolution: workspace` 선언 시 `workspace:` 키를 가진 가장 가까운 조상 pubspec 디렉터리(Melos의 워크스페이스 루트 정의와 동일)를 realpath로 채택한다. 자동 감지 실패(조상 루트 부재·pubspec 파싱 불가)는 분석 루트로 폴백하되 `pub-workspace-root-not-found`·`pub-workspace-pubspec-unparsed` limitation을 실어 조인 기준 어긋남을 조용히 넘기지 않는다 — 둘은 호출 측 한계라 isthmus는 심각도를 바꾸지 않고 그대로 전달한다. cartograph의 `--project`는 분석 루트 자체이므로 realpath 정규화 규칙만으로 이 정의를 만족한다. 조인 가능 여부는 소비자 설치본으로 왕복 실측했다(dartograph#38·#52: 모노레포 2패키지의 `project` 문자열 일치와 isthmus check 조인 성공, 옵션 없는 구행동 문서의 거부까지 양방향)
|
|
278
|
+
- 소비자는 정확한 문자열 일치를 유지하며 경로를 스스로 해결하지 않는다(isthmus는 JSON 파일만 읽는다). 소비자는 정규화 이행 여부를 검증할 수 없다 — 검증 가능한 것은 문서 간 `project` 문자열 일치뿐이고, 정규화 위반은 오직 조인 입력 오류로만 관측된다. realpath가 수렴시키는 것은 symlink·`..`·슬래시 축뿐이다. Unicode NFC/NFD 표기 차이, 대소문자 무시 파일시스템의 표기 차이, 마운트 별칭은 같은 디렉터리에 다른 문자열로 남고 불일치로 거부된다(안전하지만 디버깅이 필요하다). 근거: 같은 정규화가 없으면 macOS의 `/tmp`↔`/private/tmp`처럼 같은 디렉터리가 도구마다 다른 문자열이 된다(isthmus에서 재현). cartograph는 Foundation의 `resolvingSymlinksInPath().standardizedFileURL.path`가 `/private/tmp`을 `/tmp`으로 출력함을 실측하고 주입된 POSIX realpath를 채택했고(cartograph#73, 0.10.1 — 실측 입출력 쌍은 그 PR 본문 참조), dartograph의 `Directory.resolveSymbolicLinks()`는 POSIX에서 같은 기준을 만족한다
|
|
279
|
+
- 한 번의 조인 입력에는 호출 측 플랫폼(dart·js) 문서와 수신 측 플랫폼(swift·kotlin) 문서가 각각 최소 하나 있어야 한다. 한쪽만 있는 입력은 한쪽 관찰을 경계 불일치로 오독할 수 있으므로 소비자는 입력 오류로 거부한다. 사실이 없는 문서도 해당 플랫폼이 분석됐다는 근거로 인정한다
|
|
280
|
+
|
|
281
|
+
생산자는 채널 생성자와 핸들러 등록 사이의 변수 참조를 따라 채널 이름을 `channel-register`에 옮긴다. `FlutterMethodChannel` 객체를 만들기만 하고 핸들러를 달지 않은 코드는 등록 사실이 아니다.
|
|
282
|
+
|
|
283
|
+
### `target` 호환 규칙
|
|
284
|
+
|
|
285
|
+
- 사실이 없을 때만 문서의 `target`은 `null`이다
|
|
286
|
+
- 사실이 하나 이상이고 한 브리지 메커니즘만 담으면 그 값을 쓴다
|
|
287
|
+
- 버전 1에는 사실별 `target`이 없다. 한 Swift 프로젝트에 Flutter와 React Native 사실이
|
|
288
|
+
함께 있으면 생산자는 결정적인 대표값을 쓰고 정확히 `mixed-targets:`로 시작하는
|
|
289
|
+
limitation을 반드시 추가한다
|
|
290
|
+
- 소비자는 `mixed-targets` 문서에서 사실별 메커니즘을 복원할 수 없으므로 조인을 보류한다. 생산자는 위의 정확한 표기를 써야 하며, 소비자는 대소문자·앞 공백·콜론 누락처럼 명백한 변형도 fail-closed로 보류한다. 단, `non-mixed-targets`나 `mixed-targets-like`처럼 낱말 내부에 포함된 표기는 다른 의미의 산문이므로 보류 근거로 삼지 않고 단어 경계와 대소문자 무시(`(?<![\w-])mixed-targets(?![\w-])/i`)로 판정한다. CLI 명령은 빈 정상 결과를 내지 않고 도구 실패(종료 코드 2)를 반환한다. 안전한 혼합 프로젝트 지원은 문서를 target별로 나누거나 다음 형식 버전에 사실별 target을 추가한 뒤 제공한다
|
|
291
|
+
|
|
292
|
+
소비자는 `platform`과 fact 역할도 함께 검증한다. Dart/JS는 호출 측 종류만,
|
|
293
|
+
Swift/Kotlin은 수신 측 종류만 생산할 수 있다.
|
|
294
|
+
|
|
295
|
+
### 입력 자원 상한
|
|
296
|
+
|
|
297
|
+
- 한 명령은 최대 256개 교환 문서를 받는다
|
|
298
|
+
- 한 문서는 최대 100,000개 fact를 담는다
|
|
299
|
+
- CLI는 파일 하나당 UTF-16 문자열 길이 16Mi, 전체 64Mi를 넘으면 파싱 전에 거부한다
|
|
300
|
+
- 경계 그래프의 Cartesian 간선은 최대 100,000개다
|
|
301
|
+
|
|
302
|
+
## 되돌려 주는 형식: 외부 보존 근거
|
|
303
|
+
|
|
304
|
+
isthmus `retentions --for <tool>` 의 출력. 자매 도구의 `--external-retentions <path>` 가 읽는다.
|
|
305
|
+
|
|
306
|
+
```jsonc
|
|
307
|
+
{
|
|
308
|
+
"format": "external-retentions",
|
|
309
|
+
"version": 0,
|
|
310
|
+
"producedBy": { "name": "isthmus", "version": "x.y.z" },
|
|
311
|
+
"generatedAt": "…",
|
|
312
|
+
"retentions": [
|
|
313
|
+
{
|
|
314
|
+
"symbol": { "usr": "s:…", "qualifiedName": "CameraPlugin.takePhoto" },
|
|
315
|
+
"reason": "bridge",
|
|
316
|
+
"evidence": {
|
|
317
|
+
"channel": "com.example/camera",
|
|
318
|
+
"method": "takePhoto",
|
|
319
|
+
"caller": { "platform": "dart", "path": "lib/camera.dart", "line": 42 },
|
|
320
|
+
"callers": [
|
|
321
|
+
{ "platform": "dart", "path": "lib/camera.dart", "line": 42 },
|
|
322
|
+
{ "platform": "dart", "path": "lib/widget.dart", "line": 7 }
|
|
323
|
+
],
|
|
324
|
+
"callersOmitted": 3
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
자매 도구는 이것을 `RetentionReason.externalBridge` 로 매핑하고, `--explain` 에서 `evidence` 를 그대로 문장으로 만든다.
|
|
332
|
+
|
|
333
|
+
- `caller` 은 대표 호출 위치다. 결정적 순서(플랫폼·경로·줄·열)의 첫 호출이며
|
|
334
|
+
v0 초안부터 있던 필드라 옛 소비자가 계속 읽는다.
|
|
335
|
+
- 선택 `callers` 는 이 근거의 **전체** 호출 위치(대표 포함)를 같은 결정적 순서로
|
|
336
|
+
실는다. 호출이 둘 이상일 때만 두어, 호출이 하나인 근거는 기존 문서와 바이트가
|
|
337
|
+
같다. 근거당 상한은 100개이며, 문서 전체의 호출 위치 총상한은 1,000,000개다.
|
|
338
|
+
- 상한을 넘은 호출은 조용히 버리지 않고 선택 `callersOmitted` (비음수 정수,
|
|
339
|
+
0이면 생략)로 밝힌다. `omittedObjectiveCHandlers` 와 같은 계수 공개 원칙이다.
|
|
340
|
+
- 소비 도구는 모르는 필드를 무시한다(Swift `JSONDecoder` 의 기본 동작). 그래서
|
|
341
|
+
이 확장은 생산자(isthmus)를 먼저 배포해도 안전하고, 소비 도구가 `callers` 를
|
|
342
|
+
문장으로 치는 것은 별도 구현 사항이다.
|
|
343
|
+
- v2 Basic·Event 경계의 보존 근거에는 메서드가 없다. literal로 확정된
|
|
344
|
+
`message-handle`·`stream-handle`의 Swift 심볼을 `evidence.channel`과 호출자만으로
|
|
345
|
+
싣고 `method`를 생략한다 — 자매 도구의 `Evidence.method`도 선택 필드다. dynamic
|
|
346
|
+
prefix 후보·ObjC v2 핸들러는 v1과 같은 규칙으로 제외하고, ObjC 수는
|
|
347
|
+
`omittedObjectiveCHandlers`에 함께 센다.
|
|
348
|
+
|
|
349
|
+
cartograph의 보존 문서는 **Swift 그래프 선언**을 대상으로 완전해야 한다. 명시적
|
|
350
|
+
`sourceLanguage: "objective-c"` 구현은 조인·진단·query의 증거로 남기지만 Swift 보존 대상은
|
|
351
|
+
아니므로 그 목록에 넣지 않는다. Kotlin 핸들러를 cartograph 보존에서 제외하는 것과 같은
|
|
352
|
+
범위 구분이며, symbol 없는 Swift 핸들러를 조용히 버리는 예외가 아니다. ObjC가 Swift로
|
|
353
|
+
위임하는 관계는 별도 증거가 필요하고, ObjC 핸들러 이름으로 Swift USR을 만들지 않는다.
|
|
354
|
+
`omittedObjectiveCHandlers`(선택적 비음수 정수)에 목록에서 제외한 매치 ObjC 핸들러 수를
|
|
355
|
+
(target, channel, method, source location)별로 센다. 0이면 키를 생략한다. cartograph는
|
|
356
|
+
이 수를 외부 보존 근거의 한계로 알려 빈 목록을 Swift와 ObjC 전체의 보존 결과로 오인하지 않게 한다.
|
|
357
|
+
이 필드를 모르는 옛 isthmus는 ObjC 매치도 심볼 없는 Swift로 보아 retentions에서 실패한다.
|
|
358
|
+
따라서 ObjC 사실을 내는 생산자보다 이 확장을 지원하는 소비자를 먼저 배포한다.
|
|
359
|
+
|
|
360
|
+
이 문서는 대상 범위 안에서 부분적으로 만들지 않는다. 소비 도구가 읽을 수 있는 수신 측 문서가 입력에 없거나, 호출자가 있는데도 `symbol`이 없어 근거로 바꿀 수 없는 매치 핸들러가 있으면 isthmus는 일부만 담은 목록 대신 도구 실패(종료 코드 2)로 끝낸다. 근거가 빠진 목록은 소비자에게 살아 있는 핸들러를 미사용으로 보이게 하기 때문이다. 문서 전체 호출 위치 총상한(1,000,000개)을 초과한 경우에도 부분 근거를 내지 않고 입력을 좁히도록 안내하며 종료 코드 2로 실패한다.
|
|
361
|
+
|
|
362
|
+
## 자매 도구가 해야 할 일 (선행 작업)
|
|
363
|
+
|
|
364
|
+
| 도구 | 명령 | 낼 것 | 읽을 것 |
|
|
365
|
+
|---|---|---|---|
|
|
366
|
+
| cartograph | `bridges --format json` | Swift 의 `FlutterMethodChannel(name:)`, `setMethodCallHandler`, `case "…"`, `RCT_EXPORT_*`, `@objc(…)` | `--external-retentions` |
|
|
367
|
+
| dartograph | `bridges --format json` | `MethodChannel(…)`, `invokeMethod(…)`, Pigeon 산출물 | (없음 — Dart 쪽이 부르는 쪽) |
|
|
368
|
+
| kartograph | `bridges --format json` | `MethodChannel(…)`, `setMethodCallHandler`, `when (call.method)`, `@ReactModule`, `@ReactMethod` | `--external-retentions` |
|
|
369
|
+
| isthmus 내장 | `extract-js` | `NativeModules.*`, `TurboModuleRegistry.get*`, `requireNativeModule`, `requireNativeComponent` 계열, 바인딩 해석된 멤버 호출 | — |
|
|
370
|
+
|
|
371
|
+
**cartograph가 첫 번째 생산 구현이다.** PR #11에서 SwiftSyntax 스캐너와 `bridges --format json`이 버전 1로 구현됐다.
|
|
372
|
+
|
|
373
|
+
cartograph의 버전 1 구현은 `symbol.usr`을 붙이기 위해 인덱스 스토어를 요구한다. 인덱스가 없으면 불완전한 문서를 내보내지 않고 도구 실패(종료 코드 2)로 끝난다. 이는 문서 형식의 limitation이 아니라 생산 명령의 선행 조건이다.
|
|
374
|
+
|
|
375
|
+
## Phase 0 결정
|
|
376
|
+
|
|
377
|
+
- **Swift `case` 귀속**: 감싸는 타입·함수의 `qualifiedName`과, 생산 구현이 가진 안정 식별자(`usr`)를 `symbol`에 넣는다. 사실의 `location`은 `case` 문자열 위치다. Phase 0 SwiftSyntax 실험은 USR을 만들 수 없어 그 수를 `missing-handler-usrs`로 보고한다
|
|
378
|
+
- **Pigeon · Turbo Modules codegen**: 버전 1에는 별도 `codegen-resolved` 종류나 플래그를 추가하지 않는다. 생성 코드의 리터럴도 같은 채널·메서드 사실이고 조인 규칙이 같기 때문이다. 버전 1은 생성 여부를 계약에 싣지 않으며, 필요해지면 조인 키를 바꾸지 않는 선택 필드로 추가한다. 소비자는 경로만 보고 사용자 작성 코드라고 가정하지 않는다
|
|
379
|
+
- **한 단계 상수 추적**: `const kChannel = '…'`와 Swift `static let`처럼 같은 파일의 문자열 상수 한 단계는 정적 사실로 낸다. 그 이상이거나 보간된 표현식은 원문과 `dynamic: true`로 보존한다
|
|
380
|
+
- **구문 해석 한계**: Dart의 해석하지 못한 `invokeMethod` receiver는 `unresolved-receiver-invocations:`, Swift의 named-function handler는 `opaque-handler-bodies:` limitation으로 센다. 로컬 선언이 import된 `FlutterMethodChannel`을 가리면 `shadowed-flutter-method-channel:`로 알리고 사실 생성을 보류한다
|
|
381
|
+
- **Swift 조건부 컴파일**: Flutter를 import한 파일에 `#if`가 있으면 활성 구성을 추측하지 않고 compiler-indexed 추출이 필요하다고 실패한다
|
|
382
|
+
- **버전 1 승격**: `expected/dart.json`과 `expected/swift.json`을 실제 추출기로 만들고, 채널 1개·메서드 1개 연결, 핸들러 없는 호출 1개, 호출 없는 핸들러 2개를 `expected/join.json`으로 대조해 충족했다
|
package/docs/IMPACT.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# 변경 전 브리지 영향 점검
|
|
2
|
+
|
|
3
|
+
`impact`는 npm 0.7.0 발행본에 포함된 명령이다. checkout에서 개발할 때는
|
|
4
|
+
`npm ci && npm run build` 후 `node dist/cli/main.js impact ...`로 실행한다.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
isthmus impact --file ios/Runner/CameraPlugin.swift dart.json swift.json
|
|
8
|
+
isthmus impact --symbol 'CameraPlugin.handle' dart.json swift.json --compact
|
|
9
|
+
isthmus impact --changes changes.json dart.json swift.json --strict
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
파일·심볼·변경 목록 중 한 가지 방식만 선택한다. 파일은 producer의 `location.path`와
|
|
13
|
+
같은 프로젝트 상대 경로다. 심볼은 `qualifiedName` 또는 USR과 정확히 일치하며,
|
|
14
|
+
같은 이름이 여러 위치에 있으면 모두 보고한다. 선택 값에 경로 정규화·접두 매칭·rename
|
|
15
|
+
추론을 적용하지 않는다. 심볼의 전역 유일성을 이름만으로 주장하지 않는다.
|
|
16
|
+
|
|
17
|
+
## 변경 목록 입력
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"format": "isthmus-changes",
|
|
22
|
+
"version": 1,
|
|
23
|
+
"files": ["ios/Runner/CameraPlugin.swift", "lib/camera.dart"],
|
|
24
|
+
"symbols": ["CameraPlugin.handle"]
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
두 목록은 합집합이며 하나는 생략 가능하다. 정규화 전 합계 1~10,000개를 허용한다.
|
|
29
|
+
파일은 bridge-facts의 상대 경로 조건, 심볼은 안전한 비어 있지 않은 문자열 조건을 따른다.
|
|
30
|
+
중복 제거 후 문자열 순으로 정렬한다. 알 수 없는 필드는 버린다. 입력 파일당 16 MiB,
|
|
31
|
+
변경 목록과 bridge-facts 전체 합계 64 MiB UTF-16 텍스트 길이 예산을 적용한다.
|
|
32
|
+
|
|
33
|
+
## 결과와 해석
|
|
34
|
+
|
|
35
|
+
`isthmus-impact` v1은 다음을 보존한다.
|
|
36
|
+
|
|
37
|
+
- `project`: 증거 상대 경로의 기준. CLI 실행 디렉터리와 동일하다고 추측하지 않는다.
|
|
38
|
+
파일 존재를 확인하지 않은 snapshot 위치를 실제 로컬 파일 링크로 만들지 않는다.
|
|
39
|
+
|
|
40
|
+
- `selection` / `unmatchedSelectors`: 요청 목록과 관찰하지 못한 선택. 삭제된 코드는
|
|
41
|
+
삭제 전 snapshot으로 조회한다. 새 snapshot에서 없다는 것은 영향이 없다는 증거가 아니다.
|
|
42
|
+
- `selectedFacts`: 실제 선택된 위치·플랫폼·target·종류·심볼·동적 여부. 동적/미귀속 사실도
|
|
43
|
+
이 목록과 `summary.unresolvedSelectedFacts`에 남는다.
|
|
44
|
+
- `channels`: 관련 생성·등록 위치와 범위를 넓힌 `reason`.
|
|
45
|
+
- `methods`: 메서드별 Dart 호출과 Swift 핸들러. 생성·등록 변경은 그 채널의 모든 메서드로
|
|
46
|
+
넓힌다(`channel-wiring`). 메서드 변경은 해당 논리 키만 선택한다(`method`).
|
|
47
|
+
- `reviewFiles`: 선택 지점, 관련 호출·핸들러·배선 파일의 중복 없는 목록.
|
|
48
|
+
- `issues`: 기존 check 정책으로 계산한 관련 진단. 다른 채널의 진단은 섞지 않는다.
|
|
49
|
+
- `limitations` / `relevantLimitations`: 전체 한계와 선택에 관련된 한계. 스코프 없는 한계는
|
|
50
|
+
해당 target 전체에 적용한다. 미관찰·미해석 선택은 범위를 좁힐 수 없어 전체 한계를 보존한다.
|
|
51
|
+
- `inputs`: 생산 도구·버전·플랫폼·target·생성 시각. 파일 입력 순서와 무관하게 정렬한다.
|
|
52
|
+
|
|
53
|
+
`summary.observedFacts`는 전체 입력 fact 수, `selectedFacts`는 중복 제거한 선택 근거 수다.
|
|
54
|
+
`--compact`는 JSON 공백만 줄이며 근거·한계를 생략하지 않는다.
|
|
55
|
+
|
|
56
|
+
| 코드 | 의미 |
|
|
57
|
+
|---|---|
|
|
58
|
+
| 0 | 보고서 생성 성공. `unobserved`도 기본 모드에서는 보고한다. |
|
|
59
|
+
| 1 | `--strict`에서 관련 error·미검증 진단·관련 분석 한계·미해석 선택·미관찰 선택이 남음. |
|
|
60
|
+
| 2 | 읽기·JSON·교환 계약·입력 예산·조인 보류 등 실행 실패. 부분 보고서 없음. |
|
|
61
|
+
| 64 | 인수 또는 직접 입력한 파일/심볼 선택 오류. |
|
|
62
|
+
|
|
63
|
+
이 명령의 strict는 check strict보다 분석 공백에 엄격하다. 기존 check/diff 동작은 유지한다.
|
|
64
|
+
|
|
65
|
+
## 런타임 관찰 연결
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
isthmus impact --file lib/dynamic.dart dart.json swift.json \
|
|
69
|
+
--runtime runtime.json --revision current-source-revision --compact
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
두 옵션은 함께 지정한다. runtime의 project는 정적 입력과 같아야 하고 revision은 호출자가
|
|
73
|
+
선언한 분석 revision과 같아야 한다. 정적 bridge-facts v1에는 revision 필드가 없으므로,
|
|
74
|
+
CI/호출자가 같은 소스에서 사실을 생산했다는 문맥을 제공해야 한다. 소비자가 이를 추측하지 않는다.
|
|
75
|
+
|
|
76
|
+
선택된 호출 파일·정확한 fact 위치 또는 선택된 정적 주소와 관련된 런타임 이벤트를 모은다.
|
|
77
|
+
동적 Dart 호출에서 관찰된 구체적인 MethodChannel 주소로 Swift 핸들러 후보를 찾으면
|
|
78
|
+
`reason: runtime-observation`으로 관련 메서드와 검토 파일을 넓힌다. 정적 invocation 사실을
|
|
79
|
+
합성하지 않고 `runtime` 필드에 별도 근거를 둔다. 정적 미해석 수와 기존 진단은 그대로 남는다.
|
|
80
|
+
|
|
81
|
+
런타임 run의 platform이 ios/macos이고 transport가 MethodChannel인 경우에만 현재 Swift
|
|
82
|
+
정적 핸들러 후보를 찾는다. native 심볼이나 엔진이 실제 실행됐다고 확정하지 않는다.
|
|
83
|
+
BasicMessageChannel과 다른 OS는 `unsupported`, 정적 후보가 없으면 `unobserved`다.
|
|
84
|
+
revision이 오래되면 이벤트를 연결하지 않고 `stale`로 보존한다.
|
|
85
|
+
|
|
86
|
+
주소·인스턴스별 호출을 묶고 호출 위치는 20개까지만 보여 주며 `callersOmitted`를 기록한다.
|
|
87
|
+
`reviewFiles`는 표시 생략과 무관하게 관찰한 전체 관련 파일을 유지한다. 런타임 실패·대기·
|
|
88
|
+
중단·유실·오래된 기록·관련 관찰 부재·정적 후보 공백도 strict 실패 조건이다.
|
|
89
|
+
|
|
90
|
+
## 현재 경계
|
|
91
|
+
|
|
92
|
+
`scope: "bridge"`, `complete: false`는 항상 명시한다. 현재 기능은 브리지에 직접 등장한
|
|
93
|
+
코드의 변경 범위를 찾는다. 언어 내부 helper→handler→Dart 화면으로 이어지는 전이 경로,
|
|
94
|
+
실제 엔진·등록 수명과 테스트 시나리오의 포괄성은 아직 검증하지 않는다. 런타임 관찰은
|
|
95
|
+
위 방식으로 후보를 넓히는 근거이며, 전체 의존성의 완전성이나 실제 핸들러 실행 증명은 아니다.
|
|
96
|
+
이들은 [전체 목표](COMPETITIVENESS.md)의 남은 구현이며 이 명령의 존재로 완료 처리하지 않는다.
|
package/docs/MCP.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# MCP 서버 (`isthmus serve`)
|
|
2
|
+
|
|
3
|
+
`isthmus serve`는 stdio 위에서 MCP(Model Context Protocol)를 말한다.
|
|
4
|
+
코딩 에이전트(Claude Code, Devin, 호환 클라이언트)가 이 프로세스를 자식으로 띄우고,
|
|
5
|
+
일곱 개의 분석 도구를 JSON-RPC `tools/call`로 호출한다. 각 도구는 대응하는 CLI
|
|
6
|
+
명령과 정확히 같은 실행 경로·보고서 형식·한계 보고를 재사용한다 — MCP 응답에
|
|
7
|
+
담기는 문서는 파이프로 받은 CLI 출력과 동일하다.
|
|
8
|
+
|
|
9
|
+
## 실행과 연결
|
|
10
|
+
|
|
11
|
+
서버는 인자 없이 시작하고, 한 줄에 JSON-RPC 메시지 하나씩 주고받는다(newline-delimited).
|
|
12
|
+
stdin이 닫히면 종료 코드 0으로 끝난다. 세션 상태를 쌓지 않으므로 재시작해도 잃는 것이 없다.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
isthmus serve # MCP stdio 서버로 실행
|
|
16
|
+
isthmus serve --verbose # usage 64 — 플래그는 없다
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
클라이언트 설정 예시(개념적 — 클라이언트별 키 이름은 다르다):
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{ "command": "isthmus", "args": ["serve"] }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
지원 프로토콜 버전은 `2025-06-18`·`2025-03-26`·`2024-11-05`다. `initialize`가
|
|
26
|
+
요청한 버전을 지원하면 그대로 협상하고, 모르는 버전에는 최신 지원 버전을 제안한다.
|
|
27
|
+
`notifications/*` 알림에는 응답하지 않고, 배치 요청은 거부한다(-32600).
|
|
28
|
+
|
|
29
|
+
## 도구
|
|
30
|
+
|
|
31
|
+
모든 도구의 `documents`는 GRAPH-EXCHANGE v1/v2 브리지 사실 문서 경로 2개 이상이다.
|
|
32
|
+
경로는 서버 프로세스의 작업 디렉터리 기준으로 해석된다 — 클라이언트는 읽을 수 있는
|
|
33
|
+
경로만 넘겨야 한다.
|
|
34
|
+
|
|
35
|
+
| 도구 | 대응 명령 | 선택 인자 |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `check` | `isthmus check` | `strict` |
|
|
38
|
+
| `query` | `isthmus query` | `name`(필수) |
|
|
39
|
+
| `graph` | `isthmus graph` | `format: json\|dot\|mermaid` |
|
|
40
|
+
| `diff` | `isthmus diff` | `before`/`after`(필수), `strict` |
|
|
41
|
+
| `impact` | `isthmus impact` | `file`·`symbol`·`changes` 중 정확히 하나(필수), `runtime`, `revision`, `strict`, `compact` |
|
|
42
|
+
| `preflight` | `isthmus preflight` | `context`(필수), `runtime[]`, `expectations`, `revision`, `summary`, `limit`, `explain`, `strict`, `compact` |
|
|
43
|
+
| `retentions` | `isthmus retentions` | `producer`(필수, 현재 `cartograph`만) |
|
|
44
|
+
|
|
45
|
+
## 응답 의미
|
|
46
|
+
|
|
47
|
+
- 보고서 JSON은 `content[0].text`에 문자열로 담긴다 — CLI stdout과 동일한 문서다.
|
|
48
|
+
- 명령의 stderr(질의 힌트·원인 메시지)가 있으면 두 번째 text 블록으로 뒤따른다.
|
|
49
|
+
- `isError`는 **문서를 만들지 못한 실패**(usage 64, 입력/내부 2)에만 세운다.
|
|
50
|
+
`query`의 `notFound`(64)나 `check --strict`의 발견(1)은 정답 문서를 실은 정상
|
|
51
|
+
응답이다 — 문서 안의 `status`·`summary`·`limitations`가 판정 근거다.
|
|
52
|
+
- 발견·한계·미해석 사실은 CLI와 같은 규칙으로 보존된다. `isError: false`와 빈
|
|
53
|
+
결과를 완전성의 증거로 읽지 않는다 — `limitations`를 함께 본다.
|
|
54
|
+
|
|
55
|
+
## 범위와 주의점
|
|
56
|
+
|
|
57
|
+
- 서버는 호출자의 파일 읽기 권한을 그대로 상속한다 — MCP 표면 자체에는 인증이 없다.
|
|
58
|
+
부모 에이전트가 이미 읽을 수 있는 파일만 도구 인자로 넘기는 모델이며, 네트워크
|
|
59
|
+
노출은 하지 않는다(stdio 전용).
|
|
60
|
+
- `--update-baseline`·`--output` 같은 파일 쓰기 경로는 도구 인자로 열지 않았다.
|
|
61
|
+
- 이것은 제품 명령의 **트랜스포트**다. 새 분석이나 새 보고서 형식을 추가하지 않고,
|
|
62
|
+
교환 계약(GRAPH-EXCHANGE)의 범위를 넓히지도 않는다.
|