leerness 1.17.0 → 1.18.0
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 +93 -0
- package/README.md +5 -5
- package/bin/leerness.js +169 -37
- package/lib/pure-utils.js +1 -1
- package/lib/session-close.js +17 -1
- package/package.json +1 -1
- package/scripts/e2e.js +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,98 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.18.0 — 2026-06-10 — 🛡️ [안정화/Stable] 범용 검증 하네스 안정 minor
|
|
4
|
+
|
|
5
|
+
**🛡️ 안정화(Stable) minor — "범용 AI 코딩 하네스" 실증 격차 전부 해소.** 5개 독립 클린룸 실사용 평가(Python/Node/Rust 실개발·에이전트 교대·적대 공격)가 찾은 P1 2건 + P2 3건(1.17.2~1.17.6)을 검증·통합해 npm 공개. R-0011 정책의 9번째 minor. 새 포지셔닝: **"어떤 언어, 어떤 AI 에이전트로 작업하든 — 증거 없이는 끝났다고 말할 수 없게."**
|
|
6
|
+
|
|
7
|
+
### 이번 minor 통합 (1.17.1~1.17.6)
|
|
8
|
+
- **🌐 테스트 명령 해석 체인 (P1)**: `verify-claim --run-tests` 가 npm test 하드코딩 → `--test-cmd` > 설정 `testCommand` > 실제 npm test(placeholder 제외) > skip. 파이썬 프로젝트 오판 해소 + pytest 출력 파싱.
|
|
9
|
+
- **🛡️ 빈껍데기·가짜 테스트 차단 (P1)**: 주석뿐 구현(비주석 코드 0줄) done 게이팅 FAIL + 테스트-구현 연결 검사(12개 언어, 파이썬 `#` 스텁 포함). `--json implementationSubstance/testImplLink`.
|
|
10
|
+
- **🧪 테스트 카운트 정직화 (P2)**: pytest·루트 `*.test.*` 관례 인식 + 측정불가="검증 미수행"(통과 위장 금지) + 한국어 "테스트 N개" 주장 추출(부풀린 주장 차단).
|
|
11
|
+
- **🔗 unknown flag 거부 (P2)**: `task update --next-action` 같은 미존재 옵션이 값을 조용히 버리던 것 → 거부 + did-you-mean(인수인계 유실 차단).
|
|
12
|
+
- **🔚 마감 정합 (P2)**: session close 가 done 낙관 의심·살아있는 시크릿을 재확인해 표면화 — 마감이 마지막 관문 역할.
|
|
13
|
+
- **plan add 공백제목 손상 수정** + milestone 파서 개행 미흡수(1.17.1).
|
|
14
|
+
|
|
15
|
+
### 검증 (회귀 0)
|
|
16
|
+
- **selftest 222 PASS** · **E2E 365/365 PASS** · npm gate=minor_bump. 각 격차 행위 재현(공격 차단 + 정직 작업 과탐 0) 완료.
|
|
17
|
+
|
|
18
|
+
### 안정화 표시 (R-0006)
|
|
19
|
+
CHANGELOG [안정화/Stable] · git tag (Stable) · GitHub release (Stable) · npm dist-tag `stable` 시도.
|
|
20
|
+
|
|
21
|
+
## 1.17.6 — 2026-06-10 — 범용성⑤(P2 완결): session close 마감 정합 (UR-0049)
|
|
22
|
+
|
|
23
|
+
**🔚 마감이 "마지막 관문" 역할을 하도록.** 5축 실증에서 — verify-claim 이 거짓으로 판정했던 task 가 마감 보고서에 평범한 "done"으로, gate 가 시크릿으로 실패 중인데 "clean (sleep 안전)"으로 선언되던 정합 문제 해소. **이로써 5축 격차(P1 2건 + P2 3건) 전부 닫힘.**
|
|
24
|
+
|
|
25
|
+
### 변경 (UR-0049)
|
|
26
|
+
- **done 낙관 재확인**: 마감 시 done task 들의 evidence 를 코드 흔적과 재대조(`_detectOptimism`) — verify-claim 을 건너뛴 거짓 주장(예: "DB 저장 구현" 인데 코드에 DB 호출 없음)을 `⚠ 낙관 의심 미해소` 로 표면화. `--json completionHonesty.optimismUnresolved`.
|
|
27
|
+
- **마감 보안 재확인**: 커밋 대상 시크릿이 살아있으면 `🚨 마감 보안: N건 미해소 — clean 아님` 표면화. `--json closeSecurity.committedSecrets`.
|
|
28
|
+
- advisory(차단 X) — 마감 흐름은 유지하되 거짓/위험이 조용히 통과하지 않음.
|
|
29
|
+
|
|
30
|
+
### 검증 (회귀 0)
|
|
31
|
+
- **selftest 221→222** · **E2E 365/365** · 행위 3종: 거짓 DB 주장 done + 시크릿 → 두 경고 표면화 ✓ · --json 필드 ✓ · 진짜 구현+시크릿 제거 → 경고 0(과탐 0) ✓.
|
|
32
|
+
- patch(1.17.6) — npm 미배포(R-0011, GitHub). **다음: 1.18.0 [Stable] "범용 검증 하네스" minor(1.17.1~1.17.6 묶음 공개).**
|
|
33
|
+
|
|
34
|
+
## 1.17.5 — 2026-06-10 — 범용성④: 모르는 옵션 조용히 무시 차단 (UR-0048)
|
|
35
|
+
|
|
36
|
+
**🔗 인수인계 유실의 최악 양식 차단.** 5축 실증에서 — `task update --next-action "..."` 처럼 존재하지 않는 옵션을 줘도 "✓ task updated"만 출력하고 값을 버려, **쓴 에이전트는 기록됐다고 믿고 다음 에이전트는 placeholder 를 받던** P2 해소.
|
|
37
|
+
|
|
38
|
+
### 변경 (UR-0048)
|
|
39
|
+
- **`task update` unknown flag 거부**: 지원 외 `--플래그` 발견 시 exit 1 + **did-you-mean**(`--next-action` → "혹시 `--next`?") + 지원 플래그/사용법 안내. `--json` 은 `{code:'unknown_flag'}` 구조화.
|
|
40
|
+
- 전역 플래그(`--path/--json/--force/--lenient`)는 항상 허용 — 과탐 0. 재사용 가능한 `_rejectUnknownFlags` 헬퍼(다른 쓰기 명령 확대 여지).
|
|
41
|
+
|
|
42
|
+
### 검증 (회귀 0)
|
|
43
|
+
- **selftest 220→221** · **E2E 365/365** · 행위 4종: `--next-action` 거부+제안 ✓ · `--json` 구조화 ✓ · 올바른 `--next` 정상 저장 ✓ · 전역 플래그 통과 ✓.
|
|
44
|
+
- patch(1.17.5) — npm 미배포(R-0011, GitHub). 잔여: UR-0049(마감 정합) → 완료 시 1.18.0 "범용 검증" minor.
|
|
45
|
+
|
|
46
|
+
## 1.17.4 — 2026-06-10 — 범용성③: 테스트 카운트 "측정실패=통과" 역전 해소 (UR-0047)
|
|
47
|
+
|
|
48
|
+
**🧪 5개 클린룸 리포트가 공통 지적한 P2.** "실측: 테스트 파일 못 찾음"이라면서 바로 아래 "✓ pass (실측 ≥ 주장)"로 통과시키던 모순 — 부풀린 테스트 주장("cargo test 12개", "테스트 50개")이 검증을 헛통과하던 것 해소.
|
|
49
|
+
|
|
50
|
+
### 변경 (UR-0047)
|
|
51
|
+
- **관례 확대**: 주장된 테스트 파일 우선 합산 + 글롭(pytest `test_*.py`·`*_test.py` / 루트·tests/ 의 `*.test.*`·`*.spec.*`) — 이전엔 `tests/test.js` 등 3개 하드코딩. 파이썬은 `def test_` 개수로 카운트.
|
|
52
|
+
- **측정불가 = 검증 미수행**: 테스트 파일을 못 찾으면 "⊘ 측정 불가 — 주장 N개 검증 미수행 (pass 아님)" 정직 표기 + verdict `testCountMatch: null`(게이팅 미기여 — false 만 FAIL).
|
|
53
|
+
- **🐛 주장 추출 보강(맹신 X 행위검증 중 발견)**: "테스트 50개"(한국어 자연어순)를 못 잡아 **부풀린 주장이 카운트 검증을 아예 안 탔음** → `테스트 N개` 패턴 추가.
|
|
54
|
+
|
|
55
|
+
### 검증 (회귀 0)
|
|
56
|
+
- **selftest 219→220** · **E2E 365/365** · 행위 4종: pytest 실측(2개) ✓ · **부풀린 "테스트 50개"(실측 2) exit 1 차단**(이전 헛통과) · 측정불가 정직 표기 ✓ · 루트 `*.test.js` 인식(실측 3) ✓ · 정직 주장 exit 0(과탐 0).
|
|
57
|
+
- patch(1.17.4) — npm 미배포(R-0011, GitHub). 잔여: UR-0048(unknown flag)·UR-0049(마감 정합).
|
|
58
|
+
|
|
59
|
+
## 1.17.3 — 2026-06-10 — 범용성②: 빈껍데기 구현·가짜 테스트 차단 (UR-0046)
|
|
60
|
+
|
|
61
|
+
**🛡️ 간판 기능 "거짓완료 차단"의 상한선 끌어올리기.** 5축 실증 분석의 Attack C — 주석뿐인 구현 파일 + 아무것도 검사하지 않는 `assert(true)` 테스트가 `verify-claim` 을 **exit 0 으로 완전 통과**하던 P1 — 을 차단.
|
|
62
|
+
|
|
63
|
+
### 변경 (UR-0046)
|
|
64
|
+
- **구현 실체(스텁) 검사**: 주장된 구현 파일(테스트 제외, js/ts/py/go/rs 등 12종 확장자)의 **비주석 코드줄이 0** 이면 done 게이팅 FAIL — "주석/TODO 뿐인 빈껍데기"를 확실 신호만으로 차단(과탐 0).
|
|
65
|
+
- **테스트-구현 연결 검사**: 구현+테스트가 함께 주장됐는데 어떤 테스트도 구현 파일명을 참조하지 않으면 "빈 테스트 의심" advisory ⚠ (`--strict-claims` 시 FAIL).
|
|
66
|
+
- `--json` verdict 에 `implementationSubstance` / `testImplLink` + `stubFiles` 노출(머신 경로 동일 게이팅).
|
|
67
|
+
|
|
68
|
+
### 검증 (회귀 0)
|
|
69
|
+
- **selftest 218→219** · **E2E 365/365** · Attack C 재현: 스텁+가짜테스트 **exit 0→1 차단** · 진짜 구현 exit 0(과탐 0) · **파이썬 스텁(#주석뿐)도 차단**(언어중립) · --json verdict 노출.
|
|
70
|
+
- patch(1.17.3) — npm 미배포(R-0011, GitHub). 잔여 격차: UR-0047(테스트카운트 역전)·0048(unknown flag)·0049(마감 정합).
|
|
71
|
+
|
|
72
|
+
## 1.17.2 — 2026-06-10 — 범용성①: verify-claim 테스트 명령 해석 체인 (UR-0045)
|
|
73
|
+
|
|
74
|
+
**🌐 "범용 AI 코딩 하네스" 5축 실증 분석의 P1 1호 해소.** `verify-claim --run-tests` 가 `npm test` 하드코딩이라 — 테스트가 전부 통과한 파이썬 프로젝트(npm init 잔재 placeholder 존재)를 "주장 불일치 FAIL"로 오판하던 범용성 정면 반례를 수정.
|
|
75
|
+
|
|
76
|
+
### 변경 (UR-0045)
|
|
77
|
+
- **테스트 명령 해석 체인**: `--test-cmd "<명령>"` > `.harness/leerness-config.json` 의 `"testCommand"` > package.json 의 **실제** test 스크립트(npm placeholder "no test specified" 는 테스트가 아니므로 제외) > **skip 표기**(불일치 판정 금지).
|
|
78
|
+
- **pytest 출력 파싱**: "N passed in …s" 형식 인식(파이썬 러너 pass/fail 비율 표시).
|
|
79
|
+
- 신규 value-flag `--test-cmd` 를 `nonFlagArgs` withValue 에 등록(1.14.2 교훈 적용 — 제목 흡수 차단).
|
|
80
|
+
|
|
81
|
+
### 검증 (회귀 0)
|
|
82
|
+
- **selftest 217→218** · **E2E 365/365** · 행위 재현: ① placeholder-only 파이썬 프로젝트 `--run-tests` → skip + exit 0(오판 해소, 이전 exit 1) ② `--test-cmd "node mytest.js"` → 실행+파싱(2/2 passed)+주장 대조 ✓ ③ config testCommand 경유 동작 ④ 실제 npm test 스크립트는 기존대로 ⑤ 실패 명령은 정직하게 exit 1.
|
|
83
|
+
- patch(1.17.2) — npm 미배포(R-0011, GitHub). 다음: UR-0046(스텁/연결 검사 — 빈껍데기 구현 차단).
|
|
84
|
+
|
|
85
|
+
## 1.17.1 — 2026-06-09 — 17번째 버그헌트: plan add 공백제목 데이터 손상 차단
|
|
86
|
+
|
|
87
|
+
**🔬 17번째 버그헌트(이번 세션 신규코드 회귀 + 광역 스윕).** 신규코드(gate --json stdout-swap·memory --json·_GROUP_USAGE·scan break·_cellSafe)는 **0 결함**(5000키 664ms 등 입증). plan add 입력검증 1건 발견·수정.
|
|
88
|
+
|
|
89
|
+
### 수정 (재현 검증)
|
|
90
|
+
- **🟠 plan add 공백 제목 → plan.md 손상 (P2)**: `plan add " "`(공백만)이 `|| 기본값`을 우회(truthy)해 빈 제목으로 기록 → milestone 파서의 `\s*` 가 개행을 흡수해 **다음 줄 `Status: planned` 를 제목으로 오인**. 2중 수정: ① dispatch 에서 `.trim()` 후 판정(공백→기본값 '새 계획'), ② 파서 `\.\s*` → `\.[ \t]*`(개행 미흡수, 5곳 + pure-utils) — 손상 클래스 차단.
|
|
91
|
+
|
|
92
|
+
### 검증 (회귀 0)
|
|
93
|
+
- **selftest 216→217** · **E2E 365/365** · `plan add " "` → '새 계획'(손상 없음)·정상 제목 보존 행위 재현.
|
|
94
|
+
- patch(1.17.1) — npm 미배포(R-0011, GitHub). (P3 plan add 빈-인자 기본값은 의도된 동작 — trim 으로 안전화.)
|
|
95
|
+
|
|
3
96
|
## 1.17.0 — 2026-06-09 — 🛡️ [안정화/Stable] 외부 클린룸 일관성 안정 minor
|
|
4
97
|
|
|
5
98
|
**🛡️ 안정화(Stable) minor.** 외부 클린룸 리뷰(게시본 무README 신규사용자 관점)에서 도출한 --json·CLI 일관성 개선(1.16.1~1.16.2)을 검증·통합해 npm 공개. R-0011 정책의 8번째 minor. 영상은 HyperFrames "문제→해소" 디자인.
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
>
|
|
12
|
+
> **어떤 언어, 어떤 AI 에이전트로 작업하든 — "증거 없이는 끝났다고 말할 수 없게" 만드는 AI 코딩 운영 레이어.** 코드를 대신 쓰는 도구가 아니라, AI 에이전트의 **기억·인수인계·검증·감사·보안 가드**를 프로젝트에 영속화하는 CLI + MCP 서버입니다. (이 포지셔닝은 5개 독립 클린룸 실사용 평가 — Python/Node/Rust 실개발·에이전트 교대·적대 공격 — 로 검증됐습니다.)
|
|
13
13
|
|
|
14
14
|
[](https://www.npmjs.com/package/leerness) ·  · **런타임 의존성 0** · **install-script 0** · offline-first · Node ≥ 18 · MIT
|
|
15
15
|
|
|
@@ -186,7 +186,7 @@ MIT
|
|
|
186
186
|
<!-- leerness:project-readme:start -->
|
|
187
187
|
## Leerness Project Harness
|
|
188
188
|
|
|
189
|
-
이 프로젝트는 Leerness v1.
|
|
189
|
+
이 프로젝트는 Leerness v1.18.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
190
190
|
|
|
191
191
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
192
192
|
|
|
@@ -240,7 +240,7 @@ leerness memory restore decision <date|title>
|
|
|
240
240
|
|
|
241
241
|
### MCP server (외부 AI 통합)
|
|
242
242
|
|
|
243
|
-
Leerness v1.
|
|
243
|
+
Leerness v1.18.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
|
|
244
244
|
|
|
245
245
|
```jsonc
|
|
246
246
|
// 카테고리별
|
|
@@ -261,7 +261,7 @@ Leerness v1.17.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code
|
|
|
261
261
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
262
262
|
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) 다음 라운드 예약.
|
|
263
263
|
|
|
264
|
-
현재 누적: **70 라운드 (1.9.40 → 1.
|
|
264
|
+
현재 누적: **70 라운드 (1.9.40 → 1.18.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
265
265
|
|
|
266
266
|
### 성능 가이드 (1.9.140 측정)
|
|
267
267
|
|
|
@@ -299,6 +299,6 @@ leerness release pack --close --auto-main-push
|
|
|
299
299
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
300
300
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
301
301
|
|
|
302
|
-
Last synced by Leerness v1.
|
|
302
|
+
Last synced by Leerness v1.18.0: 2026-06-10
|
|
303
303
|
<!-- leerness:project-readme:end -->
|
|
304
304
|
|
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
|
+
const VERSION = '1.18.0';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -216,7 +216,7 @@ function _resolveRoot(positional) {
|
|
|
216
216
|
}
|
|
217
217
|
function nonFlagArgs() {
|
|
218
218
|
const out = [];
|
|
219
|
-
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score','--include','--days','--gh-pages-src','--roadmap','--since','--agents','--model','--timeout','--retry-on-fail','--label','--score','--tokens','--alternatives','--impact','--tag','--surface','--depends-on','--affects','--co-changes-with','--files','--branch','--remote','--task-add','--next-action','--role','--provider','--env-var','--deploy','--token-lifetime-hours','--port','--secret','--keep','--shell','--ps-version','--done-when']); // 1.14.2 (UR-0032): --done-when 값이 positional 로 누출돼 milestone 제목에 흡수되던 것
|
|
219
|
+
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score','--include','--days','--gh-pages-src','--roadmap','--since','--agents','--model','--timeout','--retry-on-fail','--label','--score','--tokens','--alternatives','--impact','--tag','--surface','--depends-on','--affects','--co-changes-with','--files','--branch','--remote','--task-add','--next-action','--role','--provider','--env-var','--deploy','--token-lifetime-hours','--port','--secret','--keep','--shell','--ps-version','--done-when','--test-cmd']); // 1.14.2 (UR-0032): --done-when 값이 positional 로 누출돼 milestone 제목에 흡수되던 것 차단. 1.17.2 (UR-0045): --test-cmd 동일 원칙(신규 value-flag 는 반드시 여기 등록)
|
|
220
220
|
const a = process.argv.slice(2);
|
|
221
221
|
for (let i = 0; i < a.length; i++) {
|
|
222
222
|
const x = a[i];
|
|
@@ -3557,6 +3557,55 @@ function _selfTestCases() {
|
|
|
3557
3557
|
const src = read(__filename);
|
|
3558
3558
|
return src.includes('const _GROUP_USAGE = {') && src.includes("if (_GROUP_USAGE[cmd] && !args[1])") && src.includes("'subcommand_required'");
|
|
3559
3559
|
} },
|
|
3560
|
+
{ name: '17th 버그헌트 P2: plan add 공백제목 trim(기본값) + milestone 파서 개행 미흡수 (1.17.1)', run: () => {
|
|
3561
|
+
const src = read(__filename);
|
|
3562
|
+
const wired = src.includes("args.slice(2).join(' ').trim() || '새 계획'") && src.includes('(M-\\d{4})\\.[ \\t]*(.+?)$');
|
|
3563
|
+
// 파서 동작: 공백제목 milestone 이 다음 줄 'Status:' 를 제목으로 먹지 않음
|
|
3564
|
+
const block = '### M-0006. \nStatus: planned\nProgress: 0%\n';
|
|
3565
|
+
const m = block.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
3566
|
+
const safe = !m || (m[2] || '').indexOf('Status') === -1;
|
|
3567
|
+
return wired && safe;
|
|
3568
|
+
} },
|
|
3569
|
+
{ name: '범용성 P1 (UR-0045): verify-claim --run-tests 테스트 명령 해석 체인(--test-cmd>config>실제 npm test>skip) + placeholder 미실행 (1.17.2)', run: () => {
|
|
3570
|
+
const src = read(__filename);
|
|
3571
|
+
const chain = src.includes("let testCmd = arg('--test-cmd', null)") && src.includes("typeof cfg.testCommand === 'string'") && src.includes("!/no test specified/i.test(ts)") && src.includes('테스트 명령 미지정');
|
|
3572
|
+
const wired = src.includes("'--done-when','--test-cmd'") && src.includes('runCommandSafe(testCmd, []') && src.includes('cmd: testCmd');
|
|
3573
|
+
// pytest 출력 파싱: "3 passed in 0.05s"
|
|
3574
|
+
const py = ('3 passed in 0.05s'.match(/(\d+)\s+passed\b/i) || [])[1] === '3';
|
|
3575
|
+
return chain && wired && py;
|
|
3576
|
+
} },
|
|
3577
|
+
{ name: '범용성 P1② (UR-0046): verify-claim 스텁 구현 차단 + 테스트-구현 연결 검사 (1.17.3)', run: () => {
|
|
3578
|
+
const src = read(__filename);
|
|
3579
|
+
const wired = src.includes('const stubFiles = [];') && src.includes('implementationSubstance: stubFiles.length === 0') && src.includes('testImplLink: testLinkOk') && src.includes("(claimsChecked && stubFiles.length > 0) || (has('--strict-claims') && testLinkOk === false)");
|
|
3580
|
+
// 스텁 판정 로직 재현: 주석뿐 파일 → 코드줄 0
|
|
3581
|
+
const stub = '// TODO: call stripe\n// TODO: verify signature\n\n';
|
|
3582
|
+
const real = '// handler\nmodule.exports = function(){ return 1; };\n';
|
|
3583
|
+
const count = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '').split('\n').map(l => l.trim()).filter(t => t && !t.startsWith('//') && !t.startsWith('#')).length;
|
|
3584
|
+
return wired && count(stub) === 0 && count(real) > 0;
|
|
3585
|
+
} },
|
|
3586
|
+
{ name: '범용성 P2 (UR-0047): 테스트 카운트 관례 확대(pytest/루트 *.test.*) + 측정불가=검증미수행(역전 해소) (1.17.4)', run: () => {
|
|
3587
|
+
const src = read(__filename);
|
|
3588
|
+
const wired = src.includes('const _countTests = (fp)') && src.includes("def\\s+test_") && src.includes('측정 불가 — 주장') && src.includes('out.verdict.testCountMatch === false') && src.includes('const testMeasured = actualTestCount != null;');
|
|
3589
|
+
// 관례 글롭: pytest/루트 test 파일명 매칭
|
|
3590
|
+
const re = /^test_.+\.py$|_test\.py$|\.test\.[cm]?[jt]sx?$|\.spec\.[cm]?[jt]sx?$/i;
|
|
3591
|
+
const glob = re.test('test_calc.py') && re.test('calc_test.py') && re.test('rateLimiter.test.js') && re.test('app.spec.ts') && !re.test('calc.py') && !re.test('index.js');
|
|
3592
|
+
// 파이썬 카운트: def test_ 2개
|
|
3593
|
+
const py = ('def test_a():\n pass\ndef test_b():\n pass\n'.match(/^\s*def\s+test_/gm) || []).length === 2;
|
|
3594
|
+
return wired && glob && py;
|
|
3595
|
+
} },
|
|
3596
|
+
{ name: '범용성 P2 (UR-0048): task update unknown flag 거부 + did-you-mean(인수인계 유실 차단) (1.17.5)', run: () => {
|
|
3597
|
+
const src = read(__filename);
|
|
3598
|
+
const wired = src.includes('function _rejectUnknownFlags(allowed, usageHint)') && src.includes("'unknown_flag'") && src.includes("_rejectUnknownFlags(['--status', '--evidence', '--next', '--note']");
|
|
3599
|
+
// did-you-mean prefix 로직: --next-action 은 --next 를 prefix 로 가짐
|
|
3600
|
+
const dymOk = '--next-action'.startsWith('--next');
|
|
3601
|
+
return wired && dymOk;
|
|
3602
|
+
} },
|
|
3603
|
+
{ name: '범용성 P2 완결 (UR-0049): session close 마감 정합 — done 낙관 재확인 + 시크릿 재확인 (1.17.6)', run: () => {
|
|
3604
|
+
const sc = read(path.join(path.dirname(__filename), '..', 'lib', 'session-close.js'));
|
|
3605
|
+
const wired = sc.includes('_detectOptimism, _scanCodeForPatterns, _collectSecretFindings } = deps') && sc.includes('optimismUnresolved') && sc.includes('jsonResult.closeSecurity') && sc.includes('마감 보안: 커밋 대상 시크릿');
|
|
3606
|
+
const injected = read(__filename).includes('_updateUserRequest, _detectOptimism, _scanCodeForPatterns, _collectSecretFindings });');
|
|
3607
|
+
return wired && injected;
|
|
3608
|
+
} },
|
|
3560
3609
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3561
3610
|
];
|
|
3562
3611
|
}
|
|
@@ -4121,7 +4170,7 @@ function commandsCmd(root) {
|
|
|
4121
4170
|
{ cmd: 'scan secrets [path]', desc: '시크릿 탐지' },
|
|
4122
4171
|
{ cmd: 'encoding check [path]', desc: '인코딩 검증' },
|
|
4123
4172
|
{ cmd: 'lazy detect [path] [--json]', desc: '게으른 작업 감지 (1.9.101)' },
|
|
4124
|
-
{ cmd: 'verify-claim <T-ID> [--run-tests] [--strict-claims] [--require-evidence]', desc: '주장 검증 (1.9.18~26) — --require-evidence: done 주장에 파일+테스트 근거 강제 (1.9.287)' },
|
|
4173
|
+
{ cmd: 'verify-claim <T-ID> [--run-tests] [--test-cmd "<명령>"] [--strict-claims] [--require-evidence]', desc: '주장 검증 (1.9.18~26) — --require-evidence: done 주장에 파일+테스트 근거 강제 (1.9.287) · --test-cmd: 비-JS 테스트 명령 (1.17.2)' },
|
|
4125
4174
|
{ cmd: 'optimism-check <T-ID>', desc: '낙관적 API 감지 (1.9.26)' },
|
|
4126
4175
|
{ cmd: 'requests audit|list|complete|drop|auto-complete', desc: '사용자 요청 추적 (1.9.207/223)' },
|
|
4127
4176
|
{ cmd: 'pre-wake-audit [path] [--last]', desc: 'sleep 전 점검 (1.9.209)' },
|
|
@@ -6445,7 +6494,7 @@ function planListCmd(root, opts = {}) {
|
|
|
6445
6494
|
// ### M-XXXX. <title> 블록 추출
|
|
6446
6495
|
const blocks = text.split(/\n(?=### M-\d{4}\.)/);
|
|
6447
6496
|
for (const b of blocks) {
|
|
6448
|
-
const headerMatch = b.match(/^### (M-\d{4})
|
|
6497
|
+
const headerMatch = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
6449
6498
|
if (!headerMatch) continue;
|
|
6450
6499
|
const id = headerMatch[1];
|
|
6451
6500
|
const title = headerMatch[2].trim();
|
|
@@ -6715,8 +6764,22 @@ function taskAdd(root, text) {
|
|
|
6715
6764
|
} catch {} // review 실패는 task add 자체에 영향 X
|
|
6716
6765
|
}
|
|
6717
6766
|
}
|
|
6767
|
+
// 1.17.5 (UR-0048, 5축 실증 P2): 모르는 옵션 조용히 무시 차단 — `task update --next-action "x"` 처럼 오타/미존재 플래그가
|
|
6768
|
+
// "✓ task updated" 와 함께 값을 버려, 쓴 에이전트는 기록됐다고 믿고 다음 에이전트는 placeholder 를 받는(인수인계 유실) 최악의 실패 양식.
|
|
6769
|
+
// prefix 기반 did-you-mean(--next-action → --next) + exit 1. 전역 플래그(--path/--json/--force/--lenient)는 항상 허용.
|
|
6770
|
+
function _rejectUnknownFlags(allowed, usageHint) {
|
|
6771
|
+
const all = new Set([...allowed, '--path', '--json', '--force', '--lenient']);
|
|
6772
|
+
const seen = process.argv.slice(2).filter(a => a.startsWith('--')).map(a => a.split('=')[0]);
|
|
6773
|
+
const unknown = [...new Set(seen.filter(f => !all.has(f)))];
|
|
6774
|
+
if (!unknown.length) return true;
|
|
6775
|
+
const dym = (u) => { const c = [...all].find(k => u.startsWith(k) || k.startsWith(u)); return c ? ` — 혹시 ${c}?` : ''; };
|
|
6776
|
+
failJson(has('--json'), 'unknown_flag', `알 수 없는 옵션: ${unknown.map(u => u + dym(u)).join(', ')} (지원: ${[...allowed].join(' ')}${usageHint ? ' · ' + usageHint : ''}) — 값이 조용히 버려지는 것을 방지하기 위해 거부`);
|
|
6777
|
+
return false;
|
|
6778
|
+
}
|
|
6779
|
+
|
|
6718
6780
|
function taskUpdate(root, id) {
|
|
6719
6781
|
if (!_requireInit(root, 'task update')) return; // 1.9.311 (UR-0047): init 가드
|
|
6782
|
+
if (!_rejectUnknownFlags(['--status', '--evidence', '--next', '--note'], 'task update T-0001 --status done --evidence "..." --next "..."')) { process.exitCode = 1; return; } // 1.17.5 (UR-0048)
|
|
6720
6783
|
if (!id) return fail('id required (e.g., task update T-0001 --status in-progress)');
|
|
6721
6784
|
if (!_validateChoice(arg('--status', null), TASK_STATUSES, 'task status')) { process.exitCode = 1; return; } // 1.9.310 (UR-0046)
|
|
6722
6785
|
const rows = readProgressRows(root);
|
|
@@ -7147,7 +7210,7 @@ function _jaccard(a, b) {
|
|
|
7147
7210
|
function taskRelink(root) {
|
|
7148
7211
|
root = absRoot(root);
|
|
7149
7212
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
7150
|
-
const milestones = [...planText.matchAll(/^### (M-\d{4})
|
|
7213
|
+
const milestones = [...planText.matchAll(/^### (M-\d{4})\.[ \t]*(.+?)$/gm)]
|
|
7151
7214
|
.map(m => ({ id: m[1], text: m[2].trim() }));
|
|
7152
7215
|
const rows = readProgressRows(root);
|
|
7153
7216
|
const linkedM = new Set(rows.map(r => (r.evidence.match(/M-\d{4}/) || [])[0]).filter(Boolean));
|
|
@@ -9612,6 +9675,11 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9612
9675
|
// 4) N개 테스트 (단순 카운트)
|
|
9613
9676
|
const m4 = evidence.match(/(\d+)\s*개\s*테스트/);
|
|
9614
9677
|
if (m4) declaredTestCount = parseInt(m4[1], 10);
|
|
9678
|
+
// 4b) 테스트 N개 (1.17.4 UR-0047: 한국어 자연어순 '테스트 50개 통과' — 이전 미인식으로 부풀린 주장이 카운트 검증을 아예 안 탔음)
|
|
9679
|
+
if (!declaredTestCount) {
|
|
9680
|
+
const m4b = evidence.match(/테스트\s*(\d+)\s*개/);
|
|
9681
|
+
if (m4b) declaredTestCount = parseInt(m4b[1], 10);
|
|
9682
|
+
}
|
|
9615
9683
|
// 5) N tests (영문 단순 카운트)
|
|
9616
9684
|
if (!declaredTestCount) {
|
|
9617
9685
|
const m5 = evidence.match(/(\d+)\s*tests?\b/i);
|
|
@@ -9620,6 +9688,27 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9620
9688
|
|
|
9621
9689
|
// 실제 파일 존재 검사
|
|
9622
9690
|
const fileChecks = files.map(f => ({ file: f, exists: exists(path.join(root, f)) }));
|
|
9691
|
+
|
|
9692
|
+
// 1.17.3 (UR-0046 범용성 P1②): 빈껍데기(스텁) 구현 + 테스트-구현 연결 검사 — "주석뿐 구현 + assert(true) 테스트"가 verify-claim 을 exit 0 으로 통과하던 공격(5축 실증 Attack C) 차단.
|
|
9693
|
+
// ① 스텁: 주장된 코드 파일(테스트 제외)의 비주석 코드줄이 0 이면 확정 스텁 — done 게이팅 FAIL(확실 신호만, 과탐 0).
|
|
9694
|
+
// ② 연결: 주장에 구현+테스트가 모두 있는데 어떤 테스트도 구현 파일명(basename)을 참조하지 않으면 — 기본 advisory ⚠, --strict-claims 시 FAIL.
|
|
9695
|
+
const _VC_CODE_EXT = /\.(js|mjs|cjs|jsx|ts|tsx|py|rb|go|rs|java|cs|php)$/i;
|
|
9696
|
+
const _VC_TEST_PAT = /(^|[\\/])(test_[^\\/]+\.[a-z]+|[^\\/]+[._-]test\.[a-z]+|[^\\/]+\.spec\.[a-z]+)$|(^|[\\/])tests?[\\/]/i;
|
|
9697
|
+
const stubFiles = [];
|
|
9698
|
+
for (const c of fileChecks) {
|
|
9699
|
+
if (!c.exists || !_VC_CODE_EXT.test(c.file) || _VC_TEST_PAT.test(c.file)) continue;
|
|
9700
|
+
let body = ''; try { body = read(path.join(root, c.file)); } catch { continue; }
|
|
9701
|
+
if (!body || body.length > 512 * 1024) continue;
|
|
9702
|
+
const codeLines = body.replace(/\/\*[\s\S]*?\*\//g, '').split('\n').map(l => l.trim()).filter(t => t && !t.startsWith('//') && !t.startsWith('#'));
|
|
9703
|
+
if (codeLines.length === 0) stubFiles.push(c.file);
|
|
9704
|
+
}
|
|
9705
|
+
const _vcImpl = fileChecks.filter(c => c.exists && _VC_CODE_EXT.test(c.file) && !_VC_TEST_PAT.test(c.file)).map(c => c.file);
|
|
9706
|
+
const _vcTests = fileChecks.filter(c => c.exists && _VC_CODE_EXT.test(c.file) && _VC_TEST_PAT.test(c.file)).map(c => c.file);
|
|
9707
|
+
let testLinkOk = null; // null = 판단 불가(구현·테스트가 함께 주장되지 않음)
|
|
9708
|
+
if (_vcImpl.length && _vcTests.length) {
|
|
9709
|
+
const bases = _vcImpl.map(f => path.basename(f).replace(/\.[a-z]+$/i, ''));
|
|
9710
|
+
testLinkOk = _vcTests.some(tf => { let t = ''; try { t = read(path.join(root, tf)); } catch { return false; } return bases.some(b => b && t.includes(b)); });
|
|
9711
|
+
}
|
|
9623
9712
|
// 1.9.302 (UR-0042, 외부리뷰 Opus G-1): git diff 시맨틱 교차검증 — 주장한 파일이 실제로 변경됐는가.
|
|
9624
9713
|
// "파일 존재"만으로는 "테스트만 통과하면 done" 허위완료를 못 막음(Opus). git working tree+직전커밋 변경과 대조.
|
|
9625
9714
|
const gitChanged = _gitChangedFiles(root); // Set | null(git repo 아님 → 검증 불가)
|
|
@@ -9629,33 +9718,55 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9629
9718
|
// 1.13.2 (Karpathy 가이드라인 3 "외과적 변경", UR-0030): 역방향 교차검증 — git 에 변경됐으나 evidence/주장에 없는 파일(scope-creep / 요청 범위 밖 변경 신호). 하네스 자체 기록(.harness 등)은 제외. advisory(오탐 방지 — 기본 FAIL 아님, 표면화만).
|
|
9630
9719
|
const _SCOPE_SKIP = /^(\.harness[\\/]|\.git[\\/]|node_modules[\\/]|\.claude[\\/]|dist[\\/]|build[\\/])/;
|
|
9631
9720
|
const changedNotClaimed = gitApplicable ? [...gitChanged].filter(g => !_SCOPE_SKIP.test(g) && !files.some(f => _claimFileInGit(f, new Set([g])))) : [];
|
|
9632
|
-
// 테스트
|
|
9721
|
+
// 테스트 카운트 (1.17.4, UR-0047): 주장된 테스트 파일 우선 + 관례 글롭(pytest test_*.py·*_test.py / 루트·tests/ 의 *.test.*·*.spec.*) 인식.
|
|
9722
|
+
// 이전엔 tests/test.js 등 3개 하드코딩 — pytest/node:test 루트 관례가 안 보여 "파일 못 찾음"인데 ✓ pass 로 표기(측정실패=통과 역전, 5축 실증 P2 공통).
|
|
9723
|
+
const _countTests = (fp) => {
|
|
9724
|
+
let t = ''; try { t = read(fp); } catch { return 0; }
|
|
9725
|
+
if (/\.py$/i.test(fp)) { const d = (t.match(/^\s*def\s+test_/gm) || []).length; return d || (t.match(/^\s*assert\b/gm) || []).length; }
|
|
9726
|
+
return (t.match(/\bcheck\s*\(/g) || t.match(/\b(it|test)\s*\(/g) || []).length;
|
|
9727
|
+
};
|
|
9633
9728
|
let actualTestCount = null;
|
|
9634
|
-
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9638
|
-
|
|
9639
|
-
|
|
9640
|
-
|
|
9729
|
+
if (_vcTests.length) {
|
|
9730
|
+
actualTestCount = _vcTests.reduce((a, f) => a + _countTests(path.join(root, f)), 0);
|
|
9731
|
+
} else {
|
|
9732
|
+
const found = new Set();
|
|
9733
|
+
for (const tf of ['tests/test.js', 'test/test.js', 'tests/index.js']) if (exists(path.join(root, tf))) found.add(tf);
|
|
9734
|
+
if (!found.size) {
|
|
9735
|
+
for (const dir of ['', 'tests', 'test']) {
|
|
9736
|
+
let ents = []; try { ents = fs.readdirSync(path.join(root, dir)); } catch { continue; }
|
|
9737
|
+
for (const e of ents) if (/^test_.+\.py$|_test\.py$|\.test\.[cm]?[jt]sx?$|\.spec\.[cm]?[jt]sx?$/i.test(e)) found.add(dir ? dir + '/' + e : e);
|
|
9738
|
+
if (found.size) break; // 루트 우선 (루트에 있으면 tests/ 중복 스캔 안 함)
|
|
9739
|
+
}
|
|
9641
9740
|
}
|
|
9741
|
+
if (found.size) actualTestCount = [...found].reduce((a, f) => a + _countTests(path.join(root, f)), 0);
|
|
9642
9742
|
}
|
|
9743
|
+
const testMeasured = actualTestCount != null;
|
|
9643
9744
|
|
|
9644
|
-
// 1.9.19: --run-tests —
|
|
9745
|
+
// 1.9.19: --run-tests — 테스트 자동 실행 + pass/fail 파싱
|
|
9746
|
+
// 1.17.2 (UR-0045 범용성 P1): 테스트 명령 해석 체인 — --test-cmd > leerness-config.json testCommand > 실제 npm test 스크립트 > skip.
|
|
9747
|
+
// 이전엔 npm test 하드코딩 → 비-JS(파이썬 등) 프로젝트에서 npm init 잔재 placeholder("no test specified"&&exit 1)가 실행돼
|
|
9748
|
+
// 테스트 전부 통과한 작업을 "주장 불일치 FAIL"로 오판(5축 클린룸 실증 P1). placeholder 는 테스트가 아니므로 skip 처리.
|
|
9645
9749
|
let runResult = null;
|
|
9646
9750
|
if (has('--run-tests')) {
|
|
9647
|
-
|
|
9648
|
-
if (!
|
|
9649
|
-
|
|
9751
|
+
let testCmd = arg('--test-cmd', null);
|
|
9752
|
+
if (!testCmd) {
|
|
9753
|
+
try { const cfg = JSON.parse(read(path.join(root, '.harness', 'leerness-config.json'))); if (cfg && typeof cfg.testCommand === 'string' && cfg.testCommand.trim()) testCmd = cfg.testCommand.trim(); } catch {}
|
|
9754
|
+
}
|
|
9755
|
+
if (!testCmd) {
|
|
9756
|
+
const pkgPath = path.join(root, 'package.json');
|
|
9757
|
+
if (exists(pkgPath)) {
|
|
9758
|
+
let pkg = null;
|
|
9759
|
+
try { pkg = JSON.parse(read(pkgPath)); } catch {}
|
|
9760
|
+
const ts = pkg && pkg.scripts && pkg.scripts.test;
|
|
9761
|
+
if (ts && !/no test specified/i.test(ts)) testCmd = 'npm test';
|
|
9762
|
+
}
|
|
9763
|
+
}
|
|
9764
|
+
if (!testCmd) {
|
|
9765
|
+
runResult = { skipped: true, reason: '테스트 명령 미지정 — 비-JS 프로젝트는 --test-cmd "<명령>" 또는 .harness/leerness-config.json 의 "testCommand" 로 지정 (불일치 판정 아님)' };
|
|
9650
9766
|
} else {
|
|
9651
|
-
|
|
9652
|
-
|
|
9653
|
-
|
|
9654
|
-
if (!hasTestScript) {
|
|
9655
|
-
runResult = { skipped: true, reason: 'scripts.test 없음' };
|
|
9656
|
-
} else {
|
|
9657
|
-
// 1.9.299 (UR-0039): 신뢰 못 할 워크스페이스 npm test → runCommandSafe + scrubSecrets (시크릿 노출 차단 + cwd jail).
|
|
9658
|
-
const r = runCommandSafe('npm test', [], { cwd: root, root, encoding: 'utf8', allowShell: true, scrubSecrets: true, timeout: 5 * 60 * 1000, kind: 'verify_claim_test' });
|
|
9767
|
+
{
|
|
9768
|
+
// 1.9.299 (UR-0039): 신뢰 못 할 워크스페이스 테스트 실행 → runCommandSafe + scrubSecrets (시크릿 노출 차단 + cwd jail).
|
|
9769
|
+
const r = runCommandSafe(testCmd, [], { cwd: root, root, encoding: 'utf8', allowShell: true, scrubSecrets: true, timeout: 5 * 60 * 1000, kind: 'verify_claim_test' });
|
|
9659
9770
|
const out = (r.stdout || '') + (r.stderr || '');
|
|
9660
9771
|
// 1.9.20: 파싱 패턴 확장 — 한국어 + jest/mocha/tap/vitest
|
|
9661
9772
|
let parsed = null;
|
|
@@ -9677,8 +9788,14 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9677
9788
|
const m4 = out.match(/#\s*pass\s+(\d+)/i);
|
|
9678
9789
|
if (m4) parsed = { num: parseInt(m4[1], 10), denom: parseInt(m4[1], 10) };
|
|
9679
9790
|
}
|
|
9791
|
+
// 5) pytest: "N passed in 0.12s" (UR-0045 — 파이썬 러너 출력 인식)
|
|
9792
|
+
if (!parsed) {
|
|
9793
|
+
const m5 = out.match(/(\d+)\s+passed\b/i);
|
|
9794
|
+
if (m5) parsed = { num: parseInt(m5[1], 10), denom: parseInt(m5[1], 10) };
|
|
9795
|
+
}
|
|
9680
9796
|
runResult = {
|
|
9681
9797
|
skipped: false,
|
|
9798
|
+
cmd: testCmd,
|
|
9682
9799
|
exitCode: r.status,
|
|
9683
9800
|
parsed,
|
|
9684
9801
|
allPassed: r.status === 0 && (!parsed || (parsed && parsed.num === parsed.denom))
|
|
@@ -9723,11 +9840,14 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9723
9840
|
actual: { fileChecks, testCount: actualTestCount },
|
|
9724
9841
|
verdict: {
|
|
9725
9842
|
filesAllExist,
|
|
9726
|
-
testCountMatch: declaredTestCount == null
|
|
9843
|
+
testCountMatch: declaredTestCount == null ? null : (!testMeasured ? null : actualTestCount >= declaredTestCount), // 1.17.4 (UR-0047): null=측정불가(검증 미수행 — pass 아님), false 만 게이팅
|
|
9727
9844
|
evidenceComplete: !mustHaveEvidence ? null : evq.ok,
|
|
9728
9845
|
claimsConsistent: !claimsChecked ? null : strictOk, // 1.11.2 (UR-0175): optimism+정직성 (기본 게이팅)
|
|
9729
|
-
gitCrossCheck: !gitApplicable ? null : !gitStrongMismatch
|
|
9846
|
+
gitCrossCheck: !gitApplicable ? null : !gitStrongMismatch, // 1.11.2 (UR-0175): git 교차검증 (머신 경로 노출)
|
|
9847
|
+
implementationSubstance: stubFiles.length === 0, // 1.17.3 (UR-0046): 주장된 구현이 주석/빈껍데기뿐이면 false
|
|
9848
|
+
testImplLink: testLinkOk // 1.17.3 (UR-0046): 테스트가 구현을 참조하는가 (null=판단불가)
|
|
9730
9849
|
},
|
|
9850
|
+
stubFiles: stubFiles.slice(0, 10),
|
|
9731
9851
|
evidence: { required: mustHaveEvidence, ...evq },
|
|
9732
9852
|
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 })) },
|
|
9733
9853
|
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 }),
|
|
@@ -9744,7 +9864,7 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9744
9864
|
log(JSON.stringify(out, null, 2));
|
|
9745
9865
|
if (runResult && !runResult.skipped && !runResult.allPassed) return process.exit(1);
|
|
9746
9866
|
// 1.11.2 (UR-0175): --json 도 optimism+git 게이팅 — 머신 경로가 허위완료를 통과시키지 않도록(human 경로와 동일).
|
|
9747
|
-
if (!filesAllExist ||
|
|
9867
|
+
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(측정불가)은 미기여
|
|
9748
9868
|
return;
|
|
9749
9869
|
}
|
|
9750
9870
|
|
|
@@ -9762,15 +9882,15 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9762
9882
|
log(`## 🧪 테스트 카운트`);
|
|
9763
9883
|
if (declaredPass) log(` 주장 (pass): ${declaredPass.num}/${declaredPass.denom}`);
|
|
9764
9884
|
if (declaredTestCount) log(` 주장 (개수): ${declaredTestCount}개`);
|
|
9765
|
-
if (actualTestCount != null) log(` 실측:
|
|
9766
|
-
else log(` 실측: 테스트 파일 못 찾음 (
|
|
9885
|
+
if (actualTestCount != null) log(` 실측: ${actualTestCount}개 테스트 호출 (${_vcTests.length ? '주장된 테스트 파일' : '관례 탐색: 루트/tests·test_*.py·*.test.*'})`);
|
|
9886
|
+
else log(` 실측: 테스트 파일 못 찾음 — 카운트 검증 미수행 (pass 아님)`);
|
|
9767
9887
|
|
|
9768
9888
|
// 1.9.19: --run-tests 결과
|
|
9769
9889
|
let runTestsOk = true;
|
|
9770
9890
|
let declaredPassMatchesActual = true;
|
|
9771
9891
|
if (runResult) {
|
|
9772
9892
|
log('');
|
|
9773
|
-
log(`## 🚦
|
|
9893
|
+
log(`## 🚦 ${runResult.cmd || '테스트'} 실행 (--run-tests)`);
|
|
9774
9894
|
if (runResult.skipped) {
|
|
9775
9895
|
log(` ⚠ skipped: ${runResult.reason}`);
|
|
9776
9896
|
} else {
|
|
@@ -9792,7 +9912,8 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9792
9912
|
const testOk = declaredTestCount == null || actualTestCount == null || actualTestCount >= declaredTestCount;
|
|
9793
9913
|
log(`## 종합`);
|
|
9794
9914
|
log(` - 파일 모두 존재: ${allFilesOk ? '✓ pass' : '✗ FAIL (일부 누락)'}`);
|
|
9795
|
-
|
|
9915
|
+
// 1.17.4 (UR-0047): 측정 불가는 '통과' 가 아니라 '검증 미수행' — 이전엔 실측 0 인데 ✓ pass(실측≥주장) 모순 표기.
|
|
9916
|
+
log(` - 테스트 카운트: ${declaredTestCount == null ? '⊘ (주장 없음)' : !testMeasured ? `⊘ 측정 불가 — 주장 ${declaredTestCount}개 검증 미수행 (pass 아님)` : testOk ? '✓ pass (실측 ≥ 주장)' : '⚠ 주장보다 적음'}`);
|
|
9796
9917
|
if (runResult && !runResult.skipped) {
|
|
9797
9918
|
log(` - npm test 실행: ${runTestsOk ? '✓ all passed' : '✗ FAIL'}`);
|
|
9798
9919
|
if (declaredPass) log(` - 주장과 실행 결과 일치: ${declaredPassMatchesActual ? '✓ pass' : '⚠ 다름'}`);
|
|
@@ -9822,7 +9943,18 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9822
9943
|
log(` - evidence 완전성 (done 기본 강제): ${evidenceQualityOk ? '✓ pass (파일+테스트 근거 있음)' : `✗ FAIL (누락: ${evq.missing.join(', ')})`}`);
|
|
9823
9944
|
if (!evidenceQualityOk) log(` · done 주장은 수정 파일 경로 + 테스트명/개수 가 evidence 에 있어야 함 (테스트 통과만으로는 불충분). 완화: --lenient`);
|
|
9824
9945
|
}
|
|
9825
|
-
|
|
9946
|
+
// 1.17.3 (UR-0046): 구현 실체(스텁) + 테스트-구현 연결 — Attack C(주석뿐 구현+assert(true)) 차단.
|
|
9947
|
+
if (stubFiles.length) {
|
|
9948
|
+
log(` - 구현 실체 (done 기본): ✗ FAIL — 주장된 구현 파일이 주석/빈껍데기뿐: ${stubFiles.slice(0, 5).join(', ')} (비주석 코드 0줄)`);
|
|
9949
|
+
} else if (claimsChecked && _vcImpl.length) {
|
|
9950
|
+
log(` - 구현 실체 (done 기본): ✓ pass (주장 구현 파일에 실코드 존재)`);
|
|
9951
|
+
}
|
|
9952
|
+
if (testLinkOk === false) {
|
|
9953
|
+
log(` - 테스트-구현 연결: ⚠ 주장된 테스트(${_vcTests.slice(0, 3).join(', ')})가 구현 파일을 참조하지 않음 — 빈 테스트(assert(true)) 의심${has('--strict-claims') ? ' → FAIL' : ' (advisory — --strict-claims 시 FAIL)'}`);
|
|
9954
|
+
} else if (testLinkOk === true && claimsChecked) {
|
|
9955
|
+
log(` - 테스트-구현 연결: ✓ pass (테스트가 구현을 참조)`);
|
|
9956
|
+
}
|
|
9957
|
+
const overallFail = !allFilesOk || !testOk || (runResult && !runResult.skipped && !runTestsOk) || (claimsChecked && !strictOk) || !evidenceQualityOk || !gitClaimOk || (claimsChecked && stubFiles.length > 0) || (has('--strict-claims') && testLinkOk === false);
|
|
9826
9958
|
// 1.9.287: 정직한 한계 고지 — 테스트 통과 ≠ 의미적 구현 정확성
|
|
9827
9959
|
if (claimsChecked || mustHaveEvidence) {
|
|
9828
9960
|
log('');
|
|
@@ -11694,7 +11826,7 @@ function llmBenchRecordCmd(root) {
|
|
|
11694
11826
|
|
|
11695
11827
|
const _sessionClose = require('../lib/session-close');
|
|
11696
11828
|
// 1.9.425 (UR-0025/UR-0125 큰 핸들러 모듈화 10번째): sessionClose → lib/session-close.js (DI 위임)
|
|
11697
|
-
function sessionClose(root, opts = {}) { return _sessionClose.sessionClose(root, opts, { VERSION, STATUSES, MARK, has, arg, harnessPath: __filename, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest }); }
|
|
11829
|
+
function sessionClose(root, opts = {}) { return _sessionClose.sessionClose(root, opts, { VERSION, STATUSES, MARK, has, arg, harnessPath: __filename, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest, _detectOptimism, _scanCodeForPatterns, _collectSecretFindings }); } // 1.17.6 (UR-0049): 마감 정합 — done 낙관 재확인 + 시크릿 재확인
|
|
11698
11830
|
|
|
11699
11831
|
function readmeCmd(root) { syncReadme(absRoot(root)); }
|
|
11700
11832
|
function consistencyCheck(root) {
|
|
@@ -12180,7 +12312,7 @@ function _brainstormFor(root, topic) {
|
|
|
12180
12312
|
const planText = read(planFile_brainstorm);
|
|
12181
12313
|
const milestoneBlocks = planText.split(/\n(?=### M-\d{4}\.)/);
|
|
12182
12314
|
for (const b of milestoneBlocks) {
|
|
12183
|
-
const m = b.match(/^### (M-\d{4})
|
|
12315
|
+
const m = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
12184
12316
|
if (m && matches(b)) {
|
|
12185
12317
|
const idx = planText.indexOf(b);
|
|
12186
12318
|
const lineNo = idx >= 0 ? planText.slice(0, idx).split('\n').length : 0;
|
|
@@ -12475,7 +12607,7 @@ function brainstormCmd(root, topic) {
|
|
|
12475
12607
|
const planText = read(planFile_b2);
|
|
12476
12608
|
const milestoneBlocks = planText.split(/\n(?=### M-\d{4}\.)/);
|
|
12477
12609
|
for (const b of milestoneBlocks) {
|
|
12478
|
-
const m = b.match(/^### (M-\d{4})
|
|
12610
|
+
const m = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
12479
12611
|
if (m && matches(b)) {
|
|
12480
12612
|
const idx = planText.indexOf(b);
|
|
12481
12613
|
const lineNo = idx >= 0 ? planText.slice(0, idx).split('\n').length : 0;
|
|
@@ -19433,7 +19565,7 @@ async function main() {
|
|
|
19433
19565
|
const root = absRoot(arg('--path', process.cwd())); const sub = args[1] || 'show';
|
|
19434
19566
|
if (sub==='show') return planShow(root);
|
|
19435
19567
|
if (sub==='init') return planInit(root);
|
|
19436
|
-
if (sub==='add') return planAdd(root, args.slice(2).join(' ') || '새 계획');
|
|
19568
|
+
if (sub==='add') return planAdd(root, args.slice(2).join(' ').trim() || '새 계획'); // 17th 버그헌트 P2: 공백-only 제목이 || 기본값을 우회(truthy)해 plan.md 손상(파서가 다음 줄 'Status:' 흡수) → trim 후 판정
|
|
19437
19569
|
if (sub==='drop') return planDrop(root, args.slice(2).join(' ') || '드랍 항목');
|
|
19438
19570
|
if (sub==='remove') return planRemoveCmd(root, args[2]);
|
|
19439
19571
|
if (sub==='progress') return planProgress(root, { json: has('--json'), updateIntent: args.slice(2).some(a => /^M-\d/i.test(a)) || has('--status') || arg('--progress', null) != null }); // 1.9.447 (UR-0145): --json + 변경의도 인자 경고
|
package/lib/pure-utils.js
CHANGED
|
@@ -322,7 +322,7 @@ function _roadmapParseMilestones(text) {
|
|
|
322
322
|
const s = String(text || '');
|
|
323
323
|
const out = [];
|
|
324
324
|
// 1.9.352 (UR-0068 외부리뷰): 다음 milestone 직전까지 block 한정 — 이전 구현은 slice(m.index) 로 다음 milestone 의 Status/Progress 를 누출했음
|
|
325
|
-
const matches = [...s.matchAll(/^### (M-\d{4})
|
|
325
|
+
const matches = [...s.matchAll(/^### (M-\d{4})\.[ \t]*(.+?)$/gm)]; // 17th 버그헌트 P2: \s* 가 개행 흡수해 빈 제목 milestone 이 다음 줄(Status:)을 제목으로 먹던 것 차단
|
|
326
326
|
for (let i = 0; i < matches.length; i++) {
|
|
327
327
|
const m = matches[i];
|
|
328
328
|
const end = i + 1 < matches.length ? matches[i + 1].index : s.length;
|
package/lib/session-close.js
CHANGED
|
@@ -11,7 +11,7 @@ const { log, ok, warn, fail, failJson, today, now, absRoot, exists, read, readBu
|
|
|
11
11
|
const { _sanitizeFences, _parseArchiveBlocks } = require('./pure-utils');
|
|
12
12
|
|
|
13
13
|
function sessionClose(root, opts = {}, deps = {}) {
|
|
14
|
-
const { VERSION, STATUSES, MARK, has, arg, harnessPath, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest } = deps;
|
|
14
|
+
const { VERSION, STATUSES, MARK, has, arg, harnessPath, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest, _detectOptimism, _scanCodeForPatterns, _collectSecretFindings } = deps;
|
|
15
15
|
root = absRoot(root);
|
|
16
16
|
// 1.10.4 (13th 버그헌트 P2, UR-0167): 경로 없음/디렉토리 아님 → 구조화 에러 + exit 1. mkdir <path>/.harness ENOTDIR 크래시 & 실패를 성공(exit 0)으로 오판하던 문제 차단.
|
|
17
17
|
if (!exists(root) || !fs.statSync(root).isDirectory()) { failJson(!!opts.json || has('--json'), 'path_not_found', `경로 없음 또는 디렉토리 아님: ${root}`); return; }
|
|
@@ -34,6 +34,22 @@ function sessionClose(root, opts = {}, deps = {}) {
|
|
|
34
34
|
const _doneNoEvidence = (buckets['done'] || []).filter(r => !r.evidence || /^(\s*|user-request|-)$/.test(r.evidence) || /^plan:M-\d{4}\s*$/.test(r.evidence));
|
|
35
35
|
jsonResult.completionHonesty = { doneTotal: (buckets['done'] || []).length, doneWithoutEvidence: _doneNoEvidence.length, ids: _doneNoEvidence.slice(0, 5).map(r => r.id) };
|
|
36
36
|
if (_doneNoEvidence.length) log(` ⚠ 완료 정직성: done ${_doneNoEvidence.length}건 evidence 없음/placeholder (${_doneNoEvidence.slice(0, 3).map(r => r.id).join(', ')}) — verify-claim 권장 (advisory)`);
|
|
37
|
+
// 1.17.6 (UR-0049 마감 정합): done 의 미해소 낙관 의심 재확인 — verify-claim 을 건너뛴 거짓 주장(evidence 에 API/DB 주장 있는데 코드 흔적 없음)이
|
|
38
|
+
// 평범한 'done' 으로 마감을 무사 통과하던 것(5축 실증 P2: 거짓 DB 주장이 done 으로 마감, gate 실패 중 'clean' 선언) — 마감이 마지막 관문 역할을 하도록 재확인. advisory.
|
|
39
|
+
let _doneOptimism = [];
|
|
40
|
+
try {
|
|
41
|
+
if (_detectOptimism && _scanCodeForPatterns && (buckets['done'] || []).length) {
|
|
42
|
+
const _codeText = _scanCodeForPatterns(root);
|
|
43
|
+
_doneOptimism = (buckets['done'] || []).map(r => ({ id: r.id, suspects: _detectOptimism(r.evidence || '', _codeText) || [] })).filter(x => x.suspects.length);
|
|
44
|
+
}
|
|
45
|
+
} catch {}
|
|
46
|
+
jsonResult.completionHonesty.optimismUnresolved = _doneOptimism.map(x => ({ id: x.id, kinds: x.suspects.map(s => s.kind) }));
|
|
47
|
+
if (_doneOptimism.length) log(` ⚠ 완료 정직성: done ${_doneOptimism.length}건 낙관 의심 미해소 (${_doneOptimism.slice(0, 3).map(x => x.id).join(', ')}) — evidence 주장 vs 코드 흔적 불일치, 마감 전 verify-claim 재확인 권장 (advisory)`);
|
|
48
|
+
// 1.17.6 (UR-0049): 마감 보안 재확인 — 커밋 대상 시크릿이 살아있으면 'clean' 으로 마감하지 않도록 표면화. advisory(차단 X).
|
|
49
|
+
let _closeSecrets = 0;
|
|
50
|
+
try { if (_collectSecretFindings) _closeSecrets = ((_collectSecretFindings(root) || {}).committed || []).length; } catch {}
|
|
51
|
+
jsonResult.closeSecurity = { committedSecrets: _closeSecrets };
|
|
52
|
+
if (_closeSecrets) log(` 🚨 마감 보안: 커밋 대상 시크릿 ${_closeSecrets}건 미해소 — clean 아님, leerness scan secrets 확인 후 마감 권장`);
|
|
37
53
|
|
|
38
54
|
function rowsToList(arr) {
|
|
39
55
|
if (!arr || !arr.length) return '- 없음';
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -451,9 +451,11 @@ total++;
|
|
|
451
451
|
fs.writeFileSync(path.join(tmpV, 'src/myMod.js'), 'module.exports = {};\n');
|
|
452
452
|
fs.writeFileSync(path.join(tmpV, 'tests/test.js'), 'check(1); check(2); check(3); check(4); check(5);\n');
|
|
453
453
|
// T-row를 evidence와 함께 추가
|
|
454
|
+
// 1.17.4 (UR-0047): evidence 에 명시적 개수 주장(테스트 5개) 포함 — 카운트 검증이 실제로 수행되는 경로를 테스트.
|
|
455
|
+
// 이전 evidence 는 "(5/5 통과)"(pass 비율)만 있어 개수 주장이 없었는데도 옛 코드가 "✓ pass (실측 ≥ 주장)" 으로 표기(측정실패=통과 모순의 일부) — 정직화로 "⊘ (주장 없음)" 이 되므로 의도(카운트 검증)에 맞게 주장을 명시.
|
|
454
456
|
fs.appendFileSync(path.join(tmpV, '.harness/progress-tracker.md'),
|
|
455
|
-
'| T-0099 | done | 신모듈 | src/myMod.js + tests/test.js (5/5 통과) | next | 2026-05-14 |\n');
|
|
456
|
-
// 정상: 파일 존재 + 테스트 5개
|
|
457
|
+
'| T-0099 | done | 신모듈 | src/myMod.js + tests/test.js 테스트 5개 (5/5 통과) | next | 2026-05-14 |\n');
|
|
458
|
+
// 정상: 파일 존재 + 테스트 5개 (주장 5 = 실측 5)
|
|
457
459
|
const r = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0099', '--path', tmpV], { encoding: 'utf8', timeout: 15000 });
|
|
458
460
|
const okPass = r.status === 0 && /✓ src\/myMod\.js/.test(r.stdout) && /✓ tests\/test\.js/.test(r.stdout) && /pass \(실측 ≥ 주장\)/.test(r.stdout);
|
|
459
461
|
// 파일 없는 케이스 → exit ≠ 0
|