@tienne/gestalt 0.88.0 → 0.89.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/CLAUDE.md +6 -2
  2. package/README.ko.md +2 -1
  3. package/README.md +3 -1
  4. package/dist/package.json +2 -1
  5. package/dist/plugin/skills/_shared/proactive-routing.md +1 -0
  6. package/dist/plugin/skills/architecture/SKILL.md +913 -0
  7. package/dist/schemas/architecture-ir.schema.json +596 -0
  8. package/dist/src/architecture/canvas-geometry.d.ts +40 -0
  9. package/dist/src/architecture/canvas-geometry.d.ts.map +1 -0
  10. package/dist/src/architecture/canvas-geometry.js +212 -0
  11. package/dist/src/architecture/canvas-geometry.js.map +1 -0
  12. package/dist/src/architecture/codeowners.d.ts +16 -0
  13. package/dist/src/architecture/codeowners.d.ts.map +1 -0
  14. package/dist/src/architecture/codeowners.js +58 -0
  15. package/dist/src/architecture/codeowners.js.map +1 -0
  16. package/dist/src/architecture/compare-layout.d.ts +27 -0
  17. package/dist/src/architecture/compare-layout.d.ts.map +1 -0
  18. package/dist/src/architecture/compare-layout.js +72 -0
  19. package/dist/src/architecture/compare-layout.js.map +1 -0
  20. package/dist/src/architecture/dataflow-layout.d.ts +29 -0
  21. package/dist/src/architecture/dataflow-layout.d.ts.map +1 -0
  22. package/dist/src/architecture/dataflow-layout.js +229 -0
  23. package/dist/src/architecture/dataflow-layout.js.map +1 -0
  24. package/dist/src/architecture/doc-fresh.d.ts +31 -0
  25. package/dist/src/architecture/doc-fresh.d.ts.map +1 -0
  26. package/dist/src/architecture/doc-fresh.js +84 -0
  27. package/dist/src/architecture/doc-fresh.js.map +1 -0
  28. package/dist/src/architecture/doc-link.d.ts +84 -0
  29. package/dist/src/architecture/doc-link.d.ts.map +1 -0
  30. package/dist/src/architecture/doc-link.js +396 -0
  31. package/dist/src/architecture/doc-link.js.map +1 -0
  32. package/dist/src/architecture/doc-routes.d.ts +39 -0
  33. package/dist/src/architecture/doc-routes.d.ts.map +1 -0
  34. package/dist/src/architecture/doc-routes.js +123 -0
  35. package/dist/src/architecture/doc-routes.js.map +1 -0
  36. package/dist/src/architecture/doc-scan.d.ts +107 -0
  37. package/dist/src/architecture/doc-scan.d.ts.map +1 -0
  38. package/dist/src/architecture/doc-scan.js +617 -0
  39. package/dist/src/architecture/doc-scan.js.map +1 -0
  40. package/dist/src/architecture/doc-stale.d.ts +32 -0
  41. package/dist/src/architecture/doc-stale.d.ts.map +1 -0
  42. package/dist/src/architecture/doc-stale.js +60 -0
  43. package/dist/src/architecture/doc-stale.js.map +1 -0
  44. package/dist/src/architecture/drilldown.d.ts +51 -0
  45. package/dist/src/architecture/drilldown.d.ts.map +1 -0
  46. package/dist/src/architecture/drilldown.js +1160 -0
  47. package/dist/src/architecture/drilldown.js.map +1 -0
  48. package/dist/src/architecture/endpoint-match.d.ts +43 -0
  49. package/dist/src/architecture/endpoint-match.d.ts.map +1 -0
  50. package/dist/src/architecture/endpoint-match.js +133 -0
  51. package/dist/src/architecture/endpoint-match.js.map +1 -0
  52. package/dist/src/architecture/flow-layout.d.ts +79 -0
  53. package/dist/src/architecture/flow-layout.d.ts.map +1 -0
  54. package/dist/src/architecture/flow-layout.js +519 -0
  55. package/dist/src/architecture/flow-layout.js.map +1 -0
  56. package/dist/src/architecture/focus.d.ts +14 -0
  57. package/dist/src/architecture/focus.d.ts.map +1 -0
  58. package/dist/src/architecture/focus.js +107 -0
  59. package/dist/src/architecture/focus.js.map +1 -0
  60. package/dist/src/architecture/global-context.d.ts +23 -0
  61. package/dist/src/architecture/global-context.d.ts.map +1 -0
  62. package/dist/src/architecture/global-context.js +67 -0
  63. package/dist/src/architecture/global-context.js.map +1 -0
  64. package/dist/src/architecture/html-client.d.ts +35 -0
  65. package/dist/src/architecture/html-client.d.ts.map +1 -0
  66. package/dist/src/architecture/html-client.js +1852 -0
  67. package/dist/src/architecture/html-client.js.map +1 -0
  68. package/dist/src/architecture/html-renderer.d.ts +22 -0
  69. package/dist/src/architecture/html-renderer.d.ts.map +1 -0
  70. package/dist/src/architecture/html-renderer.js +1290 -0
  71. package/dist/src/architecture/html-renderer.js.map +1 -0
  72. package/dist/src/architecture/html-theme.d.ts +25 -0
  73. package/dist/src/architecture/html-theme.d.ts.map +1 -0
  74. package/dist/src/architecture/html-theme.js +681 -0
  75. package/dist/src/architecture/html-theme.js.map +1 -0
  76. package/dist/src/architecture/index.d.ts +9 -0
  77. package/dist/src/architecture/index.d.ts.map +1 -0
  78. package/dist/src/architecture/index.js +9 -0
  79. package/dist/src/architecture/index.js.map +1 -0
  80. package/dist/src/architecture/ir-schema.d.ts +1832 -0
  81. package/dist/src/architecture/ir-schema.d.ts.map +1 -0
  82. package/dist/src/architecture/ir-schema.js +343 -0
  83. package/dist/src/architecture/ir-schema.js.map +1 -0
  84. package/dist/src/architecture/kind-text.d.ts +21 -0
  85. package/dist/src/architecture/kind-text.d.ts.map +1 -0
  86. package/dist/src/architecture/kind-text.js +42 -0
  87. package/dist/src/architecture/kind-text.js.map +1 -0
  88. package/dist/src/architecture/layout.d.ts +227 -0
  89. package/dist/src/architecture/layout.d.ts.map +1 -0
  90. package/dist/src/architecture/layout.js +627 -0
  91. package/dist/src/architecture/layout.js.map +1 -0
  92. package/dist/src/architecture/mcp-tool-match.d.ts +49 -0
  93. package/dist/src/architecture/mcp-tool-match.d.ts.map +1 -0
  94. package/dist/src/architecture/mcp-tool-match.js +53 -0
  95. package/dist/src/architecture/mcp-tool-match.js.map +1 -0
  96. package/dist/src/architecture/merge.d.ts +54 -0
  97. package/dist/src/architecture/merge.d.ts.map +1 -0
  98. package/dist/src/architecture/merge.js +681 -0
  99. package/dist/src/architecture/merge.js.map +1 -0
  100. package/dist/src/architecture/micro-app.d.ts +20 -0
  101. package/dist/src/architecture/micro-app.d.ts.map +1 -0
  102. package/dist/src/architecture/micro-app.js +41 -0
  103. package/dist/src/architecture/micro-app.js.map +1 -0
  104. package/dist/src/architecture/name-ref-match.d.ts +43 -0
  105. package/dist/src/architecture/name-ref-match.d.ts.map +1 -0
  106. package/dist/src/architecture/name-ref-match.js +127 -0
  107. package/dist/src/architecture/name-ref-match.js.map +1 -0
  108. package/dist/src/architecture/packs/data.d.ts +109 -0
  109. package/dist/src/architecture/packs/data.d.ts.map +1 -0
  110. package/dist/src/architecture/packs/data.js +64 -0
  111. package/dist/src/architecture/packs/data.js.map +1 -0
  112. package/dist/src/architecture/packs/generic.d.ts +47 -0
  113. package/dist/src/architecture/packs/generic.d.ts.map +1 -0
  114. package/dist/src/architecture/packs/generic.js +35 -0
  115. package/dist/src/architecture/packs/generic.js.map +1 -0
  116. package/dist/src/architecture/packs/harness.d.ts +96 -0
  117. package/dist/src/architecture/packs/harness.d.ts.map +1 -0
  118. package/dist/src/architecture/packs/harness.js +66 -0
  119. package/dist/src/architecture/packs/harness.js.map +1 -0
  120. package/dist/src/architecture/packs/index.d.ts +840 -0
  121. package/dist/src/architecture/packs/index.d.ts.map +1 -0
  122. package/dist/src/architecture/packs/index.js +87 -0
  123. package/dist/src/architecture/packs/index.js.map +1 -0
  124. package/dist/src/architecture/packs/infra.d.ts +91 -0
  125. package/dist/src/architecture/packs/infra.d.ts.map +1 -0
  126. package/dist/src/architecture/packs/infra.js +57 -0
  127. package/dist/src/architecture/packs/infra.js.map +1 -0
  128. package/dist/src/architecture/packs/knowledge.d.ts +72 -0
  129. package/dist/src/architecture/packs/knowledge.d.ts.map +1 -0
  130. package/dist/src/architecture/packs/knowledge.js +58 -0
  131. package/dist/src/architecture/packs/knowledge.js.map +1 -0
  132. package/dist/src/architecture/packs/process.d.ts +87 -0
  133. package/dist/src/architecture/packs/process.d.ts.map +1 -0
  134. package/dist/src/architecture/packs/process.js +52 -0
  135. package/dist/src/architecture/packs/process.js.map +1 -0
  136. package/dist/src/architecture/packs/render-classes.d.ts +11 -0
  137. package/dist/src/architecture/packs/render-classes.d.ts.map +1 -0
  138. package/dist/src/architecture/packs/render-classes.js +92 -0
  139. package/dist/src/architecture/packs/render-classes.js.map +1 -0
  140. package/dist/src/architecture/packs/types.d.ts +66 -0
  141. package/dist/src/architecture/packs/types.d.ts.map +1 -0
  142. package/dist/src/architecture/packs/types.js +18 -0
  143. package/dist/src/architecture/packs/types.js.map +1 -0
  144. package/dist/src/architecture/packs/web-product.d.ts +348 -0
  145. package/dist/src/architecture/packs/web-product.d.ts.map +1 -0
  146. package/dist/src/architecture/packs/web-product.js +226 -0
  147. package/dist/src/architecture/packs/web-product.js.map +1 -0
  148. package/dist/src/architecture/sequence-layout.d.ts +50 -0
  149. package/dist/src/architecture/sequence-layout.d.ts.map +1 -0
  150. package/dist/src/architecture/sequence-layout.js +140 -0
  151. package/dist/src/architecture/sequence-layout.js.map +1 -0
  152. package/dist/src/architecture/service-facts.d.ts +30 -0
  153. package/dist/src/architecture/service-facts.d.ts.map +1 -0
  154. package/dist/src/architecture/service-facts.js +88 -0
  155. package/dist/src/architecture/service-facts.js.map +1 -0
  156. package/dist/src/architecture/store.d.ts +50 -0
  157. package/dist/src/architecture/store.d.ts.map +1 -0
  158. package/dist/src/architecture/store.js +320 -0
  159. package/dist/src/architecture/store.js.map +1 -0
  160. package/dist/src/architecture/types.d.ts +400 -0
  161. package/dist/src/architecture/types.d.ts.map +1 -0
  162. package/dist/src/architecture/types.js +75 -0
  163. package/dist/src/architecture/types.js.map +1 -0
  164. package/dist/src/architecture/validator.d.ts +65 -0
  165. package/dist/src/architecture/validator.d.ts.map +1 -0
  166. package/dist/src/architecture/validator.js +1249 -0
  167. package/dist/src/architecture/validator.js.map +1 -0
  168. package/dist/src/core/errors.d.ts +4 -0
  169. package/dist/src/core/errors.d.ts.map +1 -1
  170. package/dist/src/core/errors.js +8 -0
  171. package/dist/src/core/errors.js.map +1 -1
  172. package/dist/src/mcp/schemas.d.ts +317 -0
  173. package/dist/src/mcp/schemas.d.ts.map +1 -1
  174. package/dist/src/mcp/schemas.js +151 -0
  175. package/dist/src/mcp/schemas.js.map +1 -1
  176. package/dist/src/mcp/server.d.ts.map +1 -1
  177. package/dist/src/mcp/server.js +7 -1
  178. package/dist/src/mcp/server.js.map +1 -1
  179. package/dist/src/mcp/tools/architecture-passthrough.d.ts +9 -0
  180. package/dist/src/mcp/tools/architecture-passthrough.d.ts.map +1 -0
  181. package/dist/src/mcp/tools/architecture-passthrough.js +587 -0
  182. package/dist/src/mcp/tools/architecture-passthrough.js.map +1 -0
  183. package/dist/src/utils/claude-projects.d.ts +19 -0
  184. package/dist/src/utils/claude-projects.d.ts.map +1 -0
  185. package/dist/src/utils/claude-projects.js +157 -0
  186. package/dist/src/utils/claude-projects.js.map +1 -0
  187. package/dist/src/utils/read-only-tools.d.ts +19 -0
  188. package/dist/src/utils/read-only-tools.d.ts.map +1 -0
  189. package/dist/src/utils/read-only-tools.js +132 -0
  190. package/dist/src/utils/read-only-tools.js.map +1 -0
  191. package/package.json +2 -1
  192. package/plugin/.codex-plugin/plugin.json +1 -1
  193. package/plugin/.mcp.json +1 -1
  194. package/plugin/mcp.json +1 -1
  195. package/plugin/skills/_shared/proactive-routing.md +1 -0
  196. package/plugin/skills/architecture/SKILL.md +913 -0
  197. package/schemas/architecture-ir.schema.json +596 -0
