leerness 1.12.0 → 1.13.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/CHANGELOG.md CHANGED
@@ -1,5 +1,96 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.13.0 — 2026-06-09 — 🛡️ [안정화/Stable] verify-claim 다언어 + 정직성·자원·보안 안정화
4
+
5
+ **🛡️ 안정화(Stable) minor. 헤드라인 = verify-claim 다언어 지원(비-JS 개발자 핵심 회귀 수정).** 15번째 멀티에이전트 버그헌트 성과(1.12.2~1.12.5)를 검증·통합해 npm 공개. R-0011 정책의 4번째 minor.
6
+
7
+ ### ⚠️ 동작 변경 (중요)
8
+ - **verify-claim 기본 게이트가 Python·Ruby·Go·C#·Java·PHP·Rust 등 비-JS 구현을 인식**합니다(1.12.0 의 게이트는 JS 패턴만 알아 비-JS 정상 완료를 오차단했음). 정직하게 구현한 완료는 언어 무관 통과, 가짜 완료는 여전히 차단.
9
+
10
+ ### 이번 minor 통합 (1.12.2~1.12.5)
11
+ - **🌐 verify-claim 다언어 지원** (UR-0014): `OPTIMISM_PATTERNS.codeRe` 에 교차언어 idiom 추가 — 비-JS 정상 done-claim 오차단(exit 1) 제거. 1.12.0 핵심가치의 비-JS 회귀 수정.
12
+ - **🔒 MCP 정직성** (UR-0181): unknown tool 호출의 임의경로 쓰기 차단(사용통계 기록을 도구 검증 후로). path-타게팅은 generic 서버 설계로 확인(맹신 X — 과수정 회피).
13
+ - **🔍 탐지 정직성** (UR-0182/0183): lazy detect TODO 파일별 추적(무관 task 의 'TODO' 글자 전역 억제 제거) + session close 완료 정직성 advisory(증거 없는 done 노출).
14
+ - **🧹 견고성/자원/정확성** (UR-0015~0021): glossary 표 파이프 escape · MCP _chunkSize 클램프(무한루프 데이터손실) · api-skill CRLF/BOM · shell-guard 공백없는 `&&` · 대형파일 stat-before-read(메모리 2배 스파이크) · 중첩 skip-dir 시크릿 오탐 · requirements pip 디렉티브.
15
+
16
+ ### 검증 (회귀 0)
17
+ - **selftest 210 PASS** · **E2E 365/365 PASS** · npm gate=minor_bump. Python API done-claim→exit 0, 중첩 node_modules 시크릿 제외, MCP unknown tool 쓰기 차단 등 행위 재현.
18
+
19
+ ### 안정화 표시 (R-0006)
20
+ CHANGELOG [안정화/Stable] · git tag annotation (Stable) · GitHub release (Stable) · npm dist-tag `stable` 시도.
21
+
22
+ ## 1.12.5 — 2026-06-09 — 15th 버그헌트 잔여 5종: CRLF·shell-guard·메모리·skip-dir·requirements
23
+
24
+ **🧹 15번째 버그헌트 잔여 클러스터(UR-0017~0021) 일괄 처리 — 견고성/자원/정확성.**
25
+
26
+ ### 변경
27
+ - **api-skill CRLF/BOM 복구** (UR-0017, P2): `_loadAPISkill` 가 raw readFileSync 라 CRLF 파일에서 frontmatter 전부 유실 + `api-skill match` 크래시(body undefined). `read()`(BOM strip) + `\r\n`/`\r` 정규화 + fallback 에 `body` 추가(1.9.408 SKILL.md 수정 누락분).
28
+ - **shell-guard 공백없는 `&&`/`||` 탐지** (UR-0018, P2): `/\s&&\s/` 가 양쪽 공백을 요구해 `npm run build&&npm test`(PS5.1 에서 실패하는 흔한 형태) 미탐 → 공백 무관 토큰 매칭.
29
+ - **대형 파일 stat-before-read** (UR-0019, P2): `_scanCodeForPatterns`·`scan secrets`·`encoding check` 가 size-cap 을 read() **후** 검사해 대형 파일 1개가 메모리 2배 스파이크(200MB→RSS 464MB) → 읽기 **전** stat 으로 초과 파일 건너뜀. verify-claim/gate 메모리 안정화.
30
+ - **중첩 skip-dir 제외** (UR-0020, P3): `isSkippedRel` 가 root-anchored 만 매칭해 중첩 `node_modules`/`.git`/`dist` 가 스캔돼 오탐 → 경로 세그먼트 매칭(SCAN_SKIP_DIRS Set).
31
+ - **requirements.txt 디렉티브 skip** (UR-0021, P3): `-e`/`-r`/`--hash` pip 디렉티브를 패키지로 파싱하던 것 → `-` 라인 skip + 영숫자 시작 요구.
32
+
33
+ ### 검증 (회귀 0)
34
+ - **selftest 209→210**, 행위 재현: api-skill CRLF→"Real Name"+match exit 0, scan secrets 중첩 node_modules/dist/.git 제외(root.js 만 탐지), requirements `["requests","flask"]`.
35
+ - 개발 중 발견·수정: `SCAN_SKIP_DIRS` 는 Set(.has) — `.includes` 오용을 selftest 가 배포 전 차단(handoff 보안 스캔 회귀 포함).
36
+ - patch(1.12.5, 같은 minor) — R-0011 정책상 npm 미배포. **15번째 버그헌트(UR-0014~0021) 전부 처리 완료.**
37
+
38
+ ## 1.12.4 — 2026-06-09 — 🌐 verify-claim 다언어 지원 + glossary 표/MCP 페이지네이션 (15th 버그헌트 P1/P2)
39
+
40
+ **🔴 핵심 회귀 수정: verify-claim 기본 게이트가 非JS 정상 완료를 오차단하던 문제.** 15번째 멀티에이전트 버그헌트(크로스플랫폼·최신기능·성능 3관점) 결과 중 즉시-수정 가능한 고가치 3건.
41
+
42
+ ### 변경
43
+ - **🔴 verify-claim 다언어 지원** (UR-0014, P1): 1.12.0 의 optimism 기본 게이트가 `OPTIMISM_PATTERNS.codeRe` 를 **JS 전용**으로 두고도 코드 스캐너는 13개 언어를 읽어, Python(`requests`)·Ruby(`Net::HTTP`)·Go(`http.Get`)·C#(`HttpClient`)·Java·PHP·Rust 로 정직하게 구현한 done-claim 을 "호출 흔적 없음"으로 **오차단(exit 1)** 했음. 각 codeRe 에 교차언어 idiom 추가 → 非JS 정상 완료 통과. (검출 관대화 = 정직한 작업 오차단 제거; 과탐보다 안전.)
44
+ - **glossary.md 표 파이프 escape** (UR-0015, P2): 표 셀에 `_lineSafe`(개행만 제거) 대신 `_cellSafe`(파이프 escape) 적용 — 의존성 description 의 `|` 가 표 칼럼을 깨뜨리던 문제(node_modules description fallback 벡터).
45
+ - **MCP 페이지네이션 _chunkSize 클램프** (UR-0016, P2): 음수/소수 `_chunkSize` → 빈 출력 무한 루프(데이터 손실)였음 → 양의 정수로 클램프.
46
+
47
+ ### 검증 (회귀 0)
48
+ - **selftest 207→209**, 행위 재현: Python API done-claim 기본 verify-claim **exit 0**(false-fail 제거), glossary `|`→`\|`, _chunkSize 음수 클램프.
49
+ - patch(1.12.4, 같은 minor) — R-0011 정책상 npm 미배포. 15th 잔여(UR-0017~0021): _loadAPISkill CRLF, shell-guard 공백없는 &&, 대형파일 stat-before-read, 중첩 skip-dir, requirements `-` — 후속.
50
+
51
+ ## 1.12.3 — 2026-06-09 — 정직성 마무리: lazy TODO 파일별 추적 + session close 완료 정직성 (14th 버그헌트, UR-0182/0183)
52
+
53
+ **🔍 14번째 버그헌트 잔여 2건 — 정직성 탐지 정밀화.**
54
+
55
+ ### 변경
56
+ - **lazy detect TODO 파일별 추적** (UR-0182): `todo_untracked` 가 아무 task 의 request/evidence 에 'TODO' 글자만 있어도 모든 코드 TODO 경보를 **전역 억제**(무관한 task 1개가 전부 묵음)하던 문제 → 해당 TODO 의 **파일을 참조하는 task 가 없는 것만** 미추적으로 경보. `--auto-track` 도 미추적 TODO 만 등록.
57
+ - **session close 완료 정직성 advisory** (UR-0183): 마감 시 done 인데 evidence 가 비었거나 placeholder 인 task 를 노출(`completionHonesty` JSON 필드 + `⚠ 완료 정직성` 라인). 차단하지 않는 advisory(session close 의 advisory 철학) — verify-claim 권장 환기.
58
+
59
+ ### 검증 (회귀 0)
60
+ - **selftest 206→208**, 행위 재현: 무관 task 의 'TODO' 글자에도 코드 TODO 경보 유지(untracked=1), session close `completionHonesty.doneWithoutEvidence` 노출.
61
+ - patch(1.12.3, 같은 minor) — R-0011 정책상 npm 미배포. **14번째 버그헌트 백로그(UR-0175~0183) 전부 처리 완료.**
62
+
63
+ ## 1.12.2 — 2026-06-09 — MCP unknown-tool 임의경로 쓰기 차단 (14th 버그헌트, UR-0181)
64
+
65
+ **🧹 MCP 사용통계 기록을 도구 검증 후로 — unknown tool 의 임의 경로 쓰기 차단.**
66
+
67
+ ### 조사 정정 (맹신 X)
68
+ 외부 에이전트가 "MCP `path` 인자가 서버 루트를 탈출(traversal/정책 우회)"을 P2 보안으로 보고했으나, **직접 조사 + e2e 로 확인 결과 MCP 서버는 generic 설계**임: `mcp serve` 는 cwd 에서 실행되고 각 `tools/call` 의 `path` 로 대상 프로젝트를 지정(의도된 기능 — e2e 가 temp 경로 타게팅을 검증). policy 도 대상-프로젝트별로 path 에서 로드(올바름). 즉 **path-타게팅 자체는 취약점이 아니라 설계**(신뢰된 로컬 MCP 모델). 처음 적용한 "루트 가둠"은 이 설계를 깨 e2e 5건 실패 → **revert**.
69
+
70
+ ### 진짜 버그 수정
71
+ - `_bumpMcpUsage`(사용 통계 쓰기)가 **unknown-tool 검증 전**에 실행돼, 미등록 도구 호출로도 임의 경로에 `.harness/cache` 가 생성됐음 → **검증 후(알려진 도구만)로 이동**.
72
+
73
+ ### 검증 (회귀 0)
74
+ - **selftest 206→207** (bump 위치 와이어), e2e 365/365. 프로젝트-scoped 가둠은 opt-in 플래그로 별도 검토(backlog).
75
+ - patch(1.12.2, 같은 minor) — R-0011 정책상 npm 미배포. 14th 잔여: UR-0182(lazy TODO)·UR-0183(session close 정직성).
76
+
77
+ ## 1.12.1 — 2026-06-08 — 🩹 [Stable hotfix] 클린룸 selftest false-alarm 수정 (UR-0008)
78
+
79
+ **1.12.0 직후 발견된 사용자-노출 결함 핫픽스 — npm 배포(예외).**
80
+
81
+ ### 문제 (게시본 클린룸 검증으로 발견)
82
+ `npm i leerness@1.12.0` 후 `leerness selftest` 실행 시 **"1/206 실패 — 설치 손상 의심"** false-alarm. 패키지는 정상 동작(모든 명령 기능 OK)이나, 무결성 게이트인 selftest 가 거짓 경보 → 정직성이 핵심인 도구의 신뢰 훼손.
83
+
84
+ ### 원인 + 수정
85
+ - UR-0151 selftest 케이스가 소스 자기검증에 **멀티라인 exact-string `includes("…\n …")`**(줄바꿈+정확 4공백)를 사용 → 공백/줄바꿈/환경에 취약(repo 통과, 클린룸 실패).
86
+ - → **공백 유연 정규식 `\s+`** 으로 견고화. 로컬 tarball 클린룸 설치 후 **selftest 206/206 통과** 검증(게시 전).
87
+
88
+ ### 배포 정책 예외 (R-0011)
89
+ - R-0011(patch 는 npm 미배포)의 **예외 — 직전 게시 minor(1.12.0)의 사용자-노출 결함 안정성 핫픽스**라 npm 배포. dist-tag `stable` 유지.
90
+
91
+ ### 검증 (회귀 0)
92
+ - selftest 206(repo) + **206(클린룸 tarball)** · E2E 365/365.
93
+
3
94
  ## 1.12.0 — 2026-06-08 — 🛡️ [안정화/Stable] 핵심가치(증거-우선 완료) 복원 + 용어집 안정 minor
