leerness 1.36.108 → 1.36.109
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/README.md +4 -4
- package/bin/leerness.js +69 -10
- package/lib/analyzers.js +9 -3
- package/package.json +1 -1
- package/scripts/e2e.js +99 -0
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ MIT
|
|
|
122
122
|
<!-- leerness:project-readme:start -->
|
|
123
123
|
## Leerness Project Harness
|
|
124
124
|
|
|
125
|
-
이 프로젝트는 Leerness v1.36.
|
|
125
|
+
이 프로젝트는 Leerness v1.36.109 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
126
126
|
|
|
127
127
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
128
128
|
|
|
@@ -176,7 +176,7 @@ leerness memory restore decision <date|title>
|
|
|
176
176
|
|
|
177
177
|
### MCP server (외부 AI 통합)
|
|
178
178
|
|
|
179
|
-
Leerness v1.36.
|
|
179
|
+
Leerness v1.36.109는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
|
|
180
180
|
|
|
181
181
|
```jsonc
|
|
182
182
|
// 카테고리별
|
|
@@ -197,7 +197,7 @@ Leerness v1.36.108는 stdio JSON-RPC MCP server를 내장합니다 — Claude Co
|
|
|
197
197
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
198
198
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
199
199
|
|
|
200
|
-
현재 누적: **v1.9.x → 1.36.
|
|
200
|
+
현재 누적: **v1.9.x → 1.36.109 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
201
201
|
|
|
202
202
|
### 성능 가이드
|
|
203
203
|
|
|
@@ -235,5 +235,5 @@ leerness release pack --close --auto-main-push
|
|
|
235
235
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
236
236
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
237
237
|
|
|
238
|
-
Last synced by Leerness v1.36.
|
|
238
|
+
Last synced by Leerness v1.36.109: 2026-08-10
|
|
239
239
|
<!-- leerness:project-readme:end -->
|
package/bin/leerness.js
CHANGED
|
@@ -34,7 +34,7 @@ const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE
|
|
|
34
34
|
const { tokenizeForRank: _tokenizeForRank, expandQuery: _expandQuery, scoreHits: _scoreHits, suggestTerms: _suggestTerms } = require('../lib/search-core'); // 1.36.23: memory search 랭킹 코어(순수·0-deps)
|
|
35
35
|
const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
|
|
36
36
|
|
|
37
|
-
const VERSION = '1.36.
|
|
37
|
+
const VERSION = '1.36.109';
|
|
38
38
|
|
|
39
39
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
40
40
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -5178,10 +5178,17 @@ function _selfTestCases() {
|
|
|
5178
5178
|
&& E('module.exports = require("./x"); void 0;\n') === false;
|
|
5179
5179
|
return base && bypass && real && noop && noopFP;
|
|
5180
5180
|
} },
|
|
5181
|
-
{ name: '위장 스텁 차단 (1.18.2):
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5181
|
+
{ name: '위장 스텁 차단 (1.18.2 · 1.36.109 행위전환): 빈껍데기 판정이 실제로 동작 + 스텁 루프가 그 판정을 쓴다', run: () => {
|
|
5182
|
+
// 1.36.109: 종전엔 스텁 루프의 호출 줄을 **정확 리터럴**로 붙잡았다. 이번 라운드에 그 줄을 리팩터하자
|
|
5183
|
+
// (마커 파일 판정을 끼워야 했다) 리터럴이 사라져 메타가드가 즉시 잡았다 — 자기참조 소스가드가 공허해지는 형태다.
|
|
5184
|
+
// 표현이 아니라 **동작**을 본다.
|
|
5185
|
+
// ⚠ 배선(루프가 이 판정을 실제로 호출하는가)은 **여기서 소스로 확인하지 않는다**: 그 정규식을 쓰자
|
|
5186
|
+
// 바로 위 설명 주석이 스스로를 만족시켜 변이가 생존했다(자체 변이 시험이 잡았다 — 같은 함정의 재생산).
|
|
5187
|
+
// 배선은 e2e 블록 M 이 행위로 지킨다(0바이트 파일 → stub-impl). 배선을 끊으면 그 블록이 실패한다.
|
|
5188
|
+
const shells = ['', ' \n\n ', '/' + '/ TODO\n' + '/' + '/ later\n', '"use strict"; module.exports = {};\n', '/* c */\nvoid 0;\n'];
|
|
5189
|
+
const reals = ['function f(){return 1;}\nmodule.exports={f};\n', 'def f():\n return 1\n', 'export const x = 1;\nexport function g(){ return x; }\n'];
|
|
5190
|
+
const judged = shells.every(s => _vcImplIsEmpty(s) === true) && reals.every(s => _vcImplIsEmpty(s) === false);
|
|
5191
|
+
return judged && read(__filename).includes('비주석 코드 0줄 또는 빈 export 껍데기');
|
|
5185
5192
|
} },
|
|
5186
5193
|
{ name: '품질 렌즈 (1.18.3): 카탈로그 무결성 — 사용자 원문 질문 + affects 상호참조 유효 (행위)', run: () => {
|
|
5187
5194
|
const keys = Object.keys(LENS_CATALOG);
|
|
@@ -6423,7 +6430,7 @@ function _selfTestCases() {
|
|
|
6423
6430
|
return clar && shape && noDbUrl && reqOk && guards && uiOk && _p0013LibraryOk() && _p0088CurrentStatePreserved()
|
|
6424
6431
|
&& _p0101SurfaceOk() && _p0101SkillIdOk() && _p0101PathStrictOk() && _p0102HonestyOk()
|
|
6425
6432
|
&& _p0103FlagsOk() && _p0103JsonOk() && _p0103RootOk() && _p0104UsageOk()
|
|
6426
|
-
&& _p0014SecretBaselineOk() && _p0104ReminderOk() && _p0015ModeOk() && _p0097AsyncLockOk();
|
|
6433
|
+
&& _p0014SecretBaselineOk() && _p0104ReminderOk() && _p0015ModeOk() && _p0097AsyncLockOk() && _p0109ClaimGateOk();
|
|
6427
6434
|
} },
|
|
6428
6435
|
{ name: '시크릿 스캐너 F-06 (1.36.56, 외부감사): 무명 확장자 소형 텍스트 스캔(이진 NUL 제외) + 같은 토큰 중복 보고 dedupe — 행위검사', run: () => {
|
|
6429
6436
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_sc56_'));
|
|
@@ -8101,6 +8108,28 @@ function _p0015ModeOk() {
|
|
|
8101
8108
|
// 순수 호출로 계약을 고정한다: 프로미스가 끝나기 전에는 락 파일이 살아 있어야 하고, 끝나면 사라져야 한다.
|
|
8102
8109
|
// selftest 는 `c.run() === true` 만 통과시키므로(프로미스는 자동 실패) **동기 thenable** 로 같은 분기를
|
|
8103
8110
|
// 결정적으로 밟는다 — `typeof out.then === 'function'` 이라는 판별 조건이 동일하다.
|
|
8111
|
+
// 1.36.109 (완료게이트 사냥): 두 결함을 **순수 호출**로 고정한다. 둘 다 양방향으로 단언한다 —
|
|
8112
|
+
// 막아야 할 것을 막는지, 그리고 통과해야 할 것이 계속 통과하는지. false-BLOCK 은 false-PASS 보다 나쁘다.
|
|
8113
|
+
function _p0109ClaimGateOk() {
|
|
8114
|
+
try {
|
|
8115
|
+
const an = require('../lib/analyzers');
|
|
8116
|
+
// ① 빈 파일은 가장 명백한 스텁이다. 종전엔 `!body` 때문에 "스텁 아님" 이었고, 공백 3자보다 0바이트가 더 쉽게 통과했다.
|
|
8117
|
+
const emptyIsStub = _vcImplIsEmpty('') === true && _vcImplIsEmpty(' \n\n ') === true;
|
|
8118
|
+
const realIsNot = _vcImplIsEmpty('function f(){return 1;}\nmodule.exports={f};\n') === false
|
|
8119
|
+
&& _vcImplIsEmpty('def f():\n return 1\n') === false; // 대조군 — 실코드는 스텁이 아니다
|
|
8120
|
+
const badInput = _vcImplIsEmpty(null) === false && _vcImplIsEmpty(undefined) === false; // 문자열 아닌 입력은 판정하지 않는다
|
|
8121
|
+
// ② 밑줄로 시작하는 파일명이 **잘리지 않고 통째로** 추출돼야 한다. 잘리면 없는 파일을 찾아 정직한 주장을 거부한다.
|
|
8122
|
+
const grab = (s) => (String(s).match(an._EVIDENCE_FILE_RE()) || [])[0] || null;
|
|
8123
|
+
const underscore = grab('src/_helpers.js 구현 완료') === 'src/_helpers.js'
|
|
8124
|
+
&& grab('pages/_app.tsx 수정') === 'pages/_app.tsx'
|
|
8125
|
+
&& grab('pkg/__init__.py 추가') === 'pkg/__init__.py'
|
|
8126
|
+
&& grab('_private/util.ts 정리') === '_private/util.ts';
|
|
8127
|
+
const plainStill = grab('src/normal.js 구현') === 'src/normal.js' // 대조군 — 기존 인식 유지
|
|
8128
|
+
&& grab('ops/containers/Dockerfile 작성') === 'ops/containers/Dockerfile'
|
|
8129
|
+
&& grab('.github/workflows/ci.yml 갱신') === '.github/workflows/ci.yml';
|
|
8130
|
+
return emptyIsStub && realIsNot && badInput && underscore && plainStill;
|
|
8131
|
+
} catch { return false; }
|
|
8132
|
+
}
|
|
8104
8133
|
function _p0097AsyncLockOk() {
|
|
8105
8134
|
const os2 = require('os');
|
|
8106
8135
|
const arena = fs.mkdtempSync(path.join(os2.tmpdir(), 'leerness-p97lk-'));
|
|
@@ -15165,7 +15194,12 @@ const _VC_EMPTY_SHELL_RE = new RegExp(
|
|
|
15165
15194
|
].join('|') + ')(?:\\s+as\\s+[A-Za-z0-9_$.<>\\[\\] ]+?)?\\s*;?$' // 선택적 TS 캐스트(as any) + ;
|
|
15166
15195
|
);
|
|
15167
15196
|
function _vcImplIsEmpty(body) {
|
|
15168
|
-
|
|
15197
|
+
// 1.36.109 (완료게이트 사냥): 빈 문자열은 **가장 명백한 스텁**인데 `!body` 가 그걸 "스텁 아님" 으로 만들었다.
|
|
15198
|
+
// 공백 3자짜리 파일은 stub-impl 로 막히는데 0바이트는 통과했다 — 같은 것이 더 비어 있을수록 통과하는 역전이다.
|
|
15199
|
+
// (`touch src/x.js` + evidence "src/x.js 구현 완료" → verify-claim 통과, 실측.)
|
|
15200
|
+
// 읽기 실패는 호출부가 catch 로 걸러 여기 오지 않는다. 빈 본문은 곧 빈 구현이다.
|
|
15201
|
+
if (typeof body !== 'string') return false;
|
|
15202
|
+
if (body === '') return true;
|
|
15169
15203
|
// 블록주석 제거 → 줄별 trim/주석줄 제거 → 인라인 // 주석 제거(따옴표 없는 줄만, 문자열 보호) → join.
|
|
15170
15204
|
const codeLines = body.replace(/\/\*[\s\S]*?\*\//g, '').split('\n').map(l => {
|
|
15171
15205
|
let t = l.trim();
|
|
@@ -15271,6 +15305,12 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
15271
15305
|
for (const m of evidence.matchAll(FILE_RE)) {
|
|
15272
15306
|
const prev = m.index > 0 ? evidence[m.index - 1] : '';
|
|
15273
15307
|
if (prev === '\\' || prev === '/') continue; // suffix-of-longer-path(UNC \\srv\..., C:\..., URL) → skip. ':' 는 "파일:src/x.js" 정상 케이스라 스킵 안 함
|
|
15308
|
+
// 1.36.109 (codex 검수 P2, 재현됨): 구분자만 보던 가드에 두 구멍이 있었다 —
|
|
15309
|
+
// `https://…/docs#_anchor.js` 의 URL 조각과 `C:_anchor.js` 의 드라이브 상대 경로가 **루트 상대 파일 주장**이 됐다.
|
|
15310
|
+
// (밑줄 확장 전에도 같은 형태로 `anchor.js` 가 통과했다 — 내가 만든 클래스는 아니지만 여기서 닫는다.)
|
|
15311
|
+
// `:` 를 통째로 막으면 "파일:src/x.js" 같은 정상 표기가 죽으므로, **앞이 단일 영문자인 `:` 만** 드라이브로 본다.
|
|
15312
|
+
if (prev === '#') continue; // URL 조각
|
|
15313
|
+
if (prev === ':' && /^[A-Za-z]$/.test(evidence[m.index - 2] || '')) continue; // C:name (드라이브 상대)
|
|
15274
15314
|
filePatterns.push(m[0].replace(/\\/g, '/'));
|
|
15275
15315
|
}
|
|
15276
15316
|
// 중복 제거 + "tests/test.js" 같은 결과를 유지 (이미 `..` 없으니 그대로)
|
|
@@ -15317,15 +15357,34 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
15317
15357
|
// 1.17.3 (UR-0046 범용성 P1②): 빈껍데기(스텁) 구현 + 테스트-구현 연결 검사 — "주석뿐 구현 + assert(true) 테스트"가 verify-claim 을 exit 0 으로 통과하던 공격(5축 실증 Attack C) 차단.
|
|
15318
15358
|
// ① 스텁: 주장된 코드 파일(테스트 제외)의 비주석 코드줄이 0 이면 확정 스텁 — done 게이팅 FAIL(확실 신호만, 과탐 0).
|
|
15319
15359
|
// ② 연결: 주장에 구현+테스트가 모두 있는데 어떤 테스트도 구현 파일명(basename)을 참조하지 않으면 — 기본 advisory ⚠, --strict-claims 시 FAIL.
|
|
15320
|
-
|
|
15360
|
+
// 1.36.109 (codex 검수 P2): `pyi` 추가 — 여기에 없으면 위의 마커 예외가 **도달 불가**다(광고만 하고 안 도는 코드).
|
|
15361
|
+
const _VC_CODE_EXT = /\.(js|mjs|cjs|jsx|ts|tsx|pyi|py|rb|go|rs|java|cs|php)$/i;
|
|
15321
15362
|
const _VC_TEST_PAT = /(^|[\\/])(test_[^\\/]+\.[a-z]+|[^\\/]+[._-]test\.[a-z]+|[^\\/]+\.spec\.[a-z]+)$|(^|[\\/])tests?[\\/]/i;
|
|
15322
15363
|
const stubFiles = [];
|
|
15364
|
+
// 1.36.109 (codex 검수 P1, 재현됨): 관례상 비어 있는 **마커 파일**은 스텁으로 몰면 안 되지만,
|
|
15365
|
+
// 그것만으로 "구현했다" 가 성립해서도 안 된다. 처음엔 무조건 예외로 뒀더니 빈 `pkg/__init__.py` **하나만**
|
|
15366
|
+
// 인용한 done 이 exit 0 을 받았다(검수가 재현) — 내가 막겠다고 고친 바로 그 형태를 예외가 되살린 것이다.
|
|
15367
|
+
// 그래서 예외는 "다른 실질 파일이 함께 인용됐을 때만" 적용한다. 판정은 루프 뒤로 미룬다.
|
|
15368
|
+
const markerFiles = [];
|
|
15369
|
+
const _isPkgMarker = (f) => /(^|[\\/])__init__\.pyi?$/.test(f);
|
|
15323
15370
|
for (const c of fileChecks) {
|
|
15324
15371
|
if (!c.exists || !_VC_CODE_EXT.test(c.file) || _VC_TEST_PAT.test(c.file)) continue;
|
|
15325
15372
|
let body = ''; try { body = read(path.join(root, c.file)); } catch { continue; }
|
|
15326
|
-
|
|
15373
|
+
// 1.36.109: `!body` 를 스킵 조건에 두면 0바이트 파일이 스텁 검사를 통째로 건너뛴다(가드 두 겹이 서로를 가림 —
|
|
15374
|
+
// 여기와 `_vcImplIsEmpty` 양쪽에 같은 falsy 함정이 있었고 한 곳만 고치면 구멍이 남는다). 크기 상한만 남긴다.
|
|
15375
|
+
if (body.length > 512 * 1024) continue;
|
|
15327
15376
|
// 1.18.2: 코드 0줄(기존) + 빈 export 껍데기(위장 스텁) 통합 판정.
|
|
15328
|
-
if (_vcImplIsEmpty(body))
|
|
15377
|
+
if (!_vcImplIsEmpty(body)) continue;
|
|
15378
|
+
if (_isPkgMarker(c.file)) { markerFiles.push(c.file); continue; } // 판정 보류 — 아래에서 결정
|
|
15379
|
+
stubFiles.push(c.file);
|
|
15380
|
+
}
|
|
15381
|
+
// 빈 마커를 봐 주는 조건: 같이 인용된 **비어 있지 않은** 코드 파일이 하나라도 있어야 한다.
|
|
15382
|
+
// (파이썬 패키지 추가는 보통 `__init__.py` + 실제 모듈이 함께 온다 — 그 정직한 형태만 통과시킨다.)
|
|
15383
|
+
if (markerFiles.length) {
|
|
15384
|
+
const substantive = fileChecks.some(c =>
|
|
15385
|
+
c.exists && _VC_CODE_EXT.test(c.file) && !_VC_TEST_PAT.test(c.file) && !_isPkgMarker(c.file)
|
|
15386
|
+
&& !stubFiles.includes(c.file));
|
|
15387
|
+
if (!substantive) for (const f of markerFiles) stubFiles.push(f);
|
|
15329
15388
|
}
|
|
15330
15389
|
const _vcImpl = fileChecks.filter(c => c.exists && _VC_CODE_EXT.test(c.file) && !_VC_TEST_PAT.test(c.file)).map(c => c.file);
|
|
15331
15390
|
const _vcTests = fileChecks.filter(c => c.exists && _VC_CODE_EXT.test(c.file) && _VC_TEST_PAT.test(c.file)).map(c => c.file);
|
package/lib/analyzers.js
CHANGED
|
@@ -40,16 +40,22 @@ function _shellGuardAnalyze(cmd, ctx) {
|
|
|
40
40
|
// 실측으로 확인된 사각지대: 다단계 경로의 확장자 없는 파일(ops/containers/Dockerfile),
|
|
41
41
|
// 선두 점 디렉토리(.github/workflows/ci.yml → github/… 로 잘림), 점파일(.gitignore),
|
|
42
42
|
// 그리고 Dockerfile.dev 가 Dockerfile 로 잘려 "없는 파일" 이 되던 것.
|
|
43
|
-
|
|
43
|
+
// 1.36.109 (codex 검수 P2): `pyi` 추가 — 타입 스텁(PEP 561)은 실제 산출물인데 추출조차 안 돼
|
|
44
|
+
// `pkg/__init__.pyi` 만 인용한 정직한 주장이 "파일 0건" 으로 거부됐다. 길이 내림차순 규칙상 `py` 앞에 둔다.
|
|
45
|
+
const _VC_FILE_EXTS = 'webmanifest|properties|tscn|tres|godot|json5|prisma|swift|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|vue|svelte|pyi|md|js|ts|gd|cs|py|rb|go|rs|kt|sh|cpp|c|h';
|
|
44
46
|
// 확장자 없는 표준 파일명 — 임의의 대문자 단어가 아니라 정확한 철자만(대소문자 구분)으로 FP 억제.
|
|
45
47
|
const _VC_BARE_FILES = 'Dockerfile|Containerfile|Makefile|Gemfile|Procfile|Rakefile|Jenkinsfile|Vagrantfile|Caddyfile|Brewfile|Justfile|CODEOWNERS|LICENCE|LICENSE|NOTICE';
|
|
46
48
|
// 점파일 — 확장자 규칙으로는 잡히지 않지만 명백한 파일 참조.
|
|
47
49
|
const _VC_DOTFILES = '\\.(?:gitignore|gitattributes|gitmodules|dockerignore|npmrc|npmignore|nvmrc|editorconfig|prettierrc|eslintrc|babelrc|browserslistrc)';
|
|
48
50
|
// 경로 접두: 0개 이상 세그먼트(각 세그먼트는 선두 점 허용) — `ops/containers/`, `.github/workflows/` 모두 포함.
|
|
49
|
-
|
|
51
|
+
// 1.36.109 (완료게이트 사냥): 첫 글자에 `_` 가 없어서 `src/_helpers.js` 가 `helpers.js` 로 **잘려서** 추출됐다 —
|
|
52
|
+
// "파일 없음" 이 아니라 **없는 파일을 찾아** files-missing 으로 정직한 주장을 거부했다(실측).
|
|
53
|
+
// 흔한 이름들이 여기 걸린다: Next.js `pages/_app.tsx` · `_document.tsx` · 파이썬 `__init__.py` · `_private/…`.
|
|
54
|
+
// false-BLOCK 은 false-PASS 보다 나쁘다 — 사용자는 이유 없이 막히고 게이트를 끄게 된다.
|
|
55
|
+
const _VC_PREFIX = '(?:\\.?[A-Za-z0-9_][A-Za-z0-9._-]*[\\/\\\\])*';
|
|
50
56
|
// basename: (a) 확장자 파일 (b) 표준 bare 파일명(뒤에 .확장자가 오면 (a)에 양보) (c) 점파일
|
|
51
57
|
// bare 파일은 관례적 접미사를 허용한다(Dockerfile.dev / Makefile.local) — 없으면 그 주장이 "파일 0건"이 된다.
|
|
52
|
-
const _VC_BASE = `(?:[A-Za-z0-
|
|
58
|
+
const _VC_BASE = `(?:[A-Za-z0-9_][\\w.-]*\\.(?:${_VC_FILE_EXTS})\\b|(?:${_VC_BARE_FILES})(?:\\.[A-Za-z0-9_-]+)?(?![\\w.-])|${_VC_DOTFILES}(?![\\w-]))`;
|
|
53
59
|
function _EVIDENCE_FILE_RE(flags = '') { return new RegExp(`${_VC_PREFIX}${_VC_BASE}`, flags); }
|
|
54
60
|
|
|
55
61
|
function _evidenceQuality(evidence) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.109",
|
|
4
4
|
"description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
package/scripts/e2e.js
CHANGED
|
@@ -10358,6 +10358,105 @@ total++;
|
|
|
10358
10358
|
if (!ok) failed++;
|
|
10359
10359
|
}
|
|
10360
10360
|
|
|
10361
|
+
// ── 1.36.109 블록 M: 완료 게이트 — "done 에는 증거가 필요하다" 는 제품의 이름값이다.
|
|
10362
|
+
// 적대 사냥에서 두 방향으로 뚫려 있었다(대조군 2/2 통과 상태에서 측정 — 전부 거부하는 고장난 게이트가 아니었다):
|
|
10363
|
+
// · false-PASS: `touch src/x.js` 후 "src/x.js 구현 완료" 로 done 이 통과했다.
|
|
10364
|
+
// 공백 3자짜리 파일은 stub-impl 로 막히는데 **0바이트는 통과** — 더 비어 있을수록 쉽게 통과하는 역전이었다.
|
|
10365
|
+
// 원인은 `!body` 를 스킵 조건에 둔 것이고, 같은 falsy 함정이 호출부와 `_vcImplIsEmpty` **두 곳**에 있어
|
|
10366
|
+
// 한 곳만 고치면 구멍이 남았다(가드가 서로를 가리는 형태).
|
|
10367
|
+
// · false-BLOCK: `src/_helpers.js` 를 인용하면 추출기가 `helpers.js` 로 **잘라서** 없는 파일을 찾았다.
|
|
10368
|
+
// Next.js `pages/_app.tsx`, 파이썬 `__init__.py` 처럼 흔한 이름이 전부 여기 걸렸다. 이유 없이 막히면
|
|
10369
|
+
// 사용자는 게이트를 끈다 — false-BLOCK 이 false-PASS 보다 나쁘다.
|
|
10370
|
+
// 두 방향을 한 블록에서 함께 단언한다. 한쪽만 보면 "전부 거부" 나 "전부 통과" 로 도망갈 수 있다.
|
|
10371
|
+
{
|
|
10372
|
+
total++;
|
|
10373
|
+
let ok = false; const dbg = {};
|
|
10374
|
+
const sb = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-claim109-'));
|
|
10375
|
+
try {
|
|
10376
|
+
const ENV = Object.assign({}, process.env, { TMPDIR: sb, TEMP: sb, TMP: sb });
|
|
10377
|
+
let seq = 0;
|
|
10378
|
+
const mk = (setup) => {
|
|
10379
|
+
const d = path.join(sb, 'p' + (++seq));
|
|
10380
|
+
fs.mkdirSync(d, { recursive: true });
|
|
10381
|
+
fs.writeFileSync(path.join(d, 'package.json'), '{"name":"p","version":"0.1.0"}');
|
|
10382
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { cwd: d, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
10383
|
+
setup(d); return d;
|
|
10384
|
+
};
|
|
10385
|
+
const R = (d, a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { cwd: d, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
10386
|
+
const verdict = (d, evidence) => {
|
|
10387
|
+
const id = (String(R(d, ['task', 'add', '주장']).stdout || '').match(/T-\d{4,}/) || [])[0];
|
|
10388
|
+
R(d, ['task', 'update', id, '--status', 'done', '--evidence', evidence]);
|
|
10389
|
+
const r = R(d, ['verify-claim', id, '--json']);
|
|
10390
|
+
let j = null; try { j = JSON.parse(String(r.stdout || '')); } catch {}
|
|
10391
|
+
return { ok: j ? j.ok : null, reasons: j ? (j.reasons || []) : [] };
|
|
10392
|
+
};
|
|
10393
|
+
const W = (d, rel, body) => { const p = path.join(d, rel); fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, body); };
|
|
10394
|
+
|
|
10395
|
+
// ① 0바이트 = 스텁 (그리고 공백만도 여전히 스텁 — 둘이 같은 판정이어야 한다)
|
|
10396
|
+
const dEmpty = mk(d => W(d, 'src/empty.js', ''));
|
|
10397
|
+
const vEmpty = verdict(dEmpty, 'src/empty.js 구현 완료');
|
|
10398
|
+
const dBlank = mk(d => W(d, 'src/blank.js', ' \n\n '));
|
|
10399
|
+
const vBlank = verdict(dBlank, 'src/blank.js 구현 완료');
|
|
10400
|
+
dbg.emptyBlocked = vEmpty.ok === false && vEmpty.reasons.includes('stub-impl');
|
|
10401
|
+
dbg.blankBlocked = vBlank.ok === false && vBlank.reasons.includes('stub-impl');
|
|
10402
|
+
|
|
10403
|
+
// ② 대조군 — 실제 구현은 계속 통과해야 한다(게이트가 전부 거부하면 이 블록은 무의미하다)
|
|
10404
|
+
const dReal = mk(d => W(d, 'src/real.js', 'function f(){return 1;}\nmodule.exports={f};\n'));
|
|
10405
|
+
dbg.realPasses = verdict(dReal, 'src/real.js 구현 완료').ok === true;
|
|
10406
|
+
|
|
10407
|
+
// ③ 밑줄 시작 파일명이 잘리지 않는다 — 실존하는데 files-missing 이 나면 정직한 주장의 오차단
|
|
10408
|
+
const dUs = mk(d => W(d, 'src/_helpers.js', 'function h(){return 1;}\nmodule.exports={h};\n'));
|
|
10409
|
+
const vUs = verdict(dUs, 'src/_helpers.js 구현 완료');
|
|
10410
|
+
dbg.underscoreOk = vUs.ok === true && !vUs.reasons.includes('files-missing');
|
|
10411
|
+
const dNext = mk(d => W(d, 'pages/_app.tsx', 'export default function App(){ return null; }\n'));
|
|
10412
|
+
const vNext = verdict(dNext, 'pages/_app.tsx 수정 완료');
|
|
10413
|
+
dbg.nextjsOk = vNext.ok === true && !vNext.reasons.includes('files-missing');
|
|
10414
|
+
// 디렉토리 이름이 `_` 로 시작하는 경우도 같은 결함이다 — 경로 접두와 basename 은 별개 문자 클래스라
|
|
10415
|
+
// 한쪽만 고치면 나머지가 남는다(변이 시험에서 basename 만 되돌렸을 때 이 케이스는 통과했다).
|
|
10416
|
+
const dPriv = mk(d => W(d, '_private/util.ts', 'export function u(){ return 1; }\n'));
|
|
10417
|
+
const vPriv = verdict(dPriv, '_private/util.ts 정리 완료');
|
|
10418
|
+
dbg.privDirOk = vPriv.ok === true && !vPriv.reasons.includes('files-missing');
|
|
10419
|
+
|
|
10420
|
+
// ④ 관례상 빈 파일(__init__.py)은 스텁으로 막지 않는다 — 단, **다른 실질 파일과 함께일 때만**.
|
|
10421
|
+
const dPy = mk(d => { W(d, 'pkg/__init__.py', ''); W(d, 'pkg/core.py', 'def f():\n return 1\n'); });
|
|
10422
|
+
const vPy = verdict(dPy, 'pkg/__init__.py, pkg/core.py 구현 완료');
|
|
10423
|
+
dbg.initPyExempt = vPy.ok === true;
|
|
10424
|
+
// ④-b codex 검수 P1(재현됨): 마커 **단독**은 구현 증거가 아니다. 처음 판에서는 무조건 예외라
|
|
10425
|
+
// 빈 `__init__.py` 하나만 인용한 done 이 통과했고, 이 블록은 ④가 core.py 를 함께 인용한 탓에 못 잡았다.
|
|
10426
|
+
// 가드가 놓친 이유가 "조건을 안 만들어서" 였다 — 조건을 만든다.
|
|
10427
|
+
const dPyOnly = mk(d => W(d, 'pkg/__init__.py', ''));
|
|
10428
|
+
const vPyOnly = verdict(dPyOnly, 'pkg/__init__.py 구현 완료');
|
|
10429
|
+
dbg.markerOnlyBlocked = vPyOnly.ok === false && vPyOnly.reasons.includes('stub-impl');
|
|
10430
|
+
// ④-c 타입 스텁(PEP 561)도 같은 규칙 — 인식되어야 예외가 도달한다(안 그러면 광고만 하는 코드).
|
|
10431
|
+
const dPyi = mk(d => { W(d, 'pkg/__init__.pyi', ''); W(d, 'pkg/core.pyi', 'def f() -> int: ...\n'); });
|
|
10432
|
+
const vPyi = verdict(dPyi, 'pkg/__init__.pyi, pkg/core.pyi 타입 스텁 추가');
|
|
10433
|
+
dbg.pyiRecognized = vPyi.ok === true;
|
|
10434
|
+
// ⑤-b URL 조각 / 드라이브 상대 경로는 루트 상대 파일 주장이 아니다 — 실존 파일이 있어도 세탁되면 안 된다.
|
|
10435
|
+
const dFrag = mk(d => W(d, '_anchor.js', 'function a(){return 1;}\nmodule.exports={a};\n'));
|
|
10436
|
+
const vFrag = verdict(dFrag, 'https://example.test/docs#_anchor.js 검토 완료');
|
|
10437
|
+
dbg.urlFragBlocked = vFrag.ok === false;
|
|
10438
|
+
const dDrive = mk(d => W(d, '_anchor.js', 'function a(){return 1;}\nmodule.exports={a};\n'));
|
|
10439
|
+
const vDrive = verdict(dDrive, 'C:_anchor.js 검토 완료');
|
|
10440
|
+
dbg.driveRelBlocked = vDrive.ok === false;
|
|
10441
|
+
// 대조군 — 정상 표기 `파일:src/x.js` 는 계속 인식돼야 한다(`:` 를 통째로 막지 않았는지)
|
|
10442
|
+
const dColon = mk(d => W(d, 'src/colon.js', 'function c(){return 1;}\nmodule.exports={c};\n'));
|
|
10443
|
+
const vColon = verdict(dColon, '파일:src/colon.js 구현 완료');
|
|
10444
|
+
dbg.colonFormOk = vColon.ok === true;
|
|
10445
|
+
|
|
10446
|
+
// ⑤ 없는 파일은 여전히 막힌다(추출기를 넓혔다고 존재 검사가 느슨해지지 않았는지)
|
|
10447
|
+
const dGhost = mk(() => {});
|
|
10448
|
+
const vGhost = verdict(dGhost, 'src/_ghost.js 구현 완료');
|
|
10449
|
+
dbg.ghostStillBlocked = vGhost.ok === false && vGhost.reasons.includes('files-missing');
|
|
10450
|
+
|
|
10451
|
+
ok = dbg.emptyBlocked && dbg.blankBlocked && dbg.realPasses && dbg.underscoreOk
|
|
10452
|
+
&& dbg.nextjsOk && dbg.privDirOk && dbg.initPyExempt && dbg.ghostStillBlocked
|
|
10453
|
+
&& dbg.markerOnlyBlocked && dbg.pyiRecognized && dbg.urlFragBlocked && dbg.driveRelBlocked && dbg.colonFormOk;
|
|
10454
|
+
} catch (e) { dbg.err = String(e && e.message).slice(0, 200); } finally { try { fs.rmSync(sb, { recursive: true, force: true }); } catch {} }
|
|
10455
|
+
console.log(ok ? '✓ M(1.36.109) 완료 게이트: 0바이트=스텁(공백과 동일 판정) + 밑줄 파일명 무손실 추출 + __init__.py 예외 + 실구현/없는파일 대조군'
|
|
10456
|
+
: '✗ 1.36.109 완료 게이트 실패 ' + JSON.stringify(dbg));
|
|
10457
|
+
if (!ok) failed++;
|
|
10458
|
+
}
|
|
10459
|
+
|
|
10361
10460
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
10362
10461
|
if (failed > 0) process.exit(1);
|
|
10363
10462
|
|