leerness 1.12.1 → 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 +74 -0
- package/README.md +4 -4
- package/bin/leerness.js +67 -16
- package/lib/analyzers.js +90 -90
- package/lib/catalogs.js +11 -10
- package/lib/pure-utils.js +7 -4
- package/lib/session-close.js +620 -616
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
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
|
+
|
|
3
77
|
## 1.12.1 — 2026-06-08 — 🩹 [Stable hotfix] 클린룸 selftest false-alarm 수정 (UR-0008)
|
|
4
78
|
|
|
5
79
|
**1.12.0 직후 발견된 사용자-노출 결함 핫픽스 — npm 배포(예외).**
|
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.
|
|
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.
|
|
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.1는 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.
|
|
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.
|
|
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.
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -3522,6 +3522,44 @@ function _selfTestCases() {
|
|
|
3522
3522
|
return md.includes(m.GLOSSARY_START) && md.includes('react') && /미정의|unknownpkgxyz/.test(md)
|
|
3523
3523
|
&& read(__filename).includes("if (cmd === 'glossary')");
|
|
3524
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
|
+
} },
|
|
3525
3563
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3526
3564
|
];
|
|
3527
3565
|
}
|
|
@@ -3799,9 +3837,10 @@ function _serializeAPISkill(id, name, urls, direction, doc) {
|
|
|
3799
3837
|
function _loadAPISkill(root, id) {
|
|
3800
3838
|
const fp = path.join(_apiSkillsDir(root), id + '.md');
|
|
3801
3839
|
if (!fs.existsSync(fp)) return null;
|
|
3802
|
-
|
|
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');
|
|
3803
3842
|
const fm = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
3804
|
-
if (!fm) return { id, content, urls: [], name: id };
|
|
3843
|
+
if (!fm) return { id, content, urls: [], name: id, body: content };
|
|
3805
3844
|
const meta = {};
|
|
3806
3845
|
fm[1].split('\n').forEach(l => {
|
|
3807
3846
|
const m = l.match(/^(\w+):\s*(.*)$/);
|
|
@@ -7319,8 +7358,10 @@ function getExtraSkipDirs(root) {
|
|
|
7319
7358
|
return read(f).split('\n').map(s => s.trim().replace(/\/+$/, '')).filter(s => s && !s.startsWith('#'));
|
|
7320
7359
|
}
|
|
7321
7360
|
function isSkippedRel(rel, extras = []) {
|
|
7322
|
-
|
|
7323
|
-
|
|
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 + '/'));
|
|
7324
7365
|
}
|
|
7325
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','']);
|
|
7326
7367
|
function* walk(root, base = root, depth = 0, extras = null) {
|
|
@@ -7356,6 +7397,8 @@ function _collectSecretFindings(root) {
|
|
|
7356
7397
|
const isEnvFamily = /^\.env(\.|$)/.test(path.basename(file));
|
|
7357
7398
|
if (!SCAN_TEXT_EXT.has(ext) && !isEnvFamily) continue;
|
|
7358
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; }
|
|
7359
7402
|
try { text = read(file); } catch { continue; }
|
|
7360
7403
|
if (text.length > 1024 * 1024) continue;
|
|
7361
7404
|
const fileRel = (file === root) ? path.basename(file) : rel(root, file);
|
|
@@ -7414,6 +7457,8 @@ function encodingCheck(root, opts = {}) {
|
|
|
7414
7457
|
const ext = path.extname(file).toLowerCase();
|
|
7415
7458
|
if (!SCAN_TEXT_EXT.has(ext)) continue;
|
|
7416
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; }
|
|
7417
7462
|
try { buf = readBuf(file); } catch { continue; }
|
|
7418
7463
|
if (buf.length === 0) continue;
|
|
7419
7464
|
if (buf.length > 5 * 1024 * 1024) continue;
|
|
@@ -7521,14 +7566,16 @@ function lazyDetect(root, opts = {}) {
|
|
|
7521
7566
|
}
|
|
7522
7567
|
}
|
|
7523
7568
|
if (todoCount > 0) {
|
|
7524
|
-
|
|
7525
|
-
|
|
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) {
|
|
7526
7573
|
issues++;
|
|
7527
|
-
_warn(`code has ${todoCount} TODO/FIXME/XXX (
|
|
7528
|
-
{ kind: 'todo_untracked', severity: 'warn', todoCount, newCount: newTodos.length, newTodos:
|
|
7529
|
-
//
|
|
7530
|
-
if (!jsonMode)
|
|
7531
|
-
if (has('--auto-track') &&
|
|
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) {
|
|
7532
7579
|
// 1.9.411 (8번째 버그헌트, UR-0115): TODO 일괄 등록을 단일 read-modify-write 로 직렬화.
|
|
7533
7580
|
// 종전: TODO 마다 nextId(plan+progress 전체 스캔) + upsertProgress(전체 read+write) → O(T × tracker크기) (다수 TODO 자동등록 시 O(N²) 행걸림).
|
|
7534
7581
|
// 개선: 락 1회 안에서 rows 1회 읽고, 최대 T-id 1회 계산, 전부 push, 1회 write → O(N + T).
|
|
@@ -10267,7 +10314,8 @@ function _scanCodeForPatterns(root) {
|
|
|
10267
10314
|
if (budget <= 0) return;
|
|
10268
10315
|
if (e.isDirectory()) { if (!SKIP.test(e.name) && !e.name.startsWith('.')) walk(path.join(p, e.name), depth + 1); continue; }
|
|
10269
10316
|
if (!/\.(js|ts|jsx|tsx|gd|cs|py|rb|go|rs|java|php|kt|swift)$/i.test(e.name)) continue;
|
|
10270
|
-
|
|
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 {}
|
|
10271
10319
|
}
|
|
10272
10320
|
}
|
|
10273
10321
|
walk(root, 0);
|
|
@@ -15344,13 +15392,14 @@ function mcpServeCmd(root) {
|
|
|
15344
15392
|
send({ jsonrpc: '2.0', id, result: { tools: TOOLS } });
|
|
15345
15393
|
} else if (req.method === 'tools/call') {
|
|
15346
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)
|
|
15347
15397
|
const targetPath = args.path || root;
|
|
15348
|
-
// 1.9.70: MCP tools/call 자동 사용 통계 — 어떤 도구가 자주/드물게 호출되는지 가시화
|
|
15349
|
-
try { _bumpMcpUsage(targetPath, name); } catch {}
|
|
15350
15398
|
let cliArgs;
|
|
15351
15399
|
try {
|
|
15352
15400
|
cliArgs = _mcpToCliArgs(name, args, targetPath);
|
|
15353
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 의 임의경로 쓰기 차단)
|
|
15354
15403
|
// 1.9.288 (Codex gpt-5.5 리뷰 #1 수렴): MCP 도구도 policy enforce 적용 — read-only enforce 시 write 도구 차단.
|
|
15355
15404
|
// 이전: _policyEnforce 는 agents multi --execute 한 곳뿐 → MCP state_start 등이 정책 우회하고 .leerness 기록.
|
|
15356
15405
|
// cliArgs(실제 실행 명령) 로 required tier 판정 → enforce ON 이고 초과 시 JSON-RPC error 반환(실행 안 함).
|
|
@@ -15365,7 +15414,9 @@ function mcpServeCmd(root) {
|
|
|
15365
15414
|
const r = callLeerness(cliArgs);
|
|
15366
15415
|
// 1.9.61: cursor 기반 페이지네이션 — 긴 출력은 cursor offset로 다음 청크
|
|
15367
15416
|
const fullText = r.stdout || r.stderr || '(no output)';
|
|
15368
|
-
|
|
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;
|
|
15369
15420
|
const cursor = (args._cursor && /^\d+$/.test(String(args._cursor))) ? parseInt(args._cursor, 10) : 0;
|
|
15370
15421
|
const chunk = fullText.slice(cursor, cursor + CHUNK_SIZE);
|
|
15371
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 &&
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
1011
|
-
const
|
|
1012
|
-
|
|
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
|
}
|