@@ -0,0 +1,681 @@
1
+ import { LEGACY_PACK_IDS, resolvePackIds } from './packs/index.js';
2
+ import { createHash } from 'node:crypto';
3
+ import { isAbsolute } from 'node:path';
4
+ import { normalizeRemoteUrl } from '../utils/claude-projects.js';
5
+ import { matchEndpoints } from './endpoint-match.js';
6
+ import { parseArchitectureIr } from './ir-schema.js';
7
+ import { nodeKey } from './store.js';
8
+ import { deriveLineStyle, stripPrivateExcerpts, validateArchitectureIr, } from './validator.js';
9
+ const ENDPOINT_LABEL_RE = /^([A-Za-z*]+)\s+(\S.*)$/;
10
+ /** 분석마다 찾은 action이 다를 수 있어 합집합을 정렬해 둔다. 순서가 입력 순서를 따르면 같은 결과가 안 나온다 */
11
+ function unionActions(a, b) {
12
+ return [...new Set([...(a ?? []), ...b])].sort(byText);
13
+ }
14
+ function byText(a, b) {
15
+ return a < b ? -1 : a > b ? 1 : 0;
16
+ }
17
+ // 키 순서가 달라도 같은 IR이면 같은 문자열이 나와야 입력 정렬과 근거 중복 제거가 흔들리지 않는다
18
+ function canonicalJson(value) {
19
+ if (Array.isArray(value))
20
+ return `[${value.map(canonicalJson).join(',')}]`;
21
+ if (value !== null && typeof value === 'object') {
22
+ const entries = Object.entries(value)
23
+ .filter(([, v]) => v !== undefined)
24
+ .sort(([a], [b]) => byText(a, b));
25
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(',')}}`;
26
+ }
27
+ return JSON.stringify(value);
28
+ }
29
+ function repoKey(repo) {
30
+ const remote = repo.remote?.trim();
31
+ if (remote)
32
+ return `remote:${normalizeRemoteUrl(remote)}`;
33
+ // remote가 없는 레포(클라우드 조회용 가짜 레포 등)는 이름으로만 알아본다
34
+ return `local:${repo.name.trim().toLowerCase()}`;
35
+ }
36
+ function uniqueId(base, taken) {
37
+ let id = base;
38
+ for (let n = 2; taken.has(id); n++)
39
+ id = `${base}-${n}`;
40
+ taken.add(id);
41
+ return id;
42
+ }
43
+ function hasUserEvidence(node) {
44
+ return node.evidence.some((e) => e.type === 'user');
45
+ }
46
+ function unionEvidence(into, more) {
47
+ const seen = new Set(into.map(canonicalJson));
48
+ const out = [...into];
49
+ for (const ev of more) {
50
+ const key = canonicalJson(ev);
51
+ if (seen.has(key))
52
+ continue;
53
+ seen.add(key);
54
+ out.push(ev);
55
+ }
56
+ return out;
57
+ }
58
+ const CONFLICT_QUESTION = {
59
+ displayName: (name, values) => `"${name}"의 표시 이름이 분석마다 달라요 (${values.map((v) => `"${v}"`).join(', ')}). 어느 이름으로 부르면 될까요?`,
60
+ parent: (name, values) => `"${name}"가 분석마다 다른 곳에 속해 있어요 (${values.map((v) => `"${v}"`).join(', ')}). 어디에 두면 될까요?`,
61
+ environment: (name, values) => `"${name}"의 환경이 분석마다 달라요 (${values.join(', ')}). 어느 환경이 맞나요?`,
62
+ account: (name, values) => `"${name}"가 속한 계정이 분석마다 달라요 (${values.map((v) => `"${v}"`).join(', ')}). 어느 계정이 맞나요?`,
63
+ };
64
+ /**
65
+ * 값이 하나로 모이면 그 값을, 갈리면 user 근거가 있는 쪽 값을 고른다.
66
+ * user 근거 쪽도 갈리거나 없으면 아무 값도 고르지 않고 후보 목록을 돌려준다.
67
+ */
68
+ function settle(claims) {
69
+ const distinct = [...new Set(claims.map((c) => c.value))];
70
+ if (distinct.length <= 1)
71
+ return { value: distinct[0] };
72
+ const userValues = [...new Set(claims.filter((c) => c.user).map((c) => c.value))];
73
+ if (userValues.length === 1)
74
+ return { value: userValues[0] };
75
+ return { conflict: distinct.sort(byText) };
76
+ }
77
+ function inputErrors(input, errors) {
78
+ return errors.map((e) => ({
79
+ code: e.code,
80
+ message: `입력 ${input}: ${e.message}`,
81
+ input,
82
+ ...(e.nodeId !== undefined ? { nodeId: e.nodeId } : {}),
83
+ ...(e.edgeId !== undefined ? { edgeId: e.edgeId } : {}),
84
+ }));
85
+ }
86
+ /**
87
+ * 따로 돌린 분석 IR 여럿을 하나로 합친다.
88
+ *
89
+ * 레포는 별칭(id)이 아니라 git remote로 알아본다. 노드는 nodeKey(kind, 레포, 정규화한 label)로
90
+ * 알아본다. 같은 노드면 하나로 합치고 근거는 합집합으로 남긴다. 표시 이름이나 parent가 갈리면
91
+ * 한쪽을 고르지 않고 값을 비운 채 미해결 질문으로 돌린다. user 근거가 있는 쪽 값은 그대로 둔다.
92
+ * 한 IR에서 핸들러를 못 찾은 엔드포인트는 다른 IR의 핸들러 달린 엔드포인트와 method, 경로로
93
+ * 맞춰 `handles`로 잇는다.
94
+ *
95
+ * 입력은 내용 해시 순으로 정렬한 뒤 합치므로 넘긴 순서와 상관없이 같은 결과가 나온다.
96
+ * 입력은 바꾸지 않는다. 검증과 렌더는 결과를 validate, render에 그대로 넘긴다.
97
+ */
98
+ export function mergeArchitectureIrs(irs, opts = {}) {
99
+ if (irs.length < 2) {
100
+ return {
101
+ ok: false,
102
+ errors: [{ code: 'MERGE_TOO_FEW', message: '합칠 IR이 둘 이상 있어야 한다.' }],
103
+ };
104
+ }
105
+ const views = [...new Set(irs.map((ir) => ir.view))];
106
+ if (views.length > 1) {
107
+ return {
108
+ ok: false,
109
+ errors: [
110
+ {
111
+ code: 'MERGE_VIEW_MISMATCH',
112
+ message: `뷰가 다른 IR은 합치지 않는다 (${views.sort(byText).join(', ')}).`,
113
+ },
114
+ ],
115
+ };
116
+ }
117
+ const structural = irs.flatMap((ir, i) => {
118
+ const result = validateArchitectureIr(ir, { checkFiles: false });
119
+ return result.ok ? [] : inputErrors(i, result.errors);
120
+ });
121
+ if (structural.length > 0)
122
+ return { ok: false, errors: structural };
123
+ // 보고에는 호출한 사람이 넘긴 순서의 번호를 쓴다. 합치는 순서만 해시로 정한다
124
+ const ordered = irs
125
+ .map((ir, input) => ({ ir: stripPrivateExcerpts(ir), input, hash: '' }))
126
+ .map((entry) => ({
127
+ ...entry,
128
+ hash: createHash('sha256').update(canonicalJson(entry.ir)).digest('hex'),
129
+ }))
130
+ .sort((a, b) => byText(a.hash, b.hash) || a.input - b.input);
131
+ // ── 레포 ──
132
+ const repoIdByKey = new Map();
133
+ const takenRepoIds = new Set();
134
+ const repos = [];
135
+ const repoMaps = new Map();
136
+ for (const { ir, input } of ordered) {
137
+ const map = new Map();
138
+ for (const repo of ir.repos) {
139
+ const key = repoKey(repo);
140
+ let id = repoIdByKey.get(key);
141
+ if (id === undefined) {
142
+ id = uniqueId(repo.id, takenRepoIds);
143
+ repoIdByKey.set(key, id);
144
+ repos.push({ ...repo, id });
145
+ }
146
+ else {
147
+ const existing = repos.find((r) => r.id === id);
148
+ // 상대 경로 root는 그 IR을 그린 위치 기준이라 여기서는 못 푼다. 절대 경로가 있으면 그쪽을 쓴다
149
+ if (repo.root !== undefined &&
150
+ (existing.root === undefined || (!isAbsolute(existing.root) && isAbsolute(repo.root))))
151
+ existing.root = repo.root;
152
+ if (existing.remote === undefined && repo.remote !== undefined)
153
+ existing.remote = repo.remote;
154
+ }
155
+ map.set(repo.id, id);
156
+ }
157
+ // repos에 없는 레포 이름을 쓴 노드도 받아준다. 다른 IR의 레포 id와 겹치면 이름을 바꾼다
158
+ for (const node of ir.nodes) {
159
+ if (map.has(node.repo))
160
+ continue;
161
+ const key = `local:${node.repo.trim().toLowerCase()}`;
162
+ let id = repoIdByKey.get(key);
163
+ if (id === undefined) {
164
+ id = node.repo === '' ? '' : uniqueId(node.repo, takenRepoIds);
165
+ repoIdByKey.set(key, id);
166
+ }
167
+ map.set(node.repo, id);
168
+ }
169
+ repoMaps.set(input, map);
170
+ }
171
+ const remapLocation = (ev, map) => {
172
+ if (ev.type === 'live' || ev.type === 'user')
173
+ return { ...ev };
174
+ const colon = ev.location.indexOf(':');
175
+ if (colon <= 0)
176
+ return { ...ev };
177
+ const mapped = map.get(ev.location.slice(0, colon));
178
+ if (mapped === undefined)
179
+ return { ...ev };
180
+ return { ...ev, location: `${mapped}${ev.location.slice(colon)}` };
181
+ };
182
+ const remapEvidence = (list, map) => list.map((ev) => remapLocation(ev, map));
183
+ // ── 노드 ──
184
+ const slotsByKey = new Map();
185
+ const slots = [];
186
+ const slotOf = new Map(); // `${input}\u0000${원래 id}` → slot
187
+ for (const { ir, input } of ordered) {
188
+ const map = repoMaps.get(input);
189
+ const loaded = new Set(ir.edges.filter((e) => e.kind === 'loads').map((e) => e.to));
190
+ for (const raw of ir.nodes) {
191
+ const repo = map.get(raw.repo);
192
+ const evidence = remapEvidence(raw.evidence, map);
193
+ const platformEvidence = raw.platformEvidence
194
+ ? Object.fromEntries(Object.entries(raw.platformEvidence).map(([p, list]) => [
195
+ p,
196
+ remapEvidence(list ?? [], map),
197
+ ]))
198
+ : undefined;
199
+ const key = nodeKey({ kind: raw.kind, repo, label: raw.label });
200
+ const user = hasUserEvidence(raw);
201
+ // 한 입력 안에서 키가 겹친 두 노드는 그 분석이 일부러 나눈 것이라 서로 합치지 않는다
202
+ const candidates = slotsByKey.get(key) ?? [];
203
+ let slot = candidates.find((s) => !s.inputs.has(input));
204
+ if (!slot) {
205
+ const { displayName: _d, displayNameInferred: _di, parent: _p, account: _a, environment: _e, ...rest } = raw;
206
+ slot = {
207
+ node: {
208
+ ...rest,
209
+ repo,
210
+ evidence: [],
211
+ ...(platformEvidence ? { platformEvidence: {} } : {}),
212
+ },
213
+ inputs: new Set(),
214
+ displayName: [],
215
+ displayNameInferred: new Map(),
216
+ parent: [],
217
+ account: [],
218
+ environment: [],
219
+ };
220
+ candidates.push(slot);
221
+ slotsByKey.set(key, candidates);
222
+ slots.push(slot);
223
+ }
224
+ slot.inputs.add(input);
225
+ slot.node.evidence = unionEvidence(slot.node.evidence, evidence);
226
+ // 호스트 분석이 리모트를 먼저 들여왔어도 리모트 레포 분석이 있으면 그쪽 레포를 쓴다
227
+ if (raw.kind === 'micro_app' && !loaded.has(raw.id) && slot.ownRepo === undefined) {
228
+ slot.ownRepo = repo;
229
+ slot.node.repo = repo;
230
+ }
231
+ if (slot.node.description === undefined && raw.description !== undefined) {
232
+ slot.node.description = raw.description;
233
+ }
234
+ if (raw.actions !== undefined)
235
+ slot.node.actions = unionActions(slot.node.actions, raw.actions);
236
+ if (raw.platforms !== undefined) {
237
+ const merged = new Set([...(slot.node.platforms ?? []), ...raw.platforms]);
238
+ slot.node.platforms = [...merged].sort(byText);
239
+ }
240
+ if (platformEvidence) {
241
+ const target = (slot.node.platformEvidence ??= {});
242
+ for (const [p, list] of Object.entries(platformEvidence)) {
243
+ target[p] = unionEvidence(target[p] ?? [], list);
244
+ }
245
+ }
246
+ if (raw.displayName !== undefined) {
247
+ slot.displayName.push({ value: raw.displayName, user });
248
+ // 같은 이름을 한쪽은 추정, 한쪽은 문서 그대로라고 했으면 문서 그대로 쪽을 믿는다
249
+ const prevInferred = slot.displayNameInferred.get(raw.displayName);
250
+ const inferred = raw.displayNameInferred === true;
251
+ slot.displayNameInferred.set(raw.displayName, prevInferred === undefined ? inferred : prevInferred && inferred);
252
+ }
253
+ if (raw.parent !== undefined)
254
+ slot.parent.push({ input, id: raw.parent, user });
255
+ if (raw.account !== undefined)
256
+ slot.account.push({ input, id: raw.account, user });
257
+ if (raw.environment !== undefined)
258
+ slot.environment.push({ value: raw.environment, user });
259
+ slotOf.set(`${input}\u0000${raw.id}`, slot);
260
+ }
261
+ }
262
+ const takenNodeIds = new Set();
263
+ const idOfSlot = new Map();
264
+ for (const slot of slots)
265
+ idOfSlot.set(slot, uniqueId(slot.node.id, takenNodeIds));
266
+ const mapNode = (input, id) => {
267
+ const slot = slotOf.get(`${input}\u0000${id}`);
268
+ return slot ? idOfSlot.get(slot) : id;
269
+ };
270
+ // 질문에는 id 대신 사람이 읽을 이름을 보여준다. 표시 이름이 갈린 노드는 label로 부른다
271
+ const nameById = new Map(slots.map((s) => [idOfSlot.get(s), settle(s.displayName).value ?? s.node.label]));
272
+ const conflictQuestions = [];
273
+ const nameOf = (slot) => slot.node.label;
274
+ const nodes = slots.map((slot) => {
275
+ const id = idOfSlot.get(slot);
276
+ const node = { ...slot.node, id };
277
+ const ask = (field, values) => {
278
+ conflictQuestions.push({
279
+ id: `merge:${field}:${id}`,
280
+ subject: { nodeId: id },
281
+ question: CONFLICT_QUESTION[field](nameOf(slot), values),
282
+ });
283
+ };
284
+ const dn = settle(slot.displayName);
285
+ if (dn.value !== undefined) {
286
+ node.displayName = dn.value;
287
+ if (slot.displayNameInferred.get(dn.value))
288
+ node.displayNameInferred = true;
289
+ }
290
+ else if (dn.conflict)
291
+ ask('displayName', dn.conflict);
292
+ const env = settle(slot.environment);
293
+ if (env.value !== undefined)
294
+ node.environment = env.value;
295
+ else if (env.conflict)
296
+ ask('environment', env.conflict);
297
+ for (const field of ['parent', 'account']) {
298
+ const claims = slot[field].map((c) => ({ value: mapNode(c.input, c.id), user: c.user }));
299
+ const settled = settle(claims);
300
+ if (settled.value !== undefined)
301
+ node[field] = settled.value;
302
+ // 호스트 여럿이 같이 쓰는 리모트는 서비스가 갈리는 게 맞다. 묻지 않고 어느 서비스에도 안 넣어 따로 세운다
303
+ else if (settled.conflict && field === 'parent' && node.kind === 'micro_app')
304
+ continue;
305
+ else if (settled.conflict)
306
+ ask(field, settled.conflict.map((v) => nameById.get(v) ?? v));
307
+ }
308
+ return node;
309
+ });
310
+ const edgeSlots = new Map();
311
+ const edgeSlotOf = new Map();
312
+ for (const { ir, input } of ordered) {
313
+ const map = repoMaps.get(input);
314
+ for (const raw of ir.edges) {
315
+ const from = mapNode(input, raw.from);
316
+ const to = mapNode(input, raw.to);
317
+ const key = `${from}\u0000${to}\u0000${raw.kind}`;
318
+ let slot = edgeSlots.get(key);
319
+ const evidence = remapEvidence(raw.evidence, map);
320
+ if (!slot) {
321
+ slot = { edge: { ...raw, from, to, evidence: [] }, inputs: new Set() };
322
+ edgeSlots.set(key, slot);
323
+ }
324
+ slot.inputs.add(input);
325
+ slot.edge.evidence = unionEvidence(slot.edge.evidence, evidence);
326
+ if (raw.lineStyle === 'solid')
327
+ slot.edge.lineStyle = 'solid';
328
+ if (raw.actions !== undefined)
329
+ slot.edge.actions = unionActions(slot.edge.actions, raw.actions);
330
+ edgeSlotOf.set(`${input}\u0000${raw.id}`, slot);
331
+ }
332
+ }
333
+ const takenEdgeIds = new Set();
334
+ const edgeIdOf = new Map();
335
+ for (const slot of edgeSlots.values())
336
+ edgeIdOf.set(slot, uniqueId(slot.edge.id, takenEdgeIds));
337
+ const edges = [...edgeSlots.values()].map((slot) => ({
338
+ ...slot.edge,
339
+ id: edgeIdOf.get(slot),
340
+ }));
341
+ const mapEdge = (input, id) => {
342
+ const slot = edgeSlotOf.get(`${input}\u0000${id}`);
343
+ return slot ? edgeIdOf.get(slot) : id;
344
+ };
345
+ // ── 레포를 넘는 엔드포인트 연결 ──
346
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
347
+ const inputsOf = new Map(slots.map((s) => [idOfSlot.get(s), s.inputs]));
348
+ const handlesFrom = new Map();
349
+ for (const e of edges) {
350
+ if (e.kind !== 'handles' || nodeById.get(e.to)?.kind !== 'app_module')
351
+ continue;
352
+ const list = handlesFrom.get(e.from) ?? [];
353
+ list.push(e);
354
+ handlesFrom.set(e.from, list);
355
+ }
356
+ const parseEndpoint = (node) => {
357
+ const m = ENDPOINT_LABEL_RE.exec(node.label.trim());
358
+ return m ? { method: m[1].toUpperCase(), path: m[2] } : null;
359
+ };
360
+ const endpoints = nodes.filter((n) => n.kind === 'endpoint');
361
+ const routes = endpoints.filter((n) => handlesFrom.has(n.id));
362
+ const orphanGroups = new Map();
363
+ for (const n of endpoints) {
364
+ if (handlesFrom.has(n.id))
365
+ continue;
366
+ const sig = [...inputsOf.get(n.id)].sort((a, b) => a - b).join(',');
367
+ const group = orphanGroups.get(sig) ?? [];
368
+ group.push(n);
369
+ orphanGroups.set(sig, group);
370
+ }
371
+ const crossEdges = [];
372
+ const disjoint = (a, b) => [...a].every((x) => !b.has(x));
373
+ for (const [sig, group] of [...orphanGroups.entries()].sort(([a], [b]) => byText(a, b))) {
374
+ const own = new Set(sig.split(',').map(Number));
375
+ const feCalls = group.flatMap((n) => {
376
+ const parsed = parseEndpoint(n);
377
+ return parsed ? [{ id: n.id, ...parsed }] : [];
378
+ });
379
+ // 같은 분석 안에서 이미 못 맞춘 쌍을 다시 맞추지 않는다. 다른 분석이 찾은 핸들러만 후보다
380
+ const beRoutes = routes.flatMap((n) => {
381
+ if (!disjoint(own, inputsOf.get(n.id)))
382
+ return [];
383
+ const parsed = parseEndpoint(n);
384
+ return parsed ? [{ id: n.id, repo: n.repo, ...parsed }] : [];
385
+ });
386
+ if (feCalls.length === 0 || beRoutes.length === 0)
387
+ continue;
388
+ const result = matchEndpoints({ feCalls, beRoutes, prefixCandidates: opts.prefixCandidates });
389
+ for (const match of result.matches) {
390
+ for (const handle of handlesFrom.get(match.beRouteId)) {
391
+ const fe = nodeById.get(match.feCallId);
392
+ // FE 쪽 경로 줄과 BE 쪽 라우트 줄이 둘 다 있어야 "같은 API"라는 주장이 선다
393
+ const evidence = unionEvidence(fe.evidence.filter((e) => e.type === 'code' || e.type === 'spec'), handle.evidence);
394
+ crossEdges.push({
395
+ id: uniqueId(`merge:handles:${fe.id}:${handle.to}`, takenEdgeIds),
396
+ from: fe.id,
397
+ to: handle.to,
398
+ kind: 'handles',
399
+ evidence,
400
+ lineStyle: deriveLineStyle(evidence),
401
+ });
402
+ }
403
+ }
404
+ for (const miss of result.unmatched) {
405
+ if (miss.reason === 'no_route')
406
+ continue;
407
+ const fe = nodeById.get(miss.feCallId);
408
+ const names = miss.candidates.map((id) => {
409
+ const n = nodeById.get(id);
410
+ return `"${n.label}" (${n.repo})`;
411
+ });
412
+ conflictQuestions.push({
413
+ id: `merge:endpoint:${fe.id}`,
414
+ subject: { nodeId: fe.id },
415
+ question: `"${fe.label}"를 받는 핸들러가 다른 분석에 여럿 있어요 (${names.join(', ')}). 어느 쪽이 받나요?`,
416
+ });
417
+ }
418
+ }
419
+ edges.push(...crossEdges);
420
+ // ── 도메인 흐름 ──
421
+ // 흐름은 한 분석이 자기 서비스를 보고 쓴 것이라 서로 합치지 않고 나란히 싣는다. id만 겹치지 않게 바꾼다
422
+ const flows = [];
423
+ const takenFlowIds = new Set();
424
+ const takenStepIds = new Set();
425
+ const stepIdOf = new Map(); // `${input}\u0000${원래 id}` → 새 id. 단계와 전이가 함께 쓴다
426
+ const mapStep = (input, id) => stepIdOf.get(`${input}\u0000${id}`) ?? id;
427
+ for (const { ir, input } of ordered) {
428
+ const map = repoMaps.get(input);
429
+ for (const f of ir.flows ?? []) {
430
+ for (const item of [...f.steps, ...f.transitions]) {
431
+ stepIdOf.set(`${input}\u0000${item.id}`, uniqueId(item.id, takenStepIds));
432
+ }
433
+ flows.push({
434
+ ...f,
435
+ id: uniqueId(f.id, takenFlowIds),
436
+ ...(f.service !== undefined ? { service: mapNode(input, f.service) } : {}),
437
+ steps: f.steps.map((st) => ({
438
+ ...st,
439
+ id: mapStep(input, st.id),
440
+ ...(st.refs !== undefined ? { refs: st.refs.map((r) => mapNode(input, r)) } : {}),
441
+ evidence: remapEvidence(st.evidence, map),
442
+ })),
443
+ transitions: f.transitions.map((t) => ({
444
+ ...t,
445
+ id: mapStep(input, t.id),
446
+ from: mapStep(input, t.from),
447
+ to: mapStep(input, t.to),
448
+ evidence: remapEvidence(t.evidence, map),
449
+ })),
450
+ });
451
+ }
452
+ }
453
+ // ── 질문별 투영 ──
454
+ // 투영도 한 분석이 자기 지도를 보고 쓴 것이라 나란히 싣는다. 노드와 엣지는 합친 id로 바꾸고 투영과 메시지 id만 겹치지 않게 한다
455
+ const projections = [];
456
+ const takenProjectionIds = new Set();
457
+ const takenMessageIds = new Set();
458
+ const messageIdOf = new Map();
459
+ const mapMessage = (input, id) => messageIdOf.get(`${input}\u0000${id}`) ?? id;
460
+ for (const { ir, input } of ordered) {
461
+ const map = repoMaps.get(input);
462
+ for (const pr of ir.projections ?? []) {
463
+ for (const m of pr.messages) {
464
+ messageIdOf.set(`${input}\u0000${m.id}`, uniqueId(m.id, takenMessageIds));
465
+ }
466
+ projections.push({
467
+ ...pr,
468
+ id: uniqueId(pr.id, takenProjectionIds),
469
+ ...(pr.participants !== undefined
470
+ ? { participants: pr.participants.map((id) => mapNode(input, id)) }
471
+ : {}),
472
+ ...(pr.sides !== undefined
473
+ ? {
474
+ sides: pr.sides.map((side) => ({
475
+ ...side,
476
+ nodes: side.nodes.map((id) => mapNode(input, id)),
477
+ })),
478
+ }
479
+ : {}),
480
+ messages: pr.messages.map((m) => ({
481
+ ...m,
482
+ id: mapMessage(input, m.id),
483
+ from: mapNode(input, m.from),
484
+ to: mapNode(input, m.to),
485
+ ...(m.edge !== undefined ? { edge: mapEdge(input, m.edge) } : {}),
486
+ evidence: remapEvidence(m.evidence, map),
487
+ })),
488
+ });
489
+ }
490
+ }
491
+ // ── 기술 그림 구간 ──
492
+ // 같은 id면 한 구간으로 합친다. 두 분석이 같은 이름으로 나눴으면 같은 칸이라고 보는 게 그림을 덜 쪼갠다
493
+ const stages = [];
494
+ for (const { ir, input } of ordered) {
495
+ const map = repoMaps.get(input);
496
+ for (const st of ir.stages ?? []) {
497
+ const nodesOf = st.nodes?.map((id) => mapNode(input, id));
498
+ const reposOf = st.repos?.map((r) => map.get(r) ?? r);
499
+ const existing = stages.find((x) => x.id === st.id);
500
+ if (existing === undefined) {
501
+ stages.push({
502
+ ...st,
503
+ ...(nodesOf !== undefined ? { nodes: nodesOf } : {}),
504
+ ...(reposOf !== undefined ? { repos: reposOf } : {}),
505
+ });
506
+ continue;
507
+ }
508
+ if (nodesOf !== undefined)
509
+ existing.nodes = [...new Set([...(existing.nodes ?? []), ...nodesOf])];
510
+ if (st.kinds !== undefined)
511
+ existing.kinds = [...new Set([...(existing.kinds ?? []), ...st.kinds])];
512
+ // 한쪽이 레포를 안 가렸으면 그 구간은 모든 레포를 받는다. 합친 뒤에도 그쪽 노드가 빠지지 않게 가림을 푼다
513
+ if (existing.repos === undefined || reposOf === undefined)
514
+ delete existing.repos;
515
+ else
516
+ existing.repos = [...new Set([...existing.repos, ...reposOf])];
517
+ }
518
+ }
519
+ // ── 미해결 질문 ──
520
+ const unresolved = [];
521
+ const takenQuestionIds = new Set();
522
+ const seenQuestion = new Map();
523
+ for (const { ir, input } of ordered) {
524
+ for (const q of ir.unresolved) {
525
+ const subject = {
526
+ ...(q.subject.nodeId !== undefined ? { nodeId: mapNode(input, q.subject.nodeId) } : {}),
527
+ ...(q.subject.edgeId !== undefined ? { edgeId: mapEdge(input, q.subject.edgeId) } : {}),
528
+ ...(q.subject.stepId !== undefined ? { stepId: mapStep(input, q.subject.stepId) } : {}),
529
+ ...(q.subject.transitionId !== undefined
530
+ ? { transitionId: mapStep(input, q.subject.transitionId) }
531
+ : {}),
532
+ ...(q.subject.messageId !== undefined
533
+ ? { messageId: mapMessage(input, q.subject.messageId) }
534
+ : {}),
535
+ };
536
+ const key = [
537
+ subject.nodeId,
538
+ subject.edgeId,
539
+ subject.stepId,
540
+ subject.transitionId,
541
+ subject.messageId,
542
+ q.question.trim(),
543
+ ].join('\u0000');
544
+ const twin = seenQuestion.get(key);
545
+ if (twin) {
546
+ if ((twin.answer ?? '').trim() === '' && q.answer !== undefined)
547
+ twin.answer = q.answer;
548
+ continue;
549
+ }
550
+ const merged = { ...q, id: uniqueId(q.id, takenQuestionIds), subject };
551
+ seenQuestion.set(key, merged);
552
+ unresolved.push(merged);
553
+ }
554
+ }
555
+ const conflictIds = [];
556
+ for (const q of conflictQuestions) {
557
+ const id = uniqueId(q.id, takenQuestionIds);
558
+ conflictIds.push(id);
559
+ unresolved.push({ ...q, id });
560
+ }
561
+ // ── 맥락 소스 ──
562
+ const sources = new Map();
563
+ for (const { ir } of ordered) {
564
+ for (const s of ir.sourcesUsed) {
565
+ const key = `${s.via}\u0000${s.identifier}`;
566
+ const prev = sources.get(key);
567
+ if (!prev) {
568
+ sources.set(key, { ...s });
569
+ continue;
570
+ }
571
+ // 한쪽이라도 private이면 private로 남긴다. 공개로 풀면 공유본에 이름이 샌다
572
+ if (s.visibility === 'private')
573
+ prev.visibility = 'private';
574
+ prev.readOnly = prev.readOnly && s.readOnly;
575
+ prev.probeHit = prev.probeHit || s.probeHit;
576
+ }
577
+ }
578
+ // ── 제품 그룹 ──
579
+ // 이미 합친 IR을 다시 합치면 그 안의 그룹을 그대로 물려받는다. 아니면 입력 하나가 제품 하나다
580
+ const groups = [];
581
+ const takenGroupIds = new Set();
582
+ for (const { ir, input } of ordered) {
583
+ const own = ir.groups ?? [
584
+ {
585
+ id: `group-${groups.length + 1}`,
586
+ name: opts.groupNames?.[input] ?? defaultGroupName(ir),
587
+ members: ir.nodes.map((n) => n.id),
588
+ },
589
+ ];
590
+ for (const g of own) {
591
+ const members = [...new Set(g.members.map((m) => mapNode(input, m)))].sort(byText);
592
+ if (members.length === 0)
593
+ continue;
594
+ groups.push({ id: uniqueId(g.id, takenGroupIds), name: g.name, members });
595
+ }
596
+ }
597
+ const generatedAt = ordered
598
+ .map((o) => o.ir.generatedAt)
599
+ .sort(byText)
600
+ .at(-1);
601
+ // 한쪽이라도 packs를 적었으면 양쪽 어휘를 다 담는다. 안 적은 쪽은 LEGACY_PACK_IDS로 읽는다
602
+ const packs = ordered.some((o) => o.ir.packs !== undefined)
603
+ ? resolvePackIds(ordered.flatMap((o) => o.ir.packs ?? LEGACY_PACK_IDS))
604
+ : undefined;
605
+ const draft = stripPrivateExcerpts({
606
+ schemaVersion: ordered[0].ir.schemaVersion,
607
+ view: ordered[0].ir.view,
608
+ ...(packs !== undefined ? { packs } : {}),
609
+ repos,
610
+ nodes,
611
+ edges,
612
+ unresolved,
613
+ sourcesUsed: [...sources.values()],
614
+ generatedAt,
615
+ ...(groups.length > 0 ? { groups } : {}),
616
+ ...(flows.length > 0 ? { flows } : {}),
617
+ ...(stages.length > 0 ? { stages } : {}),
618
+ ...(projections.length > 0 ? { projections } : {}),
619
+ });
620
+ // 스키마를 한 번 더 통과시키면 객체 키가 스키마 순서로 다시 놓인다. 입력의 키 순서가 바이트에 새지 않는다
621
+ const reparsed = parseArchitectureIr(draft);
622
+ if (!reparsed.ok) {
623
+ return { ok: false, errors: [{ code: 'MERGE_INTERNAL', message: reparsed.error.message }] };
624
+ }
625
+ const ir = reparsed.value;
626
+ const sharedNodes = slots
627
+ .filter((s) => s.inputs.size > 1)
628
+ .map((s) => {
629
+ const n = nodeById.get(idOfSlot.get(s));
630
+ return {
631
+ id: n.id,
632
+ kind: n.kind,
633
+ label: n.label,
634
+ repo: n.repo,
635
+ ...(n.displayName !== undefined ? { displayName: n.displayName } : {}),
636
+ inputs: [...s.inputs].sort((a, b) => a - b),
637
+ };
638
+ })
639
+ .sort((a, b) => byText(a.kind, b.kind) || byText(a.id, b.id));
640
+ return {
641
+ ok: true,
642
+ ir,
643
+ report: {
644
+ inputs: irs.length,
645
+ sharedNodes,
646
+ crossRepoEdges: crossEdges.map((e) => e.id).sort(byText),
647
+ conflictQuestions: conflictIds.sort(byText),
648
+ islands: countIslands(irs.length, slots, crossEdges, inputsOf),
649
+ },
650
+ };
651
+ }
652
+ function defaultGroupName(ir) {
653
+ const services = ir.nodes
654
+ .filter((n) => n.kind === 'service')
655
+ .sort((a, b) => byText(a.id, b.id))
656
+ .map((n) => n.displayName ?? n.label);
657
+ if (services.length === 0)
658
+ return ir.repos[0]?.name ?? '분석';
659
+ return services.length === 1 ? services[0] : `${services[0]} 외 ${services.length - 1}`;
660
+ }
661
+ function countIslands(inputs, slots, crossEdges, inputsOf) {
662
+ const parent = Array.from({ length: inputs }, (_, i) => i);
663
+ const find = (x) => {
664
+ while (parent[x] !== x)
665
+ x = parent[x] = parent[parent[x]];
666
+ return x;
667
+ };
668
+ const join = (group) => {
669
+ const [first, ...rest] = [...group];
670
+ if (first === undefined)
671
+ return;
672
+ for (const x of rest)
673
+ parent[find(x)] = find(first);
674
+ };
675
+ for (const slot of slots)
676
+ join(slot.inputs);
677
+ for (const e of crossEdges)
678
+ join([...inputsOf.get(e.from), ...inputsOf.get(e.to)]);
679
+ return new Set(parent.map((_, i) => find(i))).size;
680
+ }
681
+ //# sourceMappingURL=merge.js.map