deel-local-cli 1.20.12 → 1.20.13

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 CHANGED
@@ -20,7 +20,7 @@
20
20
 
21
21
  [![Node.js CI](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/test.yml?branch=main&logo=github&logoColor=white&label=Node.js%20CI)](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
22
22
  [![CodeQL](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/codeql.yml?branch=main&logo=github&logoColor=white&label=CodeQL)](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
23
- [![tests](https://img.shields.io/badge/tests-10%2C125%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/ko/develop.md)
23
+ [![tests](https://img.shields.io/badge/tests-10%2C215%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/ko/develop.md)
24
24
 
25
25
  [![dependencies](https://img.shields.io/badge/dependencies-0-1a7f37)](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
26
26
  [![ESM](https://img.shields.io/badge/ESM-Node%2020%2B-5FA04E?logo=javascript&logoColor=white)](package.json)
@@ -301,7 +301,7 @@ deel --offline
301
301
  무엇이 어디로 갈 수 있는지는 켤 때 화면 맨 위에 늘 적혀 있습니다.
302
302
 
303
303
  ```
304
- deel 1.20.12 ⌂ 이 안
304
+ deel 1.20.13 ⌂ 이 안
305
305
  보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
306
306
  ```
307
307
 
@@ -1289,6 +1289,7 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
1289
1289
 
1290
1290
  | 판 | 무엇이 바뀌었나 |
1291
1291
  |---|---|
1292
+ | [1.20.13](docs/ko/releases/1.20.md#12013) | 띄어쓰기 한 칸이 시킨 말을 통째로 숨겼습니다 |
1292
1293
  | [1.20.12](docs/ko/releases/1.20.md#12012) | 켠다고 적은 자물쇠가 아무것도 안 잠그고 있었습니다 |
1293
1294
  | [1.20.11](docs/ko/releases/1.20.md#12011) | 안전망이 있다고 말하는데 없었습니다 |
1294
1295
  | [1.20.10](docs/ko/releases/1.20.md#12010) | 답하지 않았는데 답한 것이 되어 있었습니다 |
package/README.md CHANGED
@@ -20,7 +20,7 @@ Vendor APIs connect too — **only when you say so**
20
20
 
21
21
  [![Node.js CI](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/test.yml?branch=main&logo=github&logoColor=white&label=Node.js%20CI)](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
22
22
  [![CodeQL](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/codeql.yml?branch=main&logo=github&logoColor=white&label=CodeQL)](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
23
- [![tests](https://img.shields.io/badge/tests-10%2C125%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/en/develop.md)
23
+ [![tests](https://img.shields.io/badge/tests-10%2C215%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/en/develop.md)
24
24
 
25
25
  [![dependencies](https://img.shields.io/badge/dependencies-0-1a7f37)](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
26
26
  [![ESM](https://img.shields.io/badge/ESM-Node%2020%2B-5FA04E?logo=javascript&logoColor=white)](package.json)
@@ -374,7 +374,7 @@ deel --offline
374
374
  The destination is printed at the top of every session:
375
375
 
376
376
  ```
377
- deel 1.20.12 ⌂ inside
377
+ deel 1.20.13 ⌂ inside
378
378
  Sends to this machine 127.0.0.1:11434 ← nowhere else
379
379
  ```
380
380
 
@@ -1350,6 +1350,7 @@ so one run tells you everything.
1350
1350
 
1351
1351
  | Version | What changed |
1352
1352
  |---|---|
1353
+ | [1.20.13](docs/en/releases/1.20.md#12013) | One space hid the whole instruction |
1353
1354
  | [1.20.12](docs/en/releases/1.20.md#12012) | The lock we said was on was not attached to anything |
1354
1355
  | [1.20.11](docs/en/releases/1.20.md#12011) | The safety net said it was there when it wasn't |
1355
1356
  | [1.20.10](docs/en/releases/1.20.md#12010) | Nobody answered, yet an answer had been given |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deel-local-cli",
3
- "version": "1.20.12",
3
+ "version": "1.20.13",
4
4
  "description": "A coding-agent CLI that runs on local models and private gateways. Zero dependencies, Node 20+.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/acp/map.js CHANGED
@@ -25,6 +25,8 @@
25
25
  //
26
26
  // 읽기만 하는 순수 함수를 들여오는 것은 순수함을 안 깬다. 규격을 읽는 규칙은
27
27
  // backend/adapter.js 한 곳에만 있어야 한다 (집안 규칙: 자는 한 벌).
28
+ import { isAbsolute, resolve } from 'node:path';
29
+
28
30
  import { 부른것들, 결과들, 본문글 } from '../backend/adapter.js';
29
31
  import { 첫이름 } from '../tools/label.js';
30
32
 
@@ -40,9 +42,12 @@ const 갈래표 = {
40
42
  Write: 'edit',
41
43
  Append: 'edit',
42
44
  Edit: 'edit',
45
+ Move: 'move',
43
46
  Glob: 'search',
44
47
  Grep: 'search',
45
48
  Recall: 'search',
49
+ Def: 'search',
50
+ Refs: 'search',
46
51
  Bash: 'execute',
47
52
  Jobs: 'execute',
48
53
  Verify: 'execute',
@@ -50,6 +55,7 @@ const 갈래표 = {
50
55
  Task: 'think',
51
56
  TodoWrite: 'think',
52
57
  Skill: 'think',
58
+ Ask: 'other',
53
59
  Remember: 'other',
54
60
  };
55
61
 
@@ -77,8 +83,15 @@ export function 도구이름표(이름, 인자) {
77
83
  : null)
78
84
  ?? (Array.isArray(a.paths) && a.paths.length ? `${a.paths.length}개` : null)
79
85
  ?? (Array.isArray(a.todos) ? `${a.todos.length}건` : null);
80
- const 안 = 첫 == null ? '' : 자르기(String(첫), 80);
81
- return 안 ? `${이름}(${안})` : String(이름 ?? '도구');
86
+ /*
87
+ * 이름표는 에디터의 **한 줄** 머리다. 본문 줄이는 자르기() 를 쓰면 그 끝에
88
+ * `\n… (N자 줄임)` 이 붙어서 머리가 두 줄이 되고, 한 줄을 미리 정해 둔
89
+ * 자리라 그 줄이 깨진다. 긴 명령 하나면 바로 그렇게 된다.
90
+ */
91
+ const 안 = 첫 == null ? '' : 한줄로(String(첫), 80);
92
+ // 이름이 없으면 「도구」 다. 앞칸만 채우면 화면에 `undefined(a.js)` 가 뜬다.
93
+ const 이름글 = String(이름 ?? '').trim() || '도구';
94
+ return 안 ? `${이름글}(${안})` : 이름글;
82
95
  }
83
96
 
84
97
  /**
@@ -88,11 +101,21 @@ export function 도구이름표(이름, 인자) {
88
101
  * 있는데, 에디터는 절대 경로라야 연다. 인자만 보고 넘기면 눌러도 안 열리는
89
102
  * 링크가 되고, 그건 없느니만 못하다.
90
103
  */
91
- export function 도구자리(이름, 인자, 결과) {
104
+ export function 도구자리(이름, 인자, 결과, { 뿌리 = null } = {}) {
92
105
  const 모은것 = [];
93
106
  const 넣기 = (p) => {
94
107
  const s = typeof p === 'string' ? p.trim() : '';
95
- if (s && !모은것.includes(s)) 모은것.push(s);
108
+ if (!s) return;
109
+ /*
110
+ * 상대 경로는 **여기서** 절대 경로로 편다.
111
+ *
112
+ * 에디터는 절대 경로라야 연다. 상대 경로를 그대로 주면 눌러도 아무 일이
113
+ * 안 일어나는데, 화면에는 멀쩡한 링크로 보인다 — 없느니만 못하다.
114
+ * 도는 중(tool_call)에는 결과가 아직 없어서 인자에 적힌 상대 경로뿐이라,
115
+ * 이 자리를 안 펴면 도구가 도는 내내 링크가 다 죽어 있다.
116
+ */
117
+ const 편것 = 뿌리 && !isAbsolute(s) ? resolve(뿌리, s) : s;
118
+ if (!모은것.includes(편것)) 모은것.push(편것);
96
119
  };
97
120
 
98
121
  넣기(결과?.changed);
@@ -141,7 +164,7 @@ export function 도구내용(결과, 최대 = 2000) {
141
164
  * @param {string} 아이디 이 세션 안에서 유일한 번호
142
165
  * @param {object} ev loop.js 가 흘린 `tool` 이벤트
143
166
  */
144
- export function 도구끝남(아이디, ev) {
167
+ export function 도구끝남(아이디, ev, { 뿌리 = null } = {}) {
145
168
  return {
146
169
  sessionUpdate: 'tool_call_update',
147
170
  toolCallId: 아이디,
@@ -149,12 +172,12 @@ export function 도구끝남(아이디, ev) {
149
172
  kind: 도구갈래(ev?.name),
150
173
  status: 도구탈났나(ev?.result) ? 'failed' : 'completed',
151
174
  content: 도구내용(ev?.result),
152
- locations: 도구자리(ev?.name, ev?.args, ev?.result),
175
+ locations: 도구자리(ev?.name, ev?.args, ev?.result, { 뿌리 }),
153
176
  };
154
177
  }
155
178
 
156
179
  /** 이제 막 시작한 도구 호출. */
157
- export function 도구시작(아이디, 이름, 인자) {
180
+ export function 도구시작(아이디, 이름, 인자, { 뿌리 = null } = {}) {
158
181
  return {
159
182
  sessionUpdate: 'tool_call',
160
183
  toolCallId: 아이디,
@@ -162,7 +185,7 @@ export function 도구시작(아이디, 이름, 인자) {
162
185
  kind: 도구갈래(이름),
163
186
  status: 'in_progress',
164
187
  content: [],
165
- locations: 도구자리(이름, 인자, null),
188
+ locations: 도구자리(이름, 인자, null, { 뿌리 }),
166
189
  };
167
190
  }
168
191
 
@@ -227,7 +250,7 @@ function 인자풀기(x) {
227
250
  * @param {object[]} messages store.js 가 읽어 온 대화
228
251
  * @returns {object[]} session/update 의 update 자리에 그대로 넣을 것들
229
252
  */
230
- export function 되살린것(messages, { 최대내용 = 2000 } = {}) {
253
+ export function 되살린것(messages, { 최대내용 = 2000, 뿌리 = null } = {}) {
231
254
  const 줄 = Array.isArray(messages) ? messages : [];
232
255
 
233
256
  /*
@@ -315,7 +338,7 @@ export function 되살린것(messages, { 최대내용 = 2000 } = {}) {
315
338
  content: 없음
316
339
  ? [{ type: 'content', content: { type: 'text', text: '결과가 안 남았습니다 — 이 도구가 도는 중에 끊겼습니다.' } }]
317
340
  : [{ type: 'content', content: { type: 'text', text: 자르기(String(답), 최대내용) } }],
318
- locations: 도구자리(이름, 인자, null),
341
+ locations: 도구자리(이름, 인자, null, { 뿌리 }),
319
342
  });
320
343
  }
321
344
  }
@@ -365,3 +388,9 @@ function 자르기(s, n) {
365
388
  const t = String(s ?? '');
366
389
  return t.length > n ? `${t.slice(0, n)}\n… (${t.length - n}자 줄임)` : t;
367
390
  }
391
+
392
+ /** 한 줄짜리 자리(이름표)에 쓴다. 줄바꿈을 빈칸으로 눕히고 말줄임만 붙인다. */
393
+ function 한줄로(s, n) {
394
+ const t = String(s ?? '').replace(/\s+/g, ' ').trim();
395
+ return t.length > n ? `${t.slice(0, n)}…` : t;
396
+ }
package/src/acp/serve.js CHANGED
@@ -445,7 +445,7 @@ export async function acp(opts = {}) {
445
445
  title: 도구이름표(이름, 인자),
446
446
  kind: 도구갈래(이름),
447
447
  status: 'pending',
448
- locations: 도구자리(이름, 인자, null),
448
+ locations: 도구자리(이름, 인자, null, { 뿌리: 방.root }),
449
449
  rawInput: 인자 ?? {},
450
450
  },
451
451
  options: [
@@ -643,17 +643,17 @@ export async function acp(opts = {}) {
643
643
  break;
644
644
 
645
645
  case 'tool_start':
646
- 보내기(도구시작(도구맡기기(방, ev.name), ev.name, ev.args));
646
+ 보내기(도구시작(도구맡기기(방, ev.name), ev.name, ev.args, { 뿌리: 방.root }));
647
647
  break;
648
648
 
649
649
  case 'tools_start':
650
650
  for (const 이름 of ev.names ?? []) {
651
- 보내기(도구시작(도구맡기기(방, 이름), 이름, null));
651
+ 보내기(도구시작(도구맡기기(방, 이름), 이름, null, { 뿌리: 방.root }));
652
652
  }
653
653
  break;
654
654
 
655
655
  case 'tool':
656
- 보내기(도구끝남(도구찾기(방, ev.name), ev));
656
+ 보내기(도구끝남(도구찾기(방, ev.name), ev, { 뿌리: 방.root }));
657
657
  적기();
658
658
  break;
659
659
 
@@ -882,7 +882,7 @@ export async function acp(opts = {}) {
882
882
  } catch { /* 못 읽으면 기억하고 있는 것으로 그린다 */ }
883
883
 
884
884
  let 흘린것 = 0;
885
- for (const update of 되살린것(그릴것)) {
885
+ for (const update of 되살린것(그릴것, { 뿌리: 방.root })) {
886
886
  관.알림('session/update', { sessionId: 방.id, update });
887
887
  흘린것++;
888
888
  }
@@ -373,7 +373,23 @@ export const 표 = {
373
373
 
374
374
  ask: [
375
375
  // 설명은 아무것도 안 바꾸는 모드라 잘못 고르면 답답해진다. 신호를 엄격히 본다.
376
- [/뭐(야|예요|입니까|지)\s*\??$/m, 5], [/무엇(인가|입니까|이야)/, 5],
376
+ /*
377
+ * ── 물음표 **뒤에** 한마디를 더 붙이면 물음이 아니게 됐다 ─────────
378
+ *
379
+ * 줄 끝(`$`)에 못박혀 있었다. 그런데 사람은 물음표로 말을 안 끝낸다 —
380
+ *
381
+ * "이거 뭐야?" → ask 5점 → 설명 모드
382
+ * "이거 뭐야? 알려줘" → ask 3점 → 종합 모드
383
+ *
384
+ * 「알려줘」 를 붙였더니 물음이 아닌 것이 됐다. 붙인 말은 **더 설명해
385
+ * 달라는 말**인데 점수가 되레 깎였다(5 → 3). 남은 '알려 줘' 3점은
386
+ * 문턱 5에 모자라서 종합으로 떨어졌다.
387
+ *
388
+ * 물음표가 있으면 그 자리가 문장 끝이다. 줄 끝이 아니어도 된다.
389
+ * 물음표가 없을 때만 줄 끝을 따진다 — "뭐야 이게" 같은 말을 아무 데서나
390
+ * 줍지 않으려는 것이 원래 못의 뜻이었고, 그건 그대로 둔다.
391
+ */
392
+ [/뭐(야|예요|입니까|지)\s*(?:\?|$)/m, 5], [/무엇(인가|입니까|이야)/, 5],
377
393
  /*
378
394
  * 「뭐 하는 파일이야?」 — 터미널에서 돌려 보다 걸린 자리다.
379
395
  *
@@ -476,10 +492,46 @@ export const 표 = {
476
492
  * 문제가 아니라 **낱말 문제**다. 「말해 줘」 는 `explain` 과 같은 부류의
477
493
  * 물음 시킴말이라 그쪽에 넣는다 — 못을 뽑으면 위의 `Rewrite how to` 가
478
494
  * 도로 열린다.
495
+ *
496
+ * ── 공손한 한마디가 못 앞에 끼어들었다 ──────────────────────────────
497
+ *
498
+ * 첫머리 못을 `^\s*` 로 박아 놓으니 **빈칸 말고는 아무것도** 앞에 못
499
+ * 온다. 그런데 영어로 묻는 사람은 그 자리에 거의 항상 한마디를 붙인다 —
500
+ *
501
+ * "tell me what this does" → ask 5점
502
+ * "Please tell me what this does" → 전부 0점 → 종합 모드
503
+ * "Could you tell me what this does" → 전부 0점 → 종합 모드
504
+ *
505
+ * 공손하게 물을수록 물음으로 안 읽혔다. 답만 하면 되는 턴이 파일을
506
+ * 고칠 수 있는 모드로 돌았다는 뜻이다.
507
+ *
508
+ * 그래서 못 바로 뒤에 **공손말 한 마디**를 허락한다. 못을 뽑는 것이
509
+ * 아니다 — "Rewrite this function to show me the result." 은 여전히
510
+ * 0점이다. 그 자리의 `to show` 앞에는 공손말이 아니라 딴 말이 있다.
511
+ *
512
+ * 한 마디가 아니라 **두 마디**가 붙기도 한다. "Please could you tell
513
+ * me …" 는 실제로 흔한데, 세로줄로 갈라 놓으면 둘 중 하나만 먹고
514
+ * 나머지가 남아 통째로 0점이 됐다. 그래서 갈래가 아니라 차례로 둔다.
515
+ *
516
+ * `what` · `how` 쪽에는 `please` 만 준다. 「Could you what …」 은 영어에
517
+ * 없는 꼴이라 받아 봐야 걸릴 자리가 없다.
518
+ *
519
+ * ── 물어보고 **나서 고치라**는 한 문장 ───────────────────────────
520
+ *
521
+ * "Please show me the config and rename it to config.json."
522
+ * "Could you tell me what changed and update the changelog?"
523
+ *
524
+ * 공손말을 받아 주자 이것들이 5점을 꽉 채워 **묻기 모드**로 갔다. 묻기는
525
+ * 파일을 못 고치는 모드라, 사람은 설명만 받고 이름은 그대로다. 공손말을
526
+ * 받기 전에는 0점이라 종합(고칠 수 있는 자리)에 남아 있었으니, 이 변경이
527
+ * 없던 자리를 만든 셈이다.
528
+ *
529
+ * `and`·`then` 으로 이은 시킴 마디가 같은 문장에 있으면 이 규칙을 끈다.
530
+ * 「and why」 「and how it works」 처럼 움직씨가 아닌 것은 안 건드린다.
479
531
  */
480
- [/\bexplain(?:s|ing|ed)?\b|(?:^\s*|[.!?]\s+|\n\s*)(?:(?:tell|show)\s+(?:me|us)\b|walk\s+(?:me|us)\s+through\b)/im, 5],
481
- [/(?:^\s*|[.!?]\s+|\n\s*)what\b(?:(?:[^.!?\n]|\.(?=[\w/]))*\?|(?:'s|(?:\s+\w+){0,3}\s+(?:is|are|was|were|does|do|did|can|should)|\s+to)\b)/im, 5],
482
- [/(?:^\s*|[.!?]\s+|\n\s*)how\b(?:(?:[^.!?\n]|\.(?=[\w/]))*\?|'s\b|\s+(?:to\b|does|do|did|is|are|was|were|can|should)\b)/im, 5],
532
+ [/(?:\bexplain(?:s|ing|ed)?\b|(?:^\s*|[.!?]\s+|\n\s*)(?:(?:please|pls)\s*,?\s+)?(?:(?:could|can|would|will)\s+you\s+)?(?:please\s*,?\s+)?(?:(?:tell|show)\s+(?:me|us)\b|walk\s+(?:me|us)\s+through\b))(?![^.!?\n]*\b(?:and|then)\s+(?:also\s+)?(?:implement|add|fix|write|rename|update|delete|remove|create|build|make|refactor|rewrite)\b)/im, 5],
533
+ [/(?:^\s*|[.!?]\s+|\n\s*)(?:(?:please|pls)\s*,?\s+)?what\b(?:(?:[^.!?\n]|\.(?=[\w/]))*\?|(?:'s|(?:\s+\w+){0,3}\s+(?:is|are|was|were|does|do|did|can|should)|\s+to)\b)/im, 5],
534
+ [/(?:^\s*|[.!?]\s+|\n\s*)(?:(?:please|pls)\s*,?\s+)?how\b(?:(?:[^.!?\n]|\.(?=[\w/]))*\?|'s\b|\s+(?:to\b|does|do|did|is|are|was|were|can|should)\b)/im, 5],
483
535
  [/읽어(만)?\s*보고\s*(설명|알려)/, 4],
484
536
  ],
485
537
 
@@ -581,10 +633,53 @@ const 문턱 = {
581
633
  * 쪽이다. '고쳐줘' 한마디에 창이 뜨면 그게 더 나쁘다.
582
634
  * 그래서 **계획말 + 잇는말** 이 앞에 있고 그 **뒤에** 실행말이 와야만 겹침이다.
583
635
  */
584
- const 계획말 = '계획|플랜|설계|구상|기획|정리|검토|조사|파악|살펴|알아보|분석|로드맵|방향|순서|밑그림|초안';
585
- // '정리하고' 처럼 잇는 꼴이어야 한다. 그냥 '정리' 하나로는 겹침이 아니다 —
586
- // "코드 정리해줘" 는 계획이 아니라 그 자체가 할 일이다.
587
- const 잇는말 = '해\\s*주고|해\\s*놓고|하고\\s*나서|하고서|한\\s*다음|한\\s*뒤|한\\s*후|하고|해서|잡고|짜고|세우고|낸\\s*뒤|낸\\s*다음';
636
+ /*
637
+ * '알아보' 가 아니라 '알아' 다. 「알아보고」 는 '알아' + '보고' 로 갈라야
638
+ * 아래 잇는말의 '보고' 와 이어진다. '알아보' 로 묶어 두면 남는 것이 '고'
639
+ * 하나뿐이라 어떤 잇는말과도 안 맞았다 — 아래 머리말 참고.
640
+ */
641
+ const 계획말 = '계획|플랜|설계|구상|기획|정리|검토|조사|파악|살펴|알아|분석|로드맵|방향|순서|밑그림|초안';
642
+ /*
643
+ * '정리하고' 처럼 잇는 꼴이어야 한다. 그냥 '정리' 하나로는 겹침이 아니다 —
644
+ * "코드 정리해줘" 는 계획이 아니라 그 자체가 할 일이다.
645
+ *
646
+ * ── 한국말에서 가장 흔한 이음꼴이 빠져 있었다 ───────────────────────────
647
+ *
648
+ * 「…해 보고」 다. 「살펴보고」 「알아보고」 「검토해 보고」 — 계획을 먼저
649
+ * 하고 그다음에 하라는 말을 사람들이 실제로 제일 많이 쓰는 꼴인데, 목록에
650
+ * 없었다. 그래서 이 규칙이 잡으라고 만들어진 바로 그 문장이 안 잡혔다 —
651
+ *
652
+ * "살펴보고 만들어줘" → 겹침 아님 → code 모드 → 계획 한 줄 없이 파일부터
653
+ * "정리하고 만들어줘" → 겹침 → 계획을 보여 주고 승인을 받음
654
+ *
655
+ * 같은 말인데 이음꼴 하나로 갈렸다. 앞의 사람은 「먼저 보고 나서」 라고
656
+ * 적어 놓고도 계획을 못 봤다.
657
+ *
658
+ * ── 「보고」 를 이름씨와 가르는 자리 ────────────────────────────────────
659
+ *
660
+ * 처음엔 `보고(?![서를가])` 하나로 막았다. 「검토 보고서를 만들어줘」 는
661
+ * 겹침이 아니라 그냥 할 일이니 맞는 방향이었는데, **가르는 자리가 틀렸다.**
662
+ * 「살펴보고서 만들어줘」 의 '-고서' 는 이름씨가 아니라 멀쩡한 이음씨끝인데
663
+ * 같이 막혔다. 뒤에 뭐가 오느냐만 봐서는 둘을 못 가른다.
664
+ *
665
+ * 앞을 봐야 갈린다 — 그런데 「빈칸이 있나」 가 아니라 **앞말이 무엇이냐** 다.
666
+ *
667
+ * "살펴보고서 만들어줘" — 앞이 움직씨 줄기(살펴) → 이음말
668
+ * "검토보고서 작성해줘" — 앞이 이름씨(검토) → '보고서' 라는 한 낱말
669
+ *
670
+ * 처음엔 빈칸만 보고 갈랐다. 붙어 있으면 이음말로 쳤더니 「검토보고서」
671
+ * 「분석보고서」 가 통째로 겹침이 됐다 — 보고서를 써 달라는 말이 계획 모드로
672
+ * 가서 계획만 내고 멈춘다. 빈칸은 우연이고, 갈리는 것은 앞말의 품사다.
673
+ *
674
+ * '살펴·알아' 는 움직씨 줄기라 뒤에 오는 '보고' 가 언제나 도움움직씨다.
675
+ * '검토·분석·조사' 는 이름씨라 '보고' 가 붙으면 한 낱말이 된다. 이름씨 쪽은
676
+ * '해' 를 끼워야 도움움직씨가 된다 — 「검토해 보고」. 그 꼴에는 뒷말을
677
+ * 따진다(「검토해 보고서를」 은 다시 이름씨다).
678
+ */
679
+ const 이름씨아님 = '(?![서를가에의는도만까와과])';
680
+ const 잇는말 = '해\\s*주고|해\\s*놓고|하고\\s*나서|하고서|한\\s*다음|한\\s*뒤|한\\s*후|하고|해서|잡고|짜고|세우고|낸\\s*뒤|낸\\s*다음'
681
+ + `|(?<=(?:살펴|알아)\\s{0,1})보고|해\\s*보고${이름씨아님}`
682
+ + '|(?<=(?:살펴|알아)\\s{0,1})본\\s*(?:다음|뒤|후)|해\\s*본\\s*(?:다음|뒤|후)';
588
683
  const 실행말 = /만들어|만들자|만들어라|구현|작성|추가(해|하)|생성|써\s*(줘|주세요)|짜\s*(줘|주세요)|고쳐|세팅|설치|배포/;
589
684
  const 계획먼저 = new RegExp(`(${계획말})\\s*(?:을|를)?\\s*(?:${잇는말})`);
590
685
  /*
@@ -606,8 +701,47 @@ const 계획먼저 = new RegExp(`(${계획말})\\s*(?:을|를)?\\s*(?:${잇는
606
701
  *
607
702
  * 그래서 쉼표를 뺀다. 차례를 뜻하는 말이 있을 때만 겹침이다. 한국어 쪽은
608
703
  * 처음부터 잇는말 목록으로 이렇게 하고 있었고, 영어 쪽만 헐거웠다.
704
+ *
705
+ * ── 쉼표를 목록에서만 뺐지, 지나가는 길은 안 막았다 ─────────────────────
706
+ *
707
+ * 위를 그렇게 고쳐 놓고도 **바로 그 예문이 여전히 겹침으로 잡혔다.**
708
+ *
709
+ * "independently design, implement, test, and build the feature"
710
+ *
711
+ * 이음말 목록에서 `,` 를 뺀 것은 맞다. 그런데 목록에 남아 있던 맨 `and` 가
712
+ * `design` 에서 예순 자 떨어진 **맨 끝의 `, and build`** 와 짝지어졌다.
713
+ * 사이의 쉼표들은 `[\s\S]{0,60}?` 가 그냥 지나갔다. 열거를 차례로 읽는
714
+ * 결과는 처음과 똑같았다 — 계획 모드로 가서 계획만 내고 멈춘다.
715
+ *
716
+ * 그래서 길을 나눈다.
717
+ *
718
+ * - `and then` · `then` · `after that` · `before you` 처럼 **차례를 적어
719
+ * 둔 말**이면 사이에 쉼표가 있어도 겹침이다 ("Plan the migration,
720
+ * then implement it" — 사람이 차례라고 썼다).
721
+ * - 맨 `and` 하나로 잇는 것은 **쉼표를 하나까지만 넘는다** ("Design and
722
+ * implement" 과 "Plan the migration, and implement it" 은 겹침,
723
+ * "design, implement, test, and build" 는 열거).
724
+ *
725
+ * 쉼표 하나를 남겨 둔 까닭은, 영어에서 `and` 앞에 쉼표를 찍는 것이 그냥
726
+ * 흔해서다. 열거와 갈리는 자리는 **쉼표가 몇 개냐** 다 — 열거는 `and` 에
727
+ * 닿기까지 쉼표를 여러 번 넘어야 하고, 차례는 많아야 한 번이다.
728
+ *
729
+ * ── `the design` 은 계획을 세우라는 말이 아니다 ─────────────────────────
730
+ *
731
+ * 이 네 낱말은 움직씨이기도 하고 이름씨이기도 하다. 앞에 관사가 붙으면
732
+ * 이름씨다 — 그때는 계획을 세우라는 말이 아니라 그냥 무엇을 가리키는 말이다.
733
+ *
734
+ * "The design is fine. Run the linter and write the changelog."
735
+ * "Our plan is approved. Open the file and make the change."
736
+ *
737
+ * 둘 다 겹침으로 잡혀 계획 모드로 갔다. 뒤의 것은 사람이 **이미 승인됐다고
738
+ * 적어 놨는데** 또 계획을 내고 승인을 물었다.
739
+ *
740
+ * 맨 `and` 갈래는 문장 끝도 못 넘게 한다. 쉼표보다 센 구분자인데 `[^,]` 가
741
+ * 마침표·물음표·줄바꿈을 다 지나갔다. 그리고 `\s*` 는 상한이 없어서 예순 자
742
+ * 잣대가 빈칸으로 뚫렸다 — `[ \t]*` 로 좁힌다.
609
743
  */
610
- const 영어겹침 = /\b(plan|design|outline|draft)\b[\s\S]{0,60}?\b(?:and\s+then|then|and|after\s+(?:that|approval)|before\s+you)\s+(?:then\s+)?\b(build|implement|create|write|make|code)\b/i;
744
+ const 영어겹침 = /(?<!\b(?:the|a|an|this|that|our|your|my|its|their)\s)\b(plan|design|outline|draft)\b(?:[\s\S]{0,60}?\b(?:and\s+then|then|after\s+(?:that|approval)|before\s+you)|[^,.!?\n]{0,60}?,?[ \t]{0,2}\band)\s+(?:then\s+)?\b(build|implement|create|write|make|code)\b/i;
611
745
 
612
746
  /*
613
747
  * ── 두 말이 **얼마나 붙어 있어야** 겹침인가 ─────────────────────────────
@@ -740,7 +874,120 @@ export const 손대는동사 = '만들|고쳐|고치|바꿔|바꾸|추가|넣어
740
874
  * (`\\S{0,3}` 는 처음부터 두 개라 멀쩡했다. 한 무늬 안에서 한쪽만 틀렸으니
741
875
  * 눈으로는 더 안 보인다.)
742
876
  */
743
- const 손대라는말 = new RegExp(`(${손대는동사})\\S{0,3}(줘|주세요|주라|해라|하라|하자|해다오|해줄래|할래|어라|아라|여라|라(?=[\\s.!?~,]|$))`);
877
+ /*
878
+ * ── 띄어쓰기 한 칸이 시킴말을 통째로 숨겼다 ─────────────────────────────
879
+ *
880
+ * 사이를 `\S{0,3}` 로 적어서 **빈칸을 하나도 못 넘었다.** 그런데 「수정해 줘」
881
+ * 「고쳐 줘」 는 맞춤법대로 띄어 쓴 꼴이다. 그래서 같은 말이 이렇게 갈렸다 —
882
+ *
883
+ * "이 구조를 살펴보고 코드 수정해줘" → code 모드
884
+ * "이 구조를 살펴보고 코드 수정해 줘" → 종합 (손대라는 말이 아닌 것으로 읽힘)
885
+ *
886
+ * 모드만 갈린 것이 아니다. 이 검사는 「하라고 한 일을 두고 무슨 일을 할지
887
+ * 되묻지 않는다」 는 자리(askcheck.js)에도 쓰인다. 띄어 쓴 사람은 그 그물이
888
+ * 통째로 안 걸려서, 시켜 놓고도 「무슨 일을 할까요?」 를 다시 받았다.
889
+ *
890
+ * 세 글자 상한은 그대로다. 그래서 "나누는 설계만 봐줘" 는 여전히 안 걸린다 —
891
+ * 「나누」 와 「줘」 사이가 일곱 자다.
892
+ */
893
+ /*
894
+ * ── 「계획 정리해 줘」 는 계획을 다시 세워 달라는 말이다 ────────────────
895
+ *
896
+ * 영어 쪽에는 이 예외가 처음부터 있었다. `Update the plan.` 은 시킴말로 안
897
+ * 센다 — 고칠 파일 이름이 아니라 **계획 그 자체**가 목적어라서, 계획 모드로
898
+ * 가는 것이 맞다. 그런데 한국어 쪽에는 그 예외가 없었다.
899
+ *
900
+ * "이전 계획 정리해 줘" → 종합
901
+ * "Update the plan." → 계획
902
+ *
903
+ * 같은 말이 언어에 따라 다른 데로 갔다. 한국어로 물은 사람만 계획을 못 받았다.
904
+ *
905
+ * 목적어가 계획거리일 때만, 그리고 **계획을 손질하는 뜻의 동사**일 때만
906
+ * 뺀다. 영어가 그은 선과 같은 자리다 — `Implement the plan we agreed on.`
907
+ * 은 영어에서도 시킴말이다. 계획을 실행하라는 말은 손대라는 말이 맞다.
908
+ * 그래서 '구현·추가·삭제…' 는 그대로 두고 '만들·작성·바꾸·정리·수정' 만 뺀다.
909
+ *
910
+ * 「계획서」 「plan.md」 는 안 걸린다. 그건 계획거리가 아니라 **파일**이고,
911
+ * 파일을 고치라는 말은 고치라는 말이다. 영어가 `plan.md` 를 가르는 방법
912
+ * (`\.(?!\w)`)과 같은 뜻이다.
913
+ *
914
+ * 목록을 손대는동사 에서 갈라 내므로, 새 동사를 넣으면 자동으로 「예외 없는」
915
+ * 쪽에 들어간다 — 예외로 뺀 것은 '만들·작성·바꾸(바꿔)·정리·수정' 다섯 뜻이다.
916
+ *
917
+ * 계획거리와 동사 사이에 '좀' 이 끼는 것도 받는다. 「계획 좀 수정해 줘」 는
918
+ * 「계획 수정해 줘」 와 같은 말인데, 한 글자 때문에 딴 데로 가면 안 된다.
919
+ */
920
+ /*
921
+ * 계획거리는 plan 점수표에 있는 것만 적는다. '기획' 을 넣어 봤더니 예외는
922
+ * 걸리는데 갈 데가 없었다 — plan 점수가 0이라 「손대라는 말이 아니다」 까지만
923
+ * 가고 결국 종합이나 코드로 떨어졌다. 빼는 자리와 보내는 자리가 다르면
924
+ * 어느 쪽도 제 일을 못 한다.
925
+ *
926
+ * 사이에 끼는 말도 받는다. 「계획 다시 정리해 줘」 「계획 좀 더 정리해줘」 는
927
+ * 「계획 정리해 줘」 와 같은 말이다. 아무 말이나 받으면 「계획 파일 수정해 줘」
928
+ * 까지 빠지므로, 끼워도 되는 것은 **어찌씨 몇 개**로 못박는다.
929
+ */
930
+ const 계획거리 = '계획|플랜|로드맵';
931
+ const 끼는말 = '좀|더|다시|새로|전체|모두|싹|한번';
932
+ const 계획손질 = new Set(['만들', '작성', '바꿔', '바꾸', '정리', '수정']);
933
+ const 동사들 = 손대는동사.split('|');
934
+ const 계획도손대는동사 = 동사들.filter((v) => 계획손질.has(v)).join('|');
935
+ const 계획말고손대는동사 = 동사들.filter((v) => !계획손질.has(v)).join('|');
936
+ const 계획거리뒤 = `(?<!(?:${계획거리})[을를]?\\s{0,3}(?:(?:${끼는말})\\s{0,3}){0,2})`;
937
+ /*
938
+ * ── 빈칸을 넘어도 되는 어미와 안 되는 어미 ──────────────────────────────
939
+ *
940
+ * 「빈칸을 하나도 못 넘는다」 를 고치면서 사이를 `[^\n]{0,3}` 으로 열었다.
941
+ * 그런데 **어미마다 빈칸 사정이 다르다.**
942
+ *
943
+ * 빈칸을 넘어야 하는 것은 도움움직씨 '주다' 뿐이다 — 「고쳐 줘」 「고쳐
944
+ * 주세요」 는 맞춤법대로 띄어 쓴 꼴이다. '하자·해라·어라' 같은 씨끝은
945
+ * 줄기에서 절대 안 떨어진다.
946
+ *
947
+ * 그래서 전부에 빈칸을 열어 줬더니 이런 것이 걸렸다 —
948
+ *
949
+ * "추가 하자 있는 부분 알려줘" → 손대라는 말
950
+ *
951
+ * '하자' 가 씨끝이 아니라 **이름씨(결함)** 인데, 앞의 '추가' 와 빈칸으로만
952
+ * 떨어져 있어서 「추가하자」 로 읽혔다. 묻는 말이 고치라는 말이 됐다.
953
+ *
954
+ * ── 「고치지 마라」 는 고치라는 말이 아니다 ─────────────────────────────
955
+ *
956
+ * 이건 빈칸을 열기 **전부터** 있던 구멍이다(붙여 쓴 「고치지마라」 가 그때도
957
+ * 걸렸다). 사이가 세 글자라 '지 마' 가 통째로 들어앉고, 남은 '라' 가 시킴꼴
958
+ * 로 읽힌다. 하지 말라는 말이 하라는 말이 되는 것이라 뒤집힘이 제일 크다.
959
+ */
960
+ /*
961
+ * ── 맨 「라」 는 시킴꼴이기도 하고 서술격조사이기도 하다 ─────────────────
962
+ *
963
+ * "임시 파일 지워라" — 해라체 시킴꼴
964
+ * "이걸 개선이라 볼 수 있는지 검토해 줘" — 「개선이다」 의 '-이라'
965
+ *
966
+ * 뒤엣것이 시킴말로 읽혀서, 검토해 달라는 물음이 「고치라는 말」 이 됐다.
967
+ * 그러면 점검 모드가 후보에서 빠지고 화면에는 **거꾸로 된 까닭**이 뜬다 —
968
+ * 「고치라는 말이라 읽기 전용 모드로 안 보냄」. 고치라고 한 적이 없는데.
969
+ *
970
+ * 가르는 자리는 줄기의 종류다. '개선·구현·배포' 같은 한자어는 시킴꼴이
971
+ * 언제나 '…해라' 라 맨 '라' 가 붙을 일이 없다 — 붙었으면 그건 '-이라' 다.
972
+ * '지워·바꿔·고쳐' 같은 토박이 줄기만 맨 '라' 를 쓴다.
973
+ */
974
+ const 해붙는동사 = new Set(['추가', '삭제', '정리', '개선', '구현', '작성', '적용',
975
+ '설치', '배포', '리팩터링', '리팩터', '분리', '통일', '세팅', '수정']);
976
+ const 해붙는것 = 동사들.filter((v) => 해붙는동사.has(v)).join('|');
977
+ const 맨라 = `${해붙는것 ? `(?<!(?:${해붙는것})이?)` : ''}라(?=[\\s.!?~,]|$)`;
978
+ const 붙는끝 = `해라|하라|하자|해다오|할래|어라|아라|여라|${맨라}`;
979
+ const 띄는끝 = '줘|주세요|주라|줄래';
980
+ const 하지마 = '(?!\\S{0,2}지\\s*마|\\S{0,2}지\\s*말)';
981
+ /*
982
+ * 빈 갈래를 안 만든다. 위 두 목록은 손대는동사 를 갈라 낸 것이라, 목록을
983
+ * 손대다가 한쪽이 비면 `(?:)` 가 되고 **빈 글에 걸린다** — 그러면 모든 말이
984
+ * 손대라는 말이 되어 읽기 전용 모드가 통째로 사라진다. 아무 데서도 안 터지고
985
+ * 화면에도 안 나오는 꼴이라, 만들지 않는 쪽으로 막아 둔다.
986
+ */
987
+ const 손대라는말 = new RegExp(`(?:${[
988
+ 계획말고손대는동사 && `(?:${계획말고손대는동사})`,
989
+ 계획도손대는동사 && `${계획거리뒤}(?:${계획도손대는동사})`,
990
+ ].filter(Boolean).join('|')})${하지마}(?:\\S{0,3}(?:${붙는끝})|[^\\n]{0,3}(?:${띄는끝}))`);
744
991
 
745
992
  // 파일을 못 고치는 모드. modes.js 에서 쓰기 도구를 안 주는 것들과 같아야 한다.
746
993
  export const 읽기만하는모드 = new Set(['architect', 'plan', 'ask', 'inspect']);
package/src/tools/fig.js CHANGED
@@ -241,7 +241,7 @@ function 가지풀기(뿌리, 자식표, 줄들, 깊이, 셈) {
241
241
  * @returns {{ok:true, 갈래:'fig', 덩이들:[{이름,문단들}], 말:string[], 노드수:number}
242
242
  * |{ok:false, error:string, 끝났다?:boolean}}
243
243
  */
244
- export function readFig(경로또는버퍼) {
244
+ export function readFig(경로또는버퍼, { 최대노드 = 노드최대 } = {}) {
245
245
  let buf;
246
246
  try {
247
247
  buf = Buffer.isBuffer(경로또는버퍼) ? 경로또는버퍼 : readFileSync(경로또는버퍼);
@@ -341,7 +341,8 @@ export function readFig(경로또는버퍼) {
341
341
  const 문서 = 뿌리들.find((n) => n.type === 'DOCUMENT') ?? 뿌리들[0];
342
342
  const 쪽들 = (자식표.get(열쇠(문서?.guid)) ?? []).filter((n) => n.type === 'CANVAS');
343
343
 
344
- const 셈 = { 남은: 노드최대 };
344
+ const 상한노드 = Math.max(1, Number(최대노드) || 노드최대);
345
+ const 셈 = { 남은: 상한노드 };
345
346
  const 덩이들 = [];
346
347
 
347
348
  const 개요 = [];
@@ -355,11 +356,24 @@ export function readFig(경로또는버퍼) {
355
356
 
356
357
  if (쪽들.length) {
357
358
  for (const 쪽 of 쪽들) {
359
+ const 아이들 = 자식표.get(열쇠(쪽.guid)) ?? [];
358
360
  const 줄들 = [];
359
- for (const 아이 of 자식표.get(열쇠(쪽.guid)) ?? []) 가지풀기(아이, 자식표, 줄들, 0, 셈);
361
+ for (const 아이 of 아이들) 가지풀기(아이, 자식표, 줄들, 0, 셈);
362
+ /*
363
+ * 상한에 걸려 못 편 쪽을 「비어 있습니다」 라고 적으면 안 된다.
364
+ *
365
+ * 앞쪽 쪽에서 상한을 다 써 버리면 뒤쪽은 한 줄도 안 나온다. 그때 「비어
366
+ * 있다」 고 적으면, 시안에 멀쩡히 들어 있는 화면을 사람도 모델도 없는
367
+ * 것으로 읽는다 — 없는 화면을 새로 그리게 된다. 안 폈다고 말한다.
368
+ */
369
+ const 못편것 = !줄들.length && 아이들.length > 0;
360
370
  덩이들.push({
361
371
  이름: String(쪽.name ?? '').trim() || '이름 없는 쪽',
362
- 문단들: 줄들.length ? 줄들 : ['(이 쪽은 비어 있습니다.)'],
372
+ 문단들: 줄들.length
373
+ ? 줄들
374
+ : [못편것
375
+ ? `(도형 ${상한노드.toLocaleString('en-US')}개 상한에 걸려 이 쪽은 못 폈습니다 — 비어 있는 것이 아닙니다.)`
376
+ : '(이 쪽은 비어 있습니다.)'],
363
377
  });
364
378
  }
365
379
  } else {
@@ -370,7 +384,7 @@ export function readFig(경로또는버퍼) {
370
384
  }
371
385
 
372
386
  if (셈.남은 <= 0) {
373
- 말.push(`도형이 ${노드최대.toLocaleString('en-US')}개를 넘어 거기까지만 폈습니다 — 뒷부분은 안 실렸습니다`);
387
+ 말.push(`도형이 ${상한노드.toLocaleString('en-US')}개를 넘어 거기까지만 폈습니다 — 뒷부분은 안 실렸습니다`);
374
388
  }
375
389
 
376
390
  return { ok: true, 갈래: 'fig', 덩이들, 말, 노드수: 노드들.length };