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.
Files changed (99) hide show
  1. package/README.ko.md +135 -31
  2. package/README.md +134 -16
  3. package/Skills/isthmus/SKILL.md +17 -7
  4. package/compatibility.json +10 -0
  5. package/dist/cli/check-command.d.ts +2 -2
  6. package/dist/cli/check-command.js +22 -9
  7. package/dist/cli/check-command.js.map +1 -1
  8. package/dist/cli/command-support.d.ts +19 -1
  9. package/dist/cli/command-support.js +37 -8
  10. package/dist/cli/command-support.js.map +1 -1
  11. package/dist/cli/diff-command.js +4 -3
  12. package/dist/cli/diff-command.js.map +1 -1
  13. package/dist/cli/doctor-command.d.ts +11 -0
  14. package/dist/cli/doctor-command.js +128 -0
  15. package/dist/cli/doctor-command.js.map +1 -0
  16. package/dist/cli/extract-js-command.d.ts +33 -0
  17. package/dist/cli/extract-js-command.js +233 -0
  18. package/dist/cli/extract-js-command.js.map +1 -0
  19. package/dist/cli/graph-command.js +13 -6
  20. package/dist/cli/graph-command.js.map +1 -1
  21. package/dist/cli/init-command.d.ts +11 -0
  22. package/dist/cli/init-command.js +115 -0
  23. package/dist/cli/init-command.js.map +1 -0
  24. package/dist/cli/main.js +52 -1
  25. package/dist/cli/main.js.map +1 -1
  26. package/dist/cli/mcp-server.d.ts +16 -0
  27. package/dist/cli/mcp-server.js +534 -0
  28. package/dist/cli/mcp-server.js.map +1 -0
  29. package/dist/cli/query-command.js +13 -6
  30. package/dist/cli/query-command.js.map +1 -1
  31. package/dist/cli/retentions-command.js +14 -7
  32. package/dist/cli/retentions-command.js.map +1 -1
  33. package/dist/cli/serve-command.d.ts +13 -0
  34. package/dist/cli/serve-command.js +43 -0
  35. package/dist/cli/serve-command.js.map +1 -0
  36. package/dist/exchange/capture-config.d.ts +18 -0
  37. package/dist/exchange/capture-config.js +100 -0
  38. package/dist/exchange/capture-config.js.map +1 -0
  39. package/dist/exchange/messages.d.ts +9 -24
  40. package/dist/exchange/messages.js +25 -55
  41. package/dist/exchange/messages.js.map +1 -1
  42. package/dist/exchange/parse.d.ts +61 -0
  43. package/dist/exchange/parse.js +115 -12
  44. package/dist/exchange/parse.js.map +1 -1
  45. package/dist/extract/js-document.d.ts +22 -0
  46. package/dist/extract/js-document.js +230 -0
  47. package/dist/extract/js-document.js.map +1 -0
  48. package/dist/extract/js-scan.d.ts +93 -0
  49. package/dist/extract/js-scan.js +1205 -0
  50. package/dist/extract/js-scan.js.map +1 -0
  51. package/dist/extract/lexer.d.ts +26 -0
  52. package/dist/extract/lexer.js +316 -0
  53. package/dist/extract/lexer.js.map +1 -0
  54. package/dist/join/join.d.ts +66 -1
  55. package/dist/join/join.js +171 -1
  56. package/dist/join/join.js.map +1 -1
  57. package/dist/join/messages.d.ts +7 -4
  58. package/dist/join/messages.js +15 -8
  59. package/dist/join/messages.js.map +1 -1
  60. package/dist/report/check-report.d.ts +20 -3
  61. package/dist/report/check-report.js +240 -8
  62. package/dist/report/check-report.js.map +1 -1
  63. package/dist/report/codequality.d.ts +45 -0
  64. package/dist/report/codequality.js +74 -0
  65. package/dist/report/codequality.js.map +1 -0
  66. package/dist/report/diff.d.ts +25 -3
  67. package/dist/report/diff.js +91 -11
  68. package/dist/report/diff.js.map +1 -1
  69. package/dist/report/graph.d.ts +10 -4
  70. package/dist/report/graph.js +60 -6
  71. package/dist/report/graph.js.map +1 -1
  72. package/dist/report/preflight-view.js +28 -1
  73. package/dist/report/preflight-view.js.map +1 -1
  74. package/dist/report/preflight.d.ts +4 -4
  75. package/dist/report/preflight.js +81 -46
  76. package/dist/report/preflight.js.map +1 -1
  77. package/dist/report/query.d.ts +11 -4
  78. package/dist/report/query.js +84 -17
  79. package/dist/report/query.js.map +1 -1
  80. package/dist/report/retentions.d.ts +17 -5
  81. package/dist/report/retentions.js +85 -8
  82. package/dist/report/retentions.js.map +1 -1
  83. package/dist/report/rules.d.ts +16 -0
  84. package/dist/report/rules.js +30 -0
  85. package/dist/report/rules.js.map +1 -0
  86. package/dist/report/sarif.d.ts +2 -8
  87. package/dist/report/sarif.js +2 -10
  88. package/dist/report/sarif.js.map +1 -1
  89. package/docs/BRIDGE-EVENTS.md +87 -0
  90. package/docs/BRIDGE-MESSAGES.md +23 -8
  91. package/docs/GRAPH-EXCHANGE.md +141 -14
  92. package/docs/IMPACT.md +1 -1
  93. package/docs/MCP.md +62 -0
  94. package/docs/PREFLIGHT.md +24 -8
  95. package/docs/RUNTIME.md +2 -2
  96. package/docs/TOOLCHAIN.md +5 -5
  97. package/package.json +5 -2
  98. package/scripts/capture-preflight.mjs +32 -44
  99. package/scripts/run-child.mjs +1 -0
