leerness 1.35.13 → 1.35.15

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,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.35.15 — 2026-07-07 — codex 협업 라운드 ⑤: encoding check 적대적 헌트 — 파괴적 --apply + FP/FN 5종 수정
4
+
5
+ **사용자 지시 "codex와 협업" 라운드 ⑤ — gate 임베드 `encoding check` + 변형 `env encoding-check --apply` 적대적 헌트.** 한국어-우선 사용자에게 CP949 오손은 실사용 영향이 큼. 자체 프로브 + 실제 cmd.exe 런타임 실측 + codex(gpt-5.5 xhigh, 웹검색 포함) 독립 헌터 교차.
6
+
7
+ ### 확정 수정 (자체 프로브 — 읽기전용 gate 체크)
8
+ - **FP — 순수-ASCII `.bat` 이 gate 를 거짓 실패.** `.bat missing chcp 65001` 이 내용과 무관하게 모든 `.bat` 에 발화 → `@echo off` 만 있는 ASCII 배치도 exit 1. 비-ASCII 바이트가 있을 때만 경고하도록 게이팅(CP949 오손 위험이 실제로 있을 때만).
9
+ - **FN — `.cmd` 가 gate 에서 미스캔.** `.cmd` 가 `SCAN_TEXT_EXT` 에 없어 encodingCheck/scanSecrets 가 `.cmd` 를 전혀 안 봄(반면 `env encoding-check` 는 `.cmd` 스캔 → 불일치). `.cmd` 추가 + `.bat` 와 동일 규칙(chcp 검사 · BOM 예외) 적용.
10
+
11
+ ### 확정 수정 (codex 독립 헌터 교차 — 재현 후 채택)
12
+ - **codex #4 DESTRUCTIVE — `--apply` 가 CP949 파일을 손상.** `env encoding-check --apply` 가 비-ASCII+no-BOM 파일에 transcode 없이 UTF-8 BOM 만 붙임 → CP949 본문에 BOM 을 붙이면 'UTF-8 이라 주장하는 CP949' 손상본(mojibake 악화). **재현 확정**(CP949 `.ps1` → BOM+CP949). 수정: **유효 UTF-8 본문일 때만 BOM 추가**, 비-UTF-8 은 skip(수동 transcode 안내). 추가로 `.bat/.cmd` 는 BOM 이 cmd.exe 코드페이지를 안 바꾸므로 skip(첫 줄 `chcp 65001` 안내) — 이제 `--apply` 는 유효-UTF-8 `.ps1`(PS5.1 이 BOM 으로 UTF-8 인식) 에만 작용. `.sh`/shebang 가드(1.9.409)와 동류.
13
+ - **codex #7 FN — BOM 이 roundtrip 경고를 억제.** 읽기전용 체크의 `&& !hasBOM` 가 BOM+깨진본문(예: 위 --apply 가 만든 손상본)을 통째 통과시킴. 제거 → BOM 있어도 본문이 invalid UTF-8 이면 탐지(유효 UTF-8+BOM 은 byte-exact roundtrip 이라 오탐 0, 실증).
14
+ - **codex #6 FN — NUL 을 첫 4096B 만 검사.** 전체 버퍼(≤5MB, 이미 로드됨) 스캔으로 확대 — 긴 ASCII 헤더 뒤 NUL 미탐 차단.
15
+
16
+ ### 맹신 X 반증 / 미채택
17
+ - codex #2 "BOM 이 cmd.exe 첫 명령을 깨뜨림"(구형 Windows SuperUser 인용) → **실제 Win11 cmd.exe 실측 결과 BOM 있는 .bat 정상 실행**(refuted, 플랫폼 의존). 파괴적 아님 → --apply 의 .bat/.cmd 는 BOM 미추가로 정리(무익하고 구형에선 위험할 수 있어서, 파괴 근거로는 미채택). #8(C2A1 우연 유효-UTF-8)은 CP949 디코더 없이 불가(0-deps) → by-design.
18
+
19
+ ### 검증
20
+ - selftest 273. e2e-core **20**(신규 CLI 픽스처 4: 순수-ASCII .bat clean · mojibake .cmd 탐지 · --apply CP949 무손상 · --apply 유효 .ps1 BOM). full e2e **384**. 게시본 1.35.15 재실증.
21
+
22
+ ## 1.35.14 — 2026-07-06 — codex 협업 라운드 ④: scan secrets 적대적 헌트 — 하드코딩 자격증명 FN 2종 수정
23
+
24
+ **사용자 지시 "codex와 협업" 라운드 ④ — 보안-임계 `scan secrets` 적대적 FP/FN 헌트.** gate 에 임베드된 보안 가드라 FN=시크릿 유출, FP=정직 빌드 파손. codex(gpt-5.5 xhigh)를 독립 헌터로 백그라운드 실행하고, Claude 자체 프로브 매트릭스(24 픽스처)를 로컬 스캐너에 병렬로 돌려 교차. **하드코딩 자격증명 패턴의 고빈도 FN·FP 5종 확정·수정.**
25
+
26
+ ### 확정 수정 (자체 프로브 — FN 2종 + 결합 FP)
27
+ - **FN #1 — 복합 env 키 미탐.** `Hardcoded credential (unquoted)` / `password assignment` 정규식의 선두 `\b` 가 `_` 앞(word-word 경계)에서 안 걸려 **`DB_PASSWORD=` / `REDIS_PASSWORD=` / `JWT_SECRET=` / `MY_API_KEY=` 같은 복합키(.env 관례)를 전부 미탐**. `\b` → `(?<![A-Za-z0-9])` 로 교체 — `_` 만 추가 허용하므로 기존 매칭 완전 보존 + 신규 FP 0.
28
+ - **FN #2 — JSON 따옴표-키 미탐.** quoted 패턴 `keyword\s*[:=]` 이 `"password": "x"` 처럼 **키워드와 `:` 사이의 닫는 따옴표 때문에 JSON 스타일 키를 전부 미탐**(config.json 극히 흔함). 키워드 뒤 `["']?` 추가로 흡수. 이로써 이제 매칭되는 사전 단어 값(`"secret": "required"` / `"password": "hashed"`)이 새 FP 로 노출 → **unquoted 와 동일한 `requireSecretLike` 엔트로피 게이트를 quoted 에도 적용**해 억제(literal 워드리스트 아님 — 일관·비취약. [[lesson-widening-recall-resurfaces-masked-fp]]).
29
+ - **보강 — Slack app-level 토큰.** `xox[baprs]-` 만으론 Socket Mode 의 `xapp-` 토큰 미탐 → `xapp` prefix 추가(Slack 전용이라 FP 0).
30
+
31
+ ### 확정 수정 (codex 독립 헌터 교차 — 재현 후 채택, A/B/C)
32
+ - **A — Django `SECRET_KEY = "…"` 등 미탐 (codex FN#3).** quoted 키워드 목록에 `secret_key`/`access_key`/`access_token`/`auth_token` 이 없어(unquoted 목록엔 있었음) `SECRET_KEY`/`ACCESS_TOKEN` 따옴표 대입을 미탐. quoted 목록을 unquoted 와 일관되게 확장(`secret_key` 를 `secret` 앞 longer-first). requireSecretLike + placeholder 가드 유지라 FP-safe.
33
+ - **B — 개인키 변종 미탐 (codex FN#6).** `-----BEGIN (?:RSA|OPENSSH|EC|DSA|PGP )?PRIVATE KEY-----` 이 `ENCRYPTED PRIVATE KEY` / `PGP PRIVATE KEY BLOCK` 을 미탐 → `(?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?` 로 일반화. `PUBLIC KEY`/`CERTIFICATE` 는 'PRIVATE KEY' 없어 미매치(FP 0).
34
+ - **C — 로컬-개발 DB 기본자격 FP (codex FP#1).** `postgres://postgres:postgres@localhost` / `amqp://guest:guest@`(docker-compose 극히 흔함)의 기본 비번을 실키로 오탐(빌드 파손) → placeholder 정확-일치 목록에 `postgres/postgresql/guest/mysql/mongodb/mongo/redis` 추가(`root/admin` 과 동일 성격, 정확-일치만이라 실 비번엔 FN 0).
35
+
36
+ ### 맹신 X 교차검증 / 의도적 미수정 (보안-우선 · 별도 헌트)
37
+ - 재현으로 by-design 확정(미수정): 저엔트로피 all-lowercase 무-digit 값(unquoted 정책상 스킵) · JWT/Bearer 문서 샘플 토큰 · 코드 var-ref(digit 접미) — **커밋된 샘플 토큰 플래그가 보안-우선 관점에서 정당**, 억제 시 실키 FN 위험 → conservative 유지([[lesson-adversarial-harden-heuristic]]).
38
+ - 백로그(별도 헌트 필요, FP 위험): unquoted 값의 특수문자(`DB_PASSWORD=Tr0ub4dor&3`, charset) · YAML `password: val`(`:` 무따옴표) · 추가 프로바이더 패턴(Stripe `whsec_`/Google refresh) · Firebase 공개 API 키(정책의존).
39
+
40
+ ### 검증
41
+ - selftest **273** (신규 행위검사: 복합/JSON키·Slack xapp·사전단어 FP 억제 + A/B/C — 복합키·Django·개인키변종 탐지 AND DB기본자격·PUBLIC KEY clean 을 한 케이스에 고정). e2e-core **16** (신규 CLI 픽스처 6: 복합키/JSON키/사전단어 + Django/ENCRYPTED-PK/DB기본자격). full e2e **384**. 게시본 1.35.14 재실증.
42
+
3
43
  ## 1.35.13 — 2026-07-06 — 테스트 3-tier: test:core 중간 티어(플래그십 행위 스위트) 신설 (UR-0014)
4
44
 
5
45
  **사용자 지시 "codex와 협업" 라운드 ③ — e2e 3-tier 분리.** GPT5.5 평가가 "npm test 가 300초 내 미완료"라 지적한 후속. 전체 e2e.js(384 케이스, ~12분)를 리스크 있게 쪼개는 대신, **누락된 중간 티어를 별도 파일로 신설**(릴리스 게이트 무손상).
package/README.md CHANGED
@@ -125,7 +125,7 @@ MIT
125
125
  <!-- leerness:project-readme:start -->
126
126
  ## Leerness Project Harness
127
127
 
128
- 이 프로젝트는 Leerness v1.35.13 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
128
+ 이 프로젝트는 Leerness v1.35.15 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
129
129
 
130
130
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
131
131
 
@@ -179,7 +179,7 @@ leerness memory restore decision <date|title>
179
179
 
180
180
  ### MCP server (외부 AI 통합)
181
181
 
182
- Leerness v1.35.13는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
182
+ Leerness v1.35.15는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
183
183
 
184
184
  ```jsonc
185
185
  // 카테고리별
@@ -200,7 +200,7 @@ Leerness v1.35.13는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
200
200
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
201
201
  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) 다음 라운드 예약.
202
202
 
203
- 현재 누적: **70 라운드 (1.9.40 → 1.35.13)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
203
+ 현재 누적: **70 라운드 (1.9.40 → 1.35.15)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
204
204
 
205
205
  ### 성능 가이드 (1.9.140 측정)
206
206
 
@@ -238,6 +238,6 @@ leerness release pack --close --auto-main-push
238
238
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
239
239
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
240
240
 
241
- Last synced by Leerness v1.35.13: 2026-07-06
241
+ Last synced by Leerness v1.35.15: 2026-07-07
242
242
  <!-- leerness:project-readme:end -->
243
243
 
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.13';
35
+ const VERSION = '1.35.15';
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') 시 호스트 프로세스 오염.
@@ -2590,6 +2590,17 @@ function envCmd(root, sub) {
2590
2590
  result.applied.push({ file: r.file, action: 'skipped-shebang (BOM은 shebang을 깨뜨림 — .sh는 no-BOM UTF-8 유지)' });
2591
2591
  continue;
2592
2592
  }
2593
+ // 1.35.15 (encoding 헌트, codex #2): .bat/.cmd 는 BOM 추가 금지 — cmd.exe 는 BOM 으로 코드페이지를 바꾸지 않음(첫 줄 'chcp 65001' 이 올바른 해법). BOM 은 무익하고 일부 Windows 에선 첫 명령을 깨뜨림 → 거짓 수정 대신 chcp 안내.
2594
+ if (/\.(bat|cmd)$/i.test(r.file)) {
2595
+ result.applied.push({ file: r.file, action: 'skipped-batch (cmd.exe: 첫 줄에 chcp 65001 추가가 정답 — BOM은 코드페이지를 안 바꿈)' });
2596
+ continue;
2597
+ }
2598
+ // 1.35.15 (encoding 헌트, codex #4 DESTRUCTIVE): 본문이 유효 UTF-8 일 때만 BOM 추가. CP949/Latin-1 등 비-UTF-8 본문에 BOM 만 붙이면 'UTF-8 이라 주장하는 CP949' 손상본이 됨(transcode 없이 파괴적) → skip + 수동 transcode 안내. (mojibake 를 더 악화시키던 자기-파괴 차단, .sh 가드와 동류.)
2599
+ const isValidUtf8 = Buffer.from(orig.toString('utf8'), 'utf8').equals(orig);
2600
+ if (!isValidUtf8) {
2601
+ result.applied.push({ file: r.file, action: 'skipped-nonutf8 (본문이 비-UTF-8(CP949 등) — BOM 추가는 손상. UTF-8 로 먼저 transcode 필요)' });
2602
+ continue;
2603
+ }
2593
2604
  const bom = Buffer.from([0xEF, 0xBB, 0xBF]);
2594
2605
  const fixed = Buffer.concat([bom, orig]);
2595
2606
  fs.writeFileSync(fullPath, fixed);
@@ -2951,6 +2962,39 @@ function _selfTestCases() {
2951
2962
  const realDecl = p._parseContractSpec('function realFn(){}\n').declared.includes('realFn'); // 회귀: 펜스 밖 선언 유지
2952
2963
  return bracket && dotStill && fenceExcluded && realDecl;
2953
2964
  } },
2965
+ { name: '시크릿 스캐너 FN 헌트 (1.35.14): 하드코딩 자격증명 복합/JSON키 탐지 + Slack xapp + 사전단어 FP 억제 — 행위검사', run: () => {
2966
+ const pats = require('../lib/catalogs').SECRET_PATTERNS;
2967
+ const pu = require('../lib/pure-utils');
2968
+ // collector 와 동일한 per-pattern 판정(placeholder + requireSecretLike)을 재현.
2969
+ const hit = (text) => {
2970
+ const names = new Set();
2971
+ for (const { name, re, valueGroup, requireSecretLike } of pats) {
2972
+ re.lastIndex = 0; let m;
2973
+ while ((m = re.exec(text))) {
2974
+ const val = (valueGroup != null) ? m[valueGroup] : m[0];
2975
+ if (re.lastIndex === m.index) re.lastIndex++;
2976
+ if (pu._isPlaceholderSecret(val)) continue;
2977
+ if (valueGroup != null && requireSecretLike && !pu._looksSecretLike(val)) continue;
2978
+ names.add(name);
2979
+ }
2980
+ }
2981
+ return names;
2982
+ };
2983
+ const compoundEnv = hit('DB_PASSWORD=Sup3rSecretValue99').has('Hardcoded credential (unquoted)'); // FN #1: 복합 env 키 (선두 \b→lookbehind)
2984
+ const jsonKey = hit('{ "db_password": "Xk9mP2qL7vR4nT8wZ0" }').has('Hardcoded password assignment'); // FN #2: JSON 따옴표-키 (["']? 흡수)
2985
+ const camelKey = hit('apiKey: "s3cretApiValue99"').has('Hardcoded password assignment'); // camelCase ci 키워드
2986
+ const slackApp = hit('SLACK=xapp-1-A04ABCDEFGH-1234567890123-abcdef0123').has('Slack token'); // FN: Slack app-level(xapp-)
2987
+ const dictFP = !hit('{ "secret": "required", "password": "hashed" }').has('Hardcoded password assignment'); // 사전단어 FP 억제(requireSecretLike)
2988
+ const plainStart = hit('password=plainStart1234').has('Hardcoded credential (unquoted)'); // 회귀: 단순 키 유지
2989
+ // 1.35.14 codex 헌트 A/B/C
2990
+ const djangoKey = hit('SECRET_KEY = "n7zU4lYq8mP2rC5tV0bX9aQ1"').has('Hardcoded password assignment'); // A: Django SECRET_KEY quoted
2991
+ const pkVariants = hit('-----BEGIN ENCRYPTED PRIVATE KEY-----').has('Generic private key') // B: ENCRYPTED/PGP BLOCK 변종
2992
+ && hit('-----BEGIN PGP PRIVATE KEY BLOCK-----').has('Generic private key')
2993
+ && !hit('-----BEGIN PUBLIC KEY-----').has('Generic private key'); // 안전: PUBLIC KEY 미탐
2994
+ const dbDefaultFP = !hit('postgres://postgres:postgres@localhost/app').has('DB connection string (embedded password)') // C: 로컬 기본자격 억제
2995
+ && hit('postgres://app:Xk9mP2qL7vR4nT8wZ0@db/app').has('DB connection string (embedded password)'); // 실 비번은 탐지
2996
+ return compoundEnv && jsonKey && camelKey && slackApp && dictFP && plainStart && djangoKey && pkVariants && dbDefaultFP;
2997
+ } },
2954
2998
  { name: 'honesty-check: AI 인식론적 정직성 3차원 + MCP/CLI/strict 통합 (사용자명시 1.9.305)', run: () => { const h = _epistemicHonestyCheck; const d1 = h('이 기능은 항상 정상 동작합니다').findings.some(f => f.dim === 'pretend-knowledge'); const d2 = h('아마 될 것 같습니다. 구현 완료했습니다').findings.some(f => f.dim === 'premature-judgment'); const d3 = h('이 API 의 rate limit 은 초당 5회입니다').findings.some(f => f.dim === 'no-info-gathering'); const clean = h('src/api.js 수정, 12/12 통과 (Exit: 0)').ok === true; const src = read(__filename); const wired = require('../lib/mcp-tools').some(t => t.name === 'leerness_honesty_check') && /if \(cmd === 'honesty-check'\)/.test(src) && /honestyFindings = _epistemicHonestyCheck/.test(src); return d1 && d2 && d3 && clean && wired; } },
2955
2999
  { name: 'exit code 일관성: fail()→exitCode 1 행위 + unknown 명령 안내 (UR-0045 / CV-5 행위화 1.9.366)', run: () => { if (typeof fail !== 'function') return false; const saved = process.exitCode; const _w = process.stdout.write; let setOk = false; try { process.stdout.write = () => true; process.exitCode = 0; fail('selftest probe'); setOk = process.exitCode === 1; } finally { process.stdout.write = _w; process.exitCode = saved; } const src = read(__filename); const dispatchOk = /알 수 없는 명령: \$\{cmd\}/.test(src); return setOk && dispatchOk; } },
2956
3000
  { name: 'brief: 프로젝트 청사진 set/show/export + README 개요 섹션 (UR-0055 사용자명시 1.9.307)', run: () => { const src = read(__filename); const fnOk = typeof briefCmd === 'function' && typeof _loadBrief === 'function' && typeof _briefBlueprint === 'function' && _BRIEF_FIELDS.length === 10; const b = { project: 'X', intro: 'i', purpose: 'p', problem: '', features: ['f1', 'f2'], stack: ['s'], architecture: '', users: [], success: [], nonGoals: [], currentState: '' }; const bp = _briefBlueprint(b, VERSION); const bpOk = /Blueprint/.test(bp) && /소개 \(Intro\)/.test(bp) && /f1/.test(bp) && /신규 프로젝트 시작 가이드/.test(bp); const rb = _briefReadmeBlock(b); const rbOk = rb.includes(BRIEF_START) && rb.includes(BRIEF_END) && /프로젝트 개요/.test(rb) && /\*\*목적\*\*/.test(rb); return fnOk && bpOk && rbOk && /if \(cmd === 'brief'\)/.test(src); } },
@@ -8199,7 +8243,8 @@ function isSkippedRel(rel, extras = []) {
8199
8243
  if (segs.some(s => SCAN_SKIP_DIRS.has(s))) return true; // SCAN_SKIP_DIRS 는 Set
8200
8244
  return extras.some(d => rel === d || rel.startsWith(d + '/'));
8201
8245
  }
8202
- const SCAN_TEXT_EXT = new Set(['.js','.ts','.jsx','.tsx','.mjs','.cjs','.json','.json5','.jsonc','.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','']);
8246
+ // 1.35.15 (encoding 헌트 FN): '.cmd' 추가 — .bat 와 기능 동일한 Windows 배치인데 누락되어 encodingCheck/scanSecrets 가 .cmd 를 전혀 스캔 안 하던 미탐(env encoding-check 는 .cmd 를 스캔했어 불일치).
8247
+ const SCAN_TEXT_EXT = new Set(['.js','.ts','.jsx','.tsx','.mjs','.cjs','.json','.json5','.jsonc','.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','.cmd','.ps1','']);
8203
8248
  function* walk(root, base = root, depth = 0, extras = null) {
8204
8249
  if (depth > 12) return;
8205
8250
  if (extras === null) extras = getExtraSkipDirs(root);
@@ -8306,20 +8351,25 @@ function encodingCheck(root, opts = {}) {
8306
8351
  if (buf.length === 0) continue;
8307
8352
  if (buf.length > 5 * 1024 * 1024) continue;
8308
8353
  const fileRel = rel(root, file);
8309
- if (buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF) { if (ext !== '.ps1' && ext !== '.bat') { warnings++; findings.push({ file: fileRel, issue: 'UTF-8 BOM' }); } } // 1.9.353 (UR-0067 외부리뷰): .ps1/.bat 는 PS5.1 호환 위해 BOM 의도적일 수 있어 예외 (env encoding 정책과 모순 해소)
8354
+ if (buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF) { if (ext !== '.ps1' && ext !== '.bat' && ext !== '.cmd') { warnings++; findings.push({ file: fileRel, issue: 'UTF-8 BOM' }); } } // 1.9.353 (UR-0067 외부리뷰): .ps1/.bat/.cmd 는 PS5.1 호환/배치 BOM 의도적일 수 있어 예외 (1.35.15: .cmd 추가 — .bat 와 동일 취급, env encoding 정책과 정합)
8310
8355
  else if ((buf[0] === 0xFF && buf[1] === 0xFE) || (buf[0] === 0xFE && buf[1] === 0xFF)) { warnings++; findings.push({ file: fileRel, issue: 'UTF-16 BOM' }); }
8311
- let nul = false; for (let i = 0; i < Math.min(buf.length, 4096); i++) if (buf[i] === 0) { nul = true; break; }
8312
- if (nul) { warnings++; findings.push({ file: fileRel, issue: 'NUL byte (binary in text path)' }); }
8313
- if (ext === '.bat') {
8314
- const text = buf.toString('utf8').replace(/^/, '');
8315
- if (!/^@?chcp\s+65001/i.test(text.split(/\r?\n/, 1)[0] || '')) { warnings++; findings.push({ file: fileRel, issue: '.bat missing chcp 65001' }); }
8356
+ // 1.35.15 (encoding 헌트 FN #6): NUL 4096B 보던 것을 전체 버퍼(≤5MB, 이미 메모리 로드됨) 확대 ASCII 헤더 NUL 숨은 이진 파일 미탐 차단.
8357
+ if (buf.includes(0)) { warnings++; findings.push({ file: fileRel, issue: 'NUL byte (binary in text path)' }); }
8358
+ if (ext === '.bat' || ext === '.cmd') {
8359
+ // 1.35.15 (encoding 헌트 FP): 순수-ASCII 배치는 CP949 오손 위험이 없어 chcp 불필요 → 비-ASCII 바이트가 있을 때만 경고(이전엔 @echo off 만 있는 ASCII .bat 도 무조건 발화해 gate 를 거짓 실패시킴). + .cmd 도 .bat 와 동일 검사.
8360
+ let hasNonAscii = false;
8361
+ for (let i = 0; i < Math.min(buf.length, 4096); i++) if (buf[i] >= 0x80) { hasNonAscii = true; break; }
8362
+ if (hasNonAscii) {
8363
+ const text = buf.toString('utf8').replace(/^/, '');
8364
+ if (!/^@?chcp\s+65001/i.test(text.split(/\r?\n/, 1)[0] || '')) { warnings++; findings.push({ file: fileRel, issue: `${ext} missing chcp 65001` }); }
8365
+ }
8316
8366
  }
8317
8367
  // 1.9.353 (UR-0067 외부리뷰): 가-힣 게이트 제거 — CP949/Latin-1 등 invalid UTF-8 은 한글 포함 여부와 무관하게 round-trip 불일치로 감지(이전: 한글 없으면 통과 = false negative). 유효 UTF-8 은 byte-exact round-trip 이라 오탐 없음.
8368
+ // 1.35.15 (encoding 헌트 FN #7): '&& !hasBOM' 제거 — BOM 이 있어도 본문이 깨진(CP949) 파일은 round-trip 불일치라 탐지해야 함. 기존 가드는 BOM+CP949 본문(예: --apply 가 CP949 파일에 BOM 만 붙여 만든 손상본)을 통째 통과시켰음. 유효 UTF-8(+BOM 포함)은 byte-exact round-trip 이라 오탐 0(실증).
8318
8369
  try {
8319
8370
  const text = buf.toString('utf8');
8320
8371
  const reBuf = Buffer.from(text, 'utf8');
8321
- const hasBOM = buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF;
8322
- if (!reBuf.equals(buf) && !hasBOM) {
8372
+ if (!reBuf.equals(buf)) {
8323
8373
  warnings++; findings.push({ file: fileRel, issue: 'invalid UTF-8 roundtrip (CP949/mojibake 의심)' });
8324
8374
  }
8325
8375
  } catch {}
package/lib/catalogs.js CHANGED
@@ -376,11 +376,18 @@ const SECRET_PATTERNS = [
376
376
  { name: 'npm token', re: /\bnpm_[A-Za-z0-9]{36}\b/g },
377
377
  { name: 'Google API key', re: /\bAIza[0-9A-Za-z_\-]{35}\b/g },
378
378
  { name: 'Google OAuth token', re: /\bya29\.[A-Za-z0-9_-]{20,}/g },
379
- { name: 'Slack token', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
380
- { name: 'Generic private key', re: /-----BEGIN (?:RSA |OPENSSH |EC |DSA |PGP )?PRIVATE KEY-----/g },
381
- { name: 'Hardcoded password assignment', re: /\b(?:password|passwd|pwd|secret|api_key|apikey)\s*[:=]\s*["']([^"'\s]{6,})["']/gi, valueGroup: 1 }, // 1.9.365 CV-6: valueGroupplaceholder 오탐 억제
379
+ // 1.35.14 (시크릿 헌트): Slack app-level 토큰(xapp-, Socket Mode) 보강 — 기존 xox[baprs]- 만으론 미탐(FN). xapp- 는 Slack 전용 prefix라 FP 0.
380
+ { name: 'Slack token', re: /\b(?:xox[baprs]|xapp)-[A-Za-z0-9-]{10,}\b/g },
381
+ // 1.35.14 (codex 헌트 FN #6): ENCRYPTED PRIVATE KEY / PGP PRIVATE KEY BLOCK 변종 미탐 일반화. PUBLIC KEY/CERTIFICATE 는 'PRIVATE KEY' 없어 미매치(FP 0).
382
+ { name: 'Generic private key', re: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g },
383
+ // 1.35.14 (시크릿 헌트 FN #2): 선두 \b → (?<![A-Za-z0-9]) 로 교체해 복합키(DB_PASSWORD/JWT_SECRET, 키워드 앞 '_')를 포함 — \b 는 '_password' 처럼 word-word 경계라 미탐이었음.
384
+ // + 키워드 뒤 ["']? 추가 → JSON 따옴표-키("password": "x") 흡수 — 닫는 따옴표가 keyword 와 : 사이에 끼어 기존엔 전부 미탐(config.json 극히 흔함). lookbehind 는 '_' 만 추가 허용 → 신규 FP 0.
385
+ // + requireSecretLike 신규 적용: JSON 키를 이제 맞추므로 사전 단어 값("secret":"required"/"password":"hashed"/"api_key":"provided")이 노출 → unquoted 와 동일 엔트로피 게이트로 억제(literal 워드리스트 아님, 일관). 실키/디짓포함/len24+ 만 통과.
386
+ // + 1.35.14 (codex 헌트 FN #3): quoted 키워드에 secret_key/access_key/access_token/auth_token 추가(unquoted 목록과 일관) → Django SECRET_KEY="…" 등 미탐 해소. secret_key 를 secret 앞에 배치(longer-first).
387
+ { name: 'Hardcoded password assignment', re: /(?<![A-Za-z0-9])(?:password|passwd|pwd|secret_key|secret|api_key|apikey|access_key|access_token|auth_token)["']?\s*[:=]\s*["']([^"'\s]{6,})["']/gi, valueGroup: 1, requireSecretLike: true }, // 1.9.365 CV-6: valueGroup → placeholder 오탐 억제
382
388
  // 1.9.365 (외부리뷰 CV-6/UR-0081): unquoted 자격증명 (KEY=secret123) FN 보강. 값이 시크릿스러운지(_looksSecretLike) + placeholder 아닌지 후처리. 코드 var-ref(점 포함)는 charset 으로 제외.
383
- { name: 'Hardcoded credential (unquoted)', re: /\b(?:password|passwd|pwd|secret|secret_key|api_key|apikey|access_key|access_token|auth_token)\s*=\s*([A-Za-z0-9_\-/+]{8,})(?=[\s;,)]|$)/gi, valueGroup: 1, requireSecretLike: true },
389
+ // 1.35.14 (시크릿 헌트 FN #1): 선두 \b(?<![A-Za-z0-9]) — DB_PASSWORD=/REDIS_PASSWORD=/JWT_SECRET= 같은 복합키(.env 관례) 커버. 기존 \b '_' 앞에서 안 걸려 미탐.
390
+ { name: 'Hardcoded credential (unquoted)', re: /(?<![A-Za-z0-9])(?:password|passwd|pwd|secret|secret_key|api_key|apikey|access_key|access_token|auth_token)\s*=\s*([A-Za-z0-9_\-/+]{8,})(?=[\s;,)]|$)/gi, valueGroup: 1, requireSecretLike: true },
384
391
  // 1.9.350 (UR-0060 외부리뷰 3모델): 누락 패턴 보강
385
392
  { name: 'GitLab PAT', re: /\bglpat-[A-Za-z0-9_-]{20,}\b/g },
386
393
  { name: 'JWT', re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
package/lib/pure-utils.js CHANGED
@@ -646,7 +646,8 @@ function _isPlaceholderSecret(value) {
646
646
  if (!v) return true;
647
647
  // 전체가 placeholder 토큰
648
648
  // 1.26.1 (13번째 외부리뷰 P2): DB URI 등 placeholder 자격증명(user:password / root:root / yourpassword …) 추가 — 전체-값 정확 일치만 매칭하므로 실키(길고 고엔트로피)에는 FN 영향 0.
649
- if (/^(?:x{3,}|\*{3,}|\.{3,}|-+|0+|1234567890?|12345678|abc123|secret|password|passwd|pass|changeme|change[-_]me|changeit|replace[-_]?me|placeholder|example|examples?|sample|dummy|test|testing|foo|bar|baz|tbd|todo|none|null|undefined|nil|empty|redacted|hidden|value|string|here|root|admin|user|username|yourpassword|your[-_]?password|mypassword)$/.test(v)) return true;
649
+ // 1.35.14 (codex 헌트 FP #1): postgres/guest/mysql/mongodb/redis 등 로컬-개발 기본 자격증명 추가 — docker-compose 의 postgres://postgres:postgres@ / amqp://guest:guest@ 오탐(빌드 파손) 차단. root/admin 과 동일 성격(정확-일치만이라 실키 FN 0).
650
+ if (/^(?:x{3,}|\*{3,}|\.{3,}|-+|0+|1234567890?|12345678|abc123|secret|password|passwd|pass|changeme|change[-_]me|changeit|replace[-_]?me|placeholder|example|examples?|sample|dummy|test|testing|foo|bar|baz|tbd|todo|none|null|undefined|nil|empty|redacted|hidden|value|string|here|root|admin|user|username|yourpassword|your[-_]?password|mypassword|postgres|postgresql|guest|mysql|mongodb|mongo|redis)$/.test(v)) return true;
650
651
  // 1.9.405 (8번째 버그헌트 회귀수정, UR-0109): placeholder 단어 신호를 entropy 가드보다 먼저 검사.
651
652
  // 1.9.401 회귀: 긴 서술형 placeholder('your-super-secret-api-key-example-value')가 고엔트로피(영숫자24+ & 고유12+)를 넘어 실키로 오탐(FP).
652
653
  // → placeholder 마커 단어가 있으면 entropy 가드 무시하고 placeholder 로 판정. 실키 prefix(sk-/AKIA 등)는 마커보다 우선(FN 방지).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.35.13",
3
+ "version": "1.35.15",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
@@ -68,12 +68,66 @@ console.log('# leerness core (test:core) — flagship behavioral guarantees');
68
68
  fs.rmSync(d, { recursive: true, force: true });
69
69
  }
70
70
 
71
- // (5) scan secrets: 커밋 시크릿 차단 / 클린 통과
71
+ // (5) scan secrets: 커밋 시크릿 차단 / 클린 통과 + 하드코딩 자격증명 복합/JSON키 FN (1.35.14)
72
72
  {
73
73
  const d = fresh();
74
74
  assert('scan secrets clean → exit 0', run(d, ['scan', 'secrets', '.']).status === 0);
75
75
  fs.writeFileSync(path.join(d, 'k.js'), 'const k = "AKIAJQXMP7RZ2KL9WXYZ";\n');
76
76
  assert('scan secrets committed key → exit 1', run(d, ['scan', 'secrets', '.']).status === 1);
77
+ fs.rmSync(path.join(d, 'k.js'), { force: true });
78
+ // 1.35.14 FN #1: 복합 env 키(DB_PASSWORD=)를 탐지해야 함 (기존 선두 \b 는 '_' 앞에서 미탐)
79
+ fs.writeFileSync(path.join(d, 'app.env'), 'DB_PASSWORD=Sup3rSecretValue99\n');
80
+ assert('scan secrets catches compound env key (DB_PASSWORD=) → exit 1', run(d, ['scan', 'secrets', '.']).status === 1);
81
+ fs.rmSync(path.join(d, 'app.env'), { force: true });
82
+ // 1.35.14 FN #2: JSON 따옴표-키("db_password": "…")를 탐지해야 함
83
+ fs.writeFileSync(path.join(d, 'cfg.json'), '{ "db_password": "Xk9mP2qL7vR4nT8wZ0" }\n');
84
+ assert('scan secrets catches JSON quoted key → exit 1', run(d, ['scan', 'secrets', '.']).status === 1);
85
+ fs.rmSync(path.join(d, 'cfg.json'), { force: true });
86
+ // 1.35.14 FP 안전: 사전 단어 값("secret":"required")은 requireSecretLike 로 억제 → clean
87
+ fs.writeFileSync(path.join(d, 'schema.json'), '{ "secret": "required", "password": "hashed" }\n');
88
+ assert('scan secrets keeps dictionary-word schema clean → exit 0', run(d, ['scan', 'secrets', '.']).status === 0);
89
+ fs.rmSync(path.join(d, 'schema.json'), { force: true });
90
+ // 1.35.14 A: Django SECRET_KEY = "…" 탐지 (quoted 키워드 확장)
91
+ fs.writeFileSync(path.join(d, 'settings.py'), 'SECRET_KEY = "n7zU4lYq8mP2rC5tV0bX9aQ1"\n');
92
+ assert('scan secrets catches Django SECRET_KEY → exit 1', run(d, ['scan', 'secrets', '.']).status === 1);
93
+ fs.rmSync(path.join(d, 'settings.py'), { force: true });
94
+ // 1.35.14 B: ENCRYPTED PRIVATE KEY 변종 탐지
95
+ fs.writeFileSync(path.join(d, 'id.key'), '-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIF\n-----END ENCRYPTED PRIVATE KEY-----\n');
96
+ assert('scan secrets catches ENCRYPTED PRIVATE KEY → exit 1', run(d, ['scan', 'secrets', '.']).status === 1);
97
+ fs.rmSync(path.join(d, 'id.key'), { force: true });
98
+ // 1.35.14 C: 로컬-개발 DB 기본자격(postgres:postgres) 은 clean (docker-compose FP 차단)
99
+ fs.writeFileSync(path.join(d, 'compose.yml'), 'DATABASE_URL: postgres://postgres:postgres@localhost/app\n');
100
+ assert('scan secrets keeps local DB defaults clean → exit 0', run(d, ['scan', 'secrets', '.']).status === 0);
101
+ fs.rmSync(d, { recursive: true, force: true });
102
+ }
103
+
104
+ // (6) encoding check: 순수-ASCII .bat clean(FP 차단) / Korean mojibake .cmd 탐지(FN, .cmd 스캔) (1.35.15)
105
+ {
106
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-enc-'));
107
+ // 순수-ASCII .bat(@echo off, chcp 없음)은 CP949 오손 위험이 없어 clean 이어야 함(이전엔 무조건 실패시키던 FP)
108
+ fs.writeFileSync(path.join(d, 'ascii.bat'), '@echo off\r\necho hello world\r\n', 'latin1');
109
+ assert('encoding: pure-ASCII .bat clean (no false chcp warning) → exit 0', cp.spawnSync(process.execPath, [CLI, 'encoding', 'check', d], { encoding: 'utf8', timeout: 40000 }).status === 0);
110
+ // Korean mojibake(CP949 0xC7 0xD1) .cmd 는 이제 스캔되어 탐지되어야 함(이전엔 .cmd 미스캔 FN)
111
+ fs.writeFileSync(path.join(d, 'korean.cmd'), Buffer.from([0x40, 0x65, 0x63, 0x68, 0x6f, 0x20, 0xC7, 0xD1, 0x0d, 0x0a]));
112
+ assert('encoding: Korean mojibake .cmd flagged (.cmd now scanned) → exit 1', cp.spawnSync(process.execPath, [CLI, 'encoding', 'check', d], { encoding: 'utf8', timeout: 40000 }).status === 1);
113
+ fs.rmSync(d, { recursive: true, force: true });
114
+ }
115
+
116
+ // (7) env encoding-check --apply: CP949 .ps1 을 손상시키지 않고(BOM만 붙이는 파괴 차단) 유효 UTF-8 .ps1 만 BOM 추가 (1.35.15 codex #4 DESTRUCTIVE)
117
+ {
118
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-apply-'));
119
+ // CP949 본문(Write-Host "안녕", 안=BE C8 녕=B3 E7) — --apply 가 BOM 만 붙이면 손상되므로 skip 되어 파일이 그대로여야 함
120
+ const cp949 = Buffer.concat([Buffer.from('Write-Host "', 'latin1'), Buffer.from([0xBE, 0xC8, 0xB3, 0xE7]), Buffer.from('"\r\n', 'latin1')]);
121
+ fs.writeFileSync(path.join(d, 'cp949.ps1'), cp949);
122
+ cp.spawnSync(process.execPath, [CLI, 'env', 'encoding-check', '--apply', '--path', d], { encoding: 'utf8', timeout: 40000 });
123
+ const after = fs.readFileSync(path.join(d, 'cp949.ps1'));
124
+ assert('encoding --apply: CP949 .ps1 NOT mutated (no destructive BOM-on-CP949)', after.equals(cp949) && !(after[0] === 0xEF && after[1] === 0xBB && after[2] === 0xBF));
125
+ // 유효 UTF-8 .ps1(한글, no BOM)은 정당하게 BOM 추가되어야 함
126
+ fs.writeFileSync(path.join(d, 'utf8.ps1'), Buffer.from('Write-Host "안녕"\r\n', 'utf8'));
127
+ cp.spawnSync(process.execPath, [CLI, 'env', 'encoding-check', '--apply', '--path', d], { encoding: 'utf8', timeout: 40000 });
128
+ const u = fs.readFileSync(path.join(d, 'utf8.ps1'));
129
+ const uValid = Buffer.from(u.toString('utf8'), 'utf8').equals(u);
130
+ assert('encoding --apply: valid-UTF-8 .ps1 gets BOM (still valid)', u[0] === 0xEF && u[1] === 0xBB && u[2] === 0xBF && uValid);
77
131
  fs.rmSync(d, { recursive: true, force: true });
78
132
  }
79
133