leerness 1.19.0 → 1.20.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,62 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.20.0 — 2026-06-15 — 🛡️ [안정화/Stable] 품질 렌즈 완전판 + 신뢰 투명성 안정 minor
4
+
5
+ **🛡️ 안정화(Stable) minor — "AI가 스스로 질문하고 행동하도록" 완성.** 직전 minor(1.19.0) 이후 누적된 패치 3건(1.19.1~1.19.3)을 검증·통합해 npm 공개. R-0011 정책의 11번째 stable minor. 핵심 테마: **사용자 자기질문 품질 렌즈를 정적 명령에서 완전한 워크플로 기능으로** + 신뢰 투명성(클린룸 평가 공개).
6
+
7
+ ### 이번 minor 통합 (1.19.1~1.19.3)
8
+ - **🧭 품질 렌즈 완전판 (UR-0003, 사용자 핵심 철학)**: 1.18.3 의 정적 `lens` 명령을 3단계로 완성 —
9
+ - **v2 (1.19.2)**: `verify-claim` 이 완료를 검증하는 바로 그 순간, 주장된 파일 확장자로 관련 렌즈 질문을 advisory 로 노출(결정적 매핑, 게이트 아님). 코드: "선임 개발자가 복잡하다 느끼지 않을까?" / 디자인: "이쁘고 직관적인가?".
10
+ - **v3 (1.19.3)**: 프로젝트별 커스텀 렌즈 `.harness/quality-lenses.json` — 게임은 "60fps 끊김?", 웹은 접근성을 자기 기준에 추가. 내장+커스텀 불변 병합(읽기만, 쓰기 명령 없음).
11
+ - lens 도메인 대소문자/공백 정규화(1.19.1).
12
+ - **🔬 신뢰 투명성 (GPT-5.5 #5, UR-0009, 1.19.1)**: `docs/clean-room-evaluations.md` 공개 — README "독립 클린룸 평가" 주장을 확인 가능하게(방법론·4개 평가·**정직한 한계**). 과대였던 "5개" 수치를 검증 가능한 서술로 교체.
13
+ - **🛡️ 19th 버그헌트 (1.19.1)**: 1.18.x 신규 표면(권한 술어·빈껍데기 정규식·lens·audit) 엣지 14종 + 크래시 벡터 — 견고 확인(버그 0, lens 정규화만 수정).
14
+
15
+ ### 검증 (회귀 0)
16
+ - **selftest 231→235** · **E2E 365/365** · 렌즈 v2/v3 행위 재현 + 클린룸 문서 + 게시본 재실증.
17
+
18
+ ### 안정화 표시 (R-0006)
19
+ CHANGELOG [안정화/Stable] · git tag (Stable) · GitHub release (`--latest`) · npm dist-tag `stable` 시도.
20
+
21
+ ## 1.19.3 — 2026-06-14 — 품질 렌즈 완전판 v3: 프로젝트별 커스텀 렌즈 (UR-0003)
22
+
23
+ **🧭 렌즈를 "프로젝트 인지"로.** 내장 5종(code/design/docs/test/security)에 더해, 프로젝트가 자기 품질 기준을 추가할 수 있습니다 — 게임 프로젝트는 "60fps에서 끊기지 않나?", 웹 프로젝트는 접근성("키보드만으로 모든 기능 가능?")을 자기질문에 넣습니다. 사용자의 "각 분야 고려" 철학을 프로젝트 맥락까지 확장.
24
+
25
+ ### 변경 (UR-0003 렌즈 완전판 v3)
26
+ - **프로젝트 커스텀 렌즈**: `.harness/quality-lenses.json` 을 `lens` 와 verify-claim advisory 가 내장 catalog 와 **병합**. 포맷 `{ "domains": { "code": { "questions": ["..."] }, "a11y": { "title":"접근성", "persona":"...", "questions":[...] } } }`. 기존 도메인 → 질문 추가(dedup, 최대 8), 신규 도메인 → 추가. 표시에 `[프로젝트]`/`[+프로젝트 질문]` 라벨.
27
+ - **읽기-병합만, 쓰기 명령 없음 (R-0006 보수)**: leerness 는 JSON 을 읽어 병합·표시만 함(쓰기 경로 위험 0). JSON 은 AI/사용자가 편집 — 커스텀 없으면 편집 방법을 `lens` 가 안내. 순수 함수 `_mergeLensCatalog`(불변, 내장 catalog 미오염) + `_loadProjectLenses`.
28
+ - 잔여(UR-0003 완전판): handoff 힌트, MCP 노출, 인과관계 자동 추론 — 백로그.
29
+
30
+ ### 검증 (회귀 0)
31
+ - **selftest 234→235** (병합 행위: 기존 도메인 질문 추가·신규 도메인·잘못된 입력 무시·내장 불변성) · 프로젝트 커스텀 렌즈 행위 재현(게임 fps + a11y 도메인 병합 표시) · **E2E 365/365**.
32
+ - patch(1.19.3) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
33
+
34
+ ## 1.19.2 — 2026-06-14 — 품질 렌즈 완전판 v2: verify-claim 완료-검증 순간에 분야별 자기질문 (UR-0003)
35
+
36
+ **🧭 렌즈를 "수동 참조 명령"에서 "완료 검증 흐름의 일부"로.** 1.18.3 의 `leerness lens` 는 AI 가 직접 호출해야만 보였습니다. 이제 `verify-claim` 이 **완료를 검증하는 바로 그 순간**(사용자 표현: "완료 선언 전 자기질문") 주장된 파일의 종류에 맞는 렌즈 질문을 advisory 로 노출 — AI 의 자기질문이 수동 습관이 아니라 워크플로의 일부가 됩니다.
37
+
38
+ ### 변경 (UR-0003 렌즈 완전판 v2)
39
+ - **verify-claim 렌즈 advisory**: done 주장 검증 시, 주장된 파일 확장자로 관련 렌즈 도메인을 **결정적으로** 판정(`_lensDomainsForFiles` — 퍼지 키워드 추론 아님)해 해당 분야 질문을 노출. 예: `pay.js + app.css` → `code`(선임 개발자가 복잡하다 느끼지 않을까?) + `design`(이쁘고 직관적인가?). 매핑: UI/스타일(css/scss/tsx/jsx/vue/html)→design, 문서(md/mdx/rst)→docs, 테스트파일→test, 실코드(js/ts/py/…)→code. 최대 2개(클러터 방지), 고정 순서(code 먼저).
40
+ - **advisory 전용** — 게이트가 아님(exit code 불변). 기계검증(파일/테스트/스텁)을 통과해도 "사람이 보기에 좋은가"는 별개라는 점을 완료 순간에 상기. `--json` 경로엔 영향 없음(human 출력 한정).
41
+ - 잔여(UR-0003 완전판): 프로젝트별 커스텀 렌즈(`.harness/quality-lenses.json`), handoff 힌트, MCP 노출, 인과관계 자동 추론 — 백로그.
42
+
43
+ ### 검증 (회귀 0)
44
+ - **selftest 233→234** (`_lensDomainsForFiles` 행위: 단일/혼합/문서/테스트/빈/최대2 매핑) · verify-claim advisory 행위 재현(혼합 파일 → code+design) · **E2E 365/365**.
45
+ - patch(1.19.2) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
46
+
47
+ ## 1.19.1 — 2026-06-14 — 19th 버그헌트(신규 표면 견고 확인) + 클린룸 평가 공개 (GPT-5.5 #5)
48
+
49
+ **🔬 갓 배포한 1.19.0의 신규 표면을 적대적으로 헌트 — 표면은 견고, 사소 1건만 수정 + 신뢰증거 공개.** 1.18.x 추가분(권한 술어·빈껍데기 정규식·lens·audit)을 엣지케이스로 직접 탐침: 순수 술어 14종 + CLI 크래시/주입 벡터 — **크래시·스택트레이스·오판 0**. 유일 발견은 `lens` 도메인 대소문자 미정규화.
50
+
51
+ ### 변경
52
+ - **🧭 lens 도메인 정규화 (19th 헌트)**: `leerness lens Code` / `lens CODE ` 도 인식(trim + toLowerCase). 이전엔 정확한 소문자만 허용해 사소한 오류.
53
+ - **🔬 클린룸 평가 공개 (GPT-5.5 #5, UR-0009)**: `docs/clean-room-evaluations.md` 신설 — README 의 "독립 클린룸 평가로 검증" 주장을 **확인 가능하게**. 방법론(게시본 npm 설치·행위 검증·적대 시도), 수행한 4개 평가(5축 현장조사·1.18.0/1.19.0 게시본 재실증·1.18.2 적대 스텁 워크플로), 그리고 **정직한 한계**(휴리스틱이지 의미검증 아님 / 커버리지·mutation·제3자 재현은 미해결)를 명시. README(영/한)에서 링크. 과대홍보였던 "5개" 수치를 검증 가능한 서술로 교체.
54
+ - 잔여(UR-0009): 코드 커버리지/mutation testing 은 spawn 기반 e2e 특성상 별도 작업으로 백로그.
55
+
56
+ ### 검증 (회귀 0)
57
+ - **selftest 231→233** (lens 정규화 + 클린룸 문서 행위 가드) · 순수 술어 14종 엣지 통과 · 크래시 벡터 0 · **E2E 365/365**.
58
+ - patch(1.19.1) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
59
+
3
60
  ## 1.19.0 — 2026-06-14 — 🛡️ [안정화/Stable] 검증 하네스 강화 + 정직한 프레이밍 안정 minor
4
61
 
5
62
  **🛡️ 안정화(Stable) minor — "거짓 완료를 못 하게" 만드는 검증 계층을 한층 더 단단하게.** 직전 minor(1.18.0) 이후 누적된 패치 4건(1.18.1~1.18.4)을 검증·통합해 npm 공개. R-0011 정책의 10번째 stable minor. 핵심 테마: **검증 계층 강화(비-JS 거짓FAIL 해소 + 위장 스텁 차단) + AI 자기질문 품질 렌즈 + 정직한 프레이밍(협조 vs 강제, 영문 우선)**.
package/README.ko.md CHANGED
@@ -11,7 +11,7 @@
11
11
  ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝
12
12
  ```
13
13
 
14
- > **어떤 언어, 어떤 AI 에이전트로 작업하든 — "증거 없이는 끝났다고 말할 수 없게" 만드는 AI 코딩 운영 레이어.** 코드를 대신 쓰는 도구가 아니라, AI 에이전트의 **기억·인수인계·검증·감사·보안 가드**를 프로젝트에 영속화하는 CLI + MCP 서버입니다. (이 포지셔닝은 5개 독립 클린룸 실사용 평가 — Python/Node/Rust 실개발·에이전트 교대·적대 공격 — 로 검증됐습니다.)
14
+ > **어떤 언어, 어떤 AI 에이전트로 작업하든 — "증거 없이는 끝났다고 말할 수 없게" 만드는 AI 코딩 운영 레이어.** 코드를 대신 쓰는 도구가 아니라, AI 에이전트의 **기억·인수인계·검증·감사·보안 가드**를 프로젝트에 영속화하는 CLI + MCP 서버입니다. (이 포지셔닝은 독립 클린룸 평가 — Python/Node/Rust 실개발·에이전트 교대·검증기 적대 공격 — 로 확인됐습니다. 방법론·결과·정직한 한계: [docs/clean-room-evaluations.md](./docs/clean-room-evaluations.md))
15
15
 
16
16
  [![npm](https://img.shields.io/npm/v/leerness)](https://www.npmjs.com/package/leerness) · ![MCP tools](https://img.shields.io/badge/MCP--tools-85-blue) · **런타임 의존성 0** · **install-script 0** · offline-first · Node ≥ 18 · MIT
17
17
 
package/README.md CHANGED
@@ -63,7 +63,7 @@ Built-in harnesses remember what the AI **said**. leerness verifies what the AI
63
63
  | Secrets · encoding · drift guards | none | `scan secrets` · `encoding check` · `drift check --auto-fix` — CI-ready |
64
64
  | Lock-in | one vendor | any agent, any language, 0 runtime dependencies |
65
65
 
66
- This positioning was verified by **5 independent clean-room evaluations** — real Python/Node/Rust development, agent swaps, and adversarial attacks against the verifier itself (fake tests, comment-only stubs, inflated test counts — all rejected).
66
+ This positioning was verified by **independent clean-room evaluations** — fresh `npm install` into temp dirs, driven by behavior only, including adversarial attacks against the verifier itself (fake tests, comment-only stubs, inflated test counts — all rejected). Methodology, results, and honest limitations: **[docs/clean-room-evaluations.md](./docs/clean-room-evaluations.md)**.
67
67
 
68
68
  ---
69
69
 
@@ -102,7 +102,7 @@ MIT
102
102
  <!-- leerness:project-readme:start -->
103
103
  ## Leerness Project Harness
104
104
 
105
- 이 프로젝트는 Leerness v1.19.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
105
+ 이 프로젝트는 Leerness v1.20.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
106
106
 
107
107
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
108
108
 
@@ -156,7 +156,7 @@ leerness memory restore decision <date|title>
156
156
 
157
157
  ### MCP server (외부 AI 통합)
158
158
 
159
- Leerness v1.19.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
159
+ Leerness v1.20.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
160
160
 
161
161
  ```jsonc
162
162
  // 카테고리별
@@ -177,7 +177,7 @@ Leerness v1.19.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code
177
177
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
178
178
  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) 다음 라운드 예약.
179
179
 
180
- 현재 누적: **70 라운드 (1.9.40 → 1.19.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
180
+ 현재 누적: **70 라운드 (1.9.40 → 1.20.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
181
181
 
182
182
  ### 성능 가이드 (1.9.140 측정)
183
183
 
@@ -215,6 +215,6 @@ leerness release pack --close --auto-main-push
215
215
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
216
216
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
217
217
 
218
- Last synced by Leerness v1.19.0: 2026-06-14
218
+ Last synced by Leerness v1.20.0: 2026-06-15
219
219
  <!-- leerness:project-readme:end -->
220
220
 
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.19.0';
35
+ const VERSION = '1.20.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') 시 호스트 프로세스 오염.
@@ -3657,6 +3657,40 @@ function _selfTestCases() {
3657
3657
  const a = read(path.join(path.dirname(__filename), '..', 'lib', 'audit.js'));
3658
3658
  return a.includes('Last synced by Leerness v') && a.includes('readme_synced_version_stale');
3659
3659
  } },
3660
+ { name: '19th 헌트 (1.19.1): lens 도메인 대소문자/공백 정규화 (소스 가드)', run: () => {
3661
+ const src = read(__filename);
3662
+ return src.includes("if (domain != null) domain = String(domain).trim().toLowerCase();");
3663
+ } },
3664
+ { name: '렌즈 완전판 v3 (1.19.3, UR-0003): 프로젝트 커스텀 렌즈 병합 (행위)', run: () => {
3665
+ const M = _mergeLensCatalog;
3666
+ const a = M({ code: { title: '코드', persona: 'p', questions: ['Q1'], affects: [] } }, { code: { questions: ['Q1', 'Q2'] } });
3667
+ const appendOk = a.code.questions.length === 2 && a.code.questions[1] === 'Q2' && a.code._customAdded === true;
3668
+ const b = M({ code: { title: '코드', persona: 'p', questions: ['Q'], affects: [] } }, { a11y: { title: '접근성', persona: '스크린리더', questions: ['키보드로 가능?'] } });
3669
+ const newOk = b.a11y && b.a11y.title === '접근성' && b.a11y._custom === true && b.code.questions.length === 1;
3670
+ const c = M({ code: { questions: ['Q'], affects: [] } }, { bad: { title: 'x' }, nope: null });
3671
+ const ignoreOk = !c.bad && !c.nope && c.code.questions.length === 1;
3672
+ const before = LENS_CATALOG.code.questions.length;
3673
+ M(LENS_CATALOG, { code: { questions: ['임시'] } });
3674
+ const immutableOk = LENS_CATALOG.code.questions.length === before;
3675
+ return appendOk && newOk && ignoreOk && immutableOk;
3676
+ } },
3677
+ { name: '렌즈 완전판 v2 (1.19.2, UR-0003): 파일 확장자 → 렌즈 도메인 매핑 (행위)', run: () => {
3678
+ const F = _lensDomainsForFiles;
3679
+ const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
3680
+ return eq(F(['src/api.js']), ['code'])
3681
+ && eq(F(['ui/Button.tsx', 'styles/app.css']), ['design']) // UI/스타일 → design (코드 ext 아닌 tsx 는 design 우선)
3682
+ && eq(F(['src/pay.js', 'src/Button.tsx']), ['code', 'design']) // 혼합 → code 먼저, design (최대 2)
3683
+ && eq(F(['README.md']), ['docs'])
3684
+ && eq(F(['tests/foo.test.js']), ['code', 'test']) // 테스트파일(.js) → code+test (고정 순서: code 먼저)
3685
+ && eq(F([]), [])
3686
+ && eq(F(['a.css', 'b.md', 'c.js', 'd.test.js']).length, 2); // 최대 2개 클러터 방지
3687
+ } },
3688
+ { name: 'GPT-5.5 평가 #5 (1.19.1, UR-0009): 클린룸 평가 문서 공개 + 한계 명시 (행위)', run: () => {
3689
+ const dp = path.join(path.dirname(__filename), '..', 'docs', 'clean-room-evaluations.md');
3690
+ if (!exists(dp)) return true; // 패키지에 없으면 스킵(설치본 안전)
3691
+ const d = read(dp);
3692
+ return /AI clean-room evaluations/i.test(d) && /heuristic, not semantic/i.test(d) && /npm i leerness@/.test(d);
3693
+ } },
3660
3694
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3661
3695
  ];
