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
@@ -75,7 +75,10 @@ export const PROFILES = {
75
75
  en: 'deep',
76
76
  desc: '전 단계 한 칸씩 위로 — 어려운 일에만',
77
77
  descEn: 'Every stage one notch up - for hard work only',
78
- shift: { plan: +1, work: 0, fix: +1 },
78
+ // 「전 단계」 는 이어가기도 뜻한다. 여기만 0 이라 깊게를 골라도 그 단계는
79
+ // 균일과 똑같이 돌았다 — 그런데 파일을 쓰는 호출은 대개 이어가기에서
80
+ // 나온다. 어려운 일에만 쓰라고 만든 배분이 정작 일하는 자리에서 안 깊었다.
81
+ shift: { plan: +1, work: +1, fix: +1 },
79
82
  share: { plan: 0.50, work: 0.45, fix: 0.50 },
80
83
  },
81
84
  };
@@ -126,7 +129,9 @@ export function normalizeProfile(v) {
126
129
  const 친것 = String(v ?? '').trim();
127
130
  const k = 친것.toLowerCase();
128
131
  if (Object.hasOwn(PROFILES, k)) return k;
129
- return Object.hasOwn(ALIAS, 친것) ? ALIAS[친것] : null;
132
+ // 별칭도 **같은 잣대**로 본다. 여기만 친 그대로 봤더니 `DEEP` 은 되는데
133
+ // `Uniform` 은 「모르는 배분입니다」 였다 — 한 줄 위아래가 서로 달랐다.
134
+ return Object.hasOwn(ALIAS, k) ? ALIAS[k] : null;
130
135
  }
131
136
 
132
137
  // 기준 강도에서 몇 칸 옮긴다. 끝을 넘지 않는다.
