leerness 1.36.109 → 1.36.110
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 +71 -5
- package/lib/analyzers.js +68 -3
- package/package.json +1 -1
- package/scripts/e2e.js +72 -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.110 하네스를 사용합니다. 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.110는 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.109는 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.110 릴리스 태그 이력** (수백 라운드) · _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.110: 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.110';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -6430,7 +6430,7 @@ function _selfTestCases() {
|
|
|
6430
6430
|
return clar && shape && noDbUrl && reqOk && guards && uiOk && _p0013LibraryOk() && _p0088CurrentStatePreserved()
|
|
6431
6431
|
&& _p0101SurfaceOk() && _p0101SkillIdOk() && _p0101PathStrictOk() && _p0102HonestyOk()
|
|
6432
6432
|
&& _p0103FlagsOk() && _p0103JsonOk() && _p0103RootOk() && _p0104UsageOk()
|
|
6433
|
-
&& _p0014SecretBaselineOk() && _p0104ReminderOk() && _p0015ModeOk() && _p0097AsyncLockOk() && _p0109ClaimGateOk();
|
|
6433
|
+
&& _p0014SecretBaselineOk() && _p0104ReminderOk() && _p0015ModeOk() && _p0097AsyncLockOk() && _p0109ClaimGateOk() && _p0110HonestyOk();
|
|
6434
6434
|
} },
|
|
6435
6435
|
{ name: '시크릿 스캐너 F-06 (1.36.56, 외부감사): 무명 확장자 소형 텍스트 스캔(이진 NUL 제외) + 같은 토큰 중복 보고 dedupe — 행위검사', run: () => {
|
|
6436
6436
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_sc56_'));
|
|
@@ -8110,6 +8110,60 @@ function _p0015ModeOk() {
|
|
|
8110
8110
|
// 결정적으로 밟는다 — `typeof out.then === 'function'` 이라는 판별 조건이 동일하다.
|
|
8111
8111
|
// 1.36.109 (완료게이트 사냥): 두 결함을 **순수 호출**로 고정한다. 둘 다 양방향으로 단언한다 —
|
|
8112
8112
|
// 막아야 할 것을 막는지, 그리고 통과해야 할 것이 계속 통과하는지. false-BLOCK 은 false-PASS 보다 나쁘다.
|
|
8113
|
+
// 1.36.110 (T-0099): 미검증 자백/헤지 탐지 — **언어 대칭**과 **오탐 0** 을 순수 호출로 고정한다.
|
|
8114
|
+
// 기준선은 재현율 1/16(6%), 한국어 0/8 이었다. 같은 뜻의 주장이 표기 언어에 따라 갈리면 판정이 아니라 우연이다.
|
|
8115
|
+
// 재현율보다 FP 가 먼저다 — 정직한 주장이 막히면 사용자는 게이트를 끈다. 실제 done 이력 139건 발화 0 으로 확인했다.
|
|
8116
|
+
function _p0110HonestyOk() {
|
|
8117
|
+
try {
|
|
8118
|
+
const an = require('../lib/analyzers');
|
|
8119
|
+
const F = (t, done) => an._epistemicHonestyCheck(t, done ? { claimedDone: true } : undefined).findings;
|
|
8120
|
+
const high = (t, done) => F(t, done).filter(f => f.severity === 'high').length > 0;
|
|
8121
|
+
// 이 라운드가 추가한 것은 **권고**다 — 차단하지 않는다. 가드도 그 계약을 그대로 단언한다.
|
|
8122
|
+
const hit = (t, done) => F(t, done).some(f => f.dim === 'unverified-admission');
|
|
8123
|
+
// ① 언어 대칭 — 같은 뜻의 ko/en 쌍이 같은 판정을 받아야 한다(1차 강화에서 4쌍이 갈렸고,
|
|
8124
|
+
// 그중 하나는 한국어 음절 합성 때문이었다: `듯하다` 는 잡히는데 `듯합니다` 는 안 잡혔다).
|
|
8125
|
+
// 이 라운드가 **권고로** 잡는 것 — 직접적인 미검증 자백. 두 언어가 같은 판정을 받아야 한다.
|
|
8126
|
+
const pairs = [
|
|
8127
|
+
['테스트는 아직 안 돌렸지만 완료했습니다', 'Done, but not tested yet.'],
|
|
8128
|
+
['로직 반영했는데 엣지 케이스는 안 봤습니다.', 'Logic applied; edge cases unchecked.'],
|
|
8129
|
+
];
|
|
8130
|
+
const symmetric = pairs.every(([ko, en]) => hit(ko, true) === hit(en, true) && hit(ko, true) === true);
|
|
8131
|
+
// ①-c **차단하지 않는다** — 이 신호로 done 을 막지 않기로 한 결정을 고정한다.
|
|
8132
|
+
// 차단으로 올리려면 산문 파싱이 아니라 도구 자신의 실행 증거를 근거로 삼아야 한다(별도 과제).
|
|
8133
|
+
const neverBlocks = pairs.every(([ko, en]) => high(ko, true) === false && high(en, true) === false);
|
|
8134
|
+
// 기존 차단 차원(추정 표현)은 **그대로**여야 한다. 한국어 음절 합성 회귀를 여기서 고정한다 —
|
|
8135
|
+
// `듯\s*(?:하|싶)` 는 "듯하다" 는 잡지만 "듯합니다"(하+ㅂ→합)는 못 잡아 ko/en 이 갈렸다.
|
|
8136
|
+
const highPairs = [['해결된 듯합니다. 다음에 확인하겠습니다', 'Looks resolved. I will confirm next time.']];
|
|
8137
|
+
const highSymmetric = highPairs.every(([ko, en]) => high(ko, true) === true && high(en, true) === true);
|
|
8138
|
+
// ①-b **의도적으로 잡지 않는 것** — 미래 범위 표현. "추후 확인 예정" 은 (a) 이 작업을 검증 안 했다 와
|
|
8139
|
+
// (b) 앞으로 더 볼 게 남았다 를 모두 뜻할 수 있어 정규식으로 가를 수 없다. 자체 FP 헌트에서 이 표현이
|
|
8140
|
+
// 문서 수정·설정 변경·의존성 bump·revert 를 막았다(전부 정직한 완료 주장). 애매하면 통과시킨다.
|
|
8141
|
+
const futureScopeAllowed = [
|
|
8142
|
+
'docs/guide.md 갱신. 오탈자가 더 있을 수 있어 추후 확인 예정.',
|
|
8143
|
+
'package.json 의존성 3개 bump. 호환성은 추후 확인.',
|
|
8144
|
+
'Bumped deps in package.json. Compatibility will be checked later.',
|
|
8145
|
+
].every(t => hit(t, true) === false);
|
|
8146
|
+
// ② 오탐 0 — 근거를 댄 정직한 주장은 어떤 표현을 써도 통과한다. `!hasSource` 가 방어선이다.
|
|
8147
|
+
const honest = [
|
|
8148
|
+
'src/queue.ts 재작성. jest 88 passed, 0 failed.',
|
|
8149
|
+
'Rewrote src/queue.ts. jest: 88 passed, 0 failed.',
|
|
8150
|
+
'src/a.js 구현. 테스트 20/20 통과. 성능 개선 효과는 추후 측정 예정.', // 헤지 낱말 + 근거 → 통과해야 한다
|
|
8151
|
+
'오타 수정만 했습니다. 검증 필요 없는 주석 변경. src/c.js', // "필요 없는" 은 반대 의미
|
|
8152
|
+
'Comment typo only in src/c.js — needs no verification.',
|
|
8153
|
+
// 러너 이름을 열거하지 않는다는 계약 — 실행했는데 막히면 명백한 오차단이다(자체 FP 헌트가 6건 잡았다).
|
|
8154
|
+
'app/src/Main.kt 구현. ./gradlew test 통과. 엣지 케이스는 안 봤습니다.',
|
|
8155
|
+
'Program.cs 구현. dotnet test 실행 완료. 통합 시나리오는 아직 확인 못 했습니다.',
|
|
8156
|
+
'app/models/user.rb 구현. bundle exec rspec 실행. 엣지 케이스는 안 봤습니다.',
|
|
8157
|
+
'src/x.ts 구현. ./scripts/verify.sh 실행해 통과. 회귀는 아직 안 봤습니다.',
|
|
8158
|
+
];
|
|
8159
|
+
const noFalseBlock = honest.every(t => hit(t, true) === false);
|
|
8160
|
+
// ③ claimedDone 은 **호출부가 아는 사실**을 받는 옵션이다 — 기본값 동작은 바뀌지 않아야 한다
|
|
8161
|
+
// (독립 표면 `honesty-check --text` 가 갑자기 더 엄해지면 그건 이 라운드가 약속한 범위 밖이다).
|
|
8162
|
+
const optIn = hit('로직 반영했는데 엣지 케이스는 안 봤습니다.', true) === true
|
|
8163
|
+
&& hit('엣지 케이스는 안 봤습니다.', false) === false; // 산문에 완료 낱말이 없으면 기본값에선 안 잡는다
|
|
8164
|
+
return symmetric && neverBlocks && highSymmetric && futureScopeAllowed && noFalseBlock && optIn;
|
|
8165
|
+
} catch { return false; }
|
|
8166
|
+
}
|
|
8113
8167
|
function _p0109ClaimGateOk() {
|
|
8114
8168
|
try {
|
|
8115
8169
|
const an = require('../lib/analyzers');
|
|
@@ -15513,11 +15567,17 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
15513
15567
|
// 판정 기준은 **실행 확증**이어야 한다 — 실제로 돌았고 실패하지 않았는가(allPassed = exit 0 이며 파싱됐다면 전부 통과).
|
|
15514
15568
|
const _runProven = !!(runResult && !runResult.skipped && runResult.allPassed);
|
|
15515
15569
|
const unverifiableClaim = claimsChecked && files.length === 0 && !_runProven;
|
|
15516
|
-
let optimismSuspects = [], honestyFindings = [], strictOk = true;
|
|
15570
|
+
let optimismSuspects = [], honestyFindings = [], honestyAdvisory = [], strictOk = true;
|
|
15517
15571
|
if (claimsChecked) {
|
|
15518
15572
|
const codeText = _scanCodeForPatterns(root);
|
|
15519
15573
|
optimismSuspects = _detectOptimism(evidence, codeText);
|
|
15520
|
-
|
|
15574
|
+
// 1.36.110 (T-0099): 여기 도달했다는 것은 이미 완료 주장(done/--require-evidence/--strict-claims)이라는 뜻이다.
|
|
15575
|
+
// 그 사실을 넘겨 산문에서 완료 낱말을 다시 찾는 중복 요구를 없앤다(그것이 재현율 병목이었다).
|
|
15576
|
+
const _hAll = _epistemicHonestyCheck(evidence, { claimedDone: true }).findings;
|
|
15577
|
+
honestyFindings = _hAll.filter(f => f.severity === 'high');
|
|
15578
|
+
// 미검증 자백은 **권고**다 — 게이팅하지 않는다(왜 그렇게 정했는지는 lib/analyzers.js 주석 참조).
|
|
15579
|
+
// 다만 보이지 않으면 탐지한 의미가 없으므로 사람 출력과 --json 에 함께 싣는다.
|
|
15580
|
+
honestyAdvisory = _hAll.filter(f => f.severity !== 'high');
|
|
15521
15581
|
strictOk = optimismSuspects.length === 0 && honestyFindings.length === 0;
|
|
15522
15582
|
}
|
|
15523
15583
|
const gitStrongMismatch = gitApplicable && claimedInGit.length === 0; // 변경 있는데 주장 파일이 git 변경에 전무
|
|
@@ -15574,7 +15634,8 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
15574
15634
|
},
|
|
15575
15635
|
stubFiles: stubFiles.slice(0, 10),
|
|
15576
15636
|
evidence: { required: mustHaveEvidence, ...evq },
|
|
15577
|
-
|
|
15637
|
+
// 1.36.110: `advisory` 는 **게이팅하지 않는** 신호다(ok 에 영향 없음). 소비자가 경계를 오인하지 않도록 키를 분리한다.
|
|
15638
|
+
claims: !claimsChecked ? null : { ok: strictOk, optimism: optimismSuspects.map(s => ({ kind: s.kind, label: s.label })), honesty: honestyFindings.map(f => ({ dim: f.dim, label: f.label })), advisory: honestyAdvisory.map(f => ({ dim: f.dim, label: f.label, severity: f.severity })) },
|
|
15578
15639
|
git: gitChanged === null ? { applicable: false, reason: 'not-a-git-repo' } : (!gitApplicable ? { applicable: false, reason: 'no-working-changes-or-no-claimed-files' } : { applicable: true, claimedInGit: claimedInGit.length, claimedNotInGit, strongMismatch: gitStrongMismatch, changedNotClaimed }),
|
|
15579
15640
|
scopeCreep: !gitApplicable ? null : { count: changedNotClaimed.length, files: changedNotClaimed.slice(0, 10) } // 1.13.2 (Karpathy 외과적 변경): 요청/주장 밖 변경 파일(advisory)
|
|
15580
15641
|
};
|
|
@@ -15673,6 +15734,11 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
15673
15734
|
for (const s of optimismSuspects) log(t(` · [${s.kind}] ${s.label}: evidence에 주장 있는데 코드에 호출 흔적 없음`, ` · [${s.kind}] ${s.label}: claimed in evidence but no call trace in code`));
|
|
15674
15735
|
for (const f of honestyFindings) log(` · [${t('정직성', 'honesty')}:${f.dim}] ${f.label}: ${f.detail}`);
|
|
15675
15736
|
}
|
|
15737
|
+
// 1.36.110 (T-0099): 권고는 통과/실패와 **무관하게** 보여 준다 — 안 보이면 탐지한 의미가 없다.
|
|
15738
|
+
// 차단하지 않는 이유(오차단 위험·우회 가능성)는 lib/analyzers.js 에 적었다.
|
|
15739
|
+
for (const f of honestyAdvisory) {
|
|
15740
|
+
log(t(` - ⓘ 권고(차단 아님) [${f.dim}] ${f.label}: ${f.detail}`, ` - ⓘ advisory (non-blocking) [${f.dim}] ${f.label}: ${f.detail}`));
|
|
15741
|
+
}
|
|
15676
15742
|
}
|
|
15677
15743
|
// 1.9.302 (UR-0042) + 1.11.2 (UR-0175): git diff 시맨틱 교차검증 — 주장 파일이 실제 git 변경에 있는가. gitClaimOk/gitStrongMismatch 는 상단 공유(done 기본 게이팅, --lenient 완화).
|
|
15678
15744
|
if (gitChanged === null) {
|
package/lib/analyzers.js
CHANGED
|
@@ -94,8 +94,15 @@ function _parseEvidenceStats(text) {
|
|
|
94
94
|
|
|
95
95
|
// 1.9.305 (사용자 명시): AI 인식론적 정직성 점검 — 모르는 걸 아는 척 / 정보 미수집 / 미검증 섣부른 판단 휴리스틱 탐지.
|
|
96
96
|
// 순수 함수(텍스트→findings). 휴리스틱 advisory — 단정/추정/외부참조 표현 vs 근거·수집 흔적 대조. opt-in 점검용.
|
|
97
|
-
|
|
97
|
+
// 1.36.110 (T-0099): `opts.claimedDone` — 호출부가 **이미 완료 주장임을 아는** 경우.
|
|
98
|
+
// verify-claim 은 status 가 done 인 행에만 이 검사를 돌린다(claimsChecked). 그 문맥에서
|
|
99
|
+
// "산문에 완료 낱말이 있는가" 를 다시 요구하는 건 중복이고, 실제로 그것이 재현율의 병목이었다 —
|
|
100
|
+
// 홀드아웃에서 놓친 6건 중 5건이 결론 어휘 미등록("마쳤/잡은/shipped/applied/gone")이었다.
|
|
101
|
+
// 완료 동의어를 두 언어로 계속 채우는 건 끝이 없다(휴리스틱 토끼굴). 호출부가 아는 사실을 받는다.
|
|
102
|
+
// 기본값은 종전 그대로 — `honesty-check --text` 같은 독립 표면의 동작은 바뀌지 않는다.
|
|
103
|
+
function _epistemicHonestyCheck(text, opts) {
|
|
98
104
|
const t = String(text || '');
|
|
105
|
+
const claimedDone = !!(opts && opts.claimedDone);
|
|
99
106
|
const findings = [];
|
|
100
107
|
// 공통: 근거/출처 흔적 (파일경로·URL·테스트결과·Exit·문서·api-skill·인용·조회 흔적)
|
|
101
108
|
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);
|
|
@@ -103,9 +110,67 @@ function _epistemicHonestyCheck(text) {
|
|
|
103
110
|
const definitive = /(반드시|항상|언제나|무조건|확실(?:히|함|하게)|당연히|틀림없|100\s*%|always|never|guaranteed|definitely|obviously|certainly)/i.test(t);
|
|
104
111
|
if (definitive && !hasSource) findings.push({ dim: 'pretend-knowledge', severity: 'high', label: '근거 없는 단정', detail: '단정적 표현이 있으나 근거/출처(파일·문서·테스트·로그)가 없음 — 모르는 정보를 아는 척할 위험.' });
|
|
105
112
|
// 차원2: 미검증 섣부른 판단 — 추정 표현 + 완료/성공 결론인데 근거 없음
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
// 1.36.110 (T-0099): 이 판정은 **언어에 상관없이** 같아야 하는데 실측 재현율이 영어 2/4 · 한국어 0/8 이었다.
|
|
114
|
+
// 같은 뜻의 주장이 표기 언어에 따라 통과/차단이 갈리면 그건 판정이 아니라 우연이다
|
|
115
|
+
// (`should work, will test later` 는 잡고 `아마 동작할 겁니다. 나중에 테스트 예정.` 은 통과시켰다).
|
|
116
|
+
// 측정 결과 원인은 둘이었다:
|
|
117
|
+
// (a) 결론 어휘가 좁다 — `작동` 은 있는데 `동작` 이 없고, `수정했`·`반영`·`적용` 이 빠져 있었다.
|
|
118
|
+
// (b) **미검증 자백** 신호가 아예 없었다 — "테스트는 아직 안 돌렸지만 완료했습니다" 에는 추정 표현이
|
|
119
|
+
// 하나도 없다. 이건 헤지가 아니라 **자기모순**이라 헤지보다 강한 신호다.
|
|
120
|
+
// 강화 전에 FP 코퍼스를 먼저 고정했다(정직한 주장 무탐지 100%). `!hasSource` 가 그 방어선이고,
|
|
121
|
+
// 근거를 댄 주장은 어떤 표현을 쓰든 통과한다 — "20/20 통과 … 성능은 추후 측정 예정" 같은 정직한 문장이 그 예다.
|
|
122
|
+
// ⚠ 세 목록은 **언어 대칭**을 유지해야 한다. 한쪽에만 표현을 더하면 같은 뜻의 주장이 표기 언어에 따라
|
|
123
|
+
// 갈린다 — 실제로 1차 강화에서 4쌍이 갈렸고(ko HIT / en MISS 등) 대칭 검사가 그걸 잡았다.
|
|
124
|
+
// 새 표현을 넣을 때는 반대 언어의 짝을 같이 넣고, 코퍼스에 그 쌍을 추가한다.
|
|
125
|
+
// ⚠ 한국어 **음절 합성** 함정: `듯\s*(?:하|싶)` 는 "듯하다" 는 잡지만 "듯합니다" 는 못 잡는다 —
|
|
126
|
+
// 하 + ㅂ 이 한 음절 `합` 으로 합쳐지기 때문이다. 대칭 검사가 이 한 글자 차이로 ko/en 판정이 갈린 것을 잡았다.
|
|
127
|
+
// 한국어 표현을 넣을 때는 활용형(하/해/한/했/함/합)을 함께 적어야 한다.
|
|
128
|
+
const assumption = /(아마|추정|것\s*같|듯\s*(?:하|해|한|했|함|합|싶)|probably|likely|maybe|perhaps|i\s*(?:think|assume|guess|believe|suppose)|seems?\s*to|appears?\s*to|looks?\s*(?:like|good|fine|ok|resolved|fixed|done)|should\s*(?:work|be|pass|fix)|expect(?:ed|s)?\s*to|생각(?:됩니다|된다|함|돼)|일\s*것|예상(?:됩니다|된다|됨|돼)|짐작)/i.test(t);
|
|
129
|
+
// 미검증 자백 — "아직 안 했다 / 나중에 하겠다" 를 스스로 말하면서 완료를 주장하는 형태.
|
|
130
|
+
// `(?!\s*없)` 는 "검증 필요 없는 오타 수정" 같은 **반대 의미**를 배제한다(FP 코퍼스가 잡아냈다).
|
|
131
|
+
// ⚠ **미래 범위 표현은 넣지 않는다** — "추후 확인 예정"·"will check later" 는 두 가지를 뜻할 수 있다:
|
|
132
|
+
// (a) 이 작업을 검증하지 않았다 (b) 앞으로 더 볼 것이 남았다. 정규식으로 둘을 가를 수 없다.
|
|
133
|
+
// 자체 FP 헌트에서 그 표현들이 문서 수정·설정 변경·의존성 bump·revert 를 막았다(실측 4건) —
|
|
134
|
+
// 전부 정직한 완료 주장이다. 애매하면 통과시킨다: false-BLOCK 이 false-PASS 보다 나쁘다.
|
|
135
|
+
// 여기 남기는 것은 **이미 일어난 일에 대한 부정**뿐이다("아직 안 돌렸다"·"not tested yet"·"unchecked").
|
|
136
|
+
const unverified = /(아직\s*(?:안|못)\s*(?:돌|해|확인|테스트|검증)|테스트\s*(?:는\s*)?(?:아직|안\s*(?:했|돌))|확인(?:은|을)?\s*(?:못|안)\s*(?:해|했|함)|안\s*(?:봤|돌렸|해봤)|미확인|not\s*(?:yet\s*)?(?:tested|verified|checked)|have\s*not\s*(?:tested|verified|checked)|untested|unverified|unchecked|yet\s*to\s*(?:test|verify|check)|todo\s*:?\s*(?:verify|test))/i.test(t);
|
|
137
|
+
const conclusion = /(완료|done|성공|통과|해결(?:됨|된|했|함|되었)|fixed|resolv(?:e|es|ed)|works?\b|complete[ds]?\b|implement(?:ed|s|ation)?\b|작동(?:함|한다|됨|할)|동작(?:함|한다|됨|할)|구현(?:됨|했|완료)|수정(?:됨|했|완료)|반영(?:됨|했|완료)|적용(?:됨|했|완료)|처리(?:됨|했|완료)|고쳤|고쳐(?:짐|졌)|끝냈|마무리(?:함|했|됨))/i.test(t);
|
|
138
|
+
// 1.36.110 (행위 가드가 순수 측정의 오류를 잡았다): `hasSource` 는 "파일을 언급했는가" 다.
|
|
139
|
+
// 그런데 verify-claim 의 evidence 는 **거의 항상 파일을 인용한다**(안 하면 files-missing/unverifiable 로 이미 막힌다) —
|
|
140
|
+
// 그래서 이 판정이 정작 필요한 문맥에서 도달 불가였다(실제 done 이력 139건 발화 0 의 진짜 이유).
|
|
141
|
+
// "아직 테스트 안 했다" 를 반박하는 것은 파일 이름이 아니라 **실행 증거**다. done 문맥에서만 기준을 그것으로 좁힌다.
|
|
142
|
+
// 기본 표면(`honesty-check --text`)의 판정은 종전 그대로 — 범위를 넘어 조이지 않는다.
|
|
143
|
+
// ⚠ 러너 **이름을 열거하지 않는다**. 처음엔 npm/pytest/cargo/jest… 를 나열했는데, 자체 FP 헌트에서
|
|
144
|
+
// Gradle · .NET · RSpec · PHPUnit · Elixir · Swift · 커스텀 스크립트가 **실제로 테스트를 돌렸는데도** 막혔다.
|
|
145
|
+
// 생태계마다 러너를 채우는 건 끝이 없다(완료 동의어와 같은 토끼굴). 러너 이름이 아니라
|
|
146
|
+
// **실행/결과의 흔적**을 본다: 실행 마커(Exit/Command) · 결과 수치 · 테스트 어휘 + 실행/통과 낱말.
|
|
147
|
+
const execMarker = /\bExit\s*[:=]|\bCommand\s*[:=]|exit\s*code/i.test(t);
|
|
148
|
+
const testOutcome = /\d+\s*(?:\/\s*\d+\s*)?(?:통과|passed|passing|성공)|\ball\s+(?:tests?\s+)?(?:passed|green)\b|BUILD\s+SUCCESS/i.test(t);
|
|
149
|
+
// 테스트/검증 어휘가 **실행·통과 낱말과 가까이** 있으면 실행 흔적으로 본다(러너 무관).
|
|
150
|
+
// ⚠ **부정을 건너뛰면 안 된다**: 처음엔 단순히 "테스트 낱말 … 실행/통과" 로 잡았는데,
|
|
151
|
+
// 그러면 `테스트는 아직 안 돌렸지만` 이 "테스트를 돌렸다" 로 읽혀 이 라운드의 대표 케이스가 통과했다.
|
|
152
|
+
// 사이에 부정어(안/못/않/없/not/no)가 오면 실행 흔적으로 세지 않는다(tempered 매칭).
|
|
153
|
+
// 부정어는 **뒤에 공백이 오는 것만** 본다 — "안정성"·"못지않게" 같은 낱말을 부정으로 오인하면
|
|
154
|
+
// 실행 흔적을 못 세고, 그건 곧 정직한 주장의 오차단으로 이어진다.
|
|
155
|
+
const NEG = '(?:(?!안\\s|못\\s|않|없|\\bnot\\b|\\bno\\b)[^\\n])';
|
|
156
|
+
const testWords = 'test|spec|unit|e2e|integration|verify|테스트|스펙|검증|점검';
|
|
157
|
+
const ranWords = '실행|돌렸|돌림|통과|성공|passed|passing|ran\\b|run\\b|green|success';
|
|
158
|
+
const testRun = new RegExp(`(?:${testWords})${NEG}{0,24}(?:${ranWords})`, 'i').test(t)
|
|
159
|
+
|| new RegExp(`(?:실행|ran|run)${NEG}{0,24}(?:${testWords})`, 'i').test(t);
|
|
160
|
+
const ranSomething = execMarker || testOutcome || testRun;
|
|
161
|
+
const refuted = claimedDone ? ranSomething : hasSource;
|
|
162
|
+
// 종전 차원(추정 표현 + 결론 + 근거 없음)은 **그대로** 유지한다 — 기존 사용자의 게이팅 동작을 바꾸지 않는다.
|
|
108
163
|
if (assumption && conclusion && !hasSource) findings.push({ dim: 'premature-judgment', severity: 'high', label: '검증 없는 섣부른 판단', detail: '가정·추정 표현과 완료·성공 결론이 함께 있으나 검증 근거가 없음 — 검증 없이 섣부르게 판단할 위험.' });
|
|
164
|
+
// 1.36.110 (T-0099) — **권고(medium)로 낸다. 차단하지 않는다.** 왜 그렇게 정했는지 남긴다:
|
|
165
|
+
// 이 신호를 차단으로 쓰려고 두 라운드를 넓혔는데, 자체 FP 헌트가 6건(실제로 테스트를 돌린 Gradle·.NET·
|
|
166
|
+
// RSpec·PHPUnit·Elixir·Swift 등)을, 외부 검수가 3건을 더 찾았다. 근본 이유 셋:
|
|
167
|
+
// (a) 직접 자백 표현이 두 언어에서 유계가 아니다("아직 테스트를 수행하지 않았습니다" 같은 어순 변형).
|
|
168
|
+
// (b) "검증 안 함" · "부분만 검증" · "해당 없음(문서 수정)" 을 정규식으로 가를 수 없다.
|
|
169
|
+
// (c) evidence 에 `Exit: 0` 한 줄만 적어도 반박된다 — 차단은 쉽게 우회되고, 그만큼 일관되지 못하다.
|
|
170
|
+
// 가끔 정직한 작업을 막고 자주 놓치는 게이트는 **명확한 권고보다 나쁘다**. 차단으로 올리려면
|
|
171
|
+
// 산문 파싱이 아니라 도구 자신의 실행 증거(`--run-tests` 결과)를 근거로 삼아야 한다 — 그건 별도 과제다.
|
|
172
|
+
// `unverified` 는 호출부가 done 을 보증할 때만 본다(기본 표면 동작 불변 약속을 지키기 위해).
|
|
173
|
+
if (claimedDone && unverified && !refuted) findings.push({ dim: 'unverified-admission', severity: 'medium', label: '미검증 자백', detail: '완료로 표시했으나 evidence 가 스스로 "아직 확인하지 않았다"고 말하며 실행 증거(테스트 결과·Exit·실행 흔적)가 없음 — 차단하지는 않되 재확인 권장.' });
|
|
109
174
|
// 차원3: 정보 미수집 — 외부 API/라이브러리/버전/스펙 언급인데 수집·근거 흔적 없음
|
|
110
175
|
// \bAPI\b(?!\.[a-z]) 로 파일경로(api.js/api.ts) 오탐 제외. 강한 근거(hasSource)나 수집 흔적(gathered) 있으면 통과.
|
|
111
176
|
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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.110",
|
|
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
|
@@ -10457,6 +10457,78 @@ total++;
|
|
|
10457
10457
|
if (!ok) failed++;
|
|
10458
10458
|
}
|
|
10459
10459
|
|
|
10460
|
+
// ── 1.36.110 블록 N (T-0099): 미검증 자백이 done 을 통과시키던 것.
|
|
10461
|
+
// 기준선 실측: 재현율 1/16(6%) — `should work, will test later` 는 잡고 같은 뜻의 한국어는 0/8 이었다.
|
|
10462
|
+
// 같은 뜻의 주장이 표기 언어에 따라 통과/차단이 갈리면 그건 판정이 아니라 우연이다.
|
|
10463
|
+
// 구조적 병목은 "산문에 완료 낱말이 있는가" 를 다시 요구한 것이었다 — verify-claim 은 이미 done 행에만
|
|
10464
|
+
// 이 검사를 돌린다. 호출부가 아는 사실(claimedDone)을 넘겨 중복 요구를 없앴다.
|
|
10465
|
+
// FP 를 먼저 고정했다: 실제 done 이력 139건 발화 0 · 홀드아웃 정직 주장 8/8 무탐지.
|
|
10466
|
+
{
|
|
10467
|
+
total++;
|
|
10468
|
+
let ok = false; const dbg = {};
|
|
10469
|
+
const sb = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-honest110-'));
|
|
10470
|
+
try {
|
|
10471
|
+
const ENV = Object.assign({}, process.env, { TMPDIR: sb, TEMP: sb, TMP: sb });
|
|
10472
|
+
let seq = 0;
|
|
10473
|
+
const mk = (setup) => {
|
|
10474
|
+
const d = path.join(sb, 'p' + (++seq));
|
|
10475
|
+
fs.mkdirSync(d, { recursive: true });
|
|
10476
|
+
fs.writeFileSync(path.join(d, 'package.json'), '{"name":"p","version":"0.1.0"}');
|
|
10477
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { cwd: d, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
10478
|
+
if (setup) setup(d); return d;
|
|
10479
|
+
};
|
|
10480
|
+
const R = (d, a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { cwd: d, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
10481
|
+
const W = (d, rel, body) => { const p = path.join(d, rel); fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, body); };
|
|
10482
|
+
const verdict = (d, ev, extra = []) => {
|
|
10483
|
+
const id = (String(R(d, ['task', 'add', '주장']).stdout || '').match(/T-\d{4,}/) || [])[0];
|
|
10484
|
+
R(d, ['task', 'update', id, '--status', 'done', '--evidence', ev]);
|
|
10485
|
+
const r = R(d, ['verify-claim', id, '--json', ...extra]);
|
|
10486
|
+
let j = null; try { j = JSON.parse(String(r.stdout || '')); } catch {}
|
|
10487
|
+
const adv = (j && j.claims && Array.isArray(j.claims.advisory)) ? j.claims.advisory : [];
|
|
10488
|
+
return { ok: j ? j.ok : null, reasons: j ? (j.reasons || []) : [], advisory: adv.map(a => a.dim), out: String(r.stdout || '') };
|
|
10489
|
+
};
|
|
10490
|
+
const impl = d => W(d, 'src/impl.js', 'function f(){return 1;}\nmodule.exports={f};\n');
|
|
10491
|
+
|
|
10492
|
+
// ① 미검증 자백 → **권고로 보고하되 차단하지 않는다**. ko/en 이 같은 판정이어야 한다(언어 대칭).
|
|
10493
|
+
// 차단하지 않기로 한 이유: 직접 자백 표현이 두 언어에서 유계가 아니고, "검증 안 함"과 "부분만 검증"과
|
|
10494
|
+
// "해당 없음(문서 수정)"을 정규식으로 가를 수 없으며, evidence 에 `Exit: 0` 한 줄이면 반박된다.
|
|
10495
|
+
// 자체 FP 헌트 6건 + 외부 검수 3건이 전부 그 경계에서 나왔다. 차단으로 올리려면 산문 파싱이 아니라
|
|
10496
|
+
// 도구 자신의 실행 증거(`--run-tests`)를 근거로 삼아야 한다(별도 과제).
|
|
10497
|
+
const vKo = verdict(mk(impl), 'src/impl.js 반영했는데 엣지 케이스는 안 봤습니다.');
|
|
10498
|
+
const vEn = verdict(mk(impl), 'src/impl.js applied; edge cases unchecked.');
|
|
10499
|
+
dbg.koAdvised = vKo.advisory.includes('unverified-admission');
|
|
10500
|
+
dbg.enAdvised = vEn.advisory.includes('unverified-admission');
|
|
10501
|
+
dbg.symmetric = dbg.koAdvised === dbg.enAdvised;
|
|
10502
|
+
dbg.notBlocking = vKo.ok === true && vEn.ok === true; // 권고는 게이팅하지 않는다
|
|
10503
|
+
dbg.visibleToUser = /권고|advisory/i.test(vKo.out); // 안 보이면 탐지한 의미가 없다
|
|
10504
|
+
|
|
10505
|
+
// ② 대조군 — 근거를 댄 정직한 주장은 권고조차 붙지 않는다.
|
|
10506
|
+
const vHonest = verdict(mk(impl), 'src/impl.js 구현. 테스트 12/12 통과 (Exit: 0).');
|
|
10507
|
+
dbg.honestClean = vHonest.ok === true && vHonest.advisory.length === 0;
|
|
10508
|
+
const vHedgedButSourced = verdict(mk(impl), 'src/impl.js 구현. 20/20 passed. 성능 개선 효과는 추후 측정 예정.');
|
|
10509
|
+
dbg.sourcedHedgeClean = vHedgedButSourced.ok === true && vHedgedButSourced.advisory.length === 0;
|
|
10510
|
+
// "검증 필요 없는" 은 반대 의미 · 미래 범위 표현은 의도적으로 잡지 않는다
|
|
10511
|
+
const vNegated = verdict(mk(impl), 'src/impl.js 오타 수정만. 검증 필요 없는 주석 변경. 12/12 통과.');
|
|
10512
|
+
dbg.negationClean = vNegated.ok === true && vNegated.advisory.length === 0;
|
|
10513
|
+
const vFuture = verdict(mk(impl), 'src/impl.js 설정값만 변경. 호환성은 추후 확인.');
|
|
10514
|
+
dbg.futureScopeClean = vFuture.ok === true && vFuture.advisory.length === 0;
|
|
10515
|
+
// 러너 이름을 열거하지 않는다 — 실행했는데 권고가 붙으면 사용자에게 잘못된 신호다
|
|
10516
|
+
const vGradle = verdict(mk(impl), 'src/impl.js 구현. ./gradlew test 통과. 엣지 케이스는 안 봤습니다.');
|
|
10517
|
+
dbg.unlistedRunnerClean = vGradle.ok === true && vGradle.advisory.length === 0;
|
|
10518
|
+
|
|
10519
|
+
// ③ 기존 차단 동작은 그대로 — 이 라운드가 게이팅을 바꾸지 않았다는 것을 고정한다.
|
|
10520
|
+
const vStub = verdict(mk(d => W(d, 'src/empty.js', '')), 'src/empty.js 구현 완료');
|
|
10521
|
+
dbg.existingGateIntact = vStub.ok === false && vStub.reasons.includes('stub-impl');
|
|
10522
|
+
|
|
10523
|
+
ok = dbg.koAdvised && dbg.enAdvised && dbg.symmetric && dbg.notBlocking && dbg.visibleToUser
|
|
10524
|
+
&& dbg.honestClean && dbg.sourcedHedgeClean && dbg.negationClean && dbg.futureScopeClean
|
|
10525
|
+
&& dbg.unlistedRunnerClean && dbg.existingGateIntact;
|
|
10526
|
+
} catch (e) { dbg.err = String(e && e.message).slice(0, 200); } finally { try { fs.rmSync(sb, { recursive: true, force: true }); } catch {} }
|
|
10527
|
+
console.log(ok ? '✓ N(1.36.110/T-0099) 미검증 자백을 **권고로** 보고(차단 아님) · ko/en 동일 · 사용자에게 노출 · 정직 주장·미래범위·목록밖 러너 무탐 · 기존 게이팅 불변'
|
|
10528
|
+
: '✗ 1.36.110 정직성 판정 실패 ' + JSON.stringify(dbg));
|
|
10529
|
+
if (!ok) failed++;
|
|
10530
|
+
}
|
|
10531
|
+
|
|
10460
10532
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
10461
10533
|
if (failed > 0) process.exit(1);
|
|
10462
10534
|
|