3662
3696
  }
@@ -4235,23 +4269,74 @@ const LENS_CATALOG = {
4235
4269
  affects: ['code', 'test'], affectsNote: '보안 가드를 넣었다면 우회/오탐 테스트가 따라와야 함'
4236
4270
  }
4237
4271
  };
4272
+ // 1.19.3 (UR-0003 렌즈 완전판 v3): 프로젝트별 커스텀 렌즈 — .harness/quality-lenses.json 읽기-병합(쓰기 명령 없음, AI/사용자가 편집).
4273
+ // 포맷: { "domains": { "code": { "questions": ["추가 질문"] }, "a11y": { "title":"접근성", "persona":"스크린리더 사용자", "questions":[...], "affects":["design"] } } }
4274
+ function _loadProjectLenses(root) {
4275
+ try {
4276
+ const p = path.join(absRoot(root || process.cwd()), '.harness', 'quality-lenses.json');
4277
+ if (!exists(p)) return {};
4278
+ const j = JSON.parse(read(p));
4279
+ return (j && typeof j === 'object' && j.domains && typeof j.domains === 'object') ? j.domains : {};
4280
+ } catch { return {}; }
4281
+ }
4282
+ // 내장 + 프로젝트 커스텀 병합. 기존 도메인 → 질문 추가(dedup, 최대 8); 신규 도메인 → 기본값으로 추가. _custom 플래그로 표시.
4283
+ function _mergeLensCatalog(builtin, custom) {
4284
+ const out = {};
4285
+ for (const [k, v] of Object.entries(builtin || {})) out[k] = Object.assign({}, v, { questions: (v.questions || []).slice(), affects: (v.affects || []).slice() });
4286
+ for (const [k, c] of Object.entries(custom || {})) {
4287
+ if (!c || typeof c !== 'object') continue;
4288
+ const cq = Array.isArray(c.questions) ? c.questions.filter(q => typeof q === 'string' && q.trim()) : [];
4289
+ if (out[k]) {
4290
+ const seen = new Set(out[k].questions);
4291
+ for (const q of cq) if (!seen.has(q)) { out[k].questions.push(q); seen.add(q); }
4292
+ out[k].questions = out[k].questions.slice(0, 8);
4293
+ out[k]._customAdded = cq.length > 0;
4294
+ } else if (cq.length) {
4295
+ out[k] = { title: (c.title || k), persona: (c.persona || '검토자'), questions: cq.slice(0, 8), affects: Array.isArray(c.affects) ? c.affects : [], affectsNote: c.affectsNote || '(프로젝트 정의)', _custom: true };
4296
+ }
4297
+ }
4298
+ return out;
4299
+ }
4300
+ function _effectiveLensCatalog(root) { return _mergeLensCatalog(LENS_CATALOG, _loadProjectLenses(root)); }
4301
+
4302
+ // 1.19.2 (UR-0003 렌즈 완전판 v2): 주장된 파일 확장자 → 관련 품질 렌즈 도메인 (결정적, 키워드 추론 아님).
4303
+ // verify-claim 이 완료-검증 순간(사용자: "완료 선언 전 자기질문")에 해당 분야 질문을 advisory 로 노출하는 데 사용.
4304
+ function _lensDomainsForFiles(files) {
4305
+ const out = [];
4306
+ const arr = Array.isArray(files) ? files : [];
4307
+ const has = (re) => arr.some(f => typeof f === 'string' && re.test(f));
4308
+ if (has(/\.(css|scss|sass|less|styl|tsx|jsx|vue|svelte|html?|astro)$/i)) out.push('design'); // UI/스타일
4309
+ if (has(/\.(md|mdx|rst|adoc|txt)$/i)) out.push('docs'); // 문서
4310
+ if (has(/(^|[\\/])(test_[^\\/]+\.[a-z]+|[^\\/]+[._-]test\.[a-z]+|[^\\/]+\.spec\.[a-z]+)$|(^|[\\/])tests?[\\/]/i)) out.push('test'); // 테스트
4311
+ if (has(/\.(js|mjs|cjs|ts|py|rb|go|rs|java|cs|php)$/i)) out.push('code'); // 실코드
4312
+ // 순서: code 먼저(가장 일반), 그다음 design/docs/test. 중복 제거 + 최대 2개(클러터 방지).
4313
+ const ordered = ['code', 'design', 'docs', 'test'].filter(d => out.includes(d));
4314
+ return ordered.slice(0, 2);
4315
+ }
4238
4316
  function lensCmd(domain, opts = {}) {
4239
4317
  const jsonMode = !!opts.json || has('--json');
4240
- if (domain && !LENS_CATALOG[domain]) {
4241
- return fail(`알 없는 렌즈: ${domain} 유효값: ${Object.keys(LENS_CATALOG).join(', ')}`);
4242
- }
4243
- const picked = domain ? { [domain]: LENS_CATALOG[domain] } : LENS_CATALOG;
4318
+ // 1.19.1 (19th 버그헌트): 도메인 인자 정규화 — `lens Code` / `lens CODE ` 도 인식(대소문자·공백 무관).
4319
+ if (domain != null) domain = String(domain).trim().toLowerCase();
4320
+ // 1.19.3: 내장 + 프로젝트 커스텀(.harness/quality-lenses.json) 병합 catalog.
4321
+ const root = opts.root || arg('--path', process.cwd());
4322
+ const catalog = _effectiveLensCatalog(root);
4323
+ if (domain && !catalog[domain]) {
4324
+ return fail(`알 수 없는 렌즈: ${domain} — 유효값: ${Object.keys(catalog).join(', ')}`);
4325
+ }
4326
+ const picked = domain ? { [domain]: catalog[domain] } : catalog;
4244
4327
  if (jsonMode) { log(JSON.stringify({ ok: true, lenses: picked }, null, 2)); return; }
4245
- log(`# leerness lens 분야별 자기질문 품질 렌즈 (1.18.3)`);
4328
+ const hasCustom = Object.values(catalog).some(l => l && (l._custom || l._customAdded));
4329
+ log(`# leerness lens — 분야별 자기질문 품질 렌즈 (1.18.3)${hasCustom ? ' + 프로젝트 커스텀(.harness/quality-lenses.json)' : ''}`);
4246
4330
  log(`완료 선언 전 해당 분야 질문에 스스로 답해보세요. "그렇다(통과)"라고 답할 수 없으면 아직 완료가 아닙니다.`);
4247
4331
  for (const [key, l] of Object.entries(picked)) {
4248
4332
  log('');
4249
- log(`## ${key} (${l.title}) — 페르소나: ${l.persona}`);
4333
+ log(`## ${key} (${l.title}) — 페르소나: ${l.persona}${l._custom ? ' [프로젝트]' : (l._customAdded ? ' [+프로젝트 질문]' : '')}`);
4250
4334
  l.questions.forEach((q, i) => log(` ${i + 1}. ${q}`));
4251
- log(` ↔ 인과: ${key} 를 바꾸면 → ${l.affects.join(', ')} 질문도 다시 — ${l.affectsNote}`);
4335
+ log(` ↔ 인과: ${key} 를 바꾸면 → ${(l.affects || []).join(', ') || '(없음)'} 질문도 다시 — ${l.affectsNote}`);
4252
4336
  }
4253
4337
  log('');
4254
- log(`사용: leerness lens <${Object.keys(LENS_CATALOG).join('|')}> · 완료 검증과 함께: leerness verify-claim T-XXXX`);
4338
+ log(`사용: leerness lens <${Object.keys(catalog).join('|')}> · 완료 검증과 함께: leerness verify-claim T-XXXX`);
4339
+ if (!hasCustom) log(`프로젝트 커스텀 렌즈: .harness/quality-lenses.json 에 { "domains": { "code": { "questions": ["..."] } } } 추가`);
4255
4340
  }
