isthmus-cli 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +170 -45
- package/README.md +178 -36
- package/Skills/isthmus/SKILL.md +104 -5
- package/compatibility.json +10 -0
- package/dist/cli/check-command.d.ts +2 -2
- package/dist/cli/check-command.js +22 -9
- package/dist/cli/check-command.js.map +1 -1
- package/dist/cli/command-support.d.ts +20 -2
- package/dist/cli/command-support.js +38 -9
- package/dist/cli/command-support.js.map +1 -1
- package/dist/cli/diff-command.js +4 -3
- package/dist/cli/diff-command.js.map +1 -1
- package/dist/cli/doctor-command.d.ts +11 -0
- package/dist/cli/doctor-command.js +128 -0
- package/dist/cli/doctor-command.js.map +1 -0
- package/dist/cli/extract-js-command.d.ts +33 -0
- package/dist/cli/extract-js-command.js +233 -0
- package/dist/cli/extract-js-command.js.map +1 -0
- package/dist/cli/graph-command.js +13 -6
- package/dist/cli/graph-command.js.map +1 -1
- package/dist/cli/impact-command.d.ts +5 -0
- package/dist/cli/impact-command.js +111 -0
- package/dist/cli/impact-command.js.map +1 -0
- package/dist/cli/init-command.d.ts +11 -0
- package/dist/cli/init-command.js +115 -0
- package/dist/cli/init-command.js.map +1 -0
- package/dist/cli/main.js +67 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/mcp-server.d.ts +16 -0
- package/dist/cli/mcp-server.js +534 -0
- package/dist/cli/mcp-server.js.map +1 -0
- package/dist/cli/preflight-command.d.ts +5 -0
- package/dist/cli/preflight-command.js +83 -0
- package/dist/cli/preflight-command.js.map +1 -0
- package/dist/cli/query-command.js +13 -6
- package/dist/cli/query-command.js.map +1 -1
- package/dist/cli/retentions-command.js +14 -7
- package/dist/cli/retentions-command.js.map +1 -1
- package/dist/cli/runtime-command.d.ts +5 -0
- package/dist/cli/runtime-command.js +42 -0
- package/dist/cli/runtime-command.js.map +1 -0
- package/dist/cli/runtime-json-reader.d.ts +12 -0
- package/dist/cli/runtime-json-reader.js +37 -0
- package/dist/cli/runtime-json-reader.js.map +1 -0
- package/dist/cli/serve-command.d.ts +13 -0
- package/dist/cli/serve-command.js +43 -0
- package/dist/cli/serve-command.js.map +1 -0
- package/dist/exchange/capture-config.d.ts +18 -0
- package/dist/exchange/capture-config.js +100 -0
- package/dist/exchange/capture-config.js.map +1 -0
- package/dist/exchange/impact-selection.d.ts +13 -0
- package/dist/exchange/impact-selection.js +35 -0
- package/dist/exchange/impact-selection.js.map +1 -0
- package/dist/exchange/kartograph-impact.d.ts +4 -0
- package/dist/exchange/kartograph-impact.js +169 -0
- package/dist/exchange/kartograph-impact.js.map +1 -0
- package/dist/exchange/messages.d.ts +36 -0
- package/dist/exchange/messages.js +96 -0
- package/dist/exchange/messages.js.map +1 -0
- package/dist/exchange/parse.d.ts +69 -0
- package/dist/exchange/parse.js +127 -20
- package/dist/exchange/parse.js.map +1 -1
- package/dist/exchange/preflight-context.d.ts +71 -0
- package/dist/exchange/preflight-context.js +317 -0
- package/dist/exchange/preflight-context.js.map +1 -0
- package/dist/exchange/producer-impact.d.ts +17 -0
- package/dist/exchange/producer-impact.js +226 -0
- package/dist/exchange/producer-impact.js.map +1 -0
- package/dist/exchange/runtime.d.ts +69 -0
- package/dist/exchange/runtime.js +160 -0
- package/dist/exchange/runtime.js.map +1 -0
- package/dist/extract/js-document.d.ts +22 -0
- package/dist/extract/js-document.js +230 -0
- package/dist/extract/js-document.js.map +1 -0
- package/dist/extract/js-scan.d.ts +93 -0
- package/dist/extract/js-scan.js +1205 -0
- package/dist/extract/js-scan.js.map +1 -0
- package/dist/extract/lexer.d.ts +26 -0
- package/dist/extract/lexer.js +316 -0
- package/dist/extract/lexer.js.map +1 -0
- package/dist/join/join.d.ts +66 -1
- package/dist/join/join.js +171 -1
- package/dist/join/join.js.map +1 -1
- package/dist/join/message-address.d.ts +7 -0
- package/dist/join/message-address.js +37 -0
- package/dist/join/message-address.js.map +1 -0
- package/dist/join/messages.d.ts +26 -0
- package/dist/join/messages.js +87 -0
- package/dist/join/messages.js.map +1 -0
- package/dist/report/check-report.d.ts +20 -3
- package/dist/report/check-report.js +240 -8
- package/dist/report/check-report.js.map +1 -1
- package/dist/report/codequality.d.ts +45 -0
- package/dist/report/codequality.js +74 -0
- package/dist/report/codequality.js.map +1 -0
- package/dist/report/diff.d.ts +25 -3
- package/dist/report/diff.js +91 -10
- package/dist/report/diff.js.map +1 -1
- package/dist/report/graph.d.ts +10 -4
- package/dist/report/graph.js +60 -6
- package/dist/report/graph.js.map +1 -1
- package/dist/report/impact.d.ts +62 -0
- package/dist/report/impact.js +168 -0
- package/dist/report/impact.js.map +1 -0
- package/dist/report/preflight-runtime.d.ts +42 -0
- package/dist/report/preflight-runtime.js +193 -0
- package/dist/report/preflight-runtime.js.map +1 -0
- package/dist/report/preflight-view.d.ts +146 -0
- package/dist/report/preflight-view.js +216 -0
- package/dist/report/preflight-view.js.map +1 -0
- package/dist/report/preflight.d.ts +104 -0
- package/dist/report/preflight.js +331 -0
- package/dist/report/preflight.js.map +1 -0
- package/dist/report/query.d.ts +11 -4
- package/dist/report/query.js +84 -17
- package/dist/report/query.js.map +1 -1
- package/dist/report/retentions.d.ts +17 -5
- package/dist/report/retentions.js +85 -8
- package/dist/report/retentions.js.map +1 -1
- package/dist/report/rules.d.ts +16 -0
- package/dist/report/rules.js +30 -0
- package/dist/report/rules.js.map +1 -0
- package/dist/report/runtime-impact.d.ts +38 -0
- package/dist/report/runtime-impact.js +94 -0
- package/dist/report/runtime-impact.js.map +1 -0
- package/dist/report/runtime.d.ts +54 -0
- package/dist/report/runtime.js +137 -0
- package/dist/report/runtime.js.map +1 -0
- package/dist/report/sarif.d.ts +2 -8
- package/dist/report/sarif.js +2 -10
- package/dist/report/sarif.js.map +1 -1
- package/dist/report/sorted-json.d.ts +1 -1
- package/dist/report/sorted-json.js +2 -2
- package/dist/report/sorted-json.js.map +1 -1
- package/docs/BRIDGE-EVENTS.md +87 -0
- package/docs/BRIDGE-MESSAGES.md +117 -0
- package/docs/GRAPH-EXCHANGE.md +382 -0
- package/docs/IMPACT.md +96 -0
- package/docs/MCP.md +62 -0
- package/docs/PREFLIGHT.md +311 -0
- package/docs/RUNTIME.md +172 -0
- package/docs/TOOLCHAIN.md +101 -0
- package/package.json +14 -2
- package/scripts/build-preflight-toolchain.mjs +186 -0
- package/scripts/capture-preflight.mjs +380 -0
- package/scripts/run-child.mjs +17 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CheckReport } from './check-report.ts';
|
|
2
|
+
import { type IssueFingerprint } from './rules.ts';
|
|
3
|
+
/**
|
|
4
|
+
* GitLab Code Quality가 받아들이는 심각도 어휘다. check의 error는 실제
|
|
5
|
+
* 불일치라 major로, warning은 미해결·비대칭 관찰이라 minor로 내린다.
|
|
6
|
+
*/
|
|
7
|
+
export type CodeQualitySeverity = 'info' | 'minor' | 'major' | 'critical' | 'blocker';
|
|
8
|
+
/**
|
|
9
|
+
* GitLab Code Quality 보고서의 발견 항목 하나다. 형식은 CodeClimate 기반이며
|
|
10
|
+
* `fingerprint`가 같은 이슈의 병합 대상 판별에 쓰인다.
|
|
11
|
+
*/
|
|
12
|
+
export interface CodeQualityFinding {
|
|
13
|
+
readonly description: string;
|
|
14
|
+
readonly check_name: string;
|
|
15
|
+
readonly fingerprint: string;
|
|
16
|
+
readonly severity: CodeQualitySeverity;
|
|
17
|
+
readonly location: {
|
|
18
|
+
readonly path: string;
|
|
19
|
+
readonly lines: {
|
|
20
|
+
readonly begin: number;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* check 보고서를 GitLab Code Quality 발견 목록으로 바꾼다.
|
|
26
|
+
*
|
|
27
|
+
* 이 형식이 표현하지 못하는 것을 명시한다:
|
|
28
|
+
* - 베이스라인이 받아들인 이슈는 제외한다 — 억제 개념이 없어 위젯이 다시
|
|
29
|
+
* 발견으로 표시하면 안 되기 때문이다(SARIF의 `suppressions`와 다른 결정).
|
|
30
|
+
* - 두 번째 이후 증거 끝점은 실리지 않는다 — CodeClimate의 `other_locations`
|
|
31
|
+
* 선택 필드를 GitLab이 받아들이는지 검증하지 않았다(SARIF의
|
|
32
|
+
* `relatedLocations`와 다른 범위).
|
|
33
|
+
* - `report.limitations`는 표현되지 않는다 — SARIF와 같은 한계이며, 분석
|
|
34
|
+
* 공백 신호는 `*-unverified` 계열의 minor 발견으로만 전달된다.
|
|
35
|
+
* - `location.path`는 URI 인코딩하지 않은 프로젝트 상대 경로다 — SARIF의
|
|
36
|
+
* `sarifUri`와 다른 소비자(URI 참조가 아니라 파일 뷰어 경로)라서다.
|
|
37
|
+
*
|
|
38
|
+
* `fingerprint`는 논리 이슈 식별자(code·target·channel·method) 해시다.
|
|
39
|
+
* GitLab은 같은 지문의 발견을 병합하므로, check 보고서가 같은 논리 키의
|
|
40
|
+
* 이슈를 두 개 이상 담으면 안 되는 상류 불변에 의존한다 — 중복이 관찰되면
|
|
41
|
+
* 깨진 아티팩트 대신 명시적으로 실패한다.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createCodeQualityFindings(report: CheckReport, issueFingerprint: IssueFingerprint): readonly CodeQualityFinding[];
|
|
44
|
+
/** 발견 목록을 결정적인 JSON 문자열로 인코딩한다. */
|
|
45
|
+
export declare function encodeCodeQualityReport(findings: readonly CodeQualityFinding[]): string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { checkIssueRuleDescriptions, } from "./rules.js";
|
|
2
|
+
import { encodeSortedJson } from "./sorted-json.js";
|
|
3
|
+
/**
|
|
4
|
+
* check 보고서를 GitLab Code Quality 발견 목록으로 바꾼다.
|
|
5
|
+
*
|
|
6
|
+
* 이 형식이 표현하지 못하는 것을 명시한다:
|
|
7
|
+
* - 베이스라인이 받아들인 이슈는 제외한다 — 억제 개념이 없어 위젯이 다시
|
|
8
|
+
* 발견으로 표시하면 안 되기 때문이다(SARIF의 `suppressions`와 다른 결정).
|
|
9
|
+
* - 두 번째 이후 증거 끝점은 실리지 않는다 — CodeClimate의 `other_locations`
|
|
10
|
+
* 선택 필드를 GitLab이 받아들이는지 검증하지 않았다(SARIF의
|
|
11
|
+
* `relatedLocations`와 다른 범위).
|
|
12
|
+
* - `report.limitations`는 표현되지 않는다 — SARIF와 같은 한계이며, 분석
|
|
13
|
+
* 공백 신호는 `*-unverified` 계열의 minor 발견으로만 전달된다.
|
|
14
|
+
* - `location.path`는 URI 인코딩하지 않은 프로젝트 상대 경로다 — SARIF의
|
|
15
|
+
* `sarifUri`와 다른 소비자(URI 참조가 아니라 파일 뷰어 경로)라서다.
|
|
16
|
+
*
|
|
17
|
+
* `fingerprint`는 논리 이슈 식별자(code·target·channel·method) 해시다.
|
|
18
|
+
* GitLab은 같은 지문의 발견을 병합하므로, check 보고서가 같은 논리 키의
|
|
19
|
+
* 이슈를 두 개 이상 담으면 안 되는 상류 불변에 의존한다 — 중복이 관찰되면
|
|
20
|
+
* 깨진 아티팩트 대신 명시적으로 실패한다.
|
|
21
|
+
*/
|
|
22
|
+
export function createCodeQualityFindings(report, issueFingerprint) {
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
return report.issues
|
|
25
|
+
.filter((issue) => issue.suppressed !== true)
|
|
26
|
+
.map((issue) => {
|
|
27
|
+
const finding = codeQualityFinding(issue, issueFingerprint);
|
|
28
|
+
if (seen.has(finding.fingerprint)) {
|
|
29
|
+
throw new Error('Cannot create Code Quality findings with duplicate fingerprints.');
|
|
30
|
+
}
|
|
31
|
+
seen.add(finding.fingerprint);
|
|
32
|
+
return finding;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** 발견 목록을 결정적인 JSON 문자열로 인코딩한다. */
|
|
36
|
+
export function encodeCodeQualityReport(findings) {
|
|
37
|
+
return encodeSortedJson(findings);
|
|
38
|
+
}
|
|
39
|
+
/** 논리 이슈 하나를 Code Quality 발견으로 바꾼다. 위치는 첫 번째 증거 끝점이다. */
|
|
40
|
+
function codeQualityFinding(issue, issueFingerprint) {
|
|
41
|
+
const primary = issue.evidence[0];
|
|
42
|
+
if (primary === undefined) {
|
|
43
|
+
throw new Error('Cannot create a Code Quality finding without evidence.');
|
|
44
|
+
}
|
|
45
|
+
const { line } = primary.location;
|
|
46
|
+
if (line < 1) {
|
|
47
|
+
throw new Error('Cannot create a Code Quality location before the first line.');
|
|
48
|
+
}
|
|
49
|
+
const context = issue.method === undefined
|
|
50
|
+
? `Channel '${issue.channel}'`
|
|
51
|
+
: `Channel '${issue.channel}', method '${issue.method}'`;
|
|
52
|
+
return {
|
|
53
|
+
description: `${checkIssueRuleDescriptions[issue.code]} ${context} (target: ${issue.target})`,
|
|
54
|
+
check_name: `isthmus:${issue.code}`,
|
|
55
|
+
fingerprint: issueFingerprint(issue),
|
|
56
|
+
severity: codeQualitySeverity(issue.severity),
|
|
57
|
+
location: {
|
|
58
|
+
path: primary.location.path,
|
|
59
|
+
lines: { begin: line },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* check 심각도를 Code Quality 어휘로 내린다. `CheckIssue.severity`가 커지면
|
|
65
|
+
* 조용한 minor 폴백 대신 명시적으로 실패한다.
|
|
66
|
+
*/
|
|
67
|
+
function codeQualitySeverity(severity) {
|
|
68
|
+
if (severity === 'error')
|
|
69
|
+
return 'major';
|
|
70
|
+
if (severity === 'warning')
|
|
71
|
+
return 'minor';
|
|
72
|
+
throw new Error('Cannot map an unknown check severity to Code Quality.');
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=codequality.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codequality.js","sourceRoot":"","sources":["../../src/report/codequality.ts"],"names":[],"mappings":"AACA,OAAO,EACL,0BAA0B,GAE3B,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAuBpD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,yBAAyB,CACvC,MAAmB,EACnB,gBAAkC;IAElC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,OAAO,MAAM,CAAC,MAAM;SACjB,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;SAC5C,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QACb,MAAM,OAAO,GAAG,kBAAkB,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC;QAC5D,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CACb,kEAAkE,CACnE,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QAC9B,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC,CAAC;AACP,CAAC;AAED,mCAAmC;AACnC,MAAM,UAAU,uBAAuB,CACrC,QAAuC;IAEvC,OAAO,gBAAgB,CAAC,QAAQ,CAAC,CAAC;AACpC,CAAC;AAED,yDAAyD;AACzD,SAAS,kBAAkB,CACzB,KAAiB,EACjB,gBAAkC;IAElC,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAClC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC5E,CAAC;IACD,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC;IAClC,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,8DAA8D,CAAC,CAAC;IAClF,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,KAAK,SAAS;QACxC,CAAC,CAAC,YAAY,KAAK,CAAC,OAAO,GAAG;QAC9B,CAAC,CAAC,YAAY,KAAK,CAAC,OAAO,cAAc,KAAK,CAAC,MAAM,GAAG,CAAC;IAC3D,OAAO;QACL,WAAW,EACT,GAAG,0BAA0B,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,OAAO,aAAa,KAAK,CAAC,MAAM,GAAG;QAClF,UAAU,EAAE,WAAW,KAAK,CAAC,IAAI,EAAE;QACnC,WAAW,EAAE,gBAAgB,CAAC,KAAK,CAAC;QACpC,QAAQ,EAAE,mBAAmB,CAAC,KAAK,CAAC,QAAQ,CAAC;QAC7C,QAAQ,EAAE;YACR,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,IAAI;YAC3B,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;SACvB;KACF,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,SAAS,mBAAmB,CAC1B,QAAgC;IAEhC,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,OAAO,CAAC;IACzC,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC;IAC3C,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;AAC3E,CAAC","sourcesContent":["import type { CheckIssue, CheckReport } from './check-report.ts';\nimport {\n checkIssueRuleDescriptions,\n type IssueFingerprint,\n} from './rules.ts';\nimport { encodeSortedJson } from './sorted-json.ts';\n\n/**\n * GitLab Code Quality가 받아들이는 심각도 어휘다. check의 error는 실제\n * 불일치라 major로, warning은 미해결·비대칭 관찰이라 minor로 내린다.\n */\nexport type CodeQualitySeverity = 'info' | 'minor' | 'major' | 'critical' | 'blocker';\n\n/**\n * GitLab Code Quality 보고서의 발견 항목 하나다. 형식은 CodeClimate 기반이며\n * `fingerprint`가 같은 이슈의 병합 대상 판별에 쓰인다.\n */\nexport interface CodeQualityFinding {\n readonly description: string;\n readonly check_name: string;\n readonly fingerprint: string;\n readonly severity: CodeQualitySeverity;\n readonly location: {\n readonly path: string;\n readonly lines: { readonly begin: number };\n };\n}\n\n/**\n * check 보고서를 GitLab Code Quality 발견 목록으로 바꾼다.\n *\n * 이 형식이 표현하지 못하는 것을 명시한다:\n * - 베이스라인이 받아들인 이슈는 제외한다 — 억제 개념이 없어 위젯이 다시\n * 발견으로 표시하면 안 되기 때문이다(SARIF의 `suppressions`와 다른 결정).\n * - 두 번째 이후 증거 끝점은 실리지 않는다 — CodeClimate의 `other_locations`\n * 선택 필드를 GitLab이 받아들이는지 검증하지 않았다(SARIF의\n * `relatedLocations`와 다른 범위).\n * - `report.limitations`는 표현되지 않는다 — SARIF와 같은 한계이며, 분석\n * 공백 신호는 `*-unverified` 계열의 minor 발견으로만 전달된다.\n * - `location.path`는 URI 인코딩하지 않은 프로젝트 상대 경로다 — SARIF의\n * `sarifUri`와 다른 소비자(URI 참조가 아니라 파일 뷰어 경로)라서다.\n *\n * `fingerprint`는 논리 이슈 식별자(code·target·channel·method) 해시다.\n * GitLab은 같은 지문의 발견을 병합하므로, check 보고서가 같은 논리 키의\n * 이슈를 두 개 이상 담으면 안 되는 상류 불변에 의존한다 — 중복이 관찰되면\n * 깨진 아티팩트 대신 명시적으로 실패한다.\n */\nexport function createCodeQualityFindings(\n report: CheckReport,\n issueFingerprint: IssueFingerprint,\n): readonly CodeQualityFinding[] {\n const seen = new Set<string>();\n return report.issues\n .filter((issue) => issue.suppressed !== true)\n .map((issue) => {\n const finding = codeQualityFinding(issue, issueFingerprint);\n if (seen.has(finding.fingerprint)) {\n throw new Error(\n 'Cannot create Code Quality findings with duplicate fingerprints.',\n );\n }\n seen.add(finding.fingerprint);\n return finding;\n });\n}\n\n/** 발견 목록을 결정적인 JSON 문자열로 인코딩한다. */\nexport function encodeCodeQualityReport(\n findings: readonly CodeQualityFinding[],\n): string {\n return encodeSortedJson(findings);\n}\n\n/** 논리 이슈 하나를 Code Quality 발견으로 바꾼다. 위치는 첫 번째 증거 끝점이다. */\nfunction codeQualityFinding(\n issue: CheckIssue,\n issueFingerprint: IssueFingerprint,\n): CodeQualityFinding {\n const primary = issue.evidence[0];\n if (primary === undefined) {\n throw new Error('Cannot create a Code Quality finding without evidence.');\n }\n const { line } = primary.location;\n if (line < 1) {\n throw new Error('Cannot create a Code Quality location before the first line.');\n }\n const context = issue.method === undefined\n ? `Channel '${issue.channel}'`\n : `Channel '${issue.channel}', method '${issue.method}'`;\n return {\n description:\n `${checkIssueRuleDescriptions[issue.code]} ${context} (target: ${issue.target})`,\n check_name: `isthmus:${issue.code}`,\n fingerprint: issueFingerprint(issue),\n severity: codeQualitySeverity(issue.severity),\n location: {\n path: primary.location.path,\n lines: { begin: line },\n },\n };\n}\n\n/**\n * check 심각도를 Code Quality 어휘로 내린다. `CheckIssue.severity`가 커지면\n * 조용한 minor 폴백 대신 명시적으로 실패한다.\n */\nfunction codeQualitySeverity(\n severity: CheckIssue['severity'],\n): CodeQualitySeverity {\n if (severity === 'error') return 'major';\n if (severity === 'warning') return 'minor';\n throw new Error('Cannot map an unknown check severity to Code Quality.');\n}\n"]}
|
package/dist/report/diff.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { BridgeFactsDocument } from '../exchange/parse.ts';
|
|
2
|
-
import type { JoinLimitation, MatchedMethod } from '../join/join.ts';
|
|
2
|
+
import type { JoinLimitation, MatchedBoundaryName, MatchedMethod } from '../join/join.ts';
|
|
3
|
+
import type { BridgeMessageDocument } from '../exchange/messages.ts';
|
|
4
|
+
import type { MessageBridgeRoute } from '../join/messages.ts';
|
|
3
5
|
import type { CheckIssue } from './check-report.ts';
|
|
4
6
|
/** 두 스냅샷의 생산 도구 버전 근거다. */
|
|
5
7
|
export interface DiffProducerVersion {
|
|
@@ -21,12 +23,26 @@ export interface BridgeDiffDocument {
|
|
|
21
23
|
readonly summary: {
|
|
22
24
|
readonly addedMethods: number;
|
|
23
25
|
readonly removedMethods: number;
|
|
26
|
+
readonly addedModules: number;
|
|
27
|
+
readonly removedModules: number;
|
|
28
|
+
readonly addedComponents: number;
|
|
29
|
+
readonly removedComponents: number;
|
|
24
30
|
readonly introducedErrors: number;
|
|
25
31
|
readonly introducedWarnings: number;
|
|
26
32
|
readonly resolvedIssues: number;
|
|
33
|
+
/** `messages` 입력이 있을 때만 실린다 — literal로 확정된 v2 경계 수다. */
|
|
34
|
+
readonly addedMessageBoundaries?: number;
|
|
35
|
+
readonly removedMessageBoundaries?: number;
|
|
27
36
|
};
|
|
28
37
|
readonly addedMethods: readonly MatchedMethod[];
|
|
29
38
|
readonly removedMethods: readonly MatchedMethod[];
|
|
39
|
+
readonly addedModules: readonly MatchedBoundaryName[];
|
|
40
|
+
readonly removedModules: readonly MatchedBoundaryName[];
|
|
41
|
+
readonly addedComponents: readonly MatchedBoundaryName[];
|
|
42
|
+
readonly removedComponents: readonly MatchedBoundaryName[];
|
|
43
|
+
/** `messages` 입력이 있을 때만 실리는 literal v2 경계다. */
|
|
44
|
+
readonly addedMessageBoundaries?: readonly MessageBridgeRoute[];
|
|
45
|
+
readonly removedMessageBoundaries?: readonly MessageBridgeRoute[];
|
|
30
46
|
readonly introducedIssues: readonly CheckIssue[];
|
|
31
47
|
readonly resolvedIssues: readonly CheckIssue[];
|
|
32
48
|
readonly limitations: {
|
|
@@ -40,5 +56,11 @@ export interface BridgeDiffDocument {
|
|
|
40
56
|
readonly after: readonly DiffProducerVersion[];
|
|
41
57
|
};
|
|
42
58
|
}
|
|
43
|
-
/**
|
|
44
|
-
|
|
59
|
+
/**
|
|
60
|
+
* 동일 프로젝트의 관찰 결과를 비교하며 삭제 안전성이나 rename을 추측하지 않는다.
|
|
61
|
+
*
|
|
62
|
+
* `beforeMessages`·`afterMessages`를 주면 Basic·Event v2 경계도 비교한다. 두 스냅샷
|
|
63
|
+
* 모두 있어야 하며 transport 집합이 같아야 한다 — 한쪽만 있으면 관찰 차이가 아니라
|
|
64
|
+
* 입력 구성 차이다.
|
|
65
|
+
*/
|
|
66
|
+
export declare function createBridgeDiff(before: readonly BridgeFactsDocument[], after: readonly BridgeFactsDocument[], beforeMessages?: readonly BridgeMessageDocument[], afterMessages?: readonly BridgeMessageDocument[]): BridgeDiffDocument;
|
package/dist/report/diff.js
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import { isCallerPlatform, isReceiverPlatform } from "../exchange/parse.js";
|
|
1
2
|
import { compareStrings } from "../compare.js";
|
|
2
3
|
import { BridgeJoinValidationError, isBridgeJoinDeferred, joinBridgeDocuments } from "../join/join.js";
|
|
4
|
+
import { joinMessageBridges } from "../join/messages.js";
|
|
3
5
|
import { baselineEntryKey } from "./baseline.js";
|
|
4
6
|
import { createCheckReport } from "./check-report.js";
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
+
/**
|
|
8
|
+
* 동일 프로젝트의 관찰 결과를 비교하며 삭제 안전성이나 rename을 추측하지 않는다.
|
|
9
|
+
*
|
|
10
|
+
* `beforeMessages`·`afterMessages`를 주면 Basic·Event v2 경계도 비교한다. 두 스냅샷
|
|
11
|
+
* 모두 있어야 하며 transport 집합이 같아야 한다 — 한쪽만 있으면 관찰 차이가 아니라
|
|
12
|
+
* 입력 구성 차이다.
|
|
13
|
+
*/
|
|
14
|
+
export function createBridgeDiff(before, after, beforeMessages, afterMessages) {
|
|
7
15
|
validateSnapshots(before, after);
|
|
16
|
+
validateMessageSnapshots(beforeMessages, afterMessages);
|
|
17
|
+
const project = before[0]?.project ?? after[0].project;
|
|
8
18
|
const oldJoin = joinBridgeDocuments(before);
|
|
9
19
|
const newJoin = joinBridgeDocuments(after);
|
|
10
20
|
if (isBridgeJoinDeferred(oldJoin) || isBridgeJoinDeferred(newJoin)) {
|
|
@@ -13,24 +23,53 @@ export function createBridgeDiff(before, after) {
|
|
|
13
23
|
+ `${before.length} documents, and the after inputs observed `
|
|
14
24
|
+ `${newJoin.observedFacts} facts across ${after.length} documents.`);
|
|
15
25
|
}
|
|
16
|
-
const
|
|
17
|
-
|
|
26
|
+
const oldMessages = beforeMessages === undefined
|
|
27
|
+
? undefined
|
|
28
|
+
: joinMessageBridges(beforeMessages, project);
|
|
29
|
+
const newMessages = afterMessages === undefined
|
|
30
|
+
? undefined
|
|
31
|
+
: joinMessageBridges(afterMessages, project);
|
|
32
|
+
const oldReport = createCheckReport(oldJoin, oldMessages);
|
|
33
|
+
const newReport = createCheckReport(newJoin, newMessages);
|
|
18
34
|
const addedMethods = difference(newJoin.matchedMethods, oldJoin.matchedMethods, logicalKey);
|
|
19
35
|
const removedMethods = difference(oldJoin.matchedMethods, newJoin.matchedMethods, logicalKey);
|
|
36
|
+
const addedModules = difference(newJoin.matchedModules, oldJoin.matchedModules, logicalKey);
|
|
37
|
+
const removedModules = difference(oldJoin.matchedModules, newJoin.matchedModules, logicalKey);
|
|
38
|
+
const addedComponents = difference(newJoin.matchedComponents, oldJoin.matchedComponents, logicalKey);
|
|
39
|
+
const removedComponents = difference(oldJoin.matchedComponents, newJoin.matchedComponents, logicalKey);
|
|
20
40
|
const introducedIssues = difference(newReport.issues, oldReport.issues, baselineEntryKey);
|
|
21
41
|
const resolvedIssues = difference(oldReport.issues, newReport.issues, baselineEntryKey);
|
|
42
|
+
const messageBoundaries = oldMessages === undefined || newMessages === undefined
|
|
43
|
+
? {}
|
|
44
|
+
: {
|
|
45
|
+
addedMessageBoundaries: difference(matchedMessageRoutes(newMessages), matchedMessageRoutes(oldMessages), messageBoundaryKey),
|
|
46
|
+
removedMessageBoundaries: difference(matchedMessageRoutes(oldMessages), matchedMessageRoutes(newMessages), messageBoundaryKey),
|
|
47
|
+
};
|
|
22
48
|
return {
|
|
23
49
|
format: 'isthmus-diff',
|
|
24
50
|
version: 1,
|
|
25
51
|
summary: {
|
|
26
52
|
addedMethods: addedMethods.length,
|
|
27
53
|
removedMethods: removedMethods.length,
|
|
54
|
+
addedModules: addedModules.length,
|
|
55
|
+
removedModules: removedModules.length,
|
|
56
|
+
addedComponents: addedComponents.length,
|
|
57
|
+
removedComponents: removedComponents.length,
|
|
28
58
|
introducedErrors: introducedIssues.filter((issue) => issue.severity === 'error').length,
|
|
29
59
|
introducedWarnings: introducedIssues.filter((issue) => issue.severity === 'warning').length,
|
|
30
60
|
resolvedIssues: resolvedIssues.length,
|
|
61
|
+
...(oldMessages === undefined || newMessages === undefined ? {} : {
|
|
62
|
+
addedMessageBoundaries: matchedMessageBoundaryCount(newMessages, oldMessages),
|
|
63
|
+
removedMessageBoundaries: matchedMessageBoundaryCount(oldMessages, newMessages),
|
|
64
|
+
}),
|
|
31
65
|
},
|
|
32
66
|
addedMethods,
|
|
33
67
|
removedMethods,
|
|
68
|
+
addedModules,
|
|
69
|
+
removedModules,
|
|
70
|
+
addedComponents,
|
|
71
|
+
removedComponents,
|
|
72
|
+
...messageBoundaries,
|
|
34
73
|
introducedIssues,
|
|
35
74
|
resolvedIssues,
|
|
36
75
|
limitations: {
|
|
@@ -42,19 +81,61 @@ export function createBridgeDiff(before, after) {
|
|
|
42
81
|
producers: { before: producerVersions(before), after: producerVersions(after) },
|
|
43
82
|
};
|
|
44
83
|
}
|
|
84
|
+
/** literal로 양쪽이 관찰된 v2 경계만 돌려준다 — 후보 prefix는 확정 매치가 아니다. */
|
|
85
|
+
function matchedMessageRoutes(messages) {
|
|
86
|
+
return messages.routes.filter((route) => route.matching === 'literal' && route.senders.length > 0 && route.handlers.length > 0);
|
|
87
|
+
}
|
|
88
|
+
/** 두 스냅샷의 literal v2 경계 키를 비교해 추가된 수를 센다. */
|
|
89
|
+
function matchedMessageBoundaryCount(left, right) {
|
|
90
|
+
return difference(matchedMessageRoutes(left), matchedMessageRoutes(right), messageBoundaryKey).length;
|
|
91
|
+
}
|
|
92
|
+
/** v2 경계는 transport·주소로만 식별한다 — 같은 채널 이름이라도 transport가 다르다. */
|
|
93
|
+
function messageBoundaryKey(route) {
|
|
94
|
+
return JSON.stringify([route.transport, route.channel]);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* v2 입력은 두 스냅샷 모두 있거나 모두 없어야 하며, transport 집합이 같아야 한다.
|
|
98
|
+
*
|
|
99
|
+
* 한쪽에만 v2가 있으면 그 transport의 경계 전부가 추가·삭제로 보인다 — 관찰 차이가
|
|
100
|
+
* 아니라 입력 구성 차이므로 입력 오류로 거부한다.
|
|
101
|
+
*/
|
|
102
|
+
function validateMessageSnapshots(before, after) {
|
|
103
|
+
if (before === undefined && after === undefined)
|
|
104
|
+
return;
|
|
105
|
+
if (before === undefined || after === undefined) {
|
|
106
|
+
throw new BridgeJoinValidationError('Diff message inputs must be present in both snapshots; rebuild both from one checkout.');
|
|
107
|
+
}
|
|
108
|
+
const transports = (docs) => JSON.stringify([...new Set(docs.map(({ transport }) => transport))].sort(compareStrings));
|
|
109
|
+
if (transports(before) !== transports(after)) {
|
|
110
|
+
throw new BridgeJoinValidationError('Diff message inputs must observe the same transports in both snapshots.');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
45
113
|
/** 플랫폼 누락이나 다른 프로젝트를 코드 삭제로 오해하지 않도록 입력 구성을 고정한다. */
|
|
46
114
|
function validateSnapshots(before, after) {
|
|
47
115
|
const all = [...before, ...after];
|
|
48
116
|
if (new Set(all.map((doc) => doc.project)).size !== 1 ||
|
|
49
|
-
|
|
50
|
-
|
|
117
|
+
new Set(all.filter((doc) => isReceiverPlatform(doc.platform)).map((doc) => doc.platform)).size !== 1 ||
|
|
118
|
+
![before, after].every((docs) => docs.some((doc) => isCallerPlatform(doc.platform)) &&
|
|
119
|
+
docs.some((doc) => isReceiverPlatform(doc.platform))) ||
|
|
51
120
|
JSON.stringify(producerInventory(before)) !== JSON.stringify(producerInventory(after)) ||
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
throw new BridgeJoinValidationError('Diff requires the same project
|
|
55
|
-
+ 'inventories in both snapshots; rebuild both
|
|
121
|
+
JSON.stringify(snapshotTargets(before)) !== JSON.stringify(snapshotTargets(after)) ||
|
|
122
|
+
all.some((doc) => !isCallerPlatform(doc.platform) && !isReceiverPlatform(doc.platform))) {
|
|
123
|
+
throw new BridgeJoinValidationError('Diff requires the same project, the same bridge targets, and matching '
|
|
124
|
+
+ 'caller/native producer inventories in both snapshots; rebuild both '
|
|
125
|
+
+ 'snapshots from one checkout.');
|
|
56
126
|
}
|
|
57
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* 스냅샷이 관찰한 브리지 target 집합을 정렬해 돌려준다.
|
|
130
|
+
*
|
|
131
|
+
* target 집합이 다른 두 시점을 비교하면 한쪽 target의 사실 전부가 삭제·추가로
|
|
132
|
+
* 보이므로 관찰 차이가 아니라 입력 구성 차이다. 사실이 없는 문서의 null target은
|
|
133
|
+
* 집합에 넣지 않는다.
|
|
134
|
+
*/
|
|
135
|
+
function snapshotTargets(docs) {
|
|
136
|
+
return [...new Set(docs.flatMap((doc) => doc.target === null ? [] : [doc.target]))]
|
|
137
|
+
.sort(compareStrings);
|
|
138
|
+
}
|
|
58
139
|
/** 버전 변화는 출력하되 플랫폼·도구별 문서 개수 변화는 허용하지 않는다. */
|
|
59
140
|
function producerInventory(docs) {
|
|
60
141
|
return docs.map((doc) => JSON.stringify([doc.platform, doc.tool.name])).sort(compareStrings);
|
package/dist/report/diff.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/report/diff.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAM/C,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEvG,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AA2CtD,sDAAsD;AACtD,MAAM,UAAU,gBAAgB,CAC9B,MAAsC,EACtC,KAAqC;IAErC,iBAAiB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACjC,MAAM,OAAO,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;IAC3C,IAAI,oBAAoB,CAAC,OAAO,CAAC,IAAI,oBAAoB,CAAC,OAAO,CAAC,EAAE,CAAC;QACnE,MAAM,IAAI,yBAAyB,CACjC,uEAAuE;cACrE,qCAAqC,OAAO,CAAC,aAAa,gBAAgB;cAC1E,GAAG,MAAM,CAAC,MAAM,4CAA4C;cAC5D,GAAG,OAAO,CAAC,aAAa,iBAAiB,KAAK,CAAC,MAAM,aAAa,CACrE,CAAC;IACJ,CAAC;IACD,MAAM,SAAS,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC7C,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,CAAC,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,UAAU,CAAC,CAAC;IAC5F,MAAM,cAAc,GAAG,UAAU,CAAC,OAAO,CAAC,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,UAAU,CAAC,CAAC;IAC9F,MAAM,gBAAgB,GAAG,UAAU,CAAC,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC1F,MAAM,cAAc,GAAG,UAAU,CAAC,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IACxF,OAAO;QACL,MAAM,EAAE,cAAuB;QAC/B,OAAO,EAAE,CAAU;QACnB,OAAO,EAAE;YACP,YAAY,EAAE,YAAY,CAAC,MAAM;YACjC,cAAc,EAAE,cAAc,CAAC,MAAM;YACrC,gBAAgB,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,MAAM;YACvF,kBAAkB,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,MAAM;YAC3F,cAAc,EAAE,cAAc,CAAC,MAAM;SACtC;QACD,YAAY;QACZ,cAAc;QACd,gBAAgB;QAChB,cAAc;QACd,WAAW,EAAE;YACX,MAAM,EAAE,SAAS,CAAC,WAAW;YAC7B,KAAK,EAAE,SAAS,CAAC,WAAW;YAC5B,KAAK,EAAE,UAAU,CAAC,SAAS,CAAC,WAAW,EAAE,SAAS,CAAC,WAAW,EAAE,aAAa,CAAC;YAC9E,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC,WAAW,EAAE,SAAS,CAAC,WAAW,EAAE,aAAa,CAAC;SACjF;QACD,SAAS,EAAE,EAAE,MAAM,EAAE,gBAAgB,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,gBAAgB,CAAC,KAAK,CAAC,EAAE;KAChF,CAAC;AACJ,CAAC;AAED,qDAAqD;AACrD,SAAS,iBAAiB,CAAC,MAAsC,EAAE,KAAqC;IACtG,MAAM,GAAG,GAAG,CAAC,GAAG,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC;IAClC,IAAI,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC;QACnD,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,KAAK,MAAM,CAAC;YAC1E,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC;QAC/C,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;QACtF,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,MAAM,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,CAAC;YACrE,CAAC,GAAG,CAAC,MAAM,KAAK,IAAI,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,EAAE,CAAC;QACvD,MAAM,IAAI,yBAAyB,CACjC,0EAA0E;cACxE,0EAA0E,CAC7E,CAAC;IACJ,CAAC;AACH,CAAC;AAED,8CAA8C;AAC9C,SAAS,iBAAiB,CAAC,IAAoC;IAC7D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;AAC/F,CAAC;AAED,yCAAyC;AACzC,SAAS,gBAAgB,CAAC,IAAoC;IAC5D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,GAAG,GAAG,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;SAC9F,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1E,CAAC;AAED,gDAAgD;AAChD,SAAS,UAAU,CAAC,IAA2F;IAC7G,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC;AAC1E,CAAC;AAED,SAAS,aAAa,CAAC,IAAyL;IAC9M,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,QAAQ,IAAI,IAAI,EAAE,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC;AAC3H,CAAC;AAED,uCAAuC;AACvC,SAAS,UAAU,CAAI,IAAkB,EAAE,KAAmB,EAAE,GAAwB;IACtF,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IACzC,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;SACnD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpD,CAAC","sourcesContent":["import type { BridgeFactsDocument, BridgeTarget } from '../exchange/parse.ts';\nimport { compareStrings } from '../compare.ts';\nimport type {\n BridgeJoinResult,\n JoinLimitation,\n MatchedMethod,\n} from '../join/join.ts';\nimport { BridgeJoinValidationError, isBridgeJoinDeferred, joinBridgeDocuments } from '../join/join.ts';\nimport type { CheckIssue } from './check-report.ts';\nimport { baselineEntryKey } from './baseline.ts';\nimport { createCheckReport } from './check-report.ts';\n\n/** 두 스냅샷의 생산 도구 버전 근거다. */\nexport interface DiffProducerVersion {\n readonly platform: string;\n readonly name: string;\n readonly version: string;\n readonly generatedAt: string;\n}\n\n/**\n * 동일 프로젝트의 전후 관찰을 비교한 isthmus-diff 문서다.\n *\n * added/removed는 키 집합 차이라 소스 줄 이동은 차이가 아니다. introduced와\n * resolved는 논리 이슈 키 기준이라 코드가 바뀌면(-unverified → 본 판정)\n * 해결·추가 한 쌍으로 보인다 — 심각도 상승은 새 관찰이기 때문이다.\n */\nexport interface BridgeDiffDocument {\n readonly format: 'isthmus-diff';\n readonly version: 1;\n readonly summary: {\n readonly addedMethods: number;\n readonly removedMethods: number;\n readonly introducedErrors: number;\n readonly introducedWarnings: number;\n readonly resolvedIssues: number;\n };\n readonly addedMethods: readonly MatchedMethod[];\n readonly removedMethods: readonly MatchedMethod[];\n readonly introducedIssues: readonly CheckIssue[];\n readonly resolvedIssues: readonly CheckIssue[];\n readonly limitations: {\n readonly before: readonly JoinLimitation[];\n readonly after: readonly JoinLimitation[];\n readonly added: readonly JoinLimitation[];\n readonly removed: readonly JoinLimitation[];\n };\n readonly producers: {\n readonly before: readonly DiffProducerVersion[];\n readonly after: readonly DiffProducerVersion[];\n };\n}\n\n/** 동일 프로젝트의 관찰 결과를 비교하며 삭제 안전성이나 rename을 추측하지 않는다. */\nexport function createBridgeDiff(\n before: readonly BridgeFactsDocument[],\n after: readonly BridgeFactsDocument[],\n): BridgeDiffDocument {\n validateSnapshots(before, after);\n const oldJoin = joinBridgeDocuments(before);\n const newJoin = joinBridgeDocuments(after);\n if (isBridgeJoinDeferred(oldJoin) || isBridgeJoinDeferred(newJoin)) {\n throw new BridgeJoinValidationError(\n 'Cannot compare deferred bridge joins; split mixed bridge targets and '\n + `retry. The before inputs observed ${oldJoin.observedFacts} facts across `\n + `${before.length} documents, and the after inputs observed `\n + `${newJoin.observedFacts} facts across ${after.length} documents.`,\n );\n }\n const oldReport = createCheckReport(oldJoin);\n const newReport = createCheckReport(newJoin);\n const addedMethods = difference(newJoin.matchedMethods, oldJoin.matchedMethods, logicalKey);\n const removedMethods = difference(oldJoin.matchedMethods, newJoin.matchedMethods, logicalKey);\n const introducedIssues = difference(newReport.issues, oldReport.issues, baselineEntryKey);\n const resolvedIssues = difference(oldReport.issues, newReport.issues, baselineEntryKey);\n return {\n format: 'isthmus-diff' as const,\n version: 1 as const,\n summary: {\n addedMethods: addedMethods.length,\n removedMethods: removedMethods.length,\n introducedErrors: introducedIssues.filter((issue) => issue.severity === 'error').length,\n introducedWarnings: introducedIssues.filter((issue) => issue.severity === 'warning').length,\n resolvedIssues: resolvedIssues.length,\n },\n addedMethods,\n removedMethods,\n introducedIssues,\n resolvedIssues,\n limitations: {\n before: oldReport.limitations,\n after: newReport.limitations,\n added: difference(newReport.limitations, oldReport.limitations, limitationKey),\n removed: difference(oldReport.limitations, newReport.limitations, limitationKey),\n },\n producers: { before: producerVersions(before), after: producerVersions(after) },\n };\n}\n\n/** 플랫폼 누락이나 다른 프로젝트를 코드 삭제로 오해하지 않도록 입력 구성을 고정한다. */\nfunction validateSnapshots(before: readonly BridgeFactsDocument[], after: readonly BridgeFactsDocument[]): void {\n const all = [...before, ...after];\n if (new Set(all.map((doc) => doc.project)).size !== 1 ||\n ![before, after].every((docs) => docs.some((doc) => doc.platform === 'dart') &&\n docs.some((doc) => doc.platform === 'swift')) ||\n JSON.stringify(producerInventory(before)) !== JSON.stringify(producerInventory(after)) ||\n all.some((doc) => (doc.platform !== 'dart' && doc.platform !== 'swift') ||\n (doc.target !== null && doc.target !== 'flutter'))) {\n throw new BridgeJoinValidationError(\n 'Diff requires the same project and matching Flutter dart/swift producer '\n + 'inventories in both snapshots; rebuild both snapshots from one checkout.',\n );\n }\n}\n\n/** 버전 변화는 출력하되 플랫폼·도구별 문서 개수 변화는 허용하지 않는다. */\nfunction producerInventory(docs: readonly BridgeFactsDocument[]): string[] {\n return docs.map((doc) => JSON.stringify([doc.platform, doc.tool.name])).sort(compareStrings);\n}\n\n/** 추출기 업그레이드가 관찰 차이의 원인인지 검토할 버전 근거다. */\nfunction producerVersions(docs: readonly BridgeFactsDocument[]) {\n return docs.map((doc) => ({ platform: doc.platform, ...doc.tool, generatedAt: doc.generatedAt }))\n .sort((a, b) => compareStrings(JSON.stringify(a), JSON.stringify(b)));\n}\n\n/** 충돌 없는 논리 키로 비교해 소스 줄 이동을 추가·삭제로 보고하지 않는다. */\nfunction logicalKey(item: { readonly target: BridgeTarget; readonly channel: string; readonly method?: string }): string {\n return JSON.stringify([item.target, item.channel, item.method ?? null]);\n}\n\nfunction limitationKey(item: { readonly platform: string; readonly target: string | null; readonly tool: string; readonly message: string; readonly channels?: readonly string[]; readonly origin?: 'consumer' }): string {\n return JSON.stringify([item.platform, item.target, item.tool, item.message, item.channels ?? null, item.origin ?? null]);\n}\n\n/** 키 집합 차이를 안정적으로 정렬하고 원래 증거를 보존한다. */\nfunction difference<T>(left: readonly T[], right: readonly T[], key: (item: T) => string): T[] {\n const existing = new Set(right.map(key));\n return left.filter((item) => !existing.has(key(item)))\n .sort((a, b) => compareStrings(key(a), key(b)));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/report/diff.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BridgeFactsDocument, BridgeTarget } from '../exchange/parse.ts';\nimport { isCallerPlatform, isReceiverPlatform } from '../exchange/parse.ts';\nimport { compareStrings } from '../compare.ts';\nimport type {\n BridgeJoinResult,\n JoinLimitation,\n MatchedBoundaryName,\n MatchedMethod,\n} from '../join/join.ts';\nimport { BridgeJoinValidationError, isBridgeJoinDeferred, joinBridgeDocuments } from '../join/join.ts';\nimport type { BridgeMessageDocument } from '../exchange/messages.ts';\nimport type { MessageBridgeJoin, MessageBridgeRoute } from '../join/messages.ts';\nimport { joinMessageBridges } from '../join/messages.ts';\nimport type { CheckIssue } from './check-report.ts';\nimport { baselineEntryKey } from './baseline.ts';\nimport { createCheckReport } from './check-report.ts';\n\n/** 두 스냅샷의 생산 도구 버전 근거다. */\nexport interface DiffProducerVersion {\n readonly platform: string;\n readonly name: string;\n readonly version: string;\n readonly generatedAt: string;\n}\n\n/**\n * 동일 프로젝트의 전후 관찰을 비교한 isthmus-diff 문서다.\n *\n * added/removed는 키 집합 차이라 소스 줄 이동은 차이가 아니다. introduced와\n * resolved는 논리 이슈 키 기준이라 코드가 바뀌면(-unverified → 본 판정)\n * 해결·추가 한 쌍으로 보인다 — 심각도 상승은 새 관찰이기 때문이다.\n */\nexport interface BridgeDiffDocument {\n readonly format: 'isthmus-diff';\n readonly version: 1;\n readonly summary: {\n readonly addedMethods: number;\n readonly removedMethods: number;\n readonly addedModules: number;\n readonly removedModules: number;\n readonly addedComponents: number;\n readonly removedComponents: number;\n readonly introducedErrors: number;\n readonly introducedWarnings: number;\n readonly resolvedIssues: number;\n /** `messages` 입력이 있을 때만 실린다 — literal로 확정된 v2 경계 수다. */\n readonly addedMessageBoundaries?: number;\n readonly removedMessageBoundaries?: number;\n };\n readonly addedMethods: readonly MatchedMethod[];\n readonly removedMethods: readonly MatchedMethod[];\n readonly addedModules: readonly MatchedBoundaryName[];\n readonly removedModules: readonly MatchedBoundaryName[];\n readonly addedComponents: readonly MatchedBoundaryName[];\n readonly removedComponents: readonly MatchedBoundaryName[];\n /** `messages` 입력이 있을 때만 실리는 literal v2 경계다. */\n readonly addedMessageBoundaries?: readonly MessageBridgeRoute[];\n readonly removedMessageBoundaries?: readonly MessageBridgeRoute[];\n readonly introducedIssues: readonly CheckIssue[];\n readonly resolvedIssues: readonly CheckIssue[];\n readonly limitations: {\n readonly before: readonly JoinLimitation[];\n readonly after: readonly JoinLimitation[];\n readonly added: readonly JoinLimitation[];\n readonly removed: readonly JoinLimitation[];\n };\n readonly producers: {\n readonly before: readonly DiffProducerVersion[];\n readonly after: readonly DiffProducerVersion[];\n };\n}\n\n/**\n * 동일 프로젝트의 관찰 결과를 비교하며 삭제 안전성이나 rename을 추측하지 않는다.\n *\n * `beforeMessages`·`afterMessages`를 주면 Basic·Event v2 경계도 비교한다. 두 스냅샷\n * 모두 있어야 하며 transport 집합이 같아야 한다 — 한쪽만 있으면 관찰 차이가 아니라\n * 입력 구성 차이다.\n */\nexport function createBridgeDiff(\n before: readonly BridgeFactsDocument[],\n after: readonly BridgeFactsDocument[],\n beforeMessages?: readonly BridgeMessageDocument[],\n afterMessages?: readonly BridgeMessageDocument[],\n): BridgeDiffDocument {\n validateSnapshots(before, after);\n validateMessageSnapshots(beforeMessages, afterMessages);\n const project = before[0]?.project ?? after[0]!.project;\n const oldJoin = joinBridgeDocuments(before);\n const newJoin = joinBridgeDocuments(after);\n if (isBridgeJoinDeferred(oldJoin) || isBridgeJoinDeferred(newJoin)) {\n throw new BridgeJoinValidationError(\n 'Cannot compare deferred bridge joins; split mixed bridge targets and '\n + `retry. The before inputs observed ${oldJoin.observedFacts} facts across `\n + `${before.length} documents, and the after inputs observed `\n + `${newJoin.observedFacts} facts across ${after.length} documents.`,\n );\n }\n const oldMessages = beforeMessages === undefined\n ? undefined\n : joinMessageBridges(beforeMessages, project);\n const newMessages = afterMessages === undefined\n ? undefined\n : joinMessageBridges(afterMessages, project);\n const oldReport = createCheckReport(oldJoin, oldMessages);\n const newReport = createCheckReport(newJoin, newMessages);\n const addedMethods = difference(newJoin.matchedMethods, oldJoin.matchedMethods, logicalKey);\n const removedMethods = difference(oldJoin.matchedMethods, newJoin.matchedMethods, logicalKey);\n const addedModules = difference(newJoin.matchedModules, oldJoin.matchedModules, logicalKey);\n const removedModules = difference(oldJoin.matchedModules, newJoin.matchedModules, logicalKey);\n const addedComponents = difference(newJoin.matchedComponents, oldJoin.matchedComponents, logicalKey);\n const removedComponents = difference(oldJoin.matchedComponents, newJoin.matchedComponents, logicalKey);\n const introducedIssues = difference(newReport.issues, oldReport.issues, baselineEntryKey);\n const resolvedIssues = difference(oldReport.issues, newReport.issues, baselineEntryKey);\n const messageBoundaries = oldMessages === undefined || newMessages === undefined\n ? {}\n : {\n addedMessageBoundaries: difference(matchedMessageRoutes(newMessages),\n matchedMessageRoutes(oldMessages), messageBoundaryKey),\n removedMessageBoundaries: difference(matchedMessageRoutes(oldMessages),\n matchedMessageRoutes(newMessages), messageBoundaryKey),\n };\n return {\n format: 'isthmus-diff' as const,\n version: 1 as const,\n summary: {\n addedMethods: addedMethods.length,\n removedMethods: removedMethods.length,\n addedModules: addedModules.length,\n removedModules: removedModules.length,\n addedComponents: addedComponents.length,\n removedComponents: removedComponents.length,\n introducedErrors: introducedIssues.filter((issue) => issue.severity === 'error').length,\n introducedWarnings: introducedIssues.filter((issue) => issue.severity === 'warning').length,\n resolvedIssues: resolvedIssues.length,\n ...(oldMessages === undefined || newMessages === undefined ? {} : {\n addedMessageBoundaries: matchedMessageBoundaryCount(newMessages, oldMessages),\n removedMessageBoundaries: matchedMessageBoundaryCount(oldMessages, newMessages),\n }),\n },\n addedMethods,\n removedMethods,\n addedModules,\n removedModules,\n addedComponents,\n removedComponents,\n ...messageBoundaries,\n introducedIssues,\n resolvedIssues,\n limitations: {\n before: oldReport.limitations,\n after: newReport.limitations,\n added: difference(newReport.limitations, oldReport.limitations, limitationKey),\n removed: difference(oldReport.limitations, newReport.limitations, limitationKey),\n },\n producers: { before: producerVersions(before), after: producerVersions(after) },\n };\n}\n\n/** literal로 양쪽이 관찰된 v2 경계만 돌려준다 — 후보 prefix는 확정 매치가 아니다. */\nfunction matchedMessageRoutes(messages: MessageBridgeJoin): readonly MessageBridgeRoute[] {\n return messages.routes.filter((route) =>\n route.matching === 'literal' && route.senders.length > 0 && route.handlers.length > 0);\n}\n\n/** 두 스냅샷의 literal v2 경계 키를 비교해 추가된 수를 센다. */\nfunction matchedMessageBoundaryCount(\n left: MessageBridgeJoin,\n right: MessageBridgeJoin,\n): number {\n return difference(matchedMessageRoutes(left), matchedMessageRoutes(right), messageBoundaryKey).length;\n}\n\n/** v2 경계는 transport·주소로만 식별한다 — 같은 채널 이름이라도 transport가 다르다. */\nfunction messageBoundaryKey(route: MessageBridgeRoute): string {\n return JSON.stringify([route.transport, route.channel]);\n}\n\n/**\n * v2 입력은 두 스냅샷 모두 있거나 모두 없어야 하며, transport 집합이 같아야 한다.\n *\n * 한쪽에만 v2가 있으면 그 transport의 경계 전부가 추가·삭제로 보인다 — 관찰 차이가\n * 아니라 입력 구성 차이므로 입력 오류로 거부한다.\n */\nfunction validateMessageSnapshots(\n before: readonly BridgeMessageDocument[] | undefined,\n after: readonly BridgeMessageDocument[] | undefined,\n): void {\n if (before === undefined && after === undefined) return;\n if (before === undefined || after === undefined) {\n throw new BridgeJoinValidationError(\n 'Diff message inputs must be present in both snapshots; rebuild both from one checkout.',\n );\n }\n const transports = (docs: readonly BridgeMessageDocument[]): string =>\n JSON.stringify([...new Set(docs.map(({ transport }) => transport))].sort(compareStrings));\n if (transports(before) !== transports(after)) {\n throw new BridgeJoinValidationError(\n 'Diff message inputs must observe the same transports in both snapshots.',\n );\n }\n}\n\n/** 플랫폼 누락이나 다른 프로젝트를 코드 삭제로 오해하지 않도록 입력 구성을 고정한다. */\nfunction validateSnapshots(before: readonly BridgeFactsDocument[], after: readonly BridgeFactsDocument[]): void {\n const all = [...before, ...after];\n if (new Set(all.map((doc) => doc.project)).size !== 1 ||\n new Set(all.filter((doc) => isReceiverPlatform(doc.platform)).map((doc) => doc.platform)).size !== 1 ||\n ![before, after].every((docs) => docs.some((doc) => isCallerPlatform(doc.platform)) &&\n docs.some((doc) => isReceiverPlatform(doc.platform))) ||\n JSON.stringify(producerInventory(before)) !== JSON.stringify(producerInventory(after)) ||\n JSON.stringify(snapshotTargets(before)) !== JSON.stringify(snapshotTargets(after)) ||\n all.some((doc) => !isCallerPlatform(doc.platform) && !isReceiverPlatform(doc.platform))) {\n throw new BridgeJoinValidationError(\n 'Diff requires the same project, the same bridge targets, and matching '\n + 'caller/native producer inventories in both snapshots; rebuild both '\n + 'snapshots from one checkout.',\n );\n }\n}\n\n/**\n * 스냅샷이 관찰한 브리지 target 집합을 정렬해 돌려준다.\n *\n * target 집합이 다른 두 시점을 비교하면 한쪽 target의 사실 전부가 삭제·추가로\n * 보이므로 관찰 차이가 아니라 입력 구성 차이다. 사실이 없는 문서의 null target은\n * 집합에 넣지 않는다.\n */\nfunction snapshotTargets(docs: readonly BridgeFactsDocument[]): BridgeTarget[] {\n return [...new Set(docs.flatMap((doc) => doc.target === null ? [] : [doc.target]))]\n .sort(compareStrings);\n}\n\n/** 버전 변화는 출력하되 플랫폼·도구별 문서 개수 변화는 허용하지 않는다. */\nfunction producerInventory(docs: readonly BridgeFactsDocument[]): string[] {\n return docs.map((doc) => JSON.stringify([doc.platform, doc.tool.name])).sort(compareStrings);\n}\n\n/** 추출기 업그레이드가 관찰 차이의 원인인지 검토할 버전 근거다. */\nfunction producerVersions(docs: readonly BridgeFactsDocument[]) {\n return docs.map((doc) => ({ platform: doc.platform, ...doc.tool, generatedAt: doc.generatedAt }))\n .sort((a, b) => compareStrings(JSON.stringify(a), JSON.stringify(b)));\n}\n\n/** 충돌 없는 논리 키로 비교해 소스 줄 이동을 추가·삭제로 보고하지 않는다. */\nfunction logicalKey(item: { readonly target: BridgeTarget; readonly channel: string; readonly method?: string }): string {\n return JSON.stringify([item.target, item.channel, item.method ?? null]);\n}\n\nfunction limitationKey(item: { readonly platform: string; readonly target: string | null; readonly tool: string; readonly message: string; readonly channels?: readonly string[]; readonly origin?: 'consumer' }): string {\n return JSON.stringify([item.platform, item.target, item.tool, item.message, item.channels ?? null, item.origin ?? null]);\n}\n\n/** 키 집합 차이를 안정적으로 정렬하고 원래 증거를 보존한다. */\nfunction difference<T>(left: readonly T[], right: readonly T[], key: (item: T) => string): T[] {\n const existing = new Set(right.map(key));\n return left.filter((item) => !existing.has(key(item)))\n .sort((a, b) => compareStrings(key(a), key(b)));\n}\n"]}
|
package/dist/report/graph.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BridgeLocation, BridgePlatform, BridgeSymbol, BridgeSourceLanguage, BridgeTarget } from '../exchange/parse.ts';
|
|
2
2
|
import type { BridgeJoinResult, JoinLimitation } from '../join/join.ts';
|
|
3
|
+
import type { MessageBridgeJoin } from '../join/messages.ts';
|
|
3
4
|
/** 소스 위치 하나를 나타내는 경계 그래프 노드다. */
|
|
4
5
|
export interface BridgeGraphNode {
|
|
5
6
|
readonly id: string;
|
|
@@ -12,7 +13,7 @@ export interface BridgeGraphNode {
|
|
|
12
13
|
export interface BridgeGraphEdge {
|
|
13
14
|
readonly from: string;
|
|
14
15
|
readonly to: string;
|
|
15
|
-
readonly kind: 'channel' | 'method';
|
|
16
|
+
readonly kind: 'channel' | 'method' | 'module' | 'component' | 'message' | 'stream';
|
|
16
17
|
readonly target: BridgeTarget;
|
|
17
18
|
readonly channel: string;
|
|
18
19
|
readonly method?: string;
|
|
@@ -37,7 +38,12 @@ export declare class BridgeGraphValidationError extends Error {
|
|
|
37
38
|
}
|
|
38
39
|
/** 경계 그래프를 요청한 출력 형식으로 렌더링한다. */
|
|
39
40
|
export declare function renderBridgeGraph(graph: BridgeGraphDocument, format: 'json' | 'dot' | 'mermaid'): string;
|
|
40
|
-
/**
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* 조인 결과의 매치만 경계 그래프로 만든다.
|
|
43
|
+
*
|
|
44
|
+
* `messages`를 주면 literal로 확정된 Basic·Event 경계가 `message`·`stream`
|
|
45
|
+
* 간선으로 더해진다. dynamic prefix 후보는 확정 매치가 아니므로 담지 않는다.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createBridgeGraph(joined: BridgeJoinResult, messages?: MessageBridgeJoin): BridgeGraphDocument;
|
|
42
48
|
/** 모든 Cartesian 간선 수를 할당 전에 계산해 메모리 폭증을 막는다. */
|
|
43
|
-
export declare function assertBridgeGraphSize(joined: BridgeJoinResult): void;
|
|
49
|
+
export declare function assertBridgeGraphSize(joined: BridgeJoinResult, messages?: MessageBridgeJoin): void;
|
package/dist/report/graph.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isBridgeJoinDeferred } from "../join/join.js";
|
|
1
|
+
import { compareLimitations, isBridgeJoinDeferred } from "../join/join.js";
|
|
2
2
|
import { compareStrings } from "../compare.js";
|
|
3
3
|
import { encodeSortedJson } from "./sorted-json.js";
|
|
4
4
|
/** 한 번에 생성할 수 있는 경계 그래프 간선 수다. */
|
|
@@ -94,30 +94,66 @@ function edgeLabel(edge) {
|
|
|
94
94
|
: `${edge.channel}#${edge.method}`;
|
|
95
95
|
return `${edge.kind} ${key}`;
|
|
96
96
|
}
|
|
97
|
-
/**
|
|
98
|
-
|
|
97
|
+
/**
|
|
98
|
+
* 조인 결과의 매치만 경계 그래프로 만든다.
|
|
99
|
+
*
|
|
100
|
+
* `messages`를 주면 literal로 확정된 Basic·Event 경계가 `message`·`stream`
|
|
101
|
+
* 간선으로 더해진다. dynamic prefix 후보는 확정 매치가 아니므로 담지 않는다.
|
|
102
|
+
*/
|
|
103
|
+
export function createBridgeGraph(joined, messages) {
|
|
99
104
|
if (isBridgeJoinDeferred(joined)) {
|
|
100
105
|
throw new Error('Cannot create a graph from a deferred bridge join.');
|
|
101
106
|
}
|
|
102
|
-
assertBridgeGraphSize(joined);
|
|
107
|
+
assertBridgeGraphSize(joined, messages);
|
|
103
108
|
const nodes = new Map();
|
|
104
109
|
const edges = [];
|
|
105
110
|
addChannelEdges(joined, nodes, edges);
|
|
106
111
|
addMethodEdges(joined, nodes, edges);
|
|
112
|
+
addNameEdges(joined.matchedModules, 'module', nodes, edges);
|
|
113
|
+
addNameEdges(joined.matchedComponents, 'component', nodes, edges);
|
|
114
|
+
if (messages !== undefined)
|
|
115
|
+
addMessageEdges(messages, nodes, edges);
|
|
107
116
|
return {
|
|
108
117
|
format: 'isthmus-graph',
|
|
109
118
|
version: 1,
|
|
110
119
|
nodes: [...nodes.values()].sort((left, right) => compareStrings(left.id, right.id)),
|
|
111
120
|
edges: edges.sort(compareEdges),
|
|
112
|
-
limitations:
|
|
121
|
+
limitations: messages === undefined
|
|
122
|
+
? joined.limitations
|
|
123
|
+
: [...joined.limitations, ...messages.limitations].sort(compareLimitations),
|
|
113
124
|
};
|
|
114
125
|
}
|
|
126
|
+
/** literal로 확정된 Basic·Event 경계의 발신→수신 간선을 추가한다. */
|
|
127
|
+
function addMessageEdges(messages, nodes, edges) {
|
|
128
|
+
for (const route of messages.routes) {
|
|
129
|
+
if (route.matching !== 'literal')
|
|
130
|
+
continue;
|
|
131
|
+
for (const sender of route.senders) {
|
|
132
|
+
for (const handler of route.handlers) {
|
|
133
|
+
const from = addNode(nodes, sender);
|
|
134
|
+
const to = addNode(nodes, handler);
|
|
135
|
+
edges.push({
|
|
136
|
+
from,
|
|
137
|
+
to,
|
|
138
|
+
kind: route.transport === 'event-channel' ? 'stream' : 'message',
|
|
139
|
+
target: 'flutter',
|
|
140
|
+
channel: route.channel,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
115
146
|
/** 모든 Cartesian 간선 수를 할당 전에 계산해 메모리 폭증을 막는다. */
|
|
116
|
-
export function assertBridgeGraphSize(joined) {
|
|
147
|
+
export function assertBridgeGraphSize(joined, messages) {
|
|
117
148
|
let edgeCount = 0;
|
|
118
149
|
const endpointCounts = [
|
|
119
150
|
...joined.matchedChannels.map(({ creations, registrations }) => [creations.length, registrations.length]),
|
|
120
151
|
...joined.matchedMethods.map(({ invocations, handlers }) => [invocations.length, handlers.length]),
|
|
152
|
+
...joined.matchedModules.map(({ callers, receivers }) => [callers.length, receivers.length]),
|
|
153
|
+
...joined.matchedComponents.map(({ callers, receivers }) => [callers.length, receivers.length]),
|
|
154
|
+
...(messages?.routes ?? [])
|
|
155
|
+
.filter((route) => route.matching === 'literal')
|
|
156
|
+
.map(({ senders, handlers }) => [senders.length, handlers.length]),
|
|
121
157
|
];
|
|
122
158
|
for (const [fromCount, toCount] of endpointCounts) {
|
|
123
159
|
if (toCount > 0 &&
|
|
@@ -164,6 +200,24 @@ function addMethodEdges(joined, nodes, edges) {
|
|
|
164
200
|
}
|
|
165
201
|
}
|
|
166
202
|
}
|
|
203
|
+
/** 매치된 모듈·컴포넌트 이름의 호출→export 간선을 추가한다. */
|
|
204
|
+
function addNameEdges(matches, kind, nodes, edges) {
|
|
205
|
+
for (const match of matches) {
|
|
206
|
+
for (const caller of match.callers) {
|
|
207
|
+
for (const receiver of match.receivers) {
|
|
208
|
+
const from = addNode(nodes, caller);
|
|
209
|
+
const to = addNode(nodes, receiver);
|
|
210
|
+
edges.push({
|
|
211
|
+
from,
|
|
212
|
+
to,
|
|
213
|
+
kind,
|
|
214
|
+
target: match.target,
|
|
215
|
+
channel: match.channel,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
167
221
|
/** 증거 위치를 그래프 노드로 추가하고 안정 ID를 돌려준다. */
|
|
168
222
|
function addNode(nodes, endpoint) {
|
|
169
223
|
const id = endpointId(endpoint);
|