4
95
 
5
96
  **🛡️ 안정화(Stable) minor. 헤드라인 = verify-claim 기본 모드가 허위완료를 차단하도록 복원(동작 변경).** 14번째 멀티에이전트 버그헌트 성과(1.11.1~1.11.4)를 검증·통합해 npm 공개. R-0011 정책의 3번째 minor.
package/README.md CHANGED
@@ -168,7 +168,7 @@ MIT
168
168
  <!-- leerness:project-readme:start -->
169
169
  ## Leerness Project Harness
170
170
 
171
- 이 프로젝트는 Leerness v1.12.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
171
+ 이 프로젝트는 Leerness v1.13.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
172
172
 
173
173
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
174
174
 
@@ -222,7 +222,7 @@ leerness memory restore decision <date|title>
222
222
 
223
223
  ### MCP server (외부 AI 통합)
224
224
 
225
- Leerness v1.12.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
225
+ Leerness v1.13.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
226
226
 
227
227
  ```jsonc
228
228
  // 카테고리별
@@ -243,7 +243,7 @@ Leerness v1.12.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code
243
243
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
244
244
  1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
245
245
 
246
- 현재 누적: **70 라운드 (1.9.40 → 1.12.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
246
+ 현재 누적: **70 라운드 (1.9.40 → 1.13.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
247
247
 
248
248
  ### 성능 가이드 (1.9.140 측정)
249
249
 
@@ -281,6 +281,6 @@ leerness release pack --close --auto-main-push
281
281
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
282
282
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
283
283
 
284
- Last synced by Leerness v1.12.0: 2026-06-08
284
+ Last synced by Leerness v1.13.0: 2026-06-09
285
285
  <!-- leerness:project-readme:end -->
286
286
 
package/bin/leerness.js CHANGED
@@ -32,7 +32,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
32
32
  // 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
33
33
  const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _TOOL_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 · 1.11.4 (UR-0007): _TOOL_CATALOG
34
34
 
35
- const VERSION = '1.12.0';
35
+ const VERSION = '1.13.0';
36
36
 
37
37
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
38
38
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -3362,9 +3362,10 @@ function _selfTestCases() {
3362
3362
  } },
3363
3363
  { name: 'UR-0151: decision/lesson/rule add positional path 지원(_taskPositionalPath 재사용, cwd 오염 차단) (1.9.445)', run: () => {
3364
3364
  const src = read(__filename);
3365
+ // 1.12.1 (UR-0008): 멀티라인 exact-string includes 는 공백/줄바꿈/환경에 취약(클린룸 selftest false-alarm) → 공백 유연 정규식(\s+)으로 견고화.
3365
3366
  const rule = src.includes("ruleAdd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd(), _parseAddTitle(args, 2))");
3366
- const lesson = src.includes("if (cmd === 'lesson') {\n const root = absRoot(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd());");
3367
- const decision = src.includes("if (cmd === 'decision') {\n const root = absRoot(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd());");
3367
+ const lesson = /if \(cmd === 'lesson'\) \{\s+const root = absRoot\(arg\('--path', null\) \|\| _taskPositionalPath\(args, 2\) \|\| process\.cwd\(\)\)/.test(src);
3368
+ const decision = /if \(cmd === 'decision'\) \{\s+const root = absRoot\(arg\('--path', null\) \|\| _taskPositionalPath\(args, 2\) \|\| process\.cwd\(\)\)/.test(src);
3368
3369
  // rule add 의 --trigger 값은 경로 아님(path-like 아님) + 값-플래그 제외
3369
3370
  const m = require('../lib/pure-utils');
3370
3371
  const trig = m._taskPositionalPath(['rule', 'add', '룰', '--trigger', 'every-update', '/p'], 2) === '/p'
@@ -3521,6 +3522,44 @@ function _selfTestCases() {
3521
3522
  return md.includes(m.GLOSSARY_START) && md.includes('react') && /미정의|unknownpkgxyz/.test(md)
3522
3523
  && read(__filename).includes("if (cmd === 'glossary')");
3523
3524
  } },
3525
+ { name: '14th 버그헌트 (UR-0181): MCP _bumpMcpUsage 를 unknown-tool 검증 후로 이동(unknown tool 임의경로 쓰기 차단) (1.12.2)', run: () => {
3526
+ const src = read(__filename);
3527
+ // _bumpMcpUsage 호출이 unknown-tool 가드(cliArgs === null return) "뒤"에 위치 — generic 서버 path-타게팅은 유지(취약점 아님), unknown tool 쓰기만 차단.
3528
+ return /if \(cliArgs === null\) return send[\s\S]{0,400}?_bumpMcpUsage\(targetPath, name\)/.test(src);
3529
+ } },
3530
+ { name: '14th 버그헌트 P2/P3 (UR-0182/0183): lazy TODO 파일별 추적 + session close 완료 정직성 advisory (1.12.3)', run: () => {
3531
+ const src = read(__filename);
3532
+ const todoPerFile = src.includes('const untrackedTodos = newTodos.filter(t => !taskText.includes(t.file));') && src.includes("kind: 'todo_untracked'");
3533
+ const scSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'session-close.js'));
3534
+ const honesty = scSrc.includes('jsonResult.completionHonesty =') && scSrc.includes("doneWithoutEvidence: _doneNoEvidence.length");
3535
+ return todoPerFile && honesty;
3536
+ } },
3537
+ { name: '15th 버그헌트 P1/P2 (UR-0014/0015/0016): optimism 다언어 codeRe + glossary _cellSafe + MCP _chunkSize 클램프 (1.12.4)', run: () => {
3538
+ const cat = require('../lib/catalogs').OPTIMISM_PATTERNS;
3539
+ const api = cat.find(p => p.kind === 'API');
3540
+ // 다언어: Python requests / Ruby Net::HTTP / Go http.Get / C# HttpClient 매칭
3541
+ const multiLang = api.codeRe.test('requests.get(url)') && api.codeRe.test('Net::HTTP.get') && api.codeRe.test('http.Get(url)') && api.codeRe.test('new HttpClient()') && api.codeRe.test('fetch(');
3542
+ const m = require('../lib/pure-utils');
3543
+ // glossary 표 셀 파이프 escape
3544
+ const md = m._renderGlossaryMd([{ term: 'x', plainKo: 'a | b', plainEn: 'a | b', category: 'c', source: 'catalog' }], {});
3545
+ const pipeEsc = md.includes('a \\| b') && !/\| a \| b \|/.test(md.replace(/\\\|/g, '§'));
3546
+ const src = read(__filename);
3547
+ const clamp = src.includes('const _cs = Math.floor(Number(args._chunkSize));') && src.includes('(Number.isFinite(_cs) && _cs > 0) ? _cs : 50000');
3548
+ return multiLang && pipeEsc && clamp;
3549
+ } },
3550
+ { name: '15th 잔여 클러스터 (UR-0017~0021): api-skill CRLF + shell-guard 공백없는&& + stat-before-read + 중첩skip + requirements 디렉티브 (1.12.5)', run: () => {
3551
+ const src = read(__filename);
3552
+ const apiCrlf = src.includes("const content = read(fp).replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');") && src.includes('urls: [], name: id, body: content }');
3553
+ const statBeforeRead = src.includes('if (fs.statSync(file).size > 1024 * 1024) continue;') && src.includes('if (fs.statSync(file).size > 5 * 1024 * 1024) continue;') && src.includes('if (fs.statSync(fp2).size > budget) continue;');
3554
+ const nestedSkip = src.includes('segs.some(s => SCAN_SKIP_DIRS.has(s))');
3555
+ const an = require('../lib/analyzers');
3556
+ const sg = an._shellGuardAnalyze('npm run build&&npm test', { shell: 'powershell', psVersion: 5 });
3557
+ const shellNoSpace = (sg.issues || []).some(i => i.rule === 'ps5-chain');
3558
+ const m = require('../lib/pure-utils');
3559
+ const reqs = m._parseRequirementsTxt('-e git+https://x\n-r base.txt\n--hash=sha256:abc\nrequests==2.31\n');
3560
+ const reqDirectives = reqs.length === 1 && reqs[0] === 'requests';
3561
+ return apiCrlf && statBeforeRead && nestedSkip && shellNoSpace && reqDirectives;
3562
+ } },
3524
3563
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3525
3564
  ];
3526
3565
  }
@@ -3798,9 +3837,10 @@ function _serializeAPISkill(id, name, urls, direction, doc) {
3798
3837
  function _loadAPISkill(root, id) {
3799
3838
  const fp = path.join(_apiSkillsDir(root), id + '.md');
3800
3839
  if (!fs.existsSync(fp)) return null;
3801
- const content = fs.readFileSync(fp, 'utf8');
3840
+ // 1.12.5 (15th 버그헌트 P2, UR-0017): read()(BOM strip) + CRLF/CR 정규화 — 이전 raw readFileSync 는 CRLF 파일에서 '^---\n' 불일치로 frontmatter 전부 유실(1.9.408 SKILL.md 수정 누락분). 또 fallback 에 body 추가 → _matchAPISkills 의 s.body.slice 크래시 방지.
3841
+ const content = read(fp).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
3802
3842
  const fm = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
3803
- if (!fm) return { id, content, urls: [], name: id };
3843
+ if (!fm) return { id, content, urls: [], name: id, body: content };
3804
3844
  const meta = {};
3805
3845
  fm[1].split('\n').forEach(l => {
3806
3846
  const m = l.match(/^(\w+):\s*(.*)$/);
@@ -7318,8 +7358,10 @@ function getExtraSkipDirs(root) {
7318
7358
  return read(f).split('\n').map(s => s.trim().replace(/\/+$/, '')).filter(s => s && !s.startsWith('#'));
7319
7359
  }
7320
7360
  function isSkippedRel(rel, extras = []) {
7321
- const all = [...SCAN_SKIP_DIRS, ...extras];
7322
- return all.some(d => rel === d || rel.startsWith(d + '/'));
7361
+ // 1.12.5 (15th 버그헌트 P3, UR-0020): 중첩 skip-dir 도 제외 — 이전엔 root-anchored prefix 만 매칭해 deep/node_modules, sub/.git, vendor/dist 가 스캔돼 오탐. SCAN_SKIP_DIRS 는 단일 dir 명이라 경로 세그먼트로 매칭(_scanShellScriptsEncoding 의 basename skip 과 일관).
7362
+ const segs = rel.split('/');
7363
+ if (segs.some(s => SCAN_SKIP_DIRS.has(s))) return true; // SCAN_SKIP_DIRS 는 Set
7364
+ return extras.some(d => rel === d || rel.startsWith(d + '/'));
7323
7365
  }
7324
7366
  const SCAN_TEXT_EXT = new Set(['.js','.ts','.jsx','.tsx','.mjs','.cjs','.json','.md','.txt','.env','.bash','.sh','.yml','.yaml','.toml','.ini','.cfg','.py','.rb','.go','.rs','.java','.kt','.swift','.cs','.php','.sql','.html','.css','.scss','.less','.xml','.bat','.ps1','']);
7325
7367
  function* walk(root, base = root, depth = 0, extras = null) {
@@ -7355,6 +7397,8 @@ function _collectSecretFindings(root) {
7355
7397
  const isEnvFamily = /^\.env(\.|$)/.test(path.basename(file));
7356
7398
  if (!SCAN_TEXT_EXT.has(ext) && !isEnvFamily) continue;
7357
7399
  let text;
7400
+ // 1.12.5 (15th 버그헌트 P2, UR-0019): stat-before-read — 1MB 초과 파일은 읽지 않고 건너뜀(이전엔 read 후 검사라 대형 파일 통째 로드).
7401
+ try { if (fs.statSync(file).size > 1024 * 1024) continue; } catch { continue; }
7358
7402
  try { text = read(file); } catch { continue; }
7359
7403
  if (text.length > 1024 * 1024) continue;
7360
7404
  const fileRel = (file === root) ? path.basename(file) : rel(root, file);
@@ -7413,6 +7457,8 @@ function encodingCheck(root, opts = {}) {
7413
7457
  const ext = path.extname(file).toLowerCase();
7414
7458
  if (!SCAN_TEXT_EXT.has(ext)) continue;
7415
7459
  let buf;
7460
+ // 1.12.5 (15th 버그헌트 P2, UR-0019): stat-before-read — 5MB 초과 파일은 읽지 않고 건너뜀(이전엔 readBuf 후 검사라 대형 파일 통째 로드).
7461
+ try { if (fs.statSync(file).size > 5 * 1024 * 1024) continue; } catch { continue; }
7416
7462
  try { buf = readBuf(file); } catch { continue; }
7417
7463
  if (buf.length === 0) continue;
7418
7464
  if (buf.length > 5 * 1024 * 1024) continue;
@@ -7520,14 +7566,16 @@ function lazyDetect(root, opts = {}) {
7520
7566
  }
7521
7567
  }
7522
7568
  if (todoCount > 0) {
7523
- const hasTodoTask = rows.some(r => /TODO|FIXME|XXX/.test(r.request) || /TODO|FIXME|XXX/i.test(r.evidence));
7524
- if (!hasTodoTask) {
7569
+ // 1.12.3 (14th 버그헌트 P2, UR-0182): 파일별 추적 — 기존엔 아무 task 의 request/evidence 가 'TODO' 글자만 포함해도 모든 코드 TODO 경보를 전역 억제(무관한 task 1개가 전부 묵음)했음. 이제 해당 TODO 의 파일을 참조하는 task 가 없는 것만 미추적으로 경보.
7570
+ const taskText = rows.map(r => `${r.request || ''} ${r.evidence || ''}`).join('\n');
7571
+ const untrackedTodos = newTodos.filter(t => !taskText.includes(t.file));
7572
+ if (untrackedTodos.length > 0) {
7525
7573
  issues++;
7526
- _warn(`code has ${todoCount} TODO/FIXME/XXX (new: ${newTodos.length}) but no progress-tracker entry tracks them`,
7527
- { kind: 'todo_untracked', severity: 'warn', todoCount, newCount: newTodos.length, newTodos: newTodos.slice(0, 5) });
7528
- // TODO 처음 5개 표시 (verbose 모드만)
7529
- if (!jsonMode) newTodos.slice(0, 5).forEach(t => log(` ${t.file}:${t.line} ${t.text}`));
7530
- if (has('--auto-track') && newTodos.length) {
7574
+ _warn(`code has ${todoCount} TODO/FIXME/XXX (untracked: ${untrackedTodos.length}) no progress-tracker entry references their files`,
7575
+ { kind: 'todo_untracked', severity: 'warn', todoCount, newCount: newTodos.length, untrackedCount: untrackedTodos.length, newTodos: untrackedTodos.slice(0, 5) });
7576
+ // 미추적 TODO 처음 5개 표시 (verbose 모드만)
7577
+ if (!jsonMode) untrackedTodos.slice(0, 5).forEach(t => log(` ${t.file}:${t.line} ${t.text}`));
7578
+ if (has('--auto-track') && untrackedTodos.length) {
7531
7579
  // 1.9.411 (8번째 버그헌트, UR-0115): TODO 일괄 등록을 단일 read-modify-write 로 직렬화.
7532
7580
  // 종전: TODO 마다 nextId(plan+progress 전체 스캔) + upsertProgress(전체 read+write) → O(T × tracker크기) (다수 TODO 자동등록 시 O(N²) 행걸림).
7533
7581
  // 개선: 락 1회 안에서 rows 1회 읽고, 최대 T-id 1회 계산, 전부 push, 1회 write → O(N + T).
@@ -10266,7 +10314,8 @@ function _scanCodeForPatterns(root) {
10266
10314
  if (budget <= 0) return;
10267
10315
  if (e.isDirectory()) { if (!SKIP.test(e.name) && !e.name.startsWith('.')) walk(path.join(p, e.name), depth + 1); continue; }
10268
10316
  if (!/\.(js|ts|jsx|tsx|gd|cs|py|rb|go|rs|java|php|kt|swift)$/i.test(e.name)) continue;
10269
- try { const t = read(path.join(p, e.name)); combined += t + '\n'; budget -= t.length; } catch {}
10317
+ // 1.12.5 (15th 버그헌트 P2, UR-0019): stat-before-read 이전엔 read() budget 검사라 대형 파일 1개가 통째 메모리에 로드(200MB→RSS 464MB). 남은 budget 초과 파일은 읽지 않고 건너뜀.
10318
+ try { const fp2 = path.join(p, e.name); if (fs.statSync(fp2).size > budget) continue; const t = read(fp2); combined += t + '\n'; budget -= t.length; } catch {}
10270
10319
  }
10271
10320
  }
10272
10321
  walk(root, 0);
@@ -15343,13 +15392,14 @@ function mcpServeCmd(root) {
15343
15392
  send({ jsonrpc: '2.0', id, result: { tools: TOOLS } });
15344
15393
  } else if (req.method === 'tools/call') {
15345
15394
  const { name, arguments: args = {} } = req.params || {};
15395
+ // 1.12.2 (14th 버그헌트, UR-0181): MCP 서버는 generic 설계 — cwd 에서 실행 후 각 호출의 path 로 대상 프로젝트 지정(의도된 기능, e2e 가 검증). 즉 path-타게팅 자체는 취약점 아님(신뢰된 로컬 MCP). policy 도 대상-프로젝트별로 path 에서 로드(올바름).
15396
+ // 진짜 버그는 unknown tool 도 _bumpMcpUsage 가 임의 경로에 무조건 쓰던 것 → 사용 통계를 unknown-tool 검증 "후"로 이동. (프로젝트-scoped 가둠은 opt-in 플래그로 별도 검토 — UR backlog)
15346
15397
  const targetPath = args.path || root;
15347
- // 1.9.70: MCP tools/call 자동 사용 통계 — 어떤 도구가 자주/드물게 호출되는지 가시화
15348
- try { _bumpMcpUsage(targetPath, name); } catch {}
15349
15398
  let cliArgs;
15350
15399
  try {
15351
15400
  cliArgs = _mcpToCliArgs(name, args, targetPath);
15352
15401
  if (cliArgs === null) return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } });
15402
+ try { _bumpMcpUsage(targetPath, name); } catch {} // 알려진 도구만 통계 기록(unknown tool 의 임의경로 쓰기 차단)
15353
15403
  // 1.9.288 (Codex gpt-5.5 리뷰 #1 수렴): MCP 도구도 policy enforce 적용 — read-only enforce 시 write 도구 차단.
15354
15404
  // 이전: _policyEnforce 는 agents multi --execute 한 곳뿐 → MCP state_start 등이 정책 우회하고 .leerness 기록.
15355
15405
  // cliArgs(실제 실행 명령) 로 required tier 판정 → enforce ON 이고 초과 시 JSON-RPC error 반환(실행 안 함).
@@ -15364,7 +15414,9 @@ function mcpServeCmd(root) {
15364
15414
  const r = callLeerness(cliArgs);
15365
15415
  // 1.9.61: cursor 기반 페이지네이션 — 긴 출력은 cursor offset로 다음 청크
15366
15416
  const fullText = r.stdout || r.stderr || '(no output)';
15367
- const CHUNK_SIZE = (args._chunkSize && Number.isFinite(args._chunkSize)) ? args._chunkSize : 50000;
15417
+ // 1.12.4 (15th 버그헌트 P2, UR-0016): _chunkSize 양의 정수로 클램프 — 음수/소수면 slice 빈 출력 + nextCursor 음수 → 무한 빈-루프(데이터 손실)였음.
15418
+ const _cs = Math.floor(Number(args._chunkSize));
15419
+ const CHUNK_SIZE = (Number.isFinite(_cs) && _cs > 0) ? _cs : 50000;
15368
15420
  const cursor = (args._cursor && /^\d+$/.test(String(args._cursor))) ? parseInt(args._cursor, 10) : 0;
15369
15421
  const chunk = fullText.slice(cursor, cursor + CHUNK_SIZE);
15370
15422
  const nextCursor = (cursor + CHUNK_SIZE) < fullText.length ? String(cursor + CHUNK_SIZE) : null;
package/lib/analyzers.js CHANGED
@@ -1,90 +1,90 @@
1
- // lib/analyzers.js — 순수 분석/검증 함수 (부작용 0, 입력→출력).
2
- // 1.9.304 (UR-0025): bin/harness.js 에서 비파괴 분리. selftest(evidenceQuality/parseEvidenceStats/shellGuardAnalyze/claimFileInGit)가 동작 검증.
3
- 'use strict';
4
-
5
- function _shellGuardAnalyze(cmd, ctx) {
6
- const c = String(cmd || '');
7
- const shell = (ctx && ctx.shell) || 'unknown';
8
- const psVer = ctx && ctx.psVersion != null ? parseInt(ctx.psVersion, 10) : null;
9
- const issues = [];
10
- const isWinPowerShell = shell === 'powershell' && psVer != null && psVer < 6; // 5.1 = Windows PowerShell
11
- // 규칙 1: PowerShell 5.1 에서 && / || 체이닝 미지원 (pwsh 7+ 부터 지원)
12
- if (isWinPowerShell && /\s&&\s|\s\|\|\s/.test(c)) {
13
- issues.push({ rule: 'ps5-chain', severity: 'error', detail: 'Windows PowerShell 5.1 은 && / || 연산자를 지원하지 않습니다 (PowerShell 7+ 부터 지원).', suggestion: 'A; if ($?) { B } (조건부) 또는 A; B (무조건) 로 분리. 또는 pwsh 7 설치.' });
14
- }
15
- // 규칙 2: PowerShell 에서 2>/dev/null → 2>$null
16
- if (shell === 'powershell' && /2>\s*\/dev\/null/.test(c)) {
17
- issues.push({ rule: 'ps-devnull', severity: 'error', detail: 'PowerShell 은 /dev/null 경로가 없습니다.', suggestion: '2>$null 사용 (PowerShell 리다이렉트).' });
18
- }
19
- // 규칙 3: PowerShell 에서 inline env (VAR=val cmd) 미지원
20
- if (shell === 'powershell' && /^[A-Z_][A-Z0-9_]*=[^\s]+\s+\S/.test(c.trim())) {
21
- issues.push({ rule: 'ps-inline-env', severity: 'error', detail: 'PowerShell 은 VAR=val cmd 형식의 inline 환경변수를 지원하지 않습니다.', suggestion: "$env:VAR='val'; cmd 로 분리." });
22
- }
23
- // 규칙 4: PowerShell 에서 Unix 전용 명령 (rm -rf / ls -la 등) — 별칭은 되나 플래그 오류 가능
24
- if (shell === 'powershell' && /\brm\s+-rf\b/.test(c)) {
25
- issues.push({ rule: 'ps-rm-rf', severity: 'warn', detail: 'PowerShell 에서 rm -rf 는 -rf 플래그 파싱 오류 가능 (rm 은 Remove-Item 별칭).', suggestion: 'Remove-Item -Recurse -Force <path> 사용.' });
26
- }
27
- // 규칙 5: CMD 에서 ; 는 명령 구분자가 아님 (한 줄로 실행됨)
28
- if (shell === 'cmd' && /;/.test(c) && !/&&|\|\|/.test(c)) {
29
- issues.push({ rule: 'cmd-semicolon', severity: 'warn', detail: 'CMD 는 ; 를 명령 구분자로 처리하지 않습니다 (인자로 전달됨).', suggestion: 'A && B (조건부) 또는 A & B (무조건) 사용.' });
30
- }
31
- // 규칙 6: PowerShell 에서 && 가 있으나 버전 미상 — 정보성
32
- if (shell === 'powershell' && psVer == null && /\s&&\s/.test(c)) {
33
- issues.push({ rule: 'ps-version-unknown', severity: 'info', detail: 'PowerShell 버전 미상 — 5.1 이면 && 미지원, 7+ 이면 지원.', suggestion: '$PSVersionTable.PSVersion 확인. 안전하게 A; if ($?) { B } 권장.' });
34
- }
35
- return { shell, psVersion: psVer, issues };
36
- }
37
- function _evidenceQuality(evidence) {
38
- const e = String(evidence || '');
39
- const hasFile = /(?:[A-Za-z][\w-]*[\/\\])?[A-Za-z][\w./\\-]*\.(?:js|ts|tsx|jsx|mjs|cjs|py|go|rs|rb|kt|cs|gd|java|php|swift|c|cpp|h|html|css|scss|vue|svelte|json|yaml|yml|toml|md|sql|sh)\b/i.test(e);
40
- const hasTest = /(\d+)\s*(?:\/\s*\d+\s*)?(?:통과|passed|passing|개\s*테스트)|\btests?\b\s*[:=]?\s*\d|Tests?:\s*\d|\b\d+\s*tests?\b/i.test(e);
41
- const hasLog = /Exit\s*[:=]|exit\s*code|Command\s*[:=]|npm\s+(?:test|run)|pytest|cargo\s+test|go\s+test/i.test(e);
42
- const missing = [];
43
- if (!hasFile) missing.push('수정 파일 경로');
44
- if (!hasTest) missing.push('테스트명/개수');
45
- if (!hasLog) missing.push('실행 로그(Command/Exit)');
46
- return { hasFile, hasTest, hasLog, ok: hasFile && hasTest, missing };
47
- }
48
- function _claimFileInGit(claimed, gitSet) {
49
- if (!gitSet) return null;
50
- const c = String(claimed).replace(/\\/g, '/').replace(/^\.\//, '');
51
- for (const g of gitSet) { if (g === c || g.endsWith('/' + c) || c.endsWith('/' + g)) return true; }
52
- return false;
53
- }
54
- function _parseEvidenceStats(text) {
55
- const t = String(text || '');
56
- const blocks = t.split(/\n(?=## )/).filter(b => /Command:|Exit:|verify|test/i.test(b));
57
- let pass = 0, fail = 0;
58
- for (const b of blocks) {
59
- const exitM = b.match(/Exit:\s*(-?\d+)/i);
60
- if (exitM) { (parseInt(exitM[1], 10) === 0 ? pass++ : fail++); continue; }
61
- if (/\bPASS\b|통과|성공|✓/i.test(b)) pass++;
62
- else if (/\bFAIL\b|실패|오류|error|✗/i.test(b)) fail++;
63
- }
64
- const entries = blocks.length;
65
- return { entries, pass, fail, rate: (pass + fail) ? Math.round(pass / (pass + fail) * 100) : null };
66
- }
67
-
68
- // 1.9.305 (사용자 명시): AI 인식론적 정직성 점검 — 모르는 걸 아는 척 / 정보 미수집 / 미검증 섣부른 판단 휴리스틱 탐지.
69
- // 순수 함수(텍스트→findings). 휴리스틱 advisory — 단정/추정/외부참조 표현 vs 근거·수집 흔적 대조. opt-in 점검용.
70
- function _epistemicHonestyCheck(text) {
71
- const t = String(text || '');
72
- const findings = [];
73
- // 공통: 근거/출처 흔적 (파일경로·URL·테스트결과·Exit·문서·api-skill·인용·조회 흔적)
74
- const hasSource = /(?:[\w./-]+\.(?:js|ts|tsx|jsx|py|go|rs|rb|md|json|ya?ml|toml|sql|sh)\b)|https?:\/\/|\bExit\s*[:=]|\d+\s*\/\s*\d+\s*(?:통과|passed)|\b(?:passed|passing)\b|근거[::]|출처[::]|api-skill|공식\s*문서|문서\s*(?:확인|참조|에\s*따르면)|읽었|조회(?:함|했|함\b)|확인(?:함|했|됨)|grep|로그[::]/i.test(t);
75
- // 차원1: 모르는 걸 아는 척 — 단정 표현인데 근거 없음
76
- const definitive = /(반드시|항상|언제나|무조건|확실(?:히|함|하게)|당연히|틀림없|100\s*%|always|never|guaranteed|definitely|obviously|certainly)/i.test(t);
77
- if (definitive && !hasSource) findings.push({ dim: 'pretend-knowledge', severity: 'high', label: '근거 없는 단정', detail: '단정적 표현이 있으나 근거/출처(파일·문서·테스트·로그)가 없음 — 모르는 정보를 아는 척할 위험.' });
78
- // 차원2: 미검증 섣부른 판단 — 추정 표현 + 완료/성공 결론인데 근거 없음
79
- const assumption = /(아마|추정|것\s*같|듯\s*(?:하|싶)|probably|likely|maybe|perhaps|i\s*(?:think|assume|guess|believe|suppose)|should\s*(?:work|be|pass|fix)|생각(?:됩니다|된다|함|돼)|일\s*것|예상(?:됩니다|된다|됨)|짐작)/i.test(t);
80
- const conclusion = /(완료|done|성공|통과|해결(?:됨|했|함|되었)|fixed|resolved|works?\b|작동(?:함|한다|됨)|구현(?:됨|했|완료))/i.test(t);
81
- if (assumption && conclusion && !hasSource) findings.push({ dim: 'premature-judgment', severity: 'high', label: '검증 없는 섣부른 판단', detail: '가정·추정 표현과 완료·성공 결론이 함께 있으나 검증 근거가 없음 — 검증 없이 섣부르게 판단할 위험.' });
82
- // 차원3: 정보 미수집 — 외부 API/라이브러리/버전/스펙 언급인데 수집·근거 흔적 없음
83
- // \bAPI\b(?!\.[a-z]) 로 파일경로(api.js/api.ts) 오탐 제외. 강한 근거(hasSource)나 수집 흔적(gathered) 있으면 통과.
84
- const externalRef = /(\bAPI\b(?!\.[a-z])|\bSDK\b|라이브러리|\blibrary\b|\bpackage\b|엔드포인트|\bendpoint\b|버전\s*\d|v\d+\.\d+|\bspec\b|rate\s*limit|레이트\s*리밋|문서에\s*따르면)/i.test(t);
85
- const gathered = /(https?:\/\/|api-skill|공식\s*문서|\bdocs?\b|문서\s*(?:확인|참조|읽)|읽었|조회(?:함|했)|확인(?:함|했|됨)|fetch|검색(?:함|했)|레퍼런스|reference)/i.test(t);
86
- if (externalRef && !gathered && !hasSource) findings.push({ dim: 'no-info-gathering', severity: 'medium', label: '외부 정보 미수집', detail: '외부 API/라이브러리/버전/스펙 언급이 있으나 정보 수집(공식문서·api-skill·조회) 흔적이 없음 — 정확한 정보를 먼저 수집 권장.' });
87
- return { ok: findings.length === 0, findings, dimensions: ['pretend-knowledge', 'premature-judgment', 'no-info-gathering'] };
88
- }
89
-
90
- module.exports = { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInGit, _epistemicHonestyCheck };
1
+ // lib/analyzers.js — 순수 분석/검증 함수 (부작용 0, 입력→출력).
2
+ // 1.9.304 (UR-0025): bin/harness.js 에서 비파괴 분리. selftest(evidenceQuality/parseEvidenceStats/shellGuardAnalyze/claimFileInGit)가 동작 검증.
3
+ 'use strict';
4
+
5
+ function _shellGuardAnalyze(cmd, ctx) {
6
+ const c = String(cmd || '');
7
+ const shell = (ctx && ctx.shell) || 'unknown';
8
+ const psVer = ctx && ctx.psVersion != null ? parseInt(ctx.psVersion, 10) : null;
9
+ const issues = [];
10
+ const isWinPowerShell = shell === 'powershell' && psVer != null && psVer < 6; // 5.1 = Windows PowerShell
11
+ // 규칙 1: PowerShell 5.1 에서 && / || 체이닝 미지원 (pwsh 7+ 부터 지원)
12
+ if (isWinPowerShell && /&&|\|\|/.test(c)) { // 1.12.5 (15th 버그헌트 P2, UR-0018): 공백 무관 — PS5.1 은 a&&b 도 거부(이전 /\s&&\s/ 는 양쪽 공백 요구해 npm 체인 a&&b 미탐).
13
+ issues.push({ rule: 'ps5-chain', severity: 'error', detail: 'Windows PowerShell 5.1 은 && / || 연산자를 지원하지 않습니다 (PowerShell 7+ 부터 지원).', suggestion: 'A; if ($?) { B } (조건부) 또는 A; B (무조건) 로 분리. 또는 pwsh 7 설치.' });
14
+ }
15
+ // 규칙 2: PowerShell 에서 2>/dev/null → 2>$null
16
+ if (shell === 'powershell' && /2>\s*\/dev\/null/.test(c)) {
17
+ issues.push({ rule: 'ps-devnull', severity: 'error', detail: 'PowerShell 은 /dev/null 경로가 없습니다.', suggestion: '2>$null 사용 (PowerShell 리다이렉트).' });
18
+ }
19
+ // 규칙 3: PowerShell 에서 inline env (VAR=val cmd) 미지원
20
+ if (shell === 'powershell' && /^[A-Z_][A-Z0-9_]*=[^\s]+\s+\S/.test(c.trim())) {
21
+ issues.push({ rule: 'ps-inline-env', severity: 'error', detail: 'PowerShell 은 VAR=val cmd 형식의 inline 환경변수를 지원하지 않습니다.', suggestion: "$env:VAR='val'; cmd 로 분리." });
22
+ }
23
+ // 규칙 4: PowerShell 에서 Unix 전용 명령 (rm -rf / ls -la 등) — 별칭은 되나 플래그 오류 가능
24
+ if (shell === 'powershell' && /\brm\s+-rf\b/.test(c)) {
25
+ issues.push({ rule: 'ps-rm-rf', severity: 'warn', detail: 'PowerShell 에서 rm -rf 는 -rf 플래그 파싱 오류 가능 (rm 은 Remove-Item 별칭).', suggestion: 'Remove-Item -Recurse -Force <path> 사용.' });
26
+ }
27
+ // 규칙 5: CMD 에서 ; 는 명령 구분자가 아님 (한 줄로 실행됨)
28
+ if (shell === 'cmd' && /;/.test(c) && !/&&|\|\|/.test(c)) {
29
+ issues.push({ rule: 'cmd-semicolon', severity: 'warn', detail: 'CMD 는 ; 를 명령 구분자로 처리하지 않습니다 (인자로 전달됨).', suggestion: 'A && B (조건부) 또는 A & B (무조건) 사용.' });
30
+ }
31
+ // 규칙 6: PowerShell 에서 && 가 있으나 버전 미상 — 정보성
32
+ if (shell === 'powershell' && psVer == null && /&&|\|\|/.test(c)) { // 1.12.5 (UR-0018): 공백 무관 매칭
33
+ issues.push({ rule: 'ps-version-unknown', severity: 'info', detail: 'PowerShell 버전 미상 — 5.1 이면 && 미지원, 7+ 이면 지원.', suggestion: '$PSVersionTable.PSVersion 확인. 안전하게 A; if ($?) { B } 권장.' });
34
+ }
35
+ return { shell, psVersion: psVer, issues };
36
+ }
37
+ function _evidenceQuality(evidence) {
38
+ const e = String(evidence || '');
39
+ const hasFile = /(?:[A-Za-z][\w-]*[\/\\])?[A-Za-z][\w./\\-]*\.(?:js|ts|tsx|jsx|mjs|cjs|py|go|rs|rb|kt|cs|gd|java|php|swift|c|cpp|h|html|css|scss|vue|svelte|json|yaml|yml|toml|md|sql|sh)\b/i.test(e);
40
+ const hasTest = /(\d+)\s*(?:\/\s*\d+\s*)?(?:통과|passed|passing|개\s*테스트)|\btests?\b\s*[:=]?\s*\d|Tests?:\s*\d|\b\d+\s*tests?\b/i.test(e);
41
+ const hasLog = /Exit\s*[:=]|exit\s*code|Command\s*[:=]|npm\s+(?:test|run)|pytest|cargo\s+test|go\s+test/i.test(e);
42
+ const missing = [];
43
+ if (!hasFile) missing.push('수정 파일 경로');
44
+ if (!hasTest) missing.push('테스트명/개수');
45
+ if (!hasLog) missing.push('실행 로그(Command/Exit)');
46
+ return { hasFile, hasTest, hasLog, ok: hasFile && hasTest, missing };
47
+ }
48
+ function _claimFileInGit(claimed, gitSet) {
49
+ if (!gitSet) return null;
50
+ const c = String(claimed).replace(/\\/g, '/').replace(/^\.\//, '');
51
+ for (const g of gitSet) { if (g === c || g.endsWith('/' + c) || c.endsWith('/' + g)) return true; }
52
+ return false;
53
+ }
54
+ function _parseEvidenceStats(text) {
55
+ const t = String(text || '');
56
+ const blocks = t.split(/\n(?=## )/).filter(b => /Command:|Exit:|verify|test/i.test(b));
57
+ let pass = 0, fail = 0;
58
+ for (const b of blocks) {
59
+ const exitM = b.match(/Exit:\s*(-?\d+)/i);
60
+ if (exitM) { (parseInt(exitM[1], 10) === 0 ? pass++ : fail++); continue; }
61
+ if (/\bPASS\b|통과|성공|✓/i.test(b)) pass++;
62
+ else if (/\bFAIL\b|실패|오류|error|✗/i.test(b)) fail++;
63
+ }
64
+ const entries = blocks.length;
65
+ return { entries, pass, fail, rate: (pass + fail) ? Math.round(pass / (pass + fail) * 100) : null };
66
+ }
67
+
68
+ // 1.9.305 (사용자 명시): AI 인식론적 정직성 점검 — 모르는 걸 아는 척 / 정보 미수집 / 미검증 섣부른 판단 휴리스틱 탐지.
69
+ // 순수 함수(텍스트→findings). 휴리스틱 advisory — 단정/추정/외부참조 표현 vs 근거·수집 흔적 대조. opt-in 점검용.
70
+ function _epistemicHonestyCheck(text) {
71
+ const t = String(text || '');
72
+ const findings = [];
73
+ // 공통: 근거/출처 흔적 (파일경로·URL·테스트결과·Exit·문서·api-skill·인용·조회 흔적)
74
+ const hasSource = /(?:[\w./-]+\.(?:js|ts|tsx|jsx|py|go|rs|rb|md|json|ya?ml|toml|sql|sh)\b)|https?:\/\/|\bExit\s*[:=]|\d+\s*\/\s*\d+\s*(?:통과|passed)|\b(?:passed|passing)\b|근거[::]|출처[::]|api-skill|공식\s*문서|문서\s*(?:확인|참조|에\s*따르면)|읽었|조회(?:함|했|함\b)|확인(?:함|했|됨)|grep|로그[::]/i.test(t);
75
+ // 차원1: 모르는 걸 아는 척 — 단정 표현인데 근거 없음
76
+ const definitive = /(반드시|항상|언제나|무조건|확실(?:히|함|하게)|당연히|틀림없|100\s*%|always|never|guaranteed|definitely|obviously|certainly)/i.test(t);
77
+ if (definitive && !hasSource) findings.push({ dim: 'pretend-knowledge', severity: 'high', label: '근거 없는 단정', detail: '단정적 표현이 있으나 근거/출처(파일·문서·테스트·로그)가 없음 — 모르는 정보를 아는 척할 위험.' });
78
+ // 차원2: 미검증 섣부른 판단 — 추정 표현 + 완료/성공 결론인데 근거 없음
79
+ const assumption = /(아마|추정|것\s*같|듯\s*(?:하|싶)|probably|likely|maybe|perhaps|i\s*(?:think|assume|guess|believe|suppose)|should\s*(?:work|be|pass|fix)|생각(?:됩니다|된다|함|돼)|일\s*것|예상(?:됩니다|된다|됨)|짐작)/i.test(t);
80
+ const conclusion = /(완료|done|성공|통과|해결(?:됨|했|함|되었)|fixed|resolved|works?\b|작동(?:함|한다|됨)|구현(?:됨|했|완료))/i.test(t);
81
+ if (assumption && conclusion && !hasSource) findings.push({ dim: 'premature-judgment', severity: 'high', label: '검증 없는 섣부른 판단', detail: '가정·추정 표현과 완료·성공 결론이 함께 있으나 검증 근거가 없음 — 검증 없이 섣부르게 판단할 위험.' });
82
+ // 차원3: 정보 미수집 — 외부 API/라이브러리/버전/스펙 언급인데 수집·근거 흔적 없음
83
+ // \bAPI\b(?!\.[a-z]) 로 파일경로(api.js/api.ts) 오탐 제외. 강한 근거(hasSource)나 수집 흔적(gathered) 있으면 통과.
84
+ const externalRef = /(\bAPI\b(?!\.[a-z])|\bSDK\b|라이브러리|\blibrary\b|\bpackage\b|엔드포인트|\bendpoint\b|버전\s*\d|v\d+\.\d+|\bspec\b|rate\s*limit|레이트\s*리밋|문서에\s*따르면)/i.test(t);
85
+ const gathered = /(https?:\/\/|api-skill|공식\s*문서|\bdocs?\b|문서\s*(?:확인|참조|읽)|읽었|조회(?:함|했)|확인(?:함|했|됨)|fetch|검색(?:함|했)|레퍼런스|reference)/i.test(t);
86
+ if (externalRef && !gathered && !hasSource) findings.push({ dim: 'no-info-gathering', severity: 'medium', label: '외부 정보 미수집', detail: '외부 API/라이브러리/버전/스펙 언급이 있으나 정보 수집(공식문서·api-skill·조회) 흔적이 없음 — 정확한 정보를 먼저 수집 권장.' });
87
+ return { ok: findings.length === 0, findings, dimensions: ['pretend-knowledge', 'premature-judgment', 'no-info-gathering'] };
88
+ }
89
+
90
+ module.exports = { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInGit, _epistemicHonestyCheck };
package/lib/catalogs.js CHANGED
@@ -232,37 +232,38 @@ const _LSP_LANG_PATTERNS = {
232
232
  // evidence에 "DB 저장" / "insert N건" / "DB에" → db.*/pg.*/mysql.*/mongoose.*/prisma.* 없으면 의심
233
233
  // evidence에 "이메일 발송" / "메일 전송" → sendMail/nodemailer/smtp 없으면 의심
234
234
  // 1.9.27: 패턴 카탈로그 확장 (5 → 10) + URL/메서드 단위 매핑 추가
235
+ // 1.12.4 (15th 버그헌트 P1, UR-0014): codeRe 다언어 확장 — _scanCodeForPatterns 는 13개 언어를 읽는데 패턴이 JS전용이라 Python/Ruby/Go/C#/Java/PHP/Rust 정상 구현을 '호출 흔적 없음' 으로 오판(verify-claim 기본 게이트 false-fail, 1.12.0 핵심가치 회귀). 각 codeRe 에 교차언어 idiom 추가. (검출 관대화 → 정직한 비JS 작업 오차단 제거; 과탐보다 안전한 방향.)
235
236
  const OPTIMISM_PATTERNS = [
236
237
  { kind: 'API', evidenceRe: /(API\s*호출|HTTP\s*\d{3}|POST\s*\/|GET\s*\/|PUT\s*\/|DELETE\s*\/|fetch|REST 응답|응답 확인|endpoint|엔드포인트)/i,
237
- codeRe: /\b(fetch\s*\(|http\.request|https\.request|axios\.|got\.|undici|node-fetch)/i,
238
+ codeRe: /\b(fetch\s*\(|http\.request|https\.request|axios\.|got\.|undici|node-fetch|requests\.|httpx|urllib|http\.client|net\/http|Net::HTTP|HTTParty|Faraday|HttpClient|http\.Get|http\.Post|http\.NewRequest|reqwest|curl_|HttpURLConnection|RestTemplate|URLSession)/i,
238
239
  label: 'API/HTTP 호출' },
239
240
  { kind: 'DB', evidenceRe: /(DB에?\s*저장|insert\s+\d+|데이터베이스|SQL\s*(INSERT|UPDATE|DELETE)|migration|마이그레이션 적용)/i,
240
- codeRe: /\b(db\.|pg\.|pool\.|mysql\.|mongoose\.|prisma\.|sequelize|knex|sqlite3|MongoClient|createConnection)/i,
241
+ codeRe: /\b(db\.|pg\.|pool\.|mysql\.|mongoose\.|prisma\.|sequelize|knex|sqlite3|MongoClient|createConnection|psycopg|sqlalchemy|cursor\.execute|django\.db|sqlx|gorm|database\/sql|ActiveRecord|jdbc|EntityManager|DbContext|mysqli)/i,
241
242
  label: 'DB 호출' },
242
243
  { kind: 'Email', evidenceRe: /(이메일[^.\n]{0,30}(발송|전송|보냈|보냄|완료)|메일[^.\n]{0,30}(발송|전송|보냈|보냄)|sendMail|smtp\s*(전송|발송))/i,
243
- codeRe: /\b(sendMail|nodemailer|smtp|@sendgrid|mailgun|aws-sdk\/ses|resend\.)/i,
244
+ codeRe: /\b(sendMail|nodemailer|smtp|@sendgrid|mailgun|aws-sdk\/ses|resend\.|smtplib|django\.core\.mail|ActionMailer|net\/smtp|Net::SMTP|SmtpClient|javax\.mail|JavaMailSender)/i,
244
245
  label: '이메일 전송' },
245
246
  { kind: 'Webhook', evidenceRe: /(웹훅\s*(호출|전송|발송)|webhook\s+(sent|posted|triggered))/i,
246
- codeRe: /\b(fetch\s*\(|http\.request|axios\.)/i,
247
+ codeRe: /\b(fetch\s*\(|http\.request|axios\.|requests\.|urllib|net\/http|Net::HTTP|http\.Post|reqwest|curl_|HttpClient)/i,
247
248
  label: '웹훅' },
248
249
  { kind: 'Payment', evidenceRe: /(결제\s*(완료|성공|승인|취소)|payment\s+(processed|charged)|stripe 결제|toss\s*결제|카카오페이|네이버페이|kakaopay|nicepay|iamport 결제|페이팔|paypal)/i,
249
- codeRe: /\b(stripe|toss|@stripe|tosspayments|iamport|kakao|nicepay|naverpay|paypal-rest-sdk|@paypal)/i,
250
+ codeRe: /\b(stripe|toss|@stripe|tosspayments|iamport|kakao|nicepay|naverpay|paypal-rest-sdk|@paypal|razorpay|braintree)/i,
250
251
  label: '결제' },
251
252
  // 1.9.27 신규 카테고리
252
253
  { kind: 'FileIO', evidenceRe: /(파일[^.\n]{0,20}(생성|저장|작성|기록)|\d+개[^.\n]{0,20}파일|디스크[^.\n]{0,20}저장|로그 파일 작성)/i,
253
- codeRe: /\b(fs\.write|fs\.appendFile|writeFileSync|appendFileSync|fs\/promises|fs\.createWriteStream)/i,
254
+ codeRe: /\b(fs\.write|fs\.appendFile|writeFileSync|appendFileSync|fs\/promises|fs\.createWriteStream|open\s*\([^)]*['"][wa]|File\.Write|ioutil\.WriteFile|os\.WriteFile|fopen|File\.open|Files\.write)/i,
254
255
  label: '파일 I/O 쓰기' },
255
256
  { kind: 'Queue', evidenceRe: /(메시지\s*큐|발행\s*완료|publish\s*(완료|성공)|RabbitMQ|Kafka|SQS|Redis Pub|이벤트 발행)/i,
256
- codeRe: /\b(amqp|kafkajs|rabbit|redis\.(publish|xadd)|@aws-sdk\/client-sqs|bull|bullmq)/i,
257
+ codeRe: /\b(amqp|kafkajs|rabbit|redis\.(publish|xadd)|@aws-sdk\/client-sqs|bull|bullmq|pika|kombu|confluent_kafka|sidekiq|celery|boto3)/i,
257
258
  label: '메시지 큐 발행' },
258
259
  { kind: 'Cache', evidenceRe: /(Redis[^.\n]{0,20}(저장|set|get)|캐시[^.\n]{0,20}(저장|기록|적중)|memcache)/i,
259
- codeRe: /\b(redis\.|ioredis|memcached|node-cache|@upstash\/redis|connect-redis)/i,
260
+ codeRe: /\b(redis\.|ioredis|memcached|node-cache|@upstash\/redis|connect-redis|Redis\.|StackExchange\.Redis|go-redis|jedis)/i,
260
261
  label: '캐시 저장' },
261
262
  { kind: 'Notify', evidenceRe: /(슬랙\s*(알림|발송|전송)|Slack\s+(notification|sent|posted)|Discord\s+(알림|발송|webhook)|푸시 알림 전송)/i,
262
- codeRe: /\b(@slack\/web-api|slack-webhook|discord\.js|discord-webhook|@discordjs|firebase\/messaging|expo-notifications)/i,
263
+ codeRe: /\b(@slack\/web-api|slack-webhook|discord\.js|discord-webhook|@discordjs|firebase\/messaging|expo-notifications|slack_sdk|slack-sdk|discord\.py|discordgo)/i,
263
264
  label: '슬랙/Discord 알림' },
264
265
  { kind: 'Storage', evidenceRe: /(S3\s*(업로드|저장)|GCS\s*업로드|Azure Blob|클라우드 스토리지 업로드|object storage 저장)/i,
265
- codeRe: /\b(@aws-sdk\/client-s3|aws-sdk[^a-z]|@google-cloud\/storage|@azure\/storage-blob|aws-s3)/i,
266
+ codeRe: /\b(@aws-sdk\/client-s3|aws-sdk[^a-z]|@google-cloud\/storage|@azure\/storage-blob|aws-s3|boto3|google\.cloud\.storage|azure\.storage|minio)/i,
266
267
  label: '클라우드 스토리지' }
267
268
  ];
268
269
 
package/lib/pure-utils.js CHANGED
@@ -979,7 +979,9 @@ function _parseRequirementsTxt(text) {
979
979
  const out = [];
980
980
  for (const raw of text.split(/\r?\n/)) {
981
981
  const line = raw.replace(/#.*$/, '').trim(); if (!line) continue;
982
- const m = line.match(/^([A-Za-z0-9_.\-]+)/); if (m && !out.includes(m[1])) out.push(m[1]);
982
+ // 1.12.5 (15th 버그헌트 P3, UR-0021): pip 디렉티브(-e/-r/--hash/-c) skip + 패키지명은 영숫자로 시작(이전엔 -e/-r/--hash/. 가 패키지로 파싱됨).
983
+ if (line.startsWith('-')) continue;
984
+ const m = line.match(/^([A-Za-z0-9][A-Za-z0-9_.\-]*)/); if (m && !out.includes(m[1])) out.push(m[1]);
983
985
  }
984
986
  return out;
985
987
  }
@@ -1007,9 +1009,10 @@ function _renderGlossaryMd(entries, opts = {}) {
1007
1009
  if (entries.length) {
1008
1010
  s += '| 패키지 | 쉽게 말하면 (KO) | In plain terms (EN) | 분류 | 출처 |\n|---|---|---|---|---|\n';
1009
1011
  for (const e of entries) {
1010
- const ko = lang === 'en' ? '' : _lineSafe(e.plainKo || '');
1011
- const en = lang === 'ko' ? '' : _lineSafe(e.plainEn || '');
1012
- s += `| ${_lineSafe(e.term)} | ${ko} | ${en} | ${_lineSafe(e.category || '')} | ${e.source} |\n`;
1012
+ // 1.12.4 (15th 버그헌트 P2, UR-0015): 셀은 _cellSafe(파이프 escape) — _lineSafe 는 개행만 제거해 description 의 '|' 칼럼을 깨뜨렸음(node_modules description fallback 벡터).
1013
+ const ko = lang === 'en' ? '' : _cellSafe(e.plainKo || '');
1014
+ const en = lang === 'ko' ? '' : _cellSafe(e.plainEn || '');
1015
+ s += `| ${_cellSafe(e.term)} | ${ko} | ${en} | ${_cellSafe(e.category || '')} | ${e.source} |\n`;
1013
1016
  }
1014
1017
  s += '\n';
1015
1018
  }