leerness 1.35.6 → 1.35.7
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 +17 -1
- package/README.md +4 -4
- package/bin/leerness.js +77 -32
- package/package.json +1 -1
- package/scripts/e2e.js +38 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.35.7 — 2026-07-03 — 19th GPT5.5pro 평가: verify-claim declared-pass 부풀림 게이팅 + spec 리포터 파싱 + json ok/reasons (UR-0013)
|
|
4
|
+
|
|
5
|
+
**웹 GPT 5.5 Pro 확장의 독립 평가보고서(1.35.6 게시본 대상) 검토 라운드**. 맹신 X 재현 결과 보고서의 헤드라인 버그가 **3경로 전부 실재** — 원인 분석의 소스 위치까지 정확했음. 검토 중 추가 발견 2건(자기모순 출력 + Node v26 파서 FN)도 함께 수정.
|
|
6
|
+
|
|
7
|
+
### 확정 버그 (게시본 1.35.6 재현)
|
|
8
|
+
- **declared-pass mismatch 미게이팅 (P1)**: evidence 주장 "3/3 passed" vs 실행 "2/2" 를 감지·표시("⚠ 불일치")하고도 human/`--json`/`--all` 3경로 모두 **exit 0** — 부풀린 완료 주장이 플래그십을 통과. 게다가 human 최종요약은 불일치 출력 직후 "✓ evidence 주장이 실제 파일·테스트·실행 결과와 일치" **자기모순 표기**.
|
|
9
|
+
- **Node v26 파서 FN (추가발견)**: 신형 Node 는 비-TTY 도 spec 리포터가 기본(`ℹ pass 2`) → 기존 5패턴 전부 미매치 → `parsed=null` 로 mismatch 검사 자체가 조용히 스킵.
|
|
10
|
+
|
|
11
|
+
### 수정
|
|
12
|
+
- **부풀림 게이팅 (`_declaredPassMismatch`)**: 실행 pass < 주장 pass 면 human `overallFail` + `--json` exit + collect(`--all`/`gate --claims`) `declared-pass-mismatch` reason 3경로 FAIL. **방향성 게이트** — 실행 ≥ 주장(테스트가 늘어난 정상 흐름)은 무벌점(testCountMatch 의 "실측≥주장" 규칙과 동일 철학, 1.18.1식 거짓차단 FP 회피). 완화: `--lenient`. 요약 라벨도 "✗ FAIL (주장이 실행 결과보다 부풀려짐)" 로 자기모순 제거.
|
|
13
|
+
- **`_parseTestStdout` 순수 추출 + spec 리포터 패턴 6**: 기존 5패턴(비율/jest/mocha/tap/pytest) 원형 유지 + node:test spec 리포터(`pass N`/`fail N` 라인-전체 앵커, denom = pass+fail) 신규 — 선두 비단어문자만 허용해 타 러너 프로즈 오탐 차단(테스트명 "pass 2 (1 ms)" 미매치 확인).
|
|
14
|
+
- **단건 `--json` top-level `ok`/`reasons` (평가 개선제안 ②)**: CI 가 verdict 필드를 재조합할 필요 없이 collect 경로와 동일 어휘. exit 는 reasons 기반 단일 판정으로 통합 — ok↔exit 불일치 원천 차단(기존 게이트 동일).
|
|
15
|
+
|
|
16
|
+
### 검증
|
|
17
|
+
- selftest **267** (신규: 파서 5+스펙 행위 + 게이팅 3경로 와이어 + json ok/reasons). full e2e **382** (신규: 부풀림 exit 1 human/json/--all + spec 파싱 + 정직주장/실행≥주장 무벌점 FP 가드).
|
|
18
|
+
|
|
3
19
|
## 1.35.6 — 2026-07-02 — 18th 위임실증: codex-leerness 준수 라이브 검증 + dispatch harness 브리프 + bench stdin hang 수정
|
|
4
20
|
|
|
5
21
|
**위임 실증 라운드 (사용자 명시: "codex가 leerness를 참조하고 작업을 진행하는지 확인 후 고도화")**. 라이브 재현: leerness init 된 임시 프로젝트에 위임 룰(codex 구현/Claude 검수) 등록 → **leerness 를 전혀 언급하지 않은** 순수 코딩 태스크를 dispatch 레시피 그대로 codex(0.141)에 위임.
|
|
@@ -12,7 +28,7 @@
|
|
|
12
28
|
- **agents bench codex stdin hang (확인, 실버그)**: `codex exec` 는 stdin 이 열린 파이프면 `Reading additional input from stdin...` 에서 EOF 를 무한 대기(라이브 재현 — 40바이트 출력으로 8분+ hang). bench 의 `cp.spawn(..., { shell: true })` 는 기본 stdio pipe 로 stdin 을 닫지 않아 codex 가 항상 타임아웃으로 왜곡됨 → `stdio: ['ignore','pipe','pipe']`. dispatch 안내에도 비대화형 spawn 시 stdin 닫기 경고 추가.
|
|
13
29
|
|
|
14
30
|
### 검증
|
|
15
|
-
- selftest **266** (신규: `_harnessBrief` 내용/셸-안전 + dispatch 접두 와이어 + bench stdio 가드). full e2e (
|
|
31
|
+
- selftest **266** (신규: `_harnessBrief` 내용/셸-안전 + dispatch 접두 와이어 + bench stdio 가드). full e2e **381/381** (flag-bleed 테스트는 의도 보존형으로 갱신 — 기본: 브리프 뒤 태스크 비오염, --raw: 1.9.435 원형). **npm 배포됨** (사용자 지시, automation token) — 게시본 재실증: selftest 266 + dispatch 브리프/--raw 행위 확인.
|
|
16
32
|
|
|
17
33
|
## 1.35.5 — 2026-06-27 — 17th 버그헌트: scan .json5/.jsonc FN + verify-claim git 매칭 정밀도
|
|
18
34
|
|
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@ MIT
|
|
|
115
115
|
<!-- leerness:project-readme:start -->
|
|
116
116
|
## Leerness Project Harness
|
|
117
117
|
|
|
118
|
-
이 프로젝트는 Leerness v1.35.
|
|
118
|
+
이 프로젝트는 Leerness v1.35.7 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
119
119
|
|
|
120
120
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
121
121
|
|
|
@@ -169,7 +169,7 @@ leerness memory restore decision <date|title>
|
|
|
169
169
|
|
|
170
170
|
### MCP server (외부 AI 통합)
|
|
171
171
|
|
|
172
|
-
Leerness v1.35.
|
|
172
|
+
Leerness v1.35.7는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
173
173
|
|
|
174
174
|
```jsonc
|
|
175
175
|
// 카테고리별
|
|
@@ -190,7 +190,7 @@ Leerness v1.35.6는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code
|
|
|
190
190
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
191
191
|
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) 다음 라운드 예약.
|
|
192
192
|
|
|
193
|
-
현재 누적: **70 라운드 (1.9.40 → 1.35.
|
|
193
|
+
현재 누적: **70 라운드 (1.9.40 → 1.35.7)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
194
194
|
|
|
195
195
|
### 성능 가이드 (1.9.140 측정)
|
|
196
196
|
|
|
@@ -228,6 +228,6 @@ leerness release pack --close --auto-main-push
|
|
|
228
228
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
229
229
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
230
230
|
|
|
231
|
-
Last synced by Leerness v1.35.
|
|
231
|
+
Last synced by Leerness v1.35.7: 2026-07-03
|
|
232
232
|
<!-- leerness:project-readme:end -->
|
|
233
233
|
|
package/bin/leerness.js
CHANGED
|
@@ -32,7 +32,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
32
32
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
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
|
|
|
35
|
-
const VERSION = '1.35.
|
|
35
|
+
const VERSION = '1.35.7';
|
|
36
36
|
|
|
37
37
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
38
38
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -2885,11 +2885,31 @@ function _selfTestCases() {
|
|
|
2885
2885
|
const stdinFixed = agentsSrc.includes("{ shell: true, stdio: ['ignore', 'pipe', 'pipe'] }");
|
|
2886
2886
|
return briefOk && wired && stdinFixed;
|
|
2887
2887
|
} },
|
|
2888
|
+
{ name: '19th GPT5.5평가 (UR-0013): _parseTestStdout 러너 인식(jest/tap/spec/비율) + declared-pass 부풀림 3경로 게이팅 + json ok/reasons (1.35.7)', run: () => {
|
|
2889
|
+
// (1) 파서 행위: 기존 4형식 보존 + spec 리포터(패턴 6) 신규 — denom = pass + fail.
|
|
2890
|
+
const p = _parseTestStdout;
|
|
2891
|
+
const tap = p('# tests 2\n# pass 2\n# fail 0');
|
|
2892
|
+
const spec = p('ℹ tests 2\nℹ suites 0\nℹ pass 2\nℹ fail 0\n');
|
|
2893
|
+
const specFail = p('ℹ pass 3\nℹ fail 1\n');
|
|
2894
|
+
const jest = p('Tests: 5 passed, 6 total');
|
|
2895
|
+
const ratio = p('12/12 passed');
|
|
2896
|
+
const parseOk = tap && tap.num === 2 && tap.denom === 2 && spec && spec.num === 2 && spec.denom === 2
|
|
2897
|
+
&& specFail && specFail.num === 3 && specFail.denom === 4 && jest && jest.num === 5 && jest.denom === 6
|
|
2898
|
+
&& ratio && ratio.num === 12 && p('no test output here') === null && p(' ✓ pass 2 (1 ms) extra') === null;
|
|
2899
|
+
// (2) 게이팅 와이어: 3경로(human overallFail / json reasons / collect reasons) + 방향성(실행 pass < 주장 pass 만 FAIL).
|
|
2900
|
+
const src = read(__filename);
|
|
2901
|
+
const gateWired = /overallFail = [^\n]*_declaredPassMismatch/.test(src)
|
|
2902
|
+
&& (src.match(/reasons\.push\('declared-pass-mismatch'\)/g) || []).length === 2
|
|
2903
|
+
&& src.includes('runResult.parsed.num < declaredPass.num');
|
|
2904
|
+
// (3) 단건 --json top-level ok/reasons — exit 는 reasons 기반 단일 판정.
|
|
2905
|
+
const jsonOk = src.includes('out.ok = out.reasons.length === 0') && src.includes('if (!out.ok) return process.exit(1)');
|
|
2906
|
+
return parseOk && gateWired && jsonOk;
|
|
2907
|
+
} },
|
|
2888
2908
|
{ name: 'honesty-check: AI 인식론적 정직성 3차원 + MCP/CLI/strict 통합 (사용자명시 1.9.305)', run: () => { const h = _epistemicHonestyCheck; const d1 = h('이 기능은 항상 정상 동작합니다').findings.some(f => f.dim === 'pretend-knowledge'); const d2 = h('아마 될 것 같습니다. 구현 완료했습니다').findings.some(f => f.dim === 'premature-judgment'); const d3 = h('이 API 의 rate limit 은 초당 5회입니다').findings.some(f => f.dim === 'no-info-gathering'); const clean = h('src/api.js 수정, 12/12 통과 (Exit: 0)').ok === true; const src = read(__filename); const wired = require('../lib/mcp-tools').some(t => t.name === 'leerness_honesty_check') && /if \(cmd === 'honesty-check'\)/.test(src) && /honestyFindings = _epistemicHonestyCheck/.test(src); return d1 && d2 && d3 && clean && wired; } },
|
|
2889
2909
|
{ name: 'exit code 일관성: fail()→exitCode 1 행위 + unknown 명령 안내 (UR-0045 / CV-5 행위화 1.9.366)', run: () => { if (typeof fail !== 'function') return false; const saved = process.exitCode; const _w = process.stdout.write; let setOk = false; try { process.stdout.write = () => true; process.exitCode = 0; fail('selftest probe'); setOk = process.exitCode === 1; } finally { process.stdout.write = _w; process.exitCode = saved; } const src = read(__filename); const dispatchOk = /알 수 없는 명령: \$\{cmd\}/.test(src); return setOk && dispatchOk; } },
|
|
2890
2910
|
{ name: 'brief: 프로젝트 청사진 set/show/export + README 개요 섹션 (UR-0055 사용자명시 1.9.307)', run: () => { const src = read(__filename); const fnOk = typeof briefCmd === 'function' && typeof _loadBrief === 'function' && typeof _briefBlueprint === 'function' && _BRIEF_FIELDS.length === 10; const b = { project: 'X', intro: 'i', purpose: 'p', problem: '', features: ['f1', 'f2'], stack: ['s'], architecture: '', users: [], success: [], nonGoals: [], currentState: '' }; const bp = _briefBlueprint(b, VERSION); const bpOk = /Blueprint/.test(bp) && /소개 \(Intro\)/.test(bp) && /f1/.test(bp) && /신규 프로젝트 시작 가이드/.test(bp); const rb = _briefReadmeBlock(b); const rbOk = rb.includes(BRIEF_START) && rb.includes(BRIEF_END) && /프로젝트 개요/.test(rb) && /\*\*목적\*\*/.test(rb); return fnOk && bpOk && rbOk && /if \(cmd === 'brief'\)/.test(src); } },
|
|
2891
2911
|
{ name: 'brief 2단계: update --direction 이력 + MCP leerness_brief + context 통합 (UR-0055 1.9.308)', run: () => { const src = read(__filename); const b = { project: 'X', intro: '', purpose: '', problem: '', features: [], stack: [], architecture: '', users: [], success: [], nonGoals: [], currentState: '', directionHistory: ['2026-06-04: 확대'] }; const bpOk = /개발 방향 이력/.test(_briefBlueprint(b, VERSION)) && /최근 개발 방향 변경/.test(_briefReadmeBlock(b)); const histWired = /sub === 'update'/.test(src) && /brief\.directionHistory \|\| \[\]\), `\$\{today\(\)\}/.test(src); const mcpOk = require('../lib/mcp-tools').some(t => t.name === 'leerness_brief'); const ctxOk = /brief: \{ intro:/.test(src); return bpOk && histWired && mcpOk && ctxOk; } },
|
|
2892
|
-
{ name: 'verify-claim: done 주장 evidence 기본강제 + --lenient + MCP/json 도달 (UR-0048 설치리뷰 critical 1.9.309)', run: () => { const src = read(__filename); const def = /const mustHaveEvidence = !has\('--lenient'\) && \(isDoneClaim \|\| has\('--require-evidence'\)\)/.test(src); const threshold = /has\('--require-evidence'\) \? evq\.ok : \(evq\.hasFile \|\| evq\.hasTest \|\| evq\.hasLog\)/.test(src); const jsonWired = /evidenceComplete:/.test(src) &&
|
|
2912
|
+
{ name: 'verify-claim: done 주장 evidence 기본강제 + --lenient + MCP/json 도달 (UR-0048 설치리뷰 critical 1.9.309)', run: () => { const src = read(__filename); const def = /const mustHaveEvidence = !has\('--lenient'\) && \(isDoneClaim \|\| has\('--require-evidence'\)\)/.test(src); const threshold = /has\('--require-evidence'\) \? evq\.ok : \(evq\.hasFile \|\| evq\.hasTest \|\| evq\.hasLog\)/.test(src); const jsonWired = /evidenceComplete:/.test(src) && /if \(!evidenceQualityOk\) out\.reasons\.push\('evidence-incomplete'\)/.test(src) && /if \(!out\.ok\) return process\.exit\(1\)/.test(src); /* 1.35.7: reasons 기반 단일 exit 재배선(의도 동일 — evidence 게이트가 json exit 도달) */ const mcpLenient = !!require('../lib/mcp-tools').find(t => t.name === 'leerness_verify_claim').inputSchema.properties.lenient; return def && threshold && jsonWired && mcpLenient; } },
|
|
2893
2913
|
{ name: '입력 스키마 검증: task status/rule trigger 무효값 거부 + every-round 보존 (UR-0046 설치리뷰 1.9.310)', run: () => { const src = read(__filename); const sets = TASK_STATUSES.has('done') && TASK_STATUSES.has('in-progress') && !TASK_STATUSES.has('nonsense') && RULE_TRIGGERS.has('every-round') && RULE_TRIGGERS.has('every-update') && !RULE_TRIGGERS.has('not-a-trigger'); const helper = typeof _validateChoice === 'function' && _validateChoice('done', TASK_STATUSES, 'x') === true; const wired = /_validateChoice\(arg\('--status', null\), TASK_STATUSES/.test(src) && /_validateChoice\(trigger, RULE_TRIGGERS/.test(src); return sets && helper && wired; } },
|
|
2894
2914
|
{ name: 'init 가드: 미초기화 write 차단 + 다중마커 판별 + --force 우회 (UR-0047 설치리뷰 1.9.311)', run: () => { const src = read(__filename); const fnOk = typeof _isInitialized === 'function' && typeof _requireInit === 'function'; const _fix = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_initfix_')); let liveOk = false; try { fs.writeFileSync(path.join(_fix, 'AGENTS.md'), 'x'); liveOk = _isInitialized(_fix) === true; } finally { try { fs.rmSync(_fix, { recursive: true, force: true }); } catch {} } const emptyOk = _isInitialized(path.join(os.tmpdir(), '__leerness_noinit_marker__')) === false; const wired = ["task add", "task update", "plan add", "decision add", "rule add", "lesson save", "brief set"].every(l => src.includes(`_requireInit(root, '${l}')`)) && !src.includes("_requireInit(root, 'state " + "start')"); const force = /if \(_isInitialized\(root\) \|\| has\('--force'\)\) return true/.test(src); return fnOk && liveOk && emptyOk && wired && force; } },
|
|
2895
2915
|
{ name: 'secret 스캐너 현대 키: OpenAI proj/svcacct·Anthropic api03(_)·GitHub 변종·Stripe·npm 검출 + 오탐 가드 (UR-0050 설치리뷰 1.9.312)', run: () => { const hit = (s) => SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s); }); const named = (s, nm) => SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s) && p.name === nm; }); const A = 'A'.repeat(40); const projKey = 'sk-' + 'proj-' + A + '_' + A; const svcKey = 'sk-' + 'svcacct-' + A; const antKey = 'sk-' + 'ant-api03-' + A + '_' + A; const ghoKey = 'gho_' + 'a1B2'.repeat(9); const stripeKey = 'sk_' + 'live_' + A; const npmKey = 'npm_' + 'a1B2'.repeat(9); const asiaKey = 'ASIA' + 'ABCD1234EFGH5678'; const legacy = 'sk-' + A; const hits = hit(projKey) && hit(svcKey) && hit(antKey) && hit(ghoKey) && hit(stripeKey) && hit(npmKey) && hit(asiaKey) && hit(legacy); const names = named(projKey, 'OpenAI project/service key') && named(antKey, 'Anthropic API key') && named(stripeKey, 'Stripe secret key') && named(npmKey, 'npm token'); const clean = !hit('const userName = "john' + '_doe_2024";') && !hit('https://example.com/path/to/page'); return hits && names && clean; } },
|
|
@@ -10438,6 +10458,37 @@ function _vcImplIsEmpty(body) {
|
|
|
10438
10458
|
return residue === ''; // 의미 토큰이 하나도 안 남으면 스텁
|
|
10439
10459
|
}
|
|
10440
10460
|
|
|
10461
|
+
// 1.35.7 (UR-0013, GPT5.5pro 평가): --run-tests stdout 파서 순수 추출 — selftest 단위검증 가능 + spec 리포터 인식.
|
|
10462
|
+
// 패턴 1~5는 1.9.20/UR-0045 원형 그대로, 6(node:test spec 리포터 "pass N"/"fail N" 라인)만 신규.
|
|
10463
|
+
// 추가발견 배경: Node v26 은 비-TTY 에서도 spec 리포터가 기본 → TAP "# pass N" 이 없어 parsed=null,
|
|
10464
|
+
// declared-pass 불일치 검사가 조용히 스킵됐음. 라인-전체 앵커(선두 비단어문자만 허용)로 타 러너 프로즈 오탐 차단.
|
|
10465
|
+
function _parseTestStdout(out) {
|
|
10466
|
+
out = String(out || '');
|
|
10467
|
+
// 1) X/Y passing|passed|pass|통과
|
|
10468
|
+
const m = out.match(/(\d+)\s*\/\s*(\d+)\s*(?:passed|통과|pass|passing)/i);
|
|
10469
|
+
if (m) return { num: parseInt(m[1], 10), denom: parseInt(m[2], 10) };
|
|
10470
|
+
// 2) jest: "Tests: N passed, M total" — 통과 + 총
|
|
10471
|
+
const m2 = out.match(/Tests?:\s*(?:\d+\s*failed,\s*)?(\d+)\s*passed(?:,\s*(\d+)\s*total)?/i);
|
|
10472
|
+
if (m2) return { num: parseInt(m2[1], 10), denom: parseInt(m2[2] || m2[1], 10) };
|
|
10473
|
+
// 3) mocha: "N passing" — 단독 패턴이면 total = passing
|
|
10474
|
+
const m3 = out.match(/^\s*(\d+)\s+passing\b/im);
|
|
10475
|
+
if (m3) return { num: parseInt(m3[1], 10), denom: parseInt(m3[1], 10) };
|
|
10476
|
+
// 4) tap: "# pass N"
|
|
10477
|
+
const m4 = out.match(/#\s*pass\s+(\d+)/i);
|
|
10478
|
+
if (m4) return { num: parseInt(m4[1], 10), denom: parseInt(m4[1], 10) };
|
|
10479
|
+
// 5) pytest: "N passed in 0.12s" (UR-0045 — 파이썬 러너 출력 인식)
|
|
10480
|
+
const m5 = out.match(/(\d+)\s+passed\b/i);
|
|
10481
|
+
if (m5) return { num: parseInt(m5[1], 10), denom: parseInt(m5[1], 10) };
|
|
10482
|
+
// 6) node:test spec 리포터: "ℹ pass 2" / "ℹ fail 0" — denom = pass + fail (fail 라인 없으면 0)
|
|
10483
|
+
const mp6 = out.match(/^[^\w\r\n]*pass\s+(\d+)\s*$/im);
|
|
10484
|
+
if (mp6) {
|
|
10485
|
+
const mf6 = out.match(/^[^\w\r\n]*fail\s+(\d+)\s*$/im);
|
|
10486
|
+
const p6 = parseInt(mp6[1], 10);
|
|
10487
|
+
return { num: p6, denom: p6 + (mf6 ? parseInt(mf6[1], 10) : 0) };
|
|
10488
|
+
}
|
|
10489
|
+
return null;
|
|
10490
|
+
}
|
|
10491
|
+
|
|
10441
10492
|
function verifyClaimCmd(root, taskId, opts = {}) {
|
|
10442
10493
|
root = absRoot(root);
|
|
10443
10494
|
const _j = has('--json'); // 1.9.400 (7번째 버그헌트 P1-B, UR-0105): --json 에러도 구조화
|
|
@@ -10578,31 +10629,8 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
10578
10629
|
runResult = { skipped: true, reason: `테스트 명령 차단(${r.error}) — '${testCmd}' 실행 불가 (불일치 판정 아님). leerness permissions set extended 또는 allowList 에 추가 권장` };
|
|
10579
10630
|
} else {
|
|
10580
10631
|
const out = (r.stdout || '') + (r.stderr || '');
|
|
10581
|
-
// 1.9.20
|
|
10582
|
-
|
|
10583
|
-
// 1) X/Y passing|passed|pass|통과
|
|
10584
|
-
let m = out.match(/(\d+)\s*\/\s*(\d+)\s*(?:passed|통과|pass|passing)/i);
|
|
10585
|
-
if (m) parsed = { num: parseInt(m[1], 10), denom: parseInt(m[2], 10) };
|
|
10586
|
-
// 2) jest: "Tests: N passed, M total" — 통과 + 총
|
|
10587
|
-
if (!parsed) {
|
|
10588
|
-
const m2 = out.match(/Tests?:\s*(?:\d+\s*failed,\s*)?(\d+)\s*passed(?:,\s*(\d+)\s*total)?/i);
|
|
10589
|
-
if (m2) parsed = { num: parseInt(m2[1], 10), denom: parseInt(m2[2] || m2[1], 10) };
|
|
10590
|
-
}
|
|
10591
|
-
// 3) mocha: "N passing" — 단독 패턴이면 total = passing
|
|
10592
|
-
if (!parsed) {
|
|
10593
|
-
const m3 = out.match(/^\s*(\d+)\s+passing\b/im);
|
|
10594
|
-
if (m3) parsed = { num: parseInt(m3[1], 10), denom: parseInt(m3[1], 10) };
|
|
10595
|
-
}
|
|
10596
|
-
// 4) tap: "# pass N" 또는 "ok N"
|
|
10597
|
-
if (!parsed) {
|
|
10598
|
-
const m4 = out.match(/#\s*pass\s+(\d+)/i);
|
|
10599
|
-
if (m4) parsed = { num: parseInt(m4[1], 10), denom: parseInt(m4[1], 10) };
|
|
10600
|
-
}
|
|
10601
|
-
// 5) pytest: "N passed in 0.12s" (UR-0045 — 파이썬 러너 출력 인식)
|
|
10602
|
-
if (!parsed) {
|
|
10603
|
-
const m5 = out.match(/(\d+)\s+passed\b/i);
|
|
10604
|
-
if (m5) parsed = { num: parseInt(m5[1], 10), denom: parseInt(m5[1], 10) };
|
|
10605
|
-
}
|
|
10632
|
+
// 1.9.20 패턴 1~5 + 1.35.7 spec 리포터(패턴 6) — _parseTestStdout 순수 함수로 추출 (UR-0013, selftest 단위검증)
|
|
10633
|
+
const parsed = _parseTestStdout(out);
|
|
10606
10634
|
runResult = {
|
|
10607
10635
|
skipped: false,
|
|
10608
10636
|
cmd: testCmd,
|
|
@@ -10642,6 +10670,11 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
10642
10670
|
const gitStrongMismatch = gitApplicable && claimedInGit.length === 0; // 변경 있는데 주장 파일이 git 변경에 전무
|
|
10643
10671
|
// 1.11.2 (UR-0175): git strongMismatch 는 기본 게이트에서 제외(advisory) — 커밋 후 검증(정상 흐름)에서 커밋된 파일이 working-tree 변경에 없어 false-fail 하므로. --strict-claims 시에만 FAIL 기여. 기본 게이트는 신뢰도 높은 optimism(claimsConsistent)이 담당.
|
|
10644
10672
|
const gitClaimOk = !(has('--strict-claims') && gitStrongMismatch);
|
|
10673
|
+
// 1.35.7 (UR-0013, GPT5.5pro 평가 확정버그): declared-pass 부풀림 게이팅 — 주장 비율(예: 3/3)의 pass 가 실행 pass(예: 2)보다 크면 FAIL.
|
|
10674
|
+
// 이전: 3경로(human overallFail / --json exit / collect reasons) 모두 불일치를 감지·표시만 하고 exit 0 — 부풀린 주장이 플래그십을 통과 + 최종요약은 "일치 ✓" 자기모순.
|
|
10675
|
+
// 주장 개수("N개")는 이미 게이팅(testCountMatch)되는데 비율 주장("N/M")만 안 되던 내부 비일관성 해소.
|
|
10676
|
+
// 방향성 게이트(실행 pass ≥ 주장 pass 면 통과) — 테스트가 늘어난 정상 흐름을 벌하지 않음(testCountMatch 의 실측≥주장 규칙과 동일 철학). 완화: --lenient.
|
|
10677
|
+
const _declaredPassMismatch = !lenient && !!(runResult && !runResult.skipped && runResult.parsed && declaredPass && runResult.parsed.num < declaredPass.num);
|
|
10645
10678
|
|
|
10646
10679
|
// 1.33.2 (verify-claim --all): 집계 모드는 렌더/exit 없이 verdict 만 반환. 게이팅 부울은 아래 --json/human 경로와 동일 계산을 공유(분기 없음).
|
|
10647
10680
|
if (opts.collect) {
|
|
@@ -10656,6 +10689,7 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
10656
10689
|
if (claimsChecked && stubFiles.length > 0) reasons.push('stub-impl');
|
|
10657
10690
|
if (has('--strict-claims') && testLinkOk === false) reasons.push('test-impl-unlinked');
|
|
10658
10691
|
if (_runFail) reasons.push('tests-failed');
|
|
10692
|
+
if (_declaredPassMismatch) reasons.push('declared-pass-mismatch'); // 1.35.7 (UR-0013): --all/gate --claims 도 부풀림 게이팅
|
|
10659
10693
|
return { id: taskId, request: row.request, status: row.status, ok: reasons.length === 0, reasons };
|
|
10660
10694
|
}
|
|
10661
10695
|
|
|
@@ -10688,10 +10722,21 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
10688
10722
|
out.verdict.declaredPassMatches = (runResult.parsed.num === declaredPass.num && runResult.parsed.denom === declaredPass.denom);
|
|
10689
10723
|
}
|
|
10690
10724
|
}
|
|
10725
|
+
// 1.35.7 (UR-0013 GPT5.5평가 개선②): top-level ok/reasons — CI 가 verdict 필드를 재조합하지 않아도 되도록 collect 경로와 동일 어휘.
|
|
10726
|
+
// exit 는 reasons 기반 단일 판정(ok↔exit 불일치 원천 차단) — 기존 게이트(1.11.2/1.17.3/1.17.4) 동일 + declared-pass-mismatch 추가.
|
|
10727
|
+
out.reasons = [];
|
|
10728
|
+
if (!filesAllExist) out.reasons.push('files-missing');
|
|
10729
|
+
if (out.verdict.testCountMatch === false) out.reasons.push('test-count-short');
|
|
10730
|
+
if (!evidenceQualityOk) out.reasons.push('evidence-incomplete');
|
|
10731
|
+
if (claimsChecked && !strictOk) out.reasons.push('optimism/honesty');
|
|
10732
|
+
if (!gitClaimOk) out.reasons.push('git-mismatch');
|
|
10733
|
+
if (claimsChecked && stubFiles.length > 0) out.reasons.push('stub-impl');
|
|
10734
|
+
if (has('--strict-claims') && testLinkOk === false) out.reasons.push('test-impl-unlinked');
|
|
10735
|
+
if (runResult && !runResult.skipped && !runResult.allPassed) out.reasons.push('tests-failed');
|
|
10736
|
+
if (_declaredPassMismatch) out.reasons.push('declared-pass-mismatch');
|
|
10737
|
+
out.ok = out.reasons.length === 0;
|
|
10691
10738
|
log(JSON.stringify(out, null, 2));
|
|
10692
|
-
if (
|
|
10693
|
-
// 1.11.2 (UR-0175): --json 도 optimism+git 게이팅 — 머신 경로가 허위완료를 통과시키지 않도록(human 경로와 동일).
|
|
10694
|
-
if (!filesAllExist || out.verdict.testCountMatch === false || !evidenceQualityOk || (claimsChecked && !strictOk) || !gitClaimOk || (claimsChecked && stubFiles.length > 0) || (has('--strict-claims') && testLinkOk === false)) return process.exit(1); // 1.17.3 (UR-0046): 스텁(done 기본) + 테스트 미연결(strict). 1.17.4 (UR-0047): testCountMatch null(측정불가)은 미기여
|
|
10739
|
+
if (!out.ok) return process.exit(1);
|
|
10695
10740
|
return;
|
|
10696
10741
|
}
|
|
10697
10742
|
|
|
@@ -10746,7 +10791,7 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
10746
10791
|
if (runResult && !runResult.skipped) {
|
|
10747
10792
|
// 1.27.1 (13번째 외부리뷰 #3): exit 0 인데 테스트 비율을 못 파싱한 경우(예: 비-테스트 --test-cmd)를 '✓ all passed' 로 거짓표기하지 않음 — '실행됨, 테스트 수 미확인' 으로 정직 표기(판정/exit 불변 → 이색 테스트러너 FP 없음).
|
|
10748
10793
|
log(` - ${runResult.cmd || 'npm test'} ${t('실행', 'run')}: ${runTestsOk ? (runResult.parsed ? '✓ all passed' : t('✓ 실행됨 (exit 0) — 테스트 수 미확인', '✓ ran (exit 0) — test count unconfirmed')) : '✗ FAIL'}`);
|
|
10749
|
-
if (declaredPass) log(` - ${t('주장과 실행 결과 일치', 'claimed matches run')}: ${declaredPassMatchesActual ? '✓ pass' : t('⚠ 다름', '⚠ differs')}`);
|
|
10794
|
+
if (declaredPass) log(` - ${t('주장과 실행 결과 일치', 'claimed matches run')}: ${declaredPassMatchesActual ? '✓ pass' : (_declaredPassMismatch ? t('✗ FAIL (주장이 실행 결과보다 부풀려짐)', '✗ FAIL (claim inflated vs run)') : t('⚠ 다름 (실행 ≥ 주장 — pass)', '⚠ differs (run ≥ claimed — pass)'))}`); // 1.35.7 (UR-0013): 자기모순 표기 제거 — 부풀림은 FAIL 라벨
|
|
10750
10795
|
}
|
|
10751
10796
|
// 1.11.2 (UR-0175): optimism+정직성 — done 주장은 기본 게이팅(claimsChecked). 완화: --lenient.
|
|
10752
10797
|
if (claimsChecked) {
|
|
@@ -10784,7 +10829,7 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
10784
10829
|
} else if (testLinkOk === true && claimsChecked) {
|
|
10785
10830
|
log(` - ${t('테스트-구현 연결', 'test-impl link')}: ${t('✓ pass (테스트가 구현을 참조)', '✓ pass (test references the impl)')}`);
|
|
10786
10831
|
}
|
|
10787
|
-
const overallFail = !allFilesOk || !testOk || (runResult && !runResult.skipped && !runTestsOk) || (claimsChecked && !strictOk) || !evidenceQualityOk || !gitClaimOk || (claimsChecked && stubFiles.length > 0) || (has('--strict-claims') && testLinkOk === false);
|
|
10832
|
+
const overallFail = !allFilesOk || !testOk || (runResult && !runResult.skipped && !runTestsOk) || (claimsChecked && !strictOk) || !evidenceQualityOk || !gitClaimOk || (claimsChecked && stubFiles.length > 0) || (has('--strict-claims') && testLinkOk === false) || _declaredPassMismatch; // 1.35.7 (UR-0013): declared-pass 부풀림 게이팅
|
|
10788
10833
|
// 1.9.287: 정직한 한계 고지 — 테스트 통과 ≠ 의미적 구현 정확성
|
|
10789
10834
|
if (claimsChecked || mustHaveEvidence) {
|
|
10790
10835
|
log('');
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -6818,5 +6818,43 @@ total++;
|
|
|
6818
6818
|
if (!ok) failed++;
|
|
6819
6819
|
}
|
|
6820
6820
|
|
|
6821
|
+
// 1.35.7 (UR-0013, GPT5.5pro 평가): declared-pass 부풀림 게이팅 — 주장(3/3) vs 실행(2/2) → exit 1 (human/--json/--all 3경로) + spec 리포터 파싱(패턴 6) + 실행≥주장 무벌점.
|
|
6822
|
+
total++;
|
|
6823
|
+
{
|
|
6824
|
+
let ok = false;
|
|
6825
|
+
try {
|
|
6826
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dpm-'));
|
|
6827
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
6828
|
+
fs.mkdirSync(path.join(d, 'src'), { recursive: true });
|
|
6829
|
+
fs.writeFileSync(path.join(d, 'src', 'x.js'), 'module.exports = { f: () => 1 };\n');
|
|
6830
|
+
fs.writeFileSync(path.join(d, 'x.test.js'), 'test();\ntest();\n');
|
|
6831
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'dpm', '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
6832
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'update', 'T-0002', '--status', 'done', '--evidence', 'src/x.js implemented, x.test.js added; 3/3 passed', '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
6833
|
+
// (1) 부풀림(주장 3/3, 실행 2/2 — jest 형식 echo): human exit 1 + FAIL 라벨
|
|
6834
|
+
const r1 = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--run-tests', '--test-cmd', 'echo Tests: 2 passed, 2 total', '--path', d], { encoding: 'utf8', timeout: 20000 });
|
|
6835
|
+
const inflatedFails = r1.status === 1 && /부풀려짐|inflated/.test(r1.stdout);
|
|
6836
|
+
// (2) --json: top-level ok=false + reasons 에 declared-pass-mismatch + verdict 필드 유지
|
|
6837
|
+
const r2 = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--run-tests', '--test-cmd', 'echo Tests: 2 passed, 2 total', '--json', '--path', d], { encoding: 'utf8', timeout: 20000 });
|
|
6838
|
+
let jsonOk = false; try { const j = JSON.parse(r2.stdout); jsonOk = r2.status === 1 && j.ok === false && j.reasons.includes('declared-pass-mismatch') && j.verdict.declaredPassMatches === false; } catch {}
|
|
6839
|
+
// (3) --all collect: 동일 reason 으로 집계 실패
|
|
6840
|
+
const r3 = cp.spawnSync(process.execPath, [CLI, 'verify-claim', '--all', '--run-tests', '--test-cmd', 'echo Tests: 2 passed, 2 total', '--json', '--path', d], { encoding: 'utf8', timeout: 20000 });
|
|
6841
|
+
let allOk = false; try { const j3 = JSON.parse(r3.stdout); const e3 = (j3.results || []).find(x => x.id === 'T-0002'); allOk = r3.status === 1 && e3 && e3.ok === false && e3.reasons.includes('declared-pass-mismatch'); } catch {}
|
|
6842
|
+
// (4) spec 리포터 파싱(신규 패턴 6): 라인-전체 "pass 2" → parsed 2/2 → 부풀림 감지 exit 1
|
|
6843
|
+
const r4 = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--run-tests', '--test-cmd', 'echo pass 2', '--path', d], { encoding: 'utf8', timeout: 20000 });
|
|
6844
|
+
const specParsed = r4.status === 1 && /2\/2/.test(r4.stdout);
|
|
6845
|
+
// (5) 정직 주장(2/2 = 실행) → exit 0 (FP 없음) / (6) 실행이 주장보다 많음(주장 2/2, 실행 3/3) → 무벌점 exit 0
|
|
6846
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'update', 'T-0002', '--status', 'done', '--evidence', 'src/x.js implemented, x.test.js added; 2/2 passed', '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
6847
|
+
const r5 = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--run-tests', '--test-cmd', 'echo Tests: 2 passed, 2 total', '--path', d], { encoding: 'utf8', timeout: 20000 });
|
|
6848
|
+
const r6 = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--run-tests', '--test-cmd', 'echo Tests: 3 passed, 3 total', '--path', d], { encoding: 'utf8', timeout: 20000 });
|
|
6849
|
+
const truthfulOk = r5.status === 0;
|
|
6850
|
+
const growthOk = r6.status === 0;
|
|
6851
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
6852
|
+
ok = inflatedFails && jsonOk && allOk && specParsed && truthfulOk && growthOk;
|
|
6853
|
+
if (!ok) console.log(` [dpm 디버그] inflated=${inflatedFails} json=${jsonOk} all=${allOk} spec=${specParsed} truthful=${truthfulOk} growth=${growthOk}`);
|
|
6854
|
+
} catch {}
|
|
6855
|
+
console.log(ok ? '✓ B(1.35.7) UR-0013: declared-pass 부풀림 게이팅(human/json/--all) + spec 리포터 파싱 + 실행≥주장 무벌점' : '✗ declared-pass mismatch 게이팅 실패');
|
|
6856
|
+
if (!ok) failed++;
|
|
6857
|
+
}
|
|
6858
|
+
|
|
6821
6859
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
6822
6860
|
if (failed > 0) process.exit(1);
|