leerness 1.36.2 → 1.36.3
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 +7 -0
- package/README.md +4 -4
- package/bin/leerness.js +61 -8
- package/lib/analyzers.js +90 -90
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.3 — 2026-07-08 — DB 안전성 렌즈 코어 승격 (race condition + ACID) — 사용자 명시 + codex 검수 채택
|
|
4
|
+
|
|
5
|
+
- 신규 내장 품질 렌즈 도메인 `database` (8 자기질문: lost update / check-then-act TOCTOU / atomicity / DB 제약 / isolation+retry / durability / distribution). `leerness lens database` 로 호출, verify-claim 이 DB 파일 변경 시 인라인 노출.
|
|
6
|
+
- `_lensDomainsForFiles`: DB 파일(`.sql`/`.prisma`/migrations/schema/models/repositories/dao/db·database 디렉토리/data-source·drizzle.config 등) → `database` 도메인 우선 매핑. 테스트 파일은 test 렌즈 유지(제외).
|
|
7
|
+
- `FILE_EXTS += sql|prisma` (verify-claim 파일 추출) + `_evidenceQuality += prisma` (파일 근거 인식) — `.sql`/`.prisma` 변경도 정상 검증.
|
|
8
|
+
- 저작: 6차원 워크플로 → 적대적 비평 → codex 독립 검수(SHIP-WITH-FIXES 8채택/1반박) → 게시본 재실증 → node:sqlite 데모 자가검증. selftest 276 + e2e-core 42 통과.
|
|
9
|
+
|
|
3
10
|
## 1.36.2 — 2026-07-07 — 클린룸 후속: `feature add|show|link|impact` trailing positional path 존중 (조용한 cwd 오독 + stray `.harness` scaffold 차단)
|
|
4
11
|
|
|
5
12
|
**독립 클린룸 리뷰(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.
|
|
128
|
+
이 프로젝트는 Leerness v1.36.3 하네스를 사용합니다. 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.
|
|
182
|
+
Leerness v1.36.3는 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.
|
|
203
|
+
현재 누적: **70 라운드 (1.9.40 → 1.36.3)** · 매 라운드 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.
|
|
241
|
+
Last synced by Leerness v1.36.3: 2026-07-08
|
|
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.
|
|
36
|
+
const VERSION = '1.36.3';
|
|
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,26 @@ 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
|
+
} },
|
|
3897
3917
|
{ name: 'GPT-5.5 평가 #5 (1.19.1, UR-0009) + 정직성 calibration (1.35.12): 클린룸 문서 공개 + 한계 명시 + self-administered 라벨 (행위)', run: () => {
|
|
3898
3918
|
const dp = path.join(path.dirname(__filename), '..', 'docs', 'clean-room-evaluations.md');
|
|
3899
3919
|
if (!exists(dp)) return true; // 패키지에 없으면 스킵(설치본 안전)
|
|
@@ -4734,6 +4754,34 @@ const LENS_CATALOG = {
|
|
|
4734
4754
|
],
|
|
4735
4755
|
affects: ['code', 'test'], affectsNote: '보안 가드를 넣었다면 우회/오탐 테스트가 따라와야 함',
|
|
4736
4756
|
affectsNoteEn: 'if you added a security guard, bypass/false-positive tests should follow'
|
|
4757
|
+
},
|
|
4758
|
+
// DB 안전성(동시성/트랜잭션) 렌즈 — race condition + ACID. .harness/db-safety-lens 키트에서 코어로 승격
|
|
4759
|
+
// (6차원 워크플로 저작 → 적대적 비평 → codex 독립 검수 8채택/1반박 → 게시본 실증 → node:sqlite 데모 자가검증).
|
|
4760
|
+
database: {
|
|
4761
|
+
title: 'DB 안전성', persona: '이중 결제·유실된 갱신·크래시 후 사라진 커밋으로 호출당해 본 DBA 겸 동시성·분산 리뷰어',
|
|
4762
|
+
titleEn: 'database safety', personaEn: 'a DBA and concurrency / distributed-systems reviewer',
|
|
4763
|
+
questions: [
|
|
4764
|
+
"값을 SELECT해서 앱 코드로 계산한 뒤 다시 UPDATE로 되쓰고 있지 않은가? — 두 요청이 같은 행을 동시에 읽으면 한쪽 갱신이 조용히 사라진다(lost update). 앱 계산을 없앤 원자적 조건 UPDATE(SET n = n + ?)나 버전 CAS(... WHERE version = ?, 영향 행이 0이면 성공이 아니라 충돌로 보고 재시도)로 바꿨는가?",
|
|
4765
|
+
"존재/유일성/한도/잔액 '검사'와 '쓰기'가 별개 문장으로 갈라져 있지 않은가? ('있으면 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)·부모/요약 행 잠금이 필요하다.",
|
|
4766
|
+
"함께 성공하거나 함께 실패해야 할 여러 쓰기(여러 행·여러 테이블)를 하나의 명시적 BEGIN/COMMIT으로 묶었고, 모든 실패 경로(예외·조기 return·타임아웃)에서 ROLLBACK이 보장되는가? — 문장별 autocommit이면 중간 실패 시 절반만 커밋된다(partial commit). 트랜잭션 내내 같은 커넥션이 쓰이는지, 그리고 BEGIN~COMMIT 사이에 외부 HTTP/RPC·사용자 대기를 두지 않아 트랜잭션이 짧게 유지되는지(오래 열리거나 idle-in-transaction이면 커넥션 풀이 고갈되고 MVCC 팽창·VACUUM 차단이 생기니 idle_in_transaction_session_timeout 등으로 방어)도 확인했는가?",
|
|
4767
|
+
"롤백 불가능한 부수효과(이메일·큐 발행·외부 HTTP)와 파생 데이터(집계·카운터·캐시·검색 인덱스)가 원본 커밋과 어긋날 수 있지 않은가? — 트랜잭션 안에서 부수효과를 내면 롤백돼도 유령 이메일이 나가고, 커밋 뒤 fire-and-forget면 커밋과 발행 사이 크래시에 유실된다(멱등키는 중복만 막지 이 유실은 못 막는다). 그래서 반드시 도착해야 하는 효과는 트랜잭셔널 아웃박스(같은 트랜잭션에 의도를 저장하고 커밋 뒤 릴레이가 at-least-once 발송)/내구 job 행/CDC로 보장하고, 명시적으로 best-effort인 효과만 멱등키로 보호된 '커밋 후 발행'으로 빼고, 파생 데이터는 같은 트랜잭션(또는 트리거/아웃박스/CDC)으로 원본과 원자적으로 묶은 뒤 원본에서 재계산하는 정합성 경로를 뒀는가?",
|
|
4768
|
+
"이 불변식(잔액>=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만 유니크할 때 쓰는 것).",
|
|
4769
|
+
"이 트랜잭션의 격리 수준을 의식적으로 골랐고, 데드락·직렬화 실패에 유한 재시도 루프(재시도 안에서 최신 상태 재조회, 백오프/지터, 재시도되는 트랜잭션엔 외부 부수효과가 없어 두 번 실행돼도 안전할 것)를 넣었는가? — 기본값에 그냥 맡기면 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 또는 명시적 잠금 필요) 합계·겹침 불변식엔 그 방어를 뒀고, 여러 행/테이블을 잠그는 경로는 늘 같은 순서로 잠그는가?",
|
|
4770
|
+
"사용자에게 '성공'을 돌려준 이 쓰기가 실제로 확인된 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를 명시했는가?",
|
|
4771
|
+
"동시성 방어를 인프로세스 뮤텍스·싱글턴·인메모리 dedup에 기대거나, 여러 서비스에 걸친 작업을 하나의 DB 트랜잭션으로 감쌀 수 있다고 착각하거나, 방금 프라이머리에 쓴 값을 지연된 리드 레플리카에서 읽고 있지 않은가? — 인스턴스가 2개 이상이면 인프로세스 락은 무력하니 공유 지점으로 옮기고(DB UNIQUE 제약·행 잠금·advisory lock, 큐 소비는 FOR UPDATE SKIP LOCKED), 원자성은 프로세스/네트워크 경계를 못 넘으니 크로스서비스는 아웃박스+saga+보상 트랜잭션으로, at-least-once 전달은 멱등키(UNIQUE)로, 복제 지연이 깨는 read-your-writes는 프라이머리 라우팅/LSN 확인으로 옮겼는가? 분산 락을 쓴다면 리스 만료 후 늦게 깨어난 작업이 최신 작업을 덮어쓰지 못하게 단조 증가 펜싱 토큰(DB 시퀀스/행 버전)을 발급하고 하위 자원이 낮은 토큰을 거부하게 했는가?"
|
|
4772
|
+
],
|
|
4773
|
+
questionsEn: [
|
|
4774
|
+
"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)?",
|
|
4775
|
+
"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.",
|
|
4776
|
+
"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)?",
|
|
4777
|
+
"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?",
|
|
4778
|
+
"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).",
|
|
4779
|
+
"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?",
|
|
4780
|
+
"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?",
|
|
4781
|
+
"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?"
|
|
4782
|
+
],
|
|
4783
|
+
affects: ['code', 'test'], affectsNote: '동시성 방어엔 인터리빙/부하/재시도/멀티인스턴스 실패를 재현하는 테스트가 따라와야 함(단일 인스턴스 초록불은 증거가 아님)',
|
|
4784
|
+
affectsNoteEn: 'a concurrency defense needs an interleaving/load/retry/multi-instance reproduction test'
|
|
4737
4785
|
}
|
|
4738
4786
|
};
|
|
4739
4787
|
// 1.19.3 (UR-0003 렌즈 완전판 v3): 프로젝트별 커스텀 렌즈 — .harness/quality-lenses.json 읽기-병합(쓰기 명령 없음, AI/사용자가 편집).
|
|
@@ -4776,8 +4824,13 @@ function _lensDomainsForFiles(files) {
|
|
|
4776
4824
|
if (has(/\.(md|mdx|rst|adoc|txt)$/i)) out.push('docs'); // 문서
|
|
4777
4825
|
if (has(/(^|[\\/])(test_[^\\/]+\.[a-z]+|[^\\/]+[._-]test\.[a-z]+|[^\\/]+\.spec\.[a-z]+)$|(^|[\\/])tests?[\\/]/i)) out.push('test'); // 테스트
|
|
4778
4826
|
if (has(/\.(js|mjs|cjs|ts|py|rb|go|rs|java|cs|php)$/i)) out.push('code'); // 실코드
|
|
4779
|
-
//
|
|
4780
|
-
|
|
4827
|
+
// DB 표면: 마이그레이션/스키마/ORM/DB 진입 파일 → database 렌즈(동시성/트랜잭션). 결정적 경로/확장자 매칭.
|
|
4828
|
+
// 테스트 파일(예: models/ 안의 *.test.js)은 test 렌즈를 유지하도록 database 매핑에서 제외.
|
|
4829
|
+
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;
|
|
4830
|
+
const _testRe = /(^|[\\/])(test_[^\\/]+\.[a-z]+|[^\\/]+[._-]test\.[a-z]+|[^\\/]+\.spec\.[a-z]+)$|(^|[\\/])tests?[\\/]/i;
|
|
4831
|
+
if (arr.some(f => typeof f === 'string' && _dbRe.test(f) && !_testRe.test(f))) out.push('database');
|
|
4832
|
+
// 순서: database 먼저(DB 파일이면 동시성/트랜잭션 질문을 인라인 노출), 그다음 code/design/docs/test. 중복 제거 + 최대 2개(클러터 방지).
|
|
4833
|
+
const ordered = ['database', 'code', 'design', 'docs', 'test'].filter(d => out.includes(d));
|
|
4781
4834
|
return ordered.slice(0, 2);
|
|
4782
4835
|
}
|
|
4783
4836
|
function lensCmd(domain, opts = {}) {
|
|
@@ -4851,7 +4904,7 @@ function commandsCmd(root) {
|
|
|
4851
4904
|
{ cmd: 'encoding check [path]', desc: '인코딩 검증' },
|
|
4852
4905
|
{ cmd: 'lazy detect [path] [--json]', desc: '게으른 작업 감지 (1.9.101)' },
|
|
4853
4906
|
{ 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)' },
|
|
4907
|
+
{ cmd: 'lens [code|design|docs|test|security|database] [--json]', desc: '분야별 자기질문 품질 렌즈 + 분야간 인과관계 (1.18.3, database: 동시성/트랜잭션)' },
|
|
4855
4908
|
{ cmd: 'optimism-check <T-ID>', desc: '낙관적 API 감지 (1.9.26)' },
|
|
4856
4909
|
{ cmd: 'requests audit|list|complete|drop|auto-complete', desc: '사용자 요청 추적 (1.9.207/223)' },
|
|
4857
4910
|
{ cmd: 'pre-wake-audit [path] [--last]', desc: 'sleep 전 점검 (1.9.209)' },
|
|
@@ -10675,7 +10728,7 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
10675
10728
|
// 확장자는 길이 내림차순(긴 것 먼저 매치) + \b 종결로 .ts vs .tscn 구분.
|
|
10676
10729
|
// 1.9.21: 설정/메타 파일 확장자 추가 — Godot export_presets.cfg 등 false negative 보완
|
|
10677
10730
|
// 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';
|
|
10731
|
+
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
10732
|
const FILE_RE = new RegExp(`(?:[A-Za-z][A-Za-z0-9_-]*\\/)?[A-Za-z][\\w./-]*\\.(?:${FILE_EXTS})\\b`, 'g');
|
|
10680
10733
|
const filePatterns = evidence.match(FILE_RE) || [];
|
|
10681
10734
|
// 중복 제거 + "tests/test.js" 같은 결과를 유지 (이미 `..` 없으니 그대로)
|
|
@@ -20433,7 +20486,7 @@ VERIFICATION (evidence-gated "done")
|
|
|
20433
20486
|
contract verify <spec.md> <impl.js> [--json] Spec <-> implementation match
|
|
20434
20487
|
verify-code [path] [--build] [--bench] Run tests/lint/typecheck, record evidence
|
|
20435
20488
|
gate [path] One-call CI gate: verify + audit + scan + encoding + lazy
|
|
20436
|
-
lens [code|design|docs|test|security] [--json] Quality self-question lenses
|
|
20489
|
+
lens [code|design|docs|test|security|database] [--json] Quality self-question lenses
|
|
20437
20490
|
|
|
20438
20491
|
SECURITY & HYGIENE
|
|
20439
20492
|
scan secrets [path] Committed-secret detection
|
|
@@ -20495,7 +20548,7 @@ function help() {
|
|
|
20495
20548
|
// 1.23.1 (UR-0010 Phase 6): 영어 opt-in 시 큐레이트 영어판. 기본(ko) 은 아래 한국어 help 그대로.
|
|
20496
20549
|
if (_uiLang(arg('--path', process.cwd())) === 'en') { _helpEn(); return; }
|
|
20497
20550
|
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 분야별 자기질문 품질 렌즈
|
|
20551
|
+
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
20552
|
leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
|
|
20500
20553
|
leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
|
|
20501
20554
|
leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
|
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 };
|