axmap-cli 1.3.3 → 1.5.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 (70) hide show
  1. package/CLAUDE.md +15 -9
  2. package/README.ko.md +103 -87
  3. package/README.md +104 -88
  4. package/bin/axmap.mjs +9 -3
  5. package/mcp/README.md +8 -8
  6. package/mcp/install.ps1 +6 -3
  7. package/mcp/install.sh +5 -3
  8. package/mcp/server.mjs +81 -32
  9. package/package.json +7 -11
  10. package/src/agentcli.mjs +19 -0
  11. package/src/bridge.mjs +257 -0
  12. package/src/external-console.mjs +116 -0
  13. package/src/invariants.mjs +1 -1
  14. package/src/managed-session.mjs +178 -0
  15. package/src/protocol.mjs +1 -1
  16. package/src/session-dialogue.mjs +131 -0
  17. package/src/session-status.mjs +48 -0
  18. package/src/session-usage.mjs +34 -0
  19. package/src/state.mjs +8 -0
  20. package/src/terminal-bridge.mjs +225 -0
  21. package/src/update.mjs +1 -1
  22. package/tools/bridge.mjs +96 -0
  23. package/tools/mcp-register.mjs +2 -2
  24. package/tools/run-tests.mjs +10 -3
  25. package/tools/run.mjs +64 -0
  26. package/tools/setup.mjs +11 -0
  27. package/tools/windows-console.ps1 +91 -0
  28. package/app/README.md +0 -366
  29. package/app/eval/edges.mjs +0 -242
  30. package/app/lib/adjacent.mjs +0 -125
  31. package/app/lib/agentcli.mjs +0 -153
  32. package/app/lib/analyze.mjs +0 -1159
  33. package/app/lib/cochange.mjs +0 -421
  34. package/app/lib/datanodes.mjs +0 -127
  35. package/app/lib/entry.mjs +0 -192
  36. package/app/lib/featuregraph.mjs +0 -389
  37. package/app/lib/features.mjs +0 -645
  38. package/app/lib/fetchrepo-run.mjs +0 -37
  39. package/app/lib/fetchrepo.mjs +0 -164
  40. package/app/lib/flow.mjs +0 -1089
  41. package/app/lib/ladder.mjs +0 -387
  42. package/app/lib/langs.mjs +0 -630
  43. package/app/lib/live.mjs +0 -346
  44. package/app/lib/llm.mjs +0 -594
  45. package/app/lib/newfile.mjs +0 -126
  46. package/app/lib/prdiff.mjs +0 -651
  47. package/app/lib/reveal.mjs +0 -316
  48. package/app/lib/roots.mjs +0 -186
  49. package/app/lib/scope.mjs +0 -342
  50. package/app/lib/session.mjs +0 -389
  51. package/app/lib/slots.mjs +0 -233
  52. package/app/lib/ssot.mjs +0 -277
  53. package/app/lib/teamview.mjs +0 -975
  54. package/app/lib/terms.ko.mjs +0 -169
  55. package/app/server.mjs +0 -1959
  56. package/app/web/shell.css +0 -538
  57. package/app/web/shell.html +0 -197
  58. package/app/web/shell.js +0 -638
  59. package/app/web/stage.js +0 -347
  60. package/app/web/words.js +0 -85
  61. package/src/repotarget.mjs +0 -84
  62. package/tools/cluster-experiment.mjs +0 -256
  63. package/tools/cluster-sweep.mjs +0 -226
  64. package/tools/make-icon.mjs +0 -108
  65. package/tools/persona-bench.mjs +0 -362
  66. package/tools/pick-repo.mjs +0 -229
  67. package/tools/reveal-demo.mjs +0 -158
  68. package/tools/shortcut.mjs +0 -121
  69. package/tools/smoke.mjs +0 -174
  70. package/tools/topicgraph.py +0 -154
