isthmus-cli 0.6.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 +135 -31
- package/README.md +134 -16
- package/Skills/isthmus/SKILL.md +17 -7
- 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 +19 -1
- package/dist/cli/command-support.js +37 -8
- 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/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 +52 -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/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/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/messages.d.ts +9 -24
- package/dist/exchange/messages.js +25 -55
- package/dist/exchange/messages.js.map +1 -1
- package/dist/exchange/parse.d.ts +61 -0
- package/dist/exchange/parse.js +115 -12
- package/dist/exchange/parse.js.map +1 -1
- 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/messages.d.ts +7 -4
- package/dist/join/messages.js +15 -8
- package/dist/join/messages.js.map +1 -1
- 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 -11
- 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/preflight-view.js +28 -1
- package/dist/report/preflight-view.js.map +1 -1
- package/dist/report/preflight.d.ts +4 -4
- package/dist/report/preflight.js +81 -46
- package/dist/report/preflight.js.map +1 -1
- 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/sarif.d.ts +2 -8
- package/dist/report/sarif.js +2 -10
- package/dist/report/sarif.js.map +1 -1
- package/docs/BRIDGE-EVENTS.md +87 -0
- package/docs/BRIDGE-MESSAGES.md +23 -8
- package/docs/GRAPH-EXCHANGE.md +141 -14
- package/docs/IMPACT.md +1 -1
- package/docs/MCP.md +62 -0
- package/docs/PREFLIGHT.md +24 -8
- package/docs/RUNTIME.md +2 -2
- package/docs/TOOLCHAIN.md +5 -5
- package/package.json +5 -2
- package/scripts/capture-preflight.mjs +32 -44
- package/scripts/run-child.mjs +1 -0
package/docs/GRAPH-EXCHANGE.md
CHANGED
|
@@ -5,8 +5,14 @@ isthmus 소유의 추가 입력/보고 계약은 [변경 사전 점검](IMPACT.m
|
|
|
5
5
|
[런타임 통신 검증](RUNTIME.md)에 있다. 이들은 기존 bridge-facts v1 생산자 필드를
|
|
6
6
|
변경하지 않는다. 런타임에서 지원하는 transport를 정적 producer 지원으로 해석하지 않는다.
|
|
7
7
|
|
|
8
|
-
개발 중인 [BasicMessageChannel v2](BRIDGE-MESSAGES.md)
|
|
9
|
-
|
|
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를 무시하고 초록 결과를 내지 않는다.
|
|
10
16
|
|
|
11
17
|
cartograph · kartograph · dartograph · isthmus 의 JS/TS 추출기가 **내보내고**, isthmus 가 **읽는** 형식. 이 문서가 바뀌면 네 저장소가 같이 바뀐다. 버전 1은 `experiments/phase-0/`의 Dart ↔ Swift 코퍼스를 양방향으로 조인해 검증했다.
|
|
12
18
|
|
|
@@ -76,6 +82,10 @@ cartograph · kartograph · dartograph · isthmus 의 JS/TS 추출기가 **내
|
|
|
76
82
|
| "module-export" | "module-import" | "component-export" | "component-require",
|
|
77
83
|
"channel": "com.example/camera", // 귀속할 수 없으면 null. dynamic 이면 원문 표현식
|
|
78
84
|
"method": "takePhoto", // method-* 에만
|
|
85
|
+
"mechanism": "expo", // module-*/component-* 에만(method-*와
|
|
86
|
+
// 상호 배타). 생략은 "core"
|
|
87
|
+
"optional": true, // module-import 에만 — 호출 API가
|
|
88
|
+
// 부재 시 null 반환을 허용한다는 증거
|
|
79
89
|
"dynamic": false,
|
|
80
90
|
"location": { "path": "lib/camera.dart", "line": 42, "column": 5 },
|
|
81
91
|
"symbol": { // 이 사실을 담고 있는 선언 (있으면)
|
|
@@ -97,6 +107,27 @@ cartograph · kartograph · dartograph · isthmus 의 JS/TS 추출기가 **내
|
|
|
97
107
|
|
|
98
108
|
`method-handle`의 `symbol`은 문자열 `case` 자체가 아니라 그것을 감싸는 타입·함수 선언이다. Swift 클로저에는 USR이 없으므로 `qualifiedName`은 `CameraPlugin.register`처럼 감싸는 선언을 가리키고, `location`은 실제 `case` 문자열을 가리킨다. cartograph의 생산 구현은 인덱스와 결합해 `usr`까지 채워야 한다. 구문 실험처럼 `usr`을 채우지 못하면 `missing-handler-usrs`를 `limitations`에 싣는다.
|
|
99
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
|
+
|
|
100
131
|
`location.path`는 프로젝트 루트 기준 상대 경로다. 절대 경로, `..` 상위 이동, 제어 문자를 넣지 않는다.
|
|
101
132
|
`location.line`과 `location.column`은 1부터 시작하며, `column`은 해당 줄의 UTF-8 바이트
|
|
102
133
|
오프셋에 1을 더한 값이다. 생산자는 언어 런타임의 UTF-16 또는 Unicode scalar 열을 그대로
|
|
@@ -113,12 +144,53 @@ UTC로 변환하고 밀리초 세 자리의 `YYYY-MM-DDTHH:mm:ss.SSSZ` 형식으
|
|
|
113
144
|
소비자는 버전 1에 정의되지 않은 추가 필드를 검증 경계에서
|
|
114
145
|
제거하고, 위치의 줄·열은 1 이상의 안전한 정수만 허용한다.
|
|
115
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
|
+
|
|
116
180
|
`channel: null`은 `method-handle`에서만 허용하며, "채널이 없다"가 아니라 생산자가
|
|
117
181
|
핸들러를 어느 채널에 귀속할지 **모른다**는 뜻이다. 소비자는 이 사실을 조인하지 않고,
|
|
118
182
|
호출 없는 핸들러 같은 불일치에도 포함하지 않는다. 생산자는 그 수와 원인을 정확히
|
|
119
183
|
`unattributed-method-handles:`로 시작하는 limitation으로 알려야 하며, 없으면 소비자는
|
|
120
184
|
문서를 거부한다.
|
|
121
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
|
+
|
|
122
194
|
### 종류별 의미
|
|
123
195
|
|
|
124
196
|
| kind | 누가 내는가 | 뜻 |
|
|
@@ -127,28 +199,78 @@ UTC로 변환하고 밀리초 세 자리의 `YYYY-MM-DDTHH:mm:ss.SSSZ` 형식으
|
|
|
127
199
|
| `channel-register` | Swift / Kotlin | 받는 쪽이 채널에 핸들러를 달았다 (`setMethodCallHandler`). 위치도 생성자가 아니라 이 호출을 가리킨다 |
|
|
128
200
|
| `method-invoke` | Dart / JS | `invokeMethod('m')` 호출 |
|
|
129
201
|
| `method-handle` | Swift / Kotlin | 핸들러 안에서 `case "m":` 또는 동등한 분기 |
|
|
130
|
-
| `module-export` | Swift / Kotlin | RN `RCT_EXPORT_MODULE(Name)`, `@ReactModule(name=)` |
|
|
131
|
-
| `module-import` | JS | `NativeModules.Name`, `TurboModuleRegistry.get('Name')` |
|
|
132
|
-
| `component-export` | Swift / Kotlin | RN `RCT_EXPORT_VIEW_PROPERTY` 등 뷰
|
|
133
|
-
| `component-require` | JS | `requireNativeComponent('Name')` |
|
|
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')` |
|
|
134
206
|
|
|
135
207
|
RN 의 메서드는 `method-invoke`(JS: `NativeModules.Name.method()`) / `method-handle`(네이티브: `RCT_EXPORT_METHOD(method:)`, `@ReactMethod fun method`) 로 같은 종류를 쓴다. `channel` 자리에 모듈 이름이 들어간다.
|
|
136
208
|
|
|
137
|
-
`module-*`과 `component-*`는
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
근거다.
|
|
140
239
|
|
|
141
240
|
## 조인 규칙 (isthmus 가 적용)
|
|
142
241
|
|
|
143
242
|
- `channel-create` ↔ `channel-register`: `channel` 이 같다. 플랫폼별로 따로 맞춘다 (Swift 와 Kotlin 이 각각 등록하는 것이 정상)
|
|
144
243
|
- 생성 없는 `channel-register`는 호출 측 사용을 찾지 못한 경고로 보존한다
|
|
145
244
|
- `method-invoke` ↔ `method-handle`: `(channel, method)` 가 같다
|
|
146
|
-
- `module-import` ↔ `module-export`:
|
|
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
|
+
동시에 나타날 수 있다
|
|
147
269
|
- `dynamic: true`이거나 `channel: null`인 사실은 조인하지 않고 `limitations`로 센다. 조인할 수 없다는 이유로 불일치라고 판정하지 않는다.
|
|
148
270
|
세는 주체는 소비자다. isthmus는 조인에서 제외한 dynamic 사실을 직접 세어 자신을 출처(`tool: "isthmus"`)로 밝힌 limitation으로 내보내며, 같은 위치의 중복 사실은 한 번만 센다. 생산자의 `dynamic-*` limitation은 원인을 설명하는 추가 정보이지 소비자가 신뢰의 근거로 삼는 값이 아니다. `channel: null` 핸들러도 같다. 생산자의 `unattributed-method-handles:` 신고가 없으면 문서를 거부하지만, 신고한 개수는 검증하지 않고 소비자가 실제 사실 수를 다시 센다
|
|
149
|
-
- 수신 측이 스스로 신고한 분석 공백은 심각도에 반영한다. 소비자는 `objective-c-sources:`·`shadowed-flutter-method-channel:`(등록과 핸들러를 모두 가림), `opaque-handler-bodies:`(핸들러를 가림)를 수신 측 플랫폼 문서에서 발견하면 "핸들러 없는 호출"과 "등록 없는 채널 생성"을 error가 아니라 판정 불가(`-unverified` 경고)로 보고한다. 소비자가 직접 센 `unjoined-dynamic-methods`·`unjoined-unattributed-handlers`는 핸들러를, `unjoined-dynamic-channels`는
|
|
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로 적거나 같은 접두사를 차용해도 자체 계수의 근거가 되지 않는다. 모르는 한계를 공백으로 넓게 해석하면 진짜 불일치가 경고로 묻힌다. 호출 측 플랫폼의 한계는 네이티브 코드를 가리지 않으므로 심각도를 바꾸지 않는다.
|
|
150
272
|
이 접두사들은 계약이다. 생산자는 문구를 바꿀 때 접두사를 유지하고, 새 공백 종류를 추가하면 소비자의 목록도 함께 갱신한다. 목록이 닫혀 있으므로 갱신 전까지는 그 공백이 error로 보고된다(안전한 방향).
|
|
151
|
-
완화 단위는 진단의 target이다. 사실은 target별로만 조인되므로 target을 가진 수신 문서가 신고한 공백은 그 target 진단의 심각도만 낮춘다. 사실이 없는(`target: null`) 수신 문서의 공백은 어느 target의 분석을 가리는지 귀속 근거가 없어 모든 target에 적용한다. 같은 target에 귀속된 수신 문서가 사실과 함께 공존해도 마찬가지다. 수신 문서 여러 개가 소스 트리를 나누어 가졌을 수 있어, 귀속 없는 문서가 본 소스가 해당 target의 핸들러를 가릴 가능성을 배제할 수 없기 때문이다. mixed-targets 문서의 한계도 선언한 target을 신뢰할 수 없어 귀속 없이 남긴다. 선택적 limitationScopes가 있으면 같은 target 안에서도 그 채널에만 적용한다. 범위가 없으면 기존 전체 적용을 유지한다. 같은 이유로 `objective-c-sources:`처럼 소비자가 직접 셀 수 없는 공백은 생산자의 신고를 그대로 믿는다. 과다 신고는 진짜 불일치를 경고로 묻고, 과소 신고는 거짓 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를 남긴다
|
|
152
274
|
- 위치는 증거이지 조인 키가 아니다. 같은 `(channel, method)` 사실이 여러 위치에 있어도 존재 여부는 키 집합으로 판단하고, 위치는 모두 증거로 보존한다
|
|
153
275
|
- 한 번의 조인에 넣는 모든 문서는 정확히 같은 `project` 문자열을 가져야 한다. 다른 프로젝트의 같은 채널 이름을 연결하지 않기 위해 불일치는 입력 오류로 거부한다
|
|
154
276
|
- 생산자는 `project`를 내보내기 전에 **POSIX realpath**(`realpath(3)`)로 정규화한다. 결과는 항상 symlink·`..`·중복 슬래시가 접힌 절대 경로다. 프로젝트 경로를 해결할 수 없거나 결과가 이 계약이 금지하는 제어 문자(NEL과 U+2028/U+2029 포함)를 포함하면 생산자는 문서를 내보내지 않고 실패한다 — 소비자에게 거부될 문서를 내보내지 않는다. 버전 1은 POSIX를 가정하며, Windows 정규화(드라이브 문자 대소문자, `\\?\` 접두사)는 Windows 지원 시 별도 합의한다. kartograph의 목표 기준은 JVM `Path.toRealPath()`다
|
|
@@ -217,7 +339,12 @@ isthmus `retentions --for <tool>` 의 출력. 자매 도구의 `--external-reten
|
|
|
217
339
|
0이면 생략)로 밝힌다. `omittedObjectiveCHandlers` 와 같은 계수 공개 원칙이다.
|
|
218
340
|
- 소비 도구는 모르는 필드를 무시한다(Swift `JSONDecoder` 의 기본 동작). 그래서
|
|
219
341
|
이 확장은 생산자(isthmus)를 먼저 배포해도 안전하고, 소비 도구가 `callers` 를
|
|
220
|
-
문장으로
|
|
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`에 함께 센다.
|
|
221
348
|
|
|
222
349
|
cartograph의 보존 문서는 **Swift 그래프 선언**을 대상으로 완전해야 한다. 명시적
|
|
223
350
|
`sourceLanguage: "objective-c"` 구현은 조인·진단·query의 증거로 남기지만 Swift 보존 대상은
|
|
@@ -239,7 +366,7 @@ cartograph의 보존 문서는 **Swift 그래프 선언**을 대상으로 완전
|
|
|
239
366
|
| cartograph | `bridges --format json` | Swift 의 `FlutterMethodChannel(name:)`, `setMethodCallHandler`, `case "…"`, `RCT_EXPORT_*`, `@objc(…)` | `--external-retentions` |
|
|
240
367
|
| dartograph | `bridges --format json` | `MethodChannel(…)`, `invokeMethod(…)`, Pigeon 산출물 | (없음 — Dart 쪽이 부르는 쪽) |
|
|
241
368
|
| kartograph | `bridges --format json` | `MethodChannel(…)`, `setMethodCallHandler`, `when (call.method)`, `@ReactModule`, `@ReactMethod` | `--external-retentions` |
|
|
242
|
-
| isthmus 내장 | `extract-js` | `NativeModules.*`, `TurboModuleRegistry.get`, `requireNativeComponent` | — |
|
|
369
|
+
| isthmus 내장 | `extract-js` | `NativeModules.*`, `TurboModuleRegistry.get*`, `requireNativeModule`, `requireNativeComponent` 계열, 바인딩 해석된 멤버 호출 | — |
|
|
243
370
|
|
|
244
371
|
**cartograph가 첫 번째 생산 구현이다.** PR #11에서 SwiftSyntax 스캐너와 `bridges --format json`이 버전 1로 구현됐다.
|
|
245
372
|
|
package/docs/IMPACT.md
CHANGED
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)의 범위를 넓히지도 않는다.
|
package/docs/PREFLIGHT.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# 언어 간 변경 사전 점검
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
`isthmus preflight`는 npm 0.7.0 발행본에 포함되며 JSON만 읽는다. 공개 호환 버전은
|
|
4
|
+
[COMPATIBILITY.md](COMPATIBILITY.md), 고정 source commit에서 새로 구축하는 방법은
|
|
5
|
+
[TOOLCHAIN.md](TOOLCHAIN.md)에 있다.
|
|
5
6
|
언어 내부 해석과 compiler index 생성은 producer 및 별도 workflow가 맡는다.
|
|
6
|
-
|
|
7
|
+
Flutter Dart↔Swift/Kotlin의 MethodChannel과 producer가 제공한 사용 관계를 연결한다.
|
|
7
8
|
선택적 [BasicMessageChannel v2 입력](BRIDGE-MESSAGES.md)을 함께 수집하면 literal 주소와
|
|
8
9
|
Pigeon의 증명된 prefix 후보도 연결한다. 플랫폼별 실제 실행·모든 생성 형태·앱 전체 정확도는
|
|
9
|
-
별도 검증 범위다. Kotlin 및 Basic producer 확장은
|
|
10
|
+
별도 검증 범위다. Kotlin 및 Basic producer 확장은 공개 버전(cartograph 0.18.0·kartograph
|
|
11
|
+
0.10.2·dartograph 0.14.0)에 포함됐다.
|
|
10
12
|
|
|
11
13
|
```bash
|
|
12
14
|
isthmus preflight context.json --strict --compact
|
|
@@ -66,6 +68,9 @@ binding 100,000개, 각 producer depth 128, 관계 문자열 32개, 조합된
|
|
|
66
68
|
경계에만 전파한다. `bridge-message-dependency`는 원래 참조 위치·대상과 선택된 dispatch
|
|
67
69
|
후보를 보존한다. 등록 함수 직접 변경과 closure 밖 공유 의존 변경은 전체 관련 배선을
|
|
68
70
|
포함한다. 근거가 없으면 넓은 후보와 `unresolved-message-handler-scope` 공백을 유지한다.
|
|
71
|
+
- 전송별 의존 관계 종류는 구분된다. 일반 MethodChannel 경로는 `bridge-method-dependency`,
|
|
72
|
+
BasicMessageChannel은 `bridge-message-dependency`, EventChannel은
|
|
73
|
+
`bridge-stream-dependency`다 — 소비자가 전송을 관계 종류로도 구분할 수 있다.
|
|
69
74
|
- `reviewFiles`, `issues`, `limitations`, `bridgeLimitations`와 선택적 `messageLimitations`를 함께 읽는다. 바인딩 부재,
|
|
70
75
|
후속 Dart 분석 부재, 미관찰 선택, producer truncation은 검토가 필요한 공백이다.
|
|
71
76
|
- compact는 공백만 제거한다. `--strict`는 관련 error·공백·미관찰 선택 또는 revision 불일치에
|
|
@@ -170,8 +175,8 @@ base/current를 섞은 입력은 거부한다. 경로 중간 심볼·위치·출
|
|
|
170
175
|
adapter는 경로의 node/edge 항목을 합해 1,000,000개까지 읽고 상한 초과는 입력 오류로
|
|
171
176
|
거부한다. 수집 명령은 producer의 depth 128·출력 10,000개 한도를 사용한다.
|
|
172
177
|
|
|
173
|
-
앱 내부에 복사한 공개 Pigeon 패키지의 Dart 구현까지 연결하려면
|
|
174
|
-
|
|
178
|
+
앱 내부에 복사한 공개 Pigeon 패키지의 Dart 구현까지 연결하려면 Dartograph의
|
|
179
|
+
`dartograph.yaml`에서 분석할 로컬 패키지를 명시한다(`source_packages`, dartograph 0.9.0부터).
|
|
175
180
|
|
|
176
181
|
```yaml
|
|
177
182
|
source_packages:
|
|
@@ -224,6 +229,14 @@ node node_modules/isthmus-cli/scripts/capture-preflight.mjs capture.json
|
|
|
224
229
|
}
|
|
225
230
|
```
|
|
226
231
|
|
|
232
|
+
`isthmus init [capture.json]`은 이 설정의 scaffold를 쓰고, `--toolchain <toolchain.json>`을
|
|
233
|
+
주면 `scripts/build-preflight-toolchain.mjs`가 만든 `isthmus-built-toolchain`의 실제
|
|
234
|
+
producer 명령을 채운다. 앱별로 다른 `prepare`는 자리표시자로 남고, 기존 파일은
|
|
235
|
+
`--force` 없이는 덮어쓰지 않는다. `isthmus doctor <capture.json>`은 설정을 수집기와 같은
|
|
236
|
+
정본 규칙(`src/exchange/capture-config.ts`)으로 검증하고 producer·prepare 첫 토큰이
|
|
237
|
+
PATH나 지정 경로에 실제로 있는지만 확인한다 — 제품은 producer를 실행하지 않는다. 종료
|
|
238
|
+
코드는 정상 0, 미완성 1, 입력·계약 오류 2, 사용 오류 64다.
|
|
239
|
+
|
|
227
240
|
`since` 대신 `selection: {"swift":{"files":["ios/Runner/CameraPlugin.swift"],"symbols":[]}}`
|
|
228
241
|
처럼 변경 전 대상을 명시할 수 있다. 둘을 함께 쓰지 않는다. CI에서는 checkout한 저장소의
|
|
229
242
|
base commit SHA를 since에 넣고 해당 commit을 fetch해 둔다. Git의 rename 양쪽·삭제·변경
|
|
@@ -251,14 +264,17 @@ review·runtime dependency 항목은 공백으로 표시하고 원문 producer
|
|
|
251
264
|
런타임 통신은 [별도 검증](RUNTIME.md)을 사용하며 이 정적 경로에 실제 실행 신원을 추측해 붙이지 않는다.
|
|
252
265
|
|
|
253
266
|
Basic 수집은 설정에 `"messages": true`를 추가한다. 같은 producer에 `bridges --messages`를
|
|
254
|
-
호출하며
|
|
267
|
+
호출하며 공개 호환 버전(cartograph 0.18.0·kartograph 0.10.2·dartograph 0.14.0)에 포함된
|
|
268
|
+
기능이다. 별도의 message producer를 쓸 때는
|
|
255
269
|
`"messages": {"cartograph": ["/path/to/message-cartograph"], "dartograph": ["/path/to/message-dartograph"]}`처럼
|
|
256
270
|
명령을 지정한다. 생략한 쪽은 기본 producer를 사용한다. override 실행 파일과 관련 구현도
|
|
257
271
|
`toolInputs`에 넣어야 하며 feature 설정·명령·override 버전은 캐시 키에 포함된다.
|
|
258
272
|
|
|
259
273
|
## 실행 근거
|
|
260
274
|
|
|
261
|
-
|
|
275
|
+
당시 개발 producer가 있던 환경에서 다음 검증을 실행했다. 필요한 producer 명령은
|
|
276
|
+
공개 호환 버전에 포함됐지만, 이 스크립트를 공개 실행 파일 조합으로 다시 실행하지는
|
|
277
|
+
않았다(인자 형태 확인 필요).
|
|
262
278
|
|
|
263
279
|
```bash
|
|
264
280
|
node scripts/verify-preflight-producers.mjs /path/to/cartograph /path/to/dartograph/bin/dartograph.dart /path/to/flutter
|
package/docs/RUNTIME.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# 런타임 통신 검증 계약
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
`verify-runtime`은 npm 0.7.0 발행본에 포함됐으며 명시한 시나리오에서 실제 관찰한
|
|
4
|
+
통신 결과를 검증한다. [Flutter 수집기](../packages/isthmus_runtime/README.md)는 앱의
|
|
5
5
|
BinaryMessenger에 주입해 실제 outgoing 호출을 기록한다. 정적 후보 연결은
|
|
6
6
|
[impact의 런타임 입력](IMPACT.md#런타임-관찰-연결)과
|
|
7
7
|
[preflight의 실행 대조](PREFLIGHT.md#전이-분석과-runtime-대조)를 사용한다.
|
package/docs/TOOLCHAIN.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# 고정 소스에서 사전 점검 도구 구축
|
|
2
2
|
|
|
3
|
-
개발 소스의 절차이며 현재 공개된 isthmus 0.5.0에는 새 preflight 기능이 없다.
|
|
4
3
|
이 절차는 로컬 Git 저장소의 **지정한 commit만** 새 디렉터리에 풀어 선택한 도구를 구축한다.
|
|
4
|
+
공개 버전만으로 충분하면 [COMPATIBILITY.md](COMPATIBILITY.md)의 설치 경로가 더 빠르다.
|
|
5
5
|
원본의 미커밋 수정, dist, .build, .dart_tool을 복사하지 않는다. SDK와 전역 의존성 캐시는
|
|
6
6
|
공유할 수 있으므로 빈 머신 전체 설치 시간과 구분한다. 소스 다운로드·발행은 수행하지 않는다.
|
|
7
7
|
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
- 앱의 Swift 인덱스와 실제 Flutter 검증에는 해당 앱의 Flutter/Xcode 환경이 추가로 필요하다.
|
|
18
18
|
도구 자체를 구축하는 과정은 Flutter SDK를 필수로 요구하지 않는다.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
빌드 과정의 npm/pub/SwiftPM/Gradle 의존성 해석은 네트워크를 사용할 수 있다.
|
|
20
|
+
공개 호환 버전 세트(isthmus 0.7.0 · cartograph 0.18.0 · kartograph 0.10.2 · dartograph
|
|
21
|
+
0.14.0)는 이미 발행됐다 — [COMPATIBILITY.md](COMPATIBILITY.md)를 본다. 이 절차는
|
|
22
|
+
발행본이 아니라 검증된 개발 commit을 그대로 재현할 때 쓴다. 소스 commit이 로컬 Git에
|
|
23
|
+
있어야 하며 빌드 과정의 npm/pub/SwiftPM/Gradle 의존성 해석은 네트워크를 사용할 수 있다.
|
|
24
24
|
|
|
25
25
|
## 실행
|
|
26
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "isthmus-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Join bridge facts across cross-platform application boundaries.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"flutter",
|
|
@@ -36,7 +36,10 @@
|
|
|
36
36
|
"docs/RUNTIME.md",
|
|
37
37
|
"docs/IMPACT.md",
|
|
38
38
|
"docs/BRIDGE-MESSAGES.md",
|
|
39
|
+
"docs/BRIDGE-EVENTS.md",
|
|
40
|
+
"docs/MCP.md",
|
|
39
41
|
"docs/GRAPH-EXCHANGE.md",
|
|
42
|
+
"compatibility.json",
|
|
40
43
|
"LICENSE",
|
|
41
44
|
"README.md",
|
|
42
45
|
"README.ko.md"
|
|
@@ -52,7 +55,7 @@
|
|
|
52
55
|
"verify:build": "node scripts/verify-build-contract.mjs",
|
|
53
56
|
"verify:cli": "npm run verify:build && node scripts/verify-cli-contract.mjs",
|
|
54
57
|
"verify:package": "npm run verify:build && node scripts/verify-package-contract.mjs",
|
|
55
|
-
"verify": "npm run typecheck && npm test && npm run test:phase0:join && npm run verify:build && node --test scripts/capture-preflight.test.mjs scripts/build-preflight-toolchain.test.mjs scripts/stop-detached-process.test.mjs && node scripts/verify-cli-contract.mjs && node scripts/verify-package-contract.mjs",
|
|
58
|
+
"verify": "npm run typecheck && node scripts/verify-compatibility.mjs && npm test && npm run test:phase0:join && npm run verify:build && node --test scripts/capture-preflight.test.mjs scripts/build-preflight-toolchain.test.mjs scripts/stop-detached-process.test.mjs scripts/verify-installed-compatibility.test.mjs && node scripts/verify-cli-contract.mjs && node scripts/verify-package-contract.mjs",
|
|
56
59
|
"prepublishOnly": "npm run verify"
|
|
57
60
|
},
|
|
58
61
|
"devDependencies": {
|
|
@@ -2,8 +2,9 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { createReadStream } from 'node:fs';
|
|
3
3
|
import { lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname,
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { CaptureConfigValidationError, validateCaptureConfig } from '../dist/exchange/capture-config.js';
|
|
7
8
|
import { parseImpactSelection } from '../dist/exchange/impact-selection.js';
|
|
8
9
|
import { isProjectRelativePath, isSafeNonEmptyString, parseBridgeFactsDocument } from '../dist/exchange/parse.js';
|
|
9
10
|
import { parsePreflightContext } from '../dist/exchange/preflight-context.js';
|
|
@@ -22,7 +23,12 @@ class CaptureError extends Error {}
|
|
|
22
23
|
export async function capturePreflight(config, { execute = runChild } = {}) {
|
|
23
24
|
const started = performance.now();
|
|
24
25
|
const project = await realpath(config.project);
|
|
25
|
-
|
|
26
|
+
// 검증 정본은 제품(`isthmus doctor`)과 공유한다. 계약 위반은 워크플로 오류로 감싼다.
|
|
27
|
+
try { validateCaptureConfig(config, project); }
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error instanceof CaptureConfigValidationError) throw new CaptureError(error.message);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
26
32
|
const natives = [['swift', 'cartograph'], ['kotlin', 'kartograph']].filter(([, name]) => config[name] !== undefined);
|
|
27
33
|
const producerNames = ['dartograph', ...natives.map(([, name]) => name)];
|
|
28
34
|
const output = resolve(project, config.output);
|
|
@@ -93,11 +99,20 @@ export async function capturePreflight(config, { execute = runChild } = {}) {
|
|
|
93
99
|
if (!/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(version)) throw new CaptureError('Unsupported message producer version response.');
|
|
94
100
|
tools[`${name}Messages`] = { name, version };
|
|
95
101
|
}
|
|
102
|
+
const eventCommands = config.events === undefined ? undefined : Object.fromEntries(producerNames
|
|
103
|
+
.map((name) => [name, config.events === true ? config[name] : config.events[name] ?? config[name]]));
|
|
104
|
+
if (eventCommands) for (const name of producerNames) {
|
|
105
|
+
if (JSON.stringify(eventCommands[name]) === JSON.stringify(config[name])) continue;
|
|
106
|
+
const value = (await run(eventCommands[name], ['--version'], `${name}-events-version`)).trim();
|
|
107
|
+
const version = value.replace(new RegExp(`^${name} `), '');
|
|
108
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(version)) throw new CaptureError('Unsupported event producer version response.');
|
|
109
|
+
tools[`${name}Events`] = { name, version };
|
|
110
|
+
}
|
|
96
111
|
const keyConfig = { project, inputs: config.inputs, toolInputs: config.toolInputs, prepare: config.prepare,
|
|
97
112
|
dartograph: config.dartograph, cartograph: config.cartograph ?? null, kartograph: config.kartograph ?? null,
|
|
98
113
|
// 입력 선택의 한계만 지문에 넣는다. 수집 결과로 추가되는 한계는 입력 변경이 아니다.
|
|
99
114
|
kartographSnapshot: config.kartographSnapshot ?? null, selection, selectionBase, limitations: [...limitations],
|
|
100
|
-
indexStore: config.indexStore ?? null, messageCommands: messageCommands ?? null, tools,
|
|
115
|
+
indexStore: config.indexStore ?? null, messageCommands: messageCommands ?? null, eventCommands: eventCommands ?? null, tools,
|
|
101
116
|
host: { node: process.version, platform: process.platform, arch: process.arch },
|
|
102
117
|
toolchainEnvironment: digest(Object.fromEntries(['PATH', 'SDKROOT', 'DEVELOPER_DIR', 'FLUTTER_ROOT', 'DART_SDK', 'SWIFT_EXEC',
|
|
103
118
|
'JAVA_HOME', 'ANDROID_HOME', 'ANDROID_SDK_ROOT', 'GRADLE_USER_HOME']
|
|
@@ -165,15 +180,22 @@ export async function capturePreflight(config, { execute = runChild } = {}) {
|
|
|
165
180
|
const bridges = [
|
|
166
181
|
parseBridgeFactsDocument(await json(config.dartograph, ['bridges', '--format', 'json', '--project', project, project], 'dart-bridges')),
|
|
167
182
|
];
|
|
168
|
-
const messages = messageCommands === undefined ? undefined : [
|
|
169
|
-
|
|
183
|
+
const messages = messageCommands === undefined && eventCommands === undefined ? undefined : [
|
|
184
|
+
...(messageCommands === undefined ? [] : [
|
|
185
|
+
parseMessageBridgeDocument(await json(messageCommands.dartograph, ['bridges', '--messages', '--format', 'json', '--project', project, project], 'dart-messages')),
|
|
186
|
+
]),
|
|
187
|
+
...(eventCommands === undefined ? [] : [
|
|
188
|
+
parseMessageBridgeDocument(await json(eventCommands.dartograph, ['bridges', '--events', '--format', 'json', '--project', project, project], 'dart-events')),
|
|
189
|
+
]),
|
|
170
190
|
];
|
|
171
191
|
for (const [platform, name] of natives) {
|
|
172
192
|
const args = platform === 'swift' ? nativeArgs : kotlinArgs;
|
|
173
193
|
bridges.push(parseBridgeFactsDocument(await json(config[name],
|
|
174
194
|
['bridges', '--target', 'flutter', '--format', 'json', ...args], `${platform}-bridges`)));
|
|
175
|
-
if (
|
|
195
|
+
if (messageCommands) messages.push(parseMessageBridgeDocument(await json(messageCommands[name],
|
|
176
196
|
['bridges', '--messages', '--target', 'flutter', '--format', 'json', ...args], `${platform}-messages`)));
|
|
197
|
+
if (eventCommands) messages.push(parseMessageBridgeDocument(await json(eventCommands[name],
|
|
198
|
+
['bridges', '--events', '--target', 'flutter', '--format', 'json', ...args], `${platform}-events`)));
|
|
177
199
|
}
|
|
178
200
|
const analyses = [];
|
|
179
201
|
const artifacts = {};
|
|
@@ -202,7 +224,7 @@ export async function capturePreflight(config, { execute = runChild } = {}) {
|
|
|
202
224
|
for (const requested of selections) {
|
|
203
225
|
const raw = await json(config.cartograph, ['impact', ...nativeArgs, '--format', 'json', '--limit', '10000',
|
|
204
226
|
...requested.files.flatMap((path) => ['--file', resolve(project, path)]),
|
|
205
|
-
...(requested.symbols.length ? ['--', ...requested.symbols] : [])], 'swift-impact');
|
|
227
|
+
...(requested.symbols.length ? ['--', ...requested.symbols] : [])], 'swift-impact', [0, 64]);
|
|
206
228
|
const meta = metadata('swift', requested);
|
|
207
229
|
artifacts[meta.id] = raw;
|
|
208
230
|
analyses.push(adaptCartographImpact(raw, meta));
|
|
@@ -221,7 +243,8 @@ export async function capturePreflight(config, { execute = runChild } = {}) {
|
|
|
221
243
|
artifacts[meta.id] = raw;
|
|
222
244
|
analyses.push(adaptKartographImpact(raw, meta));
|
|
223
245
|
}
|
|
224
|
-
const dartFacts = [...bridges[0].facts,
|
|
246
|
+
const dartFacts = [...bridges[0].facts,
|
|
247
|
+
...(messages ?? []).filter((document) => document.platform === 'dart').flatMap((document) => document.facts)];
|
|
225
248
|
const names = [...new Set(dartFacts.flatMap((fact) => fact.symbol ? [fact.symbol.qualifiedName] : []))].sort();
|
|
226
249
|
const subjects = new Map();
|
|
227
250
|
const conflictingBindings = new Set();
|
|
@@ -340,42 +363,7 @@ async function publish(path, value) {
|
|
|
340
363
|
await writeTextAtomically(path, encodeSortedJson(value, true));
|
|
341
364
|
}
|
|
342
365
|
|
|
343
|
-
|
|
344
|
-
function validateConfig(config, project) {
|
|
345
|
-
const command = (value) => Array.isArray(value) && value.length > 0 && value.every(isSafeNonEmptyString);
|
|
346
|
-
if (!command(config.dartograph) || (config.cartograph === undefined && config.kartograph === undefined) ||
|
|
347
|
-
(config.cartograph !== undefined && !command(config.cartograph)) ||
|
|
348
|
-
(config.kartograph !== undefined && !command(config.kartograph)) || !Array.isArray(config.prepare) ||
|
|
349
|
-
config.prepare.length === 0 || !config.prepare.every(command)) throw new CaptureError('Configure producer commands and a native index preparation command.');
|
|
350
|
-
if (config.messages !== undefined && config.messages !== true && (config.messages === null || typeof config.messages !== 'object' ||
|
|
351
|
-
Array.isArray(config.messages) || Object.entries(config.messages).some(([name, value]) =>
|
|
352
|
-
!['dartograph', 'cartograph', 'kartograph'].includes(name) || config[name] === undefined || !command(value)))) {
|
|
353
|
-
throw new CaptureError('Invalid message producer configuration.');
|
|
354
|
-
}
|
|
355
|
-
if (config.kartograph !== undefined && !isSafeNonEmptyString(config.kartographSnapshot)) {
|
|
356
|
-
throw new CaptureError('Configure a Kartograph snapshot produced by the preparation command.');
|
|
357
|
-
}
|
|
358
|
-
if (config.kartograph === undefined && config.kartographSnapshot !== undefined) throw new CaptureError('A Kartograph snapshot requires its producer.');
|
|
359
|
-
if (!Array.isArray(config.inputs) || config.inputs.length === 0 || !config.inputs.every(isProjectRelativePath) ||
|
|
360
|
-
!Array.isArray(config.toolInputs) || config.toolInputs.length === 0 || !config.toolInputs.every(isSafeNonEmptyString)) {
|
|
361
|
-
throw new CaptureError('Declare source/config inputs and producer implementation files for fingerprinting.');
|
|
362
|
-
}
|
|
363
|
-
const explicit = config.selection !== undefined;
|
|
364
|
-
const since = config.since !== undefined;
|
|
365
|
-
if (explicit === since || (explicit && (!config.selection || typeof config.selection !== 'object' || Array.isArray(config.selection))) ||
|
|
366
|
-
(since && !isSafeNonEmptyString(config.since)) || !isSafeNonEmptyString(config.output) || !isSafeNonEmptyString(config.cache)) {
|
|
367
|
-
throw new CaptureError('Configure exactly one selection or since revision, plus output and cache paths.');
|
|
368
|
-
}
|
|
369
|
-
for (const output of [config.output, `${config.output}.sources.json`, config.cache]) {
|
|
370
|
-
const target = resolve(project, output);
|
|
371
|
-
for (const input of [...config.inputs, ...config.toolInputs,
|
|
372
|
-
...(config.kartographSnapshot === undefined ? [] : [config.kartographSnapshot])]) {
|
|
373
|
-
const part = relative(resolve(project, input), target);
|
|
374
|
-
if (part === '' || (!part.startsWith('..') && !isAbsolute(part))) throw new CaptureError('Keep output and cache outside fingerprint input trees.');
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
if (resolve(project, config.output) === resolve(project, config.cache)) throw new CaptureError('Output and cache paths must differ.');
|
|
378
|
-
}
|
|
366
|
+
|
|
379
367
|
|
|
380
368
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
381
369
|
try {
|
package/scripts/run-child.mjs
CHANGED
|
@@ -9,6 +9,7 @@ export function runChild(command, arguments_, options = {}) {
|
|
|
9
9
|
cwd: options.cwd,
|
|
10
10
|
encoding: 'utf8',
|
|
11
11
|
env: options.env,
|
|
12
|
+
input: options.input,
|
|
12
13
|
timeout: options.timeout ?? defaultTimeout,
|
|
13
14
|
maxBuffer: options.maxBuffer ?? defaultMaxBuffer,
|
|
14
15
|
stdio: options.stdio,
|