leerness 1.36.15 → 1.36.16
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 +8 -0
- package/README.md +4 -4
- package/bin/leerness.js +31 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.16 — 2026-07-13 — 게시본 1.36.15 클린룸 codex 9건 채택: database 렌즈 정직/정밀 + selftest·병합 하드닝
|
|
4
|
+
|
|
5
|
+
게시본 1.36.13~1.36.15 누적 신규 표면(문항 8→12, cap 16)을 codex 로 적대 리뷰 → 9건(P2/P3) 전부 재현/채택. clean 3축(i18n 패리티·앵커 유일성·크래시 없음).
|
|
6
|
+
|
|
7
|
+
- **문항 정직화 5건(KO=EN)**: (Q6) MVCC "reads don't block writes"를 non-locking 한정으로·스냅샷 스코프는 격리수준 의존·lost update 는 PG READ COMMITTED 노출이고 PG REPEATABLE READ는 조용한 유실 아닌 직렬화 abort로 정정; SQLITE_BUSY_SNAPSHOT은 busy_timeout 이 아니라 롤백+재시작. (Q9) **자기모순 수정** — abort 장벽이 "세대 카운터를 걷어낸다"고 썼으나, 카운터는 오히려 유지·단조증가해야 stale continuation 을 거부(제거하면 ABA 재유입 — deadlock 라운드 shipped 수정과 정합). (Q11) 단일 락만으론 데드락 사이클 불가(다른 락과 얽힐 때)·파티션 co-location≠순서(in-flight 1+순서보존 프로듀서+sink 시퀀스 필요). (Q12) 상태-조건 UPDATE 만으로 비순환 전이 직렬화; 버전 조건은 상태만으로 stale 구분 불가할 때만.
|
|
8
|
+
- **구조 4건**: (F6) 커스텀 병합 초과분 조용한 절단 → `_droppedCustom` 플래그 + lens 렌더 ⚠ 경고(내장 12는 앞이라 보존, 커스텀만 잘림). (F7) "병합이 12 안 자름" selftest 가 빈 custom 이라 slice 미실행(공허) → 넘치는 custom 주입해 내장12 보존+초과6 절단을 실제 검증. (F8) 단어 하나 앵커가 실질 문항 삭제를 못 잡음 → 문항별 다중절(clause) 판별로 강화(재현: 신 가드는 변이 포착, 구 가드는 놓침). (F9) selftest 이름 "11문항"→"12문항".
|
|
9
|
+
- **검증**: selftest 286/286, F3/F6/F8 프로브 재현확정, 게이트 e2e 386/386, 게시본 클린룸 재실증.
|
|
10
|
+
|
|
3
11
|
## 1.36.15 — 2026-07-13 — database 렌즈 11→12문항 + Q6 확장: 매트릭스 partial 6요소 승격
|
|
4
12
|
|
|
5
13
|
- **왜(37-코퍼스 매트릭스 후속)**: v2 매트릭스가 partial(문항이 인접 개념만 언급)로 정직 기록한 16요소 중 실무 빈도 최상위 두 클러스터를 문항으로 승격.
|
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.16 하네스를 사용합니다. 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.16는 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.15는 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.16)** · 매 라운드 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.16: 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.16';
|
|
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,22 +3894,27 @@ 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 안전성 렌즈 (코어 승격 + playbook 역이식 + 37코퍼스 매트릭스 갭 보강): database 도메인
|
|
3897
|
+
{ name: 'DB 안전성 렌즈 (코어 승격 + playbook 역이식 + 37코퍼스 매트릭스 갭 보강): database 도메인 12문항(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
|
+
// 각 문항을 다중 절(clause)로 고정 — 앵커 한 단어만 남긴 변이(codex F8)에서도 실패하도록 2개 이상 클러스터로 판별
|
|
3902
|
+
const anyKo = (...subs) => d.questions.some(q => subs.every(s => q.includes(s)));
|
|
3903
|
+
const anyEn = (...subs) => d.questionsEn.some(q => subs.every(s => q.includes(s)));
|
|
3901
3904
|
const catOk = !!d && d.questions.length === 12 && d.questionsEn.length === 12
|
|
3902
|
-
&& d.questions[0].includes('lost update') &&
|
|
3903
|
-
&&
|
|
3904
|
-
&&
|
|
3905
|
-
&&
|
|
3906
|
-
&&
|
|
3907
|
-
&&
|
|
3908
|
-
&&
|
|
3909
|
-
&&
|
|
3910
|
-
|
|
3911
|
-
&&
|
|
3912
|
-
|
|
3905
|
+
&& d.questions[0].includes('lost update') && anyKo('TOCTOU', 'UNIQUE')
|
|
3906
|
+
&& anyKo('in-doubt', '멱등키') && anyKo('SKIP LOCKED', '펜싱')
|
|
3907
|
+
&& anyKo('thenable', '등록', 'identity 가드') && anyEn('thenables', 'REGISTERING', 'identity guard') // 9번(콜백/등록/abort)
|
|
3908
|
+
&& anyKo('ABA', '식별자', '역방향 절도') && anyEn('ABA', 'identity', 'reverse theft') // 10번(라이터/토큰/검증)
|
|
3909
|
+
&& anyKo('hot row', 'backpressure', 'bulkhead') && anyEn('hot row', 'backpressure', 'bulkhead') // 11번
|
|
3910
|
+
&& anyKo('허용 목록', 'inbox', 'aggregate') && anyEn('allow-list', 'inbox', 'aggregate') // 12번(전이/순서/멱등)
|
|
3911
|
+
&& anyKo('dirty read', 'REPEATABLE READ', 'BUSY_SNAPSHOT') && anyEn('dirty read', 'REPEATABLE READ', 'BUSY_SNAPSHOT') // Q6 확장(정직화)
|
|
3912
|
+
&& anyKo('세대 카운터', '단조 증가') && anyEn('generation counter', 'must advance') // codex F3 회귀가드(제거X→증가)
|
|
3913
|
+
// 병합 cap 을 실제로 통과시켜(빈 custom 이 아니라 넘치는 custom) 내장 12 보존 + 초과 절단을 검증 (codex F7)
|
|
3914
|
+
&& (() => {
|
|
3915
|
+
const m = _mergeLensCatalog(LENS_CATALOG, { database: { questions: Array.from({ length: 10 }, (_, i) => `__probe_q${i}`) } }).database;
|
|
3916
|
+
return m.questions.length === 16 && d.questions.every(q => m.questions.includes(q)) && m._droppedCustom === 6;
|
|
3917
|
+
})()
|
|
3913
3918
|
&& d.affects.every(a => LENS_CATALOG[a]);
|
|
3914
3919
|
// DB 파일 → database 최우선(questions[0] 인라인 노출), DB+code → [database, code], 비-DB 코드 → 회귀 없음
|
|
3915
3920
|
const mapOk = eq(F(['db/migrations/001_init.sql']), ['database'])
|
|
@@ -4917,16 +4922,16 @@ const LENS_CATALOG = {
|
|
|
4917
4922
|
"함께 성공하거나 함께 실패해야 할 여러 쓰기(여러 행·여러 테이블)를 하나의 명시적 BEGIN/COMMIT으로 묶었고, 모든 실패 경로(예외·조기 return·타임아웃)에서 ROLLBACK이 보장되는가? — 문장별 autocommit이면 중간 실패 시 절반만 커밋된다(partial commit). 트랜잭션 내내 같은 커넥션이 쓰이는지, 그리고 BEGIN~COMMIT 사이에 외부 HTTP/RPC·사용자 대기를 두지 않아 트랜잭션이 짧게 유지되는지(오래 열리거나 idle-in-transaction이면 커넥션 풀이 고갈되고 MVCC 팽창·VACUUM 차단이 생기니 idle_in_transaction_session_timeout 등으로 방어)도 확인했는가?",
|
|
4918
4923
|
"롤백 불가능한 부수효과(이메일·큐 발행·외부 HTTP)와 파생 데이터(집계·카운터·캐시·검색 인덱스)가 원본 커밋과 어긋날 수 있지 않은가? — 트랜잭션 안에서 부수효과를 내면 롤백돼도 유령 이메일이 나가고, 커밋 뒤 fire-and-forget면 커밋과 발행 사이 크래시에 유실된다(멱등키는 중복만 막지 이 유실은 못 막는다). 그래서 반드시 도착해야 하는 효과는 트랜잭셔널 아웃박스(같은 트랜잭션에 의도를 저장하고 커밋 뒤 릴레이가 at-least-once 발송)/내구 job 행/CDC로 보장하고, 명시적으로 best-effort인 효과만 멱등키로 보호된 '커밋 후 발행'으로 빼고, 파생 데이터는 같은 트랜잭션(또는 트리거/아웃박스/CDC)으로 원본과 원자적으로 묶은 뒤 원본에서 재계산하는 정합성 경로를 뒀는가?",
|
|
4919
4924
|
"이 불변식(잔액>=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만 유니크할 때 쓰는 것).",
|
|
4920
|
-
"이 트랜잭션의 격리 수준을 의식적으로 골랐고, 데드락·직렬화 실패에 유한 재시도 루프(재시도 안에서 최신 상태 재조회, 백오프/지터, 재시도되는 트랜잭션엔 외부 부수효과가 없어 두 번 실행돼도 안전할 것)를 넣었는가? — 기본값에 그냥 맡기면 dirty read(READ UNCOMMITTED 급: 커밋 전 값을 읽고 상대가 롤백)·non-repeatable read·phantom·write skew가 샌다. MVCC
|
|
4925
|
+
"이 트랜잭션의 격리 수준을 의식적으로 골랐고, 데드락·직렬화 실패에 유한 재시도 루프(재시도 안에서 최신 상태 재조회, 백오프/지터, 재시도되는 트랜잭션엔 외부 부수효과가 없어 두 번 실행돼도 안전할 것)를 넣었는가? — 기본값에 그냥 맡기면 dirty read(READ UNCOMMITTED 급: 커밋 전 값을 읽고 상대가 롤백)·non-repeatable read·phantom·write skew가 샌다. MVCC 엔진의 '보통(non-locking) 읽기'는 대체로 쓰기를 안 막지만(잠금 읽기 FOR UPDATE는 막는다), 그게 자동 안전이 아니다 — 스냅샷 스코프는 격리수준에 달렸고(Postgres/MySQL READ COMMITTED는 명령마다 새 스냅샷, REPEATABLE READ는 트랜잭션 단위 스냅샷), 스냅샷 격리 ≠ 직렬화(write skew 통과)다. lost update 노출도 격리수준에 달렸다 — Postgres READ COMMITTED는 lost update 가 새지만, Postgres REPEATABLE READ는 같은 행 동시 갱신을 '조용히 유실'이 아니라 직렬화 실패로 abort 한다(기본값은 엔진마다 다름: Postgres는 보통 READ COMMITTED, MySQL InnoDB는 보통 REPEATABLE READ). 재시도할 에러코드도 엔진별로 다르니 확인했는가(Postgres 데드락 40P01·직렬화 40001; MySQL 데드락 40001/1213·락 대기 타임아웃 1205 — 1205는 기본적으로 문장만 롤백하니 트랜잭션 전체를 BEGIN부터 재시도하거나 innodb_rollback_on_timeout을 켤 것; SQLite는 재시도 가능한 SQLITE_BUSY엔 busy_timeout으로 대기하되 SQLITE_BUSY_SNAPSHOT은 WAL 스냅샷이 낡은 것이라 더 기다려도 안 되니 롤백 후 새 트랜잭션으로 재시작, 쓰기 트랜잭션엔 BEGIN IMMEDIATE로 예방). 그리고 COMMIT 도중 타임아웃/연결끊김으로 결과가 불확실(in-doubt)한 트랜잭션은 무조건 롤백으로 단정하지 말고 멱등키/트랜잭션ID/원본 재조회로 실제 반영 여부를 재확인한 뒤 재시도하는가? 스냅샷 격리/Postgres REPEATABLE READ만으론 write skew가 안 막히니(SERIALIZABLE 또는 명시적 잠금 필요) 합계·겹침 불변식엔 그 방어를 뒀고, 여러 행/테이블을 잠그는 경로는 늘 같은 순서로 잠그는가?",
|
|
4921
4926
|
"사용자에게 '성공'을 돌려준 이 쓰기가 실제로 확인된 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를 명시했는가?",
|
|
4922
4927
|
"동시성 방어를 인프로세스 뮤텍스·싱글턴·인메모리 dedup에 기대거나, 여러 서비스에 걸친 작업을 하나의 DB 트랜잭션으로 감쌀 수 있다고 착각하거나, 방금 프라이머리에 쓴 값을 지연된 리드 레플리카에서 읽고 있지 않은가? — 인스턴스가 2개 이상이면 인프로세스 락은 무력하니 공유 지점으로 옮기고(DB UNIQUE 제약·행 잠금·advisory lock, 큐 소비는 FOR UPDATE SKIP LOCKED), 원자성은 프로세스/네트워크 경계를 못 넘으니 크로스서비스는 아웃박스+saga+보상 트랜잭션으로, at-least-once 전달은 멱등키(UNIQUE)로, 복제 지연이 깨는 read-your-writes는 프라이머리 라우팅/LSN 확인으로 옮겼는가? 분산 락을 쓴다면 리스 만료 후 늦게 깨어난 작업이 최신 작업을 덮어쓰지 못하게 단조 증가 펜싱 토큰(DB 시퀀스/행 버전)을 발급하고 하위 자원이 낮은 토큰을 거부하게 했는가?",
|
|
4923
4928
|
// 9~10번: 9-라운드 현장 dogfood 회고(review-playbook)에서 역이식 — 그린 테스트가 놓치고 리뷰가 잡은 결함 패밀리.
|
|
4924
|
-
"트랜잭션·락·리스·single-flight 가 감싼 '사용자 콜백'이 promise 를 반환할 수 있지 않은가? — async 콜백은 첫 suspension 까지만 동기라, await 하지 않으면 그 뒤 continuation 이 COMMIT/해제 뒤 보호 밖에서 실행된다(아웃박스 릴레이가 async 발행 실패를 모른 채 sent=1로 마킹, 펜스 해제 뒤 늦은 쓰기). await 를 설계로 강제하거나 thenable 을 명시적으로 거부했는가? 진행 중 작업을 추적하는 구조(단일비행 맵·락 테이블·in-progress 레지스트리)는 사용자 코드가 실행되거나 동기 throw 하기 '전'에 등록을 마치는가 — 등록 전에 콜백이 던지면 실패 경로가 반초기화 상태를 보고 원본 에러가 삼켜지거나 죽은 엔트리가 키를 영구 오염시킨다. abort/해제는
|
|
4929
|
+
"트랜잭션·락·리스·single-flight 가 감싼 '사용자 콜백'이 promise 를 반환할 수 있지 않은가? — async 콜백은 첫 suspension 까지만 동기라, await 하지 않으면 그 뒤 continuation 이 COMMIT/해제 뒤 보호 밖에서 실행된다(아웃박스 릴레이가 async 발행 실패를 모른 채 sent=1로 마킹, 펜스 해제 뒤 늦은 쓰기). await 를 설계로 강제하거나 thenable 을 명시적으로 거부했는가? 진행 중 작업을 추적하는 구조(단일비행 맵·락 테이블·in-progress 레지스트리)는 사용자 코드가 실행되거나 동기 throw 하기 '전'에 등록을 마치는가 — 등록 전에 콜백이 던지면 실패 경로가 반초기화 상태를 보고 원본 에러가 삼켜지거나 죽은 엔트리가 키를 영구 오염시킨다. abort/해제는 임시 소유권(보유 락·대기열 엔트리·무효화 표식)은 걷어내되, 되레 유지·증가시켜야 할 것은 stale continuation 을 거부할 세대 카운터/톰스톤이다 — 걷다 만 대기열 엔트리는 죽은 작업에 락을 승계시키고, 반대로 세대 카운터를 '지워버리면' 재개된 작업이 같은 세대를 다시 받아 stale 인지 못 해 abort 장벽이 깨진다(카운터는 단조 증가). 세대 재검사 없는 중단된 획득 루프도 abort 뒤에 계속 획득한다. 그리고 정리는 identity 가드(맵의 엔트리가 여전히 '내 것'일 때만 삭제)로 후속 작업의 엔트리를 지우지 않는가?",
|
|
4925
4930
|
"버전 컬럼·단조 토큰·워터마크가 지키는 불변식에 '모든' 라이터가 참여하는가 — upsert·마이그레이션·시드·관리자 경로 중 하나만 우회해도(INSERT OR REPLACE 가 version 을 기본값으로 초기화, 평범한 대입이 토큰을 되감음) 낡은 스냅샷의 CAS 가 통과한다(ABA). 그 토큰/키는 식별자를 갖는가 — 클러스터/스토어 표식 없는 맨 숫자 LSN 은 무관한 스토어의 레플리카와도 '같음'으로 비교돼 남의 데이터를 노출하고, 객체 키는 identity 비교라 같은 내용의 {id:1}도 서로 다른 키다. 수량/금액 파라미터는 경계에서 검증되는가 — 음수 qty 는 stock >= qty 가드를 '통과'하면서 이체 방향을 뒤집는다(역방향 절도). 마지막으로 그 방어의 테스트가 구현에 아부하지 않는가 — 구현이 스스로 보고하는 라벨(source:'replica')이 아니라 분기된 데이터로 판별하고, no-op/naive 변이를 실제로 돌려 그 테스트가 '실패함'을 확인했으며, '언젠가 완료/거부된다'류 활성성 검증엔 timeout 이 있어 감지기가 무력화된 변이에서 스위트가 행(hang)이 아니라 실패로 끝나는가?",
|
|
4926
4931
|
// 11번: 37-코퍼스 렌즈 검증 매트릭스(lens-validation, 2026-07)가 측정한 커버리지 갭 — hot row/부하 계열.
|
|
4927
|
-
"쓰기가 한 행/키에 몰리지 않는가(인기 상품 재고·전역 카운터·선착순 쿠폰 — hot row)? — 단일 행 락 경합은
|
|
4932
|
+
"쓰기가 한 행/키에 몰리지 않는가(인기 상품 재고·전역 카운터·선착순 쿠폰 — hot row)? — 단일 행 락 경합은 대기·타임아웃·커넥션 풀 고갈로 번지고, 다른 락과 얽히면 데드락으로도 이어진다(단일 락 하나만으로 사이클은 안 생긴다). 정확한 실시간 값이 필요 없는 카운터(좋아요·조회수)는 샤딩(counter shard N개에 분산 후 합산)으로 흩고 — 단 정확성이 필요한 재고/잔액 차감엔 샤딩을 함부로 쓰지 마라 —, 경합이 심한 같은 키 작업은 큐 직렬화나 예약 패턴(임시 점유 + expires_at 만료 회수)으로 경합 자체를 줄였는가? 큐로 순서를 지키려면 파티션 co-location 만으론 부족하다 — 같은 키에 in-flight 소비자 1개 + 순서보존 프로듀서 설정(재시도 재정렬 금지) + sink 측 시퀀스/버전 검사까지 갖췄는가? 유입이 처리량을 넘어설 때의 backpressure 설계가 있는가 — 큐 상한·rate limit·워커/커넥션 풀 상한·기능별 풀 분리(bulkhead)로 흐름을 제어하지 않고 DB 락만으로 폭주를 받으면 장애가 시스템 전체로 전파된다.",
|
|
4928
4933
|
// 12번: 37-코퍼스 매트릭스 partial 승격 2차 — 상태 전이/이벤트 순서/소비 멱등(inbox) 계열.
|
|
4929
|
-
"상태(status)를 가진 행의 전이를 허용 목록으로 제한했는가 — 현재-상태 조건 없는 UPDATE ... SET status='REFUNDED' WHERE id=? 는 이미 취소된 주문을 환불하고 배송된 주문을 결제-전으로 되돌린다. 모든 전이는 WHERE status IN (허용 이전 상태)
|
|
4934
|
+
"상태(status)를 가진 행의 전이를 허용 목록으로 제한했는가 — 현재-상태 조건 없는 UPDATE ... SET status='REFUNDED' WHERE id=? 는 이미 취소된 주문을 환불하고 배송된 주문을 결제-전으로 되돌린다. 모든 전이는 WHERE status IN (허용 이전 상태) 조건으로 걸고 영향 행 0을 불법 전이/충돌로 처리하라 — 비순환 상태머신이면 이 원자적 조건 UPDATE 하나로 직렬화된다(SHIPPED WHERE status='PAID'). 현재 상태만으로 stale 명령을 구분 못 하거나(다른 필드가 관여, 상태가 순환) 할 땐 버전/시퀀스 조건을 추가하라. 이벤트를 발행/소비한다면 순서를 aggregate 별 시퀀스·버전으로 지키는가 — created_at 만 믿으면 '주문 취소'가 '배송 시작'보다 늦게 도착하는 역전에 깨진다(상태-조건 UPDATE 가 마지막 방어선). 소비자는 inbox(처리한 message_id 기록, 재수신 무시)로 멱등한가 — at-least-once 전달에서 중복은 정상 상황이고 exactly-once 는 좁은 조건에서만 성립하니 '중복이 온다'를 기본값으로 설계하라. 그리고 같은 멱등키인데 요청 본문이 다르면 기존 결과 재사용이 아니라 거부해야 한다."
|
|
4930
4935
|
],
|
|
4931
4936
|
questionsEn: [
|
|
4932
4937
|
"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)?",
|
|
@@ -4934,13 +4939,13 @@ const LENS_CATALOG = {
|
|
|
4934
4939
|
"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)?",
|
|
4935
4940
|
"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?",
|
|
4936
4941
|
"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).",
|
|
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
|
|
4942
|
+
"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. An MVCC engine's ordinary non-locking reads generally don't block writers (locking reads like FOR UPDATE do) — but that is not automatic safety: snapshot scope depends on isolation (READ COMMITTED takes a fresh snapshot per statement; REPEATABLE READ one per transaction), and snapshot isolation is NOT serializability (write skew passes). Lost-update exposure also depends on isolation — Postgres READ COMMITTED leaks it, but Postgres REPEATABLE READ aborts a same-row concurrent update as a serialization failure rather than silently losing it (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: wait on retryable SQLITE_BUSY with busy_timeout, but SQLITE_BUSY_SNAPSHOT means a stale WAL snapshot that waiting can't fix — roll back and restart the transaction from a fresh snapshot; use BEGIN IMMEDIATE preventively for write txns)? 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?",
|
|
4938
4943
|
"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?",
|
|
4939
4944
|
"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?",
|
|
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
|
|
4945
|
+
"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 transient ownership (held locks, queue entries, invalidation flags) while PRESERVING or monotonically advancing the generation counter / tombstone that rejects a stale continuation — a leftover queue entry hands the lock to a dead transaction, but conversely DELETING the generation counter lets a resuming operation be reassigned the same generation and fail to detect it's stale (the counter must advance, not reset). A suspended acquisition loop without a post-await generation re-check also 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?",
|
|
4941
4946
|
"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,
|
|
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)
|
|
4947
|
+
"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, and connection-pool exhaustion, and can contribute to a deadlock when it entangles with other locks (one contended lock alone can't form a cycle). 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 or a reservation pattern (temporary hold + expires_at reclamation). Same-key partitioning only co-locates work — serial processing also needs a single in-flight consumer per key, an ordering-preserving producer config (no reordering on retry), and a sink-side sequence/version check. 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.",
|
|
4948
|
+
"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) and treat 0 affected rows as an illegal transition or conflict — for an acyclic state machine that atomic conditional UPDATE alone serializes it (SHIPPED WHERE status='PAID'). Add a version/sequence condition when the current state alone can't distinguish a stale or concurrent command (other fields participate, or states can cycle). 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."
|
|
4944
4949
|
],
|
|
4945
4950
|
affects: ['code', 'test'], affectsNote: '동시성 방어엔 인터리빙/부하/재시도/멀티인스턴스 실패를 재현하는 테스트가 따라와야 함(단일 인스턴스 초록불은 증거가 아님)',
|
|
4946
4951
|
affectsNoteEn: 'a concurrency defense needs an interleaving/load/retry/multi-instance reproduction test'
|
|
@@ -4966,10 +4971,13 @@ function _mergeLensCatalog(builtin, custom) {
|
|
|
4966
4971
|
if (out[k]) {
|
|
4967
4972
|
const seen = new Set(out[k].questions);
|
|
4968
4973
|
for (const q of cq) if (!seen.has(q)) { out[k].questions.push(q); seen.add(q); }
|
|
4969
|
-
|
|
4974
|
+
const before = out[k].questions.length;
|
|
4975
|
+
out[k].questions = out[k].questions.slice(0, 16); // 내장이 앞에 있어 커스텀이 tail 에서 절단됨(내장 보존)
|
|
4976
|
+
if (before > 16) out[k]._droppedCustom = before - 16; // codex F6: 조용한 절단 대신 가시화 (렌더가 경고)
|
|
4970
4977
|
out[k]._customAdded = cq.length > 0;
|
|
4971
4978
|
} else if (cq.length) {
|
|
4972
4979
|
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 };
|
|
4980
|
+
if (cq.length > 16) out[k]._droppedCustom = cq.length - 16;
|
|
4973
4981
|
}
|
|
4974
4982
|
}
|
|
4975
4983
|
return out;
|
|
@@ -5064,6 +5072,7 @@ function lensCmd(domain, opts = {}) {
|
|
|
5064
5072
|
let qs = l.questions;
|
|
5065
5073
|
if (L === 'en' && l.questionsEn) qs = l.questions.length > l.questionsEn.length ? [...l.questionsEn, ...l.questions.slice(l.questionsEn.length)] : l.questionsEn;
|
|
5066
5074
|
log(`## ${key} (${title}) — ${t('페르소나', 'persona')}: ${persona}${cflag}`);
|
|
5075
|
+
if (l._droppedCustom) log(t(` ⚠ 프로젝트 질문 ${l._droppedCustom}개가 12+커스텀 상한(16)을 넘어 잘림 — 핵심 질문을 앞쪽에 두거나 별도 도메인으로 분리하세요`, ` ⚠ ${l._droppedCustom} project question(s) dropped past the 12+custom cap (16) — put key ones first or use a separate domain`));
|
|
5067
5076
|
qs.forEach((q, i) => log(` ${i + 1}. ${q}`));
|
|
5068
5077
|
log(t(` ↔ 인과: ${key} 를 바꾸면 → ${(l.affects || []).join(', ') || '(없음)'} 질문도 다시 — ${note}`, ` ↔ causality: change ${key} → re-check ${(l.affects || []).join(', ') || '(none)'} too — ${note}`));
|
|
5069
5078
|
}
|