@@ -174,8 +179,15 @@ export function effortFor(base, profileKey, stage, { 고정 = false } = {}) {
174
179
  /** 대화가 이만큼 쌓이면 강도를 더 안 움직인다 — 그때부터는 캐시가 더 비싸다. */
175
180
  const 유지문턱 = 8;
176
181
 
177
- /** 인사·맞장구. 시킨 일이 없으면 깊이 생각할 것도 없다. */
178
- export const 인사말 = /^(안녕[가-힣]*|반(가|갑)[가-힣]*|하이|ㅎㅇ+|헬로[우가-힣]*|고마[가-힣]*|감사[가-힣]*|수고[가-힣]*|hi|hello|hey|yo|thanks?|thank you|테스트|test|ok(ay)?|네|응)[\s!.~?ㅎㅋ,]*$/i;
182
+ /*
183
+ * 인사·맞장구. 시킨 일이 없으면 깊이 생각할 것도 없다.
184
+ *
185
+ * `고[마맙]` 인 까닭 — 이 말은 ㅂ 불규칙이라 정중해질수록 어간이 바뀐다.
186
+ * `고마` 만 잡으면 반말(`고마워`)은 걸리고 존댓말(`고맙습니다`)은 안 걸리는데,
187
+ * 하필 뒤엣것이 더 흔하다. 안 걸리면 「고맙습니다」 한 마디가 max 로 생각해서
188
+ * 그 판에서 제일 비싼 토큰이 된다.
189
+ */
190
+ export const 인사말 = /^(안녕[가-힣]*|반(가|갑)[가-힣]*|하이|ㅎㅇ+|헬로[우가-힣]*|고[마맙][가-힣]*|감사[가-힣]*|수고[가-힣]*|hi|hello|hey|yo|thanks?|thank you|테스트|test|ok(ay)?|네|응)[\s!.~?ㅎㅋ,]*$/i;
179
191
 
180
192
  export function 인사인가(글) {
181
193
  return 인사말.test(String(글 ?? '').trim());
@@ -327,6 +339,18 @@ export function 가벼운강도(천장, { 켜짐 = true } = {}) {
327
339
  * @param {number} o.used 지금 쓰고 있는 양 (session.breakdown().used)
328
340
  * @param {number} o.max 사용자가 프로필에 직접 적어 둔 상한이 있으면 그것을 넘지 않는다
329
341
  */
342
+ /*
343
+ * 0 · NaN 같은 상한은 「모른다」 로 읽는다 (사냥5 L5-6).
344
+ *
345
+ * 설정에서 maxTokens 0 이 흘러오면 `max ?? MAX_CAP` 가 0 을 그대로 받아 상한이 바닥
346
+ * (MIN_CAP)에 붙었고, NaN 이면 셈 전체가 NaN 이 되어 요청의 max_tokens 가 비었다.
347
+ * 값을 거르는 것은 설정 쪽 몫이지만, 여기서 받는 쪽도 모르는 값으로 물러선다.
348
+ */
349
+ const 아는상한 = (max) => {
350
+ const n = Number(max);
351
+ return max != null && Number.isFinite(n) && n > 0 ? n : null;
352
+ };
353
+
330
354
  export function tokensFor(profileKey, stage, { ctx = 0, used = 0, max = null } = {}) {
331
355
  const p = PROFILES[normalizeProfile(profileKey) ?? 'save'];
332
356
  const share = p.share[stage] ?? 0.3;
@@ -338,16 +362,24 @@ export function tokensFor(profileKey, stage, { ctx = 0, used = 0, max = null } =
338
362
  // 남은 자리의 절반을 넘겨 주지 않는다. 답이 길어져도 다음 턴이 들어갈 자리는 남겨야 한다.
339
363
  cap = Math.min(cap, Math.floor(room / 2));
340
364
  // 아는 상한이 있으면 그것을 따른다. 모를 때만 MAX_CAP 에 선다.
341
- cap = Math.min(cap, max ?? MAX_CAP);
365
+ cap = Math.min(cap, 아는상한(max) ?? MAX_CAP);
342
366
  cap = Math.max(cap, MIN_CAP);
343
- // 남은 자리 자체가 바닥이면 울타리 아래라도 남은 만큼만 준다.
344
- return room > 0 ? Math.min(cap, Math.max(MIN_CAP, room)) : MIN_CAP;
367
+ /*
368
+ * 남은 자리 자체가 바닥이면 울타리 아래라도 남은 만큼만 준다.
369
+ *
370
+ * 여기에 `Math.max(MIN_CAP, room)` 이 한 번 더 깔려 있어서, 남은 자리가
371
+ * 100 이어도 512 가 나갔다 — 주석만 고쳐진 자리였다. 남은 자리보다 큰
372
+ * max_tokens 를 받은 게이트웨이는 답 대신 400 을 낸다. 그러면 「자리가
373
+ * 모자란다」 가 「요청이 틀렸다」 로 보여서, 접기로 풀 수 있던 자리에서
374
+ * 사람이 설정을 뒤진다.
375
+ */
376
+ return room > 0 ? Math.min(cap, room) : MIN_CAP;
345
377
  }
346
378
 
347
379
  /** 잘렸을 때 풀어 줄 최대 상한 — 남은 자리를 거의 다 내준다. */
348
380
  export function fullCap({ ctx = 0, used = 0, max = null } = {}) {
349
381
  const room = ctx > 0 ? Math.max(0, ctx - used) : 4096;
350
- return Math.max(MIN_CAP, Math.min(Math.floor(room * 0.8), max ?? MAX_CAP));
382
+ return Math.max(MIN_CAP, Math.min(Math.floor(room * 0.8), 아는상한(max) ?? MAX_CAP));
351
383
  }
352
384
 
353
385
  /**
@@ -361,14 +393,46 @@ export function fullCap({ ctx = 0, used = 0, max = null } = {}) {
361
393
  * 일부러 만들지 않는다. 그래서 말을 안 해 줘도 이걸 보고 안다.
362
394
  */
363
395
  export function wasCut(msg) {
364
- const s = String(msg?.stopped ?? '');
365
- if (s === 'length' || s === 'max_tokens' || s === 'MAX_TOKENS') return true;
366
- return (msg?.toolCalls ?? []).some((t) => t?.argsBroken);
396
+ // 게이트웨이마다 대소문자가 다르다(`Length` · `Max_Tokens`). 소문자로 맞춰 본다 — 6회차 Gemini 고리5.
397
+ const s = String(msg?.stopped ?? '').toLowerCase();
398
+ if (s === 'length' || s === 'max_tokens') return true;
399
+ /*
400
+ * 다만 **끝까지 온 틀린 JSON** 은 잘린 증거가 아니다 (사냥5 L5-5).
401
+ *
402
+ * 모델은 반쪽 JSON 을 일부러 안 만들지만, 홑따옴표·끝 쉼표·글 속 날줄바꿈으로 틀린
403
+ * JSON 은 곧잘 만든다. 그것까지 잘린 것으로 치면 걸음마다 상한을 올려 다시 부르고
404
+ * /out 을 고치라고 한다. 어댑터가 괄호·따옴표가 다 닫힌 것에 argsCut:false 를 붙인다
405
+ * (backend/adapter.js 의 잘린모양인가). 그 표가 없는 부름은 여태처럼 잘린 것으로 본다.
406
+ */
407
+ return (msg?.toolCalls ?? []).some((t) => t?.argsBroken && t.argsCut !== false);
367
408
  }
368
409
 
369
410
  // 화면에 보여줄 표. 상한은 지금 붙어 있는 모델 기준으로 계산해서 보여준다 —
370
411
  // 모델을 바꾸면 이 숫자도 같이 바뀐다.
371
- export function table(base, profileKey, room = {}) {
412
+ /*
413
+ * ── 여기 적히는 두 숫자는 **정말로 나가는 값**이어야 한다 ────────────────
414
+ *
415
+ * 이 표가 있는 까닭은 하나다 — 「max 라고 정했는데 왜 medium 인가」 에 답하는 것
416
+ * (agent/loop.js 가 stage 를 올려 보내는 자리에 그렇게 적혀 있다). 그런데 여태
417
+ * 이 표는 걸음을 도는 자리와 **따로** 같은 숫자를 한 번 더 셈했고, 둘이 갈렸다.
418
+ *
419
+ * 강도: 걸음 쪽은 `{ 고정 }` 을 들고 부른다. 바깥 모델·게이트웨이에 붙으면
420
+ * 강도가 단계별로 안 움직인다 — 요청마다 눈금이 바뀌면 그것만으로
421
+ * 60k 짜리 앞머리가 통째로 다시 나가기 때문이다(위 effortFor 머리말).
422
+ * 여기는 그 뜻을 안 받아서, 바깥 모델에서 화면은 「첫 판단 high ·
423
+ * 이어가기 medium · 막혔을 때 xhigh」 라고 적는데 세 번 다 high 가 나갔다.
424
+ * 상한: 인자를 자주 잘라 먹는 모델에는 처음부터 넉넉히 준다(agent/card.js 의
425
+ * 상한먼저올리기). 그러면 실제 상한은 fullCap 까지 올라가는데, 화면은
426
+ * 낮은 쪽만 적어서 「이만큼밖에 안 준다」 로 읽혔다.
427
+ *
428
+ * 답을 주라고 만든 화면이 틀린 답을 주면 안 보여 주느니만 못하다. 그래서 걸음이
429
+ * 쓰는 값을 **그대로 받아** 셈한다. 안 주면 여태처럼 군다 — 옛 호출부가 그대로 돈다.
430
+ *
431
+ * @param {object} o
432
+ * @param {boolean} o.고정 단계별로 안 움직이나 (loop.js 의 강도고정)
433
+ * @param {boolean} o.상한먼저올리기 잘리는 버릇이 재어진 모델인가 (card.js 의 조정)
434
+ */
435
+ export function table(base, profileKey, room = {}, { 고정 = false, 상한먼저올리기 = false } = {}) {
372
436
  const key = normalizeProfile(profileKey) ?? 'save';
373
437
  const p = PROFILES[key];
374
438
  /*
@@ -387,14 +451,20 @@ export function table(base, profileKey, room = {}) {
387
451
  desc: 한국어 ? p.desc : (p.descEn ?? p.desc),
388
452
  ctx: room.ctx ?? 0,
389
453
  used: room.used ?? 0,
454
+ // 왜 세 줄이 같은지를 화면이 말할 수 있어야 한다. 값만 같게 고쳐 두면
455
+ // 「배분을 골라 놨는데 왜 안 먹나」 라는 다음 물음이 그대로 남는다.
456
+ 고정,
457
+ 상한먼저올리기,
390
458
  rows: Object.entries(STAGES).map(([stage, s]) => ({
391
459
  stage,
392
460
  label: 한국어 ? s.label : (s.en ?? s.label),
393
461
  why: 한국어 ? s.why : (s.whyEn ?? s.why),
394
- level: effortFor(base, key, stage),
395
- cap: tokensFor(key, stage, room),
462
+ level: effortFor(base, key, stage, { 고정 }),
463
+ cap: 상한먼저올리기
464
+ ? Math.max(tokensFor(key, stage, room), fullCap(room))
465
+ : tokensFor(key, stage, room),
396
466
  share: p.share[stage],
397
- moved: p.shift[stage],
467
+ moved: 고정 ? 0 : p.shift[stage],
398
468
  })),
399
469
  };
400
470
  }
@@ -33,7 +33,35 @@ import { writeFileSync, mkdirSync } from 'node:fs';
33
33
  import { join } from 'node:path';
34
34
 
35
35
  /** 이 명령들은 '확인했다' 로 친다. 돌렸다고 다 증거가 되는 것은 아니다. */
36
- const 확인하는명령 = /(^|[\s|;&])(npm|pnpm|yarn|bun)\s+(test|run\s+(test|check|lint|build|verify|typecheck))|(^|[\s|;&])(pytest|jest|vitest|mocha|go\s+test|cargo\s+test|mvn\s+test|gradle\s+test|dotnet\s+test|tsc|eslint|ruff|mypy)\b/i;
36
+ const 확인하는명령 = /^(npm|pnpm|yarn|bun)\s+(test|run\s+(test|check|lint|build|verify|typecheck))\b|^(pnpm|yarn|bun)\s+(check|lint|build|verify|typecheck)\b|^(pytest|jest|vitest|mocha|go\s+test|cargo\s+test|mvn\s+test|gradle\s+test|dotnet\s+test|tsc|eslint|ruff|mypy)\b/i;
37
+ /*
38
+ * `run` 없이 쓰는 꼴도 잡는다 (2.0.0 8회차 판정).
39
+ *
40
+ * npm 만 스크립트에 `run` 이 있어야 하고 pnpm·yarn·bun 은 없어도 된다. 앞은 npm 규약
41
+ * 하나만 알고 있어서 `pnpm check` · `yarn lint` 가 「안 돌린 것」 으로 떨어졌다 — pnpm 을
42
+ * 쓰는 곳은 확인을 아무리 돌려도 보고서가 늘 「증명 안 됨」 이었다. 두 번째 갈래가 그
43
+ * 세 도구에 한해 맨 이름을 받는다. `pnpm install` 처럼 확인이 아닌 것은 여전히 안 잡힌다.
44
+ */
45
+ /*
46
+ * 명령 이름은 **조각 맨 앞**에서만 본다 (6회차 근거6at EV2).
47
+ *
48
+ * 앞에 빈칸만 있으면 되던 때는 인자 자리도 잡았다 — `cat eslint.config.js` 는 파일을
49
+ * 읽었을 뿐인데 「확인한 것」 칸에 올랐다. 그래서 `&& || ; | &` 로 조각내고, 앞에 붙는
50
+ * 것(환경변수 · npx · pnpm exec · python -m · uv/poetry run · sh -c · timeout · 여는
51
+ * 괄호·따옴표)을 뗀 뒤 맨 앞만 본다. `cd app && npm test` · `npx jest` 는 그대로 잡힌다.
52
+ */
53
+ const 앞에붙는것 = /^(?:[A-Za-z_]\w*=\S*\s+|(?:npx|bunx|env|time)\s+|timeout\s+\S+\s+|(?:pnpm|yarn)\s+(?:exec|dlx)\s+|python3?\s+-m\s+|(?:uv|poetry|pipenv)\s+run\s+|(?:ba)?sh\s+-c\s+|[("'`]+\s*)/i;
54
+ function 확인하는명령인가(명령) {
55
+ return String(명령 ?? '').split(/&&|\|\||[;|&\n]/).some((조각) => {
56
+ let 앞 = 조각.trim();
57
+ for (let 번 = 0; 번 < 8; 번 += 1) {
58
+ const 뗀것 = 앞.replace(앞에붙는것, '');
59
+ if (뗀것 === 앞) break;
60
+ 앞 = 뗀것;
61
+ }
62
+ return 확인하는명령.test(앞);
63
+ });
64
+ }
37
65
 
38
66
  /** 우리 쪽 확인 도구. Bash 를 안 거치고 확인하는 길이다. */
39
67
  const 확인하는도구 = new Set(['Verify', 'Test']);
@@ -70,11 +98,13 @@ export function 증거모으기(session, { audit = null, 최근 = 400, 바꾼때
70
98
  if (도구 !== 'Bash' && !확인하는도구.has(도구)) continue;
71
99
  돌린것.push({
72
100
  도구,
73
- 무엇: String(r.target ?? ''),
101
+ // target 이 비면 도구 이름으로 적는다. Verify 는 인자 없이 부르는 것이 보통이라(required: [])
102
+ // 비워 두면 통과한 확인이 「증명 없음」 으로 버려졌다 (6회차 근거6at EV3).
103
+ 무엇: String(r.target ?? '') || 도구,
74
104
  됐나: r.ok !== false,
75
105
  남긴말: r.ok === false ? `실패 — ${String(r.note ?? '').slice(0, 120)}` : String(r.note ?? '').slice(0, 120),
76
106
  때: r.at ?? null,
77
- 확인인가: 확인하는도구.has(도구) || 확인하는명령.test(String(r.target ?? '')),
107
+ 확인인가: 확인하는도구.has(도구) || 확인하는명령인가(r.target),
78
108
  });
79
109
  }
80
110
 
@@ -84,10 +114,20 @@ export function 증거모으기(session, { audit = null, 최근 = 400, 바꾼때
84
114
  const 바꾼것 = [];
85
115
  const 증명안된것 = [];
86
116
  for (const [파일, d] of session?.changes ?? new Map()) {
87
- // 고친 **뒤에** 돌린 확인만 본다.
117
+ /*
118
+ * 고친 **뒤에** 돌린 확인만 본다 — 그리고 그 「고친 때」 는 **파일마다** 다르다
119
+ * (2.0.0 8회차 판정).
120
+ *
121
+ * 여기는 `언제고쳤나` 하나만 봤다. 그 값은 부르는 쪽이 넘겨 줘야 했는데
122
+ * 세 자리(commit·work·export) 중 아무도 안 넘겼다 — 늘 null 이라 순서를 한 번도
123
+ * 안 따졌고, 고치기 **전**에 돌린 검사가 그대로 증거가 됐다. 그리고 기준이
124
+ * 하나뿐이라, 파일 A 를 고치고 검사한 뒤 파일 B 를 고쳐도 B 에까지 그 초록이
125
+ * 복제됐다. 이제 세션이 파일마다 `at` 을 남긴다(session.js 의 noteChange).
126
+ */
127
+ const 이파일고친때 = 언제고쳤나 ?? (Number.isFinite(d?.at) ? d.at : null);
88
128
  const 쓸것 = 확인시도.filter((x) => {
89
- if (언제고쳤나 == null || !x.때) return true; // 시각을 모르면 순서를 안 따진다
90
- return new Date(x.때).getTime() >= 언제고쳤나;
129
+ if (이파일고친때 == null || !x.때) return true; // 시각을 모르면 순서를 안 따진다
130
+ return new Date(x.때).getTime() >= 이파일고친때;
91
131
  });
92
132
  /*
93
133
  * **마지막** 확인이 증거다. 성공한 것 중 마지막이 아니다.
@@ -103,7 +143,7 @@ export function 증거모으기(session, { audit = null, 최근 = 400, 바꾼때
103
143
  if (!증명) {
104
144
  증명안된것.push({
105
145
  파일,
106
- 왜: 왜못믿나(돌린것, 마지막, 언제고쳤나),
146
+ 왜: 왜못믿나(돌린것, 마지막, 이파일고친때),
107
147
  });
108
148
  }
109
149
  }
@@ -162,6 +202,22 @@ export function 증거글(증거, { 제목 = '작업 증거' } = {}) {
162
202
  }
163
203
 
164
204
  // 이 절이 이 글의 요점이다. 비어 있으면 비어 있다고 분명히 적는다.
205
+ /*
206
+ * 기록이 샜으면 **글에도** 적는다 (2.0.0 8회차 판정).
207
+ *
208
+ * 위에서 「짧은 채로 내보내면 안 된다」 고 적어 두고 `기록못씀` 을 모으기만 했다.
209
+ * 화면(work.js)은 말해 줬지만 파일로 남는 것은 이 글이다 — 나중에 이 글만 보는 사람은
210
+ * 위 표가 **전부**인 줄 안다. 몇 줄이 빠졌는지 모르면 표의 「없음」 도 못 믿는다.
211
+ */
212
+ const 샘 = 증거?.기록못씀;
213
+ const 샌수 = Number(샘?.수 ?? 샘?.count ?? 0);
214
+ if (샘 && (샌수 > 0 || 샘.까닭 || 샘.why)) {
215
+ const 까닭 = 샘.까닭 ?? 샘.why ?? null;
216
+ 줄.push(`## 이 보고서는 온전하지 않습니다`, ``);
217
+ 줄.push(`- 감사기록을 **${샌수 || `몇`}줄** 못 적었습니다${까닭 ? ` — ${까닭}` : ``}.`);
218
+ 줄.push(`- 그만큼 위의 「돌린 것」 이 실제보다 적습니다. 「없음」 이 정말 안 돌렸다는 뜻이 아닐 수 있습니다.`, ``);
219
+ }
220
+
165
221
  줄.push('## 증명 안 된 것', '');
166
222
  if (!e.증명안된것?.length) 줄.push('없습니다 — 바꾼 것마다 뒤에 돌린 확인이 있습니다.', '');
167
223
  else {
@@ -37,16 +37,16 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
37
37
  import { join, dirname } from 'node:path';
38
38
  import { estimateTokens } from './session.js';
39
39
  /*
40
- * 「이걸 사실로 말해도 되나」 는 **여기서 안 정한다** (agent/신뢰도.js).
40
+ * 「이걸 사실로 말해도 되나」 는 **여기서 안 정한다** (agent/confidence.js).
41
41
  *
42
42
  * 이 파일은 무엇을 겪었나를 센다. 그 셈을 얼마나 믿을지는 다른 일이고,
43
43
  * 섞어 두면 셈을 고칠 때마다 믿음의 문턱이 같이 흔들린다 — 그리고 그건
44
44
  * 화면에 안 나타난다.
45
45
  */
46
- import { 삭힘, 며칠됐나, 뭐라할까, 하루할인된다 } from './신뢰도.js';
47
- // 파일에서 읽은 것이 **우리가 적은 모양이 맞나** (agent/성한기록.js).
46
+ import { 삭힘, 며칠됐나, 뭐라할까, 하루할인된다, 부스러기 } from './confidence.js';
47
+ // 파일에서 읽은 것이 **우리가 적은 모양이 맞나** (agent/recordshape.js).
48
48
  // 통계와 문지기는 다른 일이라 따로 둔다.
49
- import { 거른표 } from './성한기록.js';
49
+ import { 거른표 } from './recordshape.js';
50
50
 
51
51
  export const 최대토큰 = 220; // 프롬프트에 실을 상한
52
52
  const 명령최대 = 40; // 표가 끝없이 자라지 않게
@@ -55,7 +55,7 @@ const 모델최대 = 10;
55
55
  const 빈것 = () => ({ 판: 1, 명령: {}, 모델: {} });
56
56
 
57
57
  /*
58
- * 파일에서 읽은 것을 **그대로 믿지 않는다** (agent/성한기록.js 의 거른표).
58
+ * 파일에서 읽은 것을 **그대로 믿지 않는다** (agent/recordshape.js 의 거른표).
59
59
  *
60
60
  * 이 표의 열쇠는 시스템 프롬프트에 그대로 실린다. 그러니 여기가 프롬프트로
61
61
  * 들어가는 문이고, 문에는 자물쇠가 있어야 한다. 손으로 고쳤을 수도, 쓰다 만
@@ -64,12 +64,33 @@ const 빈것 = () => ({ 판: 1, 명령: {}, 모델: {} });
64
64
  * 걸러낸 것은 **버린다** — 파일에는 그대로 두고 이 판에서 안 쓸 뿐이다.
65
65
  * 모양이 안 맞는 것을 고쳐서 쓰려 들면 무엇을 어떻게 고쳤는지가 또 안 보인다.
66
66
  */
67
+ /*
68
+ * ── 모델·전선 표도 모양을 잰다 ──────────────────────────────────────────
69
+ *
70
+ * 명령 표만 거르고 있었다. JSON 으로는 멀쩡한 `{"모델":null}` · `{"모델":"abc"}` ·
71
+ * `{"전선":"x"}` · `{"모델":{"gpt-x":"str"}}` 한 줄이면 모델본것·전선본것이 그
72
+ * 값에 칸을 달다가 TypeError 로 던졌다. 부르는 자리(loop.js)는 안 감싸 있어서
73
+ * 대화가 통째로 죽었다 — 사람이 손으로 고치는 파일 한 칸 때문에.
74
+ *
75
+ * 이 두 표는 프롬프트 열쇠가 아니라 명령 표만큼 좁게 거를 까닭은 없다. 「칸을 달 수
76
+ * 있는 꼴인가」 만 본다: 표도 객체, 칸마다 값도 객체. 아닌 칸은 명령 표처럼 버린다.
77
+ */
78
+ function 객체인가(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
79
+ function 칸표(표) {
80
+ const out = {};
81
+ if (!객체인가(표)) return out;
82
+ for (const [k, v] of Object.entries(표)) if (객체인가(v)) out[k] = v;
83
+ return out;
84
+ }
85
+
67
86
  function 읽기(파일) {
68
87
  try {
69
88
  if (!existsSync(파일)) return 빈것();
70
89
  const j = JSON.parse(readFileSync(파일, 'utf8'));
71
- const 것 = { ...빈것(), ...(j && typeof j === 'object' ? j : {}) };
90
+ const 것 = { ...빈것(), ...(객체인가(j) ? j : {}) };
72
91
  것.명령 = 거른표(것.명령);
92
+ 것.모델 = 칸표(것.모델);
93
+ if (것.전선 !== undefined) 것.전선 = 칸표(것.전선);
73
94
  return 것;
74
95
  } catch { return 빈것(); }
75
96
  }
@@ -81,6 +102,30 @@ function 쓰기(파일, 값) {
81
102
  } catch { /* 못 적어도 대화는 계속돼야 한다 */ }
82
103
  }
83
104
 
105
+ /*
106
+ * 버릇 셈을 오늘 기준으로 삭힌다 (agent/confidence.js 의 하루할인된다).
107
+ *
108
+ * 명령 표는 삭힘() 이 `{ok, no}` 두 칸을 맡지만, 모델 칸은 칸 이름이 정해져
109
+ * 있지 않다(걸음·잘린인자·편집실패·되풀이·빈답…). 그래서 숫자 칸을 통째로
110
+ * 훑는다.
111
+ *
112
+ * **`보정` 만 비켜 간다.** 그건 셈이 아니라 토큰 추정 배수라, 삭히면 1 쪽이
113
+ * 아니라 0 쪽으로 가서 추정이 통째로 틀어진다. 잊을 것은 「몇 번 그랬나」 지
114
+ * 「얼마를 곱하나」 가 아니다.
115
+ *
116
+ * 부스러기는 0 으로 떨군다 — 10^-9 짜리가 남아 「겪은 적 있음」 인 척한다.
117
+ */
118
+ const 안삭히는칸 = new Set(['보정']);
119
+ function 삭히기(칸, 날수) {
120
+ const 몫 = Math.pow(하루할인된다, 날수); // 날짜를 못 재면 Infinity 라 0 이 된다
121
+ for (const [k, v] of Object.entries(칸)) {
122
+ if (typeof v !== 'number' || 안삭히는칸.has(k)) continue;
123
+ const x = v * 몫;
124
+ 칸[k] = x < 부스러기 ? 0 : x;
125
+ }
126
+ return 칸;
127
+ }
128
+
84
129
  /** 오래된 것부터 버려서 표를 상한 안에 둔다. */
85
130
  function 줄이기(표, 상한) {
86
131
  const 열쇠 = Object.keys(표);
@@ -122,25 +167,50 @@ export class 배움 {
122
167
  const 열쇠 = (없는프로그램 ? 낱말.slice(0, 1) : 낱말.slice(0, 2)).join(' ').slice(0, 40);
123
168
  if (!열쇠) return this;
124
169
  const 표 = this.폴더.명령;
125
- const 옛 = 표[열쇠] ?? { ok: 0, no: 0, at: this.지금 };
170
+ this.#명령세기(표, 열쇠, 됐나);
126
171
  /*
127
- * 지난 셈을 **오늘 기준으로 삭혀서** 이어 센다 (agent/신뢰도.js 의 삭힘).
172
+ * 됐으면 **프로그램 칸도 같이 푼다.**
173
+ *
174
+ * 없어서 실패한 것은 첫 낱말로 세는데(위) 되는 것은 두 낱말로 센다. 그래서
175
+ * 오늘 pnpm 을 깔아 `pnpm install` 이 세 번 돼도 그 성공은 다른 칸에만
176
+ * 쌓이고, `pnpm` 칸의 「안 된다」 는 아무도 안 푼다. 프롬프트가 같은
177
+ * 화면에 「되는 명령: pnpm install」 과 「안 되는 명령(다시 부르지 마라):
178
+ * pnpm」 을 나란히 싣고, 모델은 부르지 말라는 쪽을 믿는다.
128
179
  *
129
- * 그냥 더하면 석 달 전 실패와 오늘 실패가 같은 무게가 된다. 그러면 한번
130
- * 「안 된다」 로 굳은 것이 영영 안 풀린다 — 프롬프트가 부르지 말라고 하니
131
- * 안 부르고, 안 부르니 성공 셈이 안 늘고, 그래서 그 줄이 안 사라진다.
180
+ * 삭힘(confidence.js)이 언젠가 풀어 주긴 하지만 그건 **안 겪었을 때** 얘기다.
181
+ * 눈앞에서 된 것을 못 본 척하는 것은 시간이 고칠 수 있는 일이 아니다.
132
182
  *
133
- * 삭히면 그 고리가 저절로 끊긴다. 다시 안 겪은 것은 힘이 빠지고, 힘이
134
- * 빠지면 말하기를 멈추고, 그러면 모델이 다시 해 본다. 잊는 것이 곧 다시
135
- * 해 보는 것이다.
183
+ * 없던 칸을 새로 만들지는 않는다 — 안 겪은 것을 겪은 척하는 것이 된다.
136
184
  */
185
+ const 프로그램 = (낱말[0] ?? '').slice(0, 40);
186
+ if (됐나 && 프로그램 && 프로그램 !== 열쇠 && Object.hasOwn(표, 프로그램)) {
187
+ this.#명령세기(표, 프로그램, true);
188
+ }
189
+ 줄이기(표, 명령최대);
190
+ this.#저장폴더();
191
+ return this;
192
+ }
193
+
194
+ /**
195
+ * 명령 칸 하나에 한 번 센다.
196
+ *
197
+ * 지난 셈은 **오늘 기준으로 삭혀서** 이어 센다 (agent/confidence.js 의 삭힘).
198
+ *
199
+ * 그냥 더하면 석 달 전 실패와 오늘 실패가 같은 무게가 된다. 그러면 한번
200
+ * 「안 된다」 로 굳은 것이 영영 안 풀린다 — 프롬프트가 부르지 말라고 하니
201
+ * 안 부르고, 안 부르니 성공 셈이 안 늘고, 그래서 그 줄이 안 사라진다.
202
+ *
203
+ * 삭히면 그 고리가 저절로 끊긴다. 다시 안 겪은 것은 힘이 빠지고, 힘이
204
+ * 빠지면 말하기를 멈추고, 그러면 모델이 다시 해 본다. 잊는 것이 곧 다시
205
+ * 해 보는 것이다.
206
+ */
207
+ #명령세기(표, 열쇠, 됐나) {
208
+ const 옛 = 표[열쇠] ?? { ok: 0, no: 0, at: this.지금 };
137
209
  const r = { ...옛, ...삭힘(옛, 며칠됐나(this.지금, 옛.at)) };
138
210
  if (됐나) r.ok += 1; else r.no += 1;
139
211
  r.at = this.지금;
140
212
  표[열쇠] = r;
141
- 줄이기(표, 명령최대);
142
- this.#저장폴더();
143
- return this;
213
+ return r;
144
214
  }
145
215
 
146
216
  /** 모델이 보인 버릇 하나. grade.js 의 지켜본것 과 같은 이름을 쓴다. */
@@ -182,9 +252,12 @@ export class 배움 {
182
252
  * 주소는 host 만 쓴다. 경로에는 배포 이름·판 번호 같은 것이 붙어 있고,
183
253
  * 그건 사람이 설정을 조금만 바꿔도 달라져서 배운 것이 매번 새것이 된다.
184
254
  */
255
+ #호스트(주소) {
256
+ try { return new URL(String(주소 ?? '')).host; } catch { return ''; }
257
+ }
258
+
185
259
  #전선열쇠(모델, 주소, 규격) {
186
- let host = '';
187
- try { host = new URL(String(주소 ?? '')).host; } catch { host = ''; }
260
+ const host = this.#호스트(주소);
188
261
  /*
189
262
  * 규격도 열쇠에 넣는다.
190
263
  *
@@ -205,8 +278,21 @@ export class 배움 {
205
278
  /** 전선 모양 하나를 적어 둔다. */
206
279
  전선본것(모델, 주소, 카드, 규격) {
207
280
  if (!카드) return this;
281
+ /*
282
+ * **반쪽 열쇠는 안 적는다.**
283
+ *
284
+ * 열쇠는 `모델@host#규격` 이다. 앞이 비면 `@host#규격`, 뒤가 비면 `모델@`
285
+ * 인데 둘 다 「어느 모델인지 모르는 카드」 거나 「어느 창구인지 모르는
286
+ * 카드」 다. 그리고 그건 다음번에 **다른 것에 얹힌다** — 주소를 못 읽어
287
+ * 빈 host 로 적힌 카드를, 다음에 또 주소를 못 읽은 전혀 다른 창구가
288
+ * 그대로 집어 간다. 전선은 틀리게 배우면 멀쩡한 기능이 조용히 꺼진다
289
+ * (backend/wire.js: 잘못 배우는 것은 못 배우는 것보다 나쁘다).
290
+ *
291
+ * 여태 `열쇠 === '@'` 하나만 막았는데, 규격이 붙으면 `@host#openai` 라
292
+ * 그 문을 그냥 지나갔다.
293
+ */
294
+ if (!String(모델 ?? '').trim() || !this.#호스트(주소)) return this;
208
295
  const 열쇠 = this.#전선열쇠(모델, 주소, 규격);
209
- if (열쇠 === '@') return this;
210
296
  this.집.전선 = this.집.전선 ?? {};
211
297
  this.집.전선[열쇠] = { ...카드, at: this.지금 };
212
298
  줄이기(this.집.전선, 모델최대);
@@ -222,11 +308,25 @@ export class 배움 {
222
308
  return 나머지;
223
309
  }
224
310
 
311
+ /*
312
+ * 칸을 꺼낼 때 **먼저 삭힌다** (위 삭히기).
313
+ *
314
+ * 여기서 안 삭히면 오늘 한 걸음이 석 달 묵은 셈을 통째로 오늘 것으로
315
+ * 만든다 — 셈을 적을 때마다 `at` 이 오늘로 옮겨 가기 때문이다. 요약() 은
316
+ * 나이를 보고 삭혀서 말하는데, 그 한 걸음이 나이를 0 으로 되돌려 석 달
317
+ * 전 버릇을 100% 로 되살린다.
318
+ *
319
+ * 셈을 적는 자리에서 삭혀 두는 까닭은 하나 더 있다. 카드(agent/card.js)는
320
+ * 이 칸을 **나이 없이** 그대로 받아 하네스를 바꾼다. 읽는 쪽마다 제 나름대로
321
+ * 삭히면 언젠가 한쪽만 고쳐지고, 그때부터 화면과 실제가 갈린다.
322
+ */
225
323
  #모델칸(모델) {
226
324
  const 이름 = String(모델 ?? '').trim();
227
325
  if (!이름) return null;
228
326
  const 표 = this.집.모델;
229
- 표[이름] = 표[이름] ?? { 걸음: 0, at: this.지금 };
327
+ const 있던것 = 표[이름];
328
+ 표[이름] = 있던것 ?? { 걸음: 0, at: this.지금 };
329
+ if (있던것) 삭히기(표[이름], 며칠됐나(this.지금, 있던것.at));
230
330
  줄이기(표, 모델최대);
231
331
  return 표[이름];
232
332
  }
@@ -248,7 +348,7 @@ export class 배움 {
248
348
  /*
249
349
  * 되는 명령 / 안 되는 명령. 이 폴더의 사실이다.
250
350
  *
251
- * 문턱은 **여기서 안 정한다** (agent/신뢰도.js 의 뭐라할까). 여태 이 자리에
351
+ * 문턱은 **여기서 안 정한다** (agent/confidence.js 의 뭐라할까). 여태 이 자리에
252
352
  * `ok>=2 && no===0` 이 박혀 있었는데, 그 두 숫자가 어디서 왔는지는 아무 데도
253
353
  * 안 적혀 있었고 섞인 증거(다섯 번 되고 한 번 안 됨)를 아예 못 다뤘다.
254
354
  * 지금은 표본이 적으면 깎아서 보고(윌슨), 다시 안 겪은 것은 힘이 빠진다(할인).
@@ -314,10 +414,18 @@ export class 배움 {
314
414
  *
315
415
  * 그래서 전선만 지운다. 다음 요청에서 짐작으로 다시 시작하고, 서버가
316
416
  * 여전히 거절하면 같은 것을 다시 배운다 — 잃을 것이 없다.
417
+ *
418
+ * ── `'모델'` 도 제 몫만 지운다 ─────────────────────────────────────────
419
+ *
420
+ * 여기가 집을 통째로 빈것() 으로 갈아 끼우고 있었다. 빈것() 에는 전선 칸이
421
+ * 아예 없으니, 버릇 셈만 지우려던 사람이 창구마다 400 을 맞아 가며 알아낸
422
+ * 전선 모양까지 같이 잃었다 — 바로 위에서 전선만 지우는 길을 따로 낸 것이
423
+ * 아무 뜻이 없어진다. 지우는 범위는 적힌 그대로여야 한다.
317
424
  */
318
425
  지우기(어디 = '전부') {
319
426
  if (어디 === '전부' || 어디 === '폴더') { this.폴더 = 빈것(); this.#저장폴더(); }
320
- if (어디 === '전부' || 어디 === '모델') { this.집 = 빈것(); this.#저장집(); }
427
+ if (어디 === '전부') { this.집 = 빈것(); this.#저장집(); }
428
+ if (어디 === '모델') { this.집.모델 = {}; this.#저장집(); }
321
429
  if (어디 === '전선') { this.집.전선 = {}; this.#저장집(); }
322
430
  return this;
323
431
  }
@@ -329,7 +437,7 @@ export class 배움 {
329
437
  *
330
438
  * 화면이 「됨 3 · 안 됨 0」 만 보여 주면, 그게 오늘 잰 것인지 두 달 전
331
439
  * 것인지 사람이 못 가른다. 그런데 그 둘은 **다르게 취급된다** — 삭은
332
- * 것은 프롬프트에 안 실린다(agent/신뢰도.js). 화면과 실제가 갈리면
440
+ * 것은 프롬프트에 안 실린다(agent/confidence.js). 화면과 실제가 갈리면
333
441
  * 사람은 안 실린 줄을 실린 줄로 읽는다.
334
442
  *
335
443
  * 판정도 여기서 같이 낸다. 화면이 문턱을 제 나름대로 다시 계산하면
@@ -182,12 +182,21 @@ export function 매김(conn, 본것 = null, 사람이정한 = null) {
182
182
  *
183
183
  * 숫자의 뜻: 걸음 하나마다 인자 잘림이나 빈 답이 한 번씩 나면 율이 2 다.
184
184
  * 그 상태로는 무엇을 맡겨도 안 끝난다 — 무조건 '작음' 으로 내린다.
185
- * 반대로 열 걸음을 사고 없이 돌면 율이 0 이고, 그러면 이름이 뭐라 하든
186
- * 한 단 올린다. 그 모델은 실제로 잘하고 있다.
185
+ * 반대로 **여덟 걸음 이상** 사고 없이 돌면 율이 0 이고, 그러면 이름이 뭐라 하든
186
+ * 한 단 올린다. 그 모델은 실제로 잘하고 있다. (여기 전에는 「열 걸음」 이라고 적혀 있었는데
187
+ * 코드는 8 이었다 — 말을 코드에 맞추고 문턱을 검사로 못 박았다, 6회차 모델급6av-b GR1.)
187
188
  */
188
189
  const 자리 = 급들.indexOf(첫어림.급);
189
190
  if (율 >= 0.8) {
190
- return { 급: '작음', 왜: `${본것.걸음}걸음 도는 동안 사고가 잦았습니다 (인자 잘림 ${본것.잘린인자} · 빈 답 ${본것.빈답} · 편집 실패 ${본것.편집실패})`, 짐작: false };
191
+ /*
192
+ * 사유에는 **사고율() 이 센 것을 다 적는다** (2.0.0 8회차 판정).
193
+ *
194
+ * 되풀이만 빠져 있었다. 그래서 되풀이 세 번으로 내려간 모델의 사유가
195
+ * 「사고가 잦았습니다 (인자 잘림 0 · 빈 답 0 · 편집 실패 0)」 이 됐다 — 전부
196
+ * 0 이라고 적어 놓고 잦았다고 한다. 화면이 제 셈과 어긋난 말을 하면 사람은
197
+ * 그 줄을 그 뒤로 안 읽는다. 세는 것과 적는 것은 같은 목록이어야 한다.
198
+ */
199
+ return { 급: '작음', 왜: `${본것.걸음}걸음 도는 동안 사고가 잦았습니다 (인자 잘림 ${본것.잘린인자} · 빈 답 ${본것.빈답} · 편집 실패 ${본것.편집실패} · 되풀이 ${본것.되풀이})`, 짐작: false };
191
200
  }
192
201
  if (율 >= 0.3 && 자리 > 0) {
193
202
  return { 급: 급들[자리 - 1], 왜: `${본것.걸음}걸음 중 몇 번 어긋났습니다 — 한 단 낮춰 잡습니다`, 짐작: false };