leerness 1.36.2 → 1.36.4

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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.36.4 — 2026-07-09 — database 렌즈 recall: 내용 기반 DB 감지 (dogfood FN) — 평범한 이름의 DB 모듈도 렌즈 노출
4
+
5
+ **도그푸드에서 발견한 recall gap(FN) 소진.** 실사용(재고 서비스) 도중, `_lensDomainsForFiles` 가 **경로 휴리스틱**만 써서 `models/`·`.repository.`·`.sql` 등 관습적 이름의 파일만 `database` 도메인에 매핑함을 경험적으로 확인했다 — `inventory.safe.mjs`·`orders.occ.mjs` 처럼 실제로 SQLite 를 다루지만 이름에 단서가 없는 모듈은 `database` 렌즈가 **전혀 노출되지 않았다**(`["code"]` 로만 분류).
6
+
7
+ - 신규: `_isDbContentText` / `_anyDbContentInFiles` / `_withDbDomain` — verify-claim 이 주장 파일의 **내용**에 실제 DB 드라이버 import(`node:sqlite`/`pg`/`mysql2`/`better-sqlite3`/`typeorm`/`prisma` 등) 또는 산문엔 거의 없는 완결형 SQL 문장(`SELECT…FROM…WHERE`/`UPDATE…SET…WHERE`/`CREATE TABLE`/`INSERT INTO`/`ON CONFLICT`/`BEGIN IMMEDIATE`)이 있으면 `database` 렌즈를 보강 노출.
8
+ - FP 가드: 단일 키워드(`database`/`select`/`from`/`where` = 영어 단어)만으로는 안 켬 — 드라이버 import 또는 근접 다중 SQL 키워드 조합만. 산문/비-DB 코드 회귀 0(selftest 행위 검증).
9
+ - 안전: 코드 파일(.js/.mjs/.ts/.py 등)만, 512KB 이하만 읽음. path-기반 매핑은 그대로 두고 **추가 보강**만(비파괴). advisory 렌즈라 게이트 아님.
10
+ - codex FN검수 채택: **root 제한**(evidence 경로의 `../`·절대경로로 워크스페이스 밖 파일 읽기 차단, 심볼릭 링크 스킵 — P2) + **테스트 파일 제외**(SQL 담은 test 파일이 `test` 렌즈를 축출하지 않도록 — P3).
11
+ - 알려진 한계(정직 고지): 드라이버 import 가 타 파일에 있는 ORM 메서드체인 파일은 단일파일 내용분석으론 여전히 미탐(P3); `SELECT…FROM…WHERE` 형태의 드문 산문은 advisory FP 가능(저해, 게이트 아님).
12
+ - 검증: 실제 dogfood 4개 DB 모듈 전부 감지(전 false→true), 산문/README FP 0, ReDoS SAFE(<25ms @320KB), root 트래버설·테스트 제외 행위 검증. selftest 278 + full e2e 통과.
13
+
14
+ ## 1.36.3 — 2026-07-08 — DB 안전성 렌즈 코어 승격 (race condition + ACID) — 사용자 명시 + codex 검수 채택
15
+
16
+ - 신규 내장 품질 렌즈 도메인 `database` (8 자기질문: lost update / check-then-act TOCTOU / atomicity / DB 제약 / isolation+retry / durability / distribution). `leerness lens database` 로 호출, verify-claim 이 DB 파일 변경 시 인라인 노출.
17
+ - `_lensDomainsForFiles`: DB 파일(`.sql`/`.prisma`/migrations/schema/models/repositories/dao/db·database 디렉토리/data-source·drizzle.config 등) → `database` 도메인 우선 매핑. 테스트 파일은 test 렌즈 유지(제외).
18
+ - `FILE_EXTS += sql|prisma` (verify-claim 파일 추출) + `_evidenceQuality += prisma` (파일 근거 인식) — `.sql`/`.prisma` 변경도 정상 검증.
19
+ - 저작: 6차원 워크플로 → 적대적 비평 → codex 독립 검수(SHIP-WITH-FIXES 8채택/1반박) → 게시본 재실증 → node:sqlite 데모 자가검증. selftest 276 + e2e-core 42 통과.
20
+
3
21
  ## 1.36.2 — 2026-07-07 — 클린룸 후속: `feature add|show|link|impact` trailing positional path 존중 (조용한 cwd 오독 + stray `.harness` scaffold 차단)
4
22
 
5
23
  **독립 클린룸 리뷰(1.35.17 게시본)가 재현한 P2 결함 소진.** 비-프로젝트 디렉토리에서 `leerness feature add "이름" <프로젝트경로>` 를 실행하면 성공 메시지는 프로젝트 경로를 되울리지만(→ 처리된 척), 실제 feature 는 **CWD** 에 떨어지고 그 자리에 **stray `.harness` 가 조용히 scaffold** 됐다. 즉 지정한 프로젝트가 아니라 아무 폴더나 오염시켰다. `--path <project>` 는 정상 동작했다.
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.36.2 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
128
+ 이 프로젝트는 Leerness v1.36.4 하네스를 사용합니다. 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.36.2는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
182
+ Leerness v1.36.4는 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.36.2는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code
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.36.2)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
203
+ 현재 누적: **70 라운드 (1.9.40 → 1.36.4)** · 매 라운드 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.36.2: 2026-07-07
241
+ Last synced by Leerness v1.36.4: 2026-07-09
242
242
  <!-- leerness:project-readme:end -->
243
243
 
package/bin/leerness.js CHANGED
@@ -33,7 +33,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
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
  const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
35
35
 
36
- const VERSION = '1.36.2';
36
+ const VERSION = '1.36.4';
37
37
 
38
38
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
39
39
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -3850,7 +3850,7 @@ function _selfTestCases() {
3850
3850
  } },
3851
3851
  { name: '품질 렌즈 (1.18.3): lens 명령 표면 등재 + REPL 설치문항 제거 (소스 가드)', run: () => {
3852
3852
  const src = read(__filename);
3853
- const surface = src.includes("if (cmd === 'lens')") && src.includes("cmd: 'lens [code|design|docs|test|security]") && src.includes('leerness lens [code|design|docs|test|security]');
3853
+ const surface = src.includes("if (cmd === 'lens')") && src.includes("cmd: 'lens [code|design|docs|test|security|database]") && src.includes('leerness lens [code|design|docs|test|security|database]');
3854
3854
  const replGone = !src.includes('설치 완료 후 REPL agent ' + '모드를 즉시 시작할까요') && src.includes('REPL agent 모드 진입 ' + '문항 제거');
3855
3855
  return surface && replGone;
3856
3856
  } },