package/app/web/stage.js DELETED
@@ -1,347 +0,0 @@
1
- /**
2
- * 가운데 칸 — `ladder()` 가 내는 것을 번호 카드로 그린다.
3
- *
4
- * 🔴 새 시각화를 만드는 것이 아니다. **이미 이긴 텍스트를 다시 조판하는 것**이다.
5
- *
6
- * 벤치 3회차는 다섯 조건 전부에서 7/7 이었다 — 그림 없이 글자만 있어도 답은
7
- * 같았다. 그런데 `answers-graph.md` 가 이렇게도 적었다:
8
- *
9
- * "걸음 ②에서 노드 2 / 엣지 0, 걸음 ③에서 노드 8 / 엣지 3 — 화면의 3분의 2가
10
- * 검은 여백인 채로, 정작 그 걸음의 답은 전부 왼쪽 좁은 패널에 글자로 쏟아졌다"
11
- *
12
- * 그건 렌더링 문제가 아니라 **데이터 공급 문제**다. 예쁜 캔버스를 만들어도 거기에
13
- * 노드 2개만 흘려보내면 똑같이 빈다. 그래서 이 파일이 하는 일은 하나다 —
14
- * 왼쪽 패널이 글자로 쏟아내던 것을 가운데 큰 자리로 옮긴다.
15
- * 자세한 근거는 `docs/RESEARCH-FLOWCANVAS.md` 4.1.
16
- *
17
- * ── 규율 (Zap 편집기에서 가져온 것) ──────────────────────────────────────
18
- *
19
- * 번호가 붙는다 그림을 안 봐도 "2.5 를 보세요" 라고 말할 수 있다
20
- * 카드 폭이 같다 읽는 눈이 좌우로 흔들리지 않는다
21
- * 분기는 버스 하나 자식마다 선을 뽑으면 "각각의 관계" 로 읽힌다.
22
- * 한 줄기에서 갈라져야 "여기서 한 번 갈린다" 가 된다
23
- * 선은 조용하다 주인공은 카드의 글자다. 실측한 Zapier 연결선은
24
- * 바탕 대비 4.4:1 밖에 안 된다
25
- * 아무것도 안 움직인다 물리도 rAF 도 없다. 온보딩 실험 네 회차가 보고한
26
- * "찍고 누르는 사이에 대상이 이동" 이 구조적으로 사라진다
27
- *
28
- * 🔴 **좌표를 우리가 계산하지 않는다.** 카드는 평범한 DOM 이고 배치는 CSS 가
29
- * 한다. 선만 그릴 때 실제로 놓인 자리를 읽어서(`getBoundingClientRect`) 잇는다.
30
- * 손으로 좌표를 짜기 시작하면 창 크기가 바뀔 때마다 어긋난다.
31
- */
32
-
33
- const $ = (s) => document.querySelector(s)
34
-
35
- /**
36
- * 한 화면에 그리는 카드의 상한.
37
- *
38
- * Deutsch limit — 화면에 시각 요소를 50개 넘게 두면 사람이 못 읽는다 — 은
39
- * Zapier 에게는 제약이 아니다. 워크플로가 원래 30단계 미만이니까.
40
- * 우리는 저장소 하나가 2,000개를 넘는다. "전부 보여주고 사용자가 줄이게 한다" 는
41
- * D14 가 이미 버린 대안이므로, 넘치면 **넘쳤다고 말하고 자른다.**
42
- */
43
- const MAX_CARDS = 60
44
-
45
- /**
46
- * 화면에 나가는 말은 전부 여기서 온다.
47
- *
48
- * 🔴 문자열을 이 파일에 직접 쓰지 않는다. 어휘를 바꾸려면 그리기 코드를 읽어야
49
- * 하는 상태가 되면 `docs/WORDS.md` 의 판정 규칙("벤치에서 막힌 것만 바꾼다")을
50
- * 한 회차도 못 돌린다. 바꾸는 비용이 싸야 재서 정할 수 있다.
51
- */
52
- import { W } from './words.js'
53
-
54
- /** 부모 카드 아래에서 버스까지 내려가는 길이(px). CSS 의 층 간격과 맞춰 둔다. */
55
- const STUB = 18
56
-
57
- const el = (tag, cls, text) => {
58
- const n = document.createElement(tag)
59
- if (cls) n.className = cls
60
- if (text != null) n.textContent = text
61
- return n
62
- }
63
-
64
- const svgEl = (tag) => document.createElementNS('http://www.w3.org/2000/svg', tag)
65
-
66
- /** 마지막으로 그린 결과. 창 크기가 바뀌면 선만 다시 긋는다. */
67
- let drawn = null
68
-
69
- /**
70
- * 지금 보고 있는 하위 프로젝트. `''` 면 저장소 전체다.
71
- *
72
- * 🔴 모노레포에서 "어디서부터 읽나" 의 **첫 답은 파일이 아니라 프로젝트**다.
73
- * 실측(SSAFY S15P11E101): 저장소 전체로 겹을 만들면 549개 중 14개만 닿았다.
74
- * `FE/bbiyong-react` 하나로 좁히면 150개 중 93개가 5겹으로 이어진다.
75
- * 자세한 근거는 `app/lib/roots.mjs` 머리말.
76
- */
77
- let pick = null
78
- const REMEMBER = 'axmap.root'
79
-
80
- // ── 그리기 ──────────────────────────────────────────────────────────────────
81
-
82
- /**
83
- * 프로젝트 고르는 줄. 하나뿐이면 그리지 않는다 —
84
- * 고를 것이 없는데 고르라고 하면 걸음만 하나 는다.
85
- */
86
- function projectBar(info) {
87
- if (!info?.multi) return null
88
- const bar = el('div', 'projs')
89
- for (const r of info.roots) {
90
- const b = el('button', `proj${r.dir === pick ? ' on' : ''}`)
91
- // 저장소 루트 자체가 프로젝트일 수 있다. 빈 문자열을 그대로 그리면 이름이 없다.
92
- b.append(el('span', 'pn', r.dir || W.allRoots))
93
- b.append(el('span', 'pf', String(r.files)))
94
- // 🔴 선언된 것과 우리가 짐작한 것을 구분해 보여준다 (D5).
95
- b.title = r.manifest ? W.declaredBy(r.manifest) : W.guessedDir
96
- if (!r.manifest) b.append(el('span', 'guess', W.guess))
97
- b.onclick = () => {
98
- pick = r.dir
99
- try { localStorage.setItem(REMEMBER, pick) } catch { /* 그만 */ }
100
- drawStage()
101
- }
102
- bar.append(b)
103
- }
104
- if (info.outside) {
105
- bar.append(el('span', 'pout', W.outside(info.outside)))
106
- }
107
- return bar
108
- }
109
-
110
- export async function drawStage() {
111
- const stage = $('#stage')
112
- stage.replaceChildren()
113
- stage.className = 'loading'
114
- stage.textContent = W.loading
115
-
116
- let info = null
117
- try { info = await (await fetch('/api/roots')).json() } catch { /* 없으면 전체로 간다 */ }
118
-
119
- if (info?.multi && pick === null) {
120
- let saved = null
121
- try { saved = localStorage.getItem(REMEMBER) } catch { /* 그만 */ }
122
- // 기억해 둔 것이 아직 있으면 그것, 없으면 **가장 큰 프로젝트**로 시작한다.
123
- pick = info.roots.some((r) => r.dir === saved) ? saved : (info.roots[0]?.dir ?? '')
124
- }
125
-
126
- let d
127
- try {
128
- const q = pick ? `?root=${encodeURIComponent(pick)}` : ''
129
- const r = await fetch(`/api/ladder${q}`)
130
- d = await r.json()
131
- if (!r.ok) throw new Error(d.error ?? `HTTP ${r.status}`)
132
- } catch (e) {
133
- stage.className = 'note'
134
- stage.textContent = W.readFailed(e.message)
135
- return
136
- }
137
-
138
- /**
139
- * 🔴 조용히 빈 화면을 내지 않는다.
140
- * `ladder()` 는 시작점을 못 찾으면 `why` 에 이유를 담아 준다. 그것을 삼키면
141
- * 사용자는 "저장소가 비었나" 로 잘못 읽는다 (fail-closed, CLAUDE.md).
142
- */
143
- if (d.why) {
144
- stage.className = 'note'
145
- stage.textContent = d.why
146
- return
147
- }
148
- if (!d.layers?.length) {
149
- stage.className = 'note'
150
- stage.textContent = W.noLayers
151
- return
152
- }
153
-
154
- // 🔴 "읽는 중…" 을 지운다. `append` 는 있던 것 뒤에 붙이므로 안 지우면 남는다.
155
- stage.replaceChildren()
156
- stage.className = ''
157
- const wires = svgEl('svg')
158
- wires.setAttribute('class', 'wires')
159
- stage.append(wires)
160
-
161
- const board = el('div', 'board')
162
- const bar = projectBar(info)
163
- if (bar) board.append(bar)
164
- stage.append(board)
165
-
166
- /** path → 그 카드의 DOM. 선을 그을 때 부모를 찾는 데 쓴다. */
167
- const byPath = new Map()
168
- let cards = 0
169
- let cut = 0
170
-
171
- for (const layer of d.layers) {
172
- const row = el('div', 'layer')
173
-
174
- const head = el('div', 'lhead')
175
- head.append(el('span', 'lno', W.layerNo(layer.depth)))
176
- head.append(el('span', 'lcount', W.layerCount(layer.count)))
177
- row.append(head)
178
-
179
- const cols = el('div', 'cols')
180
- row.append(cols)
181
-
182
- for (const g of layer.groups) {
183
- const col = el('div', 'grp')
184
- // 묶음 이름 = 디렉터리. Zap 편집기가 가지 머리에 조건 칩을 두는 자리다.
185
- const chip = el('div', 'chip', g.dir || W.topDir)
186
- chip.append(el('span', 'cn', String(g.count)))
187
- col.append(chip)
188
-
189
- for (const f of g.files) {
190
- if (cards >= MAX_CARDS) { cut++; continue }
191
- const card = card_(f, layer.depth, cards)
192
- byPath.set(f.path, card)
193
- col.append(card)
194
- cards++
195
- }
196
- if (g.truncated) col.append(el('div', 'more', W.moreFiles(g.truncated)))
197
- cols.append(col)
198
- }
199
-
200
- if (layer.moreGroups) cols.append(el('div', 'more', W.moreGroups(layer.moreGroups)))
201
- board.append(row)
202
- }
203
-
204
- const foot = el('div', 'foot2')
205
- foot.append(el('span', null, W.reach(d.reached, d.total, d.maxDepth)))
206
-
207
- /**
208
- * 🔴 "1 / 213" 을 숫자만 내면 고장으로 읽힌다.
209
- *
210
- * 실측: BE_system 은 진입점을 정확히 찾았는데(`@SpringBootApplication`)
211
- * 거기서 닿는 파일이 1개다. 고장이 아니라 **Spring 이 import 가 아니라
212
- * 애노테이션으로 배선**하기 때문이다. D5 가 예고한 한계이고, 그 문서는
213
- * "확신 있게 틀린 답이 가장 해롭다" 고 적었다 — 아무 말 없이 빈 화면을 내는
214
- * 것이 정확히 그 실패다. 무엇을 못 보고 있는지 말한다.
215
- */
216
- if (d.total >= 20 && d.reached / d.total < 0.25) {
217
- foot.append(el('span', 'warn2', W.staticLimit))
218
- }
219
- for (const g of d.unreached?.by?.groups ?? []) {
220
- foot.append(el('span', null, W.groupCount(g.label, g.count)))
221
- }
222
- // 🔴 자른 것은 자랐다고 말한다. 조용히 자르면 "이게 전부" 로 읽힌다.
223
- if (cut) foot.append(el('span', 'warn2', W.cutCards(MAX_CARDS, cut)))
224
- if (d.unreached?.count) foot.append(el('span', null, W.unreached(d.unreached.count)))
225
- board.append(foot)
226
-
227
- drawn = { wires, byPath, stage }
228
- wire()
229
- }
230
-
231
- /**
232
- * 카드 두 줄.
233
- * 줄 1 — 여는 파일 수 · 테스트/예제 표시
234
- * 줄 2 — `겹.순번` + 파일 이름
235
- *
236
- * 🔴 `title` 을 반드시 붙인다. 벤치의 ladder 회차가 잘린 이름 때문에 `zoom` 을
237
- * 썼다가 **"잘린 글자는 픽셀을 키운다고 되살아나지 않는다"** 고 적고, `title`
238
- * 하나면 막힌 지점이 없었을 것이라고 스스로 진단했다. DOM 에서는 공짜다.
239
- */
240
- function card_(f, depth, seq) {
241
- const c = el('div', `card${f.aux ? ' aux' : ''}`)
242
- c.title = f.path
243
-
244
- const l1 = el('div', 'r1')
245
- l1.append(el('span', 'opens', W.opens(f.opens)))
246
- if (f.aux) l1.append(el('span', 'tag', W.aux))
247
- if (f.lines) l1.append(el('span', 'lines', W.lines(f.lines)))
248
- c.append(l1)
249
-
250
- const l2 = el('div', 'r2')
251
- // 번호가 이 카드의 정체다. 그림을 안 봐도 이 번호로 서로를 가리킬 수 있다.
252
- l2.append(el('span', 'no', `${depth}.${seq + 1}`))
253
- l2.append(el('span', 'nm', f.path.split('/').pop()))
254
- c.append(l2)
255
-
256
- const dir = f.path.split('/').slice(0, -1).join('/')
257
- if (dir) c.append(el('div', 'dir', dir))
258
-
259
- c.dataset.path = f.path
260
- c.dataset.from = f.from ?? ''
261
- return c
262
- }
263
-
264
- // ── 선 ──────────────────────────────────────────────────────────────────────
265
-
266
- /**
267
- * 부모 아래 → 가로 버스 한 줄 → 각 자식 위.
268
- *
269
- * 🔴 자식마다 부모에서 각각 선을 뽑으면 **세 개의 독립된 관계**로 보인다.
270
- * 한 줄기에서 갈라지면 "여기서 한 번 갈린다" 로 읽힌다. Zapier 가 분기를
271
- * 하나의 결정으로 읽히게 만드는 장치가 정확히 이것이고, 순수 기하라 공짜로 온다.
272
- * 모서리는 둥글지 않다 — Zap 편집기가 직각이다.
273
- */
274
- function busPath(px, py, kids) {
275
- if (!kids.length) return ''
276
- /**
277
- * 🔴 버스는 부모 바로 아래가 아니라 **부모와 첫 자식 사이**에 둔다.
278
- *
279
- * Zapier 는 부모와 자식이 붙어 있어서 어디에 두든 같다. 우리는 겹 사이가
280
- * 멀어서, 부모 바로 아래에 두면 거기서부터 자식까지 **긴 세로선이 다른 카드를
281
- * 가로지른다.** 실제로 그렇게 그려졌다. 중간에 두면 낙하가 짧아지고
282
- * "여기서 갈린다" 가 눈에 남는다.
283
- */
284
- const top = Math.min(...kids.map((k) => k.y))
285
- const bus = Math.max(py + STUB, (py + top) / 2)
286
- const left = Math.min(px, ...kids.map((k) => k.x))
287
- const right = Math.max(px, ...kids.map((k) => k.x))
288
- let d = `M${px} ${py}V${bus}`
289
- if (right - left > 0.5) d += `M${left} ${bus}H${right}`
290
- for (const k of kids) d += `M${k.x} ${bus}V${k.y}`
291
- return d
292
- }
293
-
294
- /**
295
- * 놓인 자리를 읽어서 잇는다.
296
- *
297
- * 카드 배치는 CSS 가 했으므로 우리는 결과만 본다. 좌표를 직접 계산하면
298
- * 창 크기·글꼴·줄바꿈이 바뀔 때마다 선이 어긋난다.
299
- */
300
- function wire() {
301
- if (!drawn) return
302
- const { wires, byPath, stage } = drawn
303
- const base = stage.getBoundingClientRect()
304
-
305
- wires.setAttribute('width', String(stage.scrollWidth))
306
- wires.setAttribute('height', String(stage.scrollHeight))
307
- wires.setAttribute('viewBox', `0 0 ${stage.scrollWidth} ${stage.scrollHeight}`)
308
- wires.replaceChildren()
309
-
310
- const mid = (r) => r.left - base.left + stage.scrollLeft + r.width / 2
311
-
312
- // 부모별로 자식을 모은다 — 그래야 버스를 공유할 수 있다
313
- const kidsOf = new Map()
314
- for (const card of byPath.values()) {
315
- const from = card.dataset.from
316
- if (!from || !byPath.has(from)) continue
317
- if (!kidsOf.has(from)) kidsOf.set(from, [])
318
- kidsOf.get(from).push(card)
319
- }
320
-
321
- for (const [from, kids] of kidsOf) {
322
- const p = byPath.get(from).getBoundingClientRect()
323
- const px = mid(p)
324
- const py = p.bottom - base.top + stage.scrollTop
325
- const pts = kids.map((k) => {
326
- const r = k.getBoundingClientRect()
327
- return { x: mid(r), y: r.top - base.top + stage.scrollTop }
328
- })
329
- const path = svgEl('path')
330
- path.setAttribute('class', 'w')
331
- path.setAttribute('d', busPath(px, py, pts))
332
- wires.append(path)
333
- }
334
- }
335
-
336
- /**
337
- * 창이나 칸 크기가 바뀌면 선만 다시 긋는다.
338
- *
339
- * 🔴 `requestAnimationFrame` 을 걸지 않는다. 카드는 안 움직이므로 다시 그릴 일이
340
- * 바뀔 때뿐이다. D16 이 "정지한 화면을 초당 60번 다시 칠하지 않는다" 로 정한
341
- * 방향 그대로이고, DOM 이라 저절로 그렇게 된다.
342
- */
343
- export function watchStage() {
344
- const stage = $('#stage')
345
- if (!stage || typeof ResizeObserver === 'undefined') return
346
- new ResizeObserver(() => wire()).observe(stage)
347
- }
package/app/web/words.js DELETED
@@ -1,85 +0,0 @@
1
- /**
2
- * 사용자에게 보이는 말이 사는 곳 — 화면 문자열을 한 파일에 모은다.
3
- *
4
- * 🔴 이 파일이 있는 이유는 번역이 아니라 **어휘를 실험 가능하게 만드는 것**이다.
5
- *
6
- * `docs/WORDS.md` 가 정한 판정 규칙은 "비전공자 벤치에서 그 단어 때문에 막힌
7
- * 기록이 있을 때만 바꾼다" 이다. 그러려면 **바꾸는 비용이 싸야** 한다.
8
- * 문자열이 `stage.js` 안에 흩어져 있으면 한 번 바꿀 때마다 그리기 코드를 읽어야
9
- * 하고, 그러면 어휘 실험을 한 회차도 못 돌린다. 여기 모으면 이 파일만 고친다.
10
- *
11
- * ── 🔴 지금은 글자를 한 자도 바꾸지 않았다 ───────────────────────────────
12
- *
13
- * 옮기기만 했다. 화면 출력이 **바이트 단위로 이전과 같다.** 일부러 그랬다 —
14
- * `docs/BENCH.md` 의 3회차가 이 문장들을 보고 잰 결과이고, 여기서 말을 바꾸면
15
- * 다음 회차가 **도구가 좋아져서 달라진 것인지 말이 달라져서인지 구분되지 않는다.**
16
- * 벤치가 "글자 하나까지 같은 프롬프트" 를 요구하는 것과 같은 이유다.
17
- *
18
- * 바꾸는 순서는 언제나 이것이다.
19
- *
20
- * 비전공자 벤치를 돌린다 → 막힌 지점을 읽는다 → 그 항목만 고친다 → 다시 잰다
21
- *
22
- * ── 무엇을 여기 넣나 ─────────────────────────────────────────────────────
23
- *
24
- * 넣는다 사용자가 화면에서 읽는 문장·낱말
25
- * 안 넣는다 코드의 이름(`entryCandidates` · `edges` · `cochange` · `membership`)
26
- *
27
- * 코드 어휘를 안 바꾸는 이유는 `docs/WORDS.md` 원칙 C 에 있다. 요약하면 —
28
- * 논문·문서·API 와의 대응이 끊기고, 개발이 느려지고, 얻는 것이 없다.
29
- * 사용자는 코드 이름을 안 본다.
30
- *
31
- * ⚠️ `🟡` 가 붙은 항목이 **전공자 어휘라서 벤치의 1순위 후보**인 것들이다.
32
- * 표를 따로 만들지 않는다 — 목록을 다른 곳에 적으면 그 목록이 먼저 낡는다
33
- * (CLAUDE.md §0.5). 표시는 문자열 바로 옆에 있어야 같이 움직인다.
34
- */
35
-
36
- export const W = {
37
- // ── 프로젝트 고르는 줄 ────────────────────────────────────────────────
38
- /** 🟡 "저장소" — git 을 모르는 사람에게 통하지 않는다 */
39
- allRoots: '(저장소 전체)',
40
- /** 🟡 "매니페스트" — package.json·build.gradle 을 아는 사람만 읽는다 */
41
- guessedDir: '매니페스트가 없어 디렉터리로 짐작한 것',
42
- declaredBy: (manifest) => `${manifest} 가 선언한 프로젝트`,
43
- guess: '추측',
44
- outside: (n) => `어디에도 안 속한 파일 ${n}개`,
45
-
46
- // ── 상태 ──────────────────────────────────────────────────────────────
47
- loading: '읽는 중…',
48
- readFailed: (msg) => `읽지 못했습니다 — ${msg}`,
49
- /** 🟡 "겹" · "진입점" — 둘 다 전공자 어휘다. 게다가 이 문장은 실패 화면이라
50
- * 비전공자가 제일 먼저 만나는 벽이 될 수 있다 */
51
- noLayers: '겹을 만들지 못했습니다 — 진입점을 못 찾았을 수 있습니다.',
52
-
53
- // ── 겹 머리 ───────────────────────────────────────────────────────────
54
- /** 🟡 "겹" */
55
- layerNo: (depth) => `${depth}겹`,
56
- layerCount: (n) => `${n}개`,
57
- topDir: '(최상위)',
58
- moreFiles: (n) => `+${n}개 더`,
59
- moreGroups: (n) => `+묶음 ${n}개 더`,
60
-
61
- // ── 바닥 요약 ─────────────────────────────────────────────────────────
62
- /** 🟡 "진입점" · "겹" */
63
- reach: (reached, total, maxDepth) =>
64
- `${reached} / ${total} 개가 진입점에서 닿습니다 · 최대 ${maxDepth}겹`,
65
- /**
66
- * 🟡 이 문장이 이 파일에서 **가장 어려운 말**이다 —
67
- * "정적 import" · "애노테이션" · "의존성 주입" · "배선" 이 한 문장에 넷이다.
68
- *
69
- * 그런데 지우면 안 된다. 이 경고가 없으면 "1 / 213" 이 고장으로 읽히고,
70
- * 그것이 `stage.js` 가 적어둔 대로 **확신 있게 틀린 답**이다.
71
- * 고칠 방향은 삭제가 아니라 **원칙 A** — 용어를 몰라도 문장이 성립하게 쓰고
72
- * 용어는 괄호로 뒤에 붙인다.
73
- */
74
- staticLimit:
75
- '정적 import 로 이어지는 것이 여기까지입니다 — 애노테이션·설정·의존성 주입으로 배선하는 프로젝트는 이렇게 보입니다',
76
- groupCount: (label, n) => `${label} ${n}개`,
77
- cutCards: (max, cut) => `화면에는 ${max}개까지만 그렸습니다 (${cut}개 생략)`,
78
- unreached: (n) => `안 닿는 파일 ${n}개`,
79
-
80
- // ── 카드 ──────────────────────────────────────────────────────────────
81
- /** import 를 "여는" 으로 옮긴 자리. 이미 원칙 A 를 한 번 적용한 예다 */
82
- opens: (n) => `여는 파일 ${n}`,
83
- aux: '테스트·예제',
84
- lines: (n) => `${n}줄`,
85
- }
@@ -1,84 +0,0 @@
1
- /**
2
- * 1.3.0: 이 파일은 데스크톱 선택 상태다. MCP는 세션의 roots/cwd를 사용하며
3
- * 전역 선택을 읽지 않는다. 아래는 데스크톱과 MCP가 공유하던 이전 설계 기록이다.
4
- *
5
- * 지금 사람이 보고 있는 저장소가 어디인지를 **프로세스 사이에 전달하는 한 칸.**
6
- *
7
- * ── 왜 파일 한 개인가 ────────────────────────────────────────────────────────
8
- *
9
- * 데스크톱 앱에서 [폴더 열기] 를 누르면 뷰어는 **프로세스를 갈아끼워서** 대상을
10
- * 바꾼다(`desktop/main.mjs` 의 `pickAndReopen`). MCP 서버에는 그 수법이 안 통한다 —
11
- * **MCP 서버를 띄운 것은 우리가 아니라 AI CLI** 이고, 남의 자식 프로세스는
12
- * 우리가 재시작시킬 수 없다. 그래서 MCP 쪽은 "교체" 가 아니라 "따라가기" 로 푼다:
13
- * 앱이 여기에 쓰고, MCP 서버가 도구를 부를 때마다 여기를 읽는다.
14
- *
15
- * 🔴 이것은 **사람에게 묻는 장치가 아니다.** stdio MCP 서버는 stdout 이 JSON-RPC
16
- * 전용이라 "저장소를 고르세요" 를 띄울 화면 자체가 없다(거기에 사람이 읽을 글을
17
- * 쓰면 클라이언트가 파싱에 실패해 서버가 죽는다). 사람의 입력은 앱에서 폴더를
18
- * 고르는 그 한 번뿐이고, 그 뒤는 이 파일을 다시 읽는 것으로 끝난다.
19
- *
20
- * ── 왜 홈 디렉터리인가 ──────────────────────────────────────────────────────
21
- *
22
- * 앱은 이미 `app.getPath('userData')` 에 상태를 적지만 그건 **Electron 전용 API** 라
23
- * 순수 node 로 도는 MCP 서버가 같은 경로를 계산할 방법이 없다. 두 프로세스가 **둘 다
24
- * 계산할 수 있는 자리**여야 하므로 홈 디렉터리를 쓴다.
25
- *
26
- * 저장소 안에 두지 않는 이유: 이 값은 *"지금 이 PC 의 이 사람이 무엇을 보고 있나"* 라
27
- * 커밋되면 안 된다. 팀원마다 다르고 어제와 오늘이 다르다.
28
- */
29
-
30
- import fs from 'node:fs'
31
- import os from 'node:os'
32
- import path from 'node:path'
33
-
34
- /**
35
- * 상태를 두는 폴더.
36
- *
37
- * `AXMAP_STATE_DIR` 은 **시험과 이동식 설치를 위한 것**이다. 테스트가 진짜 홈
38
- * 디렉터리를 건드리면 그 PC 를 쓰는 사람의 실제 대상이 바뀌어 버린다 — 테스트가
39
- * 사람의 작업 환경을 고치는 것은 어떤 이유로도 정당하지 않다.
40
- */
41
- export const stateDir = () =>
42
- process.env.AXMAP_STATE_DIR ? path.resolve(process.env.AXMAP_STATE_DIR) : path.join(os.homedir(), '.axmap')
43
-
44
- export const targetFile = () => path.join(stateDir(), 'current.json')
45
-
46
- /**
47
- * 사람이 마지막으로 고른 저장소. 없으면 `null`.
48
- *
49
- * 🔴 **폴더가 실제로 있는지 확인하고 낸다.** 지워지거나 옮겨진 경로를 그대로 내면
50
- * 그 뒤 모든 claim 이 존재하지 않는 저장소를 겨누고, 아무 오류 없이 조용히
51
- * 아무 일도 일어나지 않는다. 확신 있게 틀린 답이라 없는 답보다 나쁘다.
52
- *
53
- * 🔴 **던지지 않는다.** 이 함수는 도구를 부를 때마다 불린다. 여기서 예외가 나가면
54
- * 상태 파일 하나 때문에 MCP 전체가 죽는다. 못 읽으면 "고른 적 없음" 으로 답하고
55
- * 부르는 쪽의 다음 순위(git 루트)로 떨어지게 둔다.
56
- */
57
- export function readTarget() {
58
- try {
59
- const raw = fs.readFileSync(targetFile(), 'utf8')
60
- const dir = JSON.parse(raw)?.target
61
- if (typeof dir !== 'string' || !dir.trim()) return null
62
- const abs = path.resolve(dir)
63
- return fs.statSync(abs).isDirectory() ? abs : null
64
- } catch {
65
- return null
66
- }
67
- }
68
-
69
- /**
70
- * 사람이 고른 저장소를 적는다. 성공했으면 `true`.
71
- *
72
- * 실패해도 던지지 않는다 — 이걸 부르는 곳은 앱의 폴더 열기이고, 상태를 못 적었다고
73
- * 해서 **폴더 열기 자체가 실패해야 할 이유는 없다.** 뷰어는 그대로 열리고
74
- * MCP 만 따라오지 못한다. 기능이 하나 줄어드는 것과 창이 안 열리는 것은 다르다.
75
- */
76
- export function writeTarget(dir) {
77
- try {
78
- fs.mkdirSync(stateDir(), { recursive: true })
79
- fs.writeFileSync(targetFile(), JSON.stringify({ target: path.resolve(dir) }, null, 2) + '\n')
80
- return true
81
- } catch {
82
- return false
83
- }
84
- }