@@ -23,9 +23,11 @@ export class RetentionValidationError extends Error {
23
23
  * 성공하므로, 검증하지 않으면 보존할 근거가 없다는 사실이 빈 목록과 코드 0으로
24
24
  * 사라진다. 사실이 없는 Swift 문서도 그 플랫폼을 분석했다는 근거로 인정한다.
25
25
  */
26
- export function validateCartographRetentionInputs(documents) {
26
+ export function validateCartographRetentionInputs(documents, messageDocuments) {
27
27
  if (documents.some(({ platform }) => platform === 'swift'))
28
28
  return;
29
+ if (messageDocuments?.some(({ platform }) => platform === 'swift'))
30
+ return;
29
31
  throw new RetentionValidationError('Retentions for cartograph require at least one swift bridge facts document; '
30
32
  + 'run a swift producer for the receiver side.');
31
33
  }
@@ -33,24 +35,32 @@ export function validateCartographRetentionInputs(documents) {
33
35
  export function encodeCartographRetentionsDocument(document) {
34
36
  return encodeSortedJson(document);
35
37
  }
36
- /** 매치된 브리지 메서드를 cartograph 보존 근거로 바꾼다. */
37
- export function createCartographRetentionsDocument(joined, generatedAt, producerVersion) {
38
+ /**
39
+ * 매치된 브리지 메서드와 v2 Basic·Event 경계를 cartograph 보존 근거로 바꾼다.
40
+ *
41
+ * `messages`를 주면 literal로 확정된 v2 경계의 Swift 핸들러도 보존 근거로 싣는다.
42
+ * v2 근거에는 메서드가 없으므로 `method`를 생략하며, cartograph는 선택 필드로 읽는다.
43
+ */
44
+ export function createCartographRetentionsDocument(joined, generatedAt, producerVersion, messages) {
38
45
  if (isBridgeJoinDeferred(joined)) {
39
46
  throw new RetentionValidationError('Cannot create retentions from a deferred bridge join.');
40
47
  }
41
- rejectUnresolvedSwiftHandlers(joined);
42
- const omittedObjectiveCHandlers = countObjectiveCHandlers(joined);
48
+ rejectUnresolvedSwiftHandlers(joined, messages);
49
+ const omittedObjectiveCHandlers = countObjectiveCHandlers(joined, messages);
43
50
  return {
44
51
  format: 'external-retentions',
45
52
  version: 0,
46
53
  producedBy: { name: 'isthmus', version: producerVersion },
47
54
  generatedAt,
48
- retentions: collectCartographRetentions(joined),
55
+ retentions: [
56
+ ...collectCartographRetentions(joined),
57
+ ...(messages === undefined ? [] : collectMessageRetentions(messages)),
58
+ ],
49
59
  ...(omittedObjectiveCHandlers === 0 ? {} : { omittedObjectiveCHandlers }),
50
60
  };
51
61
  }
52
62
  /** 그래프 밖의 매치도 사라지지 않게 채널·메서드·위치별로 센다. */
53
- function countObjectiveCHandlers(joined) {
63
+ function countObjectiveCHandlers(joined, messages) {
54
64
  const keys = new Set();
55
65
  for (const method of joined.matchedMethods) {
56
66
  for (const handler of method.handlers) {
@@ -60,6 +70,16 @@ function countObjectiveCHandlers(joined) {
60
70
  handler.location.path, handler.location.line, handler.location.column]));
61
71
  }
62
72
  }
73
+ if (messages !== undefined) {
74
+ for (const route of retainedMessageRoutes(messages)) {
75
+ for (const handler of route.handlers) {
76
+ if (handler.sourceLanguage !== 'objective-c')
77
+ continue;
78
+ keys.add(JSON.stringify(['v2', route.transport, route.channel,
79
+ handler.location.path, handler.location.line, handler.location.column]));
80
+ }
81
+ }
82
+ }
63
83
  return keys.size;
64
84
  }
65
85
  /**
@@ -70,7 +90,7 @@ function countObjectiveCHandlers(joined) {
70
90
  * 핸들러를 조용히 빼면 cartograph는 그 핸들러를 계속 미사용으로 보고하고,
71
91
  * 소비자는 살아 있는 코드를 지운다. 부분 보존 문서 대신 실패를 돌려준다.
72
92
  */
73
- function rejectUnresolvedSwiftHandlers(joined) {
93
+ function rejectUnresolvedSwiftHandlers(joined, messages) {
74
94
  const unresolved = new Set();
75
95
  for (const method of joined.matchedMethods) {
76
96
  for (const handler of method.handlers) {
@@ -80,6 +100,16 @@ function rejectUnresolvedSwiftHandlers(joined) {
80
100
  unresolved.add(`${path}\u0000${line}\u0000${column}`);
81
101
  }
82
102
  }
103
+ if (messages !== undefined) {
104
+ for (const route of retainedMessageRoutes(messages)) {
105
+ for (const handler of route.handlers) {
106
+ if (handler.platform !== 'swift' || handler.sourceLanguage === 'objective-c' || handler.symbol !== undefined)
107
+ continue;
108
+ const { path, line, column } = handler.location;
109
+ unresolved.add(`${path}\u0000${line}\u0000${column}`);
110
+ }
111
+ }
112
+ }
83
113
  if (unresolved.size === 0)
84
114
  return;
85
115
  throw new RetentionValidationError(`Cannot produce retention evidence for ${unresolved.size} matched swift `
@@ -133,6 +163,53 @@ function collectCartographRetentions(joined) {
133
163
  }
134
164
  return retentions;
135
165
  }
166
+ /** literal로 확정되고 호출자가 있는 v2 경계만 보존 후보로 돌려준다. */
167
+ function retainedMessageRoutes(messages) {
168
+ return messages.routes.filter((route) => route.matching === 'literal' && route.senders.length > 0);
169
+ }
170
+ /** 매치된 v2 경계의 송신자를 Swift 심볼 보존 근거로 결합한다. */
171
+ function collectMessageRetentions(messages) {
172
+ const retentions = [];
173
+ const seen = new Set();
174
+ let callerEntries = 0;
175
+ for (const route of retainedMessageRoutes(messages)) {
176
+ const callers = route.senders
177
+ .slice(0, MAX_RETENTION_CALLERS)
178
+ .map(toRetentionCaller);
179
+ const callersOmitted = route.senders.length - callers.length;
180
+ const representative = callers[0];
181
+ if (representative === undefined)
182
+ continue;
183
+ for (const handler of route.handlers) {
184
+ if (handler.platform !== 'swift' || handler.sourceLanguage === 'objective-c' || handler.symbol === undefined)
185
+ continue;
186
+ const symbolKey = handler.symbol.usr === undefined
187
+ ? `name:${handler.symbol.qualifiedName}`
188
+ : `usr:${handler.symbol.usr}`;
189
+ // transport까지 키에 넣어 같은 이름의 MethodChannel 보존과 섞이지 않게 한다.
190
+ const retentionKey = `${symbolKey}\u0000${route.transport}\u0000${route.channel}`;
191
+ if (seen.has(retentionKey))
192
+ continue;
193
+ seen.add(retentionKey);
194
+ callerEntries += callers.length;
195
+ if (callerEntries > MAX_RETENTION_CALLER_ENTRIES) {
196
+ throw new RetentionValidationError(`Cannot produce retention evidence with more than `
197
+ + `${MAX_RETENTION_CALLER_ENTRIES} caller entries; narrow the join inputs.`);
198
+ }
199
+ retentions.push({
200
+ symbol: handler.symbol,
201
+ reason: 'bridge',
202
+ evidence: {
203
+ channel: route.channel,
204
+ caller: representative,
205
+ ...(callers.length > 1 ? { callers } : {}),
206
+ ...(callersOmitted > 0 ? { callersOmitted } : {}),
207
+ },
208
+ });
209
+ }
210
+ }
211
+ return retentions;
212
+ }
136
213
  /** 증거 끝점을 external-retentions의 호출자 형태로 바꾼다. */
137
214
  function toRetentionCaller(endpoint) {
138
215
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"retentions.js","sourceRoot":"","sources":["../../src/report/retentions.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n BridgeFactsDocument,\n BridgePlatform,\n BridgeSymbol,\n} from '../exchange/parse.ts';\nimport {\n isBridgeJoinDeferred,\n type BridgeJoinResult,\n} from '../join/join.ts';\nimport { encodeSortedJson } from './sorted-json.ts';\n\n/** cartograph가 보존할 Swift 선언 식별자다. */\nexport interface RetentionSymbol extends BridgeSymbol {\n readonly usr?: string;\n}\n\n/** 언어 경계 너머 호출자의 증거 위치다. */\nexport interface RetentionCaller {\n readonly platform: BridgePlatform;\n readonly path: string;\n readonly line: number;\n}\n\n/** 보존 판단을 설명할 채널·메서드·호출자 근거다. */\nexport interface RetentionEvidence {\n readonly channel: string;\n readonly method: string;\n /** 대표 호출 위치다. 결정적 순서의 첫 호출이며 옛 소비자가 읽는 필드다. */\n readonly caller: RetentionCaller;\n /**\n * 이 보존의 전체 호출 위치(대표 포함)다. 결정적 순서를 유지하고 호출이 둘 이상일\n * 때만 실는다. 옛 소비자는 이 필드를 모르고 대표 `caller`만 읽는다.\n */\n readonly callers?: readonly RetentionCaller[];\n /** 상한 때문에 `callers`에 실지 못한 호출 수다. 0이면 실지 않는다. */\n readonly callersOmitted?: number;\n}\n\n/** cartograph 외부 보존 근거 하나다. */\nexport interface ExternalRetention {\n readonly symbol: RetentionSymbol;\n readonly reason: 'bridge';\n readonly evidence: RetentionEvidence;\n}\n\n/** cartograph가 읽는 external-retentions 버전 0 문서다. */\nexport interface CartographRetentionsDocument {\n readonly format: 'external-retentions';\n readonly version: 0;\n readonly producedBy: Readonly<{ name: 'isthmus'; version: string }>;\n readonly generatedAt: string;\n readonly retentions: readonly ExternalRetention[];\n readonly omittedObjectiveCHandlers?: number;\n}\n\n/** 근거 하나가 실을 수 있는 호출 위치 상한이다. */\nexport const MAX_RETENTION_CALLERS = 100;\n\n/**\n * 문서 전체가 실을 수 있는 호출 위치 총상한이다.\n *\n * 호출자 증거는 (심볼 × 채널 × 메서드)마다 다시 실리므로 건당 상한만으로는 출력이\n * 입력보다 커질 수 있다. 베이스라인의 항목 상한과 같은 자원 거버넌스다.\n */\nexport const MAX_RETENTION_CALLER_ENTRIES = 1_000_000;\n\n/** 불완전한 조인으로 보존 결정을 만들 수 없음을 나타낸다. */\nexport class RetentionValidationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RetentionValidationError';\n }\n}\n\n/**\n * cartograph가 소비할 수 있는 수신 측 문서가 입력에 있는지 검증한다.\n *\n * cartograph는 Swift 심볼만 보존한다. Swift 문서가 없는 입력은 조인 자체는\n * 성공하므로, 검증하지 않으면 보존할 근거가 없다는 사실이 빈 목록과 코드 0으로\n * 사라진다. 사실이 없는 Swift 문서도 그 플랫폼을 분석했다는 근거로 인정한다.\n */\nexport function validateCartographRetentionInputs(\n documents: readonly BridgeFactsDocument[],\n): void {\n if (documents.some(({ platform }) => platform === 'swift')) return;\n throw new RetentionValidationError(\n 'Retentions for cartograph require at least one swift bridge facts document; '\n + 'run a swift producer for the receiver side.',\n );\n}\n\n/** cartograph 보존 문서를 결정적인 JSON으로 인코딩한다. */\nexport function encodeCartographRetentionsDocument(\n document: CartographRetentionsDocument,\n): string {\n return encodeSortedJson(document);\n}\n\n/** 매치된 브리지 메서드를 cartograph 보존 근거로 바꾼다. */\nexport function createCartographRetentionsDocument(\n joined: BridgeJoinResult,\n generatedAt: string,\n producerVersion: string,\n): CartographRetentionsDocument {\n if (isBridgeJoinDeferred(joined)) {\n throw new RetentionValidationError(\n 'Cannot create retentions from a deferred bridge join.',\n );\n }\n rejectUnresolvedSwiftHandlers(joined);\n const omittedObjectiveCHandlers = countObjectiveCHandlers(joined);\n return {\n format: 'external-retentions',\n version: 0,\n producedBy: { name: 'isthmus', version: producerVersion },\n generatedAt,\n retentions: collectCartographRetentions(joined),\n ...(omittedObjectiveCHandlers === 0 ? {} : { omittedObjectiveCHandlers }),\n };\n}\n\n/** 그래프 밖의 매치도 사라지지 않게 채널·메서드·위치별로 센다. */\nfunction countObjectiveCHandlers(joined: BridgeJoinResult): number {\n const keys = new Set<string>();\n for (const method of joined.matchedMethods) {\n for (const handler of method.handlers) {\n if (handler.sourceLanguage !== 'objective-c') continue;\n keys.add(JSON.stringify([method.target, method.channel, method.method,\n handler.location.path, handler.location.line, handler.location.column]));\n }\n }\n return keys.size;\n}\n\n/**\n * 심볼이 없어 보존 근거로 바꿀 수 없는 매치 Swift 핸들러를 거부한다.\n * sourceLanguage로 확인된 Objective-C 구현은 Swift 그래프의 보존 대상이 아니다.\n *\n * 교환 계약에서 `symbol`은 선택 필드다. 호출자가 있는데도 근거를 만들지 못한\n * 핸들러를 조용히 빼면 cartograph는 그 핸들러를 계속 미사용으로 보고하고,\n * 소비자는 살아 있는 코드를 지운다. 부분 보존 문서 대신 실패를 돌려준다.\n */\nfunction rejectUnresolvedSwiftHandlers(joined: BridgeJoinResult): void {\n const unresolved = new Set<string>();\n for (const method of joined.matchedMethods) {\n for (const handler of method.handlers) {\n if (handler.platform !== 'swift' || handler.sourceLanguage === 'objective-c' || handler.symbol !== undefined) continue;\n const { path, line, column } = handler.location;\n unresolved.add(`${path}\\u0000${line}\\u0000${column}`);\n }\n }\n if (unresolved.size === 0) return;\n throw new RetentionValidationError(\n `Cannot produce retention evidence for ${unresolved.size} matched swift `\n + 'handlers without a symbol; regenerate the swift document with a producer '\n + 'that attaches handler symbols.',\n );\n}\n\n/** 매치별 Dart 호출자와 Swift 심볼을 cartograph 근거로 결합한다. */\nfunction collectCartographRetentions(\n joined: BridgeJoinResult,\n): ExternalRetention[] {\n const retentions: ExternalRetention[] = [];\n const seen = new Set<string>();\n let callerEntries = 0;\n for (const method of joined.matchedMethods) {\n if (method.invocations.length === 0) continue;\n // 결정적 순서(플랫폼·경로·줄·열)의 첫 호출이 대표 증거다. 상한 밖 호출까지\n // 객체로 만들지 않도록 먼저 자른다.\n const callers = method.invocations\n .slice(0, MAX_RETENTION_CALLERS)\n .map(toRetentionCaller);\n const callersOmitted = method.invocations.length - callers.length;\n const representative = callers[0];\n if (representative === undefined) continue;\n for (const handler of method.handlers) {\n if (handler.platform !== 'swift' || handler.sourceLanguage === 'objective-c' || handler.symbol === undefined) continue;\n const symbolKey = handler.symbol.usr === undefined\n ? `name:${handler.symbol.qualifiedName}`\n : `usr:${handler.symbol.usr}`;\n const retentionKey = `${symbolKey}\\u0000${method.channel}\\u0000${method.method}`;\n if (seen.has(retentionKey)) continue;\n seen.add(retentionKey);\n callerEntries += callers.length;\n if (callerEntries > MAX_RETENTION_CALLER_ENTRIES) {\n throw new RetentionValidationError(\n `Cannot produce retention evidence with more than `\n + `${MAX_RETENTION_CALLER_ENTRIES} caller entries; narrow the join inputs.`,\n );\n }\n retentions.push({\n symbol: handler.symbol,\n reason: 'bridge',\n evidence: {\n channel: method.channel,\n method: method.method,\n caller: representative,\n ...(callers.length > 1 ? { callers } : {}),\n ...(callersOmitted > 0 ? { callersOmitted } : {}),\n },\n });\n }\n }\n return retentions;\n}\n\n/** 증거 끝점을 external-retentions의 호출자 형태로 바꾼다. */\nfunction toRetentionCaller(endpoint: {\n readonly platform: BridgePlatform;\n readonly location: { readonly path: string; readonly line: number };\n}): RetentionCaller {\n return {\n platform: endpoint.platform,\n path: endpoint.location.path,\n line: endpoint.location.line,\n };\n}\n"]}
1
+ {"version":3,"file":"retentions.js","sourceRoot":"","sources":["../../src/report/retentions.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n BridgeFactsDocument,\n BridgePlatform,\n BridgeSymbol,\n} from '../exchange/parse.ts';\nimport type { BridgeMessageDocument } from '../exchange/messages.ts';\nimport {\n isBridgeJoinDeferred,\n type BridgeJoinResult,\n} from '../join/join.ts';\nimport type { MessageBridgeJoin, MessageEndpoint } from '../join/messages.ts';\nimport { encodeSortedJson } from './sorted-json.ts';\n\n/** cartograph가 보존할 Swift 선언 식별자다. */\nexport interface RetentionSymbol extends BridgeSymbol {\n readonly usr?: string;\n}\n\n/** 언어 경계 너머 호출자의 증거 위치다. */\nexport interface RetentionCaller {\n readonly platform: BridgePlatform;\n readonly path: string;\n readonly line: number;\n}\n\n/**\n * 보존 판단을 설명할 채널·메서드·호출자 근거다.\n *\n * `method`는 MethodChannel 근거에만 있다. Basic·Event v2 경계는 메서드가 없으므로\n * 생략하며, cartograph의 `ExternalRetention.Evidence.method`도 선택 필드다.\n */\nexport interface RetentionEvidence {\n readonly channel: string;\n readonly method?: string;\n /** 대표 호출 위치다. 결정적 순서의 첫 호출이며 옛 소비자가 읽는 필드다. */\n readonly caller: RetentionCaller;\n /**\n * 이 보존의 전체 호출 위치(대표 포함)다. 결정적 순서를 유지하고 호출이 둘 이상일\n * 때만 실는다. 옛 소비자는 이 필드를 모르고 대표 `caller`만 읽는다.\n */\n readonly callers?: readonly RetentionCaller[];\n /** 상한 때문에 `callers`에 실지 못한 호출 수다. 0이면 실지 않는다. */\n readonly callersOmitted?: number;\n}\n\n/** cartograph 외부 보존 근거 하나다. */\nexport interface ExternalRetention {\n readonly symbol: RetentionSymbol;\n readonly reason: 'bridge';\n readonly evidence: RetentionEvidence;\n}\n\n/** cartograph가 읽는 external-retentions 버전 0 문서다. */\nexport interface CartographRetentionsDocument {\n readonly format: 'external-retentions';\n readonly version: 0;\n readonly producedBy: Readonly<{ name: 'isthmus'; version: string }>;\n readonly generatedAt: string;\n readonly retentions: readonly ExternalRetention[];\n readonly omittedObjectiveCHandlers?: number;\n}\n\n/** 근거 하나가 실을 수 있는 호출 위치 상한이다. */\nexport const MAX_RETENTION_CALLERS = 100;\n\n/**\n * 문서 전체가 실을 수 있는 호출 위치 총상한이다.\n *\n * 호출자 증거는 (심볼 × 채널 × 메서드)마다 다시 실리므로 건당 상한만으로는 출력이\n * 입력보다 커질 수 있다. 베이스라인의 항목 상한과 같은 자원 거버넌스다.\n */\nexport const MAX_RETENTION_CALLER_ENTRIES = 1_000_000;\n\n/** 불완전한 조인으로 보존 결정을 만들 수 없음을 나타낸다. */\nexport class RetentionValidationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RetentionValidationError';\n }\n}\n\n/**\n * cartograph가 소비할 수 있는 수신 측 문서가 입력에 있는지 검증한다.\n *\n * cartograph는 Swift 심볼만 보존한다. Swift 문서가 없는 입력은 조인 자체는\n * 성공하므로, 검증하지 않으면 보존할 근거가 없다는 사실이 빈 목록과 코드 0으로\n * 사라진다. 사실이 없는 Swift 문서도 그 플랫폼을 분석했다는 근거로 인정한다.\n */\nexport function validateCartographRetentionInputs(\n documents: readonly BridgeFactsDocument[],\n messageDocuments?: readonly BridgeMessageDocument[],\n): void {\n if (documents.some(({ platform }) => platform === 'swift')) return;\n if (messageDocuments?.some(({ platform }) => platform === 'swift')) return;\n throw new RetentionValidationError(\n 'Retentions for cartograph require at least one swift bridge facts document; '\n + 'run a swift producer for the receiver side.',\n );\n}\n\n/** cartograph 보존 문서를 결정적인 JSON으로 인코딩한다. */\nexport function encodeCartographRetentionsDocument(\n document: CartographRetentionsDocument,\n): string {\n return encodeSortedJson(document);\n}\n\n/**\n * 매치된 브리지 메서드와 v2 Basic·Event 경계를 cartograph 보존 근거로 바꾼다.\n *\n * `messages`를 주면 literal로 확정된 v2 경계의 Swift 핸들러도 보존 근거로 싣는다.\n * v2 근거에는 메서드가 없으므로 `method`를 생략하며, cartograph는 선택 필드로 읽는다.\n */\nexport function createCartographRetentionsDocument(\n joined: BridgeJoinResult,\n generatedAt: string,\n producerVersion: string,\n messages?: MessageBridgeJoin,\n): CartographRetentionsDocument {\n if (isBridgeJoinDeferred(joined)) {\n throw new RetentionValidationError(\n 'Cannot create retentions from a deferred bridge join.',\n );\n }\n rejectUnresolvedSwiftHandlers(joined, messages);\n const omittedObjectiveCHandlers = countObjectiveCHandlers(joined, messages);\n return {\n format: 'external-retentions',\n version: 0,\n producedBy: { name: 'isthmus', version: producerVersion },\n generatedAt,\n retentions: [\n ...collectCartographRetentions(joined),\n ...(messages === undefined ? [] : collectMessageRetentions(messages)),\n ],\n ...(omittedObjectiveCHandlers === 0 ? {} : { omittedObjectiveCHandlers }),\n };\n}\n\n/** 그래프 밖의 매치도 사라지지 않게 채널·메서드·위치별로 센다. */\nfunction countObjectiveCHandlers(\n joined: BridgeJoinResult,\n messages?: MessageBridgeJoin,\n): number {\n const keys = new Set<string>();\n for (const method of joined.matchedMethods) {\n for (const handler of method.handlers) {\n if (handler.sourceLanguage !== 'objective-c') continue;\n keys.add(JSON.stringify([method.target, method.channel, method.method,\n handler.location.path, handler.location.line, handler.location.column]));\n }\n }\n if (messages !== undefined) {\n for (const route of retainedMessageRoutes(messages)) {\n for (const handler of route.handlers) {\n if (handler.sourceLanguage !== 'objective-c') continue;\n keys.add(JSON.stringify(['v2', route.transport, route.channel,\n handler.location.path, handler.location.line, handler.location.column]));\n }\n }\n }\n return keys.size;\n}\n\n/**\n * 심볼이 없어 보존 근거로 바꿀 수 없는 매치 Swift 핸들러를 거부한다.\n * sourceLanguage로 확인된 Objective-C 구현은 Swift 그래프의 보존 대상이 아니다.\n *\n * 교환 계약에서 `symbol`은 선택 필드다. 호출자가 있는데도 근거를 만들지 못한\n * 핸들러를 조용히 빼면 cartograph는 그 핸들러를 계속 미사용으로 보고하고,\n * 소비자는 살아 있는 코드를 지운다. 부분 보존 문서 대신 실패를 돌려준다.\n */\nfunction rejectUnresolvedSwiftHandlers(\n joined: BridgeJoinResult,\n messages?: MessageBridgeJoin,\n): void {\n const unresolved = new Set<string>();\n for (const method of joined.matchedMethods) {\n for (const handler of method.handlers) {\n if (handler.platform !== 'swift' || handler.sourceLanguage === 'objective-c' || handler.symbol !== undefined) continue;\n const { path, line, column } = handler.location;\n unresolved.add(`${path}\\u0000${line}\\u0000${column}`);\n }\n }\n if (messages !== undefined) {\n for (const route of retainedMessageRoutes(messages)) {\n for (const handler of route.handlers) {\n if (handler.platform !== 'swift' || handler.sourceLanguage === 'objective-c' || handler.symbol !== undefined) continue;\n const { path, line, column } = handler.location;\n unresolved.add(`${path}\\u0000${line}\\u0000${column}`);\n }\n }\n }\n if (unresolved.size === 0) return;\n throw new RetentionValidationError(\n `Cannot produce retention evidence for ${unresolved.size} matched swift `\n + 'handlers without a symbol; regenerate the swift document with a producer '\n + 'that attaches handler symbols.',\n );\n}\n\n/** 매치별 Dart 호출자와 Swift 심볼을 cartograph 근거로 결합한다. */\nfunction collectCartographRetentions(\n joined: BridgeJoinResult,\n): ExternalRetention[] {\n const retentions: ExternalRetention[] = [];\n const seen = new Set<string>();\n let callerEntries = 0;\n for (const method of joined.matchedMethods) {\n if (method.invocations.length === 0) continue;\n // 결정적 순서(플랫폼·경로·줄·열)의 첫 호출이 대표 증거다. 상한 밖 호출까지\n // 객체로 만들지 않도록 먼저 자른다.\n const callers = method.invocations\n .slice(0, MAX_RETENTION_CALLERS)\n .map(toRetentionCaller);\n const callersOmitted = method.invocations.length - callers.length;\n const representative = callers[0];\n if (representative === undefined) continue;\n for (const handler of method.handlers) {\n if (handler.platform !== 'swift' || handler.sourceLanguage === 'objective-c' || handler.symbol === undefined) continue;\n const symbolKey = handler.symbol.usr === undefined\n ? `name:${handler.symbol.qualifiedName}`\n : `usr:${handler.symbol.usr}`;\n const retentionKey = `${symbolKey}\\u0000${method.channel}\\u0000${method.method}`;\n if (seen.has(retentionKey)) continue;\n seen.add(retentionKey);\n callerEntries += callers.length;\n if (callerEntries > MAX_RETENTION_CALLER_ENTRIES) {\n throw new RetentionValidationError(\n `Cannot produce retention evidence with more than `\n + `${MAX_RETENTION_CALLER_ENTRIES} caller entries; narrow the join inputs.`,\n );\n }\n retentions.push({\n symbol: handler.symbol,\n reason: 'bridge',\n evidence: {\n channel: method.channel,\n method: method.method,\n caller: representative,\n ...(callers.length > 1 ? { callers } : {}),\n ...(callersOmitted > 0 ? { callersOmitted } : {}),\n },\n });\n }\n }\n return retentions;\n}\n\n/** literal로 확정되고 호출자가 있는 v2 경계만 보존 후보로 돌려준다. */\nfunction retainedMessageRoutes(messages: MessageBridgeJoin) {\n return messages.routes.filter((route) =>\n route.matching === 'literal' && route.senders.length > 0);\n}\n\n/** 매치된 v2 경계의 송신자를 Swift 심볼 보존 근거로 결합한다. */\nfunction collectMessageRetentions(\n messages: MessageBridgeJoin,\n): ExternalRetention[] {\n const retentions: ExternalRetention[] = [];\n const seen = new Set<string>();\n let callerEntries = 0;\n for (const route of retainedMessageRoutes(messages)) {\n const callers = route.senders\n .slice(0, MAX_RETENTION_CALLERS)\n .map(toRetentionCaller);\n const callersOmitted = route.senders.length - callers.length;\n const representative = callers[0];\n if (representative === undefined) continue;\n for (const handler of route.handlers) {\n if (handler.platform !== 'swift' || handler.sourceLanguage === 'objective-c' || handler.symbol === undefined) continue;\n const symbolKey = handler.symbol.usr === undefined\n ? `name:${handler.symbol.qualifiedName}`\n : `usr:${handler.symbol.usr}`;\n // transport까지 키에 넣어 같은 이름의 MethodChannel 보존과 섞이지 않게 한다.\n const retentionKey = `${symbolKey}\\u0000${route.transport}\\u0000${route.channel}`;\n if (seen.has(retentionKey)) continue;\n seen.add(retentionKey);\n callerEntries += callers.length;\n if (callerEntries > MAX_RETENTION_CALLER_ENTRIES) {\n throw new RetentionValidationError(\n `Cannot produce retention evidence with more than `\n + `${MAX_RETENTION_CALLER_ENTRIES} caller entries; narrow the join inputs.`,\n );\n }\n retentions.push({\n symbol: handler.symbol,\n reason: 'bridge',\n evidence: {\n channel: route.channel,\n caller: representative,\n ...(callers.length > 1 ? { callers } : {}),\n ...(callersOmitted > 0 ? { callersOmitted } : {}),\n },\n });\n }\n }\n return retentions;\n}\n\n/** 증거 끝점을 external-retentions의 호출자 형태로 바꾼다. */\nfunction toRetentionCaller(endpoint: {\n readonly platform: BridgePlatform;\n readonly location: { readonly path: string; readonly line: number };\n}): RetentionCaller {\n return {\n platform: endpoint.platform,\n path: endpoint.location.path,\n line: endpoint.location.line,\n };\n}\n"]}
@@ -0,0 +1,16 @@
1
+ import type { CheckIssueCode } from './check-report.ts';
2
+ /**
3
+ * 논리 이슈 키를 안정 지문으로 바꾸는 주입 경계다. 해싱은 cli 계층이 담당한다.
4
+ * SARIF와 Code Quality가 같은 지문을 써야 두 아티팩트의 병합 기준이 일치한다.
5
+ */
6
+ export type IssueFingerprint = (issue: {
7
+ readonly code: string;
8
+ readonly target: string;
9
+ readonly channel: string;
10
+ readonly method?: string;
11
+ }) => string;
12
+ /**
13
+ * check가 보고하는 진단 종류의 산문 설명이다. SARIF 규칙 문구와 Code Quality
14
+ * 발견 설명이 같은 표를 써야 두 형식이 같은 이슈를 다르게 말하지 않는다.
15
+ */
16
+ export declare const checkIssueRuleDescriptions: Record<CheckIssueCode, string>;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * check가 보고하는 진단 종류의 산문 설명이다. SARIF 규칙 문구와 Code Quality
3
+ * 발견 설명이 같은 표를 써야 두 형식이 같은 이슈를 다르게 말하지 않는다.
4
+ */
5
+ export const checkIssueRuleDescriptions = {
6
+ 'unhandled-invocation': 'A caller-side bridge method invocation has no matching handler on any receiver-side document.',
7
+ 'unhandled-invocation-unverified': 'A caller-side bridge method invocation has no matching handler, and a receiver-side analysis gap may be hiding it.',
8
+ 'unregistered-channel-creation': 'A caller-side bridge channel creation has no matching registration on any receiver-side document.',
9
+ 'unregistered-channel-creation-unverified': 'A caller-side bridge channel creation has no matching registration, and a receiver-side analysis gap may be hiding it.',
10
+ 'registration-without-creation': 'A receiver-side channel registration has no matching caller-side channel creation.',
11
+ 'handler-without-invocation': 'A receiver-side bridge method handler has no matching caller-side invocation.',
12
+ 'module-import-without-export': 'A caller-side native module import has no matching export on any receiver-side document.',
13
+ 'module-import-without-export-unverified': 'A caller-side native module import has no matching export, and a receiver-side analysis gap may be hiding it.',
14
+ 'module-import-without-export-optional': 'A caller-side native module import has no matching export, but every observed caller tolerates absence by receiving null.',
15
+ 'module-import-mechanism-mismatch': 'A caller-side native module import matches an export name, but the observed exports resolve through a different bridge mechanism.',
16
+ 'module-export-without-import': 'A receiver-side native module export has no matching caller-side import.',
17
+ 'module-export-mechanism-mismatch': 'A receiver-side native module export matches an import name, but the observed imports resolve through a different bridge mechanism.',
18
+ 'component-require-without-export': 'A caller-side native component require has no matching export on any receiver-side document.',
19
+ 'component-require-without-export-unverified': 'A caller-side native component require has no matching export, and a receiver-side analysis gap may be hiding it.',
20
+ 'component-require-mechanism-mismatch': 'A caller-side native component require matches an export name, but the observed exports resolve through a different bridge mechanism.',
21
+ 'component-export-without-require': 'A receiver-side native component export has no matching caller-side require.',
22
+ 'component-export-mechanism-mismatch': 'A receiver-side native component export matches a require name, but the observed requires resolve through a different bridge mechanism.',
23
+ 'unhandled-message-send': 'A Dart BasicMessageChannel send has no matching native message handler on any receiver-side document.',
24
+ 'unhandled-message-send-unverified': 'A Dart BasicMessageChannel send has no matching native message handler, and a receiver-side analysis gap may be hiding it.',
25
+ 'message-handler-without-send': 'A native BasicMessageChannel handler has no matching Dart send.',
26
+ 'unhandled-stream-listen': 'A Dart EventChannel stream listener has no matching native stream handler on any receiver-side document.',
27
+ 'unhandled-stream-listen-unverified': 'A Dart EventChannel stream listener has no matching native stream handler, and a receiver-side analysis gap may be hiding it.',
28
+ 'stream-handler-without-listen': 'A native EventChannel stream handler has no matching Dart stream listener.',
29
+ };
30
+ //# sourceMappingURL=rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.js","sourceRoot":"","sources":["../../src/report/rules.ts"],"names":[],"mappings":"AAaA;;;GAGG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAmC;IACxE,sBAAsB,EACpB,+FAA+F;IACjG,iCAAiC,EAC/B,oHAAoH;IACtH,+BAA+B,EAC7B,mGAAmG;IACrG,0CAA0C,EACxC,wHAAwH;IAC1H,+BAA+B,EAC7B,oFAAoF;IACtF,4BAA4B,EAC1B,+EAA+E;IACjF,8BAA8B,EAC5B,0FAA0F;IAC5F,yCAAyC,EACvC,+GAA+G;IACjH,uCAAuC,EACrC,2HAA2H;IAC7H,kCAAkC,EAChC,mIAAmI;IACrI,8BAA8B,EAC5B,0EAA0E;IAC5E,kCAAkC,EAChC,qIAAqI;IACvI,kCAAkC,EAChC,8FAA8F;IAChG,6CAA6C,EAC3C,mHAAmH;IACrH,sCAAsC,EACpC,uIAAuI;IACzI,kCAAkC,EAChC,8EAA8E;IAChF,qCAAqC,EACnC,yIAAyI;IAC3I,wBAAwB,EACtB,uGAAuG;IACzG,mCAAmC,EACjC,4HAA4H;IAC9H,8BAA8B,EAC5B,iEAAiE;IACnE,yBAAyB,EACvB,0GAA0G;IAC5G,oCAAoC,EAClC,+HAA+H;IACjI,+BAA+B,EAC7B,4EAA4E;CAC/E,CAAC","sourcesContent":["import type { CheckIssueCode } from './check-report.ts';\n\n/**\n * 논리 이슈 키를 안정 지문으로 바꾸는 주입 경계다. 해싱은 cli 계층이 담당한다.\n * SARIF와 Code Quality가 같은 지문을 써야 두 아티팩트의 병합 기준이 일치한다.\n */\nexport type IssueFingerprint = (issue: {\n readonly code: string;\n readonly target: string;\n readonly channel: string;\n readonly method?: string;\n}) => string;\n\n/**\n * check가 보고하는 진단 종류의 산문 설명이다. SARIF 규칙 문구와 Code Quality\n * 발견 설명이 같은 표를 써야 두 형식이 같은 이슈를 다르게 말하지 않는다.\n */\nexport const checkIssueRuleDescriptions: Record<CheckIssueCode, string> = {\n 'unhandled-invocation':\n 'A caller-side bridge method invocation has no matching handler on any receiver-side document.',\n 'unhandled-invocation-unverified':\n 'A caller-side bridge method invocation has no matching handler, and a receiver-side analysis gap may be hiding it.',\n 'unregistered-channel-creation':\n 'A caller-side bridge channel creation has no matching registration on any receiver-side document.',\n 'unregistered-channel-creation-unverified':\n 'A caller-side bridge channel creation has no matching registration, and a receiver-side analysis gap may be hiding it.',\n 'registration-without-creation':\n 'A receiver-side channel registration has no matching caller-side channel creation.',\n 'handler-without-invocation':\n 'A receiver-side bridge method handler has no matching caller-side invocation.',\n 'module-import-without-export':\n 'A caller-side native module import has no matching export on any receiver-side document.',\n 'module-import-without-export-unverified':\n 'A caller-side native module import has no matching export, and a receiver-side analysis gap may be hiding it.',\n 'module-import-without-export-optional':\n 'A caller-side native module import has no matching export, but every observed caller tolerates absence by receiving null.',\n 'module-import-mechanism-mismatch':\n 'A caller-side native module import matches an export name, but the observed exports resolve through a different bridge mechanism.',\n 'module-export-without-import':\n 'A receiver-side native module export has no matching caller-side import.',\n 'module-export-mechanism-mismatch':\n 'A receiver-side native module export matches an import name, but the observed imports resolve through a different bridge mechanism.',\n 'component-require-without-export':\n 'A caller-side native component require has no matching export on any receiver-side document.',\n 'component-require-without-export-unverified':\n 'A caller-side native component require has no matching export, and a receiver-side analysis gap may be hiding it.',\n 'component-require-mechanism-mismatch':\n 'A caller-side native component require matches an export name, but the observed exports resolve through a different bridge mechanism.',\n 'component-export-without-require':\n 'A receiver-side native component export has no matching caller-side require.',\n 'component-export-mechanism-mismatch':\n 'A receiver-side native component export matches a require name, but the observed requires resolve through a different bridge mechanism.',\n 'unhandled-message-send':\n 'A Dart BasicMessageChannel send has no matching native message handler on any receiver-side document.',\n 'unhandled-message-send-unverified':\n 'A Dart BasicMessageChannel send has no matching native message handler, and a receiver-side analysis gap may be hiding it.',\n 'message-handler-without-send':\n 'A native BasicMessageChannel handler has no matching Dart send.',\n 'unhandled-stream-listen':\n 'A Dart EventChannel stream listener has no matching native stream handler on any receiver-side document.',\n 'unhandled-stream-listen-unverified':\n 'A Dart EventChannel stream listener has no matching native stream handler, and a receiver-side analysis gap may be hiding it.',\n 'stream-handler-without-listen':\n 'A native EventChannel stream handler has no matching Dart stream listener.',\n};\n"]}
@@ -1,11 +1,6 @@
1
1
  import type { CheckIssueCode, CheckReport } from './check-report.ts';
2
- /** 논리 이슈 키를 안정 지문으로 바꾸는 주입 경계다. 해싱은 cli 계층이 담당한다. */
3
- export type IssueFingerprint = (issue: {
4
- readonly code: string;
5
- readonly target: string;
6
- readonly channel: string;
7
- readonly method?: string;
8
- }) => string;
2
+ import { type IssueFingerprint } from './rules.ts';
3
+ export type { IssueFingerprint } from './rules.ts';
9
4
  /** GitHub code scanning이 받아들이는 SARIF 2.1.0 스키마 식별자다. */
10
5
  export declare const sarifSchema = "https://json.schemastore.org/sarif-2.1.0.json";
11
6
  /** 도구 소개 문서의 안정적인 위치다. */
@@ -78,4 +73,3 @@ export declare function encodeSarifLog(log: SarifLog): string;
78
73
  * 인코딩해 공백·`#`·비ASCII 파일명이 GitHub 업로드를 깨지지 않게 한다.
79
74
  */
80
75
  export declare function sarifUri(path: string): string;
81
- export {};
@@ -1,23 +1,15 @@
1
1
  import { checkIssueCodes } from "./check-report.js";
2
+ import { checkIssueRuleDescriptions, } from "./rules.js";
2
3
  import { encodeSortedJson } from "./sorted-json.js";
3
4
  /** GitHub code scanning이 받아들이는 SARIF 2.1.0 스키마 식별자다. */
4
5
  export const sarifSchema = 'https://json.schemastore.org/sarif-2.1.0.json';
5
6
  /** 도구 소개 문서의 안정적인 위치다. */
6
7
  export const sarifInformationUri = 'https://github.com/ictechgy/isthmus';
7
- /** check가 보고하는 진단 종류의 SARIF 규칙 문구다. */
8
- const ruleDescriptions = {
9
- 'unhandled-invocation': 'A caller-side bridge method invocation has no matching handler on any receiver-side document.',
10
- 'unhandled-invocation-unverified': 'A caller-side bridge method invocation has no matching handler, and a receiver-side analysis gap may be hiding it.',
11
- 'unregistered-channel-creation': 'A caller-side bridge channel creation has no matching registration on any receiver-side document.',
12
- 'unregistered-channel-creation-unverified': 'A caller-side bridge channel creation has no matching registration, and a receiver-side analysis gap may be hiding it.',
13
- 'registration-without-creation': 'A receiver-side channel registration has no matching caller-side channel creation.',
14
- 'handler-without-invocation': 'A receiver-side bridge method handler has no matching caller-side invocation.',
15
- };
16
8
  /** check 보고서를 SARIF 2.1.0 로그로 바꾼다. */
17
9
  export function createSarifLog(report, toolVersion, issueFingerprint) {
18
10
  const rules = [...checkIssueCodes].sort().map((id) => ({
19
11
  id,
20
- shortDescription: { text: ruleDescriptions[id] },
12
+ shortDescription: { text: checkIssueRuleDescriptions[id] },
21
13
  }));
22
14
  const ruleIndex = new Map(rules.map(({ id }, index) => [id, index]));
23
15
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"sarif.js","sourceRoot":"","sources":["../../src/report/sarif.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAUpD,wDAAwD;AACxD,MAAM,CAAC,MAAM,WAAW,GAAG,+CAA+C,CAAC;AAE3E,0BAA0B;AAC1B,MAAM,CAAC,MAAM,mBAAmB,GAAG,qCAAqC,CAAC;AAiCzE,uCAAuC;AACvC,MAAM,gBAAgB,GAAmC;IACvD,sBAAsB,EACpB,+FAA+F;IACjG,iCAAiC,EAC/B,oHAAoH;IACtH,+BAA+B,EAC7B,mGAAmG;IACrG,0CAA0C,EACxC,wHAAwH;IAC1H,+BAA+B,EAC7B,oFAAoF;IACtF,4BAA4B,EAC1B,+EAA+E;CAClF,CAAC;AAsBF,sCAAsC;AACtC,MAAM,UAAU,cAAc,CAC5B,MAAmB,EACnB,WAA+B,EAC/B,gBAAkC;IAElC,MAAM,KAAK,GAAG,CAAC,GAAG,eAAe,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QACrD,EAAE;QACF,gBAAgB,EAAE,EAAE,IAAI,EAAE,gBAAgB,CAAC,EAAE,CAAC,EAAE;KACjD,CAAC,CAAC,CAAC;IACJ,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IACrE,OAAO;QACL,OAAO,EAAE,WAAW;QACpB,OAAO,EAAE,OAAO;QAChB,IAAI,EAAE,CAAC;gBACL,IAAI,EAAE;oBACJ,MAAM,EAAE;wBACN,IAAI,EAAE,SAAS;wBACf,cAAc,EAAE,mBAAmB;wBACnC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;wBAChD,KAAK;qBACN;iBACF;gBACD,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACnC,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,gBAAgB,CAAC,CAChD;aACF,CAAC;KACH,CAAC;AACJ,CAAC;AAED,sCAAsC;AACtC,MAAM,UAAU,cAAc,CAAC,GAAa;IAC1C,OAAO,gBAAgB,CAAC,GAAG,CAAC,CAAC;AAC/B,CAAC;AAED,0CAA0C;AAC1C,SAAS,WAAW,CAClB,KAAiB,EACjB,SAA8B,EAC9B,gBAAkC;IAElC,MAAM,CAAC,OAAO,EAAE,GAAG,OAAO,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC;IAC7C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IACD,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;IACvE,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,KAAK,SAAS;QACxC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,gBAAgB,KAAK,CAAC,OAAO,GAAG;QAC/C,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,gBAAgB,KAAK,CAAC,OAAO,iBAAiB,KAAK,CAAC,MAAM,GAAG,CAAC;IAC/E,OAAO;QACL,MAAM,EAAE,KAAK,CAAC,IAAI;QAClB,SAAS,EAAE,KAAK;QAChB,KAAK,EAAE,KAAK,CAAC,QAAQ;QACrB,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;QAC1B,SAAS,EAAE,CAAC,EAAE,gBAAgB,EAAE,gBAAgB,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5D,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YAC9B,gBAAgB,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;gBAClD,EAAE,EAAE,KAAK,GAAG,CAAC;gBACb,gBAAgB,EAAE,gBAAgB,CAAC,QAAQ,CAAC;aAC7C,CAAC,CAAC;SACJ,CAAC;QACF,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,IAAI;YAC3B,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,IAAI,EAAE,UAAmB,EAAE,MAAM,EAAE,UAAmB,EAAE,CAAC,EAAE;YAChF,CAAC,CAAC,EAAE,CAAC;QACP,UAAU,EAAE;YACV,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC;SAChE;QACD,mBAAmB,EAAE;YACnB,cAAc,EAAE,gBAAgB,CAAC,KAAK,CAAC;SACxC;KACF,CAAC;AACJ,CAAC;AAED,mDAAmD;AACnD,SAAS,gBAAgB,CAAC,QAEzB;IACC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,CAAC,QAAQ,CAAC;IAC3C,IAAI,IAAI,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;IACnF,CAAC;IACD,OAAO;QACL,gBAAgB,EAAE,EAAE,GAAG,EAAE,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE;QAC3D,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE;KACjD,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3D,CAAC","sourcesContent":["import type {\n CheckIssue,\n CheckIssueCode,\n CheckReport,\n} from './check-report.ts';\nimport { checkIssueCodes } from './check-report.ts';\nimport { encodeSortedJson } from './sorted-json.ts';\n\n/** 논리 이슈 키를 안정 지문으로 바꾸는 주입 경계다. 해싱은 cli 계층이 담당한다. */\nexport type IssueFingerprint = (issue: {\n readonly code: string;\n readonly target: string;\n readonly channel: string;\n readonly method?: string;\n}) => string;\n\n/** GitHub code scanning이 받아들이는 SARIF 2.1.0 스키마 식별자다. */\nexport const sarifSchema = 'https://json.schemastore.org/sarif-2.1.0.json';\n\n/** 도구 소개 문서의 안정적인 위치다. */\nexport const sarifInformationUri = 'https://github.com/ictechgy/isthmus';\n\n/** SARIF 물리 위치다. 경로는 프로젝트 루트 기준 상대 경로를 그대로 쓴다. */\ninterface SarifPhysicalLocation {\n readonly artifactLocation: { readonly uri: string };\n readonly region: { readonly startLine: number; readonly startColumn: number };\n}\n\n/** 결과 하나가 위치(규칙·수준·메시지·근거)를 다 담는 형태다. */\ninterface SarifResult {\n readonly ruleId: CheckIssueCode;\n readonly ruleIndex: number;\n readonly level: 'error' | 'warning';\n readonly message: { readonly text: string };\n readonly locations: ReadonlyArray<{\n readonly physicalLocation: SarifPhysicalLocation;\n }>;\n readonly relatedLocations?: ReadonlyArray<{\n readonly id: number;\n readonly physicalLocation: SarifPhysicalLocation;\n }>;\n readonly suppressions?: ReadonlyArray<{\n readonly kind: 'external';\n readonly status: 'accepted';\n }>;\n readonly properties: {\n readonly target: string;\n readonly channel: string;\n readonly method?: string;\n };\n readonly partialFingerprints: { readonly isthmusIssueV1: string };\n}\n\n/** check가 보고하는 진단 종류의 SARIF 규칙 문구다. */\nconst ruleDescriptions: Record<CheckIssueCode, string> = {\n 'unhandled-invocation':\n 'A caller-side bridge method invocation has no matching handler on any receiver-side document.',\n 'unhandled-invocation-unverified':\n 'A caller-side bridge method invocation has no matching handler, and a receiver-side analysis gap may be hiding it.',\n 'unregistered-channel-creation':\n 'A caller-side bridge channel creation has no matching registration on any receiver-side document.',\n 'unregistered-channel-creation-unverified':\n 'A caller-side bridge channel creation has no matching registration, and a receiver-side analysis gap may be hiding it.',\n 'registration-without-creation':\n 'A receiver-side channel registration has no matching caller-side channel creation.',\n 'handler-without-invocation':\n 'A receiver-side bridge method handler has no matching caller-side invocation.',\n};\n\n/** SARIF 2.1.0 로그 문서다. 결정적 정렬로 인코딩한다. */\nexport interface SarifLog {\n readonly $schema: string;\n readonly version: '2.1.0';\n readonly runs: ReadonlyArray<{\n readonly tool: {\n readonly driver: {\n readonly name: 'isthmus';\n readonly informationUri: string;\n readonly version?: string;\n readonly rules: ReadonlyArray<{\n readonly id: CheckIssueCode;\n readonly shortDescription: { readonly text: string };\n }>;\n };\n };\n readonly results: readonly SarifResult[];\n }>;\n}\n\n/** check 보고서를 SARIF 2.1.0 로그로 바꾼다. */\nexport function createSarifLog(\n report: CheckReport,\n toolVersion: string | undefined,\n issueFingerprint: IssueFingerprint,\n): SarifLog {\n const rules = [...checkIssueCodes].sort().map((id) => ({\n id,\n shortDescription: { text: ruleDescriptions[id] },\n }));\n const ruleIndex = new Map(rules.map(({ id }, index) => [id, index]));\n return {\n $schema: sarifSchema,\n version: '2.1.0',\n runs: [{\n tool: {\n driver: {\n name: 'isthmus',\n informationUri: sarifInformationUri,\n ...(toolVersion ? { version: toolVersion } : {}),\n rules,\n },\n },\n results: report.issues.map((issue) =>\n sarifResult(issue, ruleIndex, issueFingerprint)\n ),\n }],\n };\n}\n\n/** SARIF 로그를 결정적인 JSON 문자열로 인코딩한다. */\nexport function encodeSarifLog(log: SarifLog): string {\n return encodeSortedJson(log);\n}\n\n/** 논리 이슈 하나를 규칙 참조와 물리 근거를 갖춘 결과로 바꾼다. */\nfunction sarifResult(\n issue: CheckIssue,\n ruleIndex: Map<string, number>,\n issueFingerprint: IssueFingerprint,\n): SarifResult {\n const [primary, ...related] = issue.evidence;\n if (primary === undefined) {\n throw new Error('Cannot create a SARIF result without evidence.');\n }\n const index = ruleIndex.get(issue.code);\n if (index === undefined) {\n throw new Error('Cannot create a SARIF result for an unknown rule.');\n }\n const subject = issue.method === undefined\n ? `${issue.code} on channel '${issue.channel}'`\n : `${issue.code} on channel '${issue.channel}' for method '${issue.method}'`;\n return {\n ruleId: issue.code,\n ruleIndex: index,\n level: issue.severity,\n message: { text: subject },\n locations: [{ physicalLocation: physicalLocation(primary) }],\n ...(related.length === 0 ? {} : {\n relatedLocations: related.map((endpoint, order) => ({\n id: order + 1,\n physicalLocation: physicalLocation(endpoint),\n })),\n }),\n ...(issue.suppressed === true\n ? { suppressions: [{ kind: 'external' as const, status: 'accepted' as const }] }\n : {}),\n properties: {\n target: issue.target,\n channel: issue.channel,\n ...(issue.method === undefined ? {} : { method: issue.method }),\n },\n partialFingerprints: {\n isthmusIssueV1: issueFingerprint(issue),\n },\n };\n}\n\n/** 증거 끝점을 SARIF 물리 위치로 바꾼다. 줄·열은 계약과 같은 1 기반이다. */\nfunction physicalLocation(endpoint: {\n readonly location: { readonly path: string; readonly line: number; readonly column: number };\n}): SarifPhysicalLocation {\n const { line, column } = endpoint.location;\n if (line < 1 || column < 1) {\n throw new Error('Cannot create a SARIF region before the first line or column.');\n }\n return {\n artifactLocation: { uri: sarifUri(endpoint.location.path) },\n region: { startLine: line, startColumn: column },\n };\n}\n\n/**\n * 경로를 URI 참조로 만든다. 구분자 `/`는 보존하고 각 세그먼트를 RFC 3986으로\n * 인코딩해 공백·`#`·비ASCII 파일명이 GitHub 업로드를 깨지지 않게 한다.\n */\nexport function sarifUri(path: string): string {\n return path.split('/').map(encodeURIComponent).join('/');\n}\n"]}
1
+ {"version":3,"file":"sarif.js","sourceRoot":"","sources":["../../src/report/sarif.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n CheckIssue,\n CheckIssueCode,\n CheckReport,\n} from './check-report.ts';\nimport { checkIssueCodes } from './check-report.ts';\nimport {\n checkIssueRuleDescriptions,\n type IssueFingerprint,\n} from './rules.ts';\nimport { encodeSortedJson } from './sorted-json.ts';\n\nexport type { IssueFingerprint } from './rules.ts';\n\n/** GitHub code scanning이 받아들이는 SARIF 2.1.0 스키마 식별자다. */\nexport const sarifSchema = 'https://json.schemastore.org/sarif-2.1.0.json';\n\n/** 도구 소개 문서의 안정적인 위치다. */\nexport const sarifInformationUri = 'https://github.com/ictechgy/isthmus';\n\n/** SARIF 물리 위치다. 경로는 프로젝트 루트 기준 상대 경로를 그대로 쓴다. */\ninterface SarifPhysicalLocation {\n readonly artifactLocation: { readonly uri: string };\n readonly region: { readonly startLine: number; readonly startColumn: number };\n}\n\n/** 결과 하나가 위치(규칙·수준·메시지·근거)를 다 담는 형태다. */\ninterface SarifResult {\n readonly ruleId: CheckIssueCode;\n readonly ruleIndex: number;\n readonly level: 'error' | 'warning';\n readonly message: { readonly text: string };\n readonly locations: ReadonlyArray<{\n readonly physicalLocation: SarifPhysicalLocation;\n }>;\n readonly relatedLocations?: ReadonlyArray<{\n readonly id: number;\n readonly physicalLocation: SarifPhysicalLocation;\n }>;\n readonly suppressions?: ReadonlyArray<{\n readonly kind: 'external';\n readonly status: 'accepted';\n }>;\n readonly properties: {\n readonly target: string;\n readonly channel: string;\n readonly method?: string;\n };\n readonly partialFingerprints: { readonly isthmusIssueV1: string };\n}\n\n/** SARIF 2.1.0 로그 문서다. 결정적 정렬로 인코딩한다. */\nexport interface SarifLog {\n readonly $schema: string;\n readonly version: '2.1.0';\n readonly runs: ReadonlyArray<{\n readonly tool: {\n readonly driver: {\n readonly name: 'isthmus';\n readonly informationUri: string;\n readonly version?: string;\n readonly rules: ReadonlyArray<{\n readonly id: CheckIssueCode;\n readonly shortDescription: { readonly text: string };\n }>;\n };\n };\n readonly results: readonly SarifResult[];\n }>;\n}\n\n/** check 보고서를 SARIF 2.1.0 로그로 바꾼다. */\nexport function createSarifLog(\n report: CheckReport,\n toolVersion: string | undefined,\n issueFingerprint: IssueFingerprint,\n): SarifLog {\n const rules = [...checkIssueCodes].sort().map((id) => ({\n id,\n shortDescription: { text: checkIssueRuleDescriptions[id] },\n }));\n const ruleIndex = new Map(rules.map(({ id }, index) => [id, index]));\n return {\n $schema: sarifSchema,\n version: '2.1.0',\n runs: [{\n tool: {\n driver: {\n name: 'isthmus',\n informationUri: sarifInformationUri,\n ...(toolVersion ? { version: toolVersion } : {}),\n rules,\n },\n },\n results: report.issues.map((issue) =>\n sarifResult(issue, ruleIndex, issueFingerprint)\n ),\n }],\n };\n}\n\n/** SARIF 로그를 결정적인 JSON 문자열로 인코딩한다. */\nexport function encodeSarifLog(log: SarifLog): string {\n return encodeSortedJson(log);\n}\n\n/** 논리 이슈 하나를 규칙 참조와 물리 근거를 갖춘 결과로 바꾼다. */\nfunction sarifResult(\n issue: CheckIssue,\n ruleIndex: Map<string, number>,\n issueFingerprint: IssueFingerprint,\n): SarifResult {\n const [primary, ...related] = issue.evidence;\n if (primary === undefined) {\n throw new Error('Cannot create a SARIF result without evidence.');\n }\n const index = ruleIndex.get(issue.code);\n if (index === undefined) {\n throw new Error('Cannot create a SARIF result for an unknown rule.');\n }\n const subject = issue.method === undefined\n ? `${issue.code} on channel '${issue.channel}'`\n : `${issue.code} on channel '${issue.channel}' for method '${issue.method}'`;\n return {\n ruleId: issue.code,\n ruleIndex: index,\n level: issue.severity,\n message: { text: subject },\n locations: [{ physicalLocation: physicalLocation(primary) }],\n ...(related.length === 0 ? {} : {\n relatedLocations: related.map((endpoint, order) => ({\n id: order + 1,\n physicalLocation: physicalLocation(endpoint),\n })),\n }),\n ...(issue.suppressed === true\n ? { suppressions: [{ kind: 'external' as const, status: 'accepted' as const }] }\n : {}),\n properties: {\n target: issue.target,\n channel: issue.channel,\n ...(issue.method === undefined ? {} : { method: issue.method }),\n },\n partialFingerprints: {\n isthmusIssueV1: issueFingerprint(issue),\n },\n };\n}\n\n/** 증거 끝점을 SARIF 물리 위치로 바꾼다. 줄·열은 계약과 같은 1 기반이다. */\nfunction physicalLocation(endpoint: {\n readonly location: { readonly path: string; readonly line: number; readonly column: number };\n}): SarifPhysicalLocation {\n const { line, column } = endpoint.location;\n if (line < 1 || column < 1) {\n throw new Error('Cannot create a SARIF region before the first line or column.');\n }\n return {\n artifactLocation: { uri: sarifUri(endpoint.location.path) },\n region: { startLine: line, startColumn: column },\n };\n}\n\n/**\n * 경로를 URI 참조로 만든다. 구분자 `/`는 보존하고 각 세그먼트를 RFC 3986으로\n * 인코딩해 공백·`#`·비ASCII 파일명이 GitHub 업로드를 깨지지 않게 한다.\n */\nexport function sarifUri(path: string): string {\n return path.split('/').map(encodeURIComponent).join('/');\n}\n"]}
@@ -0,0 +1,87 @@
1
+ # EventChannel 교환 계약 (bridge-facts v2)
2
+
3
+ bridge-facts v1 MethodChannel·[v2 BasicMessageChannel](BRIDGE-MESSAGES.md) 동작과 섞지
4
+ 않고 opt-in `bridges --events`가 아래 v2 문서를 출력한다. Flutter `EventChannel`은
5
+ 호출/응답이 아니라 네이티브→Dart 방향의 스트림이므로 사실 종류·전파 의미가 별도다.
6
+ 런타임 스트림 구독과 취소의 동작은 [RUNTIME](RUNTIME.md)의 범위며, 이 문서는 정적 사실만
7
+ 정의한다.
8
+
9
+ ```json
10
+ {
11
+ "format": "bridge-facts", "version": 2,
12
+ "transport": "event-channel", "platform": "dart", "target": "flutter",
13
+ "project": "/project", "generatedAt": "2026-09-16T00:00:00Z",
14
+ "tool": { "name": "dartograph", "version": "development" },
15
+ "facts": [{
16
+ "kind": "stream-listen", "channel": "dev.example/charging", "dynamic": false,
17
+ "location": { "path": "lib/battery.dart", "line": 49, "column": 9 },
18
+ "symbol": { "qualifiedName": "MethodChannelBattery.batteryStateStream" }
19
+ }],
20
+ "limitations": []
21
+ }
22
+ ```
23
+
24
+ - 기본 `bridges`는 기존 v1 출력을 유지하고 `--events`는 이 문서만 낸다. v1 문서의
25
+ `unscanned-event-channels` limitation은 v1 형식이 스트림을 담지 않는다는 사실을
26
+ 계속 보고한다 — event 사실이 존재한다는 뜻이 아니라 v1 관찰 범위의 설명이다.
27
+ - 사실 종류는 `stream-listen`(Dart `receiveBroadcastStream` 스트림 구독 지점)과
28
+ `stream-handle`(Swift/Kotlin `setStreamHandler`의 non-nil 등록, ObjC
29
+ `setStreamHandler:`) 둘뿐이다. `onListen`/`onCancel` 콜백과 `EventSink`의 이벤트
30
+ 방출은 별도 사실 종류로 만들지 않고 handler 근거의 의존성으로 남긴다.
31
+ - `EventChannel(name)` 객체를 만들기만 한 지점은 사실이 아니다 — 채널 이름은 생성자에서
32
+ `receiveBroadcastStream`/`setStreamHandler` 호출까지 변수 참조를 따라 옮긴다.
33
+ nil handler 제거(`setStreamHandler(nil)`)는 등록이 아니다.
34
+ - method 필드는 없다. 스트림 채널에 가상의 메서드 이름을 붙이지 않는다.
35
+ - project·위치·symbol·시각·문자열·사실 수·미귀속 규칙은 기존 계약의 근거 보존 원칙을
36
+ 따른다. listener는 dart, handler는 swift·kotlin 또는 `sourceLanguage: "objective-c"`의
37
+ ObjC 구현이다. 사실이 없으면 target은 null이다.
38
+ - 정적 이름은 decoded literal이다. 알 수 없으면 원래 표현식을 channel에 두고
39
+ dynamic=true다. 선택적 `channelPrefix`는 dynamic일 때만 쓰며, 의미와 한계는
40
+ [BRIDGE-MESSAGES](BRIDGE-MESSAGES.md)의 prefix 규칙과 같다.
41
+ - Native channel을 귀속할 수 없으면 channel=null이며 `unattributed-stream-handles:`
42
+ limitation을 함께 낸다. 같은 transport의 같은 주소라도 MethodChannel/Basic과
43
+ transport를 구분해 조인한다.
44
+ - `check`는 v2 문서를 직접 입력으로 받아 Event 경계를 진단한다. literal
45
+ `stream-listen`에 대응 `stream-handle`이 없으면 `unhandled-stream-listen` error
46
+ (수신 공백이면 `-unverified` warning), 대응 listener 없는 `stream-handle`은
47
+ `stream-handler-without-listen` warning이다. dynamic prefix 후보는 항상
48
+ `dynamic-stream-address` 소비자 한계로 실리고, 상대가 없으면
49
+ `unmatched-stream-boundary`가 더해진다. prefix 없는 미해석 주소는
50
+ `unresolved-message-addresses` 한계로 남긴다.
51
+ - `query`는 `stream` kind 주체로, `graph`는 literal Event 경계를 `stream` 간선으로,
52
+ `diff`는 그 추가·삭제를 낸다. dynamic prefix 후보는 확정 경계가 아니다.
53
+ - `retentions --for cartograph`는 literal Event 경계의 Swift `stream-handle`을
54
+ method 없는 보존 근거로 다. `impact`는 v1 전용으로 version 2를 명시적으로
55
+ 거부한다. `preflight`는 context의 message 문서 목록으로 소비하며 transport 필드로
56
+ 구분한다 — 다른 명령이 모르는 facts를 무시하고 초록 결과를 내게 하지 않는다.
57
+
58
+ ## handler별 의존 근거
59
+
60
+ `handlerScope`·`dependencies` 선택 필드의 형태·완전성·상한·소비 의미는
61
+ [BRIDGE-MESSAGES](BRIDGE-MESSAGES.md)의 "handler별 의존 근거" 절과 같다. 스트림
62
+ 핸들러는 클로저가 아니라 `FlutterStreamHandler` 구현 객체를 넘기는 형태가 일반적이라
63
+ 대부분 근거가 없거나 불완전으로 남는다 — 그 경우 소비자는 넓은 영향 후보를 보존하고
64
+ 정밀도 공백을 알린다. 등록 선언을 직접 변경 대상으로 선택하면 그 선언의 스트림 등록을
65
+ 포함해 검토한다.
66
+
67
+ ## Objective-C 스트림 핸들러
68
+
69
+ `platform: "swift"` 문서에 실린 `.m`/`.mm`의 `setStreamHandler:` 사실은
70
+ `sourceLanguage: "objective-c"`로 구분한다. ObjC 핸들러 본문은 Swift 그래프 밖이므로
71
+ handler 근거를 싣지 않으며(계약이 금지), 언어 심볼 귀속 실패는 기존과 같이
72
+ unbindable 증거로 남는다.
73
+
74
+ ## 생산자별 범위
75
+
76
+ - cartograph: `FlutterEventChannel(name:)` + `setStreamHandler` 와 ObjC
77
+ `setStreamHandler:`를 읽는다. Basic과 같은 변수 추적을 쓰되 스트림 전용 등록
78
+ 호출만 사실로 만든다.
79
+ - kartograph: `EventChannel(messenger, name)` + `setStreamHandler`를 같은 규칙으로
80
+ 읽는다. JVM 그래프에서 유일하게 확인한 symbol만 USR로 싣는다.
81
+ - dartograph: `EventChannel(...)` 수신자의 `receiveBroadcastStream` 호출을
82
+ `stream-listen`으로 낸다. 수신자를 풀지 못한 스트림 호출은 사실로 만들지 않고
83
+ limitation으로 센다.
84
+
85
+ 이 문서만으로 세 생산자의 지원이 완료됐거나 Flutter의 모든 스트림 등록 형태가 해석됐다는
86
+ 뜻은 아니다. literal·동적 prefix·nil handler·메서드 참조 핸들러·동일 주소의 transport
87
+ 차이와 공개 플러그인 source를 테스트한 뒤 검증된 범위를 기록한다.
@@ -1,10 +1,10 @@
1
- # BasicMessageChannel 개발 교환 계약
1
+ # BasicMessageChannel 교환 계약 (bridge-facts v2)
2
2
 
3
- 2026-09-14 개발 소스. bridge-facts v1 MethodChannel 동작과 섞지 않고 opt-in
4
- `bridges --messages`가 아래 v2 문서를 출력한다. producer/consumer 구현과 공개 source
5
- 연결을 검증했으며 handler별 의존 근거 보강은 추가 검증 중이다.
6
- 현재 사용자에게 완성된 지원으로 안내하지 않는다. 자매 저장소의 기존 작업은 보존하며
7
- 별도 worktree에서 구현·검증한다. 최종 통합/PR/호환 검증은 [진행 기록](COMPETITIVENESS.md)에 남긴다.
3
+ bridge-facts v1 MethodChannel 동작과 섞지 않고 opt-in `bridges --messages`가 아래
4
+ v2 문서를 출력한다. 공개 호환 버전(cartograph 0.18.0·kartograph 0.10.2·dartograph
5
+ 0.14.0)에 포함됐으며 isthmus 0.7.0이 소비한다. producer/consumer 구현과 공개 source
6
+ 연결을 검증했으며, 플랫폼별 실제 실행·모든 Pigeon 생성 형태의 해석은 별도 검증 범위다.
7
+ 통합/검증 이력은 [진행 기록](COMPETITIVENESS.md)에 남긴다.
8
8
 
9
9
  ```json
10
10
  {
@@ -37,8 +37,23 @@
37
37
  - channelPrefix와 runtime 주소의 관계도 후보 근거다. 기존 MethodChannel과 동일한 주소를
38
38
  쓰더라도 transport를 구분한다. prefix 후보를 literal 조인이나 실제 native 실행 신원으로
39
39
  승격하지 않는다.
40
- - v1 전용 소비자는 version 2를 명시적으로 거부한다. 초기 소비 경계는 preflight context의
41
- 별도 message 문서 목록이며, 다른 명령이 모르는 facts를 무시하고 초록 결과를 내게 하지 않는다.
40
+ - `check`는 v2 문서를 직접 입력으로 받아 Basic 경계를 진단한다. literal send에 대응
41
+ `message-handle`이 없으면 `unhandled-message-send` error(수신 공백이면
42
+ `-unverified` warning), 대응 send 없는 `message-handle`은
43
+ `message-handler-without-send` warning이다. dynamic prefix 후보는 항상
44
+ `dynamic-message-address` 소비자 한계로 실리고, 상대가 없으면
45
+ `unmatched-message-boundary`가 더해진다. prefix 없는 미해석 주소는
46
+ `unresolved-message-addresses` 한계로 남긴다. literal 경계도 상대편을 prefix
47
+ 후보가 덮으면 error 대신 후보 한계로 내린다. `--format sarif`·`codequality`와
48
+ `--baseline`도 이 코드를 그대로 싣는다.
49
+ - `query`는 v2 경계를 `message` kind 주체(EventChannel은 `stream`)로, `graph`는
50
+ literal v2 경계를 `message`·`stream` 간선으로, `diff`는 literal v2 경계의 추가·삭제와
51
+ v2 진단의 introduced/resolved를 낸다. dynamic prefix 후보는 graph·diff의 확정
52
+ 경계가 아니다.
53
+ - `retentions --for cartograph`는 literal Basic 경계의 Swift `message-handle`을
54
+ method 없는 보존 근거로 다. `impact`는 v1 전용으로 version 2를 명시적으로
55
+ 거부한다. `preflight`는 context의 별도 message 문서 목록으로 소비한다 — 다른
56
+ 명령이 모르는 facts를 무시하고 초록 결과를 내게 하지 않는다.
42
57
 
43
58
  ## handler별 의존 근거 (개발 계약)
44
59