leerness 1.36.81 → 1.36.83
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 +42 -0
- package/README.ko.md +1 -1
- package/README.md +4 -4
- package/bin/leerness.js +449 -41
- package/docs/interoperability.md +2 -2
- package/lib/analyzers.js +22 -2
- package/lib/referee.js +8 -2
- package/package.json +1 -1
- package/scripts/e2e.js +117 -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
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// lib/referee.js — 검증기 캘리브레이션 (1.36.80, P-0001 · 외부 사례 검토 채택).
|
|
2
2
|
// 문제: leerness 의 모든 검사(verify-claim/gate/contract)는 "정적·텍스트 대리 지표"였다 — 검증기 자신이
|
|
3
3
|
// 실패를 잡을 수 있는지는 아무도 증명하지 않았다. 초록 스위트가 실제로는 아무것도 검사하지 않는 경우를 못 걸렀다.
|
|
4
|
-
// 해법(anthropics/code-migration-kit 의 judge-setup 원리): 검증기를 신뢰하기 전에 **실행으로
|
|
4
|
+
// 해법(anthropics/code-migration-kit 의 judge-setup 원리): 검증기를 신뢰하기 전에 **실행으로 확인**한다 —
|
|
5
|
+
// (1.36.82 정직화) 확인하는 것은 "등록된 known-good/known-bad 쌍이 기대한 사유로 갈린다"는 것과 check 의 기동 가능성이다.
|
|
6
|
+
// good/bad 가 check 와 동일한 검증 절차를 서로 다른 상태에 대해 실행하는지는 **도구가 강제할 수 없다** — 사용자의 등록 책임이다.
|
|
5
7
|
// ① known-good 을 통과시키고 ② 일부러 망가뜨린 known-bad 를 거부하며 ③ 거부 사유가 심어둔 결함과 일치해야 하고
|
|
6
8
|
// ④ 명령/기대치가 바뀌면 캘리브레이션은 무효(stale)가 된다. 무관한 이유의 실패(환경 오류 등)는 탐지력이 아니다.
|
|
7
9
|
// 0-deps · 오프라인 · 기존 스토어 규율(손상 fail-closed · 형상 무효 거부 · --json 순수) 준수.
|
|
@@ -229,7 +231,11 @@ function refereeCmd(root, sub, rest, deps = {}) {
|
|
|
229
231
|
r.calibration = cal;
|
|
230
232
|
if (json) { log(JSON.stringify({ ok: cal.ok, id: r.id, calibrated: cal.ok, ...cal }, null, 2)); if (!cal.ok) process.exitCode = 1; return; }
|
|
231
233
|
if (cal.ok) {
|
|
232
|
-
|
|
234
|
+
// 1.36.82 (검수 High#5): "탐지력 증명됨" 은 실제보다 강한 주장이었다 — 캘리브레이션이 증명하는 것은
|
|
235
|
+
// **등록한 known-good/known-bad 쌍이 기대한 사유로 갈린다**는 것과 check 가 기동 가능하다는 것뿐이다.
|
|
236
|
+
// good/bad 가 check 와 같은 절차인지는 도구가 강제할 수 없다(사용자가 그렇게 등록해야 한다).
|
|
237
|
+
ok(`referee ${r.id} 캘리브레이션 통과 — known-good 통과 · known-bad 를 기대 사유로 거부 (good exit 0 · bad exit ${cal.badExit}${r.expectBad ? ' · 사유 일치' : ''})`);
|
|
238
|
+
log(` ⓘ 이 결과는 **등록한 good/bad 쌍**에 대한 것입니다 — good/bad 가 check 와 같은 검증 절차를 서로 다른 상태에 대해 실행할 때만 check 의 탐지력을 뜻합니다.`);
|
|
233
239
|
log(` → 이제 verify-claim --referee ${r.id} / gate --require-referee ${r.id} 가 이 검증기를 신뢰합니다.`);
|
|
234
240
|
log(` ⓘ 명령이나 기대 신호를 바꾸면 캘리브레이션은 자동 무효(stale) 가 됩니다.`);
|
|
235
241
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.83",
|
|
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
|
@@ -4144,7 +4144,11 @@ total++;
|
|
|
4144
4144
|
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4145
4145
|
// 1.9.334: catalogs import 블록 추출 후 이름 포함 확인(순서/추가 비의존 — 이후 import 추가 허용)
|
|
4146
4146
|
const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0];
|
|
4147
|
-
|
|
4147
|
+
// 1.36.83 (공허가드 스윕 후속): 이 정확 리터럴은 **bin 안의 selftest 가드 줄**에만 존재해 통과하고 있었다 —
|
|
4148
|
+
// 파일을 넘나든 자기참조(e2e 가 bin 을 읽는데, 그 문자열의 유일한 출처가 bin 의 다른 가드였다).
|
|
4149
|
+
// 1.36.83 이 그 가드를 불변식으로 바꾸자 리터럴이 사라져 이 단언의 공허함이 드러났다.
|
|
4150
|
+
// 호출부는 실제로 `_matchConstraints(_loadPlatformConstraints(root), text, lang)` 이므로 인자 추가에 견디는 불변식으로 바꾼다.
|
|
4151
|
+
const movedOut = !/const _DEFAULT_PLATFORM_CONSTRAINTS = \{/.test(harnessSrc) && /_matchConstraints\(_loadPlatformConstraints\(root\), text/.test(harnessSrc)
|
|
4148
4152
|
&& _catImp.includes('_DEFAULT_PLATFORM_CONSTRAINTS');
|
|
4149
4153
|
// 소비 명령 회귀: constraints check (review-request 도 _checkRequestConstraints 사용)
|
|
4150
4154
|
const cd = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-con-'));
|
|
@@ -6569,6 +6573,118 @@ total++;
|
|
|
6569
6573
|
if (!ok) failed++;
|
|
6570
6574
|
}
|
|
6571
6575
|
|
|
6576
|
+
// 1.36.82 회귀 (공허한 가드 스윕 High #1): **아무것도 대조하지 못한 done 주장**을 통과로 보고하지 않는다.
|
|
6577
|
+
// 사고: evidence 에 파일 경로가 없으면 fileChecks=[] → filesAllExist(빈배열 every)=true,
|
|
6578
|
+
// stubFiles=[] → implementationSubstance=true, gitApplicable 은 files>0 요구 → skip.
|
|
6579
|
+
// 전 판정이 공허참이라 "코드도 테스트도 없이 done" 이 ok:true/exit 0 이었고, 단조성이 뒤집혀 있었다
|
|
6580
|
+
// (파일명을 적어 검증 가능하게 쓸수록 FAIL, 날조할수록 PASS). 판별 케이스로 고정한다.
|
|
6581
|
+
total++;
|
|
6582
|
+
{
|
|
6583
|
+
let ok = false;
|
|
6584
|
+
try {
|
|
6585
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-vac-'));
|
|
6586
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { encoding: 'utf8', timeout: 60000 });
|
|
6587
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { encoding: 'utf8', timeout: 60000 });
|
|
6588
|
+
const J = (r) => { try { return JSON.parse(r.stdout); } catch { return null; } };
|
|
6589
|
+
const mkDone = (req, ev) => {
|
|
6590
|
+
const id = (R(['task', 'add', req]).stdout.match(/T-\d{4,}/) || [])[0];
|
|
6591
|
+
R(['task', 'update', id, '--status', 'done', '--evidence', ev, '--next', 'none']);
|
|
6592
|
+
return id;
|
|
6593
|
+
};
|
|
6594
|
+
// (1) 날조: 소스도 테스트도 없이 done + 테스트 모양 문자열만 → 차단돼야
|
|
6595
|
+
const fake = mkDone('결제 재시도 구현', 'tests: 32/32 passed');
|
|
6596
|
+
const rFake = R(['verify-claim', fake, '--json']);
|
|
6597
|
+
const jFake = J(rFake) || {};
|
|
6598
|
+
const blocked = rFake.status === 1 && (jFake.reasons || []).includes('unverifiable-claim');
|
|
6599
|
+
// (2) --lenient opt-out 은 존중돼야 (기존 계약 유지)
|
|
6600
|
+
const lenientOk = R(['verify-claim', fake, '--lenient', '--json']).status === 0;
|
|
6601
|
+
// (3) 정직한 주장은 통과해야 — false-BLOCK 0 (이 게이트의 최악 실패모드)
|
|
6602
|
+
fs.mkdirSync(path.join(d, 'src'), { recursive: true });
|
|
6603
|
+
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');
|
|
6604
|
+
fs.mkdirSync(path.join(d, 'tests2'), { recursive: true });
|
|
6605
|
+
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');
|
|
6606
|
+
const honest = mkDone('재시도 헬퍼 추가', 'src/pay.js 구현 + tests2/pay.test.js 1개 테스트 (1/1 통과)');
|
|
6607
|
+
const honestOk = R(['verify-claim', honest, '--json']).status === 0;
|
|
6608
|
+
// (4) 추출기 사각지대가 정직한 주장의 거짓 차단이 되지 않도록 — 검수(High#2)가 짚은 4형태를 모두 고정
|
|
6609
|
+
fs.writeFileSync(path.join(d, 'Dockerfile'), 'FROM node:18\n');
|
|
6610
|
+
fs.writeFileSync(path.join(d, 'Dockerfile.dev'), 'FROM node:18\n');
|
|
6611
|
+
fs.writeFileSync(path.join(d, '.gitignore'), 'node_modules\n');
|
|
6612
|
+
fs.mkdirSync(path.join(d, 'ops', 'containers'), { recursive: true });
|
|
6613
|
+
fs.writeFileSync(path.join(d, 'ops', 'containers', 'Dockerfile'), 'FROM node:18\n');
|
|
6614
|
+
fs.mkdirSync(path.join(d, '.github', 'workflows'), { recursive: true });
|
|
6615
|
+
fs.writeFileSync(path.join(d, '.github', 'workflows', 'ci.yml'), 'name: ci\n');
|
|
6616
|
+
const bareCases = [
|
|
6617
|
+
['도커 베이스 갱신', 'Dockerfile 베이스 이미지 변경 (1/1 통과)'],
|
|
6618
|
+
['도커 dev 갱신', 'Dockerfile.dev 수정 (1/1 통과)'],
|
|
6619
|
+
['무시목록 갱신', '.gitignore 갱신 (1/1 통과)'],
|
|
6620
|
+
['다단계 경로', 'ops/containers/Dockerfile 갱신 (1/1 통과)'],
|
|
6621
|
+
['선두 점 디렉토리', '.github/workflows/ci.yml 수정 (1/1 통과)'],
|
|
6622
|
+
];
|
|
6623
|
+
const bareOk = bareCases.every(([req, ev]) => R(['verify-claim', mkDone(req, ev), '--json']).status === 0);
|
|
6624
|
+
// (4b) 실제 실행 확증(exit 0)은 출력이 파싱 불가여도 면제돼야 — 커스텀 리포터를 벌하지 않는다
|
|
6625
|
+
const runId = mkDone('실행 확증', 'tests: 1/1 passed');
|
|
6626
|
+
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;
|
|
6627
|
+
// (5) 일괄/게이트 경로도 동일 판정 공유
|
|
6628
|
+
const all = J(R(['verify-claim', '--all', '--json'])) || {};
|
|
6629
|
+
const allCatches = (all.results || []).some(x => x && x.id === fake && x.ok === false);
|
|
6630
|
+
const gcj = J(R(['gate', '.', '--claims', '--json'])) || {};
|
|
6631
|
+
const vc = (gcj.checks || []).find(c => c.name === 'verify-claims');
|
|
6632
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
6633
|
+
ok = blocked && lenientOk && honestOk && bareOk && runOk && allCatches && !!vc && vc.ok === false;
|
|
6634
|
+
if (!ok) console.log(` [공허주장 디버그] blocked=${blocked} lenient=${lenientOk} honest=${honestOk} bare=${bareOk} run=${runOk} all=${allCatches} gate=${vc && vc.ok}`);
|
|
6635
|
+
} catch (e) {}
|
|
6636
|
+
console.log(ok ? '✓ B(1.36.82) 공허한 done 주장 차단: 대조 0건 → unverifiable-claim(exit 1) · --lenient 존중 · 정직한 주장/추출기 사각지대 5형태/실행확증 통과(false-BLOCK 0) · --all/gate 동일판정' : '✗ 공허한 done 주장 차단 가드 실패');
|
|
6637
|
+
if (!ok) failed++;
|
|
6638
|
+
}
|
|
6639
|
+
|
|
6640
|
+
// 1.36.82 회귀 (High #2): `gate --require-referee <id>` — 툴이 사용자에게 안내하면서 gate 는 읽지도 않아
|
|
6641
|
+
// 조용히 exit 0 이던 플래그. 이제 실제 게이팅한다(미존재/미증명/무효 거부 + 검증기가 현재 상태를 거부하면 실패).
|
|
6642
|
+
total++;
|
|
6643
|
+
{
|
|
6644
|
+
let ok = false;
|
|
6645
|
+
try {
|
|
6646
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-refgate-'));
|
|
6647
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { encoding: 'utf8', timeout: 60000 });
|
|
6648
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { encoding: 'utf8', timeout: 60000 });
|
|
6649
|
+
const J = (r) => { try { return JSON.parse(r.stdout); } catch { return null; } };
|
|
6650
|
+
const N = (js) => `node -e "${js}"`;
|
|
6651
|
+
const stepOf = (g, name) => ((g && g.checks) || []).find(c => c.name === name);
|
|
6652
|
+
// 안내 문구와 실제 지원이 일치하는가 — 이 불일치가 이번 사고의 본질이었다
|
|
6653
|
+
const advertised = fs.readFileSync(path.join(__dirname, '..', 'lib', 'referee.js'), 'utf8').includes('gate --require-referee');
|
|
6654
|
+
// (검수 High#3) **안내된 문법 그대로** — positional/--path 없이 cwd 에서. 이전 픽스처는 항상 '.' 과 --path 를
|
|
6655
|
+
// 같이 줘서 "값이 positional 경로로 흡수되던" 결함을 가렸다. 픽스처가 버그를 숨기지 않게 한다.
|
|
6656
|
+
const asAdvertised = cp.spawnSync(process.execPath, [CLI, 'gate', '--require-referee', 'nope', '--json'], { cwd: d, encoding: 'utf8', timeout: 60000 });
|
|
6657
|
+
let aj = null; try { aj = JSON.parse(asAdvertised.stdout); } catch {}
|
|
6658
|
+
const rootOk = !!aj && path.resolve(aj.root) === path.resolve(d); // root 가 <cwd>/nope 로 오인되면 실패
|
|
6659
|
+
// (검수 High#4) 값 누락은 조용히 사라지면 안 된다 — CI 의 빈 환경변수 fail-open 차단
|
|
6660
|
+
const bare = J(R(['gate', '.', '--require-referee', '--json']));
|
|
6661
|
+
const bareBlocked = !!bare && bare.ok === false && (bare.checks || []).some(c => c.code === 'referee_missing_value');
|
|
6662
|
+
const missing = J(R(['gate', '.', '--require-referee', 'nope', '--json']));
|
|
6663
|
+
const mStep = stepOf(missing, 'referee:nope');
|
|
6664
|
+
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']);
|
|
6665
|
+
const uncal = stepOf(J(R(['gate', '.', '--require-referee', 'r1', '--json'])), 'referee:r1');
|
|
6666
|
+
R(['referee', 'verify', 'r1']);
|
|
6667
|
+
const cal = stepOf(J(R(['gate', '.', '--require-referee', 'r1', '--json'])), 'referee:r1');
|
|
6668
|
+
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']);
|
|
6669
|
+
R(['referee', 'verify', 'r2']);
|
|
6670
|
+
const rejects = stepOf(J(R(['gate', '.', '--require-referee', 'r2', '--json'])), 'referee:r2');
|
|
6671
|
+
// 플래그 없으면 기존 5체크 무변경(회귀 0)
|
|
6672
|
+
const plain = J(R(['gate', '.', '--json']));
|
|
6673
|
+
const noRegression = plain && plain.total === 5 && !(plain.checks || []).some(c => /referee/.test(c.name));
|
|
6674
|
+
// 반복 지정
|
|
6675
|
+
const twice = J(R(['gate', '.', '--require-referee', 'r1', '--require-referee', 'r2', '--json']));
|
|
6676
|
+
const bothRan = ((twice && twice.checks) || []).filter(c => /^referee:/.test(c.name)).length === 2;
|
|
6677
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
6678
|
+
// 진단 코드가 --json 에 실려야 한다(모든 실패가 {name,ok:false} 로 축약되면 오배선을 구분할 수 없다)
|
|
6679
|
+
const codeExposed = !!mStep && mStep.code === 'referee_not_found';
|
|
6680
|
+
ok = advertised && rootOk && bareBlocked && codeExposed && !!mStep && mStep.ok === false && !!uncal && uncal.ok === false
|
|
6681
|
+
&& !!cal && cal.ok === true && !!rejects && rejects.ok === false && noRegression && bothRan;
|
|
6682
|
+
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}`);
|
|
6683
|
+
} catch (e) {}
|
|
6684
|
+
console.log(ok ? '✓ B(1.36.82) gate --require-referee 실제 게이팅: 안내문법 그대로 root 정상 · 미존재/미증명 거부(code 노출) · 값누락 차단 · 캘리브레이션 후 통과 · 검증기 거부 시 실패 · 반복 지정 · 플래그 없으면 5체크 무변경' : '✗ gate --require-referee 가드 실패');
|
|
6685
|
+
if (!ok) failed++;
|
|
6686
|
+
}
|
|
6687
|
+
|
|
6572
6688
|
// 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
6689
|
total++;
|
|
6574
6690
|
{
|