@@ -3894,6 +3894,60 @@ function _selfTestCases() {
3894
3894
  && eq(F([]), [])
3895
3895
  && eq(F(['a.css', 'b.md', 'c.js', 'd.test.js']).length, 2); // 최대 2개 클러터 방지
3896
3896
  } },
3897
+ { name: 'DB 안전성 렌즈 (코어 승격, db-safety-lens 키트): database 도메인 8문항 + DB파일 매핑 인라인 + 비-DB 회귀0 (행위)', run: () => {
3898
+ const F = _lensDomainsForFiles;
3899
+ const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
3900
+ const d = LENS_CATALOG.database;
3901
+ const catOk = !!d && d.questions.length === 8 && d.questionsEn.length === 8
3902
+ && d.questions[0].includes('lost update') && d.questions.some(q => q.includes('TOCTOU'))
3903
+ && d.questions.some(q => q.includes('in-doubt')) && d.questions.some(q => q.includes('SKIP LOCKED'))
3904
+ && d.affects.every(a => LENS_CATALOG[a]);
3905
+ // DB 파일 → database 최우선(questions[0] 인라인 노출), DB+code → [database, code], 비-DB 코드 → 회귀 없음
3906
+ const mapOk = eq(F(['db/migrations/001_init.sql']), ['database'])
3907
+ && eq(F(['prisma/schema.prisma']), ['database'])
3908
+ && eq(F(['src/user.repository.ts']), ['database', 'code'])
3909
+ && eq(F(['src/models/user.js']), ['database', 'code'])
3910
+ && eq(F(['src/db/client.ts']), ['database', 'code']) // codex P2d: 흔한 DB 진입파일 recall
3911
+ && eq(F(['data-source.ts']), ['database', 'code']) // codex P2d: TypeORM data-source
3912
+ && eq(F(['tests/models/user.test.js']), ['code', 'test']) // codex P2b: models/ 안 테스트파일은 test 렌즈 유지
3913
+ && eq(F(['src/api.js']), ['code']) // 비-DB 코드 회귀 0
3914
+ && eq(F(['README.md']), ['docs']);
3915
+ return catOk && mapOk;
3916
+ } },
3917
+ { name: 'DB 렌즈 recall (dogfood FN, 1.36.4): 내용기반 감지 — 평범한 이름의 DB 모듈 잡고 산문 FP 0 (행위)', run: () => {
3918
+ const T = _isDbContentText, W = _withDbDomain;
3919
+ const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
3920
+ // 잡아야: 드라이버 import + 완결형 SQL 문장(SELECT…FROM…WHERE / UPDATE…SET…WHERE) + DDL/트랜잭션 토큰
3921
+ const hit = T("import { DatabaseSync } from 'node:sqlite'")
3922
+ && T("const pg = require('pg')")
3923
+ && T("db.prepare('SELECT * FROM product WHERE id = ?')")
3924
+ && T("db.prepare('UPDATE product SET stock = stock - 1 WHERE id = ?')")
3925
+ && T("db.exec('BEGIN IMMEDIATE')") && T("INSERT INTO orders(x) VALUES(?)") && T("ON CONFLICT(idem_key) DO NOTHING");
3926
+ // FP 0: database/select/from/where 가 산문에 흩어져 있어도 단일 키워드·부분조합은 안 켠다
3927
+ const noFp = !T('// connect to the database and select from the menu')
3928
+ && !T('const total = items.reduce((a,b)=>a+b,0)')
3929
+ && !T('return items.filter(x => x.from && x.where)')
3930
+ && !T('updateProfile(user) // app update, no SQL');
3931
+ // 병합: database 우선 노출 + 최대 2 캡
3932
+ const mergeOk = eq(W(['code']), ['database', 'code']) && eq(W(['code', 'design']), ['database', 'code']) && eq(W([]), ['database']);
3933
+ return hit && noFp && mergeOk;
3934
+ } },
3935
+ { name: 'DB 렌즈 recall 보안·특례 (codex FN검수 P2/P3, 1.36.4): _anyDbContentInFiles root 제한 + 테스트파일 제외 (행위)', run: () => {
3936
+ const os = require('os');
3937
+ const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'lz-dbc-'));
3938
+ const dir = path.join(parent, 'proj'); fs.mkdirSync(dir);
3939
+ try {
3940
+ const sql = "import { DatabaseSync } from 'node:sqlite'\ndb.prepare('SELECT * FROM t WHERE id=?')\n";
3941
+ fs.writeFileSync(path.join(dir, 'svc.mjs'), sql);
3942
+ fs.writeFileSync(path.join(dir, 'svc.test.mjs'), sql); // 테스트 파일(동일 DB 내용)
3943
+ fs.writeFileSync(path.join(parent, 'outside.mjs'), sql); // root 밖(존재+DB내용)
3944
+ const plain = _anyDbContentInFiles(['svc.mjs'], dir) === true;
3945
+ const testExcluded = _anyDbContentInFiles(['svc.test.mjs'], dir) === false; // codex P3: 테스트 제외
3946
+ const traversalBlocked = _anyDbContentInFiles(['../outside.mjs'], dir) === false // codex P2: ../ 거부
3947
+ && _anyDbContentInFiles([path.join(parent, 'outside.mjs')], dir) === false; // 절대경로 거부
3948
+ return plain && testExcluded && traversalBlocked;
3949
+ } finally { try { fs.rmSync(parent, { recursive: true, force: true }); } catch { /* ignore */ } }
3950
+ } },
3897
3951
  { name: 'GPT-5.5 평가 #5 (1.19.1, UR-0009) + 정직성 calibration (1.35.12): 클린룸 문서 공개 + 한계 명시 + self-administered 라벨 (행위)', run: () => {
3898
3952
  const dp = path.join(path.dirname(__filename), '..', 'docs', 'clean-room-evaluations.md');
3899
3953
  if (!exists(dp)) return true; // 패키지에 없으면 스킵(설치본 안전)
@@ -4734,6 +4788,34 @@ const LENS_CATALOG = {
4734
4788
  ],
4735
4789
  affects: ['code', 'test'], affectsNote: '보안 가드를 넣었다면 우회/오탐 테스트가 따라와야 함',
4736
4790
  affectsNoteEn: 'if you added a security guard, bypass/false-positive tests should follow'
4791
+ },
4792
+ // DB 안전성(동시성/트랜잭션) 렌즈 — race condition + ACID. .harness/db-safety-lens 키트에서 코어로 승격
4793
+ // (6차원 워크플로 저작 → 적대적 비평 → codex 독립 검수 8채택/1반박 → 게시본 실증 → node:sqlite 데모 자가검증).
4794
+ database: {
4795
+ title: 'DB 안전성', persona: '이중 결제·유실된 갱신·크래시 후 사라진 커밋으로 호출당해 본 DBA 겸 동시성·분산 리뷰어',
4796
+ titleEn: 'database safety', personaEn: 'a DBA and concurrency / distributed-systems reviewer',
4797
+ questions: [
4798
+ "값을 SELECT해서 앱 코드로 계산한 뒤 다시 UPDATE로 되쓰고 있지 않은가? — 두 요청이 같은 행을 동시에 읽으면 한쪽 갱신이 조용히 사라진다(lost update). 앱 계산을 없앤 원자적 조건 UPDATE(SET n = n + ?)나 버전 CAS(... WHERE version = ?, 영향 행이 0이면 성공이 아니라 충돌로 보고 재시도)로 바꿨는가?",
4799
+ "존재/유일성/한도/잔액 '검사'와 '쓰기'가 별개 문장으로 갈라져 있지 않은가? ('있으면 skip 없으면 INSERT', 'count 확인 후 처리', '잔액 확인 후 차감') — 두 문장 사이에 다른 요청이 끼어들면 중복 행·한도 초과·음수 잔액이 난다(check-then-act TOCTOU). 판정을 DB로 내렸는가? — (a) 존재/유일성은 UNIQUE 제약 + INSERT ... ON CONFLICT(UPSERT)로 강제한다. 없는 행에 SELECT ... FOR UPDATE를 걸어도 Postgres에선 0행이면 아무 락도 잡지 않아 두 요청이 함께 INSERT할 수 있으니 FOR UPDATE로 삽입을 막지 마라(MySQL InnoDB는 gap/next-key 락으로 부분적으로 막지만 엔진 의존이니 제약에 기대라); (b) 이미 있는 행의 잔액/한도는 가드를 UPDATE의 WHERE로 내리고(... WHERE bal >= ?) 영향 행 0을 실패로 처리하거나 그 행을 읽는 SELECT ... FOR UPDATE로 잠근다; (c) 여러 행에 걸친 합계 불변식(sum(할당) <= 예산, 겹침 예약 금지)은 단일 행 CAS로 못 막으니 SERIALIZABLE·EXCLUDE 제약(Postgres)·부모/요약 행 잠금이 필요하다.",
4800
+ "함께 성공하거나 함께 실패해야 할 여러 쓰기(여러 행·여러 테이블)를 하나의 명시적 BEGIN/COMMIT으로 묶었고, 모든 실패 경로(예외·조기 return·타임아웃)에서 ROLLBACK이 보장되는가? — 문장별 autocommit이면 중간 실패 시 절반만 커밋된다(partial commit). 트랜잭션 내내 같은 커넥션이 쓰이는지, 그리고 BEGIN~COMMIT 사이에 외부 HTTP/RPC·사용자 대기를 두지 않아 트랜잭션이 짧게 유지되는지(오래 열리거나 idle-in-transaction이면 커넥션 풀이 고갈되고 MVCC 팽창·VACUUM 차단이 생기니 idle_in_transaction_session_timeout 등으로 방어)도 확인했는가?",
4801
+ "롤백 불가능한 부수효과(이메일·큐 발행·외부 HTTP)와 파생 데이터(집계·카운터·캐시·검색 인덱스)가 원본 커밋과 어긋날 수 있지 않은가? — 트랜잭션 안에서 부수효과를 내면 롤백돼도 유령 이메일이 나가고, 커밋 뒤 fire-and-forget면 커밋과 발행 사이 크래시에 유실된다(멱등키는 중복만 막지 이 유실은 못 막는다). 그래서 반드시 도착해야 하는 효과는 트랜잭셔널 아웃박스(같은 트랜잭션에 의도를 저장하고 커밋 뒤 릴레이가 at-least-once 발송)/내구 job 행/CDC로 보장하고, 명시적으로 best-effort인 효과만 멱등키로 보호된 '커밋 후 발행'으로 빼고, 파생 데이터는 같은 트랜잭션(또는 트리거/아웃박스/CDC)으로 원본과 원자적으로 묶은 뒤 원본에서 재계산하는 정합성 경로를 뒀는가?",
4802
+ "이 불변식(잔액>=0, 유일성, NOT NULL, 참조무결성)이 DB 제약(CHECK/UNIQUE/FK/NOT NULL)으로 강제되는가, 아니면 앱 코드 검증에만 있는가? — 앱 검증은 동시 쓰기·다른 서비스·백필 스크립트·직접 SQL이 조용히 우회한다. 위반을 시도하는 테스트로 (a) 제약이 이 엔진/연결에서 실제로 켜져 있는지(예: SQLite는 연결마다 PRAGMA foreign_keys=ON; Postgres에서 NOT VALID로 추가한 FK는 신규 INSERT/UPDATE에는 그대로 적용되고 기존 행만 검증을 건너뛴다), (b) UNIQUE 컬럼의 NULL은 표준상 서로 구별돼 NULL 행이 무제한 허용된다는 함정을 확인했는가? — NULL이 유효하지 않으면 NOT NULL, NULL을 하나만 허용하려면 Postgres 15 NULLS NOT DISTINCT나 생성열/표현식 유니크로 풀어라(부분 인덱스 WHERE col IS NOT NULL은 NULL을 제약하는 게 아니라 오히려 무제한 허용한다 — non-null만 유니크할 때 쓰는 것).",
4803
+ "이 트랜잭션의 격리 수준을 의식적으로 골랐고, 데드락·직렬화 실패에 유한 재시도 루프(재시도 안에서 최신 상태 재조회, 백오프/지터, 재시도되는 트랜잭션엔 외부 부수효과가 없어 두 번 실행돼도 안전할 것)를 넣었는가? — 기본값에 그냥 맡기면 non-repeatable read·phantom·write skew가 샌다(기본값은 엔진마다 다름: Postgres는 보통 READ COMMITTED, MySQL InnoDB는 보통 REPEATABLE READ). 재시도할 에러코드도 엔진별로 다르니 확인했는가(Postgres 데드락 40P01·직렬화 40001; MySQL 데드락 40001/1213·락 대기 타임아웃 1205 — 1205는 기본적으로 문장만 롤백하니 트랜잭션 전체를 BEGIN부터 재시도하거나 innodb_rollback_on_timeout을 켤 것; SQLite는 SQLITE_BUSY/BUSY_SNAPSHOT에 busy_timeout·쓰기 트랜잭션엔 BEGIN IMMEDIATE·필요 시 WAL로 대응). 그리고 COMMIT 도중 타임아웃/연결끊김으로 결과가 불확실(in-doubt)한 트랜잭션은 무조건 롤백으로 단정하지 말고 멱등키/트랜잭션ID/원본 재조회로 실제 반영 여부를 재확인한 뒤 재시도하는가? 스냅샷 격리/Postgres REPEATABLE READ만으론 write skew가 안 막히니(SERIALIZABLE 또는 명시적 잠금 필요) 합계·겹침 불변식엔 그 방어를 뒀고, 여러 행/테이블을 잠그는 경로는 늘 같은 순서로 잠그는가?",
4804
+ "사용자에게 '성공'을 돌려준 이 쓰기가 실제로 확인된 COMMIT에 도달했고(커밋을 await하고 결과를 검사했는가), 그 ack가 메모리 버퍼가 아니라 안정 저장소(커밋 시 WAL/redo/journal이 flush됨)를 의미하는가? — autocommit을 끈 채 커밋을 빠뜨리면 커넥션 반납 시 열린 트랜잭션이 조용히 롤백된다. 내구성 설정은 엔진마다 다르니 데이터 등급에 맞게 확인했는가(예: Postgres synchronous_commit, MySQL innodb_flush_log_at_trx_commit=1 + sync_binlog=1, SQLite synchronous=FULL; Redis는 기본이 비내구적). 노드 손실까지 견뎌야 하는 데이터라면 쿼럼/동기 복제 ack(예: w:majority, 동기 스탠바이)로 한 노드가 죽어도 유실되지 않게 했고, failover RPO를 명시했는가?",
4805
+ "동시성 방어를 인프로세스 뮤텍스·싱글턴·인메모리 dedup에 기대거나, 여러 서비스에 걸친 작업을 하나의 DB 트랜잭션으로 감쌀 수 있다고 착각하거나, 방금 프라이머리에 쓴 값을 지연된 리드 레플리카에서 읽고 있지 않은가? — 인스턴스가 2개 이상이면 인프로세스 락은 무력하니 공유 지점으로 옮기고(DB UNIQUE 제약·행 잠금·advisory lock, 큐 소비는 FOR UPDATE SKIP LOCKED), 원자성은 프로세스/네트워크 경계를 못 넘으니 크로스서비스는 아웃박스+saga+보상 트랜잭션으로, at-least-once 전달은 멱등키(UNIQUE)로, 복제 지연이 깨는 read-your-writes는 프라이머리 라우팅/LSN 확인으로 옮겼는가? 분산 락을 쓴다면 리스 만료 후 늦게 깨어난 작업이 최신 작업을 덮어쓰지 못하게 단조 증가 펜싱 토큰(DB 시퀀스/행 버전)을 발급하고 하위 자원이 낮은 토큰을 거부하게 했는가?"
4806
+ ],
4807
+ questionsEn: [
4808
+ "Am I reading a value with SELECT, computing it in app code, then writing it back with UPDATE? Two requests reading the same row concurrently silently drop one write (lost update). Did I switch to an atomic conditional UPDATE (SET n = n + ?) or a version CAS (... WHERE version = ?, treating 0 rows affected as a conflict to retry, not as success)?",
4809
+ "Are my existence/uniqueness/limit/balance 'check' and 'write' split into two separate statements ('skip if exists else INSERT', 'check count then act', 'check balance then debit')? A concurrent request slipping between them yields a duplicate row, a breached limit, or a negative balance (check-then-act TOCTOU). Did I push the decision into the DB? — (a) for existence/uniqueness, enforce it with a UNIQUE constraint + INSERT ... ON CONFLICT (upsert): a SELECT ... FOR UPDATE that returns zero rows takes NO lock on PostgreSQL, so two requests both INSERT — don't use FOR UPDATE to guard an insert (MySQL InnoDB gap/next-key locks block this partially, but that's engine-dependent; rely on the constraint); (b) for a balance/limit on an EXISTING row, fold the guard into UPDATE ... WHERE bal >= ? and treat 0 rows affected as failure, or lock that row with SELECT ... FOR UPDATE; (c) a cross-row aggregate invariant (sum(allocations) <= budget, no overlapping bookings) can't be held by a single-row CAS — it needs SERIALIZABLE, a Postgres EXCLUDE constraint, or a lock on the parent/summary row.",
4810
+ "Are the writes that must succeed-or-fail together (multiple rows / tables) wrapped in ONE explicit BEGIN/COMMIT, with ROLLBACK guaranteed on every failure path (exception, early return, timeout)? Under per-statement autocommit a mid-way failure leaves a half-applied partial commit. Did I also confirm the same pinned connection is used throughout, and that the transaction stays short — no external HTTP/RPC or user-wait between BEGIN and COMMIT (a long-open or idle-in-transaction session exhausts the connection pool and, on MVCC engines, bloats the heap and blocks VACUUM; guard with idle_in_transaction_session_timeout)?",
4811
+ "Can a non-rollbackable side effect (email, queue publish, external HTTP) or derived data (aggregate, counter, cache, search index) diverge from the source commit? A side effect issued inside the tx can't be rolled back (ghost email); one fired-and-forgotten after commit is lost on a crash between commit and dispatch (an idempotency key prevents duplicates, not this loss). So for effects that MUST arrive, guarantee them with a transactional outbox (persist intent in the same tx; a relay dispatches at-least-once after commit) / durable job row / CDC, and only move an explicitly best-effort effect to publish-after-commit guarded by an idempotency key; and bind derived data to the source atomically (same tx / trigger / outbox / CDC) with a recompute-from-source reconciliation path?",
4812
+ "Is this invariant (balance>=0, uniqueness, NOT NULL, referential integrity) enforced by a DB constraint (CHECK/UNIQUE/FK/NOT NULL), or does it live only in app-code validation? App validation is silently bypassed by concurrent writers, other services, backfill scripts, and direct SQL. Did I confirm, with a test that attempts a violation and asserts rejection: (a) the constraint is actually live on this engine/connection (e.g. SQLite needs PRAGMA foreign_keys=ON per connection; a Postgres FK added NOT VALID IS enforced for all new INSERT/UPDATE and only skips validating pre-existing rows), and (b) I'm not tripped by UNIQUE's NULL semantics — standard UNIQUE treats NULLs as distinct, so a 'unique' nullable column silently allows unlimited NULL rows? If NULL is invalid, use NOT NULL; if only one NULL is allowed, use Postgres 15 NULLS NOT DISTINCT or a generated/expression unique (a partial index WHERE col IS NOT NULL does NOT constrain NULLs — it leaves them unlimited; that's the tool for unique-when-present).",
4813
+ "Did I consciously choose this transaction's isolation level and add a bounded retry loop for deadlock / serialization failure (re-read fresh state inside the retry, use backoff/jitter, and make sure the retried transaction is free of external side effects so re-running it is safe)? Inheriting an unexamined default can leak non-repeatable reads, phantoms, or write skew (defaults differ by engine — Postgres tends to READ COMMITTED, MySQL InnoDB tends to REPEATABLE READ). Did I use the RIGHT retry error codes (Postgres deadlock 40P01 / serialization 40001; MySQL deadlock 40001/1213 / lock-wait timeout 1205 — 1205 by default rolls back only the statement, so retry the WHOLE transaction from BEGIN or set innodb_rollback_on_timeout; SQLite: handle SQLITE_BUSY/BUSY_SNAPSHOT with busy_timeout, BEGIN IMMEDIATE for write txns, WAL where apt)? And if a COMMIT is left in-doubt by a timeout/dropped connection, do I NOT assume rollback but reconcile actual persistence via idempotency key / transaction id / source-of-truth read before retrying? Snapshot isolation / Postgres REPEATABLE READ does NOT stop write skew (needs SERIALIZABLE or explicit locking), so did I guard sum/overlap invariants that way — and do all paths that lock multiple rows/tables acquire them in the same order?",
4814
+ "Did the write I returned 'success' for actually reach a checked COMMIT (did I await the commit and inspect its result), and does that ack mean stable storage — WAL/redo/journal flushed on commit — not just a memory buffer? With autocommit off and a forgotten COMMIT, the open transaction is silently rolled back when the connection returns to the pool. Durability settings differ by engine — did I verify the one matching this data's class (e.g. Postgres synchronous_commit, MySQL innodb_flush_log_at_trx_commit=1 + sync_binlog=1, SQLite synchronous=FULL; Redis is not durable by default)? For must-survive-node-loss data, did I require a quorum / synchronous-replication ack (e.g. w:majority, synchronous standby) so one node dying can't lose it, and state the failover RPO?",
4815
+ "Am I relying on an in-process mutex/singleton/in-memory dedup, or assuming one DB transaction can wrap work spanning multiple services, or reading a just-written value from a lagging read replica? With >=2 instances an in-process lock is useless, so move mutual exclusion to a shared point (DB UNIQUE constraint, row lock, advisory lock; pull queue work with FOR UPDATE SKIP LOCKED); atomicity can't cross a process/network boundary, so route cross-service work through an outbox + saga + compensating actions and collapse at-least-once delivery with an idempotency key (UNIQUE); and replication lag breaks read-your-writes, so route read-after-write / consistency-critical reads to the primary or gate on replica LSN/lag. If I use a distributed lock, did I issue a monotonically increasing fencing token (DB sequence / row version) and have the guarded resource reject lower tokens, so a worker that resumes after its lease expired can't overwrite newer work?"
4816
+ ],
4817
+ affects: ['code', 'test'], affectsNote: '동시성 방어엔 인터리빙/부하/재시도/멀티인스턴스 실패를 재현하는 테스트가 따라와야 함(단일 인스턴스 초록불은 증거가 아님)',
4818
+ affectsNoteEn: 'a concurrency defense needs an interleaving/load/retry/multi-instance reproduction test'
4737
4819
  }
4738
4820
  };
