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,137 @@
|
|
|
1
|
+
import { compareStrings } from "../compare.js";
|
|
2
|
+
import { RuntimeValidationError } from "../exchange/runtime.js";
|
|
3
|
+
import { MAX_DOCUMENTS_PER_JOIN } from "../join/join.js";
|
|
4
|
+
/** 전체 평가와 무관한 증거 표시 상한. 생략 수를 명시한다. */
|
|
5
|
+
export const MAX_RUNTIME_EVIDENCE_PER_CHECK = 20;
|
|
6
|
+
const RUNTIME_TERMINAL_OUTCOMES = [
|
|
7
|
+
'success', 'missing-handler', 'error', 'timeout',
|
|
8
|
+
];
|
|
9
|
+
/** 검증된 관찰을 라우팅 키로 색인해 기대 항목마다 전체 이벤트를 다시 훑지 않는다. */
|
|
10
|
+
export function verifyRuntimeEvidence(expectations, documents) {
|
|
11
|
+
if (documents.length > MAX_DOCUMENTS_PER_JOIN)
|
|
12
|
+
throw new RuntimeValidationError('Too many runtime documents.');
|
|
13
|
+
const ids = new Set();
|
|
14
|
+
for (const document of documents) {
|
|
15
|
+
if (document.project !== expectations.project)
|
|
16
|
+
throw new RuntimeValidationError('Runtime inputs must describe the same project.');
|
|
17
|
+
if (ids.has(document.run.id))
|
|
18
|
+
throw new RuntimeValidationError('Duplicate runtime run identifiers.');
|
|
19
|
+
ids.add(document.run.id);
|
|
20
|
+
}
|
|
21
|
+
const sorted = [...documents].sort((a, b) => compareStrings(a.run.id, b.run.id));
|
|
22
|
+
const current = sorted.filter(({ revision }) => revision === expectations.revision);
|
|
23
|
+
const eventsByRoute = new Map();
|
|
24
|
+
const expectationsByRoute = new Map();
|
|
25
|
+
for (const expected of expectations.checks) {
|
|
26
|
+
const key = routeKey(expected.scenario, expected.platform, expected, undefined);
|
|
27
|
+
let routeExpectations = expectationsByRoute.get(key);
|
|
28
|
+
if (routeExpectations === undefined) {
|
|
29
|
+
routeExpectations = { instances: new Map() };
|
|
30
|
+
expectationsByRoute.set(key, routeExpectations);
|
|
31
|
+
}
|
|
32
|
+
const allowed = expected.allowedOutcomes ?? ['success'];
|
|
33
|
+
if (expected.instance === undefined) {
|
|
34
|
+
routeExpectations.broad = intersectAllowed(routeExpectations.broad, allowed);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
routeExpectations.instances.set(expected.instance, intersectAllowed(routeExpectations.instances.get(expected.instance), allowed));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const incompleteScenarios = new Set();
|
|
41
|
+
const failures = [];
|
|
42
|
+
let pendingCalls = 0;
|
|
43
|
+
let expectedFailedCalls = 0;
|
|
44
|
+
let unexpectedFailedCalls = 0;
|
|
45
|
+
for (const { run, events, droppedEvents } of current) {
|
|
46
|
+
if (run.status !== 'completed' || droppedEvents > 0) {
|
|
47
|
+
incompleteScenarios.add(JSON.stringify([run.scenario, run.platform]));
|
|
48
|
+
}
|
|
49
|
+
for (const event of events) {
|
|
50
|
+
const evidence = { runId: run.id, sequence: event.sequence,
|
|
51
|
+
instance: event.instance, outcome: event.outcome,
|
|
52
|
+
...(event.caller === undefined ? {} : { caller: event.caller }) };
|
|
53
|
+
for (const instance of [undefined, event.instance]) {
|
|
54
|
+
const key = routeKey(run.scenario, run.platform, event, instance);
|
|
55
|
+
let bucket = eventsByRoute.get(key);
|
|
56
|
+
if (bucket === undefined) {
|
|
57
|
+
bucket = { observedCalls: 0, outcomes: emptyOutcomes(), evidence: [] };
|
|
58
|
+
eventsByRoute.set(key, bucket);
|
|
59
|
+
}
|
|
60
|
+
bucket.observedCalls++;
|
|
61
|
+
bucket.outcomes[event.outcome]++;
|
|
62
|
+
if (bucket.evidence.length < MAX_RUNTIME_EVIDENCE_PER_CHECK)
|
|
63
|
+
bucket.evidence.push(evidence);
|
|
64
|
+
}
|
|
65
|
+
const eventOutcome = event.outcome;
|
|
66
|
+
if (failed(eventOutcome)) {
|
|
67
|
+
failures.push({ runId: run.id, event });
|
|
68
|
+
const routeExpectations = expectationsByRoute.get(routeKey(run.scenario, run.platform, event, undefined));
|
|
69
|
+
const applicable = routeExpectations === undefined ? [] : [
|
|
70
|
+
...(routeExpectations.broad === undefined ? [] : [routeExpectations.broad]),
|
|
71
|
+
...(routeExpectations.instances.get(event.instance) === undefined ? [] :
|
|
72
|
+
[routeExpectations.instances.get(event.instance)]),
|
|
73
|
+
];
|
|
74
|
+
if (applicable.length > 0 && applicable.every((allowed) => allowed.includes(eventOutcome))) {
|
|
75
|
+
expectedFailedCalls++;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
unexpectedFailedCalls++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (event.outcome === 'pending')
|
|
82
|
+
pendingCalls++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const checks = [...expectations.checks].sort((a, b) => compareStrings(a.id, b.id)).map((expected) => {
|
|
86
|
+
const bucket = eventsByRoute.get(routeKey(expected.scenario, expected.platform, expected, expected.instance));
|
|
87
|
+
const observedCalls = bucket?.observedCalls ?? 0;
|
|
88
|
+
const allowed = expected.allowedOutcomes ?? ['success'];
|
|
89
|
+
const terminalRejected = bucket === undefined ? false : RUNTIME_TERMINAL_OUTCOMES
|
|
90
|
+
.some((outcome) => bucket.outcomes[outcome] > 0 && !allowed.includes(outcome));
|
|
91
|
+
const status = terminalRejected ? 'failed'
|
|
92
|
+
: observedCalls === 0 ? 'unobserved'
|
|
93
|
+
: (bucket?.outcomes.pending ?? 0) > 0 ||
|
|
94
|
+
incompleteScenarios.has(JSON.stringify([expected.scenario, expected.platform])) ? 'incomplete'
|
|
95
|
+
: 'passed';
|
|
96
|
+
return { expected, status, observedCalls, evidence: bucket?.evidence ?? [],
|
|
97
|
+
evidenceOmitted: Math.max(0, observedCalls - MAX_RUNTIME_EVIDENCE_PER_CHECK) };
|
|
98
|
+
});
|
|
99
|
+
const summary = {
|
|
100
|
+
runs: documents.length, staleRuns: documents.length - current.length,
|
|
101
|
+
incompleteRuns: current.filter(({ run }) => run.status !== 'completed').length,
|
|
102
|
+
droppedEvents: current.reduce((sum, { droppedEvents }) => sum + droppedEvents, 0),
|
|
103
|
+
failedCalls: failures.length, expectedFailedCalls, unexpectedFailedCalls, pendingCalls,
|
|
104
|
+
passedChecks: checks.filter(({ status }) => status === 'passed').length,
|
|
105
|
+
failedChecks: checks.filter(({ status }) => status === 'failed').length,
|
|
106
|
+
unobservedChecks: checks.filter(({ status }) => status === 'unobserved').length,
|
|
107
|
+
incompleteChecks: checks.filter(({ status }) => status === 'incomplete').length,
|
|
108
|
+
};
|
|
109
|
+
if (!Number.isSafeInteger(summary.droppedEvents)) {
|
|
110
|
+
throw new RuntimeValidationError('Runtime dropped-event total exceeds the integer limit.');
|
|
111
|
+
}
|
|
112
|
+
const status = summary.unexpectedFailedCalls > 0 || summary.failedChecks > 0 ? 'failed'
|
|
113
|
+
: summary.staleRuns > 0 || summary.incompleteRuns > 0 || summary.droppedEvents > 0 ||
|
|
114
|
+
summary.pendingCalls > 0 || summary.passedChecks !== checks.length ? 'incomplete' : 'passed';
|
|
115
|
+
return { format: 'isthmus-runtime-check', version: 1, project: expectations.project, revision: expectations.revision,
|
|
116
|
+
scope: 'declared-scenarios', complete: false, status, summary, checks, failures,
|
|
117
|
+
runs: sorted.map(({ run, revision, tool, droppedEvents, events }) => ({
|
|
118
|
+
run, revision, tool, droppedEvents, stale: revision !== expectations.revision, observedEvents: events.length,
|
|
119
|
+
})) };
|
|
120
|
+
}
|
|
121
|
+
/** 모든 기대가 허용하는 결과만 남겨 겹치는 기대의 좁은 조건을 보존한다. */
|
|
122
|
+
function intersectAllowed(existing, next) {
|
|
123
|
+
return existing === undefined ? next : existing.filter((outcome) => next.includes(outcome));
|
|
124
|
+
}
|
|
125
|
+
/** 관찰 결과 카운터를 생성한다. */
|
|
126
|
+
function emptyOutcomes() {
|
|
127
|
+
return { success: 0, 'missing-handler': 0, error: 0, timeout: 0, pending: 0 };
|
|
128
|
+
}
|
|
129
|
+
/** '*' 같은 실제 이름과 전체 인스턴스 선택(null)의 키 공간을 분리한다. */
|
|
130
|
+
function routeKey(scenario, platform, route, instance) {
|
|
131
|
+
return JSON.stringify([scenario, platform, route.transport, route.channel, route.method ?? null, instance ?? null]);
|
|
132
|
+
}
|
|
133
|
+
/** 성공과 아직 응답을 받지 못한 상태를 실패와 구분한다. */
|
|
134
|
+
function failed(outcome) {
|
|
135
|
+
return outcome === 'missing-handler' || outcome === 'error' || outcome === 'timeout';
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/report/runtime.ts"],"names":[],"mappings":"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","sourcesContent":["import { compareStrings } from '../compare.ts';\nimport type {\n BridgeRuntimeDocument, RuntimeEvent, RuntimeExpectation, RuntimeExpectations, RuntimeTerminalOutcome,\n} from '../exchange/runtime.ts';\nimport { RuntimeValidationError } from '../exchange/runtime.ts';\nimport { MAX_DOCUMENTS_PER_JOIN } from '../join/join.ts';\n\n/** 전체 평가와 무관한 증거 표시 상한. 생략 수를 명시한다. */\nexport const MAX_RUNTIME_EVIDENCE_PER_CHECK = 20;\n\nconst RUNTIME_TERMINAL_OUTCOMES: readonly RuntimeTerminalOutcome[] = [\n 'success', 'missing-handler', 'error', 'timeout',\n];\n\n/** 원본 run과 sequence로 재현 가능한 통신 결과 참조다. */\nexport interface RuntimeEvidence {\n readonly runId: string;\n readonly sequence: number;\n readonly instance: string;\n readonly outcome: RuntimeEvent['outcome'];\n readonly caller?: RuntimeEvent['caller'];\n}\n\n/** 같은 라우팅 기대를 반복해도 전체 호출을 다시 검사하지 않도록 집계한다. */\ninterface RouteObservations {\n observedCalls: number;\n outcomes: Record<RuntimeEvent['outcome'], number>;\n evidence: RuntimeEvidence[];\n}\n\n/** 한 라우팅에 적용되는 기대 결과들의 교집합이다. */\ninterface RouteExpectations {\n broad?: readonly RuntimeTerminalOutcome[];\n instances: Map<string, readonly RuntimeTerminalOutcome[]>;\n}\n\n/** 기대 항목 하나의 결과와 관찰 근거다. */\nexport interface RuntimeCheckResult {\n readonly expected: RuntimeExpectation;\n readonly status: 'passed' | 'failed' | 'unobserved' | 'incomplete';\n readonly observedCalls: number;\n readonly evidence: readonly RuntimeEvidence[];\n readonly evidenceOmitted: number;\n}\n\n/** 선언한 시나리오의 통신 검증 결과. 모든 실행 경로를 검증했다는 뜻은 아니다. */\nexport interface RuntimeVerificationReport {\n readonly format: 'isthmus-runtime-check';\n readonly version: 1;\n readonly project: string;\n readonly revision: string;\n readonly scope: 'declared-scenarios';\n readonly complete: false;\n readonly status: 'passed' | 'failed' | 'incomplete';\n readonly summary: {\n readonly runs: number;\n readonly staleRuns: number;\n readonly incompleteRuns: number;\n readonly droppedEvents: number;\n readonly failedCalls: number;\n readonly expectedFailedCalls: number;\n readonly unexpectedFailedCalls: number;\n readonly pendingCalls: number;\n readonly passedChecks: number;\n readonly failedChecks: number;\n readonly unobservedChecks: number;\n readonly incompleteChecks: number;\n };\n readonly checks: readonly RuntimeCheckResult[];\n readonly failures: ReadonlyArray<{ readonly runId: string; readonly event: RuntimeEvent }>;\n readonly runs: ReadonlyArray<Omit<BridgeRuntimeDocument, 'events' | 'project' | 'format' | 'version'> & {\n readonly stale: boolean;\n readonly observedEvents: number;\n }>;\n}\n\n/** 검증된 관찰을 라우팅 키로 색인해 기대 항목마다 전체 이벤트를 다시 훑지 않는다. */\nexport function verifyRuntimeEvidence(\n expectations: RuntimeExpectations, documents: readonly BridgeRuntimeDocument[],\n): RuntimeVerificationReport {\n if (documents.length > MAX_DOCUMENTS_PER_JOIN) throw new RuntimeValidationError('Too many runtime documents.');\n const ids = new Set<string>();\n for (const document of documents) {\n if (document.project !== expectations.project) throw new RuntimeValidationError('Runtime inputs must describe the same project.');\n if (ids.has(document.run.id)) throw new RuntimeValidationError('Duplicate runtime run identifiers.');\n ids.add(document.run.id);\n }\n const sorted = [...documents].sort((a, b) => compareStrings(a.run.id, b.run.id));\n const current = sorted.filter(({ revision }) => revision === expectations.revision);\n const eventsByRoute = new Map<string, RouteObservations>();\n const expectationsByRoute = new Map<string, RouteExpectations>();\n for (const expected of expectations.checks) {\n const key = routeKey(expected.scenario, expected.platform, expected, undefined);\n let routeExpectations = expectationsByRoute.get(key);\n if (routeExpectations === undefined) {\n routeExpectations = { instances: new Map() };\n expectationsByRoute.set(key, routeExpectations);\n }\n const allowed = expected.allowedOutcomes ?? ['success'];\n if (expected.instance === undefined) {\n routeExpectations.broad = intersectAllowed(routeExpectations.broad, allowed);\n } else {\n routeExpectations.instances.set(expected.instance,\n intersectAllowed(routeExpectations.instances.get(expected.instance), allowed));\n }\n }\n const incompleteScenarios = new Set<string>();\n const failures: Array<{ runId: string; event: RuntimeEvent }> = [];\n let pendingCalls = 0;\n let expectedFailedCalls = 0;\n let unexpectedFailedCalls = 0;\n for (const { run, events, droppedEvents } of current) {\n if (run.status !== 'completed' || droppedEvents > 0) {\n incompleteScenarios.add(JSON.stringify([run.scenario, run.platform]));\n }\n for (const event of events) {\n const evidence: RuntimeEvidence = { runId: run.id, sequence: event.sequence,\n instance: event.instance, outcome: event.outcome,\n ...(event.caller === undefined ? {} : { caller: event.caller }) };\n for (const instance of [undefined, event.instance]) {\n const key = routeKey(run.scenario, run.platform, event, instance);\n let bucket = eventsByRoute.get(key);\n if (bucket === undefined) {\n bucket = { observedCalls: 0, outcomes: emptyOutcomes(), evidence: [] };\n eventsByRoute.set(key, bucket);\n }\n bucket.observedCalls++;\n bucket.outcomes[event.outcome]++;\n if (bucket.evidence.length < MAX_RUNTIME_EVIDENCE_PER_CHECK) bucket.evidence.push(evidence);\n }\n const eventOutcome = event.outcome;\n if (failed(eventOutcome)) {\n failures.push({ runId: run.id, event });\n const routeExpectations = expectationsByRoute.get(routeKey(run.scenario, run.platform, event, undefined));\n const applicable = routeExpectations === undefined ? [] : [\n ...(routeExpectations.broad === undefined ? [] : [routeExpectations.broad]),\n ...(routeExpectations.instances.get(event.instance) === undefined ? [] :\n [routeExpectations.instances.get(event.instance)!]),\n ];\n if (applicable.length > 0 && applicable.every((allowed) => allowed.includes(eventOutcome))) {\n expectedFailedCalls++;\n } else {\n unexpectedFailedCalls++;\n }\n }\n if (event.outcome === 'pending') pendingCalls++;\n }\n }\n const checks = [...expectations.checks].sort((a, b) => compareStrings(a.id, b.id)).map((expected): RuntimeCheckResult => {\n const bucket = eventsByRoute.get(routeKey(expected.scenario, expected.platform, expected, expected.instance));\n const observedCalls = bucket?.observedCalls ?? 0;\n const allowed = expected.allowedOutcomes ?? ['success'];\n const terminalRejected = bucket === undefined ? false : RUNTIME_TERMINAL_OUTCOMES\n .some((outcome) => bucket.outcomes[outcome] > 0 && !allowed.includes(outcome));\n const status = terminalRejected ? 'failed'\n : observedCalls === 0 ? 'unobserved'\n : (bucket?.outcomes.pending ?? 0) > 0 ||\n incompleteScenarios.has(JSON.stringify([expected.scenario, expected.platform])) ? 'incomplete'\n : 'passed';\n return { expected, status, observedCalls, evidence: bucket?.evidence ?? [],\n evidenceOmitted: Math.max(0, observedCalls - MAX_RUNTIME_EVIDENCE_PER_CHECK) };\n });\n const summary = {\n runs: documents.length, staleRuns: documents.length - current.length,\n incompleteRuns: current.filter(({ run }) => run.status !== 'completed').length,\n droppedEvents: current.reduce((sum, { droppedEvents }) => sum + droppedEvents, 0),\n failedCalls: failures.length, expectedFailedCalls, unexpectedFailedCalls, pendingCalls,\n passedChecks: checks.filter(({ status }) => status === 'passed').length,\n failedChecks: checks.filter(({ status }) => status === 'failed').length,\n unobservedChecks: checks.filter(({ status }) => status === 'unobserved').length,\n incompleteChecks: checks.filter(({ status }) => status === 'incomplete').length,\n };\n if (!Number.isSafeInteger(summary.droppedEvents)) {\n throw new RuntimeValidationError('Runtime dropped-event total exceeds the integer limit.');\n }\n const status = summary.unexpectedFailedCalls > 0 || summary.failedChecks > 0 ? 'failed'\n : summary.staleRuns > 0 || summary.incompleteRuns > 0 || summary.droppedEvents > 0 ||\n summary.pendingCalls > 0 || summary.passedChecks !== checks.length ? 'incomplete' : 'passed';\n return { format: 'isthmus-runtime-check', version: 1, project: expectations.project, revision: expectations.revision,\n scope: 'declared-scenarios', complete: false, status, summary, checks, failures,\n runs: sorted.map(({ run, revision, tool, droppedEvents, events }) => ({\n run, revision, tool, droppedEvents, stale: revision !== expectations.revision, observedEvents: events.length,\n })) };\n}\n\n/** 모든 기대가 허용하는 결과만 남겨 겹치는 기대의 좁은 조건을 보존한다. */\nfunction intersectAllowed(\n existing: readonly RuntimeTerminalOutcome[] | undefined,\n next: readonly RuntimeTerminalOutcome[],\n): readonly RuntimeTerminalOutcome[] {\n return existing === undefined ? next : existing.filter((outcome) => next.includes(outcome));\n}\n\n/** 관찰 결과 카운터를 생성한다. */\nfunction emptyOutcomes(): Record<RuntimeEvent['outcome'], number> {\n return { success: 0, 'missing-handler': 0, error: 0, timeout: 0, pending: 0 };\n}\n\n/** '*' 같은 실제 이름과 전체 인스턴스 선택(null)의 키 공간을 분리한다. */\nfunction routeKey(\n scenario: string, platform: string, route: Pick<RuntimeEvent, 'transport' | 'channel' | 'method'>,\n instance: string | undefined,\n): string {\n return JSON.stringify([scenario, platform, route.transport, route.channel, route.method ?? null, instance ?? null]);\n}\n\n/** 성공과 아직 응답을 받지 못한 상태를 실패와 구분한다. */\nfunction failed(outcome: RuntimeEvent['outcome']): outcome is RuntimeTerminalOutcome {\n return outcome === 'missing-handler' || outcome === 'error' || outcome === 'timeout';\n}\n"]}
|
package/dist/report/sarif.d.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import type { CheckIssueCode, CheckReport } from './check-report.ts';
|
|
2
|
-
|
|
3
|
-
export type IssueFingerprint
|
|
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 {};
|
package/dist/report/sarif.js
CHANGED
|
@@ -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:
|
|
12
|
+
shortDescription: { text: checkIssueRuleDescriptions[id] },
|
|
21
13
|
}));
|
|
22
14
|
const ruleIndex = new Map(rules.map(({ id }, index) => [id, index]));
|
|
23
15
|
return {
|
package/dist/report/sarif.js.map
CHANGED
|
@@ -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;
|
|
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"]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** JSON 객체 키를 모든 깊이에서 정렬하고 마지막 개행을 붙인다. */
|
|
2
|
-
export declare function encodeSortedJson(value: unknown): string;
|
|
2
|
+
export declare function encodeSortedJson(value: unknown, compact?: boolean): string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { compareStrings } from "../compare.js";
|
|
2
2
|
/** JSON 객체 키를 모든 깊이에서 정렬하고 마지막 개행을 붙인다. */
|
|
3
|
-
export function encodeSortedJson(value) {
|
|
4
|
-
return `${JSON.stringify(sortJson(value), null, 2)}\n`;
|
|
3
|
+
export function encodeSortedJson(value, compact = false) {
|
|
4
|
+
return `${JSON.stringify(sortJson(value), null, compact ? undefined : 2)}\n`;
|
|
5
5
|
}
|
|
6
6
|
/** JSON 배열 순서는 보존하고 객체 키만 재귀 정렬한다. */
|
|
7
7
|
function sortJson(value) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sorted-json.js","sourceRoot":"","sources":["../../src/report/sorted-json.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,2CAA2C;AAC3C,MAAM,UAAU,gBAAgB,CAAC,KAAc;
|
|
1
|
+
{"version":3,"file":"sorted-json.js","sourceRoot":"","sources":["../../src/report/sorted-json.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,2CAA2C;AAC3C,MAAM,UAAU,gBAAgB,CAAC,KAAc,EAAE,OAAO,GAAG,KAAK;IAC9D,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/E,CAAC;AAED,sCAAsC;AACtC,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACrD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;SAClB,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;SACtD,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAC/C,CAAC;AACJ,CAAC","sourcesContent":["import { compareStrings } from '../compare.ts';\n\n/** JSON 객체 키를 모든 깊이에서 정렬하고 마지막 개행을 붙인다. */\nexport function encodeSortedJson(value: unknown, compact = false): string {\n return `${JSON.stringify(sortJson(value), null, compact ? undefined : 2)}\\n`;\n}\n\n/** JSON 배열 순서는 보존하고 객체 키만 재귀 정렬한다. */\nfunction sortJson(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortJson);\n if (typeof value !== 'object' || value === null) return value;\n return Object.fromEntries(\n Object.entries(value)\n .sort(([left], [right]) => compareStrings(left, right))\n .map(([key, item]) => [key, sortJson(item)]),\n );\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를 테스트한 뒤 검증된 범위를 기록한다.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# BasicMessageChannel 교환 계약 (bridge-facts v2)
|
|
2
|
+
|
|
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
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"format": "bridge-facts", "version": 2,
|
|
12
|
+
"transport": "basic-message-channel", "platform": "dart", "target": "flutter",
|
|
13
|
+
"project": "/project", "generatedAt": "2026-09-14T00:00:00Z",
|
|
14
|
+
"tool": { "name": "dartograph", "version": "development" },
|
|
15
|
+
"facts": [{
|
|
16
|
+
"kind": "message-send", "channel": "example/basic", "dynamic": false,
|
|
17
|
+
"location": { "path": "lib/api.dart", "line": 10, "column": 5 },
|
|
18
|
+
"symbol": { "qualifiedName": "Api.read" }
|
|
19
|
+
}],
|
|
20
|
+
"limitations": []
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- 기본 `bridges`는 기존 v1 MethodChannel 출력을 유지한다. v2는 Basic 전용이며
|
|
25
|
+
`message-send`(Dart send 호출)·`message-handle`(Swift/Kotlin setMessageHandler 등록)을 담는다.
|
|
26
|
+
channel을 만들기만 한 지점을 send로 만들지 않고 nil handler 제거도 등록으로 만들지 않는다.
|
|
27
|
+
- method 필드는 없다. Basic의 채널 이름을 가상의 MethodChannel 메서드로 바꾸지 않는다.
|
|
28
|
+
- project·위치·symbol·시각·문자열·사실 수·미귀속 규칙은 기존 계약의 근거 보존 원칙을 따른다.
|
|
29
|
+
sender는 dart, receiver는 swift 또는 kotlin이다. 사실이 없으면 target은 null이다.
|
|
30
|
+
- 정적 이름은 decoded literal이다. 알 수 없으면 원래 표현식을 channel에 두고 dynamic=true.
|
|
31
|
+
생성 코드를 Pigeon이라고 알아봤다는 이유로 인스턴스 suffix를 빈 문자열로 가정하지 않는다.
|
|
32
|
+
- 선택적 `channelPrefix`는 dynamic일 때만 쓴다. AST가 확인한 비어 있지 않은 literal prefix이며
|
|
33
|
+
가능한 주소의 보수적 범위다. 같은 prefix의 정적 연결은 후보이고 실제 suffix/instance가
|
|
34
|
+
같다는 증명이 아니다. 임의 변수 이름에서 prefix를 지어내지 않는다.
|
|
35
|
+
- Native channel을 귀속할 수 없으면 channel=null이며 `unattributed-message-handles:`
|
|
36
|
+
limitation을 함께 낸다. 알려지지 않은 receiver·codec·source·symbol은 숨기지 않는다.
|
|
37
|
+
- channelPrefix와 runtime 주소의 관계도 후보 근거다. 기존 MethodChannel과 동일한 주소를
|
|
38
|
+
쓰더라도 transport를 구분한다. prefix 후보를 literal 조인이나 실제 native 실행 신원으로
|
|
39
|
+
승격하지 않는다.
|
|
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를 무시하고 초록 결과를 내게 하지 않는다.
|
|
57
|
+
|
|
58
|
+
## handler별 의존 근거 (개발 계약)
|
|
59
|
+
|
|
60
|
+
공통 등록 함수에 여러 handler가 있으면 함수 수준 영향만으로 서로 독립적인 채널까지
|
|
61
|
+
전파될 수 있다. native `message-handle`은 선택적으로 다음 두 필드를 함께 제공한다.
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"handlerScope": {
|
|
66
|
+
"start": { "path": "macos/messages.g.swift", "line": 20, "column": 30 },
|
|
67
|
+
"end": { "path": "macos/messages.g.swift", "line": 28, "column": 4 },
|
|
68
|
+
"complete": true
|
|
69
|
+
},
|
|
70
|
+
"dependencies": [{
|
|
71
|
+
"kind": "call", "scope": "handler",
|
|
72
|
+
"location": { "path": "macos/messages.g.swift", "line": 23, "column": 16 },
|
|
73
|
+
"symbol": { "qualifiedName": "PluginApi.launch(url:)", "usr": "s:compiler-requirement" },
|
|
74
|
+
"dispatchTargets": [{ "qualifiedName": "Plugin.launch(url:)", "usr": "s:compiler-implementation" }]
|
|
75
|
+
}]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
예제 USR은 구조 설명용이며 실제 문서에는 compiler index의 신원만 쓴다. `symbol`의 기존
|
|
80
|
+
의미는 감싸는 등록 선언으로 유지한다. 범위 start/end는 같은 파일의 SwiftSyntax closure
|
|
81
|
+
여는/닫는 괄호 위치이며 양 끝을 포함한다. dependencies는 같은 파일의 index 발생 위치,
|
|
82
|
+
`kind: call|reference`, `scope: handler|registration`, 실제 참조 대상 symbol을 담는다.
|
|
83
|
+
handler 의존은 해당 범위 안이다. registration 의존은 같은 감싸는 선언 안에서 어떤 handler
|
|
84
|
+
closure에도 속하지 않는 공유 부분이다. 다른 handler의 본문을 공유 부분으로 취급하지 않는다.
|
|
85
|
+
|
|
86
|
+
선택적 dispatchTargets는 index가 기록한 overrides 관계로 확인된 구현 후보다. 이름으로
|
|
87
|
+
만들거나 실제 실행 대상으로 확정하지 않는다. 원래 참조한 requirement는 symbol에 남긴다.
|
|
88
|
+
행당 dispatch 후보 10,000개, fact당 dependency 10,000개, 문서 전체 dependency와 dispatch
|
|
89
|
+
후보 합계는 1,000,000개 이하이며 잘라서 complete로 내보내지 않는다.
|
|
90
|
+
|
|
91
|
+
complete는 **관찰된 index 사용 관계의 범위 귀속**에 한정된다. literal closure 범위가
|
|
92
|
+
확정되고 그 선언의 관찰된 사용 관계를 위치로 구분할 수 있으며 source/index 신선도가
|
|
93
|
+
확인될 때만 true다. 위치 없는 관계·모호한 대상·불명확한 범위·오래된 source/index·method
|
|
94
|
+
reference handler는 false 또는 근거 부재로 남긴다. 모든 런타임 의존성을 보증하지 않는다.
|
|
95
|
+
대상은 producer 그래프의 프로젝트 선언 간 사용 관계다. 인덱스가 parameter 또는 external로
|
|
96
|
+
식별한 대상의 값 흐름은 이 완전성에 포함하지 않는다. 이름이나 USR 모양으로 parameter를
|
|
97
|
+
추측하지 않으며 인덱스의 실제 대상 종류를 보존해 구분한다.
|
|
98
|
+
|
|
99
|
+
소비자는 complete한 근거가 있을 때 도달한 dependency/dispatch 후보에서 해당 boundary로
|
|
100
|
+
전파한다. 등록 선언 자체를 직접 변경 대상으로 선택하면 그 선언의 모든 handler를 포함한다.
|
|
101
|
+
감싸는 선언이 다른 handler의 호출 때문에 영향받았다는 이유만으로 모든 boundary로 다시
|
|
102
|
+
전파하지 않는다. 근거가 없거나 불완전하면 기존 넓은 후보를 보존하고 정밀도 공백을 알린다.
|
|
103
|
+
같은 boundary에 완전한 관찰과 불완전한 관찰이 함께 있으면 넓은 후보와 공백을 유지한다.
|
|
104
|
+
영향 숲에 없는, 변경과 무관한 dependency도 관련 boundary의 receiver 원문에는 남긴다.
|
|
105
|
+
|
|
106
|
+
이 문서만으로 producer/consumer 지원이 완료됐거나 Pigeon의 모든 생성 형태가 해석됐다는
|
|
107
|
+
뜻은 아니다. literal·alias·shadowing·동적 prefix·nil handler·동일 주소의 transport 차이와
|
|
108
|
+
공개 Pigeon 생성 source를 테스트한 뒤 검증된 범위를 기록한다.
|
|
109
|
+
|
|
110
|
+
## Kotlin producer의 개발 경계
|
|
111
|
+
|
|
112
|
+
Kotlin의 v2도 같은 필드·역할·상한을 사용한다. JVM 그래프에서 유일하게 확인한 symbol만
|
|
113
|
+
USR로 싣고, source 문자열에서 JVM 식별자를 합성하지 않는다. Kotlin 소스의 closure 범위와
|
|
114
|
+
bytecode 위치만으로 실제 callback의 전체 의존성을 보증하지 않는다. 위 두 필드를 제공하지
|
|
115
|
+
못하면 소비자는 넓은 영향 후보와 범위 공백을 유지한다. 정적 주소는 실제 Kotlin 문자열이며
|
|
116
|
+
동적 prefix는 구문으로 확인한 범위만 제공한다. 다른 플랫폼의 동일 주소를 Kotlin 실행 신원으로
|
|
117
|
+
사용하지 않는다. `--graph-file` 입력의 현재 소스/빌드 범위와 한계를 함께 확인한다.
|