leerness 1.36.80 → 1.36.82
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 +38 -0
- package/README.ko.md +1 -1
- package/README.md +4 -4
- package/bin/leerness.js +218 -16
- package/docs/interoperability.md +2 -2
- package/lib/analyzers.js +22 -2
- package/lib/referee.js +0 -0
- package/package.json +1 -1
- package/scripts/e2e.js +149 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.82 — 2026-07-28 — 공허한 가드 클래스 스윕: 날조된 완료 주장이 통과하던 fail-open · 안내만 되고 없던 플래그 · 자기참조 소스가드
|
|
4
|
+
|
|
5
|
+
1.36.81 의 교훈("셋업이 조용히 실패해도 부정 단언은 통과한다")을 **클래스로 확장**해, "통과하지만 아무것도 증명하지 않는 가드"를 전수 스윕했다(5개 렌즈 · 변이 테스트로 실행 확정). 보고 8건 중 직접 재현한 것만 반영했고, **수치가 과장된 보고는 정정**했다(자기참조 가드: 보고 42건 → 실측 12건. 내 1차 탐지기도 `lib/` 를 빼먹어 39건으로 과대보고했다가 교정).
|
|
6
|
+
|
|
7
|
+
- **High — 플래그십 게이트가 가장 위험한 케이스에서 반대로 작동했다.** `verify-claim` 은 evidence 에 파일 경로가 없으면 `fileChecks.every([])`=true(공허참) · `stubFiles`=[] 이라 구현실체 true · git 교차검증은 `files.length>0` 을 요구해 skip — **모든 판정이 공허참**이 되어 "코드도 테스트도 없이 done" 이 `ok:true`/exit 0 이었다. **단조성이 뒤집혀 있었다**: 파일명을 적어 검증 가능하게 쓸수록 FAIL, 아무것도 안 적고 날조할수록 PASS. 코드 주석은 "공허참은 evidenceQualityOk 가 차단한다"고 적혀 있었지만 기본 모드의 그 값은 `hasFile||hasTest||hasLog` 라 `"tests: 32/32 passed"` 문자열 하나로 통과했다 — **주석의 전제가 거짓**이었다. 새 사유 `unverifiable-claim` 을 세 경로(collect/`--json`/human)가 공유하는 한 곳에 배선. 탈출구는 두 가지뿐(evidence 에 수정 파일 경로를 적거나 `--run-tests` 로 실행 결과를 붙이거나), 기존 `--lenient` opt-out 은 그대로.
|
|
8
|
+
- **한계(과장하지 않음)**: 이 수정은 "대조된 것이 하나도 없는" 구멍을 닫을 뿐, 존재하는 아무 파일명(예: `README.md`)을 적으면 통과한다. verify-claim 은 증거 대조이지 구현 증명이 아니다.
|
|
9
|
+
- **추출기 사각지대 동반 수정**: "대조 0건 = FAIL" 로 바꾸면 추출기가 못 보는 파일이 곧 정직한 주장의 거짓 차단이 된다. 확장자 없는 표준 파일명(`Dockerfile`/`Makefile`/`LICENSE` 등)을 추출 대상에 추가하고, `_evidenceQuality` 의 "파일 언급" 판정도 같이 맞췄다(주석이 "bin FILE_RE 와 정합"을 명시하는데 한쪽만 고치면 같은 명령 안에서 정의가 둘로 갈린다).
|
|
10
|
+
- **High — 툴이 안내하는 플래그가 구현되지 않았다.** `referee verify` 성공 시 leerness 는 "이제 `gate --require-referee <id>` 가 이 검증기를 신뢰합니다"라고 출력하는데, `gate` 는 그 플래그를 **읽지도 않았다**(패키지 전체에서 그 문자열은 안내문 한 줄뿐). 사용자는 안내받은 명령을 CI 에 넣고 검증기가 머지를 막는다고 믿지만 아무 일도 일어나지 않았고, 부재/무효 검증기에서도 exit 0 이라 오배선을 알 신호조차 없었다 — 1.36.80 에서 내가 만든 약속이 곧 거짓말이었다. 이제 실제로 게이팅한다: 미존재/미증명/무효 검증기는 실행 전 거부, 검증기가 현재 상태를 거부하면 게이트 실패, 반복 지정(`--require-referee a --require-referee b`) 지원, 플래그가 없으면 기존 5체크 그대로.
|
|
11
|
+
- **High — 자기참조 소스가드**: `read(__filename)` 로 자기 파일을 읽는 selftest 가, 검사하려던 코드 리터럴이 리팩터로 사라지면 그 문자열이 **가드 자신의 줄에만** 남아 영원히 통과한다(기능을 통째로 지워도 초록). 실제로 **verify-claim 의 `--json` exit 게이트**와 **개인키 파일 스캔 강제포함**이 그 상태였다. 고가치 4건을 현재 구현 기준으로 교체하고, **확산 금지 메타가드**를 추가했다 — 제품 코드(bin 나머지 + `lib/` + `scripts/`)에 없는 **코드형** 리터럴을 붙잡은 케이스가 늘어나면 실패한다. 잔여 부채 9건은 숨기지 않고 baseline 으로 명시했다(줄이는 방향으로만 갱신).
|
|
12
|
+
- **문서 수치 메타가드**: `docs/interoperability.md` 가 "86 도구"(3릴리스 낡음)를, `README.ko.md` 가 "selftest 210 케이스"(실제 336)를 **현재 상태**로 주장한 채 npm 에 실려 나갔다. `README.md` 는 `readmeSync` 관리구간이라 동적으로 맞았지만 관리구간 밖 문서는 아무도 맞춰주지 않는다. 낡을 수치를 살아있는 출처 안내로 대체하고, README/docs 의 MCP 도구·selftest 수가 실측과 어긋나면 selftest 가 실패하도록 했다(과거 라운드 로그인 AGENTS/CLAUDE/CHANGELOG 는 과거 수치가 정당하므로 제외 — 포함하면 false-BLOCK).
|
|
13
|
+
- **가드를 만들며 두 번 틀렸고 둘 다 실행으로 잡았다**: (1) 사이트 수치 가드 1차판이 5/5 오탐(주석·파생표현·다른 개념의 "0 의존성")이라 대상을 실제로 낡은 이력이 있는 수치로 좁혔다. (2) 그 가드가 하드코딩 재삽입을 **차단하지 못했다** — JS 의 `\b` 는 ASCII 기준이라 `(?:도구|tools?)\b` 가 한글 뒤에서 성립하지 않았다. 차별적 통제를 돌리지 않았다면 아무것도 지키지 않는 가드를 출하할 뻔했다. 같은 버그로 내 수동 grep 도 낡은 주장 2건 중 1건만 보고 있었다.
|
|
14
|
+
**검수 24회전이 BLOCK 판정 + High 6건** — 전건 직접 재현 후 수정. 이번 라운드는 **내 자체 검증이 여섯 개를 다 놓쳤다**:
|
|
15
|
+
|
|
16
|
+
- **내가 고치려던 역전을 다시 만들었다.** `_runProven` 을 `runResult.parsed` 로 판정한 탓에, `--test-cmd 'node -e "process.exit(0)"'` 처럼 **실제로 실행해 exit 0 을 받아도** 출력이 파싱 불가면 차단되고, evidence 에 `"1/1 passed"` 문자열만 적은 날조는 통과했다. 커스텀 리포터·`go test` 를 쓰는 정직한 실행이 벌을 받는 구조 — 진단 신호로 삼았던 단조성 역전 그 자체다. 판정 기준을 **실행 확증**(`allPassed`)으로 교체.
|
|
17
|
+
- **추출기 사각지대 4형태가 곧 거짓 차단이었다**: `ops/containers/Dockerfile`(다단계 경로의 bare 파일) · `.github/workflows/ci.yml`(선두 점이 잘려 `github/…` 라는 없는 파일로 판정) · `.gitignore` · `Dockerfile.dev`(내가 추가한 bare 규칙이 `Dockerfile` 로 잘라 "없는 파일"로 만듦). bin 추출기와 `_evidenceQuality` 로 갈려 있던 판정을 **단일 출처**(`lib/analyzers._EVIDENCE_FILE_RE`)로 통합.
|
|
18
|
+
- **툴이 안내하는 문법 그대로 쓰면 깨졌다.** `--require-referee` 를 value-flag 목록에 등록하지 않아 값이 positional 경로로 흡수돼 root 가 `<cwd>/<id>` 가 됐다(소스 주석이 "신규 value-flag 는 반드시 여기 등록" 이라고 명시하는데 어겼다). **내 e2e 는 항상 `--path` 와 `.` 을 줘서 이 결함을 가리고 있었다** — 픽스처가 버그를 숨기는 전형이라, 이제 안내된 문법 그대로(cwd + 플래그만) 검증한다. 1.36.80/81 의 다른 신규 value-flag 도 소급 등록.
|
|
19
|
+
- **값 없는 `--require-referee` 가 조용히 사라졌다** — CI 의 `--require-referee=$EMPTY_ENV` 가 아무 게이팅 없이 exit 0(fail-open). 명시적 실패 단계(`referee_missing_value`)로 전환.
|
|
20
|
+
- **메타가드 자신이 반쯤 공허했다**: 케이스 파싱이 337 중 170 만 잡았고, 자기 제외 표식을 `'selfref' + '-guard-own'` 로 조립해 **소스에 결합된 문자열이 없어 한 번도 동작하지 않았다**(자기를 세지 않은 건 우연). 백틱 리터럴로 우회도 가능했다. 시작 경계 분할로 전수 파싱 + 백틱 포함 + 표식 교정, baseline 은 과소집계였던 9 → **실측 12** 로 정정.
|
|
21
|
+
- **"탐지력 증명됨" 은 실제보다 강한 주장이었다** — 캘리브레이션이 확인하는 것은 등록된 known-good/known-bad 쌍이 기대 사유로 갈린다는 것과 `check` 의 기동 가능성뿐이고, good/bad 가 `check` 와 같은 절차인지는 **도구가 강제할 수 없다**. 문구를 사실에 맞추고 그 조건을 명시.
|
|
22
|
+
- Medium 동반 수정: gate 의 referee 실패가 `--json` 에서 `{name, ok:false}` 로만 축약되던 것 → `code`/`exit`/`timedOut`/`spawnError`/`signal` 노출 · 캘리브레이션 timeout 정책을 gate 와 공유(느린 검증기 거짓 차단 방지) · 같은 id 반복은 순서 보존 dedupe · 문서 수치 가드의 어순 양방향·core 문맥·`80+` 하한 표현·`CONTRIBUTING/SECURITY` 스캔 보강.
|
|
23
|
+
|
|
24
|
+
- 검증: selftest 337 · e2e **406/406**(신규 2블록) · 신규 단언의 **변이 테스트**(수정을 되돌리면 정확히 실패) · 메타가드 4방향(따옴표/백틱 자기참조 차단 · 정상 가드 무오탐) · false-BLOCK 시나리오(문서만 수정 / 추출기 사각지대 5형태 / 비-done / `--run-tests` 실행확증 / `--lenient`) 실측.
|
|
25
|
+
|
|
26
|
+
## 1.36.81 — 2026-07-28 — 게시본 클린룸 실측이 잡은 2건 (referee 상태 신호 정확화 · 출하 소스 생 NUL 제거)
|
|
27
|
+
|
|
28
|
+
1.36.80 을 게시한 뒤 **게시된 tarball 로** 신규 기능을 실행해 본 결과 2건이 나왔다. 둘 다 게이트(selftest 334 · e2e 404)는 초록이었다 — 단언이 없던 자리다.
|
|
29
|
+
|
|
30
|
+
- **referee 상태 신호가 부정확했다** — `isStale()` 이 "캘리브레이션이 아예 없음"까지 stale 로 묶어, **한 번도 검증한 적 없는** 검증기가 `verify-claim` 게이트에서 `referee_stale` 로 보고됐다. 두 상태는 사용자가 취할 조치가 다르다(캘리브레이션 실행 vs 재검증). 정직한 신호가 존재 이유인 기능에서 신호 자체가 틀린 셈이라 고쳤다: stale 은 **"한때 증명됐으나 정의가 바뀌어 무효"** 만을 뜻하고, 미증명은 `referee_uncalibrated`, 캘리브레이션 실패도 `referee_uncalibrated`(사유 포함). fail-closed 동작은 이전과 동일 — 어느 상태든 통과시키지 않는다. e2e 가 미존재/미증명/무효/검증기실패 **4구분**을 고정한다.
|
|
31
|
+
- **출하 소스에 생 NUL 바이트** — 지문·ID 구분자를 소스에 raw NUL 로 써서 git/grep 이 `bin/leerness.js` 와 `lib/referee.js` 를 **바이너리로 취급**했다(diff 가 `Bin 16408 -> 17121 bytes` 로만 표시, 코드 검색 차단). 더 위험한 쪽은 정규화 도구가 이 바이트를 조용히 지우면 해시 입력이 바뀌어 **기존 프로젝트의 캘리브레이션 지문과 api-skill ID 가 한꺼번에 어긋난다**는 점이다. 런타임 값이 동일한 유니코드 이스케이프 표기로 치환(기존 지문/ID 불변 — 실측 확인) + selftest 메타가드로 bin/lib 전수 0 을 상시 강제.
|
|
32
|
+
**검수 23회전이 잡은 4건** (전건 직접 재현 확인):
|
|
33
|
+
|
|
34
|
+
- **High — fail-closed 기능의 fail-open**: `calibration.ok` 가 `"false"`(truthy 문자열)나 `1` 로 손상/조작되면 `!r.calibration.ok` 가 false 가 되어 **미증명 검증기가 게이트를 그대로 통과**했고 `referee show` 는 `calibrated: true` 로 표시했다. 이 기능의 전제 자체를 뚫는 구멍이다. `_entryValid` 가 `ok`(boolean)·`fingerprint`(비어있지 않은 문자열)를 형상 검증해 거부하고(스토어 전체 fail-closed — previews/teams 와 동일 규율), 판정 지점도 `=== true` 엄격 비교로 이중 방어. 형상 무효는 "없음" 대신 `referee_store_invalid` 로 정확히 보고한다.
|
|
35
|
+
- **캘리브레이션 실패 + 정의 변경을 `referee_stale` 로 오분류** — 증명된 적이 없는데 "있던 증명이 무효화됐다"는 거짓 서사였다. 실패 상태는 정의가 바뀌어도 `referee_uncalibrated`.
|
|
36
|
+
- **메타가드 스캔 범위가 이름보다 좁음** — `scripts/` 도 `package.json` `files` 에 포함돼 npm 에 함께 실리는데 스캔하지 않았다. bin/lib/scripts 전수로 확장.
|
|
37
|
+
- **공허한 e2e 단언 2건** — "FP 프로브 3종 거부" 중 둘은 `--expect-bad` 없이 `add` 해서 **add 자체가 `missing_args` 로 실패**했고, 이어진 `verify` 가 돌려준 `not_found`(ok:false)를 e2e 가 "거부됨"으로 통과 처리했다. 탐지력 없는 검증기를 거부한다는 걸 전혀 증명하지 못했다. add 성공을 먼저 단언하고 거부 **사유**까지 확인하도록 교체.
|
|
38
|
+
|
|
39
|
+
- 검증: selftest 335 · e2e 전건 · 게시본 실측 프로브(게이트 4구분 · 지문 동치 · 임시 워크스페이스 정리 · 포트 5337) · 검수 지적 4건 수정 전/후 실측 대조.
|
|
40
|
+
|
|
3
41
|
## 1.36.80 — 2026-07-28 — referee 캘리브레이션 · 라이브 미리보기 · 기본 스킬 정리 · 최소성 사다리 — MCP 89 도구 · R-0001 검수 22회전
|
|
4
42
|
|
|
5
43
|
**이 릴리스에 함께 들어간 것** (아래 상세):
|
package/README.ko.md
CHANGED
|
@@ -154,7 +154,7 @@ leerness mcp serve # JSON-RPC over stdio, 80+ 도구
|
|
|
154
154
|
- **원자적 UTF-8 쓰기** — temp + rename 으로 부분쓰기 손상 방지, BOM 자동 strip.
|
|
155
155
|
- **shell 미경유 MCP** — `mcp serve` 의 도구 호출은 셸을 거치지 않고 인자를 직접 전달(명령 주입 차단).
|
|
156
156
|
- **순수 `--json` / 일관 exit code** — 성공·실패·미존재 경로 모두 파싱 가능한 JSON(`{ok,error,code}`) + 실패 시 exit 1. 자동화·CI 친화.
|
|
157
|
-
- **모듈 분리(DI)** + **내장 자가검증** — `lib/` 순수 유틸/IO/카탈로그 분리, `selftest`
|
|
157
|
+
- **모듈 분리(DI)** + **내장 자가검증** — `lib/` 순수 유틸/IO/카탈로그 분리, `selftest` 가 코어 함수 무결성을 자가 검증(`doctor` 가 함께 실행하며 케이스 수를 실측 출력). 문서(README) 부재에도 코어 무결성 진단은 통과.
|
|
158
158
|
|
|
159
159
|
---
|
|
160
160
|
|
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.82 하네스를 사용합니다. 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.82는 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.80는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
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.82 릴리스 태그 이력** (수백 라운드) · _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.82: 2026-07-28
|
|
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.82';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -208,6 +208,26 @@ function arg(name, def = null) {
|
|
|
208
208
|
return eq ? eq.slice(name.length + 1) : def;
|
|
209
209
|
}
|
|
210
210
|
function has(name) { return process.argv.includes(name); }
|
|
211
|
+
// 1.36.82: 반복 지정 값 플래그를 `--flag=v` 형태까지 포함해 수집하고, **값 누락도 보고**한다.
|
|
212
|
+
// (검수 High#4) 값 없는 `--require-referee` 나 `--require-referee=` 를 조용히 버리면
|
|
213
|
+
// CI 의 `--require-referee=$EMPTY_ENV` 가 아무 게이팅 없이 exit 0 — fail-open 이다.
|
|
214
|
+
// 기존 argAll() 은 값만 돌려주므로 누락을 알 수 없어, 누락 개수를 함께 반환하는 변형을 쓴다.
|
|
215
|
+
function _argAllStrict(name) {
|
|
216
|
+
const values = [];
|
|
217
|
+
let missing = 0;
|
|
218
|
+
const a = process.argv;
|
|
219
|
+
for (let i = 0; i < a.length; i++) {
|
|
220
|
+
if (a[i] === name) {
|
|
221
|
+
const next = a[i + 1];
|
|
222
|
+
if (next != null && !next.startsWith('-')) { values.push(next); i++; }
|
|
223
|
+
else missing++;
|
|
224
|
+
} else if (a[i].startsWith(name + '=')) {
|
|
225
|
+
const v = a[i].slice(name.length + 1);
|
|
226
|
+
if (v) values.push(v); else missing++;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return { values, missing };
|
|
230
|
+
}
|
|
211
231
|
// 공통 root 해석 (외부리뷰 CV-1/UR-0076): --path(=값 포함) 플래그 > 유효 positional > cwd.
|
|
212
232
|
// 여러 dispatcher 가 positional 또는 --path 한쪽만 처리하던 불일치(특히 session close 의 wrong-root 쓰기) 해소.
|
|
213
233
|
function _resolveRoot(positional) {
|
|
@@ -218,7 +238,7 @@ function _resolveRoot(positional) {
|
|
|
218
238
|
}
|
|
219
239
|
function nonFlagArgs() {
|
|
220
240
|
const out = [];
|
|
221
|
-
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--to','--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 는 반드시 여기 등록)
|
|
241
|
+
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--to','--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','--require-referee','--referee','--expect-bad','--good','--bad','--static-dir','--dev-url']); // 1.14.2 (UR-0032): --done-when 값이 positional 로 누출돼 milestone 제목에 흡수되던 것 차단. 1.17.2 (UR-0045): --test-cmd 동일 원칙(신규 value-flag 는 반드시 여기 등록). 1.36.82 (검수 High#3): --require-referee 미등록으로 `gate --require-referee <id>` 의 값이 positional 경로로 흡수돼 root 가 <cwd>/<id> 가 됐다 — 툴이 안내하는 문법 그대로 쓰면 깨졌다. 1.36.80/81 신규 value-flag 도 소급 등록.
|
|
222
242
|
const a = process.argv.slice(2);
|
|
223
243
|
for (let i = 0; i < a.length; i++) {
|
|
224
244
|
const x = a[i];
|
|
@@ -4442,8 +4462,15 @@ function _selfTestCases() {
|
|
|
4442
4462
|
// 1) done 기본 claimsChecked(--strict-claims 옵트인 아님) 2) --json 에 claimsConsistent+gitCrossCheck 3) --lenient 탈출 4) optimism 전체스캔(walk root) 5) git strongMismatch 는 advisory(기본 게이트 제외)
|
|
4443
4463
|
const defaultGate = src.includes("const claimsChecked = !lenient && (isDoneClaim || has('--require-evidence') || has('--strict-claims'));");
|
|
4444
4464
|
const jsonExposed = src.includes('claimsConsistent: !claimsChecked ? null : strictOk') && src.includes('gitCrossCheck: !gitApplicable ? null : !gitStrongMismatch');
|
|
4445
|
-
|
|
4446
|
-
|
|
4465
|
+
// 1.36.82 (공허한 가드 스윕 High): 이 리터럴은 리팩터로 사라진 옛 표현이라 **자기 줄만 매칭**하고 있었다 —
|
|
4466
|
+
// verify-claim 의 --json exit 게이트가 어떤 selftest 로도 보호되지 않았다(게이트를 통째로 지워도 초록).
|
|
4467
|
+
// 현재 구현(reasons 기반 단일 판정 + exit 전파)을 검사한다.
|
|
4468
|
+
const jsonGate = src.includes("if (claimsChecked && !strictOk) out.reasons.push('optimism/honesty')")
|
|
4469
|
+
&& src.includes('out.ok = out.reasons.length === 0;') && src.includes('if (!out.ok) return process.exit(1);');
|
|
4470
|
+
// 1.36.82: 끝의 `;` 때문에 실제 overallFail 라인과 어긋나 **자기 줄만 매칭**하고 있었다(리팩터로 뒤에 항이 추가됨).
|
|
4471
|
+
// 현재 라인과 일치시키고, 1.36.82 에 추가한 unverifiableClaim 게이팅이 human 경로에 배선됐는지도 함께 단언한다.
|
|
4472
|
+
const overall = src.includes('(claimsChecked && !strictOk) || !evidenceQualityOk || !gitClaimOk')
|
|
4473
|
+
&& /const overallFail = unverifiableClaim \|\|/.test(src);
|
|
4447
4474
|
const wholeScan = src.includes('function _scanCodeForPatterns(root)') && src.includes('walk(root, 0);');
|
|
4448
4475
|
const gitAdvisory = src.includes("const gitClaimOk = !(has('--strict-claims') && gitStrongMismatch);");
|
|
4449
4476
|
return defaultGate && jsonExposed && jsonGate && overall && wholeScan && gitAdvisory;
|
|
@@ -4463,7 +4490,12 @@ function _selfTestCases() {
|
|
|
4463
4490
|
const src = read(__filename);
|
|
4464
4491
|
const coreDef = src.match(/function _verifyClaimsAll\(root\) \{[\s\S]*?\n\}/); // 함수 본문만 캡처(첫 줄머리 } 까지)
|
|
4465
4492
|
const core = !!coreDef && coreDef[0].includes('return { ok: failed.length === 0, total: doneRows.length, failed: failed.length, results };') && !/process\.exit\(/.test(coreDef[0]); // 코어는 절대 process.exit( 안 함(게이트 step 집계 보호)
|
|
4466
|
-
|
|
4493
|
+
// 1.36.82: 헤더 문자열을 **정확 리터럴**로 잡으면 그 줄을 만질 때마다 자기 줄만 매칭해 조용히 통과한다
|
|
4494
|
+
// (이번 라운드에만 두 번 그렇게 됐다). 표현이 아니라 **불변식**을 본다:
|
|
4495
|
+
// 헤더가 withClaims 로 5/6 을 계산하고, --claims 일 때만 verify-claims 단계가 붙는다.
|
|
4496
|
+
const gateOptIn = src.includes("const withClaims = has('--claims');")
|
|
4497
|
+
&& /# leerness gate \(\$\{[^}]*withClaims \? 6 : 5[^}]*\} checks\)/.test(src)
|
|
4498
|
+
&& src.includes("if (withClaims) step('verify-claims', () => { const r = _verifyClaimsAll(root);");
|
|
4467
4499
|
const reuse = src.includes('const res = _verifyClaimsAll(root);'); // CLI 도 코어 공유(분기 없음)
|
|
4468
4500
|
const tools = require('../lib/mcp-tools');
|
|
4469
4501
|
const def = tools.find(t => t.name === 'leerness_verify_claim_all');
|
|
@@ -4772,9 +4804,13 @@ function _selfTestCases() {
|
|
|
4772
4804
|
&& src.includes("else if (surface === 'lessons') _saveLessons(root, _parseLessonEntries(read(activePath)))");
|
|
4773
4805
|
// P2-4: plan remove 가 모든 헤딩(#{1,3} )을 경계로 split (사용자 h2 섹션 흡수 방지)
|
|
4774
4806
|
const planOk = src.includes('const blocks = text.split(/\\n(?=#{1,3} )/);');
|
|
4775
|
-
// P2-5:
|
|
4776
|
-
|
|
4777
|
-
|
|
4807
|
+
// P2-5: 숫자로 시작하는 실파일('123.js'·'2fa.ts')이 추출돼야 한다 — 안 되면 존재검사가 공허통과했다.
|
|
4808
|
+
// 1.36.82: 정규식 **조각 문자열**을 검사하던 것을 **행위 검사**로 교체 — 추출기를 단일 출처(lib/analyzers)로
|
|
4809
|
+
// 옮기자 조각이 사라져 가드가 자기 줄만 매칭할 뻔했다(정확 리터럴 가드의 전형적 부패).
|
|
4810
|
+
const _re = require('../lib/analyzers')._EVIDENCE_FILE_RE('g');
|
|
4811
|
+
const _hit = (s) => Array.from(String(s).matchAll(_re), m => m[0]);
|
|
4812
|
+
const fileReOk = _hit('123.js 수정').includes('123.js') && _hit('2fa.ts 추가').includes('2fa.ts');
|
|
4813
|
+
const analyzersOk = require('../lib/analyzers')._evidenceQuality('123.js 수정, tests: 1/1 passed').hasFile === true;
|
|
4778
4814
|
// P2-3: archive 쓰기에 _lineSafe (개행 델리미터 위조 차단)
|
|
4779
4815
|
const archiveOk = src.includes('`\\n## 제거 ${today()} (target: "${_lineSafe(target)}")\\n${archiveBlocks}\\n`');
|
|
4780
4816
|
// P2-7: drop/remove not-found 가 failJson (--json 에러 구조화)
|
|
@@ -4934,6 +4970,98 @@ function _selfTestCases() {
|
|
|
4934
4970
|
while ((m = re.exec(self)) !== null) { if (m[1].includes(NL)) offenders++; }
|
|
4935
4971
|
return offenders === 0;
|
|
4936
4972
|
} },
|
|
4973
|
+
{ name: '자기참조 소스가드 확산 금지 (1.36.82, 공허한 가드 스윕): 제품에 없는 코드 리터럴을 붙잡은 selftest 가 늘어나면 실패 (메타가드)', run: () => {
|
|
4974
|
+
// 사고: 소스가드가 `read(__filename)` 로 자기 파일을 읽는데, 검사하려는 코드 리터럴이 리팩터로 사라지면
|
|
4975
|
+
// 그 문자열은 **가드 자신의 줄에만** 남는다 → includes 는 계속 참 → 기능을 통째로 지워도 초록.
|
|
4976
|
+
// 실제로 verify-claim 의 --json exit 게이트와 개인키 파일 스캔 강제포함 가드가 이 상태였다.
|
|
4977
|
+
// 판정: selftest 영역 밖(제품 코드 = bin 나머지 + lib/ + scripts/)에 존재하지 않는 **코드형** 리터럴.
|
|
4978
|
+
// 데이터 문자열(파일명·placeholder 등)이 제품에 없는 건 정상이므로 코드 신호가 있는 것만 본다(false-BLOCK 회피).
|
|
4979
|
+
const src = read(__filename);
|
|
4980
|
+
// (검수 High#6) 1차판은 `'selfref' + '-guard-own'` 을 MARK 로 써서 **소스 본문에는 결합된 문자열이 없어**
|
|
4981
|
+
// 자기 제외가 한 번도 동작하지 않았다(자기 자신을 안 센 건 우연이었다). 소스에 그대로 존재하는 표식을 쓴다.
|
|
4982
|
+
const MARK = 'SELFREF_METAGUARD_OWN_CASE';
|
|
4983
|
+
const s = src.indexOf('function _selfTestCases(');
|
|
4984
|
+
if (s < 0) return false;
|
|
4985
|
+
const nextFn = src.indexOf('\nfunction ', s + 10);
|
|
4986
|
+
const e = nextFn < 0 ? src.length : nextFn;
|
|
4987
|
+
let outside = src.slice(0, s) + src.slice(e);
|
|
4988
|
+
for (const sub of ['lib', 'scripts']) {
|
|
4989
|
+
const dir = path.join(path.dirname(__filename), '..', sub);
|
|
4990
|
+
if (!exists(dir)) continue;
|
|
4991
|
+
for (const f of fs.readdirSync(dir)) if (f.endsWith('.js')) outside += '\n' + read(path.join(dir, f));
|
|
4992
|
+
}
|
|
4993
|
+
const region = src.slice(s, e);
|
|
4994
|
+
// (검수 High#6) 1차판의 종결자 기반 정규식(`\n } },`)은 337건 중 170건만 매치했다 —
|
|
4995
|
+
// 여러 케이스를 하나로 뭉치거나(마지막 무쉼표 케이스 등) 통째로 놓쳐 가드 자신이 반쯤 공허했다.
|
|
4996
|
+
// 케이스 **시작 경계**로 분할한다(줄머리 ` { name:`) — 종결 형태에 의존하지 않는다.
|
|
4997
|
+
const chunks = region.split(/\n(?= \{ name: )/).slice(1);
|
|
4998
|
+
// 백틱 리터럴도 스캔한다(1차판은 따옴표만 봐서 `.includes(\`…\`)` 로 우회 가능했다).
|
|
4999
|
+
const litRe = /\.includes\((['"`])((?:\\.|(?!\1)[\s\S])*?)\1\)/g;
|
|
5000
|
+
const codeish = (L) => /[(){}=;]|=>|\|\||&&/.test(L) && /[a-zA-Z_$]{3,}/.test(L);
|
|
5001
|
+
let offenders = 0;
|
|
5002
|
+
for (const body of chunks) {
|
|
5003
|
+
if (body.includes(MARK)) continue; // 이 메타가드 자신 (표식은 소스에 그대로 존재)
|
|
5004
|
+
if (!/read\(__filename\)/.test(body)) continue;
|
|
5005
|
+
litRe.lastIndex = 0;
|
|
5006
|
+
let lm;
|
|
5007
|
+
while ((lm = litRe.exec(body)) !== null) {
|
|
5008
|
+
const L = lm[2].replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
5009
|
+
if (L.length >= 20 && codeish(L) && !outside.includes(L)) { offenders++; break; }
|
|
5010
|
+
}
|
|
5011
|
+
}
|
|
5012
|
+
// 1.36.82 실측 잔여 부채 = 12건(파싱 교정 후 전수 337 케이스 기준) — 늘어나면 실패한다.
|
|
5013
|
+
// 줄이는 방향으로만 갱신할 것. "아직 남아 있음"을 숨기지 않기 위해 수치를 명시한다.
|
|
5014
|
+
// 1차판이 보고하던 9 는 케이스 파싱이 337 중 170 만 잡던 과소집계였다(검수 High#6).
|
|
5015
|
+
return offenders <= 12;
|
|
5016
|
+
} },
|
|
5017
|
+
{ name: '동봉 문서의 낡을 수치 주장 금지 (1.36.82): README/docs 의 MCP 도구·selftest 케이스 수가 실측과 불일치 시 실패 (메타가드)', run: () => {
|
|
5018
|
+
// 사고: docs/interoperability.md 가 "86 도구"(3릴리스 낡음), README.ko.md 가 "selftest 210 케이스"(실제 335) 를
|
|
5019
|
+
// **현재 상태**로 주장한 채 npm 에 실려 나갔다. README.md 는 readmeSync 관리구간이라 동적으로 맞았지만
|
|
5020
|
+
// 관리구간 밖 문서는 아무도 맞춰주지 않는다 — leerness 자신이 "측정 안 된 수치 주장 금지"를 강제하는 도구인데 자기 문서가 어겼다.
|
|
5021
|
+
// 범위(정직하게 좁힘): 현재 상태를 서술하는 문서만. AGENTS.md/CLAUDE.md/CHANGELOG.md 는 **버전별 과거 라운드 로그**라
|
|
5022
|
+
// 과거 수치가 정당하므로 제외한다(포함하면 false-BLOCK 이 된다).
|
|
5023
|
+
const root = path.join(path.dirname(__filename), '..');
|
|
5024
|
+
// (검수 Medium) npm tarball 에 실리는 루트 문서를 빠짐없이 — CONTRIBUTING/SECURITY 도 대상이다.
|
|
5025
|
+
const targets = ['README.md', 'README.ko.md', 'CONTRIBUTING.md', 'SECURITY.md'].map(f => path.join(root, f));
|
|
5026
|
+
const docsDir = path.join(root, 'docs');
|
|
5027
|
+
if (exists(docsDir)) for (const f of fs.readdirSync(docsDir)) if (f.endsWith('.md')) targets.push(path.join(docsDir, f));
|
|
5028
|
+
const mcpN = _mcpToolCount();
|
|
5029
|
+
const selfN = _selfTestCases().length;
|
|
5030
|
+
let coreN = 0; try { coreN = (require('../lib/mcp-tools').CORE || []).length; } catch {}
|
|
5031
|
+
for (const f of targets) {
|
|
5032
|
+
if (!exists(f)) continue;
|
|
5033
|
+
for (const raw of read(f).split('\n')) {
|
|
5034
|
+
// 버전 표기는 카운트 주장이 아니다 — 지우고 본다(1.36.81 을 "36 도구"로 오독하는 것 방지)
|
|
5035
|
+
const line = raw.replace(/\bv?\d+\.\d+(?:\.\d+)?\b/g, ' ');
|
|
5036
|
+
// (검수 Medium) core 프로필 문맥의 수치는 full 과 비교하면 거짓 차단이다 — 그 줄은 core 기준으로 본다.
|
|
5037
|
+
const coreCtx = /\bcore\b|코어|프로필/i.test(line);
|
|
5038
|
+
const expectMcp = coreCtx ? coreN : mcpN;
|
|
5039
|
+
// "80+" 같은 하한 표현은 정확 수치 주장이 아니다 — 대상에서 제외(false-BLOCK 회피).
|
|
5040
|
+
if (/\d\s*\+/.test(line)) continue;
|
|
5041
|
+
// (검수 Medium FN) 어순 양방향: "89 도구" 와 "MCP tools: 89" 둘 다 잡는다.
|
|
5042
|
+
// 주의: JS \b 는 ASCII 기준이라 한글 뒤에서 성립하지 않는다 → 경계는 영문 tools 에만 붙인다.
|
|
5043
|
+
let m = /\b(\d{2,3})\s*(?:개\s*)?(?:MCP\s*)?(?:도구|tools?\b)/i.exec(line)
|
|
5044
|
+
|| /(?:MCP[- ]?tools?|MCP\s*도구)\s*[:=]?\s*(\d{2,3})\b/i.exec(line);
|
|
5045
|
+
if (m && expectMcp && Number(m[1]) !== expectMcp) return false;
|
|
5046
|
+
m = /selftest[^\n]{0,16}?\b(\d{2,4})\b/i.exec(line) || /\b(\d{2,4})\s*selftest/i.exec(line);
|
|
5047
|
+
if (m && Number(m[1]) !== selfN) return false;
|
|
5048
|
+
}
|
|
5049
|
+
}
|
|
5050
|
+
return true;
|
|
5051
|
+
} },
|
|
5052
|
+
{ name: '출하 소스 생 NUL 금지 (1.36.81, 클린룸 실측): bin/lib 전수 0 — 구분자를 이스케이프로 (메타가드)', run: () => {
|
|
5053
|
+
// 1.36.80 사고: 지문/ID 구분자를 소스에 **생 NUL 바이트**로 써서 도구가 파일을 바이너리로 취급했다(grep 차단).
|
|
5054
|
+
// 정규화 도구가 조용히 지우면 해시 입력이 바뀌어 전 프로젝트의 캘리브레이션/ID 가 한꺼번에 어긋난다.
|
|
5055
|
+
// 런타임 값이 동일한 유니코드 이스케이프 표기로만 쓴다 — 이 가드 자신은 이스케이프 표기라 자기참조 안전.
|
|
5056
|
+
// (codex 검수 ③) scripts/ 도 package.json files 에 포함돼 npm 으로 함께 실린다 — "출하 소스"라면 같이 봐야 한다.
|
|
5057
|
+
const NUL = String.fromCharCode(0);
|
|
5058
|
+
const files = [__filename];
|
|
5059
|
+
for (const sub of ['lib', 'scripts']) {
|
|
5060
|
+
const dir = path.join(path.dirname(__filename), '..', sub);
|
|
5061
|
+
if (exists(dir)) for (const f of fs.readdirSync(dir)) if (f.endsWith('.js')) files.push(path.join(dir, f));
|
|
5062
|
+
}
|
|
5063
|
+
return files.every(f => !read(f).includes(NUL));
|
|
5064
|
+
} },
|
|
4937
5065
|
{ name: 'CLI 영어화 Phase 1 (1.20.2, UR-0010): _uiLang 해석(flag>env>manifest>ko) + 첫화면 _t 적용 (행위+소스)', run: () => {
|
|
4938
5066
|
const save = process.argv; const saveEnv = process.env.LEERNESS_LANG;
|
|
4939
5067
|
try {
|
|
@@ -5030,7 +5158,10 @@ function _selfTestCases() {
|
|
|
5030
5158
|
const bin = read(__filename);
|
|
5031
5159
|
const cat = read(path.join(path.dirname(__filename), '..', 'lib', 'catalogs.js'));
|
|
5032
5160
|
const pu = read(path.join(path.dirname(__filename), '..', 'lib', 'pure-utils.js'));
|
|
5033
|
-
|
|
5161
|
+
// 1.36.82 (공허한 가드 스윕): `!isKeyFile) continue;` 는 1.36.56 리팩터로 사라진 코드다 —
|
|
5162
|
+
// 그 문자열이 이 가드 자신의 줄에만 남아 **자기참조로 통과**하고 있었다(개인키 스캔을 꺼도 초록).
|
|
5163
|
+
// 현재 구현(_known 강제 포함)을 검사하도록 교체 + 행위로도 확인한다.
|
|
5164
|
+
const keyFile = bin.includes('const isKeyFile =') && bin.includes('|| isEnvFamily || isKeyFile');
|
|
5034
5165
|
const dbVg = /DB connection string[^\n]*valueGroup: 1/.test(cat);
|
|
5035
5166
|
const phPlaceholder = pu.includes('root|admin|user|username|yourpassword');
|
|
5036
5167
|
const retroGuard = bin.includes("failJson(has('--json'), 'invalid_arg'") && bin.includes('Math.min(days, 36500)');
|
|
@@ -6039,7 +6170,7 @@ function _apiSkillsDir(root) {
|
|
|
6039
6170
|
// 정체성(같은 URL·다른 name 이 같은 id 로 붕괴하던 것), Windows 예약 이름(con/prn/aux/nul/com1..9/lpt1..9) 회피.
|
|
6040
6171
|
// 충돌 접미사(-hash) 추가 후에도 이 함수를 다시 거쳐 상한이 유지된다.
|
|
6041
6172
|
function _apiSkillIdCap(id, url, name) {
|
|
6042
|
-
const seed = String(url || '') + '
|
|
6173
|
+
const seed = String(url || '') + '\u0000' + String(name || '');
|
|
6043
6174
|
if (!id || id.length > 80) id = (id || 'api').slice(0, 72).replace(/-+$/, '') + '-' + _shortHash(seed);
|
|
6044
6175
|
if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(id)) id = id + '-' + _shortHash(seed);
|
|
6045
6176
|
return id;
|
|
@@ -12971,7 +13102,12 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
12971
13102
|
// codex 버그헌트 P2: basename 첫 글자를 [A-Za-z] 로만 잡아 '123.js'·'2fa.ts' 같은 숫자시작 실파일이
|
|
12972
13103
|
// 추출 안 돼 존재검사가 vacuous-pass → 없는 파일 인용한 done 주장이 통과(gate FN). 첫 글자에 숫자 허용.
|
|
12973
13104
|
// (prev-separator 가드가 UNC/드라이브/URL 꼬리 오탐은 계속 차단하므로 FP 억제 유지)
|
|
12974
|
-
|
|
13105
|
+
// 1.36.82 (검수 High#2): 추출기와 `_evidenceQuality` 의 "파일 언급" 판정이 각자 따로였다(주석만 "정합").
|
|
13106
|
+
// 같은 명령 안에서 정의가 갈리면 정직한 주장이 사유 없이 거부되므로 **단일 출처**(lib/analyzers)를 쓴다.
|
|
13107
|
+
// 해소된 사각지대: 다단계 경로의 bare 파일(ops/containers/Dockerfile) · 선두 점 디렉토리(.github/workflows/ci.yml
|
|
13108
|
+
// 가 github/… 로 잘리던 것) · 점파일(.gitignore) · Dockerfile.dev 가 Dockerfile 로 잘려 "없는 파일"이 되던 것.
|
|
13109
|
+
void FILE_EXTS; // (구 상수 — 위 단일 출처로 대체. 주변 주석의 이력 보존용)
|
|
13110
|
+
const FILE_RE = require('../lib/analyzers')._EVIDENCE_FILE_RE('g');
|
|
12975
13111
|
// 1.36.11 (클린룸 P1-B): 매치 직전 문자가 경로 구분자면 더 긴 경로의 "꼬리"만 잡은 것(UNC \\\\server\\..., 드라이브 X:\\...,
|
|
12976
13112
|
// 숫자 시작 선두 세그먼트 등) — 잘린 꼬리를 루트-상대 경로로 검증하면 무관한 로컬 파일이 통과(우회성). 그런 매치는 버린다.
|
|
12977
13113
|
const filePatterns = [];
|
|
@@ -13135,14 +13271,32 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
13135
13271
|
// 1.9.309: 기본 임계는 비례적 — 근거 일부(파일·테스트·로그 중 하나)만 있으면 통과(Opus 의 "증거 0" 만 차단).
|
|
13136
13272
|
// --require-evidence(명시) 는 엄격(파일+테스트 모두, 1.9.287). 과탐 방지.
|
|
13137
13273
|
const evidenceQualityOk = !mustHaveEvidence || (has('--require-evidence') ? evq.ok : (evq.hasFile || evq.hasTest || evq.hasLog));
|
|
13138
|
-
//
|
|
13274
|
+
// filesAllExist 는 "주장 파일들이 실제 존재하는가" — 주장 파일이 0건이면 공허참(빈 배열의 every)이다.
|
|
13139
13275
|
const filesAllExist = fileChecks.every(c => c.exists);
|
|
13276
|
+
// 1.36.82 (공허한 가드 스윕 High #1, 실측 재현): **아무것도 대조하지 못한 주장을 통과로 보고하지 않는다.**
|
|
13277
|
+
// 이전 주석은 "공허참은 evidenceQualityOk 가 차단한다"고 적혀 있었으나 전제가 거짓이었다 —
|
|
13278
|
+
// 기본 모드의 evidenceQualityOk 는 hasFile||hasTest||hasLog 라 evidence 에 "tests: 32/32 passed" 문자열
|
|
13279
|
+
// 하나만 있어도 통과한다. 그 결과 파일 0건·테스트 0개 프로젝트에서 done 을 선언하면
|
|
13280
|
+
// filesAllExist(빈배열)=true · implementationSubstance(검사대상 0)=true · gitApplicable(files>0 요구)=false 로
|
|
13281
|
+
// **전 판정이 공허참**이 되어 ok:true/exit 0 이 나왔다.
|
|
13282
|
+
// 단조성이 뒤집혀 있었다: 파일명을 적어 검증 가능하게 쓸수록 FAIL, 아무것도 안 적고 날조할수록 PASS.
|
|
13283
|
+
// 탈출구는 두 가지뿐 — evidence 에 수정 파일 경로를 적거나, --run-tests 로 실제 실행 결과를 붙인다.
|
|
13284
|
+
// 기존 opt-out(--lenient)이 그대로 적용되므로 의도된 예외는 사용자가 되돌릴 수 있다.
|
|
13285
|
+
// (같은 저장소의 `contract verify` 가 이미 빈 계약을 empty_contract 로 거부하는 것과 대칭.)
|
|
13286
|
+
// 계산은 claimsChecked 선언 이후에 둔다(아래) — 여기서 참조하면 TDZ 다.
|
|
13140
13287
|
|
|
13141
13288
|
// 1.11.2 (14th 버그헌트 P1-CRITICAL, UR-0175): 허위완료 차단을 "기본값"에서 작동 — done 주장은 optimism(evidence 주장↔코드 호출 흔적) + git 교차검증을 기본 게이팅.
|
|
13142
13289
|
// 이전엔 --strict-claims 옵트인일 때만 잡아 기본 모드(verify-claim --run-tests)가 허위완료를 통과(exit 0)시켰고, --json 은 git 신호조차 누락(머신 경로가 더 위험)했음.
|
|
13143
13290
|
// 범위: done/완료 주장(또는 --require-evidence/--strict-claims). opt-out: --lenient. --json/human 양 경로가 동일 계산을 공유.
|
|
13144
13291
|
const lenient = has('--lenient');
|
|
13145
13292
|
const claimsChecked = !lenient && (isDoneClaim || has('--require-evidence') || has('--strict-claims'));
|
|
13293
|
+
// 1.36.82 (High #1): "아무것도 대조하지 못함" — 위 주석 참조. 세 경로(collect/--json/human)가 이 하나를 공유한다.
|
|
13294
|
+
// (검수 High#2-d, 실측 재현) 처음엔 `runResult.parsed` 를 요구했는데 그건 **같은 역전을 다시 만든 것**이었다:
|
|
13295
|
+
// `--test-cmd 'node -e "process.exit(0)"'` 처럼 실제로 실행해 exit 0 을 받았어도 출력이 파싱 불가면 차단되고,
|
|
13296
|
+
// 반대로 evidence 에 "1/1 passed" 문자열만 적은 날조는 통과했다. 커스텀 리포터·go test 등 정직한 실행이 벌을 받는다.
|
|
13297
|
+
// 판정 기준은 **실행 확증**이어야 한다 — 실제로 돌았고 실패하지 않았는가(allPassed = exit 0 이며 파싱됐다면 전부 통과).
|
|
13298
|
+
const _runProven = !!(runResult && !runResult.skipped && runResult.allPassed);
|
|
13299
|
+
const unverifiableClaim = claimsChecked && files.length === 0 && !_runProven;
|
|
13146
13300
|
let optimismSuspects = [], honestyFindings = [], strictOk = true;
|
|
13147
13301
|
if (claimsChecked) {
|
|
13148
13302
|
const codeText = _scanCodeForPatterns(root);
|
|
@@ -13170,6 +13324,7 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
13170
13324
|
const _tcMatch = declaredTestCount == null ? null : (!testMeasured ? null : actualTestCount >= declaredTestCount);
|
|
13171
13325
|
const _runFail = !!(runResult && !runResult.skipped && !runResult.allPassed);
|
|
13172
13326
|
const reasons = [];
|
|
13327
|
+
if (unverifiableClaim) reasons.push('unverifiable-claim'); // 1.36.82 (High #1): 대조된 것이 하나도 없음
|
|
13173
13328
|
if (!filesAllExist) reasons.push('files-missing');
|
|
13174
13329
|
if (_tcMatch === false) reasons.push('test-count-short');
|
|
13175
13330
|
if (!evidenceQualityOk) reasons.push('evidence-incomplete');
|
|
@@ -13218,6 +13373,7 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
13218
13373
|
// 1.35.7 (UR-0013 GPT5.5평가 개선②): top-level ok/reasons — CI 가 verdict 필드를 재조합하지 않아도 되도록 collect 경로와 동일 어휘.
|
|
13219
13374
|
// exit 는 reasons 기반 단일 판정(ok↔exit 불일치 원천 차단) — 기존 게이트(1.11.2/1.17.3/1.17.4) 동일 + declared-pass-mismatch 추가.
|
|
13220
13375
|
out.reasons = [];
|
|
13376
|
+
if (unverifiableClaim) out.reasons.push('unverifiable-claim'); // 1.36.82 (High #1)
|
|
13221
13377
|
if (!filesAllExist) out.reasons.push('files-missing');
|
|
13222
13378
|
if (out.verdict.testCountMatch === false) out.reasons.push('test-count-short');
|
|
13223
13379
|
if (!evidenceQualityOk) out.reasons.push('evidence-incomplete');
|
|
@@ -13315,7 +13471,10 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
13315
13471
|
}
|
|
13316
13472
|
// 1.9.309 (UR-0048): done 주장 evidence 완전성 — 기본 강제(상단 pre-compute). --lenient 로 opt-out.
|
|
13317
13473
|
if (mustHaveEvidence) {
|
|
13318
|
-
|
|
13474
|
+
// 1.36.82 (스윕 medium): 기본 모드의 통과 조건은 hasFile||hasTest||hasLog 인데 라벨은 늘 "파일+테스트 근거 있음"이라
|
|
13475
|
+
// hasFile:false 인 같은 실행의 JSON 과 정면 모순하는 **거짓 표기**였다. 실제로 확인된 근거만 나열한다.
|
|
13476
|
+
const _evParts = [evq.hasFile && t('파일 경로', 'file path'), evq.hasTest && t('테스트', 'test'), evq.hasLog && t('실행 로그', 'run log')].filter(Boolean);
|
|
13477
|
+
log(` - ${t('evidence 완전성 (done 기본 강제)', 'evidence completeness (done default)')}: ${evidenceQualityOk ? `✓ pass (${_evParts.join(' + ') || t('근거 없음', 'none')})` : `✗ FAIL ${t(`(누락: ${evq.missing.join(', ')})`, `(missing: ${evq.missing.join(', ')})`)}`}`);
|
|
13319
13478
|
if (!evidenceQualityOk) log(` · ${t('done 주장은 수정 파일 경로 + 테스트명/개수 가 evidence 에 있어야 함 (테스트 통과만으로는 불충분). 완화: --lenient', 'a done claim needs changed-file paths + test name/count in evidence (passing tests alone is insufficient). relax: --lenient')}`);
|
|
13320
13479
|
}
|
|
13321
13480
|
// 1.17.3 (UR-0046): 구현 실체(스텁) + 테스트-구현 연결 — Attack C(주석뿐 구현+assert(true)) 차단.
|
|
@@ -13329,7 +13488,12 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
13329
13488
|
} else if (testLinkOk === true && claimsChecked) {
|
|
13330
13489
|
log(` - ${t('테스트-구현 연결', 'test-impl link')}: ${t('✓ pass (테스트가 구현을 참조)', '✓ pass (test references the impl)')}`);
|
|
13331
13490
|
}
|
|
13332
|
-
|
|
13491
|
+
// 1.36.82 (High #1): 대조된 것이 하나도 없으면 "일치"가 아니라 **검증 불가**다 — 통과로 보고하지 않는다.
|
|
13492
|
+
if (unverifiableClaim) {
|
|
13493
|
+
log(` - ${t('검증 가능성', 'verifiability')}: ${t('✗ FAIL — 이 주장에서 실제로 대조된 것이 없습니다(주장 파일 0건 · 실행 확증 없음)', '✗ FAIL — nothing in this claim was actually checked (0 claimed files · no run evidence)')}`);
|
|
13494
|
+
log(` · ${t('evidence 에 수정 파일 경로를 적거나 --run-tests 로 실제 실행 결과를 붙이세요. 의도한 예외면 --lenient.', 'add changed-file paths to evidence, or attach a real run with --run-tests. If intended, use --lenient.')}`);
|
|
13495
|
+
}
|
|
13496
|
+
const overallFail = unverifiableClaim || !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 부풀림 게이팅 · 1.36.82 (High #1): unverifiableClaim
|
|
13333
13497
|
// 1.9.287: 정직한 한계 고지 — 테스트 통과 ≠ 의미적 구현 정확성
|
|
13334
13498
|
if (claimsChecked || mustHaveEvidence) {
|
|
13335
13499
|
log('');
|
|
@@ -15349,7 +15513,9 @@ function gate(root) {
|
|
|
15349
15513
|
// 1.34.1 (16th리뷰 정직화): 기본 5체크(특히 lazy detect)는 워크스페이스-상태(handoff/test-run/evidence 부재) 신호로 거짓완료를 잡지, 콘텐츠(파일/카운트/스텁)는 검사하지 않음.
|
|
15350
15514
|
// --claims 는 콘텐츠-레벨 검증을 추가 — 워크스페이스가 깨끗한 성숙 프로젝트에선 기본 5체크가 통과(exit 0)해도 --claims 만 콘텐츠 거짓을 잡아(exit 1) README 약속("claims fail → cannot merge")을 문자 그대로 강제. (실증 가드: e2e B(1.34.1))
|
|
15351
15515
|
const withClaims = has('--claims');
|
|
15352
|
-
|
|
15516
|
+
const _ref1 = _argAllStrict('--require-referee'); // 1.36.82 (High #2): 안내만 하고 구현이 없던 플래그 — 실제 게이팅
|
|
15517
|
+
const _refIds = _ref1.values, _refMissing = _ref1.missing;
|
|
15518
|
+
if (!jsonMode) log(`# leerness gate (${(withClaims ? 6 : 5) + _refIds.length + _refMissing} checks)`);
|
|
15353
15519
|
function step(label, fn) {
|
|
15354
15520
|
const code0 = process.exitCode || 0;
|
|
15355
15521
|
if (!jsonMode) log(`\n## ${label}`);
|
|
@@ -15360,9 +15526,13 @@ function gate(root) {
|
|
|
15360
15526
|
const failed = threw != null || !!(process.exitCode && process.exitCode !== code0);
|
|
15361
15527
|
if (threw && !jsonMode) fail(`${label} threw: ${threw.message}`);
|
|
15362
15528
|
if (failed) bad++;
|
|
15363
|
-
|
|
15529
|
+
// 1.36.82 (검수 Medium): --json 에서 모든 실패가 {name, ok:false} 로만 축약돼
|
|
15530
|
+
// referee_not_found / referee_stale / timeout / spawn 오류가 구분되지 않았다 — 단계가 남긴 진단을 그대로 싣는다.
|
|
15531
|
+
checks.push({ name: label, ok: !failed, ...(threw ? { error: threw.message } : {}), ..._stepDetail });
|
|
15532
|
+
_stepDetail = {};
|
|
15364
15533
|
process.exitCode = 0;
|
|
15365
15534
|
}
|
|
15535
|
+
let _stepDetail = {}; // step 내부에서 채우면 그 단계의 checks[] 항목에 병합된다
|
|
15366
15536
|
step('verify', () => verify(root));
|
|
15367
15537
|
step('audit', () => audit(root));
|
|
15368
15538
|
step('scan secrets', () => scanSecrets(root));
|
|
@@ -15370,6 +15540,38 @@ function gate(root) {
|
|
|
15370
15540
|
step('lazy detect', () => lazyDetect(root));
|
|
15371
15541
|
// 1.33.3: opt-in 정밀 per-claim 검증. 코어(_verifyClaimsAll)는 exit 안 하고 결과만 반환 → step 의 exitCode 감지로 실패 집계. 비-json 모드는 불일치 task 를 fail() 로 표면화.
|
|
15372
15542
|
if (withClaims) step('verify-claims', () => { const r = _verifyClaimsAll(root); if (!r.ok) { process.exitCode = 1; if (!jsonMode) r.results.filter(x => x && !x.ok).forEach(x => fail(`${x.id} 불일치: ${x.reasons.join(', ')}`)); } });
|
|
15543
|
+
// 1.36.82 (공허한 가드 스윕 High #2, 실측 확인): `referee verify` 성공 시 툴이 사용자에게
|
|
15544
|
+
// "이제 gate --require-referee <id> 가 이 검증기를 신뢰합니다" 라고 **안내하는데 gate 는 이 플래그를 읽지도 않았다**.
|
|
15545
|
+
// 사용자는 안내받은 명령을 CI 에 넣고 검증기가 머지를 막는다고 믿지만 아무 일도 일어나지 않았고,
|
|
15546
|
+
// 무효/부재 referee 여도 exit 0 이라 오배선을 알아챌 신호조차 없었다 — 안내가 곧 거짓 약속이었다.
|
|
15547
|
+
// 이제 실제로 게이팅한다: 미검증/무효/부재 검증기는 실행 전에 거부하고, 검증기가 현재 상태를 거부하면 게이트가 실패한다.
|
|
15548
|
+
// (검수 High#4) 값 누락은 조용히 버리지 않는다 — CI 의 빈 환경변수가 게이팅을 무력화하는 fail-open 을 막는다.
|
|
15549
|
+
for (let i = 0; i < _refMissing; i++) {
|
|
15550
|
+
step('referee:(값 없음)', () => {
|
|
15551
|
+
process.exitCode = 1;
|
|
15552
|
+
_stepDetail = { code: 'referee_missing_value' };
|
|
15553
|
+
if (!jsonMode) fail('--require-referee 에 검증기 id 가 없습니다 (예: --require-referee build-check). 값이 비면 게이팅이 사라지므로 실패로 처리합니다.');
|
|
15554
|
+
});
|
|
15555
|
+
}
|
|
15556
|
+
// (검수 Low) 같은 id 반복은 같은 명령을 중복 실행할 뿐 — 순서를 보존해 중복만 제거한다.
|
|
15557
|
+
for (const _rid of Array.from(new Set(_refIds))) {
|
|
15558
|
+
step(`referee:${_rid}`, () => {
|
|
15559
|
+
const g = _ref.requireReferee(root, _rid);
|
|
15560
|
+
if (!g.ok) { process.exitCode = 1; _stepDetail = { code: g.code }; if (!jsonMode) fail(`${g.code}: ${g.message}`); return; }
|
|
15561
|
+
// (검수 Medium) 캘리브레이션과 동일한 시간 정책 — referee verify --timeout 으로 늘린 검증기를
|
|
15562
|
+
// gate 에서만 10분에 끊으면 정직한 검증기가 거짓 차단된다. 저장된 값이 있으면 그것을 쓴다.
|
|
15563
|
+
const _to = Math.max(1000, Number(g.referee.timeoutMs) || 600000);
|
|
15564
|
+
const rr = cp.spawnSync(g.referee.check, { cwd: root, shell: true, encoding: 'utf8', timeout: _to, maxBuffer: 8 * 1024 * 1024 });
|
|
15565
|
+
const code = rr.status == null ? 1 : rr.status;
|
|
15566
|
+
const timedOut = !!(rr.error && /ETIMEDOUT|timed? ?out/i.test(String(rr.error.message || rr.error.code || '')));
|
|
15567
|
+
if (code !== 0) {
|
|
15568
|
+
process.exitCode = 1;
|
|
15569
|
+
const why = rr.error ? ` (${rr.error.code || rr.error.message})` : '';
|
|
15570
|
+
_stepDetail = { code: timedOut ? 'referee_timeout' : 'referee_failed', exit: code, ...(timedOut ? { timedOut: true } : {}), ...(rr.error ? { spawnError: String(rr.error.code || rr.error.message) } : {}), ...(rr.signal ? { signal: rr.signal } : {}) };
|
|
15571
|
+
if (!jsonMode) fail(`referee ${_rid} 가 현재 상태를 거부(exit ${code})${why}: ${g.referee.check}`);
|
|
15572
|
+
}
|
|
15573
|
+
});
|
|
15574
|
+
}
|
|
15373
15575
|
if (jsonMode) { log(JSON.stringify({ version: VERSION, root, ok: bad === 0, total: checks.length, failed: bad, checks }, null, 2)); if (bad) process.exitCode = 1; return; }
|
|
15374
15576
|
log(`\n# gate summary: ${bad} 단계 실패`);
|
|
15375
15577
|
if (bad) process.exitCode = 1;
|
package/docs/interoperability.md
CHANGED
|
@@ -7,7 +7,7 @@ leerness 는 **에이전트도 에디터도 아닌, 에이전트 위의 운영
|
|
|
7
7
|
| 표면 | 형태 | 용도 |
|
|
8
8
|
|---|---|---|
|
|
9
9
|
| **CLI** | 명령줄 | 모든 에이전트/사람의 기본 진입점 |
|
|
10
|
-
| **MCP** (`leerness mcp serve`) | stdio JSON-RPC
|
|
10
|
+
| **MCP** (`leerness mcp serve`) | stdio JSON-RPC (도구 수는 `leerness doctor` 가 실측 출력) | 외부 AI 가 자연어→도구 호출로 직접 사용. `adapter <tool>` 이 `.mcp.json` 자동 병합 |
|
|
11
11
|
| **`.leerness/` state substrate** | 파일(JSON) | 에이전트 간 인수인계 표준 — Claude Code 가 한 작업을 Codex/Goose 가 이어받음. 오프라인·0-deps |
|
|
12
12
|
| **지침 계층** | AGENTS.md / CLAUDE.md / 전역 조건부(`adapter codex --global`) | 문서를 읽는 에이전트 대상 협조 계층 |
|
|
13
13
|
| **강제 계층** | `enforce`(pre-commit) + `ci init`(gate) | **프로토콜 무관** — 어떤 방식으로 붙은 에이전트든 커밋/PR 관문에서 검증 |
|
|
@@ -17,7 +17,7 @@ leerness 는 **에이전트도 에디터도 아닌, 에이전트 위의 운영
|
|
|
17
17
|
**입장: 직접 구현하지 않는다 (고려 후 결정, 2026-07).**
|
|
18
18
|
|
|
19
19
|
- ACP 의 토폴로지는 에디터(클라이언트) ↔ 코딩 에이전트(서버)다. leerness 는 그 어느 쪽도 아니므로 ACP 서버/클라이언트를 구현할 구조적 자리가 없다.
|
|
20
|
-
- **간접 호환은 이미 성립**: ACP 로 에디터에 붙는 에이전트(Claude Code ACP 어댑터, Gemini CLI 등)는 도구 계층으로 MCP 를 사용한다 → 프로젝트의 `.mcp.json`(leerness adapter 가 병합)을 통해 leerness
|
|
20
|
+
- **간접 호환은 이미 성립**: ACP 로 에디터에 붙는 에이전트(Claude Code ACP 어댑터, Gemini CLI 등)는 도구 계층으로 MCP 를 사용한다 → 프로젝트의 `.mcp.json`(leerness adapter 가 병합)을 통해 leerness 의 MCP 도구 전체에 그대로 접근한다.
|
|
21
21
|
- **강제는 이미 커버**: pre-commit enforce 는 접속 프로토콜과 무관하게 작동한다(커밋이 보편 관문).
|
|
22
22
|
- 재검토 트리거: ACP 가 에이전트-측 도구 계층으로 MCP 외 자체 도구 규격을 강제하게 되는 경우.
|
|
23
23
|
|
package/lib/analyzers.js
CHANGED
|
@@ -34,10 +34,30 @@ function _shellGuardAnalyze(cmd, ctx) {
|
|
|
34
34
|
}
|
|
35
35
|
return { shell, psVersion: psVer, issues };
|
|
36
36
|
}
|
|
37
|
+
// 1.36.82 (검수 High#2): evidence 에서 파일 경로를 알아보는 **단일 출처**.
|
|
38
|
+
// 이전엔 bin 의 FILE_RE 와 여기 hasFile 정규식이 각자 따로 있어(주석만 "정합") 실제로는 어긋났고,
|
|
39
|
+
// "대조 0건이면 FAIL" 을 도입한 뒤로는 추출기 사각지대가 곧 정직한 주장의 거짓 차단이 됐다.
|
|
40
|
+
// 실측으로 확인된 사각지대: 다단계 경로의 확장자 없는 파일(ops/containers/Dockerfile),
|
|
41
|
+
// 선두 점 디렉토리(.github/workflows/ci.yml → github/… 로 잘림), 점파일(.gitignore),
|
|
42
|
+
// 그리고 Dockerfile.dev 가 Dockerfile 로 잘려 "없는 파일" 이 되던 것.
|
|
43
|
+
const _VC_FILE_EXTS = 'webmanifest|properties|tscn|tres|godot|json5|prisma|swift|java|jsx|tsx|yaml|html|scss|sass|less|gltf|conf|json|toml|lock|mdx|xml|css|svg|yml|cfg|ini|env|php|sql|mjs|cjs|vue|svelte|md|js|ts|gd|cs|py|rb|go|rs|kt|sh|cpp|c|h';
|
|
44
|
+
// 확장자 없는 표준 파일명 — 임의의 대문자 단어가 아니라 정확한 철자만(대소문자 구분)으로 FP 억제.
|
|
45
|
+
const _VC_BARE_FILES = 'Dockerfile|Containerfile|Makefile|Gemfile|Procfile|Rakefile|Jenkinsfile|Vagrantfile|Caddyfile|Brewfile|Justfile|CODEOWNERS|LICENCE|LICENSE|NOTICE';
|
|
46
|
+
// 점파일 — 확장자 규칙으로는 잡히지 않지만 명백한 파일 참조.
|
|
47
|
+
const _VC_DOTFILES = '\\.(?:gitignore|gitattributes|gitmodules|dockerignore|npmrc|npmignore|nvmrc|editorconfig|prettierrc|eslintrc|babelrc|browserslistrc)';
|
|
48
|
+
// 경로 접두: 0개 이상 세그먼트(각 세그먼트는 선두 점 허용) — `ops/containers/`, `.github/workflows/` 모두 포함.
|
|
49
|
+
const _VC_PREFIX = '(?:\\.?[A-Za-z0-9][A-Za-z0-9._-]*[\\/\\\\])*';
|
|
50
|
+
// basename: (a) 확장자 파일 (b) 표준 bare 파일명(뒤에 .확장자가 오면 (a)에 양보) (c) 점파일
|
|
51
|
+
// bare 파일은 관례적 접미사를 허용한다(Dockerfile.dev / Makefile.local) — 없으면 그 주장이 "파일 0건"이 된다.
|
|
52
|
+
const _VC_BASE = `(?:[A-Za-z0-9][\\w.-]*\\.(?:${_VC_FILE_EXTS})\\b|(?:${_VC_BARE_FILES})(?:\\.[A-Za-z0-9_-]+)?(?![\\w.-])|${_VC_DOTFILES}(?![\\w-]))`;
|
|
53
|
+
function _EVIDENCE_FILE_RE(flags = '') { return new RegExp(`${_VC_PREFIX}${_VC_BASE}`, flags); }
|
|
54
|
+
|
|
37
55
|
function _evidenceQuality(evidence) {
|
|
38
56
|
const e = String(evidence || '');
|
|
39
57
|
// codex 버그헌트 P2: basename 첫 글자에 숫자 허용 ('123.js' 등 숫자시작 실파일) — bin FILE_RE 와 정합.
|
|
40
|
-
|
|
58
|
+
// 1.36.82: "파일을 언급했는가"의 정의가 두 곳(bin 추출기 · 여기)으로 갈리면 정직한 주장이 사유 없이 거부된다.
|
|
59
|
+
// 판정을 단일 출처(_EVIDENCE_FILE_RE)로 통일한다.
|
|
60
|
+
const hasFile = _EVIDENCE_FILE_RE().test(e);
|
|
41
61
|
const hasTest = /(\d+)\s*(?:\/\s*\d+\s*)?(?:통과|passed|passing|개\s*테스트)|\btests?\b\s*[:=]?\s*\d|Tests?:\s*\d|\b\d+\s*tests?\b/i.test(e);
|
|
42
62
|
const hasLog = /Exit\s*[:=]|exit\s*code|Command\s*[:=]|npm\s+(?:test|run)|pytest|cargo\s+test|go\s+test/i.test(e);
|
|
43
63
|
const missing = [];
|
|
@@ -88,4 +108,4 @@ function _epistemicHonestyCheck(text) {
|
|
|
88
108
|
return { ok: findings.length === 0, findings, dimensions: ['pretend-knowledge', 'premature-judgment', 'no-info-gathering'] };
|
|
89
109
|
}
|
|
90
110
|
|
|
91
|
-
module.exports = { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInGit, _epistemicHonestyCheck };
|
|
111
|
+
module.exports = { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInGit, _epistemicHonestyCheck, _EVIDENCE_FILE_RE };
|
package/lib/referee.js
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.82",
|
|
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
|
@@ -6569,6 +6569,118 @@ total++;
|
|
|
6569
6569
|
if (!ok) failed++;
|
|
6570
6570
|
}
|
|
6571
6571
|
|
|
6572
|
+
// 1.36.82 회귀 (공허한 가드 스윕 High #1): **아무것도 대조하지 못한 done 주장**을 통과로 보고하지 않는다.
|
|
6573
|
+
// 사고: evidence 에 파일 경로가 없으면 fileChecks=[] → filesAllExist(빈배열 every)=true,
|
|
6574
|
+
// stubFiles=[] → implementationSubstance=true, gitApplicable 은 files>0 요구 → skip.
|
|
6575
|
+
// 전 판정이 공허참이라 "코드도 테스트도 없이 done" 이 ok:true/exit 0 이었고, 단조성이 뒤집혀 있었다
|
|
6576
|
+
// (파일명을 적어 검증 가능하게 쓸수록 FAIL, 날조할수록 PASS). 판별 케이스로 고정한다.
|
|
6577
|
+
total++;
|
|
6578
|
+
{
|
|
6579
|
+
let ok = false;
|
|
6580
|
+
try {
|
|
6581
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-vac-'));
|
|
6582
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { encoding: 'utf8', timeout: 60000 });
|
|
6583
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { encoding: 'utf8', timeout: 60000 });
|
|
6584
|
+
const J = (r) => { try { return JSON.parse(r.stdout); } catch { return null; } };
|
|
6585
|
+
const mkDone = (req, ev) => {
|
|
6586
|
+
const id = (R(['task', 'add', req]).stdout.match(/T-\d{4,}/) || [])[0];
|
|
6587
|
+
R(['task', 'update', id, '--status', 'done', '--evidence', ev, '--next', 'none']);
|
|
6588
|
+
return id;
|
|
6589
|
+
};
|
|
6590
|
+
// (1) 날조: 소스도 테스트도 없이 done + 테스트 모양 문자열만 → 차단돼야
|
|
6591
|
+
const fake = mkDone('결제 재시도 구현', 'tests: 32/32 passed');
|
|
6592
|
+
const rFake = R(['verify-claim', fake, '--json']);
|
|
6593
|
+
const jFake = J(rFake) || {};
|
|
6594
|
+
const blocked = rFake.status === 1 && (jFake.reasons || []).includes('unverifiable-claim');
|
|
6595
|
+
// (2) --lenient opt-out 은 존중돼야 (기존 계약 유지)
|
|
6596
|
+
const lenientOk = R(['verify-claim', fake, '--lenient', '--json']).status === 0;
|
|
6597
|
+
// (3) 정직한 주장은 통과해야 — false-BLOCK 0 (이 게이트의 최악 실패모드)
|
|
6598
|
+
fs.mkdirSync(path.join(d, 'src'), { recursive: true });
|
|
6599
|
+
fs.writeFileSync(path.join(d, 'src', 'pay.js'), 'function retry(fn,n){let e;for(let i=0;i<n;i++){try{return fn()}catch(x){e=x}}throw e}\nmodule.exports={retry}\n');
|
|
6600
|
+
fs.mkdirSync(path.join(d, 'tests2'), { recursive: true });
|
|
6601
|
+
fs.writeFileSync(path.join(d, 'tests2', 'pay.test.js'), 'const {retry}=require("../src/pay.js");\ntest("r",()=>{if(retry(()=>1,2)!==1)throw 0});\n');
|
|
6602
|
+
const honest = mkDone('재시도 헬퍼 추가', 'src/pay.js 구현 + tests2/pay.test.js 1개 테스트 (1/1 통과)');
|
|
6603
|
+
const honestOk = R(['verify-claim', honest, '--json']).status === 0;
|
|
6604
|
+
// (4) 추출기 사각지대가 정직한 주장의 거짓 차단이 되지 않도록 — 검수(High#2)가 짚은 4형태를 모두 고정
|
|
6605
|
+
fs.writeFileSync(path.join(d, 'Dockerfile'), 'FROM node:18\n');
|
|
6606
|
+
fs.writeFileSync(path.join(d, 'Dockerfile.dev'), 'FROM node:18\n');
|
|
6607
|
+
fs.writeFileSync(path.join(d, '.gitignore'), 'node_modules\n');
|
|
6608
|
+
fs.mkdirSync(path.join(d, 'ops', 'containers'), { recursive: true });
|
|
6609
|
+
fs.writeFileSync(path.join(d, 'ops', 'containers', 'Dockerfile'), 'FROM node:18\n');
|
|
6610
|
+
fs.mkdirSync(path.join(d, '.github', 'workflows'), { recursive: true });
|
|
6611
|
+
fs.writeFileSync(path.join(d, '.github', 'workflows', 'ci.yml'), 'name: ci\n');
|
|
6612
|
+
const bareCases = [
|
|
6613
|
+
['도커 베이스 갱신', 'Dockerfile 베이스 이미지 변경 (1/1 통과)'],
|
|
6614
|
+
['도커 dev 갱신', 'Dockerfile.dev 수정 (1/1 통과)'],
|
|
6615
|
+
['무시목록 갱신', '.gitignore 갱신 (1/1 통과)'],
|
|
6616
|
+
['다단계 경로', 'ops/containers/Dockerfile 갱신 (1/1 통과)'],
|
|
6617
|
+
['선두 점 디렉토리', '.github/workflows/ci.yml 수정 (1/1 통과)'],
|
|
6618
|
+
];
|
|
6619
|
+
const bareOk = bareCases.every(([req, ev]) => R(['verify-claim', mkDone(req, ev), '--json']).status === 0);
|
|
6620
|
+
// (4b) 실제 실행 확증(exit 0)은 출력이 파싱 불가여도 면제돼야 — 커스텀 리포터를 벌하지 않는다
|
|
6621
|
+
const runId = mkDone('실행 확증', 'tests: 1/1 passed');
|
|
6622
|
+
const runOk = cp.spawnSync(process.execPath, [CLI, 'verify-claim', runId, '--path', d, '--run-tests', '--test-cmd', 'node -e "process.exit(0)"', '--json'], { encoding: 'utf8', timeout: 90000 }).status === 0;
|
|
6623
|
+
// (5) 일괄/게이트 경로도 동일 판정 공유
|
|
6624
|
+
const all = J(R(['verify-claim', '--all', '--json'])) || {};
|
|
6625
|
+
const allCatches = (all.results || []).some(x => x && x.id === fake && x.ok === false);
|
|
6626
|
+
const gcj = J(R(['gate', '.', '--claims', '--json'])) || {};
|
|
6627
|
+
const vc = (gcj.checks || []).find(c => c.name === 'verify-claims');
|
|
6628
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
6629
|
+
ok = blocked && lenientOk && honestOk && bareOk && runOk && allCatches && !!vc && vc.ok === false;
|
|
6630
|
+
if (!ok) console.log(` [공허주장 디버그] blocked=${blocked} lenient=${lenientOk} honest=${honestOk} bare=${bareOk} run=${runOk} all=${allCatches} gate=${vc && vc.ok}`);
|
|
6631
|
+
} catch (e) {}
|
|
6632
|
+
console.log(ok ? '✓ B(1.36.82) 공허한 done 주장 차단: 대조 0건 → unverifiable-claim(exit 1) · --lenient 존중 · 정직한 주장/추출기 사각지대 5형태/실행확증 통과(false-BLOCK 0) · --all/gate 동일판정' : '✗ 공허한 done 주장 차단 가드 실패');
|
|
6633
|
+
if (!ok) failed++;
|
|
6634
|
+
}
|
|
6635
|
+
|
|
6636
|
+
// 1.36.82 회귀 (High #2): `gate --require-referee <id>` — 툴이 사용자에게 안내하면서 gate 는 읽지도 않아
|
|
6637
|
+
// 조용히 exit 0 이던 플래그. 이제 실제 게이팅한다(미존재/미증명/무효 거부 + 검증기가 현재 상태를 거부하면 실패).
|
|
6638
|
+
total++;
|
|
6639
|
+
{
|
|
6640
|
+
let ok = false;
|
|
6641
|
+
try {
|
|
6642
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-refgate-'));
|
|
6643
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { encoding: 'utf8', timeout: 60000 });
|
|
6644
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { encoding: 'utf8', timeout: 60000 });
|
|
6645
|
+
const J = (r) => { try { return JSON.parse(r.stdout); } catch { return null; } };
|
|
6646
|
+
const N = (js) => `node -e "${js}"`;
|
|
6647
|
+
const stepOf = (g, name) => ((g && g.checks) || []).find(c => c.name === name);
|
|
6648
|
+
// 안내 문구와 실제 지원이 일치하는가 — 이 불일치가 이번 사고의 본질이었다
|
|
6649
|
+
const advertised = fs.readFileSync(path.join(__dirname, '..', 'lib', 'referee.js'), 'utf8').includes('gate --require-referee');
|
|
6650
|
+
// (검수 High#3) **안내된 문법 그대로** — positional/--path 없이 cwd 에서. 이전 픽스처는 항상 '.' 과 --path 를
|
|
6651
|
+
// 같이 줘서 "값이 positional 경로로 흡수되던" 결함을 가렸다. 픽스처가 버그를 숨기지 않게 한다.
|
|
6652
|
+
const asAdvertised = cp.spawnSync(process.execPath, [CLI, 'gate', '--require-referee', 'nope', '--json'], { cwd: d, encoding: 'utf8', timeout: 60000 });
|
|
6653
|
+
let aj = null; try { aj = JSON.parse(asAdvertised.stdout); } catch {}
|
|
6654
|
+
const rootOk = !!aj && path.resolve(aj.root) === path.resolve(d); // root 가 <cwd>/nope 로 오인되면 실패
|
|
6655
|
+
// (검수 High#4) 값 누락은 조용히 사라지면 안 된다 — CI 의 빈 환경변수 fail-open 차단
|
|
6656
|
+
const bare = J(R(['gate', '.', '--require-referee', '--json']));
|
|
6657
|
+
const bareBlocked = !!bare && bare.ok === false && (bare.checks || []).some(c => c.code === 'referee_missing_value');
|
|
6658
|
+
const missing = J(R(['gate', '.', '--require-referee', 'nope', '--json']));
|
|
6659
|
+
const mStep = stepOf(missing, 'referee:nope');
|
|
6660
|
+
R(['referee', 'add', 'r1', '--check', N('process.exit(0)'), '--good', N('process.exit(0)'), '--bad', N("console.log('SIG'); process.exit(1)"), '--expect-bad', 'SIG']);
|
|
6661
|
+
const uncal = stepOf(J(R(['gate', '.', '--require-referee', 'r1', '--json'])), 'referee:r1');
|
|
6662
|
+
R(['referee', 'verify', 'r1']);
|
|
6663
|
+
const cal = stepOf(J(R(['gate', '.', '--require-referee', 'r1', '--json'])), 'referee:r1');
|
|
6664
|
+
R(['referee', 'add', 'r2', '--check', N('process.exit(7)'), '--good', N('process.exit(0)'), '--bad', N("console.log('SIG'); process.exit(1)"), '--expect-bad', 'SIG']);
|
|
6665
|
+
R(['referee', 'verify', 'r2']);
|
|
6666
|
+
const rejects = stepOf(J(R(['gate', '.', '--require-referee', 'r2', '--json'])), 'referee:r2');
|
|
6667
|
+
// 플래그 없으면 기존 5체크 무변경(회귀 0)
|
|
6668
|
+
const plain = J(R(['gate', '.', '--json']));
|
|
6669
|
+
const noRegression = plain && plain.total === 5 && !(plain.checks || []).some(c => /referee/.test(c.name));
|
|
6670
|
+
// 반복 지정
|
|
6671
|
+
const twice = J(R(['gate', '.', '--require-referee', 'r1', '--require-referee', 'r2', '--json']));
|
|
6672
|
+
const bothRan = ((twice && twice.checks) || []).filter(c => /^referee:/.test(c.name)).length === 2;
|
|
6673
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
6674
|
+
// 진단 코드가 --json 에 실려야 한다(모든 실패가 {name,ok:false} 로 축약되면 오배선을 구분할 수 없다)
|
|
6675
|
+
const codeExposed = !!mStep && mStep.code === 'referee_not_found';
|
|
6676
|
+
ok = advertised && rootOk && bareBlocked && codeExposed && !!mStep && mStep.ok === false && !!uncal && uncal.ok === false
|
|
6677
|
+
&& !!cal && cal.ok === true && !!rejects && rejects.ok === false && noRegression && bothRan;
|
|
6678
|
+
if (!ok) console.log(` [referee게이트 디버그] adv=${advertised} rootOk=${rootOk} bare=${bareBlocked} code=${codeExposed} missing=${mStep && mStep.ok} uncal=${uncal && uncal.ok} cal=${cal && cal.ok} reject=${rejects && rejects.ok} noReg=${noRegression} both=${bothRan}`);
|
|
6679
|
+
} catch (e) {}
|
|
6680
|
+
console.log(ok ? '✓ B(1.36.82) gate --require-referee 실제 게이팅: 안내문법 그대로 root 정상 · 미존재/미증명 거부(code 노출) · 값누락 차단 · 캘리브레이션 후 통과 · 검증기 거부 시 실패 · 반복 지정 · 플래그 없으면 5체크 무변경' : '✗ gate --require-referee 가드 실패');
|
|
6681
|
+
if (!ok) failed++;
|
|
6682
|
+
}
|
|
6683
|
+
|
|
6572
6684
|
// 1.35.10 (자체 gate 적대적 헌트): gate 를 "보안 가드레일"로 검증 — 8-프로브 헌트(FP 4 + FN 3 + by-design 1) 결과 제품 결함 0 확인 후, 유일 커버리지 갭(gate 레벨 보안 동작)을 회귀 가드로 고정. 커밋된 실키 → 차단(exit 1, scan secrets step), 정상 .env.example placeholder → 오탐 없이 통과(exit 0). scan-step 리팩터가 가드레일 보안을 조용히 무너뜨리는 회귀 차단.
|
|
6573
6685
|
total++;
|
|
6574
6686
|
{
|
|
@@ -7723,15 +7835,23 @@ total++;
|
|
|
7723
7835
|
const good = verify('okref');
|
|
7724
7836
|
const happyOk = good && good.ok === true && good.goodExit === 0 && good.badExit === 1 && good.expectMatched === true;
|
|
7725
7837
|
// FP 프로브 ①: known-good 이 이미 빨간 상태 → 거부(기준선 무효)
|
|
7726
|
-
|
|
7838
|
+
// 1.36.81 (codex 검수 ④, 실측 확인): 이전 두 프로브는 --expect-bad 를 빼고 add 해서 add 자체가 missing_args 로
|
|
7839
|
+
// 실패했고, 이어진 verify 는 not_found(ok:false)를 돌려줬다 — e2e 는 ok===false 만 보고 "거부됨"으로 통과시켰다.
|
|
7840
|
+
// 즉 "탐지력 없는 검증기를 거부한다"를 전혀 증명하지 못하는 공허한 단언이었다.
|
|
7841
|
+
// → add 성공을 먼저 단언하고, 거부 **사유**까지 확인한다.
|
|
7842
|
+
const a1 = add('redbase', N('process.exit(0)'), N('process.exit(3)'), N("console.log('SIG'); process.exit(1)"), 'SIG');
|
|
7727
7843
|
const p1 = verify('redbase');
|
|
7728
|
-
// FP 프로브 ②: known-bad 가 통과 → 탐지력 없음 거부
|
|
7729
|
-
add('nopower', N('process.exit(0)'), N('process.exit(0)'), N('process.exit(0)
|
|
7844
|
+
// FP 프로브 ②: known-bad 가 통과(exit 0) → 탐지력 없음 거부
|
|
7845
|
+
const a2 = add('nopower', N('process.exit(0)'), N('process.exit(0)'), N("console.log('SIG'); process.exit(0)"), 'SIG');
|
|
7730
7846
|
const p2 = verify('nopower');
|
|
7731
7847
|
// FP 프로브 ③: bad 가 무관한 이유로 실패 → 사유 불일치 거부
|
|
7732
|
-
add('wrongreason', N('process.exit(0)'), N('process.exit(0)'), N("console.log('Error: cannot find module'); process.exit(1)"), 'AssertionError');
|
|
7848
|
+
const a3 = add('wrongreason', N('process.exit(0)'), N('process.exit(0)'), N("console.log('Error: cannot find module'); process.exit(1)"), 'AssertionError');
|
|
7733
7849
|
const p3 = verify('wrongreason');
|
|
7734
|
-
const
|
|
7850
|
+
const _rz = (p) => (p && p.reasons || []).join(' | ');
|
|
7851
|
+
const probesOk = a1.status === 0 && a2.status === 0 && a3.status === 0
|
|
7852
|
+
&& p1 && p1.ok === false && /known-good/.test(_rz(p1)) // 기준선이 이미 빨감
|
|
7853
|
+
&& p2 && p2.ok === false && /exit 0|통과시킴/.test(_rz(p2)) // 심어둔 결함을 못 잡음
|
|
7854
|
+
&& p3 && p3.ok === false && p3.expectMatched === false && /사유 불일치|런처/.test(_rz(p3));
|
|
7735
7855
|
// 지문(stale): 캘리브레이션 후 명령이 바뀌면 신뢰 철회
|
|
7736
7856
|
const store = path.join(d, '.harness', 'referees.json');
|
|
7737
7857
|
const js = JSON.parse(fs.readFileSync(store, 'utf8'));
|
|
@@ -7747,8 +7867,31 @@ total++;
|
|
|
7747
7867
|
add('failing', N('process.exit(2)'), N('process.exit(0)'), N("console.log('AssertionError'); process.exit(1)"), 'AssertionError');
|
|
7748
7868
|
verify('failing');
|
|
7749
7869
|
const refFail = R(['verify-claim', 'T-0002', '--referee', 'failing', '--json']);
|
|
7870
|
+
// 1.36.81 (클린룸 실측): 세 상태는 서로 다른 코드로 보고돼야 한다 — 한 번도 검증 안 한 검증기를
|
|
7871
|
+
// stale("한때 유효했으나 무효화됨")로 보고하면 사용자가 취할 조치를 잘못 알려준다.
|
|
7872
|
+
add('nevercal', N('process.exit(0)'), N('process.exit(0)'), N("console.log('AssertionError'); process.exit(1)"), 'AssertionError');
|
|
7873
|
+
const neverUse = R(['verify-claim', 'T-0002', '--referee', 'nevercal', '--json']);
|
|
7874
|
+
const neverShown = J(R(['referee', 'show', 'nevercal', '--json']));
|
|
7875
|
+
const uncalOk = neverUse.status === 1 && (J(neverUse) || {}).code === 'referee_uncalibrated'
|
|
7876
|
+
&& neverShown && neverShown.stale === false && neverShown.calibrated === false;
|
|
7877
|
+
// 1.36.81 (codex 검수 ① High, 실측 확인): calibration.ok 가 truthy 비-boolean 이면 게이트가 통과했다 —
|
|
7878
|
+
// "미증명 검증기는 신뢰하지 않는다"는 이 기능의 전제를 정면으로 깨는 fail-open. 손상/조작 모두 차단돼야 한다.
|
|
7879
|
+
add('failopen', N('process.exit(0)'), N('process.exit(0)'), N("console.log('AssertionError'); process.exit(1)"), 'AssertionError');
|
|
7880
|
+
verify('failopen');
|
|
7881
|
+
const foStore = JSON.parse(fs.readFileSync(store, 'utf8'));
|
|
7882
|
+
foStore.find(x => x.id === 'failopen').calibration.ok = 'false'; // truthy 문자열
|
|
7883
|
+
fs.writeFileSync(store, JSON.stringify(foStore, null, 2));
|
|
7884
|
+
const foUse = R(['verify-claim', 'T-0002', '--referee', 'failopen', '--json']);
|
|
7885
|
+
const foShow = J(R(['referee', 'show', 'failopen', '--json']));
|
|
7886
|
+
const failOpenOk = foUse.status === 1 && (J(foUse) || {}).code !== undefined
|
|
7887
|
+
&& (J(foUse) || {}).code !== 'referee_failed' // 검증기 실행까지 갔다면 이미 신뢰한 것
|
|
7888
|
+
&& !(foShow && foShow.calibrated === true); // 사용자에게 "증명됨"으로 표시해서도 안 된다
|
|
7889
|
+
// 원복(이후 단언들이 이 스토어를 씀)
|
|
7890
|
+
foStore.find(x => x.id === 'failopen').calibration.ok = true;
|
|
7891
|
+
fs.writeFileSync(store, JSON.stringify(foStore, null, 2));
|
|
7750
7892
|
const gateOk = missing.status === 1 && (J(missing) || {}).code === 'referee_not_found'
|
|
7751
7893
|
&& staleUse.status === 1 && (J(staleUse) || {}).code === 'referee_stale'
|
|
7894
|
+
&& uncalOk && failOpenOk
|
|
7752
7895
|
&& refFail.status === 1 && (J(refFail) || {}).code === 'referee_failed';
|
|
7753
7896
|
// 스토어 규율: 손상 위 변경 거부 + --json 순수
|
|
7754
7897
|
const listPure = (() => { try { JSON.parse(R(['referee', 'list', '--json']).stdout); return true; } catch { return false; } })();
|
|
@@ -7758,7 +7901,7 @@ total++;
|
|
|
7758
7901
|
ok = happyOk && probesOk && staleOk && gateOk && storeOk;
|
|
7759
7902
|
if (!ok) console.log(` [referee 디버그] happy=${happyOk} probes=${probesOk} stale=${staleOk} gate=${gateOk} store=${storeOk}`);
|
|
7760
7903
|
} catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
|
|
7761
|
-
console.log(ok ? '✓ B(1.36.80) referee 캘리브레이션: 탐지력 증명(good통과+bad를 기대사유로 거부) · FP프로브 3종 거부 · 명령변경 stale · verify-claim 게이팅 · 스토어 규율' : '✗ referee 캘리브레이션 실패');
|
|
7904
|
+
console.log(ok ? '✓ B(1.36.80) referee 캘리브레이션: 탐지력 증명(good통과+bad를 기대사유로 거부) · FP프로브 3종 거부 · 명령변경 stale · verify-claim 게이팅(미존재/미검증/무효/검증기실패 4구분) · 스토어 규율' : '✗ referee 캘리브레이션 실패');
|
|
7762
7905
|
if (!ok) failed++;
|
|
7763
7906
|
}
|
|
7764
7907
|
|