4739
4821
  // 1.19.3 (UR-0003 렌즈 완전판 v3): 프로젝트별 커스텀 렌즈 — .harness/quality-lenses.json 읽기-병합(쓰기 명령 없음, AI/사용자가 편집).
@@ -4776,10 +4858,53 @@ function _lensDomainsForFiles(files) {
4776
4858
  if (has(/\.(md|mdx|rst|adoc|txt)$/i)) out.push('docs'); // 문서
4777
4859
  if (has(/(^|[\\/])(test_[^\\/]+\.[a-z]+|[^\\/]+[._-]test\.[a-z]+|[^\\/]+\.spec\.[a-z]+)$|(^|[\\/])tests?[\\/]/i)) out.push('test'); // 테스트
4778
4860
  if (has(/\.(js|mjs|cjs|ts|py|rb|go|rs|java|cs|php)$/i)) out.push('code'); // 실코드
4779
- // 순서: code 먼저(가장 일반), 그다음 design/docs/test. 중복 제거 + 최대 2개(클러터 방지).
4780
- const ordered = ['code', 'design', 'docs', 'test'].filter(d => out.includes(d));
4861
+ // DB 표면: 마이그레이션/스키마/ORM/DB 진입 파일 database 렌즈(동시성/트랜잭션). 결정적 경로/확장자 매칭.
4862
+ // 테스트 파일(예: models/ 안의 *.test.js)은 test 렌즈를 유지하도록 database 매핑에서 제외.
4863
+ const _dbRe = /\.sql$|\.prisma$|(^|[\\/])(migrations?|migrate|db|database)[\\/]|(^|[\\/])(models?|entities|repositor(?:y|ies)|daos?)[\\/]|[._-](repository|entity|model|dao|schema|migration)\.[a-z]+$|(^|[\\/])(schema|db|database|data-source|ormconfig|knexfile|drizzle\.config|typeorm\.config)\.[a-z]+$/i;
4864
+ const _testRe = /(^|[\\/])(test_[^\\/]+\.[a-z]+|[^\\/]+[._-]test\.[a-z]+|[^\\/]+\.spec\.[a-z]+)$|(^|[\\/])tests?[\\/]/i;
4865
+ if (arr.some(f => typeof f === 'string' && _dbRe.test(f) && !_testRe.test(f))) out.push('database');
4866
+ // 순서: database 먼저(DB 파일이면 동시성/트랜잭션 질문을 인라인 노출), 그다음 code/design/docs/test. 중복 제거 + 최대 2개(클러터 방지).
4867
+ const ordered = ['database', 'code', 'design', 'docs', 'test'].filter(d => out.includes(d));
4781
4868
  return ordered.slice(0, 2);
4782
4869
  }
