leerness 1.36.13 → 1.36.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +4 -4
- package/bin/leerness.js +22 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.15 — 2026-07-13 — database 렌즈 11→12문항 + Q6 확장: 매트릭스 partial 6요소 승격
|
|
4
|
+
|
|
5
|
+
- **왜(37-코퍼스 매트릭스 후속)**: v2 매트릭스가 partial(문항이 인접 개념만 언급)로 정직 기록한 16요소 중 실무 빈도 최상위 두 클러스터를 문항으로 승격.
|
|
6
|
+
- **Q6 확장(KO+EN)**: dirty read(READ UNCOMMITTED 급) 명시 + MVCC/스냅샷 가시성("자기 시점 스냅샷, 읽기가 쓰기를 안 막음 — 그러나 스냅샷 격리 ≠ 직렬화, lost update 잔존") → 2-1 MVCC·2-2 Snapshot Isolation·격리 이상 열거 승격.
|
|
7
|
+
- **추가 12번(KO+EN)**: 상태 전이 허용 목록(현재-상태+버전 조건 UPDATE, 0행=불법 전이), aggregate 별 시퀀스 순서(created_at 불신, 역전 이벤트), inbox 소비 멱등(중복=정상, exactly-once 는 좁은 조건), 멱등키 본문 불일치 거부 → 2-10 State Machine·2-16 Exactly/At-least-once·2-17 Ordering·2-21 Inbox 승격.
|
|
8
|
+
- **병합 cap 12→16**: 내장 12문항 절단 방지 (slice 2곳).
|
|
9
|
+
- **selftest**: 12문항(KO=EN) + 신규 앵커(허용 목록+inbox / allow-list+inbox / dirty read) + 병합 무절단 가드 12.
|
|
10
|
+
- **잔여 partial(정직)**: 전파(2-11)·락 입도(2-3)·데드락 감지 내부(2-5)·2PC(2-20)·서킷브레이커(2-34)·bulkhead 풀 격리(2-35)·선형화(2-29)·최종적 정합성 수렴(2-15)·구분 개념(2-36) 등은 advisory 렌즈 범위 밖으로 유지 — kit 체크리스트가 담당.
|
|
11
|
+
|
|
12
|
+
## 1.36.14 — 2026-07-13 — database 렌즈 10→11문항: 37-코퍼스 검증 매트릭스가 측정한 커버리지 갭(hot row/부하 계열) 보강
|
|
13
|
+
|
|
14
|
+
- **왜(사용자 지시)**: leerness 설치 프로젝트(lens-validation, 신규)에서 로컬 SQLite 로 37개 코퍼스 요소를 14개 실행형 픽스처로 재현하고, 요소별 렌즈 작동을 행위 테스트(설치된 CLI 로 task→done→verify-claim 트리거 + lens 실렌더 문항 근거)로 측정 — 트리거 14/14 ✓·음성 대조(비-DB 파일) 무발화 ✓·redis-only 캐시 파일 트리거 ✓(_DB_IMPORT_RE 의 redis/ioredis). 유일한 측정 갭: **hot row·sharded counter·큐 직렬화·backpressure·bulkhead 5개 요소를 다루는 문항이 0개**.
|
|
15
|
+
- **추가 11번(KO+EN)**: hot row 락 경합의 전파(대기→타임아웃→데드락→풀 고갈), 비정확 카운터의 샤딩(+재고/잔액엔 샤딩 금지 주의), 같은 키 경합의 큐 직렬화·예약 패턴(expires_at 회수), 유입>처리량 시 backpressure(큐 상한·rate limit·워커/풀 상한·bulkhead).
|
|
16
|
+
- **selftest**: database 렌즈 케이스 11문항(KO=EN) + hot row/backpressure/bulkhead 앵커(EN 동형) + 병합 cap(12) 무절단 가드 갱신.
|
|
17
|
+
- **검증**: selftest 286/286 + 매트릭스 재실행으로 f13 클러스터 '작동' 전환 확인(게시 후 lens-validation 에서 재실증). 정직 한계: 렌즈는 advisory 자기질문이며, 문항 존재 = 자동 감지가 아니다.
|
|
18
|
+
|
|
3
19
|
## 1.36.13 — 2026-07-13 — database 렌즈 8→10문항: 9라운드 dogfood playbook 역이식
|
|
4
20
|
|
|
5
21
|
- **왜**: stock-service 에서 코퍼스 9개 동시성 개념을 실구현-리뷰-재현 루프로 dogfood 한 결과(회고: db-safety-lens/review-playbook.md), 기존 8문항이 다루지 않는 결함 패밀리가 반복 확인됨 — 전부 "그린 테스트는 통과했는데 리뷰가 잡은" 실사건 기반.
|
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.15 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
129
129
|
|
|
130
130
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
131
131
|
|
|
@@ -179,7 +179,7 @@ leerness memory restore decision <date|title>
|
|
|
179
179
|
|
|
180
180
|
### MCP server (외부 AI 통합)
|
|
181
181
|
|
|
182
|
-
Leerness v1.36.
|
|
182
|
+
Leerness v1.36.15는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
183
183
|
|
|
184
184
|
```jsonc
|
|
185
185
|
// 카테고리별
|
|
@@ -200,7 +200,7 @@ Leerness v1.36.13는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
200
200
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
201
201
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
|
|
202
202
|
|
|
203
|
-
현재 누적: **70 라운드 (1.9.40 → 1.36.
|
|
203
|
+
현재 누적: **70 라운드 (1.9.40 → 1.36.15)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
204
204
|
|
|
205
205
|
### 성능 가이드 (1.9.140 측정)
|
|
206
206
|
|
|
@@ -238,6 +238,6 @@ leerness release pack --close --auto-main-push
|
|
|
238
238
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
239
239
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
240
240
|
|
|
241
|
-
Last synced by Leerness v1.36.
|
|
241
|
+
Last synced by Leerness v1.36.15: 2026-07-13
|
|
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.15';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -3894,17 +3894,22 @@ 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 안전성 렌즈 (코어 승격 +
|
|
3897
|
+
{ name: 'DB 안전성 렌즈 (코어 승격 + playbook 역이식 + 37코퍼스 매트릭스 갭 보강): database 도메인 11문항(KO=EN) + DB파일 매핑 인라인 + 비-DB 회귀0 (행위)', run: () => {
|
|
3898
3898
|
const F = _lensDomainsForFiles;
|
|
3899
3899
|
const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
3900
3900
|
const d = LENS_CATALOG.database;
|
|
3901
|
-
const catOk = !!d && d.questions.length ===
|
|
3901
|
+
const catOk = !!d && d.questions.length === 12 && d.questionsEn.length === 12
|
|
3902
3902
|
&& d.questions[0].includes('lost update') && d.questions.some(q => q.includes('TOCTOU'))
|
|
3903
3903
|
&& d.questions.some(q => q.includes('in-doubt')) && d.questions.some(q => q.includes('SKIP LOCKED'))
|
|
3904
3904
|
&& d.questions.some(q => q.includes('thenable')) // playbook 역이식 9번(콜백 async 이탈+등록/abort 장벽)
|
|
3905
3905
|
&& d.questions.some(q => q.includes('ABA')) // playbook 역이식 10번(전-라이터 참여+토큰 식별자+테스트 정직성)
|
|
3906
|
+
&& d.questions.some(q => q.includes('hot row') && q.includes('backpressure') && q.includes('bulkhead')) // 11번(37코퍼스 매트릭스 갭: hot row/샤딩/큐 직렬화/backpressure/bulkhead)
|
|
3906
3907
|
&& d.questionsEn.some(q => q.includes('thenables')) && d.questionsEn.some(q => q.includes('ABA'))
|
|
3907
|
-
&&
|
|
3908
|
+
&& d.questionsEn.some(q => q.includes('hot row') && q.includes('bulkhead'))
|
|
3909
|
+
&& d.questions.some(q => q.includes('허용 목록') && q.includes('inbox')) // 12번(상태 전이+순서+inbox 멱등)
|
|
3910
|
+
&& d.questionsEn.some(q => q.includes('allow-list') && q.includes('inbox'))
|
|
3911
|
+
&& d.questions.some(q => q.includes('dirty read')) // Q6 확장(격리 이상 열거+MVCC 가시성)
|
|
3912
|
+
&& _mergeLensCatalog(LENS_CATALOG, {}).database.questions.length === 12 // 병합 cap(16)이 내장 12문항을 자르지 않음
|
|
3908
3913
|
&& d.affects.every(a => LENS_CATALOG[a]);
|
|
3909
3914
|
// DB 파일 → database 최우선(questions[0] 인라인 노출), DB+code → [database, code], 비-DB 코드 → 회귀 없음
|
|
3910
3915
|
const mapOk = eq(F(['db/migrations/001_init.sql']), ['database'])
|
|
@@ -4912,12 +4917,16 @@ const LENS_CATALOG = {
|
|
|
4912
4917
|
"함께 성공하거나 함께 실패해야 할 여러 쓰기(여러 행·여러 테이블)를 하나의 명시적 BEGIN/COMMIT으로 묶었고, 모든 실패 경로(예외·조기 return·타임아웃)에서 ROLLBACK이 보장되는가? — 문장별 autocommit이면 중간 실패 시 절반만 커밋된다(partial commit). 트랜잭션 내내 같은 커넥션이 쓰이는지, 그리고 BEGIN~COMMIT 사이에 외부 HTTP/RPC·사용자 대기를 두지 않아 트랜잭션이 짧게 유지되는지(오래 열리거나 idle-in-transaction이면 커넥션 풀이 고갈되고 MVCC 팽창·VACUUM 차단이 생기니 idle_in_transaction_session_timeout 등으로 방어)도 확인했는가?",
|
|
4913
4918
|
"롤백 불가능한 부수효과(이메일·큐 발행·외부 HTTP)와 파생 데이터(집계·카운터·캐시·검색 인덱스)가 원본 커밋과 어긋날 수 있지 않은가? — 트랜잭션 안에서 부수효과를 내면 롤백돼도 유령 이메일이 나가고, 커밋 뒤 fire-and-forget면 커밋과 발행 사이 크래시에 유실된다(멱등키는 중복만 막지 이 유실은 못 막는다). 그래서 반드시 도착해야 하는 효과는 트랜잭셔널 아웃박스(같은 트랜잭션에 의도를 저장하고 커밋 뒤 릴레이가 at-least-once 발송)/내구 job 행/CDC로 보장하고, 명시적으로 best-effort인 효과만 멱등키로 보호된 '커밋 후 발행'으로 빼고, 파생 데이터는 같은 트랜잭션(또는 트리거/아웃박스/CDC)으로 원본과 원자적으로 묶은 뒤 원본에서 재계산하는 정합성 경로를 뒀는가?",
|
|
4914
4919
|
"이 불변식(잔액>=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만 유니크할 때 쓰는 것).",
|
|
4915
|
-
"이 트랜잭션의 격리 수준을 의식적으로 골랐고, 데드락·직렬화 실패에 유한 재시도 루프(재시도 안에서 최신 상태 재조회, 백오프/지터, 재시도되는 트랜잭션엔 외부 부수효과가 없어 두 번 실행돼도 안전할 것)를 넣었는가? — 기본값에 그냥 맡기면 non-repeatable read·phantom·write skew가
|
|
4920
|
+
"이 트랜잭션의 격리 수준을 의식적으로 골랐고, 데드락·직렬화 실패에 유한 재시도 루프(재시도 안에서 최신 상태 재조회, 백오프/지터, 재시도되는 트랜잭션엔 외부 부수효과가 없어 두 번 실행돼도 안전할 것)를 넣었는가? — 기본값에 그냥 맡기면 dirty read(READ UNCOMMITTED 급: 커밋 전 값을 읽고 상대가 롤백)·non-repeatable read·phantom·write skew가 샌다. MVCC/스냅샷 엔진은 트랜잭션이 '자기 시점 스냅샷'을 읽어 읽기가 쓰기를 안 막지만, 그게 자동 안전이 아니다 — 스냅샷 격리 ≠ 직렬화(write skew 통과)고 lost update 도 그대로 남는다(기본값은 엔진마다 다름: 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 또는 명시적 잠금 필요) 합계·겹침 불변식엔 그 방어를 뒀고, 여러 행/테이블을 잠그는 경로는 늘 같은 순서로 잠그는가?",
|
|
4916
4921
|
"사용자에게 '성공'을 돌려준 이 쓰기가 실제로 확인된 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를 명시했는가?",
|
|
4917
4922
|
"동시성 방어를 인프로세스 뮤텍스·싱글턴·인메모리 dedup에 기대거나, 여러 서비스에 걸친 작업을 하나의 DB 트랜잭션으로 감쌀 수 있다고 착각하거나, 방금 프라이머리에 쓴 값을 지연된 리드 레플리카에서 읽고 있지 않은가? — 인스턴스가 2개 이상이면 인프로세스 락은 무력하니 공유 지점으로 옮기고(DB UNIQUE 제약·행 잠금·advisory lock, 큐 소비는 FOR UPDATE SKIP LOCKED), 원자성은 프로세스/네트워크 경계를 못 넘으니 크로스서비스는 아웃박스+saga+보상 트랜잭션으로, at-least-once 전달은 멱등키(UNIQUE)로, 복제 지연이 깨는 read-your-writes는 프라이머리 라우팅/LSN 확인으로 옮겼는가? 분산 락을 쓴다면 리스 만료 후 늦게 깨어난 작업이 최신 작업을 덮어쓰지 못하게 단조 증가 펜싱 토큰(DB 시퀀스/행 버전)을 발급하고 하위 자원이 낮은 토큰을 거부하게 했는가?",
|
|
4918
4923
|
// 9~10번: 9-라운드 현장 dogfood 회고(review-playbook)에서 역이식 — 그린 테스트가 놓치고 리뷰가 잡은 결함 패밀리.
|
|
4919
4924
|
"트랜잭션·락·리스·single-flight 가 감싼 '사용자 콜백'이 promise 를 반환할 수 있지 않은가? — async 콜백은 첫 suspension 까지만 동기라, await 하지 않으면 그 뒤 continuation 이 COMMIT/해제 뒤 보호 밖에서 실행된다(아웃박스 릴레이가 async 발행 실패를 모른 채 sent=1로 마킹, 펜스 해제 뒤 늦은 쓰기). await 를 설계로 강제하거나 thenable 을 명시적으로 거부했는가? 진행 중 작업을 추적하는 구조(단일비행 맵·락 테이블·in-progress 레지스트리)는 사용자 코드가 실행되거나 동기 throw 하기 '전'에 등록을 마치는가 — 등록 전에 콜백이 던지면 실패 경로가 반초기화 상태를 보고 원본 에러가 삼켜지거나 죽은 엔트리가 키를 영구 오염시킨다. abort/해제는 그 작업의 흔적 전부(보유 락·대기열 엔트리·무효화 표식·세대 카운터)를 걷어내는 장벽인가 — 걷다 만 대기열 엔트리는 죽은 작업에 락을 승계시키고, 세대 재검사 없는 중단된 획득 루프는 abort 뒤에도 계속 획득한다. 그리고 정리는 identity 가드(맵의 엔트리가 여전히 '내 것'일 때만 삭제)로 후속 작업의 엔트리를 지우지 않는가?",
|
|
4920
|
-
"버전 컬럼·단조 토큰·워터마크가 지키는 불변식에 '모든' 라이터가 참여하는가 — upsert·마이그레이션·시드·관리자 경로 중 하나만 우회해도(INSERT OR REPLACE 가 version 을 기본값으로 초기화, 평범한 대입이 토큰을 되감음) 낡은 스냅샷의 CAS 가 통과한다(ABA). 그 토큰/키는 식별자를 갖는가 — 클러스터/스토어 표식 없는 맨 숫자 LSN 은 무관한 스토어의 레플리카와도 '같음'으로 비교돼 남의 데이터를 노출하고, 객체 키는 identity 비교라 같은 내용의 {id:1}도 서로 다른 키다. 수량/금액 파라미터는 경계에서 검증되는가 — 음수 qty 는 stock >= qty 가드를 '통과'하면서 이체 방향을 뒤집는다(역방향 절도). 마지막으로 그 방어의 테스트가 구현에 아부하지 않는가 — 구현이 스스로 보고하는 라벨(source:'replica')이 아니라 분기된 데이터로 판별하고, no-op/naive 변이를 실제로 돌려 그 테스트가 '실패함'을 확인했으며, '언젠가 완료/거부된다'류 활성성 검증엔 timeout 이 있어 감지기가 무력화된 변이에서 스위트가 행(hang)이 아니라 실패로 끝나는가?"
|
|
4925
|
+
"버전 컬럼·단조 토큰·워터마크가 지키는 불변식에 '모든' 라이터가 참여하는가 — upsert·마이그레이션·시드·관리자 경로 중 하나만 우회해도(INSERT OR REPLACE 가 version 을 기본값으로 초기화, 평범한 대입이 토큰을 되감음) 낡은 스냅샷의 CAS 가 통과한다(ABA). 그 토큰/키는 식별자를 갖는가 — 클러스터/스토어 표식 없는 맨 숫자 LSN 은 무관한 스토어의 레플리카와도 '같음'으로 비교돼 남의 데이터를 노출하고, 객체 키는 identity 비교라 같은 내용의 {id:1}도 서로 다른 키다. 수량/금액 파라미터는 경계에서 검증되는가 — 음수 qty 는 stock >= qty 가드를 '통과'하면서 이체 방향을 뒤집는다(역방향 절도). 마지막으로 그 방어의 테스트가 구현에 아부하지 않는가 — 구현이 스스로 보고하는 라벨(source:'replica')이 아니라 분기된 데이터로 판별하고, no-op/naive 변이를 실제로 돌려 그 테스트가 '실패함'을 확인했으며, '언젠가 완료/거부된다'류 활성성 검증엔 timeout 이 있어 감지기가 무력화된 변이에서 스위트가 행(hang)이 아니라 실패로 끝나는가?",
|
|
4926
|
+
// 11번: 37-코퍼스 렌즈 검증 매트릭스(lens-validation, 2026-07)가 측정한 커버리지 갭 — hot row/부하 계열.
|
|
4927
|
+
"쓰기가 한 행/키에 몰리지 않는가(인기 상품 재고·전역 카운터·선착순 쿠폰 — hot row)? — 단일 행 락 경합은 대기·타임아웃·데드락·커넥션 풀 고갈로 번진다. 정확한 실시간 값이 필요 없는 카운터(좋아요·조회수)는 샤딩(counter shard N개에 분산 후 합산)으로 흩고 — 단 정확성이 필요한 재고/잔액 차감엔 샤딩을 함부로 쓰지 마라 —, 경합이 심한 같은 키 작업은 큐 직렬화(같은 키 → 같은 파티션 순차 처리)나 예약 패턴(임시 점유 + expires_at 만료 회수)으로 경합 자체를 줄였는가? 유입이 처리량을 넘어설 때의 backpressure 설계가 있는가 — 큐 상한·rate limit·워커/커넥션 풀 상한·기능별 풀 분리(bulkhead)로 흐름을 제어하지 않고 DB 락만으로 폭주를 받으면 장애가 시스템 전체로 전파된다.",
|
|
4928
|
+
// 12번: 37-코퍼스 매트릭스 partial 승격 2차 — 상태 전이/이벤트 순서/소비 멱등(inbox) 계열.
|
|
4929
|
+
"상태(status)를 가진 행의 전이를 허용 목록으로 제한했는가 — 현재-상태 조건 없는 UPDATE ... SET status='REFUNDED' WHERE id=? 는 이미 취소된 주문을 환불하고 배송된 주문을 결제-전으로 되돌린다. 모든 전이는 WHERE status IN (허용 이전 상태) + 버전/시퀀스 조건으로 걸고, 영향 행 0을 불법 전이/충돌로 처리하라. 이벤트를 발행/소비한다면 순서를 aggregate 별 시퀀스·버전으로 지키는가 — created_at 만 믿으면 '주문 취소'가 '배송 시작'보다 늦게 도착하는 역전에 깨진다(상태-조건 UPDATE 가 마지막 방어선). 소비자는 inbox(처리한 message_id 기록, 재수신 무시)로 멱등한가 — at-least-once 전달에서 중복은 정상 상황이고 exactly-once 는 좁은 조건에서만 성립하니 '중복이 온다'를 기본값으로 설계하라. 그리고 같은 멱등키인데 요청 본문이 다르면 기존 결과 재사용이 아니라 거부해야 한다."
|
|
4921
4930
|
],
|
|
4922
4931
|
questionsEn: [
|
|
4923
4932
|
"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)?",
|
|
@@ -4925,11 +4934,13 @@ const LENS_CATALOG = {
|
|
|
4925
4934
|
"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)?",
|
|
4926
4935
|
"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?",
|
|
4927
4936
|
"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).",
|
|
4928
|
-
"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?",
|
|
4937
|
+
"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 dirty reads (READ UNCOMMITTED class: reading a value the writer then rolls back), non-repeatable reads, phantoms, or write skew. MVCC/snapshot engines let each transaction read its own point-in-time snapshot so reads don't block writes — but that is not automatic safety: snapshot isolation is NOT serializability (write skew passes) and lost updates remain (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?",
|
|
4929
4938
|
"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?",
|
|
4930
4939
|
"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?",
|
|
4931
4940
|
"Can a user callback wrapped by a transaction/lock/lease/single-flight return a promise? An async callback is synchronous only until its first suspension — if the returned promise isn't awaited, its continuations run after COMMIT/release, outside the protection (an outbox relay marking sent=1 before an async publish rejects; a fenced write landing after release). Did I await by design, or explicitly reject thenables? Does every 'track this operation' structure (single-flight map, lock table, in-progress registry) finish REGISTERING before user code can run or throw synchronously — a callback that throws pre-registration leaves the failure path staring at half-initialized state (the real error swallowed, a dead entry poisoning the key forever)? Is abort/release a real barrier that removes EVERY trace of the operation (held locks, queue entries, invalidation flags, generation counters) — a leftover queue entry hands the lock to a dead transaction, and a suspended acquisition loop without a post-await generation re-check keeps acquiring after the abort. And does cleanup use an identity guard (delete only if the map entry is still MINE) so it never removes a successor's entry?",
|
|
4932
|
-
"Does EVERY writer participate in the invariant that a version column / monotonic token / watermark protects — one bypassing path (an INSERT OR REPLACE resetting version to its default, a plain assignment rolling a token backward, a migration/seed/admin write) lets a stale snapshot's CAS pass (ABA). Does the token/key carry identity — a bare numeric LSN with no cluster/store branding compares 'equal' against an unrelated store's replica and exposes foreign data, and object keys compare by identity so two {id:1} objects are different keys. Are quantity/amount parameters validated at the boundary — a negative qty PASSES a stock >= qty guard while inverting the transfer's direction (reverse theft). Finally, do the tests for these defenses avoid flattering the implementation — assert on divergent data rather than a self-reported label (source:'replica'), actually run the no-op/naive mutant and watch the test FAIL it, and put timeouts on 'eventually completes/rejects' liveness checks so a disabled detector fails the suite instead of hanging it?"
|
|
4941
|
+
"Does EVERY writer participate in the invariant that a version column / monotonic token / watermark protects — one bypassing path (an INSERT OR REPLACE resetting version to its default, a plain assignment rolling a token backward, a migration/seed/admin write) lets a stale snapshot's CAS pass (ABA). Does the token/key carry identity — a bare numeric LSN with no cluster/store branding compares 'equal' against an unrelated store's replica and exposes foreign data, and object keys compare by identity so two {id:1} objects are different keys. Are quantity/amount parameters validated at the boundary — a negative qty PASSES a stock >= qty guard while inverting the transfer's direction (reverse theft). Finally, do the tests for these defenses avoid flattering the implementation — assert on divergent data rather than a self-reported label (source:'replica'), actually run the no-op/naive mutant and watch the test FAIL it, and put timeouts on 'eventually completes/rejects' liveness checks so a disabled detector fails the suite instead of hanging it?",
|
|
4942
|
+
"Are writes converging on one hot row/key (a popular product's stock, a global counter, a first-come coupon)? Single-row lock contention cascades into waits, timeouts, deadlocks, and connection-pool exhaustion. For counters that don't need an exact real-time value (likes, views), spread the load with sharding (N counter-shard rows summed on read) — but don't casually shard stock/balance decrements that need exactness; for heavily contended same-key work, reduce the contention itself with queue-based serialization (same key → same partition, processed in order) or a reservation pattern (temporary hold + expires_at reclamation). And when inflow exceeds throughput, is backpressure designed in — queue bounds, rate limits, worker/connection-pool caps, per-feature pool isolation (bulkhead)? Absorbing a surge with DB locks alone propagates the failure system-wide.",
|
|
4943
|
+
"Are the transitions of a status-bearing row restricted to an allow-list — an UPDATE ... SET status='REFUNDED' WHERE id=? with no current-state condition refunds an already-cancelled order and drags a shipped one back to pre-payment. Gate every transition with WHERE status IN (allowed prior states) plus a version/sequence condition, and treat 0 affected rows as an illegal transition or a conflict. If you publish/consume events, is ordering kept per aggregate via a sequence/version — trusting created_at alone breaks when 'order cancelled' arrives after 'shipping started' (the state-conditioned UPDATE is the last line of defense). Is the consumer idempotent via an inbox (record processed message_ids, ignore re-deliveries) — under at-least-once delivery duplicates are NORMAL, and exactly-once holds only under narrow conditions, so design for 'duplicates will come'. And if the same idempotency key arrives with a different request body, reject it instead of replaying the stored result."
|
|
4933
4944
|
],
|
|
4934
4945
|
affects: ['code', 'test'], affectsNote: '동시성 방어엔 인터리빙/부하/재시도/멀티인스턴스 실패를 재현하는 테스트가 따라와야 함(단일 인스턴스 초록불은 증거가 아님)',
|
|
4935
4946
|
affectsNoteEn: 'a concurrency defense needs an interleaving/load/retry/multi-instance reproduction test'
|
|
@@ -4945,7 +4956,7 @@ function _loadProjectLenses(root) {
|
|
|
4945
4956
|
return (j && typeof j === 'object' && j.domains && typeof j.domains === 'object') ? j.domains : {};
|
|
4946
4957
|
} catch { return {}; }
|
|
4947
4958
|
}
|
|
4948
|
-
// 내장 + 프로젝트 커스텀 병합. 기존 도메인 → 질문 추가(dedup, 최대
|
|
4959
|
+
// 내장 + 프로젝트 커스텀 병합. 기존 도메인 → 질문 추가(dedup, 최대 16 — 내장 최대치(database 12) + 커스텀 여유; 낮으면 내장이 잘림); 신규 도메인 → 기본값으로 추가. _custom 플래그로 표시.
|
|
4949
4960
|
function _mergeLensCatalog(builtin, custom) {
|
|
4950
4961
|
const out = {};
|
|
4951
4962
|
for (const [k, v] of Object.entries(builtin || {})) out[k] = Object.assign({}, v, { questions: (v.questions || []).slice(), affects: (v.affects || []).slice() });
|
|
@@ -4955,10 +4966,10 @@ function _mergeLensCatalog(builtin, custom) {
|
|
|
4955
4966
|
if (out[k]) {
|
|
4956
4967
|
const seen = new Set(out[k].questions);
|
|
4957
4968
|
for (const q of cq) if (!seen.has(q)) { out[k].questions.push(q); seen.add(q); }
|
|
4958
|
-
out[k].questions = out[k].questions.slice(0,
|
|
4969
|
+
out[k].questions = out[k].questions.slice(0, 16);
|
|
4959
4970
|
out[k]._customAdded = cq.length > 0;
|
|
4960
4971
|
} else if (cq.length) {
|
|
4961
|
-
out[k] = { title: (c.title || k), persona: (c.persona || '검토자'), questions: cq.slice(0,
|
|
4972
|
+
out[k] = { title: (c.title || k), persona: (c.persona || '검토자'), questions: cq.slice(0, 16), affects: Array.isArray(c.affects) ? c.affects : [], affectsNote: c.affectsNote || '(프로젝트 정의)', _custom: true };
|
|
4962
4973
|
}
|
|
4963
4974
|
}
|
|
4964
4975
|
return out;
|