4256
4341
 
4257
4342
  function commandsCmd(root) {
@@ -10141,6 +10226,19 @@ function verifyClaimCmd(root, taskId) {
10141
10226
  if (claimsChecked || mustHaveEvidence) {
10142
10227
  log('');
10143
10228
  log(` ℹ 한계: 테스트 통과는 "의미적 구현 정확성"을 보장하지 않음 — evidence 가 해당 주장(수정 파일/테스트)을 직접 링크해야 신뢰도↑.`);
10229
+ // 1.19.2 (UR-0003 렌즈 완전판 v2): 완료-검증 순간에 분야별 자기질문 advisory — 주장 파일 확장자 기반(결정적).
10230
+ // 기계검증(파일/테스트/스텁)을 통과해도 "사람이 보기에 좋은가"는 별개 → AI 가 스스로 답하도록 권장(advisory, 게이트 아님).
10231
+ const _lensDoms = _lensDomainsForFiles(files);
10232
+ if (_lensDoms.length) {
10233
+ const _lensCat = _effectiveLensCatalog(root); // 1.19.3: 프로젝트 커스텀 질문도 포함
10234
+ log('');
10235
+ log(` 🧭 품질 렌즈 (완료 선언 전 자문 — advisory, 게이트 아님):`);
10236
+ for (const d of _lensDoms) {
10237
+ const l = _lensCat[d];
10238
+ if (l) log(` · ${d}(${l.title}): ${l.questions[0]}`);
10239
+ }
10240
+ log(` → 전체 질문: leerness lens ${_lensDoms[0]}`);
10241
+ }
10144
10242
  }
10145
10243
  if (overallFail) {
10146
10244
  log('');
@@ -19927,6 +20025,6 @@ module.exports = {
19927
20025
  _isCommandPermitted, RUN_CORE_ALLOW,
19928
20026
  // 1.18.2: verify-claim 위장 스텁(빈 export 껍데기) 판정 — 단위 테스트
19929
20027
  _vcImplIsEmpty, _VC_EMPTY_SHELL_RE,
19930
- // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈 — 단위 테스트
19931
- LENS_CATALOG, lensCmd
20028
+ // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈 — 단위 테스트. 1.19.2: 파일→도메인 매핑(완료-검증 advisory)
20029
+ LENS_CATALOG, lensCmd, _lensDomainsForFiles, _mergeLensCatalog, _loadProjectLenses, _effectiveLensCatalog
19932
20030
  };
@@ -0,0 +1,67 @@
1
+ # Clean-room evaluations / 클린룸 평가 기록
2
+
3
+ > **What this is, honestly.** These are **AI clean-room evaluations** — independent agents installing the
4
+ > published npm package into fresh temp directories and exercising it from behavior only (no access to the
5
+ > source tree), including adversarial attempts to defeat leerness's own verifier. They are **not**
6
+ > third-party human security audits or peer-reviewed benchmarks. We publish them so the README claim
7
+ > "verified by independent clean-room evaluations" is checkable rather than a marketing line.
8
+ >
9
+ > 정직하게 말하면: 아래는 **AI 클린룸 평가**입니다 — 독립 에이전트가 게시된 npm 패키지를 빈 임시 폴더에
10
+ > 설치해 **소스 접근 없이 행위만으로** 검증하고, leerness 검증기 자체를 무력화하려는 적대 시도까지 포함합니다.
11
+ > 제3자 인간 보안 감사나 동료심사 벤치마크가 **아닙니다**. README 의 "독립 클린룸 평가로 검증" 주장을
12
+ > 확인 가능하게 만들기 위해 공개합니다.
13
+
14
+ ## Methodology / 방법론
15
+
16
+ Every evaluation follows the same shape:
17
+
18
+ ```bash
19
+ W=$(mktemp -d); cd "$W"; npm init -y
20
+ npm i leerness@<version> # the PUBLISHED package, not the working tree
21
+ LB="node node_modules/leerness/bin/leerness.js"
22
+ # ... drive the CLI by behavior, assert on exit codes + output, clean up ...
23
+ ```
24
+
25
+ - **Published-artifact only** — installs from npm, never reads the repo source. This is what an external user gets.
26
+ - **Behavior-asserted** — checks process exit codes and stdout/stderr, not internal state.
27
+ - **Adversarial** — a portion of every pass actively tries to make a *false* "done" claim pass verification
28
+ (comment-only stubs, empty-export shells, `assert(true)` fake tests, inflated test counts, language tricks).
29
+ - **Both directions (맹신 X / "trust nothing")** — confirmed real defects AND rejected false alarms; a finding
30
+ counts only if reproduced on the published package.
31
+
32
+ ## Evaluations run / 수행한 평가
33
+
34
+ | # | Round | Scope | Outcome |
35
+ |---|---|---|---|
36
+ | 1 | Universal-harness field study (5-axis) | Real Python / Node / Rust development, agent-swap handoff, adversarial attack on the verifier | Verdict "conditionally yes"; found 5 gaps (2×P1, 3×P2) — all closed in 1.17.2–1.17.6, shipped as the 1.18.0 stable minor |
37
+ | 2 | 1.18.0 re-verification (published) | Independently re-ran the 5 gaps against the **published** 1.18.0 | 4 closed; surfaced a new P1 (Windows `--test-cmd python` blocked → false FAIL) → fixed in 1.18.1 |
38
+ | 3 | 1.18.2 adversarial stub workflow | Bypass-hunters + false-positive-hunters vs the empty-shell stub detector | 9 one-keyword bypasses found (Object.freeze, async fn, inline comment, …) and closed; **0 false positives** across ~45 legitimate patterns |
39
+ | 4 | 1.19.0 published-artifact re-verification | Installed `leerness@1.19.0` from npm; smoke-tested headline fixes | `--test-cmd python`, empty-shell rejection, `lens`, selftest 231 — all pass on the published package |
40
+
41
+ ## What these evaluations do NOT establish / 한계 (정직)
42
+
43
+ leerness's verification is **heuristic, not semantic**. These evaluations show it reliably catches *obvious
44
+ false-done claims* (missing files, empty shells, fake/unlinked tests, inflated counts) without false-failing
45
+ honest work. They do **not** prove:
46
+
47
+ - the implementation satisfies the actual requirement, or business logic is correct;
48
+ - tests are sufficient or well-designed;
49
+ - absence of security vulnerabilities or production-runtime correctness.
50
+
51
+ Known heuristic gaps tracked for an AST/token-based redesign: multi-arg call-expression empty objects
52
+ (`Object.assign({}, {})`), Python `def …: pass` / `...` / `raise NotImplementedError`, multi-language empty
53
+ bodies. **Code coverage / mutation testing and third-party reproduction remain open** (the e2e is a
54
+ spawn-based runner, which standard coverage tools do not instrument cleanly).
55
+
56
+ ## Reproduce it yourself / 직접 재현
57
+
58
+ ```bash
59
+ W=$(mktemp -d); cd "$W"; npm init -y && npm i leerness@latest
60
+ LB="node node_modules/leerness/bin/leerness.js"
61
+ $LB init "$W" --yes
62
+ # fake "done": claim an implementation that does not exist
63
+ $LB task add "Implement X"
64
+ $LB task update T-0001 --status done --evidence "x.js done, 5 tests passed"
65
+ $LB verify-claim T-0001 # exit 1 — claim rejected (x.js missing, no tests ran)
66
+ $LB selftest # core-function integrity self-check
67
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",