deel-local-cli 1.20.12 → 2.0.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 (155) hide show
  1. package/README.ko.md +96 -77
  2. package/README.md +119 -80
  3. package/bin/deel.js +398 -32
  4. package/package.json +2 -1
  5. package/src/acp/jsonrpc.js +87 -7
  6. package/src/acp/map.js +62 -13
  7. package/src/acp/serve.js +338 -30
  8. package/src/agent/agents.js +110 -12
  9. package/src/agent/askcheck.js +50 -6
  10. package/src/agent/asks.js +59 -5
  11. package/src/agent/budget.js +9 -2
  12. package/src/agent/card.js +8 -2
  13. package/src/agent/commit.js +168 -16
  14. package/src/agent/compact.js +93 -21
  15. package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
  16. package/src/agent/effort.js +85 -15
  17. package/src/agent/evidence.js +63 -7
  18. package/src/agent/evolve.js +132 -24
  19. package/src/agent/grade.js +12 -3
  20. package/src/agent/loop.js +408 -81
  21. package/src/agent/memory.js +176 -29
  22. package/src/agent/mention.js +22 -5
  23. package/src/agent/models.js +31 -7
  24. package/src/agent/modes.js +25 -10
  25. package/src/agent/outschema.js +381 -56
  26. package/src/agent/pins.js +33 -2
  27. package/src/agent/project.js +73 -8
  28. package/src/agent/recall.js +40 -9
  29. package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
  30. package/src/agent/review.js +143 -13
  31. package/src/agent/route.js +506 -32
  32. package/src/agent/salvage.js +2 -1
  33. package/src/agent/session.js +457 -31
  34. package/src/agent/store.js +232 -22
  35. package/src/agent/threads.js +52 -3
  36. package/src/backend/adapter.js +508 -85
  37. package/src/backend/azure.js +12 -1
  38. package/src/backend/cachemark.js +31 -5
  39. package/src/backend/clientcert.js +23 -1
  40. package/src/backend/ctxsize.js +56 -9
  41. package/src/backend/detect.js +252 -19
  42. package/src/backend/http.js +383 -42
  43. package/src/backend/learn.js +101 -11
  44. package/src/backend/mcp.js +333 -24
  45. package/src/backend/price.js +61 -13
  46. package/src/backend/probe.js +188 -36
  47. package/src/backend/proxy.js +108 -14
  48. package/src/backend/quota.js +226 -30
  49. package/src/backend/retry.js +46 -3
  50. package/src/backend/scan.js +47 -10
  51. package/src/backend/scanui.js +57 -7
  52. package/src/backend/toolfit.js +204 -22
  53. package/src/backend/vision.js +14 -1
  54. package/src/backend/wire.js +58 -10
  55. package/src/cmdnames.js +16 -0
  56. package/src/commands/common.js +70 -0
  57. package/src/commands/extend.js +300 -0
  58. package/src/commands/model.js +852 -0
  59. package/src/commands/view.js +328 -0
  60. package/src/commands/work.js +823 -0
  61. package/src/commands.js +193 -2082
  62. package/src/completion.js +45 -3
  63. package/src/config.js +452 -39
  64. package/src/configexplain.js +125 -14
  65. package/src/doctor.js +164 -26
  66. package/src/i18n/en.js +22 -0
  67. package/src/i18n/index.js +31 -1
  68. package/src/i18n/ja.js +25 -1
  69. package/src/i18n/ko.js +25 -2
  70. package/src/i18n/zh.js +25 -1
  71. package/src/lsp/client.js +111 -9
  72. package/src/lsp/servers.js +86 -22
  73. package/src/oneshot.js +325 -65
  74. package/src/pack/sbom.js +28 -5
  75. package/src/pack/selfpack.js +54 -5
  76. package/src/pack/sheet.en.js +24 -3
  77. package/src/pack/tar.js +45 -8
  78. package/src/pack/zip.js +103 -8
  79. package/src/plugins/manage.js +165 -14
  80. package/src/preview/serve.js +239 -23
  81. package/src/providers/bedrock.js +30 -5
  82. package/src/providers/gemini.js +8 -0
  83. package/src/providers/index.js +17 -1
  84. package/src/repl.js +254 -61
  85. package/src/report.js +7 -2
  86. package/src/reset.js +151 -25
  87. package/src/safety/audit.js +108 -8
  88. package/src/safety/authcmd.js +75 -5
  89. package/src/safety/guard.js +507 -15
  90. package/src/safety/hooks.js +172 -20
  91. package/src/safety/keystore.js +178 -18
  92. package/src/safety/network.js +209 -6
  93. package/src/safety/policy.js +377 -36
  94. package/src/safety/runmode.js +9 -2
  95. package/src/safety/secrets.js +243 -17
  96. package/src/safety/shellenv.js +64 -3
  97. package/src/safety/trust.js +298 -32
  98. package/src/safety/undo.js +409 -17
  99. package/src/setup.js +175 -17
  100. package/src/skills/discover.js +122 -19
  101. package/src/stats.js +57 -16
  102. package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
  103. package/src/tools/clipboard.js +53 -20
  104. package/src/tools/convert.js +118 -37
  105. package/src/tools/desc.en.js +48 -5
  106. package/src/tools/doc2md.js +11 -1
  107. package/src/tools/docs.js +94 -18
  108. package/src/tools/edit-match.js +212 -13
  109. package/src/tools/encoding.js +525 -28
  110. package/src/tools/excel-com.js +63 -15
  111. package/src/tools/excel.js +12 -7
  112. package/src/tools/fastgrep.js +444 -36
  113. package/src/tools/fig.js +40 -9
  114. package/src/tools/fsutil.js +235 -21
  115. package/src/tools/hwpxwrite.js +80 -19
  116. package/src/tools/ignore.js +55 -7
  117. package/src/tools/index.js +1029 -145
  118. package/src/tools/jobs.js +325 -50
  119. package/src/tools/kiwi.js +50 -5
  120. package/src/tools/lsp.js +164 -32
  121. package/src/tools/outline.js +52 -12
  122. package/src/tools/pdf.js +366 -56
  123. package/src/tools/shell.js +12 -4
  124. package/src/tools/spawn.js +203 -18
  125. package/src/tools/todo.js +73 -4
  126. package/src/tools/verify.js +312 -43
  127. package/src/tools/webfetch.js +184 -25
  128. package/src/tools/xlsx.js +232 -31
  129. package/src/ui/ansi.js +209 -15
  130. package/src/ui/banner.js +2 -1
  131. package/src/ui/complete.js +46 -4
  132. package/src/ui/diff.js +4 -2
  133. package/src/ui/export.js +91 -17
  134. package/src/ui/inputbox.js +58 -18
  135. package/src/ui/intro.js +9 -2
  136. package/src/ui/level.js +32 -7
  137. package/src/ui/md.js +101 -15
  138. package/src/ui/motion.js +5 -2
  139. package/src/ui/notify.js +7 -1
  140. package/src/ui/office.js +37 -5
  141. package/src/ui/pastechip.js +3 -2
  142. package/src/ui/pick.js +85 -12
  143. package/src/ui/prompt.js +176 -13
  144. package/src/ui/screen.js +2 -1
  145. package/src/ui/spinner.js +6 -1
  146. package/src/ui/status.js +80 -12
  147. package/src/ui/wrap.js +23 -37
  148. /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
  149. /package/src/skills/builtin/{/352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md" → build-deep/SKILL.md} +0 -0
  150. /package/src/skills/builtin/{/354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md" → debug-step-by-step/SKILL.md} +0 -0
  151. /package/src/skills/builtin/{/353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md" → finish-all/SKILL.md} +0 -0
  152. /package/src/skills/builtin/{/354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md" → self-review/SKILL.md} +0 -0
  153. /package/src/skills/builtin/{/354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md" → simplify-code/SKILL.md} +0 -0
  154. /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
  155. /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
package/src/tools/kiwi.js CHANGED
@@ -35,6 +35,21 @@
35
35
  * 40억이면 그대로 40억 칸을 잡으려 든다. 그래서 개수를 볼 때마다 남은
36
36
  * 바이트와 견준다 — 어떤 값이든 최소 한 바이트는 먹으므로, 남은 바이트보다
37
37
  * 많은 개수는 그 자리에서 거짓이다.
38
+ *
39
+ * ── 깨진 바이트를 만나면 (8회차 · 바깥) ────────────────────────────────
40
+ *
41
+ * 규약은 하나다 — **어디까지 읽었는지 말하고 멈춘다.**
42
+ *
43
+ * 한 파일 안에 이것과 반대인 두 버릇이 같이 있었다. 스키마 갈래 바이트가
44
+ * 0·1·2 가 아니면 조용히 ENUM 으로 **바꿔서** 계속 읽었고(그 뒤는 전부 다른
45
+ * 값이 되는데 아무 말도 안 나온다), 알맹이에서 모르는 칸 번호를 만나면
46
+ * **이미 읽은 것까지** 말없이 버리고 던졌다. 지어내는 쪽과 버리는 쪽이
47
+ * 한 파일에 같이 있으면 읽는 쪽은 무엇을 받았는지 알 길이 없다.
48
+ *
49
+ * 그래서 아래 여기까지() 하나로 묶는다. 멈추는 것은 같고, 오류에 **바이트
50
+ * 자리**와 **여태 읽은 것**을 달아 보낸다. 그걸 쓸지는 부르는 쪽이 정한다 —
51
+ * fig.js 는 안 쓴다(「반만 맞는 시안은 틀린 시안보다 나쁘다」). 버리는 것과
52
+ * 버릴지를 고르게 하는 것은 다르다.
38
53
  */
39
54
 
40
55
  const 기본형 = ['bool', 'byte', 'int', 'uint', 'float', 'string', 'int64', 'uint64'];
@@ -49,6 +64,17 @@ const 깊이최대 = 100;
49
64
  /** 실수 네 바이트를 뒤집어 볼 때 쓰는 창. 새로 만들지 않고 하나를 돌려 쓴다. */
50
65
  const 실수판 = new DataView(new ArrayBuffer(4));
51
66
 
67
+ /**
68
+ * 깨진 바이트를 만났다 — **어디까지 읽었는지 말하고 멈춘다.**
69
+ *
70
+ * @param {string} 말 무엇이 깨졌나
71
+ * @param {number} 자리 그때까지 읽은 바이트 수
72
+ * @param {*} [읽은것] 여태 읽어 놓은 것. 말없이 버리지 않는다.
73
+ */
74
+ function 여기까지(말, 자리, 읽은것 = null) {
75
+ return Object.assign(new Error(`${말} — 바이트 ${자리} 까지 읽었습니다`), { 자리, 읽은것 });
76
+ }
77
+
52
78
  /**
53
79
  * 바이트를 앞에서부터 읽는 자.
54
80
  *
@@ -159,14 +185,27 @@ export class 읽개 {
159
185
  */
160
186
  export function 스키마읽기(buf) {
161
187
  const r = new 읽개(buf);
162
- const 개수 = r.varuint();
188
+ // 개수는 스키마 쪽에서도 **남은 바이트와 대조하고** 들어간다. 여기만 날 varuint 라,
189
+ // 「정의 40억 개」 라고 적힌 조각이 그 대조 없이 루프에 들어가 엉뚱한 말로 터졌다
190
+ // (`문자열이 끝나지 않았습니다`). 정의 하나도 칸 하나도 최소 한 바이트는 먹는다.
191
+ const 개수 = r.개수();
163
192
  if (개수 > 정의최대) throw new Error(`정의가 ${개수}개 — Kiwi 스키마가 아닌 것 같습니다`);
164
193
 
165
194
  const 정의들 = [];
166
195
  for (let i = 0; i < 개수; i += 1) {
167
196
  const 이름 = r.문자열();
168
- const 갈래 = 갈래이름[r.바이트()] ?? 'ENUM';
169
- const 칸수 = r.varuint();
197
+ /*
198
+ * 모르는 갈래 바이트를 ENUM 으로 **바꾸지 않는다.**
199
+ *
200
+ * 갈래는 0=ENUM · 1=STRUCT · 2=MESSAGE 셋뿐이고, 셋이 서로 다르게 읽힌다.
201
+ * 모르는 값을 ENUM 으로 치면 그 정의부터 뒤가 전부 다른 값으로 읽히는데
202
+ * 오류는 안 난다 — 갈래 바이트 7 하나가 `{"갈래":"ENUM","칸들":[]}` 로
203
+ * 조용히 둔갑했다. 지어내느니 여기까지 읽었다고 말하고 멈춘다.
204
+ */
205
+ const 갈래바이트 = r.바이트();
206
+ const 갈래 = 갈래이름[갈래바이트];
207
+ if (!갈래) throw 여기까지(`${이름} 의 정의 갈래가 ${갈래바이트} — 0·1·2 만 있습니다`, r.i, 정의들);
208
+ const 칸수 = r.개수();
170
209
  if (칸수 > 칸최대) throw new Error(`${이름} 의 칸이 ${칸수}개 — 스키마가 깨졌습니다`);
171
210
  const 칸들 = [];
172
211
  for (let j = 0; j < 칸수; j += 1) {
@@ -256,8 +295,10 @@ function 정의읽기(스키마, r, 번호, 깊이, 담나, 남길까) {
256
295
  * 여기서는 수천만 번이다. 값을 안 쓸 때만 쓰는 길이라, 담는 쪽 결과는
257
296
  * 한 글자도 안 달라진다.
258
297
  */
298
+ // 개수 대조는 바로 위 `r.개수()` 가 이미 했다. 여기 한 번 더 있던 같은 대조는
299
+ // **어떤 입력에도 안 걸리는** 죽은 단언이었다 — 지킨다고 적혀 있을 뿐 아무것도
300
+ // 안 지키는 줄은, 다음 사람이 그 줄을 믿고 위쪽 대조를 빼게 만든다.
259
301
  if (!이칸담나 && f.형 === -2) {
260
- if (n > r.남은) throw new Error(`바이트 배열이 남은 것보다 깁니다 (${n} > ${r.남은})`);
261
302
  r.i += n;
262
303
  return;
263
304
  }
@@ -281,12 +322,16 @@ function 정의읽기(스키마, r, 번호, 깊이, 담나, 남길까) {
281
322
  // MESSAGE — 번호 0 이 끝이다. 스키마에 없는 번호를 만나면 **거기서 멈춘다.**
282
323
  // 길이가 안 적혀 있어서 건너뛸 수가 없다. 짐작으로 계속 읽으면 그 뒤가
283
324
  // 전부 헛것이 되므로, 아는 데까지만 읽었다고 말하는 편이 낫다.
325
+ //
326
+ // 그 「말한다」 가 없었다. 그냥 던져서 **이미 읽은 것까지** 같이 사라졌고,
327
+ // 부르는 쪽은 한 글자도 못 읽은 것과 구별할 수 없었다. 오류에 바이트 자리와
328
+ // 여기까지 읽은 것을 달아 보낸다 — 쓸지는 부르는 쪽이 정한다.
284
329
  const 칸표 = new Map(d.칸들.map((f) => [f.값, f]));
285
330
  for (;;) {
286
331
  const id = r.varuint();
287
332
  if (id === 0) break;
288
333
  const f = 칸표.get(id);
289
- if (!f) throw new Error(`${d.이름} 에 모르는 칸 번호 ${id} — 여기서 더 못 읽습니다`);
334
+ if (!f) throw 여기까지(`${d.이름} 에 모르는 칸 번호 ${id} — 여기서 더 못 읽습니다`, r.i, 것);
290
335
  한칸(f);
291
336
  }
292
337
  return 것;
package/src/tools/lsp.js CHANGED
@@ -29,18 +29,35 @@ import { 갈래, 프로젝트갈래 } from '../lsp/servers.js';
29
29
  import { 찾을개수 } from '../agent/budget.js';
30
30
  import { 말, 세말 } from '../i18n/index.js';
31
31
 
32
- /** 한 자리를 사람이 읽을 한 줄로. 그 줄의 글까지 붙여야 열어 보지 않고도 안다. */
33
- function 한줄(scope, uri, 범위) {
32
+ /**
33
+ * 한 자리를 사람이 읽을 한 줄로. 그 줄의 글까지 붙여야 열어 보지 않고도 안다.
34
+ *
35
+ * ── 파일은 파일마다 **한 번** (사냥4 W3) ─────────────────────────────────
36
+ *
37
+ * 여기가 **자리 하나마다** 그 파일을 통째로 다시 읽었다. 4만 줄짜리 파일에 참조가
38
+ * 5000곳이면 5000번을 읽는다. 게다가 `trim()` 으로 뗀 줄은 V8 에서 원래 글의 **조각**
39
+ * 이라, 그 한 줄이 파일 전체를 붙들고 놓지 않았다 — 5000줄이 5000벌을 붙들어 힙이 넘쳤다.
40
+ *
41
+ * 그래서 한 번 부를 동안 같은 파일은 줄표 에서 꺼내 쓰고, 뗀 글은 **새 글로 옮겨 적는다**
42
+ * (Buffer 를 거치면 조각이 아니라 제 몸을 가진 글이 된다). 부르는 쪽(Refs)은 창에 안 실을
43
+ * 자리는 아예 여기로 안 보낸다.
44
+ *
45
+ * @param 줄표 한 번 부를 동안 같이 쓰는 표 (파일 → 줄들 | null). 안 주면 그 자리에서 읽는다.
46
+ */
47
+ function 한줄(scope, uri, 범위, 줄표 = new Map()) {
34
48
  let abs;
35
49
  try { abs = fileURLToPath(uri); } catch { abs = String(uri); }
36
50
  const 줄번호 = (범위?.start?.line ?? 0) + 1;
37
- let 글 = '';
38
- try {
39
- const 줄들 = readFileSync(abs, 'utf8').split(/\r?\n/);
40
- 글 = (줄들[줄번호 - 1] ?? '').trim();
41
- } catch { /* 못 읽으면 자리만 준다 */ }
51
+ // 윈도우는 서버가 드라이브 글자를 소문자로 적어 온다(c%3A) — 같은 파일을 두 번 읽지 않게.
52
+ const 열쇠 = process.platform === 'win32' ? abs.toLowerCase() : abs;
53
+ if (!줄표.has(열쇠)) {
54
+ try { 줄표.set(열쇠, readFileSync(abs, 'utf8').split(/\r?\n/)); } catch { 줄표.set(열쇠, null); /* 못 읽으면 자리만 준다 */ }
55
+ }
56
+ let 글 = (줄표.get(열쇠)?.[줄번호 - 1] ?? '').trim();
57
+ if (글.length > 160) 글 = 글.slice(0, 160) + '…';
58
+ 글 = Buffer.from(글, 'utf8').toString('utf8');
42
59
  const 보일 = scope?.show ? (() => { try { return scope.show(abs); } catch { return abs; } })() : abs;
43
- return { 파일: 보일, 줄: 줄번호, 글: 글.length > 160 ? 글.slice(0, 160) + '…' : 글, abs };
60
+ return { 파일: 보일, 줄: 줄번호, 글, abs };
44
61
  }
45
62
 
46
63
  /** LSP 의 답은 하나일 수도, 목록일 수도, LocationLink 일 수도 있다. 다 같은 모양으로 편다. */
@@ -121,9 +138,16 @@ async function 자리잡기(서버, scope, { 이름, 파일, 줄 }) {
121
138
  }
122
139
  if (답.오류) return { 오류: 답.오류 };
123
140
  const 것들 = (Array.isArray(답.값) ? 답.값 : []).filter((s) => s?.name);
124
- // 이름이 똑같은 것만. 서버는 대개 부분 일치까지 준다.
141
+ /*
142
+ * 이름이 똑같은 것만. 서버는 대개 부분 일치까지 준다.
143
+ *
144
+ * 똑같은 것이 없을 때 **아무 부분 일치나** 받았다 — `셈` 을 물으면 `셈하기` 를 짚고 답은
145
+ * 「셈 — 정의 1곳」 이었다 (2.0.0 6회차 LS1). 남의 정의를 내 것처럼 준 것이다. 되받는 것은
146
+ * 이름이 **낱말로 들어 있는** 것뿐이다 — 서버에 따라 `Cls.foo` · `foo(int)` 처럼 이름을 꾸며
147
+ * 주기 때문이다. 되받았으면 무엇을 짚었는지 같이 돌려준다 (닮은이름).
148
+ */
125
149
  const 딱맞는 = 것들.filter((s) => s.name === 이름);
126
- const 쓸것 = 딱맞는.length ? 딱맞는 : 것들;
150
+ const 쓸것 = 딱맞는.length ? 딱맞는 : 것들.filter((s) => 칸찾기(String(s.name), 이름) >= 0);
127
151
  if (!쓸것.length) {
128
152
  return {
129
153
  오류: `${이름} 을(를) 못 찾았습니다`
@@ -163,7 +187,25 @@ async function 자리잡기(서버, scope, { 이름, 파일, 줄 }) {
163
187
  서버.보여주기(abs);
164
188
  } catch { /* 그대로 간다 */ }
165
189
 
166
- return { 자리: { uri: 첫.uri, position }, 후보 };
190
+ return { 자리: { uri: 첫.uri, position }, 후보, 닮은이름: 딱맞는.length ? null : 첫.이름 };
191
+ }
192
+
193
+ /**
194
+ * 모델이 준 줄 번호. **따옴표가 붙어 와도 숫자로 본다.**
195
+ *
196
+ * 스키마에 number 라고 적어 두어도 `"line": "42"` 로 보내는 모델이 있다.
197
+ * `Number.isFinite('42')` 는 false 라 그 줄이 통째로 버려졌고, 자리잡기 는
198
+ * 줄을 안 준 것으로 보고 **파일 처음부터** 이름을 찾았다 — 문자열 42 는 4번
199
+ * 줄을, 숫자 42 는 41번 줄을 짚었다. 엉뚱한 줄을 짚어 놓고 찾았다고 답하므로
200
+ * 잘못됐다는 신호가 어디에도 안 남는다.
201
+ *
202
+ * 빈 값·빈 글자는 「안 준 것」 이다. Number('') 가 0 이라 그냥 넘기면 0번 줄을
203
+ * 짚으려 든다.
204
+ */
205
+ function 줄값(값) {
206
+ if (값 === null || 값 === undefined) return null;
207
+ const n = typeof 값 === 'string' ? (값.trim() === '' ? NaN : Number(값)) : 값;
208
+ return typeof n === 'number' && Number.isFinite(n) ? n : null;
167
209
  }
168
210
 
169
211
  /** 두 도구가 같은 앞머리를 쓴다 — 서버 얻고 자리 잡는 데까지. */
@@ -187,19 +229,44 @@ async function 채비(args, ctx) {
187
229
  if (!서버) return { 오류: '언어 서버가 이 자리에 없습니다. Grep · Outline 을 쓰세요.' };
188
230
 
189
231
  const 잡음 = await 자리잡기(서버, ctx.scope, {
190
- 이름, 파일, 줄: Number.isFinite(args.line) ? Number(args.line) : null,
232
+ 이름, 파일, 줄: 줄값(args.line),
191
233
  });
192
234
  if (잡음.오류) return { 오류: 잡음.오류 };
193
- return { 이름, 서버, 자리: 잡음.자리, 후보: 잡음.후보 ?? [] };
235
+ return { 이름, 서버, 자리: 잡음.자리, 후보: 잡음.후보 ?? [], 닮은이름: 잡음.닮은이름 ?? null };
194
236
  }
195
237
 
196
- /** 여러 곳에 같은 이름이 있으면 그대로 알려 준다. 하나를 골라 주고 아닌 척하지 않는다. */
238
+ /**
239
+ * 여러 곳에 같은 이름이 있으면 그대로 알려 준다. 하나를 골라 주고 아닌 척하지 않는다.
240
+ *
241
+ * 가르는 열쇠는 **파일과 컨테이너**다. 파일만 봤더니 한 파일 안의 서로 다른 둘(`server.go` 의
242
+ * `Server.Run` · `Worker.Run`)을 하나로 쳐서 말이 없었다 (2.0.0 6회차 LS2). 보기가 두 **파일**
243
+ * (`A.go` · `B.go`)로 적혀 있었는데, 그건 파일만 봐도 갈리는 판이라 설명하는 고장과 어긋났다.
244
+ * 그렇다고 줄마다 세면, 겹쳐쓰기(overload)처럼 같은
245
+ * 것이 여러 줄로 오는 흔한 자리에서 거짓 경고가 난다 — 컨테이너까지 같으면 같은 것으로 친다.
246
+ */
197
247
  function 여럿이면(후보, scope) {
198
248
  if (!Array.isArray(후보) || 후보.length < 2) return null;
199
- const 파일들 = new Set(후보.map((c) => { try { return scope.show(fileURLToPath(c.uri)); } catch { return c.uri; } }));
200
- if (파일들.size < 2) return null;
201
- return `같은 이름이 ${파일들.size}곳에 있습니다: ${[...파일들].slice(0, 5).join(' · ')}`
202
- + `${파일들.size > 5 ? ' …' : ''}. 다른 것을 뜻했다면 file_path 로 짚어 주세요.`;
249
+ const 곳들 = new Map(); // [파일, 컨테이너] → 보일 이름
250
+ const 파일들 = new Set();
251
+ for (const c of 후보) {
252
+ let 파일;
253
+ try { 파일 = scope.show(fileURLToPath(c.uri)); } catch { 파일 = c.uri; }
254
+ 파일들.add(파일);
255
+ const 컨테이너 = String(c.컨테이너 ?? '');
256
+ 곳들.set(JSON.stringify([파일, 컨테이너]), 컨테이너 ? `${파일} (${컨테이너})` : 파일);
257
+ }
258
+ if (곳들.size < 2) return null;
259
+ const 보일것 = [...곳들.values()];
260
+ // 한 파일 안에서 갈리면 file_path 로는 못 좁힌다 — 줄까지 짚어야 한다.
261
+ const 짚는법 = 파일들.size < 2 ? 'file_path 와 line 으로' : 'file_path 로';
262
+ return `같은 이름이 ${보일것.length}곳에 있습니다: ${보일것.slice(0, 5).join(' · ')}`
263
+ + `${보일것.length > 5 ? ' …' : ''}. 다른 것을 뜻했다면 ${짚는법} 짚어 주세요.`;
264
+ }
265
+
266
+ /** 이름이 똑같은 것이 없어 낱말로 든 다른 이름(`Cls.foo` 같은)을 짚었으면 그 이름을 말한다 (자리잡기 의 닮은이름, LS1). */
267
+ function 닮은말(이름, 닮은이름) {
268
+ if (!닮은이름 || 닮은이름 === 이름) return null;
269
+ return `「${이름}」 — 이름이 똑같은 것은 없어 「${닮은이름}」 자리를 짚었습니다. 다른 것을 뜻했다면 Grep 으로 보세요.`;
203
270
  }
204
271
 
205
272
  export const DEF_TOOL = {
@@ -233,23 +300,50 @@ export const DEF_TOOL = {
233
300
  });
234
301
  if (답.오류) return { error: `언어 서버: ${답.오류}` };
235
302
 
236
- let 곳들 = 자리들펴기(답.값).map((x) => 한줄(ctx.scope, x.uri, x.range));
303
+ const 줄표 = new Map(); // 같은 파일은 한 번만 읽는다 (한줄 머리말)
304
+ let 곳들 = 자리들펴기(답.값).map((x) => 한줄(ctx.scope, x.uri, x.range, 줄표));
237
305
  // 서버가 정의를 못 주면(선언만 있는 자리 등) 심볼 검색으로 잡은 자리를 준다.
238
306
  // 빈손으로 돌려보내는 것보다 낫고, 어디서 온 값인지 같이 말해 준다.
239
307
  let 어디서 = 'definition';
240
308
  if (!곳들.length && 후보.length) {
241
- 곳들 = 후보.map((c) => 한줄(ctx.scope, c.uri, c.range));
309
+ 곳들 = 후보.map((c) => 한줄(ctx.scope, c.uri, c.range, 줄표));
242
310
  어디서 = 'workspace/symbol';
243
311
  }
244
312
  if (!곳들.length) return { summary: `${이름}: ${말('lsp.noDef')}`, found: 0 };
245
313
 
314
+ /*
315
+ * ── 「골라 준 것이 아니다」 는 말은 **모델에게** 가야 한다 ────────────
316
+ *
317
+ * 이 말을 summary 에만 붙여 놨었다. 그런데 모델이 받는 글은 content 다 —
318
+ * loop.js 의 실을글() 은 content 가 비지 않으면 그것만 싣고, 비었을 때만
319
+ * summary 로 내려간다. 즉 **줄 자리가 있을 때는 이 말이 절대 안 갔다.**
320
+ *
321
+ * 사람 화면에는 「같은 이름이 2곳에 있습니다」 가 멀쩡히 찍히니 아무도
322
+ * 눈치를 못 챈다. 정작 모델은 자리 하나만 받고 그게 유일한 정의인 줄
323
+ * 알고, 남의 파일에 있는 같은 이름을 고쳐 놓고 답을 맺는다. 이 도구가
324
+ * 하나를 골라 주고 아닌 척하지 않겠다고 한 약속이 그 자리에서 깨진다.
325
+ *
326
+ * 두 곳에 다 적는다. 사람이 본 말과 모델이 받은 말이 같아야 한다.
327
+ */
246
328
  const 여럿 = 여럿이면(후보, ctx.scope);
329
+ /*
330
+ * 이름이 똑같은 것이 없어 꾸민 이름을 짚었거나(LS1), 정의를 못 받아 심볼 검색 자리를
331
+ * 줬으면(LS3) 그렇다고 **모델이 받는 글에도** 적는다 — 바로 위와 같은 까닭이다.
332
+ * 출처를 `source` 칸에만 두었더니 사람에게도 모델에게도 안 갔다 (2.0.0 6회차).
333
+ */
334
+ const 덧말 = [
335
+ 닮은말(이름, 준비.닮은이름),
336
+ 어디서 === 'workspace/symbol'
337
+ ? '언어 서버가 정의를 못 줘서 심볼 검색으로 찾은 자리입니다 — 정의가 아니라 선언일 수 있습니다.' : null,
338
+ 여럿,
339
+ ].filter(Boolean);
340
+ const 글 = 곳들.map((l) => `${l.파일}:${l.줄} ${l.글}`).join('\n');
247
341
  return {
248
- summary: `${이름} — ${말('lsp.defs', { n: 세말('places', 곳들.length) })}${여럿 ? `\n${여럿}` : ''}`,
342
+ summary: `${이름} — ${말('lsp.defs', { n: 세말('places', 곳들.length) })}${덧말.length ? `\n${덧말.join('\n')}` : ''}`,
249
343
  found: 곳들.length,
250
344
  source: 어디서,
251
345
  locations: 곳들,
252
- content: 곳들.map((l) => `${l.파일}:${l.줄} ${l.글}`).join('\n'),
346
+ content: 덧말.length ? `${글}\n\n${덧말.join('\n')}` : 글,
253
347
  };
254
348
  },
255
349
  };
@@ -286,12 +380,22 @@ export const REFS_TOOL = {
286
380
  });
287
381
  if (답.오류) return { error: `언어 서버: ${답.오류}` };
288
382
 
289
- const 곳들 = 자리들펴기(답.값).map((x) => 한줄(ctx.scope, x.uri, x.range));
383
+ // 여기서는 자리만 편다. 줄 글은 창에 실을 것만 아래에서 읽는다 (한줄 머리말, 사냥4 W3).
384
+ const 곳들 = 자리들펴기(답.값);
385
+ const 여럿 = 여럿이면(후보, ctx.scope);
386
+ const 닮은 = 닮은말(이름, 준비.닮은이름);
290
387
  if (!곳들.length) {
388
+ /*
389
+ * 「안 쓴다」 는 **짚은 하나**에 대한 답이다. 같은 이름이 다른 곳에도 있으면 그 말을 같이
390
+ * 해야 한다 — 빼면 모델은 지워도 되는 줄 알고 멀쩡히 쓰이는 쪽을 지운다 (2.0.0 6회차 LS5).
391
+ */
291
392
  return {
292
- summary: `${이름}: ${말('lsp.noRefs')}`
293
- + ' 정말 안 쓰는 것일 수도 있고, 언어 서버가 아직 색인 중일 수도 있습니다 —'
294
- + ' 지우기 전에 Grep 으로 한 번 더 보세요.',
393
+ summary: [
394
+ `${이름}: ${말('lsp.noRefs')}`
395
+ + ' 정말 안 쓰는 것일 수도 있고, 언어 서버가 아직 색인 중일 수도 있습니다 —'
396
+ + ' 지우기 전에 Grep 으로 한 번 더 보세요.',
397
+ 닮은, 여럿,
398
+ ].filter(Boolean).join('\n'),
295
399
  found: 0,
296
400
  };
297
401
  }
@@ -299,7 +403,9 @@ export const REFS_TOOL = {
299
403
  // 창에 맞춰 자른다. 자른 것은 자랐다고 말한다 — 조용히 자르면 모델은
300
404
  // 그게 전부인 줄 알고 나머지 자리를 안 고친다.
301
405
  const 한도 = 찾을개수(ctx.모델컨텍스트 ?? null);
302
- const 보일것 = 곳들.slice(0, 한도);
406
+ // 자르고 **나서** 읽는다. 창에 안 실을 70곳의 파일까지 열 까닭이 없다.
407
+ const 줄표 = new Map();
408
+ const 보일것 = 곳들.slice(0, 한도).map((x) => 한줄(ctx.scope, x.uri, x.range, 줄표));
303
409
  const 남은 = 곳들.length - 보일것.length;
304
410
 
305
411
  // 파일별로 묶어야 읽힌다. 같은 파일 열 줄이 흩어져 있으면 몇 파일을
@@ -313,16 +419,42 @@ export const REFS_TOOL = {
313
419
  .map(([f, 줄들]) => `${f} (${줄들.length})\n` + 줄들.map((l) => ` ${l.줄}: ${l.글}`).join('\n'))
314
420
  .join('\n');
315
421
 
316
- const 여럿 = 여럿이면(후보, ctx.scope);
422
+ /*
423
+ * ── 「70곳은 안 실었습니다」 가 모델에게는 안 갔다 ────────────────────
424
+ *
425
+ * 바로 위에서 "자른 것은 자랐다고 말한다" 고 해 놓고, 그 말을 summary
426
+ * 에만 붙였다. 모델이 받는 글은 content 이고(loop.js 의 실을글), content
427
+ * 가 비지 않으면 summary 는 아예 안 실린다. 그러니 **자를 것이 있을 때는
428
+ * 반드시** 이 말이 빠졌다 — 잘랐다는 것을 알려야 할 바로 그때만.
429
+ *
430
+ * 8k 모델이면 한도가 50이다(budget.js 의 찾을개수는 작은 모델에서 50에서
431
+ * 바닥을 친다). 참조 120곳 중 50곳을 받은 모델은 그 50곳을 고치고
432
+ * 「모든 참조를 고쳤습니다」 로 답을 맺는다. 남은 70곳은 돌려 본 뒤에야
433
+ * 드러나고, 그때는 이미 다른 것도 같이 고쳐 놓은 뒤다.
434
+ *
435
+ * 사람 화면(summary)에는 그대로 두고, 글 끝에도 같이 적는다. 같은 말을
436
+ * 두 번 적는 것이 아니라, 여태 **한쪽에만** 적혀 있던 것을 마저 적는 것이다.
437
+ */
438
+ const 잘림말 = 남은 ? `(${남은}곳은 자리가 모자라 안 실었습니다)` : '';
439
+ /*
440
+ * 파일 수는 **자르기 전 전체**로 센다. 보인 50곳(묶음)으로 셌더니 두 파일에 걸친 120곳이
441
+ * 「120곳 · 1개 파일」 이 됐다 (2.0.0 6회차 LS4) — 몇 파일을 고쳐야 하는지 먼저 말하는
442
+ * 줄이 틀린 수를 준다. 줄 글은 안 읽고 주소만 본다.
443
+ */
444
+ const 파일수 = new Set(곳들.map((x) => {
445
+ try { return ctx.scope.show(fileURLToPath(x.uri)); } catch { return x.uri; }
446
+ })).size;
447
+ const 덧말 = [잘림말, 닮은, 여럿].filter(Boolean);
317
448
  return {
318
- summary: `${이름} — ${말('lsp.refs', { 자리: 세말('places', 곳들.length), 파일: 세말('files', 묶음.size) })}`
319
- + (남은 ? ` (${남은}곳은 자리가 모자라 안 실었습니다)` : '')
449
+ summary: `${이름} — ${말('lsp.refs', { 자리: 세말('places', 곳들.length), 파일: 세말('files', 파일수) })}`
450
+ + (잘림말 ? ` ${잘림말}` : '')
451
+ + (닮은 ? `\n${닮은}` : '')
320
452
  + (여럿 ? `\n${여럿}` : ''),
321
453
  found: 곳들.length,
322
- files: 묶음.size,
454
+ files: 파일수,
323
455
  truncated: 남은 > 0,
324
456
  locations: 보일것,
325
- content: 글,
457
+ content: 덧말.length ? `${글}\n\n${덧말.join('\n')}` : 글,
326
458
  };
327
459
  },
328
460
  };
@@ -54,16 +54,40 @@ const 규칙 = {
54
54
  [/^\s*(?:export\s+)?(?:const|let|var)\s+([\p{L}$_][\p{L}\p{N}$_]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[\p{L}$_][\p{L}\p{N}$_]*)\s*=>/u, 'fn'],
55
55
  [/^\s*(?:export\s+)?(?:interface|type|enum)\s+([\p{L}$_][\p{L}\p{N}$_]*)/u, 'type'],
56
56
  [/^\s*export\s+(?:const|let|var)\s+([\p{L}$_][\p{L}\p{N}$_]*)/u, 'const'],
57
- // 클래스 안의 메서드. 들여쓰기가 있고 괄호로 이어지는 이름.
58
- [/^\s{2,}(?:static\s+|async\s+|get\s+|set\s+|#)?([\p{L}$_][\p{L}\p{N}$_]*)\s*\([^)]*\)\s*\{/u, 'method'],
57
+ /*
58
+ * 클래스 안의 메서드. 들여쓰기가 있고 괄호로 이어지는 이름.
59
+ *
60
+ * 수식어는 **여러 개 붙는다.** 여기가 `(?:static\s+|async\s+|…)?` 라 하나까지만
61
+ * 먹었고, 그래서 `static async 불러오기()` 는 `static ` 을 먹은 뒤 `async` 를
62
+ * 이름으로 잡으려다 뒤의 `(` 를 못 만나 **아예 안 걸렸다.** 요즘 코드에서 제일
63
+ * 흔한 조합이 통째로 뼈대에서 빠지고, 모델은 그 메서드가 없는 줄 안다.
64
+ * 제너레이터의 `*` 와 사사로운 이름의 `#` 도 여기서 같이 받는다.
65
+ */
66
+ /*
67
+ * 들여쓰기를 `\s{2,}` 로 잡던 때, **탭으로 들여쓴 파일은 한 개도 안 걸렸다.**
68
+ * 탭 들여쓰기는 한 단이 탭 하나라서다. `.editorconfig` 가 `indent_style = tab`
69
+ * 인 저장소에서는 위 고침이 무엇이든 클래스 메서드가 통째로 빠지고, 클래스
70
+ * 이름만 달랑 뜬다 — 모델은 그 클래스에 메서드가 없는 줄 안다. 탭 하나 또는
71
+ * 빈칸 둘로 본다. 들여쓰기가 아예 없는 `이름() {` 은 부름이라 그대로 막힌다.
72
+ */
73
+ [/^(?:\t| {2,})\s*(?:(?:static|async|get|set)\s+)*\*?\s*#?([\p{L}$_][\p{L}\p{N}$_]*)\s*\([^)]*\)\s*\{/u, 'method'],
59
74
  ],
60
75
  py: [
61
76
  [/^\s*class\s+([\p{L}_][\p{L}\p{N}_]*)/u, 'class'],
62
77
  [/^\s*(?:async\s+)?def\s+([\p{L}_][\p{L}\p{N}_]*)/u, 'fn'],
63
78
  [/^([A-Z_][A-Z0-9_]{2,})\s*[:=]/, 'const'],
64
79
  ],
80
+ /*
81
+ * java 표는 `.kt`·`.kts`·`.scala`·`.groovy` 도 같이 쓴다 — 그리고 설명서(OUTLINE_TOOL)가
82
+ * 「java/kotlin 을 읽는다」 고 적어 둔다. 그런데 여기 자바 두 줄뿐이던 때,
83
+ * Kotlin 의 `fun` 과 Scala·Groovy 의 `def` 는 **어느 무늬에도 안 걸렸다.**
84
+ * `.kt` 파일이 클래스 이름 하나만 달고 떴고, 모델은 그 클래스에 함수가 없는 줄
85
+ * 안다 — 이 파일 머리말이 「모르는 것은 모른다고 말한다」 고 적어 둔 것과 반대로,
86
+ * 읽는다고 해 놓고 반만 읽었다. `data class`·`object`·맨 위 `fun` 까지 같이 본다.
87
+ */
65
88
  java: [
66
- [/^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:final\s+)?(?:abstract\s+)?(?:class|interface|enum|record)\s+([\p{L}_][\p{L}\p{N}_]*)/u, 'class'],
89
+ [/^\s*(?:(?:public|private|protected|internal|open|sealed|data|inner|value|annotation|abstract|final|static|case|implicit)\s+)*(?:class|interface|enum|record|object|trait)\s+([\p{L}_][\p{L}\p{N}_]*)/u, 'class'],
90
+ [/^\s*(?:(?:public|private|protected|internal|open|override|suspend|inline|operator|infix|tailrec|abstract|final|static|implicit)\s+)*(?:fun|def)\s+([\p{L}_][\p{L}\p{N}_]*)/u, 'fn'],
67
91
  [/^\s*(?:public|private|protected)\s+(?:static\s+)?(?:final\s+)?[\p{L}\p{N}_<>[\],\s]+\s+([\p{L}_][\p{L}\p{N}_]*)\s*\(/u, 'method'],
68
92
  ],
69
93
  go: [
@@ -165,8 +189,22 @@ export function 뼈대뽑기(글, 확장) {
165
189
 
166
190
  const 표 = 규칙[규칙이름] ?? [];
167
191
  const 항목 = [];
168
- const 본것 = new Set();
169
-
192
+ /*
193
+ * ── 같은 이름을 **지우지 않는다** ────────────────────────────────────
194
+ *
195
+ * 여기에 `본것` 이라는 집합이 있었다. 주석은 「같은 이름이 여러 번 걸리는
196
+ * 규칙이 있다」 — 한 줄이 규칙 여럿에 걸리는 것을 막겠다는 말이었다.
197
+ * 그건 아래 `break` 가 이미 한다(한 줄은 한 가지로만 센다). 그 집합이 실제로
198
+ * 한 일은 **파일 전체에서 둘째부터 조용히 지우기**였다:
199
+ *
200
+ * class 앞 { 읽기() {} } → 남는다
201
+ * class 뒤 { 읽기() {} } → **사라진다**
202
+ * ## 보기 … ## 보기 → 뒤엣것이 사라진다
203
+ *
204
+ * 뼈대는 「무엇이 **어디에** 있나」 를 보려고 보는 것이다. 이름이 겹친다고
205
+ * 자리를 지우면, 모델은 그 클래스에 그 메서드가 없는 줄 알고 새로 만들어
206
+ * 넣는다 — 이 파일 머리말이 없애려던 바로 그 일이다.
207
+ */
170
208
  for (const [i, 한줄] of 줄들.entries()) {
171
209
  // 주석 줄은 건너뛴다. 주석 안의 예제 코드가 뼈대로 올라오면 안 된다.
172
210
  if (/^\s*(?:\/\/|\/\*|\*|#(?!\s*[#!])|--)/.test(한줄) && 규칙이름 !== 'md') continue;
@@ -178,16 +216,18 @@ export function 뼈대뽑기(글, 확장) {
178
216
  let 실갈래 = 갈래;
179
217
  if (갈래 === '#' && m[2] !== undefined) {
180
218
  const 깊이 = 규칙이름 === 'md' ? m[1].length : Number(m[1]);
181
- 이름 = `${' '.repeat(Math.max(0, 깊이 - 1))}${m[2]}`;
219
+ // `짧게` 는 빈칸을 접고 trim 한다. 들여쓰기를 먼저 붙이면 거기서 지워져 한 번도 안 나갔다 —
220
+ // 글을 먼저 줄이고 들여쓰기는 뒤에 붙인다 (6회차 뼈대6bc-b OL3).
221
+ const 글 = 짧게(m[2]);
222
+ 이름 = 글 && `${' '.repeat(Math.max(0, 깊이 - 1))}${글}`;
182
223
  실갈래 = `h${깊이}`;
183
224
  } else {
184
- 이름 = m[1];
225
+ 이름 = 짧게(m[1]);
226
+ // if·for 거르기는 메서드 규칙이 `if (…) {` 를 잡는 것을 막는 자리다. 헤딩에는 안 건다 —
227
+ // 걸면 `## class` · `## new` 같은 문서 제목이 뼈대에서 사라졌다 (6회차 뼈대6bc-b OL4).
228
+ if (이름아님.has(이름)) continue;
185
229
  }
186
- 이름 = 짧게(이름);
187
- if (!이름 || 이름아님.has(이름.trim())) continue;
188
- const 열쇠 = `${실갈래}|${이름.trim()}`;
189
- if (본것.has(열쇠)) continue; // 같은 이름이 여러 번 걸리는 규칙이 있다
190
- 본것.add(열쇠);
230
+ if (!이름) continue;
191
231
  항목.push({ 줄: i + 1, 갈래: 실갈래, 이름 });
192
232
  break; // 한 줄은 한 가지로만 센다
193
233
  }