deel-local-cli 1.18.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md 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-8%2C404%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/ko/develop.md)
23
+ [![tests](https://img.shields.io/badge/tests-9%2C093%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.18.0 ⌂ 이 안
304
+ deel 1.19.0 ⌂ 이 안
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.19.0](docs/ko/releases/1.19.md#1190) | 한쪽을 맞히면 반대쪽이 틀렸습니다 — 검사 9,093개로 양쪽을 같이 잽니다 |
1292
1293
  | [1.18.0](docs/ko/releases/1.18.md#1180) | 있는 것과 걸리는 것은 다릅니다 — 검사 8,404개가 초록인 채로 열려 있던 자리들 |
1293
1294
  | [1.17.9](docs/ko/releases/1.17.md#1179) | 한도에 걸렸는데 1초 뒤에 또 두드리고 있었습니다 |
1294
1295
  | [1.17.8](docs/ko/releases/1.17.md#1178) | 긴 지시문을 통째로 붙여 넣으면 시킨 것과 반대로 갔습니다 — 그리고 망이 한 번 끊기면 턴이 사라졌습니다 |
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-8%2C404%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/en/develop.md)
23
+ [![tests](https://img.shields.io/badge/tests-9%2C093%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.18.0 ⌂ inside
377
+ deel 1.19.0 ⌂ 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.19.0](docs/en/releases/1.19.md#1190) | Getting one side right broke the other — 9,093 checks that measure both sides |
1353
1354
  | [1.18.0](docs/en/releases/1.18.md#1180) | Having a rule is not the same as the rule firing — what was open while 8,404 checks stayed green |
1354
1355
  | [1.17.9](docs/en/releases/1.17.md#1179) | Rate-limited, and knocking again one second later |
1355
1356
  | [1.17.8](docs/en/releases/1.17.md#1178) | A long brief got the opposite of what it asked for — and one network blip threw the turn away |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deel-local-cli",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
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",
@@ -40,6 +40,223 @@
40
40
  * "점검 이력" 이 「점검 이」 로 걸리는 식으로 넓어진다 — 가르는 자리가
41
41
  * 미묘해서, 여는 것도 좁게 연다.
42
42
  */
43
+ /*
44
+ * ── 영어 시킴말 ─────────────────────────────────────────────────────────
45
+ *
46
+ * 여기 한 번만 적고 두 자리에서 쓴다 — 아래 code 점수표와, 파일 맨 아래의
47
+ * 손대라했나. 두 벌로 두면 한쪽만 늘어나고, 실제로 그렇게 어긋나 있었다.
48
+ *
49
+ * 손대라했나 는 build·create·make·develop·scaffold 를 다 알았다. 그런데
50
+ * code 점수표의 영어는 `implement|add|fix|write` 넷이 2점씩인 것과, 따로 선
51
+ * `rename` 3점이 전부였다. code 문턱은 3이라 **2점짜리 하나로는 절대 못
52
+ * 넘는다** — 무늬가 아는 동사 스무 개를 재 보니 `rename` 하나만 넘고,
53
+ * 저 넷이 2점, 나머지 열다섯이 0점이었다.
54
+ *
55
+ * "Build a complete work request and approval web application."
56
+ * 손대라했나 → 참 code 점수 → 0
57
+ *
58
+ * 그래서 영어로 시킨 일은 무슨 말이든
59
+ * 종합에 남았고 — 그것만이면 탈이 없다 — **긴 명세에서는 졌다.** 지나가던
60
+ * `error`·`failed` 가 6점이라, 만들라는 3,577자 지시문이 디버그로 갔다.
61
+ * 2차 리뷰가 「설계 낱말이 없는 명세는 그대로 디버그로 간다」 고 짚은 자리가
62
+ * 이것이고, 증상이 아니라 이쪽이 뿌리였다.
63
+ *
64
+ * 점수를 맞춰 두면 1·2등이 붙어서, 원래 있던 「비기면 안 고른다」 규칙이
65
+ * 알아서 받아 낸다. 새 울타리를 하나 더 세우는 것보다 이쪽이 낫다.
66
+ *
67
+ * 넓히되 자리는 그대로다. 첫머리에서만 본다 — 문장 가운데 낱말은 시킴말이
68
+ * 아니다. 「the build is broken」 이 code 로 가면 넓힌 것이 아니라 망가뜨린
69
+ * 것이다. 번호·목록 표시 뒤도 첫머리로 친다 — 붙여 넣는 명세가 그 꼴이다.
70
+ *
71
+ * ── 어디까지가 「첫머리」 인가 ───────────────────────────────────────────
72
+ *
73
+ * 처음엔 `[.!?
74
+ ]\s*` 였다. 마침표 뒤 **빈칸 0개**도 문장 첫머리로 친다는
75
+ * 뜻이었고, 3차 리뷰가 그 자리에서 제일 나쁜 것을 찾아냈다 —
76
+ *
77
+ * "Explain user.delete in detail." → code 모드
78
+ *
79
+ * `user.delete` 의 `delete` 가 시킴말이 되고, 설명해 달라는 말이 파일을
80
+ * 고치는 모드로 갔다. 묻기 점수는 4점이나 있었는데 「고치라는 말」 로 잡혀
81
+ * 후보에서 빠졌다 — 이 파일 맨 위가 걱정한 바로 그 모양이다.
82
+ *
83
+ * 그래서 셋을 죈다.
84
+ * · 마침표 뒤에 **빈칸이 하나는 있어야** 다음 문장이다.
85
+ * · 홑글자 뒤의 마침표는 문장 끝이 아니라 **약자**다 (`e.g. create`).
86
+ * · 시킴말 뒤에 실패말이 오면 그건 목적어가 아니라 **주어**다. 영어
87
+ * 시킴말 뒤에는 목적어가 오지 과거분사가 오지 않는다 —
88
+ * `Build failed with exit code 1.` 은 붙여 넣는 빌드 오류의 첫 줄이고,
89
+ * 그게 code 로 가면 고장 신고가 만들기 지시가 된다.
90
+ *
91
+ * 글 처음의 들여쓰기는 받아 준다(`^\s*`). 번호·목록 갈래는 처음부터
92
+ * 그랬는데 맨낱말 갈래만 안 그래서, 빈칸 두 칸으로 시작하는 지시문이
93
+ * 통째로 0점이었다.
94
+ *
95
+ * 정규식 리터럴로 적는다. 글자열을 이어 붙여 new RegExp 에 넣으면 홑따옴표
96
+ * 안에서 역빗금이 사라지고 낱말 경계가 백스페이스 글자가 된다 — 검사에서
97
+ * 잡히기 전까지 조용히 아무것도 안 맞는다. 실제로 그렇게 났다.
98
+ *
99
+ * ── 약자 목록에는 「뒤에 말이 이어지는 것」만 ───────────────────────────
100
+ *
101
+ * 8차 리뷰가 셋을 짚었고, 같은 부류를 하나 더 찾아 넷을 뺐다. 약자 목록을
102
+ * 넓히면서 **문장을 맺는 자리에 더 자주 오는 말**을 같이 넣어 버렸다.
103
+ *
104
+ * "No. Fix the bug." → `No.` 가 약자
105
+ * "Install git, node, etc. Build it." → `etc.` 가 약자
106
+ * "Ship it to Acme Inc. Create the report." → `Inc.` 가 약자
107
+ * "Sold to Acme Ltd. Build the invoice." → `Ltd.` 가 약자
108
+ *
109
+ * 넷 다 마침표가 진짜 문장 끝인데 약자로 읽혀 **뒤 문장이 통째로 죽었다.**
110
+ *
111
+ * 남길 것과 뺄 것을 가르는 것은 **그 마침표 뒤에 무엇이 오느냐**다.
112
+ * `e.g.` · `i.e.` · `Fig.` · `Dr.` · `Mr.` 는 뒤에 말이 이어지는 자리에 오는
113
+ * 약자라 마침표를 문장 끝으로 읽으면 안 된다. `No.` · `etc.` · `Inc.` ·
114
+ * `Ltd.` 는 반대로 **문장을 맺는 자리**에 훨씬 자주 온다 — 지키려던 것보다
115
+ * 잃는 것이 크면 그건 울타리가 아니라 벽이다. 그래서 뺀다.
116
+ *
117
+ * ── 공손말에 쉼표가 붙는다 ──────────────────────────────────────────────
118
+ *
119
+ * `please\s+` 는 빈칸만 받았다. 그런데 영어에서 제일 흔한 공손꼴이
120
+ * `Please, could you …` 다 — 쉼표가 하나 붙었다고 0점이 됐다.
121
+ * `please\s*,?\s+` 로 쉼표를 받아 준다. `could you` 뒤도 마찬가지다.
122
+ *
123
+ * ── 2번 갈래의 실패말은 로그꼴일 때만 막는다 ────────────────────────────
124
+ *
125
+ * 실패말 차단 목록이 두 갈래에서 달랐다. 1번 갈래(`build`·`create`…)는
126
+ * 이름씨꼴(`errors` · `failure` · `crash` · `timeout`)까지 막는데, 2번
127
+ * 갈래(`delete`·`rename`·`change`…)는 움직씨꼴만 막았다. 그래서
128
+ *
129
+ * "Delete error: permission denied" → 시킴말
130
+ *
131
+ * 같은 로그 첫 줄이 지시로 읽혔다. 그렇다고 2번 갈래에 이름씨꼴을 그냥
132
+ * 넣으면 안 된다 — `Fix errors.` · `Fix error in auth module.` 은 **진짜
133
+ * 지시**다. 1번 갈래와 달리 2번 갈래의 낱말들은 그 이름씨를 목적어로 받는다.
134
+ *
135
+ * 갈라 주는 것은 **뒤에 오는 쌍점·여는 괄호**다. 로그는 `error:` ·
136
+ * `crash (core dumped)` 꼴로 적히고, 지시문은 그렇게 안 적힌다.
137
+ * 그래서 2번 갈래의 이름씨꼴 차단은 `[:(]` 가 뒤따를 때만 건다.
138
+ * 그 목록에서 `broken` 이 빠져 있었다(9차 리뷰) — 1번 갈래는 아는 낱말인데
139
+ * 여기만 몰라서 `Rewrite broken: exit status 1` 이 지시로 읽혔다.
140
+ *
141
+ * ── 동사 뒤에 쌍점이 오면 그건 꼬리표다 ─────────────────────────────────
142
+ *
143
+ * 리뷰를 기다리는 사이에 붙여 넣을 법한 글 서른여덟 줄을 죽 재 봤다.
144
+ * 일곱이 시킴말로 읽혔고, 다섯이 한 가지 모양이었다.
145
+ *
146
+ * "fix: 값이 아닌 것을 가렸습니다" ← 이 저장소의 커밋 메시지다
147
+ * "Change-Id: I8f2c1a" git 꼬리표
148
+ * "Remove-Item : Cannot find path" 파워셸 오류
149
+ * "Update available: 1.2.3 → 1.3.0" npm 알림
150
+ * "add(a, b) { return a + b; }" 코드 한 줄
151
+ *
152
+ * 시킴말 뒤에는 **목적어**가 온다. 쌍점이 오면 그건 꼬리표고, 여는 괄호가
153
+ * 바로 붙으면 그건 부르는 짓이다. 위의 실패말 목록과 같은 생각인데,
154
+ * 저쪽은 「어떤 낱말이 오나」 를 보고 이쪽은 「어떤 글자가 오나」 를 본다.
155
+ *
156
+ * 쌍점은 **첫 낱말 바로 뒤**일 때만 꼬리표다. `Fix the bug: it is in the
157
+ * parser.` 처럼 목적어를 끼고 오는 쌍점은 그냥 지시문이다.
158
+ *
159
+ * ── 붙여 넣는 것이 로그만은 아니다 ─────────────────────────────────────
160
+ *
161
+ * SQL 도 시킴꼴로 시작한다.
162
+ *
163
+ * delete from users where id = 1;
164
+ * update accounts set balance = 0 where id = 3
165
+ *
166
+ * 「이 쿼리가 왜 이러냐」 고 물으려고 붙였는데 파일을 고치는 모드로 갔다.
167
+ * 손대라는 말로 읽히면 읽기 전용 모드가 후보에서 통째로 빠진다.
168
+ *
169
+ * 갈라 주는 것은 `from` 이 아니라 **뒤에 SQL 이 이어지느냐**다.
170
+ * `Delete from the list.` 는 그대로 지시문이고, `delete from users where`
171
+ * 는 아니다. 두 동사(`delete`·`update`)만 우리 목록과 겹치므로 그 둘에만
172
+ * 건다 — `insert`·`select` 는 애초에 여기 없다.
173
+ *
174
+ * ── 이음말 뒤도 첫머리다 ───────────────────────────────────────────────
175
+ *
176
+ * 같은 자리에서 반대쪽 구멍이 하나 나왔다.
177
+ *
178
+ * "First, set up the project. Then add tests." → 0점
179
+ *
180
+ * 붙여 넣는 명세가 딱 이 꼴인데 `First,` · `Then` 이 앞에 있다고 첫머리가
181
+ * 아니게 됐다. 번호·목록 표시를 첫머리로 치는 것과 같은 까닭으로 받는다.
182
+ * 이음말 뒤가 동사가 아니면(`Now the build is broken.`) 그대로 안 걸린다.
183
+ *
184
+ * ── 만들 것이 계획이면 그건 계획이다 ────────────────────────────────────
185
+ *
186
+ * 같은 뜻을 한국어·영어로 나란히 스물여섯 짝 재 보다 찾았다.
187
+ *
188
+ * "계획 세워 줘" → plan
189
+ * "Make a plan for this." → **code**
190
+ *
191
+ * 시킴꼴인 것은 맞다. 그런데 이 규칙이 손대라했나 의 영어 쪽이기도 해서,
192
+ * 걸리는 순간 읽기 전용 모드(plan·architect·ask·inspect)가 **후보에서
193
+ * 통째로 빠진다.** 계획을 달라는 말에 계획 모드가 남을 수가 없었다.
194
+ *
195
+ * 갈라 주는 것은 동사가 아니라 **목적어**다. 만들 것이 계획·로드맵·
196
+ * 전략이면 그건 계획 모드가 내놓는 것이지 파일을 고쳐서 되는 일이 아니다.
197
+ *
198
+ * ── 검사에 쓰던 낱말이 목록에 없었다 ────────────────────────────────────
199
+ *
200
+ * 8차 리뷰를 재 보다 찾았다. 검사 쪽이 `Rewrite what is left of the parser.`
201
+ * 를 **쓰기 지시문의 보기**로 쓰고 있는데, 정작 `rewrite` 가 목록에 없어
202
+ * 0점이었다. `write` 는 있지만 낱말 경계 때문에 `rewrite` 안에서는 안 걸린다.
203
+ *
204
+ * 0점이면 「읽기 점수가 안 붙는다」 가 언제나 참이라, 그 검사는 이 낱말에
205
+ * 대해 아무것도 안 재고 있었다. 고치는 것은 있는 자리에 넣는 일이다 —
206
+ * 다시 쓰는 것은 있는 것을 고치는 일이라
207
+ * 「고쳐 쓰는 말」 갈래다. 처음엔 손대는 갈래에 뒀는데, 그러면 계획 울타리를
208
+ * 못 받아 `Rewrite the plan.` 이 코드로 갔다(9차 리뷰) — `write` 와 짝이
209
+ * 맞아야 하는 자리다.
210
+ *
211
+ * 짓는 갈래(1번)와 고쳐 쓰는 갈래(2번)에 건다. 3번의 `implement`·`add` 는
212
+ * 반대다 — "Implement the plan we agreed on." 은 계획을 **실행**하라는
213
+ * 말이고, "Add a plan field to the schema." 의 plan 은 목적어가 아니라
214
+ * 딸린 이름씨다. 둘 다 코드가 맞다.
215
+ *
216
+ * ── 계획거리 뒤에 무엇이 와야 계획인가 ──────────────────────────────────
217
+ *
218
+ * 그 울타리를 「문장 끝 또는 전치사」 로만 열어 뒀더니, 때를 가리키는 말
219
+ * 한 마디만 붙어도 빠져나갔다(10차 리뷰).
220
+ *
221
+ * "Update the plan now." "Create a plan today." "Make a plan first."
222
+ *
223
+ * 때말 여덟과 **사이를 띄운 줄표**(`Make a plan - urgently`)를 같이 받는다.
224
+ * 줄표는 띄웠을 때만 구분자다 — `Update the plan-file.` 은 그대로 코드다.
225
+ * 계획거리 앞에 붙임표가 오는 꼴(`an action-plan`)도 계획거리로 친다.
226
+ *
227
+ * ── 실패말이 이름의 일부면 그건 로그가 아니다 ───────────────────────────
228
+ *
229
+ * 실패말 목록은 두 층이다. 움직씨꼴(`failed`·`crashed`)은 뒤에 아무 문장
230
+ * 부호나 와도 로그로 치고, 이름씨꼴(`errors`·`broken`)은 1번 갈래에서만
231
+ * 막았다. 그런데 그 이름씨 층이 마침표와 붙임표를 통째로 구분자로 쳤다.
232
+ *
233
+ * "Build errors.json" → 0점 `.json` 의 점을 문장 끝으로 읽었다
234
+ * "Create error-handler.js" → 0점 붙임표를 로그 구분자로 읽었다
235
+ * "Update crash-report.md" → 0점
236
+ *
237
+ * 이 파일 위쪽이 마침표에서 배운 것과 같다 — **점 뒤에 낱말이 붙어 있으면
238
+ * 그건 문장 끝이 아니다.** 줄표도 사이를 띄웠을 때만 구분자다.
239
+ *
240
+ * 반대쪽으로는 2·3번 갈래가 쌍점과 여는 괄호만 막고 있어서, 같은 로그가
241
+ * 어느 동사로 시작하느냐로 갈렸다.
242
+ *
243
+ * 그 자리를 줄표로만 메웠더니 마침표와 전치사꼴이 남았다(11차 리뷰).
244
+ *
245
+ * "Rewrite broken with exit code 1" "Modify broken."
246
+ *
247
+ * `broken` 은 이름씨가 아니라 **그림씨**다. `Fix errors.` 는 오류를
248
+ * 고치라는 말이지만 `Modify broken.` 은 아무 말도 아니다 — 세 갈래에서
249
+ * 다 1번 갈래와 같은 잣대를 받는다. 뒤에 이름씨가 오면(`Fix broken
250
+ * tests.`) 그건 여전히 꾸밈말이라 그대로 지시문이다.
251
+ *
252
+ * "Build broken - exit status 1" → 안 걸림 (1번 갈래)
253
+ * "Rewrite broken - exit status 1" → **걸림** (2번 갈래)
254
+ *
255
+ * 세 갈래에 같은 「띄운 줄표」 잣대를 넣어 맞춘다.
256
+ */
257
+ const 영어시킴말 =
258
+ /(?:^\s*|(?<!\b(?:e\.g|i\.e|vs|cf|approx|Fig|Dr|Mr|Mrs|Ms|St|Jr|Sr))[.!?]\s+|\n\s*|^\s*\d+[.)]\s*|^\s*[-*]\s*)(?:(?:first|then|next|after\s+that|also|now|finally|second|third|lastly|afterwards?)\s*,?\s+)?(?:please\s*,?\s+)?(?:(?:could|can|would|will)\s+(?:you|we)\s*,?\s+)?(?:please\s*,?\s+)?(?:(?:build|make|create|update|write|migrate|generate|set\s*up)\b(?![\s:-]+(?:(?:failed|failing|fails|errored|broke|crashed|succeeded|succeeds|timed[\s-]+out)\b(?=\s*(?:[.,:;!?()\[\]-]|$)|\s+(?:with|at|after|in|on|to|before|during|because|due|while|when|for|since|from)\b)|(?:failures?|errors?|broken|crash|crashes|timeouts?)\b(?=\s*(?:[,:;!?()\[\]]|$)|\s*\.(?!\w)|\s+-\s|\s+(?:with|at|after|in|on|to|before|during|because|due|while|when|for|since|from)\b)))(?!(?:[ \t]+[\w'-]+){0,3}[ \t]+(?:[\w']+-)*(?:plans?|roadmaps?|strateg(?:y|ies)|outlines?)(?=[ \t]*(?:[!?,;:)]|\.(?!\w)|$)|[ \t]+-[ \t]|[ \t]+(?:now|today|tonight|tomorrow|first|soon|asap|again)\b|[ \t]+(?:for|to|of|on|about|around|that|which|we|with|before|after|regarding|covering)\b))|(?:modify|change|develop|rewrite)\b(?![\s:-]+(?:(?:failed|failing|fails|errored|broke|crashed|succeeded|succeeds|timed[\s-]+out)\b(?=\s*(?:[.,:;!?()\[\]-]|$)|\s+(?:with|at|after|in|on|to|before|during|because|due|while|when|for|since|from)\b)|broken\b(?=\s*(?:[,:;!?()\[\]]|$)|\s*\.(?!\w)|\s+-\s|\s+(?:with|at|after|in|on|to|before|during|because|due|while|when|for|since|from)\b)|(?:failures?|errors?|crash|crashes|timeouts?)\b(?:\s*[:(]|\s+-\s)))(?!(?:[ \t]+[\w'-]+){0,3}[ \t]+(?:[\w']+-)*(?:plans?|roadmaps?|strateg(?:y|ies)|outlines?)(?=[ \t]*(?:[!?,;:)]|\.(?!\w)|$)|[ \t]+-[ \t]|[ \t]+(?:now|today|tonight|tomorrow|first|soon|asap|again)\b|[ \t]+(?:for|to|of|on|about|around|that|which|we|with|before|after|regarding|covering)\b))|(?:implement|add|fix|refactor|remove|delete|rename|scaffold)\b(?![\s:-]+(?:(?:failed|failing|fails|errored|broke|crashed|succeeded|succeeds|timed[\s-]+out)\b(?=\s*(?:[.,:;!?()\[\]-]|$)|\s+(?:with|at|after|in|on|to|before|during|because|due|while|when|for|since|from)\b)|broken\b(?=\s*(?:[,:;!?()\[\]]|$)|\s*\.(?!\w)|\s+-\s|\s+(?:with|at|after|in|on|to|before|during|because|due|while|when|for|since|from)\b)|(?:failures?|errors?|crash|crashes|timeouts?)\b(?:\s*[:(]|\s+-\s))))(?!\()(?![-\w]*[ \t]*:)(?![ \t]+[\w.-]+[ \t]*:[ \t])(?!(?<=\bdelete)\s+from\s+[\w."`]+\s*(?:where\b|order\s+by\b|limit\b|;|$))(?!(?<=\bupdate)\s+[\w."`]+\s+set\b)/im;
259
+
43
260
  export const 표 = {
44
261
  debug: [
45
262
  // 고장은 신호가 뚜렷하다. 그리고 디버그는 파일을 고칠 수 있으므로
@@ -48,6 +265,9 @@ export const 표 = {
48
265
  [/작동\s*안/, 4], [/동작\s*안/, 4],
49
266
  [/에러|오류|예외|익셉션/, 4], [/버그/, 4],
50
267
  [/실패/, 3], [/터(져|졌|진다)/, 3], [/죽(어|었|는다|네)/, 3],
268
+ // 「깨지다」 는 검사·빌드가 무너졌을 때 한국어에서 제일 흔한 말인데
269
+ // 터지다·죽다만 있고 이것이 빠져 있었다(같은 뜻 짝 재기).
270
+ [/깨(져|졌|진다|지네|지는|진)/, 3],
51
271
  [/멈(춰|췄|춘다|추)/, 3], [/먹통/, 3], [/이상(해|하다|한데)/, 3],
52
272
  [/재현/, 3], [/스택\s*트레이스|stack\s*trace|traceback/i, 4],
53
273
  // 'TypeError' 처럼 앞에 글자가 붙어 오는 일이 훨씬 흔하다.
@@ -100,6 +320,15 @@ export const 표 = {
100
320
  [/검토(해|\s*좀|를)/, 5], [/리뷰(해|\s*좀)?/, 5],
101
321
  // '감사합니다' 는 안 걸린다 — '합' 은 '해·하' 가 아니다.
102
322
  [/감사(를)?\s*(해|하)/, 4], [/훑어\s*(봐|보고|줘)/, 4],
323
+ /*
324
+ * 「살펴보다」 는 영어 `inspect` 와 같은 뜻인데 점수표에 없었다. 영어
325
+ * 쪽은 5점이라 "Inspect this code." 는 inspect 로 가고 "이 코드 살펴봐
326
+ * 줘" 는 종합에 남았다 — 한국어를 먼저 쓰는 도구에서 방향이 거꾸로다.
327
+ *
328
+ * 앞에 「원인」 이 붙으면 뺀다. 그건 고장을 쫓는 말이라 debug 자리고,
329
+ * 여기로 보내면 파일을 못 고치는 모드에서 원인만 듣고 끝난다.
330
+ */
331
+ [/(?<!(?:원인|까닭|이유)[을이은는]?\s{0,2}(?:좀\s{0,2})?)살펴\s*(봐|보고|보자|봐야|주)/, 5],
103
332
  [/취약(점|한|성)/, 5], [/보안\s*(점검|검토|문제|취약|구멍)/, 5],
104
333
  [/경합|레이스\s*컨디션|race\s*condition/i, 5],
105
334
  [/데이터\s*(손실|유실)|덮어쓰(기|면)|유실/, 4],
@@ -109,7 +338,36 @@ export const 표 = {
109
338
  [/가능성(이|을)?\s*있는/, 3],
110
339
  [/문제(가|를|점)?\s*(있|찾|없|만)/, 3],
111
340
  [/찾아\s*(줘|주세요|봐|보)/, 2],
112
- [/\baudit\b|\breview\b|\banaly[sz]e\b|\binspect\b|\bvulnerab/i, 4],
341
+ /*
342
+ * ── 읽기 쪽 영어도 같은 무게로 ──────────────────────────────────
343
+ *
344
+ * 쓰기 쪽 낱말 구멍(위 code 표)을 메우고도 **읽기 쪽은 그대로였다.**
345
+ * 같은 뜻을 한국어로 하면 여기로 오고 영어로 하면 종합에 남았다 —
346
+ * 여덟 짝을 재 보니 다섯이 어긋났다.
347
+ *
348
+ * "이 코드 검토해 줘" → inspect
349
+ * "Review this code." → 종합
350
+ *
351
+ * 까닭은 무게였다. 한국어 '점검·분석·검토·리뷰·취약' 은 낱낱이 5점
352
+ * 규칙인데, 영어는 다섯 낱말이 **4점 한 덩이**로 묶여 있었다. 문턱에
353
+ * 하나 모자라고, 여럿이 겹쳐도 한 번만 세어진다.
354
+ *
355
+ * 그래서 한국어와 같은 모양으로 쪼개고 같은 무게를 준다. `audit` 만
356
+ * 4점인 것은 한국어 '감사' 가 4점이라 그렇다 — 짝을 맞춘 것이다.
357
+ */
358
+ /*
359
+ * 활용형까지 받는다. `\b` 로 딱 닫아 두니 `Analyzing` · `reviews` ·
360
+ * `inspection` 이 0점이었다(7차 리뷰). 한국어는 '분석해·분석을·분석이'
361
+ * 를 다 받는데 영어만 원형 하나였다.
362
+ *
363
+ * `audit` 도 5점으로 올린다. 4점으로 둔 까닭을 「한국어 '감사' 가
364
+ * 4점이라」 고 적었는데, 한국어 '감사' 가 낮은 것은 **인사말과 겹뜻**
365
+ * 이어서다. 영어 audit 에는 그 겹뜻이 없다 — 엉뚱한 근거였다.
366
+ */
367
+ [/\breview(?:s|ing|ed)?\b|\binspect(?:s|ing|ion|ions|ed)?\b/i, 5],
368
+ [/\banaly[sz]e(?:s|d)?\b|\banaly[sz]ing\b|\banalysis\b/i, 5],
369
+ [/\bvulnerab/i, 5],
370
+ [/\baudit(?:s|ing|ed)?\b/i, 5],
113
371
  [/\bdata\s*loss\b|\bdeadlock\b|\blost\s*update\b/i, 4],
114
372
  ],
115
373
 
@@ -129,10 +387,99 @@ export const 표 = {
129
387
  [/뭐\s*하(는|나|니|냐)/, 3], [/무슨\s*(일|역할)/, 3],
130
388
  [/설명(해|\s*좀|을)/, 5], [/알려\s*(줘|주세요|줄래)/, 3],
131
389
  [/어떻게\s*(동작|작동|돌아가)/, 5],
390
+ /*
391
+ * 「어떻게 …?」 는 영어 `how to` 와 같은 물음인데 '동작·작동·돌아가'
392
+ * 셋만 알고 있었다. 쓰는 법을 묻는 말이 통째로 0점이었다.
393
+ *
394
+ * 손대는 낱말은 안 넣는다 — "어떻게 고쳐?" 는 고쳐 달라는 말이지
395
+ * 설명해 달라는 말이 아니다.
396
+ */
397
+ [/어떻게\s*(쓰|사용|실행|돌리|켜|끄|설정|설치|연결|고르|정하|불러|읽)/, 5],
132
398
  [/무슨\s*(뜻|의미)|의미(가|는)\s*뭐/, 5],
133
399
  [/차이(가|는|점)/, 3],
134
400
  [/궁금/, 3], [/왜\s*이렇게\s*(만들|짰|되어)/, 3],
135
- [/\bwhat\s+is\b|\bexplain\b|\bhow\s+does\b/i, 4],
401
+ /*
402
+ * 여기도 마찬가지다. 한국어 '설명·뭐야·무엇·어떻게 동작' 이 5점인데
403
+ * 영어는 세 낱말이 4점 한 덩이였다. 짝을 맞춰 쪼갠다.
404
+ *
405
+ * 묻는 말꼴도 넓힌다 — `what is` 만 알고 `what does … do` 를 몰라서
406
+ * "What does this function do?" 가 0점이었다. 한국어 '뭐 하는 거야'
407
+ * 는 5점으로 아는 말이다.
408
+ */
409
+ /*
410
+ * ── 묻는 말은 **문장 첫머리**에 있어야 묻는 말이다 ────────────────
411
+ *
412
+ * 넓히자마자 반대쪽이 샜다(7차 리뷰).
413
+ *
414
+ * "Fix what is broken in route.js" → ask 5점
415
+ *
416
+ * 고치라는 말이 물음과 비겨서 종합에 떨어졌다. 한국어 '뭐야' 규칙은
417
+ * 줄 끝(`\s*\??$`)에 못박혀 있어서 이런 일이 없다 — 영어도 첫머리로
418
+ * 못박는다. 물음표로 끝나는 문장은 낱말을 안 가리고 다 받는다.
419
+ *
420
+ * `explain` 은 첫머리가 아니어도 된다 — "Explain how …" 처럼 그 자체가
421
+ * 시킴말이고, 한국어 '설명해' 도 자리를 안 가린다.
422
+ *
423
+ * ── 「말해 줘」 부류에는 첫머리 못이 있어야 한다 ──────────────────
424
+ *
425
+ * 9차 리뷰. `tell me` · `show me` · `walk me through` 를 자리 안 가리고
426
+ * 넣었더니, 쓰기 지시문의 딸린 마디가 물음이 됐다.
427
+ *
428
+ * "Rewrite this function to show me the result." → ask 5점
429
+ * "Write the docs and tell me what changed." → ask 5점
430
+ *
431
+ * `explain` 과 다르다. `explain` 은 그 자체가 문장의 시킴말이라 어디
432
+ * 있든 묻는 말인데, 이 셋은 **딸린 마디에 더 자주 온다.** 그래서 못을 박는다.
433
+ *
434
+ * 이름씨 `explanation` 은 아예 뺀다. 8차에 사소 하나(「이름씨꼴이
435
+ * 없다」)를 메우려고 넣었는데, 그 값으로 `"Add an explanation to the
436
+ * README."` 가 물음이 됐다 — 사소를 메우려다 심각을 냈다. 첫머리에
437
+ * 못을 박으면 정작 쓸모 있는 "Give an explanation of …" 가 죽고,
438
+ * 안 박으면 위가 샌다. 얻는 것보다 잃는 것이 크면 안 넣는 것이 맞다.
439
+ *
440
+ * ── 못박아 놓고 한쪽만 못박혔다 ──────────────────────────────────
441
+ *
442
+ * 8차 리뷰. `how` 규칙을 `(첫머리)how…\?|how (to|does|…)` 로 적었다.
443
+ * 세로줄은 **규칙 전체를 가른다.** 그래서 둘째 갈래에는 첫머리 못이
444
+ * 아예 안 박혔고, 바로 위 주석이 막았다는 그 자리가 그대로 열려 있었다.
445
+ *
446
+ * "Rewrite how to build it." → ask 5점, **ask 모드**
447
+ *
448
+ * 고치라는 말이 **파일을 못 고치는 모드**로 갔다. 7차에 막은 것보다
449
+ * 한 걸음 더 나쁘다 — 그때는 비겨서 종합에 떨어졌을 뿐이었다.
450
+ * 갈래를 괄호로 묶어 못을 둘 다에 박는다.
451
+ *
452
+ * 첫머리에 `^\s*` 를 준다. 쓰기 갈래는 이미 그런데 여기만 안 그래서,
453
+ * 빈칸 두 칸으로 시작하는 물음이 0점이었다.
454
+ *
455
+ * ── 이름 안의 마침표는 문장 끝이 아니다 ──────────────────────────
456
+ *
457
+ * "What parameters does route.js take?" → 0점
458
+ *
459
+ * 물음표 갈래를 `[^.!?\n]*\?` 로 적어서 `route.js` 의 마침표에서
460
+ * 끊겼다. 마침표 뒤에 낱말 글자가 붙어 있으면 그건 문장 끝이 아니라
461
+ * 이름의 일부다 — 이 파일 위쪽 영어시킴말이 이미 아는 이야기다.
462
+ *
463
+ * ── 묻는 꼴 세 가지가 더 있었다 ──────────────────────────────────
464
+ *
465
+ * · `What <이름씨> does …` 보조동사가 `what` 바로 뒤에만 오는 줄
466
+ * 알아서, 물음표가 없으면 통째로 0점이었다.
467
+ * · `How's …` `what` 에는 준 줄임꼴을 `how` 에는 안 줬다.
468
+ * · 거꾸로 `how` 에만 준 `to` · `can` · `should` 를 `what` 에는 안
469
+ * 줬다(9차 리뷰). "What to run next" · "What should I run first" 가
470
+ * 물음표 없이는 0점이었다.
471
+ * · 이름 안 마침표 예외를 `\.(?=\w)` 로 적어 유닉스 상대 경로
472
+ * (`./route.js`)가 빠졌다. 슬래시도 낱말 글자로 친다.
473
+ * · `explanation` `inspect` 는 이름씨꼴까지 받는데 여기만 안 받았다.
474
+ *
475
+ * 그리고 `Tell me how …` 는 첫머리 못 때문에 0점인데, 이건 자리
476
+ * 문제가 아니라 **낱말 문제**다. 「말해 줘」 는 `explain` 과 같은 부류의
477
+ * 물음 시킴말이라 그쪽에 넣는다 — 못을 뽑으면 위의 `Rewrite how to` 가
478
+ * 도로 열린다.
479
+ */
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],
136
483
  [/읽어(만)?\s*보고\s*(설명|알려)/, 4],
137
484
  ],
138
485
 
@@ -157,11 +504,50 @@ export const 표 = {
157
504
  [/구현(해|\s*좀)/, 5],
158
505
  [/바꿔(줘|주세요)?|변경(해|\s*좀)/, 3],
159
506
  [/지워(줘)?|삭제(해|\s*좀)/, 3],
160
- [/이름\s*바꿔|리네임|rename/i, 3],
507
+ /*
508
+ * 영어 `rename` 은 여기서 뺐다. 첫머리 갈래가 이미 5점을 주므로
509
+ * `Rename the file.` 만 8점(5+3)이 되어, 5차에서 고친 동사 어긋남이
510
+ * 여기 하나 남아 있었다. `Rename failed - file in use` 도 3점을 받아
511
+ * 로그가 code 문턱에 닿았다(6차 리뷰). 문장 가운데 갈래로 옮긴다.
512
+ */
513
+ [/이름\s*바꿔|리네임/, 3],
161
514
  [/옮겨(줘)?/, 2],
162
515
  [/리팩터(링)?(해|\s*좀)/, 3],
163
- [/\bimplement\b|\badd\b|\bfix\b|\bwrite\b/i, 2],
516
+ /*
517
+ * 첫머리 시킴말. 한국어의 '구현해'(5점)와 같은 무게다 — 같은 말을
518
+ * 어느 말로 했느냐로 모드가 갈리면 안 된다.
519
+ *
520
+ * 증거가 세서 세게 준다. 이 규칙은 낱말 하나를 보는 것이 아니라
521
+ * **문장 첫머리 + 골라 둔 동사 + 실패말이 안 따라옴**을 다 요구한다.
522
+ * `error` 같은 맨낱말(3점)보다 훨씬 좁게 잡힌 규칙이다.
523
+ *
524
+ * 4점이었을 때 남은 자리가 있었다. 설계 낱말이 하나도 없는 긴 명세에서
525
+ * 시킴말 하나(4)가 지나가는 `error`·`failed` 둘(6)에게 졌다.
526
+ * 5점이면 1·2등이 붙고, 원래 있던 「비기면 안 고른다」 규칙이 받아 낸다 —
527
+ * 새 울타리를 세우지 않고 눈금만 맞추는 것이 이 자리의 고침이다.
528
+ */
529
+ [영어시킴말, 5],
530
+ /*
531
+ * 문장 가운데의 시킴말. 첫머리 갈래(5점)와 **겹치지 않게** 앞을 막는다.
532
+ * 안 막았더니 `Fix the web app.` 은 7점(5+2)이고 `Build the web app.` 은
533
+ * 5점이었다 — 같은 뜻인데 어느 동사를 골랐느냐로 점수가 갈렸다. 긴
534
+ * 명세에서는 그 2점이 모드를 바꾼다(하나는 code, 하나는 종합).
535
+ *
536
+ * 그래서 **앞에 낱말이 있어야** 이 규칙이 돈다. `please` 는 첫머리
537
+ * 갈래가 이미 세는 말이라 따로 뺀다.
538
+ *
539
+ * 빼는 자리에도 쉼표를 받는다. 첫머리 갈래가 `Please, fix …` 를 세게
540
+ * 되자 여기가 또 2점을 얹어 `Please, build …`(5) 와 갈렸다 —
541
+ * 겹침을 막는 자리는 겹치는 쪽과 **같은 꼴**을 봐야 한다.
542
+ */
543
+ [/(?<=[a-z,;:)\]]\s{1,3})(?<!\b(?:could|can|would|will)\s{1,3}(?:you|we)[\s,]{1,4})(?<!\bplease[\s,]{1,4})(?:implement|add|fix|write|rename)\b/i, 2],
164
544
  [/써(줘|주세요)/, 3],
545
+ /*
546
+ * 「짜다」 는 코드·검사를 만들라는 한국어에서 제일 흔한 말인데
547
+ * '써 줘' 만 있었다. 계획·순서·구조를 짜는 것은 각각 plan·architect
548
+ * 규칙이 이미 더 세게 잡으므로 여기서 따로 뺄 것이 없다.
549
+ */
550
+ [/짜\s?(줘|주세요|봐|보자)/, 3],
165
551
  ],
166
552
  };
167
553
 
@@ -382,8 +768,8 @@ export const 읽기만하는모드 = new Set(['architect', 'plan', 'ask', 'inspe
382
768
  * 안에서 `\s` 의 역슬래시가 사라지고 `\b` 는 백스페이스 글자가 된다 —
383
769
  * 검사에서 잡히기 전까지 조용히 아무것도 안 맞는다. 실제로 그렇게 났다.
384
770
  */
385
- const 영어손대라 =
386
- /(?:^|[.!?\n]\s*|^\s*\d+[.)]\s*|^\s*[-*]\s*)(?:please\s+)?(?:build|implement|create|write|make|add|fix|refactor|remove|delete|rename|migrate|update|modify|change|generate|scaffold|set\s+up|develop)\b/im;
771
+ // 위에서 세운 공용 무늘 그대로다. 두 벌로 두면 한쪽만 늘어난다.
772
+ const 영어손대라 = 영어시킴말;
387
773
 
388
774
  /** 파일을 손대라는 말이 들었나. */
389
775
  export function 손대라했나(text) {
@@ -459,21 +845,73 @@ export function route(text) {
459
845
  const 더줘야 = 긴글 ? 2 : 0;
460
846
 
461
847
  const [으뜸, 점수] = 순위[0] ?? [null, 0];
462
- if (!으뜸 || 점수 < (문턱[으뜸] ?? 4) + 더줘야) {
848
+
849
+ /*
850
+ * ── 눈금을 올리는 것으로는 두 번째 낱말을 못 막는다 ────────────────────
851
+ *
852
+ * 위의 +2 는 **낱말 하나**를 막는다. 실제 벤치마크 지시문을 그대로 넣어
853
+ * 보니 두 개가 걸렸다 — 둘 다 증상이 아니라 요구사항 조건절이었다.
854
+ *
855
+ * "… handle duplicate requests, stale requests, unexpected server errors." +3
856
+ * "… A failed operation must not leave state silently inconsistent." +3
857
+ *
858
+ * 합이 6이라 3+2 를 넘어 디버그가 됐다. 고장 난 것이 하나도 없는 **빈
859
+ * 폴더**에서 시작하라는 글인데, 디버그 모드는 「증상을 한 문장으로 다시
860
+ * 적어라 · 재현 방법부터 확보해라」 를 시킨다. 없는 증상을 찾게 된다.
861
+ *
862
+ * 눈금을 또 올려도 세 낱말이면 그대로 뚫린다. 눈금은 글 길이만 보고
863
+ * 정하는데, 지나가는 낱말은 길이가 아니라 **글의 종류**를 따라 늘기
864
+ * 때문이다. 그래서 눈금이 아니라 견줌으로 본다.
865
+ *
866
+ * 같은 글에서 설계가 8점, 디버그가 6점이었다. 「고치라는 말이라 설계로는
867
+ * 안 보낸다」 는 그대로 맞다 — 그런데 그것은 **설계로 보내지 말라**는 말
868
+ * 이지, 그 글이 디버그라는 말이 아니다. 뺀 것이 남은 으뜸보다 세다면 이
869
+ * 글이 무엇인지는 아직 안 정해진 것이다. 종합에 둔다 — 종합은 걸음마다
870
+ * 단계를 따라가므로(agent/단계.js) 계획에서 시작해 만들기로 넘어간다.
871
+ *
872
+ * 짧은 말에는 안 건다. 짧은 말에서는 낱말 하나가 곧 뜻이다.
873
+ */
874
+ const 뺀최고 = Math.max(0, ...뺀것.map((m) => 점수들[m] ?? 0));
875
+ const 지나가는말 = 긴글 && Boolean(으뜸) && 점수 < 뺀최고;
876
+
877
+ // 골랐어야 할 만한 신호가 있었는데 일부러 안 보낸 자리인가.
878
+ const 일부러 = Boolean(지나가는말 || 뺀것.length
879
+ || (긴글 && 으뜸 && 점수 >= (문턱[으뜸] ?? 4)));
880
+
881
+ if (!으뜸 || 지나가는말 || 점수 < (문턱[으뜸] ?? 4) + 더줘야) {
463
882
  // 왜 안 바꿨는지는 화면에 그대로 뜬다. "설계로 갈 뻔했는데 고치라는 말이라
464
883
  // 안 보냈다" 와 "그냥 모르겠다" 는 사람에게 아주 다른 말이다.
465
- const 왜 = 긴글 && 으뜸 && 점수 >= (문턱[으뜸] ?? 4)
466
- ? `글이 길어 ${으뜸} 신호 하나로는 정함 — 종합에서 단계를 따라갑니다`
467
- : 뺀것.length
468
- ? `${뺀것.join('·')} 신호가 있었지만 고치라는 말이라 읽기 전용 모드로 안 보냄`
469
- : '무슨 일인지 뚜렷하지 않음';
470
- return { mode: null, score: 점수, why: 왜, 겹침: false, 점수들, 뺀것 };
884
+ const 왜 = 지나가는말
885
+ ? `글이 길고 ${뺀것.join('·')}(${뺀최고}점) ${으뜸}(${점수}점) 보다 세서 `
886
+ + `${으뜸} 로 안 봄 — 종합에서 단계를 따라갑니다`
887
+ : 긴글 && 으뜸 && 점수 >= (문턱[으뜸] ?? 4)
888
+ ? `글이 길어 ${으뜸} 신호 하나로는 안 정함 — 종합에서 단계를 따라갑니다`
889
+ : 뺀것.length
890
+ ? `${뺀것.join('·')} 신호가 있었지만 고치라는 말이라 읽기 전용 모드로 안 보냄`
891
+ : '무슨 일인지 뚜렷하지 않음';
892
+ /*
893
+ * ── 「일부러 안 골랐다」 와 「그냥 모르겠다」 는 다른 말이다 ─────────
894
+ *
895
+ * 바로 위 주석이 그 구분을 적어 뒀는데, 정작 화면에는 **둘 다 아무것도
896
+ * 안 떴다.** 부르는 쪽(repl.js · oneshot.js)이 `if (골라진.mode)` 안에서만
897
+ * 찍었기 때문이다. 여기서 지은 세 갈래 까닭은 한 번도 사람에게 안 갔다.
898
+ *
899
+ * 그렇다고 전부 찍으면 안 된다. 보통 한마디는 거의 다 여기로 오고 까닭은
900
+ * 「무슨 일인지 뚜렷하지 않음」 이다 — 그건 소음이다.
901
+ *
902
+ * 그래서 **가르는 일을 여기서 한다.** 부르는 쪽이 `뺀것.length` 니
903
+ * `글이 길` 이니를 다시 헤아리게 두면 두 자리에서 따로 어긋난다.
904
+ */
905
+ return { mode: null, score: 점수, why: 왜, 겹침: false, 점수들, 뺀것, 일부러 };
471
906
  }
472
907
 
473
908
  // 1·2등이 붙으면 고르지 않는다. 반반인 것을 억지로 정하면 절반은 틀린다.
474
909
  const 둘째 = 순위[1]?.[1] ?? 0;
475
910
  if (점수 - 둘째 < 2 && 둘째 >= (문턱[순위[1][0]] ?? 4)) {
476
- return { mode: null, score: 점수, why: `${으뜸} 과 ${순위[1][0]} 가 비슷함`, 겹침: false, 점수들 };
911
+ return {
912
+ mode: null, score: 점수, why: `${으뜸} 과 ${순위[1][0]} 가 비슷함`,
913
+ 겹침: false, 점수들, 뺀것, 일부러: true,
914
+ };
477
915
  }
478
916
 
479
917
  return {
@@ -565,14 +565,88 @@ export class Session {
565
565
  /** 모델이 읽는 시스템 글 전체. 조각을 그대로 이어 붙인 것이다. */
566
566
  systemPrompt() { return this.시스템조각().join(''); }
567
567
 
568
- // 프롬프트에 실제로 올릴 스킬: 가까운 자리(프로젝트 > 사용자 > 플러그인) 순으로 상한까지.
568
+ /*
569
+ * 프롬프트에 실제로 올릴 스킬: 가까운 자리부터 상한까지.
570
+ *
571
+ * ── 갈래 하나를 안 적었다 ────────────────────────────────
572
+ *
573
+ * 자리표가 `{ project, user, plugin }` 이었고 거기에 **`builtin` 이 없었다.**
574
+ * 그래서 내장 스킬은 `?? 맨뒤` 로 떨어져 모르는 갈래와 한 덩어리가
575
+ * 됐고, 앞이 40칸을 채우면 통째로 잘렸다.
576
+ *
577
+ * 빈 폴더에서 그대로 재 봤다. 시스템 프롬프트 9,399자 중 6,700자가
578
+ * 스킬 목록이었는데 그 40개 안에 내장은 **한 개도 없었다** — 사용자
579
+ * 것 11개와 남의 플러그인 29개였다. 잘린 일곱은 하필 deel 이 무엇인지를
580
+ * 적어 둔 것들이다 — 검사-먼저 · 끝까지-하기 · 스스로-검토 …
581
+ * 플러그인을 많이 까은 PC 일수록 deel 은 조용히 제 방법을 잃었다.
582
+ *
583
+ * 그래서 둘을 한다. 자리표에 `builtin` 을 적고, **먼저 떼어 둔다.**
584
+ * 자리만 주면 사용자 스킬이 40개를 넘는 PC 에서 같은 구멍이 다시 생긴다 —
585
+ * 숫자를 올리는 것으로는 막을 수 없는 부류다. 내장은 일곩 뿐이고
586
+ * deel 의 방법 그 자체라, 그 일곩은 자리를 따로 매어 둔다.
587
+ */
569
588
  listedSkills() {
570
- const rank = { project: 0, user: 1, plugin: 2 };
571
- return this.skills
572
- .filter((s) => s.enabled)
573
- .slice()
574
- .sort((a, b) => (rank[a.source] ?? 3) - (rank[b.source] ?? 3))
575
- .slice(0, this.maxSkillsListed);
589
+ const rank = { project: 0, user: 1, builtin: 2, plugin: 3 };
590
+ const 자리 = (x) => rank[x.source] ?? 9;
591
+ const 줄세운것 = this.skills.filter((s) => s.enabled).slice()
592
+ .sort((a, b) => 자리(a) - 자리(b));
593
+ /*
594
+ * 내장 몫은 **상한의 1/4 까지**다.
595
+ *
596
+ * 처음에는 상한까지 무조건 먼저 뗐다. 그랬더니 살리려던 것이 굶기는
597
+ * 쪽이 됐다 — 2차 리뷰가 재 준 자리다.
598
+ *
599
+ * 상한 10 · project 10개 + builtin 7개 → project 3개 · builtin 7개
600
+ *
601
+ * 자리표가 말하는 순서(프로젝트 > 사용자 > 내장 > 플러그인)와 정반대다.
602
+ * 그리고 그 때문에 스킬자리.test.js 의 자리표 검사가 **죽은 검사**였다 —
603
+ * 내장은 자리표를 안 거치고 뽑혀 올라오니, rank 에서 builtin 을 지워도
604
+ * 빨개지지 않았다. 구멍을 막으려고 넣은 것이 또 다른 구멍을 만들었다.
605
+ *
606
+ * 기본 상한 40 에서는 몫이 10칸이라 일곱이 다 들어간다 — 지금과 같다.
607
+ * 상한이 좁을 때만 가까운 것에게 자리를 돌려준다. 매어 두는 값은
608
+ * 「통째로 사라지지 않는다」 이지 「무조건 다 실린다」 가 아니다.
609
+ */
610
+ /*
611
+ * 하한을 두지 않는다. `Math.max(1, …)` 로 「적어도 하나」 를 매어 뒀더니
612
+ * 상한이 1일 때 내장이 그 한 칸을 차지해 **최우선인 프로젝트가 밀렸고**,
613
+ * 상한이 0인데도 하나가 나왔다. 「상한의 1/4」 이라고 적어 놓고 하는 일은
614
+ * 그게 아니었던 것이다 — 적은 말과 어긋난 쪽은 코드였다.
615
+ * 상한이 넷보다 작으면 몫은 0이고, 그때는 자리표가 그냥 정하면 된다.
616
+ */
617
+ /*
618
+ * 상한을 **먼저 성한 수로 만든다.** 음수가 들어오면 `Math.floor(-1/4)`
619
+ * 이 -1 이고 `slice(0, -1)` 은 「끝 하나만 뺀 전부」 라, 0 개를 실어야 할
620
+ * 자리에서 넷이 실렸다(8차 리뷰). 음수 상한은 「싣지 마라」 는 뜻이지
621
+ * 「뒤에서 하나 빼고 다」 가 아니다.
622
+ *
623
+ * 그 울타리를 `Number.isFinite` 로 세웠더니 옮긴 자리에 틈이 났다
624
+ * (9차 리뷰). 그 함수는 **숫자꼴 글자열과 무한대를 함께 내친다.**
625
+ *
626
+ * maxSkillsListed = '10' → 0개 (앞 판은 10개)
627
+ * maxSkillsListed = Infinity → 0개 (앞 판은 전부)
628
+ *
629
+ * 설정 파일에서 온 값은 글자열이기 쉽고, 무한대는 「상한을 두지 마라」
630
+ * 는 뜻이다. 둘 다 스킬을 통째로 지워 버렸다 — 막으려던 것보다 나쁘다.
631
+ *
632
+ * 그래서 **먼저 숫자로 읽고**, 숫자가 아닐 때만 0 으로 본다.
633
+ * `Math.floor(Infinity)` 은 Infinity 이고 `slice(0, Infinity)` 는 전부라,
634
+ * 무한대는 저절로 「상한 없음」 이 된다.
635
+ */
636
+ const 잰값 = Number(this.maxSkillsListed);
637
+ const 상한 = Number.isNaN(잰값) ? 0 : Math.max(0, Math.floor(잰값));
638
+ const 몫 = Math.floor(상한 / 4);
639
+ const 매어둔것 = new Set(줄세운것.filter((s) => s.source === 'builtin').slice(0, 몫));
640
+ const 나머지 = new Set(줄세운것
641
+ .filter((s) => !매어둔것.has(s))
642
+ .slice(0, Math.max(0, 상한 - 매어둔것.size)));
643
+ /*
644
+ * 줄세운 것에서 **골라내기만** 한다. 두 덩이로 쪼개 이어 붙였더니 앞
645
+ * 번호 내장이 뒤 번호 내장 뒤로 밀려, 같은 갈래 안에서 차례가 뒤집혔다
646
+ * (b2 b3 b4 b5 b6 b0 b1). 「안정 정렬이라 원래 차례 그대로」 라고 적어 둔
647
+ * 주석이 그 자리에서 거짓이 됐다. 한 줄에서 거르면 그럴 일이 없다.
648
+ */
649
+ return 줄세운것.filter((s) => 매어둔것.has(s) || 나머지.has(s));
576
650
  }
577
651
 
578
652
  /*
package/src/oneshot.js CHANGED
@@ -506,6 +506,9 @@ export async function runOnce(opts = {}) {
506
506
  session.routed = 골라진.mode;
507
507
  const w = getWork(골라진.mode);
508
508
  곁(` ${c.hcyan(w.glyph)} ${c.gray(`${w.name} (${w.en}) — 말 속에 ${골라진.why} 가 있어서`)}`);
509
+ } else if (골라진.일부러) {
510
+ // 일부러 안 보낸 자리만 말한다. 대화 화면과 같은 규칙이다 (repl.js 참고).
511
+ 곁(` ${c.gray(`◇ 종합 그대로 — ${골라진.why}`)}`);
509
512
  }
510
513
  }
511
514
 
package/src/repl.js CHANGED
@@ -1758,6 +1758,18 @@ export async function chatLoop(opts = {}) {
1758
1758
  } else {
1759
1759
  say(` ${c.gray('다르면')} ${c.cyan('/code')} ${c.gray('처럼 직접 고르세요. 그때부터는 안 바뀝니다.')}`);
1760
1760
  }
1761
+ } else if (골라진.일부러) {
1762
+ /*
1763
+ * 골랐어야 할 만한 신호가 있었는데 **일부러 안 보낸** 자리다.
1764
+ *
1765
+ * 「설계 신호가 있었지만 고치라는 말이라 안 보냈다」 와 「그냥
1766
+ * 모르겠다」 는 사람에게 아주 다른 말이다. 앞엣것을 안 찍으면,
1767
+ * 설계를 기대한 사람은 종합으로 도는 화면을 보고 자동 모드가
1768
+ * 고장 났다고 여긴다. 뒤엣것은 안 찍는다 — 보통 한마디가 다
1769
+ * 그쪽이라 매번 뜨면 소음이다. 가르는 일은 route 가 한다.
1770
+ */
1771
+ say('');
1772
+ say(` ${c.gray('◇ 종합 그대로 —')} ${c.gray(골라진.why)}`);
1761
1773
  }
1762
1774
  }
1763
1775
 
@@ -87,10 +87,304 @@ export const 갈래 = [
87
87
  *
88
88
  * 값이 비었거나 이미 가려진 것은 그냥 둔다 — `API_KEY=` 만 있는 줄까지
89
89
  * 손대면 화면에 «가림» 만 늘어나고 알아볼 것이 없어진다.
90
+ *
91
+ * ── 값이 또 환경변수면 그건 값이 아니다 ────────────────────────────
92
+ *
93
+ * 벤치마크 지시문대로 웹 앱을 놓고 재 보다 걸렸다.
94
+ *
95
+ * const JWT_SECRET = process.env.JWT_SECRET;
96
+ * → const JWT_SECRET = «가림:환경변수»
97
+ *
98
+ * 가림이 하는 일은 **값**을 숨기는 것인데 `process.env.X` 는 값이
99
+ * 아니라 **참조**다. 거기 숨겨진 비밀이 없다 — 환경변수 이름은 비밀이
100
+ * 아니고, deel 문서도 `DEEL_API_KEY` 를 그대로 적는다.
101
+ *
102
+ * 이 파일 머리말이 적어 둔 그대로다 — 「넓게 잡아 코드에 흔한 글자를
103
+ * 비밀로 오인하면, 모델이 보는 코드가 조용히 달라진다.」 Node 앱에서
104
+ * 이 꼴은 설정을 묶는 자리마다 나온다. 그것이 가려지면 모델은 값이
105
+ * 어디서 오는지를 몰라, 생각보다 쉽게 박아 넣는 쪽으로 간다.
106
+ *
107
+ * 돌려주는 값(`|| 'dev-secret'`)은 그대로 둔다 — 그건 참조가 아니라
108
+ * 진짜 값이고, 보안 점검이 잡아야 할 자리다. 셸의 `$VAR` 는 안 뺀다 —
109
+ * 명령 출력에서는 그 자리에 진짜 값이 찍혀 나오기 때문이다.
110
+ *
111
+ * ── 「점으로 이어지는 것」 만 보면 일곱 꼴이 샌다 ─────────────────
112
+ *
113
+ * 처음엔 `process\.env\.` 처럼 **뒤에 점이 오는 것**만 뺐다. 3차 리뷰가
114
+ * 재 보니 대괄호·물음표점·괄호·다른 말의 관용구가 전부 새어 나갔다.
115
+ *
116
+ * const API_KEY = process.env['API_KEY'];
117
+ * → const API_KEY = «가림:환경변수»'API_KEY'];
118
+ *
119
+ * 새면 그냥 가려지는 것이 아니라 **줄이 망가진다.** 모델은 이걸 고장 난
120
+ * 코드로 보고 고치려 든다 — 가림이 하려던 일과 정반대다.
121
+ *
122
+ * 그래서 뒤에 무엇이 오는지가 아니라 **머리가 무엇인지**로 본다.
123
+ * `process.env` · `import.meta.env` · `globalThis.process.env` ·
124
+ * `globalThis.env` · `os.environ` · `os.getenv` · `ENV` · `Deno.env` ·
125
+ * `Bun.env` 아홉이고, 괄호로 감싼 것까지 받는다.
126
+ *
127
+ * ── 넓히다 진짜 값을 놓쳤다 ──────────────────────────────────────
128
+ *
129
+ * 4차 리뷰가 그 자리를 찾아냈다. 줄이 망가지는 것보다 **안 가려지는
130
+ * 것**이 훨씬 나쁘다 — 그 값이 그대로 모델에 간다.
131
+ *
132
+ * const API_KEY = "ENV-PROD-SECRET-KEY-12345"; → 안 가려졌다
133
+ *
134
+ * `ENV\[` 를 `ENV\b` 로 넓힌 탓이다. `ENV` 로 **시작하는 글자**가 전부
135
+ * 참조 취급을 받았다. 참조는 `ENV[…]` · `ENV.…` 처럼 **뭔가를 꺼내는
136
+ * 모양**이지, ENV 로 시작하는 아무 글자가 아니다. 그래서 뒤에 `[` · `.`
137
+ * · `(` 가 오는 것만 참조로 본다.
138
+ *
139
+ * ── 먹은 것은 도로 뱉는다 ────────────────────────────────────────
140
+ *
141
+ * 괄호를 잡아서 **버렸더니** 짝이 깨졌다. 5차 리뷰가 셋을 짚었다.
142
+ *
143
+ * connect(API_KEY="abc123456") → connect(API_KEY=«가림»
144
+ * const K = ("v12345" + salt); → const K = «가림» + salt);
145
+ *
146
+ * 앞엣것은 파이썬에서 제일 흔한 꼴이다. 여는 괄호는 훨씬 앞에 있는데
147
+ * 닫는 괄호만 값 뒤에 있어서, 그것만 삼키고 버렸다. 짝이 깨진 코드는
148
+ * 가린 것이 아니라 **망가뜨린 것**이다.
149
+ *
150
+ * 그래서 잡되 **그대로 도로 붙인다.** 잡은 만큼만 내놓으니 짝은 언제나
151
+ * 맞는다. 겹괄호도 같이 받는다.
152
+ *
153
+ * ── 따옴표 안은 참조가 아니다 ────────────────────────────────────
154
+ *
155
+ * `ENV` 만 「뭔가를 꺼내는 모양」 으로 죄고 나머지 여덟은 그냥 뒀더니
156
+ * `"process.env-prod-secret-12345"` 가 참조로 새어 나갔다. 아홉 다
157
+ * 죈다.
158
+ *
159
+ * 그리고 **따옴표를 지난 뒤에는 아예 안 따진다.** 따옴표 안은 글자값
160
+ * 이지 참조가 아니다 — `"ENV.PROD.SECRET.KEY.12345"` 는 참조처럼
161
+ * 생겼을 뿐 진짜 열쇠다.
162
+ *
163
+ * ── 값의 끝은 「뒤에 뭐가 오나」 로 잡으면 안 된다 ───────────────
164
+ *
165
+ * 한 판 동안 값의 끝을 **뒤따르는 글자**로 잡았다. 그 목록에 없는
166
+ * 글자가 하나만 와도 무늬가 통째로 빗나갔다 — 6차 리뷰가 넷을 찾았다.
167
+ *
168
+ * {"API_KEY":"secret12345"} → 안 가려짐
169
+ * const API_KEY = "s12345".trim(); → 안 가려짐
170
+ * const API_KEY = getToken(); → «가림») 로 괄호 깨짐
171
+ * const API_KEY = obj[key]; → «가림»] 로 대괄호 깨짐
172
+ *
173
+ * 안 가려지는 것은 그 값이 그대로 나간다는 뜻이고, 깨지는 것은 모델이
174
+ * 고치려 든다는 뜻이다. 둘 다 나쁘다.
175
+ *
176
+ * 그래서 **값 자체가 어디까지인지**로 잡는다. 값은 두 꼴뿐이다.
177
+ *
178
+ * · 따옴표 안에 든 것 — 닫는 따옴표가 곧 끝이다. 뒤에 `.trim()` 이
179
+ * 오든 `}` 가 오든 상관없다.
180
+ * · 맨 낱말 — 괄호·대괄호·중괄호·쉼표·세미콜론은 **값이 아니다.**
181
+ * 그러니 값에서 빼면 그 자리가 저절로 끝이 된다.
182
+ *
183
+ * 이렇게 쪼개니 참조 빼기도 깔끔해진다. 따옴표 안은 글자값이지 참조가
184
+ * 아니므로, 참조 검사는 맨 낱말 갈래에만 붙인다. 앞 판의
185
+ * `(?<=["'])` 같은 곁길이 필요 없다.
186
+ *
187
+ * 부르는 꼴(`getToken()`)도 뺀다. 이름 뒤에 여는 괄호가 오면 그건
188
+ * 값이 아니라 **불러오는 짓**이다.
189
+ *
190
+ * 이름 뒤의 따옴표도 받는다 — JSON 은 `"API_KEY": "..."` 꼴이다.
191
+ *
192
+ * ── 쪼개 놓고도 세 자리가 남았다 ─────────────────────────────────
193
+ *
194
+ * 7차 리뷰가 넷을 더 찾았다. 셋은 「값이 어디까지인가」 를 여전히 대충
195
+ * 봤기 때문이다.
196
+ *
197
+ * · 따옴표 갈래를 `[^"'\n]` 로 적어 **두 따옴표를 다 뺐다.** 그래서
198
+ * `"don't-leak-secret-12345"` 가 통째로 새어 나갔다. 빼야 할 것은
199
+ * **닫는 따옴표 하나**다 — 작은따옴표는 큰따옴표 안에서 그냥 글자다.
200
+ * · 부르는 꼴을 이름 **하나**로만 봐서 `service.getToken()` 이 값으로
201
+ * 잡혔다. 점으로 이어진 이름도 부르는 꼴이다.
202
+ * · 대괄호를 값에서 빼기만 하고 **앞의 이름**은 안 봤다. 그래서
203
+ * `config[key]` 의 `config` 가 값이 되고 `[key]` 가 고아로 남았다.
204
+ * `obj[key]` 가 멀쩡했던 것은 세 글자라 길이 하한에 걸린 우연이다.
205
+ *
206
+ * 넷째는 다른 부류다 — **가장 나쁜 짝**이다.
207
+ *
208
+ * const API_KEY: string = "secret12345";
209
+ * → const API_KEY: «가림:환경변수» = "secret12345";
210
+ *
211
+ * 타입 표기의 쌍점을 대입으로 읽어서, 타입 이름을 가리고 정작 진짜 값은
212
+ * 그대로 내보냈다. 줄은 망가지고 비밀은 남는다. 그래서 쌍점 뒤에
213
+ * **타입처럼 생긴 것 + 등호**가 오면 그 전부를 구분자로 친다. 타입처럼
214
+ * 생겼다는 것은 대문자로 시작하거나 아는 밑바탕 타입이라는 뜻이다 —
215
+ * `{ API_KEY: value, B: y = 1 }` 의 `value` 까지 삼키지 않으려는 것이다.
216
+ *
217
+ * ── 타입 자리를 한 꼴로만 알았다 ─────────────────────────────────
218
+ *
219
+ * 8차 리뷰. 타입 자리를 「대문자로 시작하는 서른 자」 나 「아는 밑바탕
220
+ * 타입 낱말 하나」 로만 봤다. 그래서 조금만 벗어나면 갈래가 통째로
221
+ * 빗나가고, 비밀이 **평문 그대로** 나갔다.
222
+ *
223
+ * const API_KEY: string | null = "secret12345"; 안 가려짐
224
+ * const API_KEY: string[] = "secret12345"; 안 가려짐
225
+ * const API_KEY: Record<string, Record<…>> = "…"; 타입만 가려짐
226
+ * const { API_KEY: key = "default1234" } = opts; 안 가려짐
227
+ *
228
+ * 낱말 목록을 늘리는 것은 답이 아니다. 타입 문법은 우리가 다 못 적는다.
229
+ *
230
+ * 한 판은 「타입처럼 생긴 글자」 를 줄여 적어 봤다 — 홑화살괄호와
231
+ * 대괄호와 낱말 글자. 9차 리뷰가 그 목록 밖의 여섯 꼴을 더 찾아냈다.
232
+ *
233
+ * const API_KEY: (string) = "…"; 둥근괄호
234
+ * const API_KEY: { token: string } = "…"; 중괄호
235
+ * const API_KEY: [string, number] = "…"; 튜플
236
+ * const API_KEY: A<B<C<D<string>>>> = "…"; 네 겹
237
+ * const API_KEY: 아주.긴.이름공간… = "…"; 길이 상한
238
+ *
239
+ * 여섯 다 **비밀이 평문으로 나갔다.** 적는 쪽이 지는 싸움이다.
240
+ *
241
+ * 그래서 타입이 무엇인지는 안 적는다. **어디서 끝나는가**만 본다 —
242
+ * 쌍점부터 등호까지다.
243
+ *
244
+ * ── 그 「없어야 하는 글자」 가 타입 안에도 있었다 ─────────────────
245
+ *
246
+ * 처음엔 그 사이에 따옴표·등호·세미콜론이 없어야 타입 자리로 쳤다.
247
+ * 10차 리뷰가 셋 다 **타입 안에 오는** 꼴을 찾아냈다.
248
+ *
249
+ * const API_KEY: { token: string; id: number } = "…"; 세미콜론
250
+ * const API_KEY: () => string = "…"; 화살표의 등호
251
+ * const API_KEY: Record<"token" | "key", string> = "…"; 따옴표
252
+ * const API_KEY: [string, number] | null = "…"; 쉼표
253
+ *
254
+ * 앞의 셋은 **비밀이 평문으로 나갔고**, 따옴표 꼴은 더 나빴다 —
255
+ * 타입 갈래가 빗나가자 맨 쌍점 갈래가 그 따옴표를 값으로 잡았다.
256
+ *
257
+ * const API_KEY: 'bearer' | 'basic' = "secret12345";
258
+ * → const API_KEY: «가림:환경변수» | 'basic' = "secret12345";
259
+ *
260
+ * 타입이 가려지고 비밀은 남는다. 이 파일이 「가장 나쁜 짝」 이라고
261
+ * 적어 둔 바로 그 모양이 세 판 만에 돌아왔다.
262
+ *
263
+ * 이제 셋을 다 받는다. 대신 **타입 갈래는 따옴표 친 값만 가린다.**
264
+ * 타입을 적고 나서 대입하는 값이 맨 낱말이면 그건 비밀이 아니라
265
+ * 가리키는 곳이다 — `const { API_KEY: secretKey, other } = options;`
266
+ * 의 `options` 가 그것이다. 앞 판은 이걸 가려서 줄을 못 읽게 만들었다.
267
+ *
268
+ * 「따옴표 친 값만」 이 울타리 노릇을 하기 때문에, 사이를 넓게 열어도
269
+ * 엉뚱한 것을 삼키지 않는다. 삼킬 수 있는 것은 넉 자 넘는 글자값뿐이다.
270
+ *
271
+ * 그 값이 괄호에 싸여 오는 것도 받는다(11차 리뷰) — 맨 쌍점 갈래는
272
+ * 원래 받고 있었는데 타입 갈래만 못 받아, 같은 줄이 타입을 적었느냐로
273
+ * 갈렸다. `const API_KEY: string = ("secret12345");` 가 그 자리다.
274
+ *
275
+ * ── 쉼표만 두 갈래로 본다 ────────────────────────────────────────
276
+ *
277
+ * 쉼표는 **닫는 괄호 안에서만** 타입의 일부다(`Record<string, string>` ·
278
+ * `[string, number]` · `{ token: string }`). 밖에서는 칸을 가른다 —
279
+ * `{ API_KEY: value, B: y = 1 }` 에서 쉼표를 건너뛰면 **옆 칸의 등호**를
280
+ * 대입 자리로 읽어, 가려지는 것이 옆 칸의 값이 된다.
281
+ *
282
+ * 그래서 쌍점 갈래를 둘로 둔다. 쉼표·세미콜론이 없으면 그냥 등호까지
283
+ * 가고, 있으면 **등호 앞 어딘가에 닫는 괄호**가 서 있어야 한다. 닫는
284
+ * 괄호 뒤로는 다시 쉼표·세미콜론이 없어야 한다 — `[string, number] |
285
+ * null =` 은 받고, `{ API_KEY: value, other: z =` 는 안 받는 자리다.
286
+ *
287
+ * 세미콜론도 같은 두 갈래다. 중괄호 안에서는 타입 멤버를 가르지만
288
+ * (`{ token: string; id: number }`), 밖에서는 문장을 끝낸다 —
289
+ * `API_KEY: string; const other = "hello world";` 의 뒤엣것까지
290
+ * 삼키면 안 된다. 닫는 괄호를 요구하는 것이 그 자리를 막는다.
291
+ *
292
+ * 줄바꿈을 안 넘는 것도 같은 까닭이다. 넘게 두면 `API_KEY: string`
293
+ * 다음 줄의 `foo = "hello world"` 까지 한 무늬로 읽혀 비밀도 아닌
294
+ * 글자가 가려진다 — 이 파일 머리말이 「막으려는 것보다 더 나쁜 고장」
295
+ * 이라고 적어 둔 자리다. 여러 줄에 걸친 타입 표기는 못 잡지만, 그건
296
+ * 못 가리는 것으로 끝난다.
297
+ *
298
+ * 맨 낱말 갈래에서 홑화살괄호도 뺀다. 안 빼면 위의 여러 줄 제네릭에서
299
+ * `Record<` 가 값으로 잡혀 줄이 깨졌다. 그 값에 홑화살괄호가 든 비밀
300
+ * (`API_KEY=P@ss<word>99`)은 못 가린다 — 따옴표를 치면 가려진다.
301
+ * 등호도 없이 타입만 적는 줄(`API_KEY: Map<string, Set<string>>`)이
302
+ * 깨지는 쪽이 더 나쁘다고 보고 그대로 둔다.
303
+ *
304
+ * ── 그 갈래가 타입 이름과 참조까지 가렸다 ────────────────────────
305
+ *
306
+ * 10차 판을 올리고 코드 줄 스물다섯을 쓸어봤더니 셋이 가려졌다.
307
+ *
308
+ * interface Cfg { API_KEY: string; TIMEOUT: number }
309
+ * API_KEY: string;
310
+ * const API_KEY = config.apiKey;
311
+ *
312
+ * 이 갈래가 진짜로 있어야 하는 자리는 `.env` 와 YAML 이다. 거기 오는
313
+ * 값은 밑바탕 타입 이름이 아니고, 점으로 이은 이름도 아니다. 둘을
314
+ * 덜어 낸다 — 부르는 꼴·대괄호 꼴을 이미 덜어 낸 것과 같은 갈래다.
315
+ *
316
+ * 점 갈래는 **등호·쌍점 뒤에 빈칸이 있을 때만** 덜어 낸다. `.env` 는
317
+ * `NAME=값` 으로 붙여 쓰니, 빈칸을 둔 것은 코드다. 그래서
318
+ * `API_KEY=my.secret.value` 는 그대로 가리고 `= config.apiKey` 는 둔다.
319
+ * YAML 은 쌍점 뒤에 빈칸을 두므로 점으로 이은 YAML 값은 못 가린다 —
320
+ * 따옴표를 치면 가려진다.
321
+ *
322
+ * 같은 쓸어보기에서 하나 더. 이름을 바꿔 받는 자리도 가려졌다.
323
+ *
324
+ * const { API_KEY: secretKey, other } = options;
325
+ * function f({ API_KEY: apiKey }) { … }
326
+ *
327
+ * 거기 오는 것은 **묶는 이름**이지 값이 아니다. 갈라 주는 것은 중괄호가
328
+ * 등호의 어느 쪽에 있느냐다 — 오른쪽이면 물건이고(`= { API_KEY: Value }`),
329
+ * 왼쪽이거나 부르는 자리의 인자면 풀어 받는 자리다. 뒤에 `} =` 나
330
+ * `})` 가 오는지로 가른다. `{ API_KEY: Value, … };` 처럼 `};` 로 닫는
331
+ * 물건은 그대로 둔다 — 그 자리는 위에 못박아 둔 `.env`·YAML 갈래다.
332
+ *
333
+ * ── 두 번 가려도 같아야 한다 ─────────────────────────────────────
334
+ *
335
+ * 가린 글이 이 길을 다시 지나가는 일은 흔하다(도구 출력을 가리고, 그
336
+ * 대화를 다시 가린다). 둘째 판에서 맨 쌍점 갈래가 타입의 따옴표를
337
+ * 값으로 잡으면 위 「가장 나쁜 짝」 이 뒷문으로 돌아온다. 그래서 그
338
+ * 갈래는 **줄 뒤쪽이 이미 가려져 있으면**(`= «가림:`) 안 나선다.
339
+ *
340
+ * ── 달아난 따옴표에서 끊겼다 ─────────────────────────────────────
341
+ *
342
+ * 같은 판에 하나 더. 따옴표 갈래가 `\'` 를 닫는 따옴표로 읽었다.
343
+ *
344
+ * const API_KEY = 'it\'s_a_secret_12345';
345
+ *
346
+ * 값이 `it\` 세 글자로 끊겨 길이 하한에 못 미치고, 규칙 전체가
347
+ * 빗나가 **비밀이 그대로 나갔다.** 역빗금 뒤 한 글자는 통째로 한 글자로
348
+ * 센다 — 그 자리의 따옴표는 닫는 것이 아니다.
349
+ *
350
+ * ── 값의 첫 토막만 보고 멈췄다 ───────────────────────────────────
351
+ *
352
+ * 8차 리뷰. 맨 낱말 갈래가 등호 뒤 **첫 토막**을 잡고 거기서 끝냈다.
353
+ * 값 하나만 오는 줄에서는 맞는데, 식이 오면 통째로 어긋난다.
354
+ *
355
+ * const API_KEY = isProd ? "secret12345" : "default";
356
+ * → const API_KEY = «가림:환경변수» ? "secret12345" : "default";
357
+ * const API_KEY = ENV_KEY || "secret12345";
358
+ * → const API_KEY = «가림:환경변수» || "secret12345";
359
+ * const API_KEY = await getToken();
360
+ * → const API_KEY = «가림:환경변수» getToken();
361
+ *
362
+ * 가린 것이 값이 아니다. 게다가 가렸다는 표시가 붙어서, 보는 사람은
363
+ * 가려진 줄로 안다 — 위 「가장 나쁜 짝」 과 같은 부류다.
364
+ *
365
+ * 그래서 맨 낱말은 **거기서 값이 끝나야** 값으로 친다. 뒤에 세미콜론·
366
+ * 쉼표·닫는 괄호·줄바꿈·주석표가 와야 한다. `await` 도 `ENVIRONMENT` 도
367
+ * 뒤에 말이 더 있으니 값이 아니다. `.env` · YAML 처럼 맨 낱말이 진짜
368
+ * 값인 자리는 원래 줄 끝이거나 주석표 앞이라 그대로 걸린다.
369
+ *
370
+ * ── 그렇다고 식 뒤의 글자값까지 쫓아가지는 않는다 ────────────────
371
+ *
372
+ * 고치다 보니 `ENV_KEY || "secret12345"` 의 뒤엣것도 가리고 싶어진다.
373
+ * 그런데 그건 이 파일이 **일부러 그렇게 둔 것**이다 — 위 「돌려주는
374
+ * 값은 그대로 둔다」 와 검사(`돌려주는 값은 보이게 남긴다`) 가 그
375
+ * 자리다. 되돌림 값은 참조가 아니라 박아 넣은 값이고, 보안 점검이
376
+ * 잡아야 할 자리라서 보이게 남긴다.
377
+ *
378
+ * 여기서 고치는 것은 **값이 아닌 것을 가리던 것**뿐이다. 정해 둔 것을
379
+ * 고치는 김에 슬쩍 뒤집으면, 그건 고침이 아니라 다른 결정이다.
90
380
  */
381
+ // 값 자리가 또 환경변수를 읽는 것이면 참조다 — Node · Vite · 파이썬 · Deno.
91
382
  id: '환경변수',
92
- re: /\b([A-Z][A-Z0-9_]{2,}(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|CREDENTIALS?))(\s*[:=]\s*)(["']?)(?!«)([^\s"']{4,})\3/g,
93
- 바꾸기: (m, 이름, 사이) => `${이름}${사이}${표('환경변수')}`,
383
+ re: /\b([A-Z][A-Z0-9_]{2,}(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|CREDENTIALS?))(?:(["']?[ \t]*(?::[^\n,;]{0,200}?|:[^\n]{0,200}?[>\])}][^\n,;]{0,80}?)[ \t]*=(?!=|>)\s*)((?:\(\s*)+)?(["'])(?!«)((?:\\[^\n]|(?!\4)[^\n]){4,}?)\4((?:\s*\))+)?|(["']?\s*(?::(?![^\n]{0,200}?=[ \t]*«가림:)|=)\s*)((?:\(\s*)+)?(?:(["'])(?!«)((?:\\[^\n]|(?!\9)[^\n]){4,}?)\9|(?!«)(?![\w$.]+\s*[([])(?!(?:string|number|boolean|bigint|symbol|object|unknown|never|void|null|undefined|true|false)\b)(?:(?<=[ \t])(?![A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+\b)|(?<![ \t]))(?![^\n]{0,200}?}[ \t]*(?:=(?!=|>)|\)))(?!\(*\s*(?:process\.env|import\.meta\.env|globalThis\.process\.env|globalThis\.env|os\.environ|os\.getenv|ENV(?=\s*[[.(])|Deno\.env|Bun\.env)\b)([^\s"'()[\]{},;<>]{4,})(?=[ \t\r]*(?:[;,)\]}\n#]|$)))((?:\s*\))+)?)/g,
384
+ 바꾸기: (m, 이름, 타입사이, 타입여는, 따A, 값A, 타입닫는,
385
+ 민사이, 여는, 따B, 값B, 값C, 닫는) => (타입사이 === undefined
386
+ ? `${이름}${민사이}${여는 ?? ''}${표('환경변수')}${닫는 ?? ''}`
387
+ : `${이름}${타입사이}${타입여는 ?? ''}${표('환경변수')}${타입닫는 ?? ''}`),
94
388
  },
95
389
  ];
96
390
 
@@ -50,7 +50,77 @@ const 뒤에붙일것 = /^build(:|$)/i;
50
50
  * 않은 채 `node` 로 직접 도는 파일들.
51
51
  */
52
52
  const 볼폴더 = ['scripts', 'script', 'test', 'tests', 'qa', 'tools', 'bin'];
53
- const 검사파일 = /^(qa|test|tests|check|verify|e2e|spec|bench)[-_.].+\.(m?js|cjs|ts|py|sh)$/i;
53
+ /*
54
+ * 이름이 **앞에 붙든 뒤에 붙든** 검사다. 다만 둘은 같은 관례가 아니다.
55
+ *
56
+ * 예전엔 앞머리만 봤다(`^(qa|test|…)[-_.]`). 그래서 `test-approval.mjs` 는
57
+ * 찾고 `approval.test.js` 는 못 찾았다 — 그런데 자바스크립트 쪽에서 흔한
58
+ * 것은 뒤에 붙는 쪽이다. Jest · Vitest · node:test 가 전부 `*.test.js` ·
59
+ * `*.spec.ts` 를 기본값으로 쓴다. deel 자기 저장소도 `test/guard.test.js`
60
+ * 꼴이다 — 제 검사를 제가 못 찾고 있었다(package.json 의 `test` 칸이 가려 줬다).
61
+ *
62
+ * ── 그런데 두 관례의 낱말이 다르다 ──────────────────────────────────────
63
+ *
64
+ * 처음엔 뒤쪽도 검사말 전부로 열었다. 2차 리뷰가 재 보니 헛것을 담았다 —
65
+ * `type-check.js` · `health-check.js` · `api-spec.ts`. 돌려 봐도 아무 검사도
66
+ * 안 도는 파일들이다.
67
+ *
68
+ * 앞에 붙는 것 = **스크립트 이름** qa-roles.mjs · test-approval.mjs · check-src.mjs
69
+ * 뒤에 붙는 것 = **검사 파일 이름** approval.test.js · roles.spec.ts · handler_test.js
70
+ *
71
+ * 뒤쪽 관례에 check·verify·e2e·bench 는 없다. 그리고 붙임표로 이은
72
+ * `api-spec` 은 낱말 합성이지 검사 파일이 아니다 — 그래서 뒤쪽은 점·밑줄만 본다.
73
+ *
74
+ * 낱말 하나뿐인 이름(`test.js` · `spec.js`)도 담는다. 앞뒤 두 갈래 모두
75
+ * 「구분자 + 뒤에 뭔가」 를 요구해서 이런 이름은 어느 쪽에도 안 걸렸다.
76
+ * 이 갈래도 **뒤에 붙는 관례의 낱말**만 쓴다 — 처음엔 검사말 전부를 넣어서
77
+ * `scripts/check.js` · `bench.js` · `verify.js` · `e2e.js` 가 검사로 잡혔다.
78
+ * 주석에는 「test.js·spec.js 만」 이라고 적어 놓고서다. 단독으로 쓰이는
79
+ * 검사 파일 이름은 test·spec 이지 check·bench 가 아니다.
80
+ *
81
+ * 앞에 붙는 갈래의 구분자에서 **점을 뺀다.** 4차 리뷰가 짚었다 —
82
+ * 점을 받아 두니 검사말로 시작하는 온갖 살림 파일이 검사로 잡혔다.
83
+ *
84
+ * check.config.js · test.min.js · qa.bundle.js · spec.d.ts
85
+ *
86
+ * 앞쪽 관례는 스크립트 이름이고, 스크립트 이름은 `qa-roles.mjs` ·
87
+ * `test_helpers.py` 처럼 붙임표·밑줄로 잇는다. 점으로 이으면 그건 이름이
88
+ * 아니라 **설정·번들 꼬리표**다. 「검사를 돌려 봤습니다」 하고 설정 파일을
89
+ * 실행하면, 안 돌린 검사를 돌렸다고 말하는 셈이다.
90
+ *
91
+ * 뒤에 오는 마디에도 점을 안 준다. 구분자만 고쳤더니 `.+` 가 점까지
92
+ * 삼켜서 `qa-roles.d.ts` · `test-helper.bundle.js` 가 그대로 잡혔다
93
+ * (5차 리뷰). 스크립트 이름은 한 마디다 — 그 뒤에 점이 또 나오면
94
+ * 그건 이름의 일부가 아니라 꼬리표다.
95
+ *
96
+ * 넓히되 **검사라고 적힌 것만** 담는다. 마디를 가르는 자리를 앞쪽은
97
+ * 붙임표·밑줄로, 뒤쪽은 점·밑줄로
98
+ * 못박아서 `latest.js` 가 `test` 로 읽히거나 `contest.js` 가 딸려오지 않게 한다 —
99
+ * 없는 것을 지어내면 모델이 그걸 부르고, 실패를 받고, 또 부른다.
100
+ *
101
+ * 확장자는 아래 부르는법 과 **같은 목록**이어야 한다. 무늬만 아는 확장자는
102
+ * 아무 데도 안 닿는 죽은 갈래가 된다(부르는법에 없으면 그냥 건너뛴다).
103
+ *
104
+ * ── 복수형은 test·spec 만 ───────────────────────────────────────────────
105
+ *
106
+ * 8차 리뷰가 물었다 — `checks-roles.mjs` · `benches-roles.py` 는 왜 안 되나.
107
+ * 안 되는 것이 맞다. `tests-` · `specs-` 는 실제로 쓰는 이름이고
108
+ * (`tests-integration.mjs`), `checks-` · `benches-` 는 아니다. 없는 것을
109
+ * 늘리면 모델이 없는 명령을 부르고, 실패를 받고, 또 부른다.
110
+ *
111
+ * 앞 판 검사 주석이 「앞쪽 검사말도 복수형을 받는다」 고 뭉뚱그려 적어서
112
+ * 넷 다 되는 것처럼 읽혔다. 되는 둘과 안 되는 둘을 검사에 나란히 못박아 뒀다.
113
+ *
114
+ * 점은 두 겹으로 적는다. 템플릿 글 안에서 `\.` 은 자바스크립트가 먼저 먹어서
115
+ * 정규식에 닿는 것은 아무 글자나 맞는 `.` 이다. 그러면 `a.testZjs` 같은 것이
116
+ * 검사 파일로 읽힌다. 실제로 그랬고, 이 저장소가 자동모드.test.js 로 잡는
117
+ * 바로 그 부류다.
118
+ */
119
+ const 검사말 = '(?:qa|tests?|check|verify|e2e|specs?|bench)';
120
+ const 뒤에붙는말 = '(?:tests?|specs?)';
121
+ const 검사끝 = '(m?js|cjs|ts|py|sh)';
122
+ const 검사파일 = new RegExp(
123
+ `^(?:${검사말}[-_][^.]+|.+[._]${뒤에붙는말}|${뒤에붙는말})\\.${검사끝}$`, 'i');
54
124
  const 부르는법 = { '.js': 'node', '.mjs': 'node', '.cjs': 'node', '.ts': 'node', '.py': 'python', '.sh': 'sh' };
55
125
 
56
126
  /*