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
@@ -48,8 +48,8 @@ import { readFileSync } from 'node:fs';
48
48
  /** 우리가 실제로 재는 열쇠들. 여기 없는 것은 안 재고, 안 쟀다고 말한다. */
49
49
  export const 아는열쇠 = Object.freeze([
50
50
  'type', 'enum', 'const',
51
- 'properties', 'required', 'additionalProperties',
52
- 'items', 'minItems', 'maxItems',
51
+ 'properties', 'required', 'additionalProperties', 'patternProperties',
52
+ 'items', 'prefixItems', 'minItems', 'maxItems',
53
53
  'minLength', 'maxLength', 'pattern',
54
54
  'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf',
55
55
  'anyOf', 'oneOf', 'allOf', 'not',
@@ -60,7 +60,8 @@ export const 아는열쇠 = Object.freeze([
60
60
 
61
61
  const 아는것 = new Set(아는열쇠);
62
62
  // 그 아래 열쇠가 규격 낱말이 아니라 '사람이 지은 칸 이름' 인 자리.
63
- const 이름칸 = new Set(['properties', '$defs', 'definitions']);
63
+ // patternProperties 아래 열쇠는 칸 이름을 가르는 **무늬**다 — 그것도 규격 낱말이 아니다.
64
+ const 이름칸 = new Set(['properties', 'patternProperties', '$defs', 'definitions']);
64
65
 
65
66
  /**
66
67
  * 이 스키마에서 우리가 안 보는 열쇠들.
@@ -83,7 +84,9 @@ export function 모르는열쇠(스키마, 본것 = new Set()) {
83
84
  * 진짜로 안 재는 낱말(format 같은 것)이 그 속에 묻힌다.
84
85
  */
85
86
  if (이름칸.has(k)) { for (const x of Object.values(v)) 훑기(x); }
86
- else 훑기(v);
87
+ // `default`·`examples` 아래는 **데이터**다(2.0.0 6회차 모양6 O2). 거기 적힌 `host`·`port` 를
88
+ // 「안 재는 열쇠」 로 세면 이 경고가 또 소음이 된다. 칸 이름이 `enum` 인 것은 위 갈래로 간다.
89
+ else if (!값칸.has(k)) 훑기(v);
87
90
  }
88
91
  };
89
92
  훑기(스키마);
@@ -103,6 +106,14 @@ export function 스키마읽기(경로) {
103
106
  let 글;
104
107
  try { 글 = readFileSync(경로, 'utf8'); }
105
108
  catch (err) { return { ok: false, 왜: `스키마 파일을 못 읽었습니다: ${경로}\n ${err?.message ?? err}` }; }
109
+ /*
110
+ * 맨 앞의 BOM 을 뗀다 (사냥5 B5-05).
111
+ *
112
+ * 윈도우 메모장과 PowerShell 5 의 Out-File 은 UTF-8 에 BOM 을 붙여 저장한다. JSON.parse 는
113
+ * 그 한 글자에서 멈추고, 화면은 「스키마가 JSON 이 아닙니다」 — 눈으로 보면 멀쩡한
114
+ * 파일인데. 윈도우에서 스키마를 처음 적는 사람이 첫 판에 밟는 자리다.
115
+ */
116
+ if (글.startsWith(BOM)) 글 = 글.slice(BOM.length);
106
117
  let s;
107
118
  try { s = JSON.parse(글); }
108
119
  catch (err) { return { ok: false, 왜: `스키마가 JSON 이 아닙니다: ${경로}\n ${err?.message ?? err}` }; }
@@ -119,9 +130,85 @@ export function 스키마읽기(경로) {
119
130
  + ' 같은 파일 안(#/$defs/...)으로 바꿔 주세요.',
120
131
  };
121
132
  }
133
+ /*
134
+ * 값을 한 칸도 파고들지 않고 제자리로 돌아오는 참조는 **읽는 자리에서** 막는다 (사냥5 L5-3).
135
+ *
136
+ * `{"$ref":"#"}` · a→b→a · `{"allOf":[{"$ref":"#"}]}` 는 같은 값을 같은 스키마로 끝없이
137
+ * 재게 한다. 여기서 안 막았더니 모델을 한 번 부른 **뒤에** 재는 자리에서 호출 스택이
138
+ * 넘쳤고, 그 오류가 표준출력에 맨 글로 찍혔다. 사람이 적은 스키마의 실수라 모델을
139
+ * 부르기 전에 7 로 서야 한다. 나무처럼 **값을 파고들며** 되부르는 참조(items·properties
140
+ * 아래)는 끝이 있으므로 막지 않는다.
141
+ */
142
+ const 도는곳 = 제자리참조찾기(s);
143
+ if (도는곳) {
144
+ return {
145
+ ok: false,
146
+ 왜: `스키마의 $ref 가 제자리를 돕니다: ${도는곳}\n`
147
+ + ' 값을 한 칸도 파고들지 않고 같은 자리로 돌아오면 끝없이 잽니다. 참조 고리를 끊어 주세요.',
148
+ };
149
+ }
122
150
  return { ok: true, 스키마: s, 모른것: 모르는열쇠(s) };
123
151
  }
124
152
 
153
+ // 윈도우 편집기가 파일 맨 앞에 붙이는 표시 한 글자 (스키마읽기 의 BOM 머리말).
154
+ const BOM = String.fromCharCode(0xfeff);
155
+
156
+ // 스키마가 아니라 **값**이 적히는 칸. 그 안의 `$ref` 는 글자일 뿐이라 따라가지 않는다.
157
+ const 값칸 = new Set(['enum', 'const', 'default', 'examples']);
158
+
159
+ /*
160
+ * 같은 값을 그대로 들고 옮겨 가는 길($ref · allOf · anyOf · oneOf · not)만 따라가서
161
+ * 제자리로 돌아오는 고리를 찾는다. 찾으면 사람이 읽을 길 한 줄, 없으면 null.
162
+ *
163
+ * properties·items 아래로 내려가는 길은 값도 한 칸 파고들므로 고리가 되지 않는다 —
164
+ * 그래서 그 길은 「고리 찾기」 에 안 넣고, 「어디서 시작할 수 있나」 에만 쓴다.
165
+ */
166
+ function 제자리참조찾기(뿌리) {
167
+ const 끝남 = new Set();
168
+ const 가는중 = new Set();
169
+ const 같은값길 = (n) => {
170
+ const out = [];
171
+ if (typeof n.$ref === 'string') {
172
+ const t = ref따라가기(뿌리, n.$ref);
173
+ // 참/거짓 스키마는 더 갈 곳이 없어 고리가 못 된다 — 길에 안 올린다.
174
+ if (t && typeof t === 'object') out.push([t, n.$ref]);
175
+ }
176
+ for (const k of ['allOf', 'anyOf', 'oneOf']) {
177
+ if (Array.isArray(n[k])) for (const x of n[k]) if (x && typeof x === 'object') out.push([x, k]);
178
+ }
179
+ if (n.not && typeof n.not === 'object') out.push([n.not, 'not']);
180
+ return out;
181
+ };
182
+ const 돌기 = (n, 길) => {
183
+ if (끝남.has(n)) return null;
184
+ if (가는중.has(n)) return 길.join(' → ');
185
+ 가는중.add(n);
186
+ for (const [다음, 표] of 같은값길(n)) {
187
+ const 찾음 = 돌기(다음, [...길, 표]);
188
+ if (찾음) return 찾음;
189
+ }
190
+ 가는중.delete(n);
191
+ 끝남.add(n);
192
+ return null;
193
+ };
194
+ const 본것 = new Set();
195
+ const 훑기 = (x) => {
196
+ if (!x || typeof x !== 'object' || 본것.has(x)) return null;
197
+ 본것.add(x);
198
+ if (!Array.isArray(x)) {
199
+ const 찾음 = 돌기(x, ['#']);
200
+ if (찾음) return 찾음;
201
+ }
202
+ for (const [k, v] of Object.entries(x)) {
203
+ if (!Array.isArray(x) && 값칸.has(k)) continue;
204
+ const 찾음 = 훑기(v);
205
+ if (찾음) return 찾음;
206
+ }
207
+ return null;
208
+ };
209
+ return 훑기(뿌리);
210
+ }
211
+
125
212
  /** 같은 파일 안(`#`으로 시작)이 아닌 `$ref` 를 다 찾는다. */
126
213
  export function 바깥ref찾기(스키마, 본것 = new Set()) {
127
214
  const out = [];
@@ -130,23 +217,54 @@ export function 바깥ref찾기(스키마, 본것 = new Set()) {
130
217
  본것.add(s);
131
218
  if (Array.isArray(s)) { for (const x of s) 훑기(x); return; }
132
219
  if (typeof s.$ref === 'string' && !s.$ref.startsWith('#')) out.push(s.$ref);
133
- for (const v of Object.values(s)) if (v && typeof v === 'object') 훑기(v);
220
+ /*
221
+ * 값 칸(`default`·`const`·`examples`·`enum`) 속은 스키마가 아니라 데이터라 안 들어간다
222
+ * (2.0.0 6회차 모양6 O1). 예시 값에 `{"$ref":"http://…"}` 가 있다고 멀쩡한 스키마를 거절했다.
223
+ * 칸 이름(`properties` 아래)이 `default` 인 것은 스키마이므로 그 자리는 그대로 훑는다 —
224
+ * 거기 바깥 주소를 숨기면 아래 거절이 비켜 간다.
225
+ */
226
+ for (const [칸, v] of Object.entries(s)) {
227
+ if (!v || typeof v !== 'object') continue;
228
+ if (이름칸.has(칸)) { for (const x of Object.values(v)) 훑기(x); }
229
+ else if (!값칸.has(칸)) 훑기(v);
230
+ }
134
231
  };
135
232
  훑기(스키마);
136
233
  return [...new Set(out)];
137
234
  }
138
235
 
139
- /** `#/$defs/이름` 같은 자리를 뿌리에서 따라간다. 못 찾으면 null. */
236
+ /**
237
+ * `#/$defs/이름` 같은 자리를 뿌리에서 따라간다. 못 찾으면 undefined.
238
+ *
239
+ * 스키마는 객체일 수도, 참/거짓 그 자체일 수도 있다. 그래서 「없다」 를 null 이 아니라
240
+ * undefined 로 말한다 — `false` 를 「없다」 로 되돌리면 부르는 쪽이 못 가른다.
241
+ */
140
242
  function ref따라가기(뿌리, ref) {
141
- if (typeof ref !== 'string' || !ref.startsWith('#')) return null;
142
- const 길 = ref.slice(1).split('/').filter(Boolean)
143
- .map((x) => decodeURIComponent(x.replace(/~1/g, '/').replace(/~0/g, '~')));
243
+ if (typeof ref !== 'string' || !ref.startsWith('#')) return undefined;
244
+ /*
245
+ * 퍼센트를 먼저 풀고 `~1`·`~0` 을 푼다 (RFC 6901 §6 의 차례). 날 `%` 가 섞인 조각은
246
+ * decodeURIComponent 가 URIError 를 던져 **스키마 읽기와 재기가 통째로 던졌다**
247
+ * (2.0.0 6회차 모양6 O5) — 그런 조각은 적힌 글자 그대로 찾는다.
248
+ */
249
+ const 조각들 = ref.slice(1).split('/');
250
+ /*
251
+ * 맨 앞 하나만 떼고 **빈 조각은 남긴다** (2.0.0 8회차 스키마). 포인터 `/` 의 조각은
252
+ * 「이름이 빈 칸」 하나지 조각 없음이 아니다 (RFC 6901 §3). 빈 것을 다 지웠더니
253
+ * `#/` 가 뿌리로 돌아가 엉뚱한 자리를 재고, 그 탈이 사람에게는 스키마가 틀린 것으로 보였다.
254
+ */
255
+ if (조각들[0] === '') 조각들.shift();
256
+ const 길 = 조각들.map((x) => {
257
+ let 풀린 = x;
258
+ try { 풀린 = decodeURIComponent(x); } catch { /* 적힌 그대로 */ }
259
+ return 풀린.replace(/~1/g, '/').replace(/~0/g, '~');
260
+ });
144
261
  let 여기 = 뿌리;
145
262
  for (const 조각 of 길) {
146
- if (!여기 || typeof 여기 !== 'object') return null;
263
+ if (!여기 || typeof 여기 !== 'object') return undefined;
147
264
  여기 = 여기[조각];
148
265
  }
149
- return 여기 && typeof 여기 === 'object' ? 여기 : null;
266
+ if (typeof 여기 === 'boolean') return 여기;
267
+ return 여기 && typeof 여기 === 'object' ? 여기 : undefined;
150
268
  }
151
269
 
152
270
  const 갈래 = (v) => {
@@ -165,6 +283,30 @@ const 갈래맞나 = (v, t) => {
165
283
  return typeof v === t;
166
284
  };
167
285
 
286
+ /*
287
+ * JSON 값이 같은가 — 객체는 **키 순서를 안 본다** (2.0.0 6회차 모양6뒤). JSON.stringify 로 견줬더니
288
+ * `{"b":2,"a":1}` 이 `const: {"a":1,"b":2}` 에 「여야 합니다」 로 걸렸다. 모델은 키 순서를 안 지킨다.
289
+ */
290
+ function 같은값(a, b) {
291
+ if (a === b) return true;
292
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false;
293
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
294
+ if (Array.isArray(a)) return a.length === b.length && a.every((x, i) => 같은값(x, b[i]));
295
+ const 앞열쇠 = Object.keys(a);
296
+ return 앞열쇠.length === Object.keys(b).length
297
+ && 앞열쇠.every((k) => Object.prototype.hasOwnProperty.call(b, k) && 같은값(a[k], b[k]));
298
+ }
299
+
300
+ /*
301
+ * 스키마의 무늬를 정규식으로 — **유니코드 꼴로 먼저** 짓는다 (2.0.0 6회차 모양6뒤). JSON 스키마의 정규식은
302
+ * ECMA-262 유니코드 꼴이라 글자 갈래 무늬(p 뒤 중괄호)를 쓴다. u 없이 지었더니 그게 글자 p 로 읽혀
303
+ * 맞는 글을 거절했다. u 로 안 지어지는 옛 꼴(쓸데없는 이스케이프)은 u 없이 한 번 더. 둘 다 안 되면 null.
304
+ */
305
+ function 정규식짓기(무늬) {
306
+ try { return new RegExp(무늬, 'u'); } catch { /* 옛 꼴일 수 있다 */ }
307
+ try { return new RegExp(무늬); } catch { return null; }
308
+ }
309
+
168
310
  /**
169
311
  * 값이 스키마에 맞나.
170
312
  *
@@ -174,17 +316,69 @@ const 갈래맞나 = (v, t) => {
174
316
  *
175
317
  * @returns {{ok: boolean, 탈: string[]}}
176
318
  */
177
- export function 맞나(값, 스키마, { 뿌리 = 스키마, 자리 = '' } = {}) {
319
+ export function 맞나(값, 스키마, 옵션 = {}) {
320
+ /*
321
+ * ── 같은 값을 같은 스키마로 **또** 재려 하면 거기서 끊는다 (사냥5 L5-3) ─────────
322
+ *
323
+ * `{"$ref":"#"}` 나 a→b→a 는 값을 한 칸도 안 파고들고 제자리로 돈다. 여기가 그걸 몰라
324
+ * 호출 스택이 넘쳤고, 한 방 실행은 그 RangeError 를 표준출력에 찍었다.
325
+ *
326
+ * 지금 재는 길 위에 (스키마, 값) 짝이 이미 있으면 고리다. 값을 파고들면 값이 바뀌고,
327
+ * JSON 에는 제 몸을 품는 값이 없으므로 끝이 있는 재귀(나무 모양 스키마)는 여기에 안
328
+ * 걸린다. 읽는 자리(스키마읽기)가 먼저 막지만, 이 함수를 곧장 부르는 쪽도 터지면 안 된다.
329
+ * 부르는 자리를 하나하나 고치지 않고 이름 하나로 감싼 것은, 안쪽 재귀가 전부 이 이름을
330
+ * 거쳐 가기 때문이다 — 새 열쇠를 재는 줄이 늘어도 울타리가 따라간다.
331
+ */
332
+ const 여기 = 옵션?.자리 || '(뿌리)';
333
+ if (재는길.some(([a, b]) => a === 스키마 && Object.is(b, 값))) {
334
+ return { ok: false, 탈: [`${여기}: 스키마가 제자리를 돕니다 — 같은 값을 같은 자리로 또 재려 합니다`] };
335
+ }
336
+ 재는길.push([스키마, 값]);
337
+ try { return 맞나속(값, 스키마, 옵션); } finally { 재는길.pop(); }
338
+ }
339
+
340
+ // 지금 재고 있는 (스키마, 값) 짝들 — 맞나 의 고리 울타리.
341
+ const 재는길 = [];
342
+
343
+ function 맞나속(값, 스키마, { 뿌리 = 스키마, 자리 = '' } = {}) {
178
344
  const 탈 = [];
179
345
  const 여기 = 자리 || '(뿌리)';
180
346
  const s = 스키마;
347
+ /*
348
+ * ── 참/거짓 그 자체가 스키마다 ──────────────────────────────────────────
349
+ *
350
+ * 바로 아래 줄이 「true/false 스키마와 $ref 를 먼저 푼다」 고 적어 두고는
351
+ * 정작 참·거짓은 안 풀고 있었다. 객체가 아니면 그대로 통과였다.
352
+ *
353
+ * 그래서 `false` — 규격이 「무엇도 안 된다」 고 못 박는 자리 — 가 **무엇이든
354
+ * 통과**였고, 그 열쇠는 아는열쇠 안에 있으니 「안 잰 열쇠」 목록에도 안 떴다.
355
+ * 화면은 다 쟀다고 하고, 아무것도 안 쟀다. 이 파일 머리말이 제일 나쁘다고
356
+ * 적어 둔 모양이 바로 그것이다.
357
+ */
358
+ if (s === true) return { ok: true, 탈 };
359
+ if (s === false) return { ok: false, 탈: [`${여기}: 여기에는 아무 값도 올 수 없습니다`] };
181
360
  if (!s || typeof s !== 'object') return { ok: true, 탈 };
182
361
 
183
- // true/false 스키마와 $ref 를 먼저 푼다.
362
+ /*
363
+ * `$ref` 옆의 형제 열쇠도 잰다.
364
+ *
365
+ * 여태는 가리킨 곳을 재고 **그 자리에서 돌아섰다.** 그러면
366
+ *
367
+ * { "$ref": "#/$defs/아이디", "minLength": 8 }
368
+ *
369
+ * 에서 minLength 가 한 번도 안 걸린다. 규격을 좁히려고 덧붙인 줄인데,
370
+ * 덧붙이면 덧붙인 만큼 조용히 사라진 셈이다. 가리킨 곳을 재고 **이어서**
371
+ * 형제도 잰다 — 탈은 원래 다 모아서 주는 자리라 합치기만 하면 된다.
372
+ */
184
373
  if (typeof s.$ref === 'string') {
185
374
  const 간것 = ref따라가기(뿌리, s.$ref);
186
- if (!간것) return { ok: false, 탈: [`${여기}: 스키마 안에 ${s.$ref} 가 없습니다`] };
187
- return 맞나(값, 간것, { 뿌리, 자리 });
375
+ /*
376
+ * 가리킨 곳이 `false` 여도 「없습니다」 라고 하면 안 된다 (2.0.0 8회차 스키마). 그 자리는
377
+ * 규격이 「무엇도 올 수 없다」 고 못 박은 자리라 있는 것이고, 사람은 스키마에 없는 오타를
378
+ * 찾으러 간다. 위에서 참/거짓 스키마를 푸니 그대로 넘기면 제 말이 나온다.
379
+ */
380
+ if (간것 === undefined) return { ok: false, 탈: [`${여기}: 스키마 안에 ${s.$ref} 가 없습니다`] };
381
+ 탈.push(...맞나(값, 간것, { 뿌리, 자리 }).탈);
188
382
  }
189
383
 
190
384
  if (Array.isArray(s.type) ? !s.type.some((t) => 갈래맞나(값, t)) : (s.type && !갈래맞나(값, s.type))) {
@@ -193,10 +387,10 @@ export function 맞나(값, 스키마, { 뿌리 = 스키마, 자리 = '' } = {})
193
387
  return { ok: false, 탈 };
194
388
  }
195
389
 
196
- if (Array.isArray(s.enum) && !s.enum.some((x) => JSON.stringify(x) === JSON.stringify(값))) {
390
+ if (Array.isArray(s.enum) && !s.enum.some((x) => 같은값(x, 값))) {
197
391
  탈.push(`${여기}: ${s.enum.map((x) => JSON.stringify(x)).join(' · ')} 중 하나여야 합니다`);
198
392
  }
199
- if ('const' in s && JSON.stringify(s.const) !== JSON.stringify(값)) {
393
+ if ('const' in s && !같은값(s.const, 값)) {
200
394
  탈.push(`${여기}: ${JSON.stringify(s.const)} 여야 합니다`);
201
395
  }
202
396
 
@@ -206,8 +400,9 @@ export function 맞나(값, 스키마, { 뿌리 = 스키마, 자리 = '' } = {})
206
400
  if (typeof s.pattern === 'string') {
207
401
  // 스키마를 적은 사람의 정규식이 잘못됐을 수 있다. 그것도 탈로 말한다 —
208
402
  // 조용히 통과시키면 안 재고 있는 규칙이 하나 생긴다.
209
- try { if (!new RegExp(s.pattern).test(값)) 탈.push(`${여기}: ${s.pattern} 모양이어야 합니다`); }
210
- catch { 탈.push(`${여기}: 스키마의 pattern 이 잘못된 정규식입니다 (${s.pattern})`); }
403
+ const 무늬 = 정규식짓기(s.pattern);
404
+ if (!무늬) 탈.push(`${여기}: 스키마의 pattern 이 잘못된 정규식입니다 (${s.pattern})`);
405
+ else if (!무늬.test(값)) 탈.push(`${여기}: ${s.pattern} 모양이어야 합니다`);
211
406
  }
212
407
  }
213
408
 
@@ -225,8 +420,38 @@ export function 맞나(값, 스키마, { 뿌리 = 스키마, 자리 = '' } = {})
225
420
  if (Array.isArray(값)) {
226
421
  if (Number.isFinite(s.minItems) && 값.length < s.minItems) 탈.push(`${여기}: ${s.minItems}개 이상이어야 합니다`);
227
422
  if (Number.isFinite(s.maxItems) && 값.length > s.maxItems) 탈.push(`${여기}: ${s.maxItems}개 이하여야 합니다`);
228
- if (s.items && typeof s.items === 'object' && !Array.isArray(s.items)) {
229
- 값.forEach((v, i) => 탈.push(...맞나(v, s.items, { 뿌리, 자리: `${여기}[${i}]` }).탈));
423
+ /*
424
+ * `items` 는 두 모양으로 온다.
425
+ *
426
+ * 하나는 「원소가 다 이 모양」 (스키마 하나). 여태 이것만 쟀다. 다른 하나는
427
+ * Draft-07 의 **자리별 모양** —
428
+ *
429
+ * "items": [{"type":"string"}, {"type":"number"}] ← [이름, 나이]
430
+ *
431
+ * 배열이면 위 `!Array.isArray` 에 걸려 통째로 안 쟀다. 그런데 'items' 는
432
+ * 아는열쇠 안에 있어서 「안 잰 열쇠」 에도 안 올랐다 — 안 재면서 잰다고
433
+ * 말한 자리다. 첫 칸에 숫자가 와도 그냥 나갔고, 그 값을 파이프 뒤의 jq 가
434
+ * 문자열로 알고 받았다.
435
+ *
436
+ * 자리별로 재고, 튜플보다 뒤에 오는 원소는 안 잰다 — 그건 additionalItems
437
+ * 가 정하는 것이고 우리는 그 열쇠를 모른다. 모르는 것은 모르는열쇠() 가 센다.
438
+ */
439
+ /*
440
+ * 2020-12 는 자리별 모양을 `prefixItems` 로 적고, 그때 `items` 는 **그 뒤** 원소들의 모양이다
441
+ * (2.0.0 6회차 모양6뒤). prefixItems 를 몰라서 `items` 를 0번부터 씌웠다 — `["a", 1]` 이
442
+ * 「[0]: number 이어야」 로 거절됐다.
443
+ */
444
+ const 자리별 = Array.isArray(s.prefixItems) ? s.prefixItems : (Array.isArray(s.items) ? s.items : null);
445
+ if (자리별) {
446
+ 자리별.forEach((칸, i) => {
447
+ if (i < 값.length) 탈.push(...맞나(값[i], 칸, { 뿌리, 자리: `${여기}[${i}]` }).탈);
448
+ });
449
+ }
450
+ const 뒤모양 = Array.isArray(s.prefixItems) ? s.items : (Array.isArray(s.items) ? undefined : s.items);
451
+ if (뒤모양 === true || 뒤모양 === false || (뒤모양 && typeof 뒤모양 === 'object' && !Array.isArray(뒤모양))) {
452
+ for (let i = Array.isArray(s.prefixItems) ? s.prefixItems.length : 0; i < 값.length; i += 1) {
453
+ 탈.push(...맞나(값[i], 뒤모양, { 뿌리, 자리: `${여기}[${i}]` }).탈);
454
+ }
230
455
  }
231
456
  }
232
457
 
@@ -235,13 +460,30 @@ export function 맞나(값, 스키마, { 뿌리 = 스키마, 자리 = '' } = {})
235
460
  if (!Object.prototype.hasOwnProperty.call(값, k)) 탈.push(`${여기}: ${k} 칸이 없습니다`);
236
461
  }
237
462
  const 칸들 = s.properties && typeof s.properties === 'object' ? s.properties : {};
463
+ /*
464
+ * `patternProperties` — 이름이 무늬에 맞는 칸의 모양 (2.0.0 6회차 모양6뒤). 몰라서 그런 칸이
465
+ * `additionalProperties: false` 에 「적으면 안 되는 칸」 으로 걸렸고, 무늬까지 안 재는 열쇠로 셌다.
466
+ * properties 와 무늬는 **둘 다** 씌운다(규격). 어느 쪽에도 안 걸린 칸만 additionalProperties 가 본다.
467
+ */
468
+ const 무늬들 = [];
469
+ if (s.patternProperties && typeof s.patternProperties === 'object') {
470
+ for (const [p, 칸] of Object.entries(s.patternProperties)) {
471
+ const re = 정규식짓기(p);
472
+ if (re) 무늬들.push([re, 칸]);
473
+ else 탈.push(`${여기}: 스키마의 patternProperties 무늬가 잘못된 정규식입니다 (${p})`);
474
+ }
475
+ }
238
476
  for (const [k, v] of Object.entries(값)) {
239
- if (Object.prototype.hasOwnProperty.call(칸들, k)) {
240
- 탈.push(...맞나(v, 칸들[k], { 뿌리, 자리: 여기 === '(뿌리)' ? k : `${여기}.${k}` }).탈);
241
- } else if (s.additionalProperties === false) {
477
+ const 칸자리 = 여기 === '(뿌리)' ? k : `${여기}.${k}`;
478
+ const 이름있음 = Object.prototype.hasOwnProperty.call(칸들, k);
479
+ if (이름있음) 탈.push(...맞나(v, 칸들[k], { 뿌리, 자리: 칸자리 }).탈);
480
+ const 맞은무늬 = 무늬들.filter(([re]) => re.test(k));
481
+ for (const [, 칸] of 맞은무늬) 탈.push(...맞나(v, 칸, { 뿌리, 자리: 칸자리 }).탈);
482
+ if (이름있음 || 맞은무늬.length) continue;
483
+ if (s.additionalProperties === false) {
242
484
  탈.push(`${여기}: ${k} 는 적으면 안 되는 칸입니다`);
243
485
  } else if (s.additionalProperties && typeof s.additionalProperties === 'object') {
244
- 탈.push(...맞나(v, s.additionalProperties, { 뿌리, 자리: 여기 === '(뿌리)' ? k : `${여기}.${k}` }).탈);
486
+ 탈.push(...맞나(v, s.additionalProperties, { 뿌리, 자리: 칸자리 }).탈);
245
487
  }
246
488
  }
247
489
  }
@@ -254,7 +496,13 @@ export function 맞나(값, 스키마, { 뿌리 = 스키마, 자리 = '' } = {})
254
496
  const 맞은수 = s.oneOf.filter((x) => 맞나(값, x, { 뿌리, 자리 }).ok).length;
255
497
  if (맞은수 !== 1) 탈.push(`${여기}: oneOf 중 정확히 하나에 맞아야 하는데 ${맞은수}개에 맞습니다`);
256
498
  }
257
- if (s.not && typeof s.not === 'object' && 맞나(값, s.not, { 뿌리, 자리 }).ok) {
499
+ /*
500
+ * `{"not": true}` 는 「무엇도 안 된다」 는 뜻이다 — false 스키마를 돌려 적은
501
+ * 꼴이고, 스키마를 만들어 주는 도구들이 실제로 그렇게 뱉는다. 여기가 객체만
502
+ * 보고 있어서 그 한 줄은 통째로 통과였다. 참·거짓도 스키마니 그대로 재 준다.
503
+ */
504
+ const not잴것 = s.not === true || s.not === false || (s.not && typeof s.not === 'object');
505
+ if (not잴것 && 맞나(값, s.not, { 뿌리, 자리 }).ok) {
258
506
  탈.push(`${여기}: not 에 걸립니다`);
259
507
  }
260
508
 
@@ -271,7 +519,7 @@ export function 맞나(값, 스키마, { 뿌리 = 스키마, 자리 = '' } = {})
271
519
  *
272
520
  * @returns {{ok: true, 값: any, 군말: boolean} | {ok: false, 왜: string}}
273
521
  */
274
- export function 답에서JSON뽑기(글) {
522
+ export function 답에서JSON뽑기(글, { 스키마 = null } = {}) {
275
523
  const s = String(글 ?? '').trim();
276
524
  if (!s) return { ok: false, 왜: '답이 비었습니다' };
277
525
 
@@ -280,42 +528,119 @@ export function 답에서JSON뽑기(글) {
280
528
  const 그대로 = 해보기(s);
281
529
  if (그대로) return { ...그대로, 군말: false };
282
530
 
283
- // ```json … ``` 울타리
284
- const 울타리 = /```(?:json|jsonc)?\s*\n([\s\S]*?)\n?```/i.exec(s);
285
- if (울타리) {
286
- const r = 해보기(울타리[1].trim());
287
- if (r) return { ...r, 군말: true };
531
+ /*
532
+ * ── 후보를 **다** 모은 뒤에 고른다 (사냥5 B5-06 · L5-4) ───────────────────────
533
+ *
534
+ * 여기는 첫 울타리 하나, 첫 `{`·`[` 하나만 보고 거기서 안 되면 포기했다. 그래서
535
+ * · 「See [docs] below. {"a":"x"}」 · 「- [x] done」 · 「Use {placeholder}」 처럼 JSON 앞에
536
+ * 괄호 낀 산문이 있으면 뒤의 멀쩡한 JSON 을 못 찾아 7 로 섰고,
537
+ * · 울타리가 둘인데 앞의 것이 예시면 **예시**를 답으로 냈다 — 예시가 모양에 맞으면
538
+ * 7 도 안 나고 지어낸 값이 파이프 뒤로 흘렀다.
539
+ *
540
+ * 고르는 규칙.
541
+ * 1) 울타리 안에서 읽힌 것이 하나라도 있으면 그것들만 본다. 「이것이 JSON 이다」 라고
542
+ * 모델이 표시한 자리라, 산문 속 `[1]` 같은 우연한 JSON 보다 믿을 만하다.
543
+ * 2) 없으면 맨글에서 짝이 맞는 괄호 덩이들을 앞에서부터 모은다(읽힌 덩이 안쪽은 건너뛴다).
544
+ * 3) 그중 스키마에 맞는 **마지막** 것 → 없으면 스키마 뿌리 갈래가 같은 마지막 것 →
545
+ * 그것도 없으면 그냥 마지막 것. 모델은 예시·되풀이를 앞에, 답을 끝에 둔다 — 시킬말도
546
+ * 「마지막 답은 JSON 하나」 라고 적는다. 스키마로 고르는 것은 그 규칙이 틀리는 날의
547
+ * 두 번째 그물이다. 맞는 것이 없을 때도 갈래가 같은 쪽을 골라야 되묻는 탈 목록이
548
+ * 엉뚱한 `[1]` 이야기가 아니라 진짜 답 이야기가 된다.
549
+ */
550
+ const 울타리후보 = [];
551
+ for (const m of s.matchAll(/```(?:jsonc?)?[ \t]*\r?\n?([\s\S]*?)```/gi)) {
552
+ const r = 해보기(m[1].trim());
553
+ if (r) 울타리후보.push(r);
288
554
  }
289
555
 
290
556
  /*
291
- * 앞뒤에 말이 붙은 경우. 첫 `{`(또는 `[`)부터 짝이 맞는 자리까지 잘라 본다.
557
+ * 울타리 안 것이 **하나도 스키마에 안 맞으면** 맨글도 본다 (2.0.0 6회차 · Gemini 백엔드5역).
292
558
  *
293
- * 따옴표 안의 괄호를 안 세는 것이 중요하다 — `{"글": "}"}` 를 깊이로만
294
- * 세면 첫 `}` 에서 끊긴다.
559
+ * 1) 을 그대로 두면 「예시는 이렇습니다: ```json {…} ``` 실제 답: {…}」 에서 울타리 안 예시만 보고
560
+ * 그걸 답으로 냈다 — 스키마를 같이 받아 놓고도 울타리 밖의 맞는 답을 못 봤다. 맨글에서도 스키마에
561
+ * **맞는** 것만 고르므로(아래 밖맞는것) 산문 속 우연한 `[1]` 은 여전히 안 뽑힌다.
295
562
  */
296
- const 시작 = Math.min(...['{', '['].map((ch) => { const i = s.indexOf(ch); return i < 0 ? Infinity : i; }));
297
- if (Number.isFinite(시작)) {
298
- const 열림 = s[시작];
299
- const 닫힘 = 열림 === '{' ? '}' : ']';
300
- let 깊이 = 0;
301
- let 글속 = false;
302
- for (let i = 시작; i < s.length; i++) {
303
- const ch = s[i];
304
- if (글속) {
305
- if (ch === '\\') { i++; continue; }
306
- if (ch === '"') 글속 = false;
307
- continue;
308
- }
309
- if (ch === '"') { 글속 = true; continue; }
310
- if (ch === 열림) 깊이++;
311
- else if (ch === 닫힘 && --깊이 === 0) {
312
- const r = 해보기(s.slice(시작, i + 1));
313
- if (r) return { ...r, 군말: true };
314
- break;
315
- }
563
+ const 스키마있나 = 스키마 !== null && 스키마 !== undefined;
564
+ const 맞음 = (r) => { try { return 맞나(r.값, 스키마).ok; } catch { return false; } };
565
+ const 울타리맞음 = 스키마있나 && 울타리후보.some(맞음);
566
+ const 맨글후보 = [];
567
+ if (!울타리후보.length || (스키마있나 && !울타리맞음)) {
568
+ let i = 0;
569
+ /*
570
+ * 괄호가 수천 개 박힌 긴 답에서 끝없이 돌지 않게 묶는다 — **시작 자리 수가 아니라 훑은 글자 수**로
571
+ * (2.0.0 6회차 모양6좁).
572
+ *
573
+ * 시작 자리 256번으로 묶었더니 「item[0] item[1] … 최종 답: {"a":1}」 처럼 앞 산문의 작은 괄호
574
+ * 하나하나가 멀쩡한 JSON 이라 한 번씩을 다 쓰고, 끝의 진짜 답에 닿기 전에 멈춰 `[255]` 를 냈다.
575
+ * 로그·코드 조각을 앞에 붙인 긴 답에서 그대로 생긴다. 비싼 것은 짝이 없는 괄호에서 글 끝까지
576
+ * 훑는 쪽이라, 그 글자 수를 세어 글 길이의 64배(작은 글은 최소 64KB)를 넘으면 멈춘다.
577
+ */
578
+ const 훑기예산 = Math.max(64 * s.length, 64 * 1024);
579
+ let 훑은 = 0;
580
+ while (i < s.length && 훑은 <= 훑기예산) {
581
+ const 다음 = 괄호찾기(s, i);
582
+ if (다음 < 0) break;
583
+ const 끝 = 짝찾기(s, 다음);
584
+ 훑은 += (끝 >= 0 ? 끝 + 1 : s.length) - 다음;
585
+ const r = 끝 >= 0 ? 해보기(s.slice(다음, 끝 + 1)) : null;
586
+ if (r) { 맨글후보.push(r); i = 끝 + 1; } else i = 다음 + 1;
587
+ }
588
+ }
589
+
590
+ const 후보들 = 울타리후보.length ? 울타리후보 : 맨글후보;
591
+ if (!후보들.length) return { ok: false, 왜: '답에서 JSON 을 못 찾았습니다' };
592
+ let 고른것 = 후보들.at(-1);
593
+ if (스키마있나) {
594
+ const 맞는것 = 후보들.filter(맞음);
595
+ // 울타리 안에 맞는 것이 없을 때만 — 울타리 밖에서 스키마에 맞는 것 (위 6회차 머리말).
596
+ const 밖맞는것 = 울타리후보.length && !맞는것.length ? 맨글후보.filter(맞음) : [];
597
+ const 갈래들 = 스키마 && typeof 스키마 === 'object'
598
+ ? (Array.isArray(스키마.type) ? 스키마.type : (typeof 스키마.type === 'string' ? [스키마.type] : null))
599
+ : null;
600
+ const 같은갈래 = 갈래들 ? 후보들.filter((r) => 갈래들.some((t) => 갈래맞나(r.값, t))) : [];
601
+ // 울타리 안에 맞는 것도 같은 갈래도 없을 때 — 울타리 밖의 같은 갈래 (2.0.0 6회차 모양6좁).
602
+ // 이게 없으면 「예시: ```json [1,2]``` 답: {"a":1}」 에서 {"a":1} 을 두고 갈래도 다른 예시를 냈다.
603
+ // 맞는 것이 없으니 되묻기로 가는 길이지만, 그 탈 목록이 진짜 답 이야기여야 모델이 고친다.
604
+ const 밖같은갈래 = 갈래들 && 울타리후보.length && !맞는것.length && !같은갈래.length
605
+ ? 맨글후보.filter((r) => 갈래들.some((t) => 갈래맞나(r.값, t)))
606
+ : [];
607
+ 고른것 = 맞는것.at(-1) ?? 밖맞는것.at(-1) ?? 같은갈래.at(-1) ?? 밖같은갈래.at(-1) ?? 고른것;
608
+ }
609
+ return { ...고른것, 군말: true };
610
+ }
611
+
612
+ // i 자리부터 처음 나오는 `{` 또는 `[` 의 자리. 없으면 -1.
613
+ function 괄호찾기(s, i) {
614
+ const a = s.indexOf('{', i);
615
+ const b = s.indexOf('[', i);
616
+ if (a < 0) return b;
617
+ if (b < 0) return a;
618
+ return Math.min(a, b);
619
+ }
620
+
621
+ /*
622
+ * 시작 괄호와 짝이 맞는 닫는 괄호의 자리. 없으면 -1.
623
+ *
624
+ * 따옴표 안의 괄호를 안 세는 것이 중요하다 — `{"글": "}"}` 를 깊이로만 세면 첫 `}`
625
+ * 에서 끊긴다. 따옴표는 **시작 괄호부터** 센다 — 그 앞 산문의 따옴표는 JSON 이 아니다.
626
+ */
627
+ function 짝찾기(s, 시작) {
628
+ const 열림 = s[시작];
629
+ const 닫힘 = 열림 === '{' ? '}' : ']';
630
+ let 깊이 = 0;
631
+ let 글속 = false;
632
+ for (let i = 시작; i < s.length; i++) {
633
+ const ch = s[i];
634
+ if (글속) {
635
+ if (ch === '\\') { i++; continue; }
636
+ if (ch === '"') 글속 = false;
637
+ continue;
316
638
  }
639
+ if (ch === '"') { 글속 = true; continue; }
640
+ if (ch === 열림) 깊이++;
641
+ else if (ch === 닫힘 && --깊이 === 0) return i;
317
642
  }
318
- return { ok: false, 왜: '답에서 JSON 을 못 찾았습니다' };
643
+ return -1;
319
644
  }
320
645
 
321
646
  /**
package/src/agent/pins.js CHANGED
@@ -61,7 +61,26 @@ export class 못박기 {
61
61
  return { ok: false, why: `못 박을 수 있는 것은 ${최대개수}개까지입니다 — 매 턴 실리는 글이라 상한을 둡니다.` };
62
62
  }
63
63
  this.것들.push(s);
64
- return { ok: true, 번호: this.것들.length };
64
+ /*
65
+ * ── 받은 것은 실려야 한다 (2.0.0 6회차 · Gemini 못박기6u W3) ─────────────
66
+ *
67
+ * 개수만 보고 받았다. 그런데 프롬프트에 싣는 쪽(#고른것)은 토큰 상한에서
68
+ * 끊는다 — 영어 200자 핀 12개가 다 ok 로 박히고 실린 것은 3개, 한글은 180자
69
+ * 한 줄이면 혼자서도 안 실렸다. /pin 추가 화면은 「접거나 요약해도 안
70
+ * 지워집니다」 만 찍으니, 사람은 말해 두었다고 믿는데 모델은 그 말을 못
71
+ * 받는다. 이 파일이 막으려던 바로 그 꼴이다. 안 실릴 것이면 받지 않는다.
72
+ */
73
+ if (this.#고른것(최대토큰).개수 < this.것들.length) {
74
+ this.것들.pop();
75
+ return { ok: false, why: `프롬프트에 실을 자리(${최대토큰}토큰 상한)가 모자라 이 말은 매 턴 실리지 못합니다 — 더 짧게 줄이거나 /pin 지우기 로 자리를 비우세요.` };
76
+ }
77
+ /*
78
+ * 박힌 말과 잘렸는지를 같이 돌려준다 (2.0.0 6회차 · Gemini 스레드6). 한줄최대 를 넘긴 말은
79
+ * 다듬기() 가 뒤를 자르는데, 화면은 사람이 친 원문을 박았다고 보여 줬다 — 「운영 DB 는 … 단
80
+ * 읽기는 된다」 의 뒤쪽 조건이 빠진 채 박힌 줄 모른다. 화면은 이 말을 보여 줘야 한다.
81
+ */
82
+ const 잘렸나 = String(말 ?? '').replace(/\s+/g, ' ').trim().length > s.length;
83
+ return { ok: true, 번호: this.것들.length, 말: s, 잘렸나 };
65
84
  }
66
85
 
67
86
  /**
@@ -136,5 +155,17 @@ export class 못박기 {
136
155
 
137
156
  function 다듬기(말) {
138
157
  const s = String(말 ?? '').replace(/\s+/g, ' ').trim();
139
- return s ? s.slice(0, 한줄최대) : '';
158
+ if (!s) return '';
159
+ let t = s.slice(0, 한줄최대);
160
+ /*
161
+ * ── 반쪽 글자를 안 남긴다 (2.0.0 6회차 · Gemini 못박기6u W2) ─────────────
162
+ *
163
+ * slice 는 UTF-16 칸으로 자른다. 200번째 칸에 이모지 앞쪽 반이 걸리면 짝 없는
164
+ * 서로게이트 하나가 남는다. 이 글은 **매 턴** 시스템 프롬프트에 실리므로, 그런
165
+ * 글자를 JSON 에서 거절하는 서버라면 사람이 핀을 뺄 때까지 모든 턴이 깨진다.
166
+ * 되살릴 때도 여기를 지나므로 옛 기록에 남은 반쪽도 같이 떨어진다.
167
+ */
168
+ const 끝 = t.charCodeAt(t.length - 1);
169
+ if (끝 >= 0xd800 && 끝 <= 0xdbff) t = t.slice(0, -1);
170
+ return t;
140
171
  }