4870
+ // 1.36.x (dogfood FN): path 휴리스틱은 관습적 이름(models/·.repository.·.sql 등)만 잡아, 평범한 이름의 DB 모듈
4871
+ // (inventory.safe.mjs · orders.occ.mjs 처럼 DB 를 다루지만 이름에 단서 없음)을 놓친다. 내용 기반으로 보강한다:
4872
+ // claimed 파일이 실제 DB 드라이버를 import 하거나 산문엔 안 나오는 명백한 SQL DDL/트랜잭션 토큰을 담으면 database 렌즈를 켠다.
4873
+ // FP 가드: "database" 라는 단어만으로는 안 켠다 — 드라이버 import 또는 CREATE TABLE/INSERT INTO/ON CONFLICT/BEGIN IMMEDIATE/FOR UPDATE 급 토큰이 있어야 함.
4874
+ const _DB_IMPORT_RE = /(?:require\s*\(|\bfrom|\bimport)\s*['"](?:node:sqlite|better-sqlite3|sqlite3|pg|postgres|mysql2?|knex|typeorm|sequelize|drizzle-orm|@prisma\/client|mongodb|mongoose|ioredis|redis|oracledb|tedious|@libsql\/client)['"]/i;
4875
+ // 단일 SQL 키워드는 산문 FP 위험(select/update/from/where 는 영어 단어) → 산문에 거의 없는 조합만: DDL/트랜잭션 토큰 또는
4876
+ // 완결형 문장 패턴(SELECT…FROM…WHERE / UPDATE…SET…WHERE / DELETE FROM…WHERE). 근접 조합이라 산문 FP 가 급감한다.
4877
+ const _DB_SQL_RE = /\b(?:CREATE\s+TABLE|INSERT\s+INTO|ON\s+CONFLICT|BEGIN\s+(?:IMMEDIATE|TRANSACTION|DEFERRED|EXCLUSIVE)|FOR\s+UPDATE)\b|\bSELECT\b[\s\S]{0,120}?\bFROM\b[\s\S]{0,120}?\bWHERE\b|\bUPDATE\b[\s\S]{0,80}?\bSET\b[\s\S]{0,200}?\bWHERE\b|\bDELETE\s+FROM\b[\s\S]{0,120}?\bWHERE\b/i;
4878
+ function _isDbContentText(txt) {
4879
+ return typeof txt === 'string' && (_DB_IMPORT_RE.test(txt) || _DB_SQL_RE.test(txt));
4880
+ }
4881
+ // claimed 파일 중 코드 파일을 읽어 DB 내용 신호가 있으면 true. 미존재/큰 파일(>512KB)은 건너뜀(안전·비용).
4882
+ // 보안(codex P2): 경로는 root 안으로 제한 — 절대경로/../ 트래버설로 워크스페이스 밖 파일을 읽지 않는다. 심볼릭 링크는 스킵.
4883
+ // codex P3: 테스트 파일은 DB 증강에서 제외 — test 렌즈를 축출하지 않도록(_lensDomainsForFiles 의 test 특례와 일관).
4884
+ function _anyDbContentInFiles(files, root) {
4885
+ const arr = Array.isArray(files) ? files : [];
4886
+ const base = path.resolve(root || process.cwd());
4887
+ const _testRe = /(^|[\\/])(test_[^\\/]+\.[a-z]+|[^\\/]+[._-]test\.[a-z]+|[^\\/]+\.spec\.[a-z]+)$|(^|[\\/])tests?[\\/]/i;
4888
+ for (const f of arr) {
4889
+ if (typeof f !== 'string') continue;
4890
+ if (!/\.(js|mjs|cjs|ts|tsx|py|rb|go|rs|java|cs|php)$/i.test(f)) continue;
4891
+ if (_testRe.test(f)) continue; // 테스트 파일 → test 렌즈 유지(DB 증강 제외)
4892
+ try {
4893
+ const p = path.resolve(base, f);
4894
+ if (p !== base && !p.startsWith(base + path.sep)) continue; // root 밖(../·절대경로) 거부
4895
+ const st = fs.lstatSync(p); // 심볼릭 링크는 따라가지 않음(isFile()=false → 스킵)
4896
+ if (!st.isFile() || st.size > 512 * 1024) continue;
4897
+ if (_isDbContentText(fs.readFileSync(p, 'utf8'))) return true;
4898
+ } catch { /* 미존재/권한 → 무시 */ }
4899
+ }
4900
+ return false;
4901
+ }
4902
+ // database 를 도메인 목록에 병합(우선 노출 + 최대 2 캡). database 가 code/design 등보다 앞선다.
4903
+ function _withDbDomain(domains) {
4904
+ const merged = Array.isArray(domains) ? domains.slice() : [];
4905
+ if (!merged.includes('database')) merged.unshift('database');
4906
+ return ['database', 'code', 'design', 'docs', 'test'].filter(d => merged.includes(d)).slice(0, 2);
4907
+ }
4783
4908
  function lensCmd(domain, opts = {}) {
4784
4909
  const jsonMode = !!opts.json || has('--json');
4785
4910
  // 1.19.1 (19th 버그헌트): 도메인 인자 정규화 — `lens Code` / `lens CODE ` 도 인식(대소문자·공백 무관).
@@ -4851,7 +4976,7 @@ function commandsCmd(root) {
4851
4976
  { cmd: 'encoding check [path]', desc: '인코딩 검증' },
4852
4977
  { cmd: 'lazy detect [path] [--json]', desc: '게으른 작업 감지 (1.9.101)' },
4853
4978
  { cmd: 'verify-claim <T-ID|--all> [--run-tests] [--test-cmd "<명령>"] [--strict-claims] [--require-evidence]', desc: '주장 검증 (1.9.18~26) — --all: 모든 done 주장 일괄 검증(CI·스케일, 1.33.2) · --require-evidence: done 주장에 파일+테스트 근거 강제 (1.9.287) · --test-cmd: 비-JS 테스트 명령 (1.17.2)' },
4854
- { cmd: 'lens [code|design|docs|test|security] [--json]', desc: '분야별 자기질문 품질 렌즈 + 분야간 인과관계 (1.18.3)' },
4979
+ { cmd: 'lens [code|design|docs|test|security|database] [--json]', desc: '분야별 자기질문 품질 렌즈 + 분야간 인과관계 (1.18.3, database: 동시성/트랜잭션)' },
4855
4980
  { cmd: 'optimism-check <T-ID>', desc: '낙관적 API 감지 (1.9.26)' },
4856
4981
  { cmd: 'requests audit|list|complete|drop|auto-complete', desc: '사용자 요청 추적 (1.9.207/223)' },
4857
4982
  { cmd: 'pre-wake-audit [path] [--last]', desc: 'sleep 전 점검 (1.9.209)' },
@@ -10675,7 +10800,7 @@ function verifyClaimCmd(root, taskId, opts = {}) {
10675
10800
  // 확장자는 길이 내림차순(긴 것 먼저 매치) + \b 종결로 .ts vs .tscn 구분.
10676
10801
  // 1.9.21: 설정/메타 파일 확장자 추가 — Godot export_presets.cfg 등 false negative 보완
10677
10802
  // 1.18.2: java|php|mjs|cjs 추가 — _VC_CODE_EXT 와 정합(이전엔 .java/.php 임플 주장이 추출조차 안 돼 스텁/존재 검사를 무검사 통과).
10678
- const FILE_EXTS = 'webmanifest|dockerfile|properties|tscn|tres|godot|json5|java|jsx|tsx|yaml|html|scss|sass|less|gltf|conf|json|toml|lock|mdx|xml|css|svg|yml|cfg|ini|env|php|mjs|cjs|md|js|ts|gd|cs|py|rb|go|rs|kt|sh|h';
10803
+ const FILE_EXTS = 'webmanifest|dockerfile|properties|tscn|tres|godot|json5|prisma|java|jsx|tsx|yaml|html|scss|sass|less|gltf|conf|json|toml|lock|mdx|xml|css|svg|yml|cfg|ini|env|php|sql|mjs|cjs|md|js|ts|gd|cs|py|rb|go|rs|kt|sh|h';
10679
10804
  const FILE_RE = new RegExp(`(?:[A-Za-z][A-Za-z0-9_-]*\\/)?[A-Za-z][\\w./-]*\\.(?:${FILE_EXTS})\\b`, 'g');
10680
10805
  const filePatterns = evidence.match(FILE_RE) || [];
10681
10806
  // 중복 제거 + "tests/test.js" 같은 결과를 유지 (이미 `..` 없으니 그대로)
@@ -11019,7 +11144,9 @@ function verifyClaimCmd(root, taskId, opts = {}) {
11019
11144
  log(t(` ℹ 한계: 테스트 통과는 "의미적 구현 정확성"을 보장하지 않음 — evidence 가 해당 주장(수정 파일/테스트)을 직접 링크해야 신뢰도↑.`, ` ℹ Limit: passing tests do not guarantee "semantic correctness" — evidence should directly link the claimed files/tests for higher confidence.`));
11020
11145
  // 1.19.2 (UR-0003 렌즈 완전판 v2): 완료-검증 순간에 분야별 자기질문 advisory — 주장 파일 확장자 기반(결정적).
11021
11146
  // 기계검증(파일/테스트/스텁)을 통과해도 "사람이 보기에 좋은가"는 별개 → AI 가 스스로 답하도록 권장(advisory, 게이트 아님).
11022
- const _lensDoms = _lensDomainsForFiles(files);
11147
+ let _lensDoms = _lensDomainsForFiles(files);
11148
+ // 1.36.x (dogfood FN): path 가 놓친 평범한 이름의 DB 모듈 — claimed 파일 내용에 DB 신호가 있으면 database 렌즈 보강.
11149
+ if (!_lensDoms.includes('database') && _anyDbContentInFiles(files, root)) _lensDoms = _withDbDomain(_lensDoms);
11023
11150
  if (_lensDoms.length) {
11024
11151
  const _lensCat = _effectiveLensCatalog(root); // 1.19.3: 프로젝트 커스텀 질문도 포함
11025
11152
  log('');
@@ -20433,7 +20560,7 @@ VERIFICATION (evidence-gated "done")
20433
20560
  contract verify <spec.md> <impl.js> [--json] Spec <-> implementation match
20434
20561
  verify-code [path] [--build] [--bench] Run tests/lint/typecheck, record evidence
20435
20562
  gate [path] One-call CI gate: verify + audit + scan + encoding + lazy
20436
- lens [code|design|docs|test|security] [--json] Quality self-question lenses
20563
+ lens [code|design|docs|test|security|database] [--json] Quality self-question lenses
20437
20564
 
20438
20565
  SECURITY & HYGIENE
20439
20566
  scan secrets [path] Committed-secret detection
@@ -20495,7 +20622,7 @@ function help() {
20495
20622
  // 1.23.1 (UR-0010 Phase 6): 영어 opt-in 시 큐레이트 영어판. 기본(ko) 은 아래 한국어 help 그대로.
20496
20623
  if (_uiLang(arg('--path', process.cwd())) === 'en') { _helpEn(); return; }
20497
20624
  log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path] [--all-apps] [--include p1,p2] [--since 24h|3d] [--compact] [--json] # 1.9.17-22 워크스페이스 (--compact: LLM 시스템 프롬프트용 1줄 요약)\n leerness orchestrate "<목표>" [--agents N] [--model qwen2.5:7b-instruct] [--retry-on-fail K] # 1.9.22 Ollama opt-in (LEERNESS_OLLAMA_BASE_URL 필요)\n leerness llm-bench record --score N --model X [--label L] [--tokens T] # 1.9.22 LLM 벤치 히스토리 누적\n leerness deps <capability> [--run-tests] [--json] # 1.9.24 depends-on 역방향 추적 + 자동 회귀 sweep\n leerness memory search "키" [--include-code] # 1.9.25 소스 코드 본문도 검색 (모순 감지 핵심)\n leerness brainstorm "주제" [--include-code] # 1.9.25 코드 본문 hits 포함\n leerness register-pending "<요청>" [--agent X] [--note Y] # 1.9.25 다중 세션 in-progress 즉시 등록\n leerness optimism-check <T-ID> [--json] # 1.9.26/27 낙관적 표시 감지 (1.9.27: 10 카테고리 + URL/메서드 매핑 + 신뢰도 점수)\n leerness persona list|show <id>|add <id> # 1.9.29 페르소나 카탈로그 (보안/성능/UX/testing/docs 5종 내장)\n leerness review <file> --persona <id1,id2,...> # 1.9.29 도메인 페르소나 리뷰 프롬프트 자동 생성\n leerness agents list|check|quota # 1.9.30/31 외부 AI CLI 가용성 + quota 추정 (claude/codex/agy/copilot)\n leerness agents dispatch "<task>" --to <id> # 1.9.30 활성 CLI 대상 실행 명령 생성 (실 호출 X, 사용자 실행)\n leerness agents multi "<task>" [--only c1,c2] [--write] [--execute] [--timeout 60] # 1.9.152/156 활성 N개 일괄 dispatch (--execute: 실 spawn + consensus)\n leerness provider list|add|remove [args] # 1.9.157 Provider Registry — 사용자 정의 CLI provider 동적 추가 (OpenRouter/Bedrock 흡수)\n leerness agents dispatch "<task>" --multi # 1.9.152 multi 모드 alias (또는 --to all)\n leerness setup-agents [path] [--yes|--no-setup-agents] # 1.9.32 sub-agent CLI 인터랙티브 설정 (.env + 미설치 자동 설치)\n leerness init [path] [--no-stale-check] # 1.9.33 npx 캐시 함정 — 옛 버전 자동 경고 (끄려면 --no-stale-check)\n leerness which [--json] # 1.9.164 진단: 현재 실행 경로/버전 + npm 캐시 + PATH 후보 (구버전 충돌 해결)\n leerness selftest [--json] # 1.9.258 코어 함수 무결성 자가 검증 (설치 손상/부분설치 감지, CI 친화 exit 1)\n leerness shell-guard "<command>" [--json] # 1.9.260 터미널 명령 셸 호환성 린터 (PowerShell 5.1 && 미지원 등 실행 전 감지, UR-0020)\n leerness shell-guard --record --cmd "..." --exit N # 1.9.260 실패한 터미널 명령 기록 → 다음 분석 시 회수\n leerness path-setup [--apply] [--json] # 1.9.254 leerness CLI PATH 자동 등록 (npm global bin 미등록 시)\n leerness web check|screenshot|extract <url> [--out file.png] [--selector "css"] # 1.9.165 playwright bridge (opt-in: npm i -g playwright + permissions.browser)\n leerness pc check|click|type|screenshot [--x N --y N] [--text "s"] [--out f.png] # 1.9.166 robotjs/nut-tree bridge (opt-in: npm i -g robotjs + permissions.mouse/keyboard, ⚠ full 모드 권장)\n leerness lsp check|symbols|references <file/name> [--in dir] [--json] # 1.9.167 LSP 어댑터 MVP (typescript opt-in + regex fallback, 코드 인텔리전스)\n leerness review-request "<request>" [--json] # 1.9.176 사용자 요청 사전 검토 (충돌/재사용/효율/권장 단계 — 사용자 명시)\n leerness contract verify <spec.md> <impl.js> [--json] # 1.9.35 명세 ↔ 구현 일치 검사 (함수/필드)\n leerness reuse autodetect [path] [--apply] [--json] # 1.9.35 src/*.js의 module.exports → reuse-map 후보 등록\n leerness audit [path] [--fix] # 1.9.35 --fix: session-handoff/current-state 자동 갱신\n leerness verify-claim <T-ID> ... [--strict-claims] # 1.9.26 verify-claim에 낙관적 표시 자동 검사 통합
20498
- leerness lens [code|design|docs|test|security] [--json] # 1.18.3 분야별 자기질문 품질 렌즈 + 분야간 인과관계 (완료 선언 전 자가 점검)\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--run-tests] [--json] # 1.9.18-20 evidence 자동 검증 (1.9.20: scenes/scripts 등 도메인 폴더 + jest/mocha 파싱)\n leerness verify-code [path] [--build] [--bench] # 1.9.20 --bench: scripts.bench 추가 실행 + evidence 누적\n leerness session close [path]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence|relink [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy
20625
+ leerness lens [code|design|docs|test|security|database] [--json] # 1.18.3 분야별 자기질문 품질 렌즈 (database: 동시성/트랜잭션 완료 선언 전 자가 점검)\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--run-tests] [--json] # 1.9.18-20 evidence 자동 검증 (1.9.20: scenes/scripts 등 도메인 폴더 + jest/mocha 파싱)\n leerness verify-code [path] [--build] [--bench] # 1.9.20 --bench: scripts.bench 추가 실행 + evidence 누적\n leerness session close [path]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence|relink [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy
20499
20626
  leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
20500
20627
  leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
20501
20628
  leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
@@ -21034,5 +21161,6 @@ module.exports = {
21034
21161
  // 1.18.2: verify-claim 위장 스텁(빈 export 껍데기) 판정 — 단위 테스트
21035
21162
  _vcImplIsEmpty, _VC_EMPTY_SHELL_RE,
21036
21163
  // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈 — 단위 테스트. 1.19.2: 파일→도메인 매핑(완료-검증 advisory)
21037
- LENS_CATALOG, lensCmd, _lensDomainsForFiles, _mergeLensCatalog, _loadProjectLenses, _effectiveLensCatalog
21164
+ LENS_CATALOG, lensCmd, _lensDomainsForFiles, _mergeLensCatalog, _loadProjectLenses, _effectiveLensCatalog,
21165
+ _isDbContentText, _anyDbContentInFiles, _withDbDomain
21038
21166
  };
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 && /&&|\|\|/.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) || (g.indexOf('/') >= 0 && c.endsWith('/' + g))) return true; } // 1.35.5: reverse match 는 git 경로가 다중세그먼트일 때만 — bare basename 충돌(src/test.js ↔ test.js) 차단
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|prisma|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) || (g.indexOf('/') >= 0 && c.endsWith('/' + g))) return true; } // 1.35.5: reverse match 는 git 경로가 다중세그먼트일 때만 — bare basename 충돌(src/test.js ↔ test.js) 차단
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.2",
3
+ "version": "1.36.4",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",