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/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.83';
|
|
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];
|
|
@@ -3422,7 +3442,31 @@ function _selfTestCases() {
|
|
|
3422
3442
|
const forceGuard = s.includes("const effForce = opts.force && !_USER_STATE.has(f);") && s.includes("'.harness/progress-tracker.md', '.harness/plan.md', '.harness/task-log.md'");
|
|
3423
3443
|
const skillGuard = s.includes('유효하지 않은 skill name (path traversal/경로 문자 차단)') && s.includes('이미 설치된 skill: ${skillId} — 내용이 다릅니다');
|
|
3424
3444
|
const settingsGuard = s.includes('settings.local.json 이 손상돼(JSON 파싱 실패) hook 설치를 중단');
|
|
3425
|
-
|
|
3445
|
+
// 1.36.83 (공허가드 스윕): 종전 정확-리터럴 가드는 이 줄 자신만 매칭해 기능을 지워도 초록이었다.
|
|
3446
|
+
// 불변식 정규식(심링크면 push 후 중단) + **행위검사**(실제 junction/symlink 를 따라가지 않는지)로 대체.
|
|
3447
|
+
// 판별력: lstat→stat 로 되돌리면 소스 정규식은 그대로 통과하지만 행위검사가 잡는다(실측).
|
|
3448
|
+
let symlinkGuard = /isSymbolicLink\(\)\s*\)\s*\{[^\n]*skippedFiles\.push\([^\n]*symlink/.test(s);
|
|
3449
|
+
{
|
|
3450
|
+
const _st = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_sym_'));
|
|
3451
|
+
let _linked = false;
|
|
3452
|
+
try {
|
|
3453
|
+
fs.mkdirSync(path.join(_st, '.harness'), { recursive: true });
|
|
3454
|
+
fs.writeFileSync(path.join(_st, '.harness', 'HARNESS_VERSION'), VERSION);
|
|
3455
|
+
fs.mkdirSync(path.join(_st, 'outside'), { recursive: true });
|
|
3456
|
+
fs.writeFileSync(path.join(_st, 'outside', 'a.txt'), 'A');
|
|
3457
|
+
fs.symlinkSync(path.join(_st, 'outside'), path.join(_st, '.harness', 'loop'), process.platform === 'win32' ? 'junction' : 'dir');
|
|
3458
|
+
_linked = true;
|
|
3459
|
+
} catch {} // 심링크 생성 권한 없는 환경 → 소스 불변식만으로 판정(false-BLOCK 회피)
|
|
3460
|
+
if (_linked) {
|
|
3461
|
+
try {
|
|
3462
|
+
const _rep = _migrateWorkspaceDir(_st, { dryRun: true });
|
|
3463
|
+
const _skipped = _rep.skippedFiles.some(f => /^loop\b/.test(f) && f.includes('symlink'));
|
|
3464
|
+
const _notFollowed = !_rep.copiedFiles.some(f => /^loop[\\/]/.test(f));
|
|
3465
|
+
symlinkGuard = symlinkGuard && _skipped && _notFollowed;
|
|
3466
|
+
} catch { symlinkGuard = false; }
|
|
3467
|
+
}
|
|
3468
|
+
try { fs.rmSync(_st, { recursive: true, force: true }); } catch {}
|
|
3469
|
+
}
|
|
3426
3470
|
const copySafe = /function copyRecursiveSafe[\s\S]{0,400}lstatSync/.test(s);
|
|
3427
3471
|
return forceGuard && skillGuard && settingsGuard && symlinkGuard && copySafe;
|
|
3428
3472
|
} },
|
|
@@ -3852,13 +3896,22 @@ function _selfTestCases() {
|
|
|
3852
3896
|
} },
|
|
3853
3897
|
{ name: 'MCP notification 준수: id없는 요청 무응답 가드 + ping {} (UR-0049 설치리뷰 1.9.313)', run: () => { const src = read(__filename); const guard = src.includes("const isNotification = !('id' in req)") && src.includes("req.method.startsWith('notifications/')") && src.includes('if (isNotification) return;'); const ping = src.includes("req.method === 'ping'") && /ping[\s\S]{0,140}result: \{\} \}/.test(src); return guard && ping; } },
|
|
3854
3898
|
{ name: 'PowerShell 감지: pwsh7(channel/Documents\\PowerShell/install) + ps5.1 영구경로 과경고 안함 (UR-0052 설치리뷰 1.9.314)', run: () => { const f = _detectPwshFromEnv; const pwsh7a = f({ POWERSHELL_DISTRIBUTION_CHANNEL: 'MSI:Windows 10' }).version === '7'; const pwsh7b = f({ PSModulePath: 'C:\\Users\\me\\Documents\\PowerShell\\Modules' }).version === '7'; const pwsh7c = f({ PSModulePath: 'C:\\Program Files\\PowerShell\\7\\Modules' }).version === '7'; const noFalsePs5 = f({ PSModulePath: 'C:\\Users\\me\\Documents\\WindowsPowerShell\\Modules' }).isPowerShell === false; const cmdSys = f({ PSModulePath: 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\Modules' }).isPowerShell === false; const empty = f({}).isPowerShell === false; const src = read(__filename); const wired = src.includes('const fromEnv = _detectPwshFromEnv()') && src.includes('const pwshEnv = _detectPwshFromEnv()'); return pwsh7a && pwsh7b && pwsh7c && noFalsePs5 && cmdSys && empty && wired; } },
|
|
3855
|
-
{ name: 'doc/surface 정합: doctor 명령 + stale MCP 카운트 동적화(commands/banner) (UR-0054 설치리뷰 1.9.315)', run: () => { const src = read(__filename); const doctorOk = typeof doctorCmd === 'function' && /cmd === 'doctor'/.test(src) && /# leerness doctor/.test(
|
|
3856
|
-
{ name: 'drift 마커 버그: session-handoff 프론트매터는 ^--- 일 때만 + drift 최신 Last generated (1.9.316)', run: () => { const src = read(__filename); const scSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'session-close.js')); const writeFix = scSrc.includes('if (/^---\\r?\\n/.test(cur))') && scSrc.includes('writeUtf8(handoffPath(root), frontmatter + block)');
|
|
3899
|
+
{ name: 'doc/surface 정합: doctor 명령 + stale MCP 카운트 동적화(commands/banner) (UR-0054 설치리뷰 1.9.315)', run: () => { const src = read(__filename); /* 1.36.83 (검수 Medium#1): `/# leerness doctor/.test(src)` 는 **이 가드 줄 자신**을 매칭했다 — 실제 헤더는 lib/diagnostics.js 에 있다. 구현 파일을 읽는다. */ const doctorOk = typeof doctorCmd === 'function' && /cmd === 'doctor'/.test(src) && /# leerness doctor/.test(read(path.join(path.dirname(__filename), '..', 'lib', 'diagnostics.js'))); const dynCount = /MCP 도구: \$\{_mcpToolCount\(\)\}/.test(src) && /외부 AI 통합 \(MCP \$\{_mcpToolCount\(\)\} 도구\)/.test(src); return doctorOk && dynCount; } },
|
|
3900
|
+
{ name: 'drift 마커 버그: session-handoff 프론트매터는 ^--- 일 때만 + drift 최신 Last generated (1.9.316)', run: () => { const src = read(__filename); const scSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'session-close.js')); const writeFix = scSrc.includes('if (/^---\\r?\\n/.test(cur))') && scSrc.includes('writeUtf8(handoffPath(root), frontmatter + block)'); /* 1.36.82: 이 가드는 bin(src)을 검사했지만 구현은 lib/drift.js 로 옮겨갔다 — 리터럴이 가드 자신의 줄에만 남아
|
|
3901
|
+
자기참조로 통과했다(원 버그를 되살려도 초록). 읽는 파일을 실제 구현 위치로 바로잡는다. */
|
|
3902
|
+
const drSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'drift.js'));
|
|
3903
|
+
const readFix = drSrc.includes('matchAll(/Last generated') && drSrc.includes('allGen[allGen.length - 1]'); return writeFix && readFix; } },
|
|
3857
3904
|
{ name: '텔레메트리 분리: 내부 auto-call(LEERNESS_INTERNAL) usage 집계 제외 + 주요 spawn 마킹 (UR-0051 설치리뷰 1.9.317)', run: () => { const src = read(__filename); const guard = src.includes("process.env.LEERNESS_INTERNAL !== '1'"); const marked = (src.match(/LEERNESS_INTERNAL: '1'/g) || []).length >= 10; const reviewMarked = /'review-request'[\s\S]{0,200}LEERNESS_INTERNAL: '1'/.test(src); return guard && marked && reviewMarked; } },
|
|
3858
3905
|
{ name: 'lib/pure-utils: HTML 파싱 유틸 3종 모듈 분리 + 동작 + 인라인 제거 (UR-0025 1.9.318)', run: () => { const m = require('../lib/pure-utils'); const fnOk = typeof m._htmlToText === 'function' && typeof m._extractTitle === 'function' && typeof m._extractLinks === 'function'; const work = m._htmlToText('<p>Hello <b>World</b></p>') === 'Hello World' && m._extractTitle('<html><title>My & Page</title></html>') === 'My & Page' && m._extractLinks('<a href="/a">A</a><a href="https://other.com/b">B</a>', 'https://x.com/').length === 1; const moved = m._htmlToText === _htmlToText && !/^function _htmlToText\(html\) \{/m.test(read(__filename)); return fnOk && work && moved; } },
|
|
3859
3906
|
{ name: 'MCP ToolRegistry 일치성: 모든 도구 def 가 dispatch case 보유 + 고아 case 0 + requiredTier 완비 (UR-0044 1.9.319)', run: () => { const tools = require('../lib/mcp-tools'); const src = read(__filename); const missing = tools.filter(t => !src.includes("case '" + t.name + "':")); const cases = [...src.matchAll(/case '(leerness_[a-z_]+)':/g)].map(m => m[1]); const defNames = new Set(tools.map(t => t.name)); const orphans = [...new Set(cases)].filter(c => !defNames.has(c)); const tierOk = tools.every(t => typeof t.requiredTier === 'string' && PERMISSION_TIERS.includes(t.requiredTier)); return tools.length >= 83 && missing.length === 0 && orphans.length === 0 && tierOk; } },
|
|
3860
3907
|
{ name: 'count drift 수정: _countDatedBlocks 코드펜스(템플릿) 제외 (UR-0053 1.9.320; memory count canonical 전환 후 legacy parser 보존)', run: () => { const f = _countDatedBlocks; const withTpl = '# D\n\n```md\n### 2026-01-01 — Decision 제목\n- Decision:\n```\n\n### 2026-06-04 — 실제\n- Decision: 실제\n'; const c1 = f(withTpl) === 1; const c0 = f('```md\n### 2026-01-01 — x\n```\n') === 0; const c2 = f('### 2026-01-01 — A\n### 2026-02-02 — B\n') === 2; return typeof f === 'function' && c1 && c0 && c2; } },
|
|
3861
|
-
{ name: 'decision/lesson 필드 파싱: 빈 필드가 다음 줄로 안 샘 ([ \\t]* 사용) (UR-0053 1.9.321)', run: () => { const block = '### 2026-06-05 — X\n- Decision: X\n- Reason: r\n- Alternatives: \n- Impact: 보안\n'; const alt = block.match(/- Alternatives:[ \t]*(.+)/); const imp = block.match(/- Impact:[ \t]*(.+)/); const altNoBleed = !alt || !/Impact/.test(alt[1]); const impOk = !!imp && imp[1].trim() === '보안';
|
|
3908
|
+
{ name: 'decision/lesson 필드 파싱: 빈 필드가 다음 줄로 안 샘 ([ \\t]* 사용) (UR-0053 1.9.321)', run: () => { const block = '### 2026-06-05 — X\n- Decision: X\n- Reason: r\n- Alternatives: \n- Impact: 보안\n'; const alt = block.match(/- Alternatives:[ \t]*(.+)/); const imp = block.match(/- Impact:[ \t]*(.+)/); const altNoBleed = !alt || !/Impact/.test(alt[1]); const impOk = !!imp && imp[1].trim() === '보안'; /* 1.36.82: 파서가 lib/pure-utils.js 로 이동했는데 가드는 bin 을 검사해 자기 줄만 매칭했다 —
|
|
3909
|
+
구현 파일을 읽고, 소스 문자열만이 아니라 **실제 파서 동작**으로도 확인한다(빈 필드가 다음 줄을 먹지 않는가). */
|
|
3910
|
+
const puSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'pure-utils.js'));
|
|
3911
|
+
const fixedOk = puSrc.includes('- Alternatives:[ \\t]*(.+)') && puSrc.includes('- Lesson:[ \\t]*(.+)') && puSrc.includes('- Impact:[ \\t]*(.+)');
|
|
3912
|
+
const parsed = require('../lib/pure-utils')._extractDecisionBlocks ? true : false;
|
|
3913
|
+
const behav = (() => { const objs = require('../lib/pure-utils')._decisionsFromMd('### 2026-06-05 — X\n- Decision: X\n- Reason: r\n- Alternatives: \n- Impact: 보안\n'); return objs.length === 1 && String(objs[0].alternatives || '').trim() === '' && String(objs[0].impact || '').trim() === '보안'; })();
|
|
3914
|
+
return altNoBleed && impOk && fixedOk && parsed && behav; } },
|
|
3862
3915
|
{ name: 'MCP handler 통합: _mcpToCliArgs 단일 함수 + mcpServeCmd 호출 + 인라인 switch 단일화 (UR-0044 1.9.322)', run: () => { const src = read(__filename); const fnDef = /function _mcpToCliArgs\(name, args, targetPath\) \{/.test(src); const called = src.includes('cliArgs = _mcpToCliArgs(name, args, targetPath)'); const switchCount = (src.match(/switch \(name\) \{/g) || []).length; const nullPath = src.includes('if (cliArgs === null) return send('); return fnDef && called && switchCount === 1 && nullPath; } },
|
|
3863
3916
|
{ name: 'fresh-init gate 통과: lazy detect 부재신호(handoff/test/progress) done-work 없으면 비차단 (UR-0054 ⑥ 1.9.323)', run: () => { const src = read(__filename); const doneWork = src.includes("const _hasDoneWork = rows.some(r => /^(done|completed|verified)$/i.test(r.status))"); const advisory = src.includes('_ADVISORY_KINDS') && src.includes("'handoff_never_generated'") && src.includes("'no_test_run'"); const blocking = src.includes('const blockingIssues = Math.max(0, issues - advisoryCount)') && src.includes('if (blockingIssues > 0) process.exitCode = 1'); return doneWork && advisory && blocking; } },
|
|
3864
3917
|
{ name: 'lib/pure-utils: 메모리 MD 파서 분리(_countDatedBlocks/_extractDecisionBlocks) + _compareSemver 중복제거 (UR-0025 1.9.324)', run: () => { const m = require('../lib/pure-utils'); const fnOk = typeof m._countDatedBlocks === 'function' && typeof m._extractDecisionBlocks === 'function'; const work = m._countDatedBlocks('```md\n### 2026-01-01 — T\n```\n### 2026-06-05 — R\n') === 1 && m._extractDecisionBlocks('### 2026-06-05 — A\n- Decision: x\n').length === 1; const src = read(__filename); const moved = m._countDatedBlocks === _countDatedBlocks && m._extractDecisionBlocks === _extractDecisionBlocks && !/^function _countDatedBlocks\(/m.test(src) && !/^function _compareSemver\(/m.test(src); return fnOk && work && moved; } },
|
|
@@ -3870,7 +3923,7 @@ function _selfTestCases() {
|
|
|
3870
3923
|
{ name: 'lib/pure-utils: project-brief config 분리(_BRIEF_FIELDS/_briefFilled) + 인라인 제거 (UR-0025 1.9.330)', run: () => { const m = require('../lib/pure-utils'); const cfgOk = Array.isArray(m._BRIEF_FIELDS) && m._BRIEF_FIELDS.length === 10 && m._BRIEF_FIELDS[0].key === 'intro'; const work = m._briefFilled({ intro: 'x', features: ['a'] }) === 2 && m._briefFilled({}) === 0; const src = read(__filename); const moved = m._briefFilled === _briefFilled && m._BRIEF_FIELDS === _BRIEF_FIELDS && !/^const _BRIEF_FIELDS = \[/m.test(src) && !/^function _briefFilled\(/m.test(src); return cfgOk && work && moved; } },
|
|
3871
3924
|
{ name: 'lib/pure-utils: brief 빌더 분리(_briefReadmeBlock/_briefBlueprint + BRIEF 마커, VERSION 주입) (UR-0025 1.9.331)', run: () => { const m = require('../lib/pure-utils'); const fnOk = typeof m._briefReadmeBlock === 'function' && typeof m._briefBlueprint === 'function' && m.BRIEF_START.includes('project-brief:start'); const b = { project: 'X', intro: 'i', features: ['f1'] }; const rb = m._briefReadmeBlock(b); const bp = m._briefBlueprint(b, '9.9.9'); const work = rb.includes(m.BRIEF_START) && rb.includes(m.BRIEF_END) && /f1/.test(rb) && /Blueprint/.test(bp) && /leerness v9\.9\.9/.test(bp); const src = read(__filename); const moved = m._briefBlueprint === _briefBlueprint && m.BRIEF_START === BRIEF_START && !/^function _briefReadmeBlock\(/m.test(src) && !/^function _briefBlueprint\(/m.test(src) && !/^const BRIEF_START =/m.test(src); return fnOk && work && moved; } },
|
|
3872
3925
|
{ name: 'lib/pure-utils: lessons.md 파서 분리(_parseLessonEntries) + 인라인 제거 (UR-0025 1.9.332)', run: () => { const m = require('../lib/pure-utils'); const r = m._parseLessonEntries('### 2026-06-05\n- Lesson: A\n- Tag: t\n\n### 2026-06-04\n- Lesson: B'); const work = r.length === 2 && r[0].text === 'A' && r[0].tag === 't' && r[1].tag === null && r[0].date === '2026-06-05'; const src = read(__filename); const moved = m._parseLessonEntries === _parseLessonEntries && !/^function _parseLessonEntries\(/m.test(src) && src.includes('_parseLessonEntries(read(mp))'); return work && moved; } },
|
|
3873
|
-
{ name: 'UR-0025 심층: constraints catalog→lib/catalogs + _matchConstraints→pure-utils 분리 (1.9.333) + i18n en(1.31.2)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c._DEFAULT_PLATFORM_CONSTRAINTS && Object.keys(c._DEFAULT_PLATFORM_CONSTRAINTS.platforms).length === 6 && !!c._DEFAULT_PLATFORM_CONSTRAINTS.platforms.stripe; const r = m._matchConstraints(c._DEFAULT_PLATFORM_CONSTRAINTS, 'stripe 결제'); const work = r.matched.length === 1 && r.matched[0].platform === 'stripe' && r.totalPlatforms === 6 && m._matchConstraints(null, 'x').matched.length === 0; const _H = /[가-힣]/; const enSug = (m._matchConstraints(c._DEFAULT_PLATFORM_CONSTRAINTS, 'generic api integration widget', 'en').suggestions || [])[0] || ''; const koSug = (m._matchConstraints(c._DEFAULT_PLATFORM_CONSTRAINTS, 'generic api integration widget', 'ko').suggestions || [])[0] || ''; const i18nOk = c._DEFAULT_PLATFORM_CONSTRAINTS.platforms.stripe.constraints.some(x => x.detailEn && !_H.test(x.detailEn)) && enSug.length > 0 && !_H.test(enSug) && _H.test(koSug); const src = read(__filename); const moved = _DEFAULT_PLATFORM_CONSTRAINTS === c._DEFAULT_PLATFORM_CONSTRAINTS && _matchConstraints === m._matchConstraints && !/const _DEFAULT_PLATFORM_CONSTRAINTS = \{/.test(src) &&
|
|
3926
|
+
{ name: 'UR-0025 심층: constraints catalog→lib/catalogs + _matchConstraints→pure-utils 분리 (1.9.333) + i18n en(1.31.2)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c._DEFAULT_PLATFORM_CONSTRAINTS && Object.keys(c._DEFAULT_PLATFORM_CONSTRAINTS.platforms).length === 6 && !!c._DEFAULT_PLATFORM_CONSTRAINTS.platforms.stripe; const r = m._matchConstraints(c._DEFAULT_PLATFORM_CONSTRAINTS, 'stripe 결제'); const work = r.matched.length === 1 && r.matched[0].platform === 'stripe' && r.totalPlatforms === 6 && m._matchConstraints(null, 'x').matched.length === 0; const _H = /[가-힣]/; const enSug = (m._matchConstraints(c._DEFAULT_PLATFORM_CONSTRAINTS, 'generic api integration widget', 'en').suggestions || [])[0] || ''; const koSug = (m._matchConstraints(c._DEFAULT_PLATFORM_CONSTRAINTS, 'generic api integration widget', 'ko').suggestions || [])[0] || ''; const i18nOk = c._DEFAULT_PLATFORM_CONSTRAINTS.platforms.stripe.constraints.some(x => x.detailEn && !_H.test(x.detailEn)) && enSug.length > 0 && !_H.test(enSug) && _H.test(koSug); const src = read(__filename); const moved = _DEFAULT_PLATFORM_CONSTRAINTS === c._DEFAULT_PLATFORM_CONSTRAINTS && _matchConstraints === m._matchConstraints && !/const _DEFAULT_PLATFORM_CONSTRAINTS = \{/.test(src) && /_matchConstraints\(_loadPlatformConstraints\(root\), text/.test(src); /* 1.36.82: 호출부가 lang 인자를 얻으며 정확 리터럴이 어긋나 자기 줄만 매칭했다 — 인자 추가에 견디는 불변식으로 */ return catOk && work && i18nOk && moved; } },
|
|
3874
3927
|
{ name: 'UR-0025 심층(Codex 위임·검증): intent domain catalog→lib/catalogs + _matchDomain→pure-utils 분리 (1.9.334)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c._DEFAULT_DOMAIN_CATALOG && Object.keys(c._DEFAULT_DOMAIN_CATALOG.domains).length === 5 && !!c._DEFAULT_DOMAIN_CATALOG.domains.game; const r = m._matchDomain(c._DEFAULT_DOMAIN_CATALOG, 'unity 게임'); const work = r.domain === 'game' && Array.isArray(r.components) && m._matchDomain(c._DEFAULT_DOMAIN_CATALOG, 'zzz없음').domain === null && m._matchDomain(null, 'x').domain === null; const src = read(__filename); const moved = _DEFAULT_DOMAIN_CATALOG === c._DEFAULT_DOMAIN_CATALOG && _matchDomain === m._matchDomain && !/const _DEFAULT_DOMAIN_CATALOG = \{/.test(src) && src.includes('_matchDomain(_loadDomainCatalog(root), text)'); return catOk && work && moved; } },
|
|
3875
3928
|
{ name: 'UR-0025 심층: LSP catalog→lib/catalogs(_LSP_LANG_PATTERNS) + _detectLspLang/_matchLspSymbols→pure-utils 분리 (1.9.335)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c._LSP_LANG_PATTERNS && Object.keys(c._LSP_LANG_PATTERNS).length === 5 && Array.isArray(c._LSP_LANG_PATTERNS.javascript); const langOk = m._detectLspLang('a.py') === 'python' && m._detectLspLang('b.go') === 'go' && m._detectLspLang('c.md') === 'javascript'; const sy = m._matchLspSymbols(c._LSP_LANG_PATTERNS, 'function alpha(){}\nclass Beta{}', 'javascript'); const work = sy.length === 2 && sy[0].name === 'alpha' && sy[0].kind === 'function' && sy[1].kind === 'class' && m._matchLspSymbols(null, 'x', 'javascript').length === 0; const src = read(__filename); const moved = _LSP_LANG_PATTERNS === c._LSP_LANG_PATTERNS && _detectLspLang === m._detectLspLang && _matchLspSymbols === m._matchLspSymbols && !/const _LSP_LANG_PATTERNS = \{/.test(src) && !/function _detectLspLang\(/.test(src); return catOk && langOk && work && moved; } },
|
|
3876
3929
|
{ name: 'UR-0025 심층(Codex 위임·검증): anti-laziness catalog→lib/catalogs(OPTIMISM_PATTERNS) + optimism 순수로직→pure-utils 분리 (1.9.336)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = Array.isArray(c.OPTIMISM_PATTERNS) && c.OPTIMISM_PATTERNS.length === 10 && c.OPTIMISM_PATTERNS[0].kind === 'API'; const ev = 'API 호출 완료, POST /users'; const sus = m._detectOptimism(c.OPTIMISM_PATTERNS, ev, 'function x(){}'); const conf = m._computeConfidence(c.OPTIMISM_PATTERNS, ev, 'function x(){}'); const work = sus.some(s => s.kind === 'API' && s.severity === 'high') && conf < 0.5 && m._computeConfidence(c.OPTIMISM_PATTERNS, '정리함', 'x') === 1 && m._detectOptimism(null, ev, 'x').length === 0 && m._extractUrlClaims('POST /a').length === 1 && m._verifyUrlClaim({ path: '/a' }, 'has /a') === true; const src = read(__filename); const moved = OPTIMISM_PATTERNS === c.OPTIMISM_PATTERNS && _puDetectOptimism === m._detectOptimism && !/const OPTIMISM_PATTERNS = \[/.test(src) && !/function _extractUrlClaims\(/.test(src); return catOk && work && moved; } },
|
|
@@ -3921,7 +3974,7 @@ function _selfTestCases() {
|
|
|
3921
3974
|
{ name: 'UR-0025 큰핸들러토대: lib/io.js fs 프리미티브(read/writeUtf8/exists/mkdirp/append/rel/absRoot) 분리 + round-trip (1.9.383)', run: () => { const io = require('../lib/io'); const exp = ['absRoot', 'exists', 'read', 'readBuf', 'mkdirp', 'writeUtf8', 'append', 'rel'].every(k => typeof io[k] === 'function') && io.read === read && io.writeUtf8 === writeUtf8 && io.exists === exists; const src = read(__filename); const moved = !/^function writeUtf8\(p, s\) \{/m.test(src) && !/^function read\(p\) \{/m.test(src) && !/^function exists\(p\) \{/m.test(src); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_io_')); let rt = false; try { const f = path.join(tmp, 'a', 'b.txt'); io.writeUtf8(f, '한글RT'); rt = io.exists(f) && io.read(f) === '한글RT' && io.rel(tmp, f) === 'a/b.txt'; } finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } return exp && moved && rt; } },
|
|
3922
3975
|
{ name: '5th외부평가/UR-0085: status --json 구조화 출력 + verify --json 와이어 (1.9.384)', run: () => { if (typeof status !== 'function' || typeof verify !== 'function') return false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_sj_')); const save = process.argv; const _w = process.stdout.write; let so = ''; try { fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true }); process.argv = ['node', 'h', 'status', tmp, '--json']; process.stdout.write = s => { so += s; return true; }; status(tmp); } catch {} finally { process.stdout.write = _w; process.argv = save; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } let sj; try { sj = JSON.parse(so); } catch {} const statusOk = !!sj && typeof sj.total === 'number' && typeof sj.present === 'number' && 'healthy' in sj && Array.isArray(sj.missing); const src = read(__filename); const verifyWired = /function verify\(root\) \{[\s\S]*?has\('--json'\)[\s\S]*?JSON\.stringify\(\{ ok:/.test(src); return statusOk && verifyWired; } },
|
|
3923
3976
|
{ name: '5th외부평가/UR-0086: _parseContractSpec markdown bullet 함수 감지 + 순수 추출 (1.9.385)', run: () => { const m = require('../lib/pure-utils'); if (m._parseContractSpec !== _parseContractSpec) return false; const p = _parseContractSpec('# Spec\n- add(a,b)\n* subtract(a,b)\n1. multiply(a,b)\nfunction legacy(x)\n`mentioned(`\ntick.amount\n'); const declOk = ['add', 'subtract', 'multiply', 'legacy'].every(n => p.declared.includes(n)) && p.declared.length === 4; const menOk = p.mentioned.includes('mentioned') && !p.declared.includes('mentioned'); const fieldOk = p.fields.includes('amount'); const fpOk = _parseContractSpec('- 합계 (a+b)\n- result (total)\n- foo: bar(x)\n**bold**').declared.length === 0; const src = read(__filename); const moved = src.includes('_parseContractSpec(specText)') && !/specText\.matchAll\(\/function/.test(src); return declOk && menOk && fieldOk && fpOk && moved; } },
|
|
3924
|
-
{ name: '5th외부평가/UR-0087: _gitignoreMatch git 일치(.env↛.env.bad) + env-family 스캔 (1.9.386)', run: () => { const m = require('../lib/pure-utils'); if (m._gitignoreMatch !== _gitignoreMatch) return false; const gm = _gitignoreMatch; const semOk = gm('.env', '.env') === true && gm('.env', '.env.bad') === false && gm('.env', '.env.local') === false && gm('.env.*', '.env.bad') === true && gm('.env*', '.env') === true && gm('*.pem', 'k.pem') === true && gm('src/', 'src/a.txt') === true; const src = read(__filename);
|
|
3977
|
+
{ name: '5th외부평가/UR-0087: _gitignoreMatch git 일치(.env↛.env.bad) + env-family 스캔 (1.9.386)', run: () => { const m = require('../lib/pure-utils'); if (m._gitignoreMatch !== _gitignoreMatch) return false; const gm = _gitignoreMatch; const semOk = gm('.env', '.env') === true && gm('.env', '.env.bad') === false && gm('.env', '.env.local') === false && gm('.env.*', '.env.bad') === true && gm('.env*', '.env') === true && gm('*.pem', 'k.pem') === true && gm('src/', 'src/a.txt') === true; const src = read(__filename); let envFamilyScan = false; { /* 1.36.83 (공허가드 스윕): 종전 리터럴 '!SCAN_TEXT_EXT.has(ext) && !isEnv' + 'Family' 은 1.36.56 리팩터(_known)로 제품에서 사라져 이 줄 자신만 매칭하던 공허 가드였다. env-family 강제포함을 행위로 검사한다: .env.production 은 확장자('.production')가 allow-list 밖 + 64KB 초과라, isEnvFamily 가 _known 에서 빠지면 '작은 텍스트' 폴백 경로에서 크기로 걸러져 미탐된다(실측 1→0). */ const _t = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_envfam_')); try { fs.writeFileSync(path.join(_t, '.env.production'), 'x'.repeat(70 * 1024) + '\nAWS_ACCESS_KEY_ID=' + 'AKIAJQXMP7RZ2KL9WXYZ' + '\n'); const _r = _collectSecretFindings(_t); envFamilyScan = _r.findings.some(f => f.file === '.env.production' && f.name === 'AWS Access Key'); } catch { envFamilyScan = false; } finally { try { fs.rmSync(_t, { recursive: true, force: true }); } catch {} } } const delegated = src.includes('return _gitignoreMatch(gi, fileRel)'); return semOk && envFamilyScan && delegated; } },
|
|
3925
3978
|
{ name: 'UR-0088 5th외부평가 일관성: incident/runs list 빈 케이스 --json 구조화 (1.9.387)', run: () => { if (typeof incidentListCmd !== 'function' || typeof runsListCmd !== 'function') return false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_lj_')); const save = process.argv; const _w = process.stdout.write; let io = '', ro = ''; try { fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true }); process.argv = ['node', 'h', 'incident', 'list', '--json']; process.stdout.write = s => { io += s; return true; }; incidentListCmd(tmp); process.stdout.write = _w; process.argv = ['node', 'h', 'runs', 'list', '--json']; process.stdout.write = s => { ro += s; return true; }; runsListCmd(tmp); } catch {} finally { process.stdout.write = _w; process.argv = save; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } let ij, rj; try { ij = JSON.parse(io); rj = JSON.parse(ro); } catch {} return !!ij && ij.total === 0 && Array.isArray(ij.items) && !!rj && rj.total === 0 && Array.isArray(rj.items); } },
|
|
3926
3979
|
{ name: 'UR-0025 큰핸들러 모듈화: migrate audit/apply/plan → lib/migrate.js + DI 위임 + 동작 (1.9.388)', run: () => { const m = require('../lib/migrate'); const expOk = typeof m.migrateAuditCmd === 'function' && typeof m.migrateApplyCmd === 'function' && typeof m.migratePlanCmd === 'function'; const src = read(__filename); const delegated = src.includes("require('../lib/migrate')") && src.includes('_migrate.migrateAuditCmd(root, opts, _migrateDeps())') && src.includes('_migrate.migratePlanCmd(root, opts, _migrateDeps())'); const movedToLib = read(path.join(path.dirname(__filename), '..', 'lib', 'migrate.js')).includes('leerness-plan-'); let behavOk = false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_mig_')); const save = process.argv; const _w = process.stdout.write; let out = ''; try { fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true }); fs.writeFileSync(path.join(tmp, '.harness', 'HARNESS_VERSION'), VERSION); process.argv = ['node', 'h', 'migrate', 'audit', tmp, '--json']; process.stdout.write = s => { out += s; return true; }; migrateAuditCmd(tmp, { json: true }); } catch {} finally { process.stdout.write = _w; process.argv = save; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } try { const j = JSON.parse(out); behavOk = j.version === VERSION && typeof j.willChange === 'number' && Array.isArray(j.findings); } catch {} return expOk && delegated && movedToLib && behavOk; } },
|
|
3927
3980
|
{ name: 'UR-0025 큰핸들러 모듈화: teamCmd → lib/team.js + DI 위임 + 동작 (1.9.389)', run: () => { const m = require('../lib/team'); const expOk = typeof m.teamCmd === 'function'; const src = read(__filename); const delegated = src.includes("require('../lib/team')") && src.includes('_team.teamCmd(root, sub, id, opts,'); const teamSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'team.js')); const movedToLib = teamSrc.includes("require('./pure-utils')") && teamSrc.includes('_teamDeployGate') && teamSrc.includes('알 수 없는 team 하위명령'); let behavOk = false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_tm_')); const save = process.argv; const _w = process.stdout.write; let out = ''; try { fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true }); process.argv = ['node', 'h', 'team', 'list', '--json']; process.stdout.write = s => { out += s; return true; }; teamCmd(tmp, 'list', undefined, { json: true }); } catch {} finally { process.stdout.write = _w; process.argv = save; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } try { const j = JSON.parse(out); behavOk = j.version === VERSION && j.count === 0 && Array.isArray(j.teams); } catch {} return expOk && delegated && movedToLib && behavOk; } },
|
|
@@ -3942,7 +3995,25 @@ function _selfTestCases() {
|
|
|
3942
3995
|
{ name: '7번째 버그헌트 P1-B (UR-0105): verify-claim/optimism-check/honesty-check --json 에러 구조화 (1.9.400)', run: () => { const src = read(__filename); const vc = /function verifyClaimCmd[\s\S]{0,1200}?failJson\(_j, 'not_found'/.test(src); const oc = /function optimismCheckCmd[\s\S]{0,700}?failJson\(_j, 'not_found'/.test(src); const hc = /function honestyCheckCmd[\s\S]{0,900}?failJson\(has\('--json'\), 'not_found'/.test(src); return vc && oc && hc; } }, // 1.30.5: {0,400}→{0,700} (F4 가 missing_args 라인을 en/ko 로 늘려 not_found 가 창 밖) · 1.33.2: vc {0,700}→{0,1200} (opts.collect 가드 라인이 not_found 를 더 밀어냄)
|
|
3943
3996
|
{ name: '7번째 버그헌트 P1-C (UR-0106): 시크릿 FN — gitignore 부정(!) + placeholder substring 정밀화 (1.9.401)', run: () => { const m = require('../lib/pure-utils'); const gm = m._gitignoreMatch; const negOk = gm('*.example\n!.env.example', '.env.example') === false && gm('*.log', 'a.log') === true && gm('a.log\n!a.log', 'a.log') === false && gm('.env', '.env') === true; const ph = m._isPlaceholderSecret; const phOk = ph('sk-EXAMPLEab12cd34ef56gh78ij90kl') === false && ph('sk-proj-realKEYexample9988776655') === false && ph('your-key-here') === true && ph('changeme') === true && ph('example') === true && ph('xxxxxxxxxxxxxxxxxxxxxxxxxxxx') === true; return negOk && phOk; } },
|
|
3944
3997
|
{ name: '7번째 버그헌트 P1-A 잔여 (UR-0108): decisions/lessons MD projection 개행 주입 차단 _lineSafe (1.9.402)', run: () => { const m = require('../lib/pure-utils'); if (m._lineSafe !== _lineSafe) return false; const lsOk = _lineSafe('a\nb\r\nc') === 'a b c'; const md = m._renderDecisionsMd([{ date: '2026-06-07', title: 'real\n### 2099-01-01 — FAKE\n- Decision: forged', decision: 'd', reason: 'r' }]); const re = m._decisionsFromMd(md); const noInject = re.length === 1 && !/^### 2099-01-01 — FAKE/m.test(md); const lmd = m._renderLessonsMd([{ date: '2026-06-07', text: 'l1\n### FAKE\n- Lesson: x', tag: 't' }]); const lre = m._parseLessonEntries(lmd); const lNoInject = lre.length === 1; return lsOk && noInject && lNoInject; } },
|
|
3945
|
-
{ name: '7번째 버그헌트 P2 (UR-0107): api-skill show/drop 에러 exit code 1 (1.9.403)', run: () => {
|
|
3998
|
+
{ name: '7번째 버그헌트 P2 (UR-0107): api-skill show/drop 에러 exit code 1 (1.9.403)', run: () => {
|
|
3999
|
+
// 낡은 소스-리터럴(인라인 `process.exitCode = 1`) 대신 **행위 검사**: 1.36.74 에서 에러 경로가 failJson 으로
|
|
4000
|
+
// 리팩터되며 리터럴이 가드 자기 줄에만 남아 공허참이 됐다. 실제로 호출해 exit code 를 확인한다.
|
|
4001
|
+
if (typeof apiSkillCmd !== 'function') return false;
|
|
4002
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_apisk_'));
|
|
4003
|
+
const save = process.argv; const _w = process.stdout.write; const _e = process.stderr.write; const savedExit = process.exitCode;
|
|
4004
|
+
const c = {};
|
|
4005
|
+
try {
|
|
4006
|
+
process.stdout.write = () => true; process.stderr.write = () => true;
|
|
4007
|
+
const ex = (tail, sub) => { process.argv = ['node', 'h'].concat(tail); process.exitCode = 0; const p = apiSkillCmd(tmp, sub); if (p && typeof p.catch === 'function') p.catch(() => {}); return process.exitCode || 0; };
|
|
4008
|
+
c.showNoId = ex(['api-skill', 'show'], 'show');
|
|
4009
|
+
c.dropNoId = ex(['api-skill', 'drop'], 'drop');
|
|
4010
|
+
c.showNf = ex(['api-skill', 'show', 'NOPE'], 'show');
|
|
4011
|
+
c.dropNf = ex(['api-skill', 'drop', 'NOPE'], 'drop');
|
|
4012
|
+
c.addNoUrl = ex(['api-skill', 'add'], 'add');
|
|
4013
|
+
c.list = ex(['api-skill', 'list'], 'list');
|
|
4014
|
+
} catch (e) { return false; } finally { process.stdout.write = _w; process.stderr.write = _e; process.argv = save; process.exitCode = savedExit; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
4015
|
+
return c.showNoId === 1 && c.dropNoId === 1 && c.showNf === 1 && c.dropNf === 1 && c.addNoUrl === 1 && c.list === 0;
|
|
4016
|
+
} },
|
|
3946
4017
|
{ name: '7번째 버그헌트 P2 (UR-0105 잔여): reuse autodetect / creds check --json 에러 구조화 (1.9.404)', run: () => { const src = read(__filename); const reuseOk = src.includes("failJson(has('--json'), 'no_scan_dir'"); const credsOk = src.includes("failJson(has('--json'), 'no_service'"); return reuseOk && credsOk; } },
|
|
3947
4018
|
{ name: '8번째 버그헌트 회귀수정 (UR-0109): 긴 서술형 placeholder FP 차단(마커 우선) + 실키 FN 유지 (1.9.405)', run: () => { const m = require('../lib/pure-utils'); const ph = m._isPlaceholderSecret; const fpFixed = ph('your-super-secret-api-key-example-value') === true && ph('this-is-just-an-example-placeholder-value') === true && ph('example-api-key-do-not-use-1234567890') === true; const fnKept = ph('sk-EXAMPLEab12cd34ef56gh78ij90kl') === false && ph('sk-proj-realKEYexample9988776655') === false; const realKept = ph('a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6') === false; const shortPh = ph('your-api-key-here') === true && ph('changeme') === true; return fpFixed && fnKept && realKept && shortPh; } },
|
|
3948
4019
|
{ name: '8번째 버그헌트 (UR-0110): rule/decision/lesson add 동시쓰기 _withLock 직렬화 (UR-0043 갭 메움) (1.9.406)', run: () => { const src = read(__filename); const L = '_withLock('; const ruleLock = src.includes(L + 'rulesPath' + '(root), () =>'); const decLock = src.includes(L + 'decisionsJsonPath' + '(root), () =>'); const lesLock = src.includes(L + 'lessonsJsonPath' + '(root), () =>'); return ruleLock && decLock && lesLock; } },
|
|
@@ -3952,10 +4023,56 @@ function _selfTestCases() {
|
|
|
3952
4023
|
{ name: '8번째 버그헌트 (UR-0114): absRoot 비문자열(--path 값없음 boolean true) → cwd 폴백(raw TypeError 차단) (1.9.410)', run: () => { const io = require('../lib/io'); const cwd = process.cwd(); const tBool = io.absRoot(true) === cwd; const tEmpty = io.absRoot('') === cwd; const tUndef = io.absRoot(undefined) === cwd; const tSpace = io.absRoot(' ') === cwd; const tReal = io.absRoot(os.tmpdir()) === path.resolve(os.tmpdir()); return tBool && tEmpty && tUndef && tSpace && tReal; } },
|
|
3953
4024
|
{ name: '8번째 버그헌트 (UR-0115): lazy detect --auto-track 단일 RMW 배치(O(T×N)→O(N+T)) (1.9.411)', run: () => { const src = read(__filename); const batched = src.includes("8번째 버그헌트, UR-0115") && /has\('--auto-track'\)[\s\S]{0,500}?_withLock\(progressPath\(root\), \(\) => \{[\s\S]{0,1200}?writeProgressRows/.test(src); const noPerTodoUpsert = !/for \(const t of newTodos\) \{\s*const id = nextId\(root, 'T'\);/.test(src); return batched && noPerTodoUpsert; } },
|
|
3954
4025
|
{ name: '6번째 외부평가 Opus P1 (UR-0100): list-family(decision/feature/plan/runs/team list) positional path 지원 (조용한 cwd 오독 차단) (1.9.412)', run: () => { const src = read(__filename); const L = '_resolveRoot('; const decOk = src.includes("decisionListCmd(absRoot(" + L + "args[2]))"); const planOk = src.includes("planListCmd(absRoot(" + L + "args[2]))"); const featOk = src.includes("featureListCmd(absRoot(" + L + "args[2]))"); const runsOk = src.includes("runsListCmd(absRoot(" + L + "args[2]))"); const teamOk = src.includes(L + "args[1] === 'list' ? args[2] : null)"); return decOk && planOk && featOk && runsOk && teamOk; } },
|
|
3955
|
-
{ name: '6번째 외부평가 codex P2 (UR-0101): action 명령(task/decision/rule/lesson add) --json 구조화 출력 (1.9.413)', run: () => {
|
|
4026
|
+
{ name: '6번째 외부평가 codex P2 (UR-0101): action 명령(task/decision/rule/lesson add) --json 구조화 출력 (1.9.413)', run: () => {
|
|
4027
|
+
// 낡은 소스-리터럴(task 의 status 표현이 _normTaskStatus 로 바뀌며 썩음) 대신 **행위 검사**:
|
|
4028
|
+
// 4개 add 명령을 --json 으로 실제 호출해 구조화 페이로드(단일 JSON 문서)를 파싱·검증한다.
|
|
4029
|
+
if (typeof taskAdd !== 'function' || typeof decisionAdd !== 'function' || typeof lessonSave !== 'function' || typeof ruleAdd !== 'function') return false;
|
|
4030
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_addjson_'));
|
|
4031
|
+
const save = process.argv; const _w = process.stdout.write; const savedExit = process.exitCode;
|
|
4032
|
+
let t = null, d = null, l = null, r = null, rDup = null;
|
|
4033
|
+
try {
|
|
4034
|
+
fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
|
|
4035
|
+
fs.writeFileSync(path.join(tmp, '.harness', 'HARNESS_VERSION'), VERSION);
|
|
4036
|
+
const cap = (argv, fn) => {
|
|
4037
|
+
let out = ''; process.argv = argv; process.stdout.write = s => { out += s; return true; };
|
|
4038
|
+
try { fn(); } finally { process.stdout.write = _w; }
|
|
4039
|
+
const line = out.split(/\r?\n/).map(x => x.trim()).filter(x => x.startsWith('{')).pop();
|
|
4040
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
4041
|
+
};
|
|
4042
|
+
t = cap(['node', 'h', 'task', 'add', 'JSON계약 T', '--json', '--no-review'], () => taskAdd(tmp, 'JSON계약 T'));
|
|
4043
|
+
d = cap(['node', 'h', 'decision', 'add', 'JSON계약 D', '--json'], () => decisionAdd(tmp, 'JSON계약 D'));
|
|
4044
|
+
l = cap(['node', 'h', 'lesson', 'save', 'JSON계약 L', '--json', '--tag', 'tg'], () => lessonSave(tmp, 'JSON계약 L'));
|
|
4045
|
+
r = cap(['node', 'h', 'rule', 'add', 'JSON계약 R', '--json', '--trigger', 'every-session'], () => ruleAdd(tmp, 'JSON계약 R'));
|
|
4046
|
+
rDup = cap(['node', 'h', 'rule', 'add', 'JSON계약 R', '--json', '--trigger', 'every-session'], () => ruleAdd(tmp, 'JSON계약 R'));
|
|
4047
|
+
} catch (e) { return false; } finally { process.stdout.write = _w; process.argv = save; process.exitCode = savedExit; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
4048
|
+
const taskJ = !!t && t.ok === true && /^T-\d{4}/.test(String(t.id)) && t.status === 'requested' && t.request === 'JSON계약 T';
|
|
4049
|
+
const decJ = !!d && d.ok === true && d.title === 'JSON계약 D';
|
|
4050
|
+
const lesJ = !!l && l.ok === true && l.text === 'JSON계약 L' && l.tag === 'tg';
|
|
4051
|
+
const ruleJ = !!r && r.ok === true && r.rule === 'JSON계약 R' && r.trigger === 'every-session' && r.skipped === false && !!rDup && rDup.skipped === true;
|
|
4052
|
+
return taskJ && decJ && lesJ && ruleJ;
|
|
4053
|
+
} },
|
|
3956
4054
|
{ name: '9th 외부평가 Codex P2 (UR-0121 잔여): health 보안 정직화(커밋 시크릿 반영) + status scope:install (1.9.418)', run: () => {
|
|
3957
4055
|
const src = read(__filename);
|
|
3958
|
-
|
|
4056
|
+
// 1.36.83 (검수 High#1, 실측 확정): 이 세 단언은 bin 을 읽지만 검증 대상은 lib/health.js 에 있다 —
|
|
4057
|
+
// `_collectSecretFindings(root)`/`committedSecrets` 는 bin 의 **무관한 다른 호출**이 만족시키고
|
|
4058
|
+
// 한국어 문구는 이 가드 줄 자신이 만족시킨다. 실제로 lib/health.js 의 스캐너 연결을 제거해
|
|
4059
|
+
// 커밋된 시크릿이 있는데도 healthy:true 가 나오게 만들어도 selftest 는 337/337 초록이었다.
|
|
4060
|
+
// → 실제 시크릿을 심은 워크스페이스로 healthCmd 를 돌려 **행위**로 검증한다.
|
|
4061
|
+
let healthWired = false;
|
|
4062
|
+
{
|
|
4063
|
+
const _hd = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_health_'));
|
|
4064
|
+
try {
|
|
4065
|
+
mkdirp(path.join(_hd, '.harness'));
|
|
4066
|
+
writeUtf8(path.join(_hd, '.harness', 'HARNESS_VERSION'), VERSION);
|
|
4067
|
+
// 다른 selftest 케이스가 "placeholder 아님"으로 단언하는 실 탐지 대상 값(분할 표기로 자기참조 회피)
|
|
4068
|
+
writeUtf8(path.join(_hd, 'cfg.js'), 'const k = "' + 'AKIA' + 'JQXMP7RZ2KL9WXYZ' + '";\n');
|
|
4069
|
+
const _r = cp.spawnSync(process.execPath, [__filename, 'health', _hd, '--json'],
|
|
4070
|
+
{ encoding: 'utf8', timeout: 120000, maxBuffer: 16 * 1024 * 1024 });
|
|
4071
|
+
const _j = JSON.parse(_r.stdout.slice(_r.stdout.indexOf('{')));
|
|
4072
|
+
const _s = (_j.checks || {}).security || {};
|
|
4073
|
+
healthWired = _s.committedSecrets >= 1 && _s.critical === true && _j.healthy === false;
|
|
4074
|
+
} catch { healthWired = false; } finally { try { fs.rmSync(_hd, { recursive: true, force: true }); } catch {} }
|
|
4075
|
+
}
|
|
3959
4076
|
const statusScope = src.includes("scope: 'install'") && src.includes('healthyMeaning');
|
|
3960
4077
|
return healthWired && statusScope && typeof healthCmd === 'function' && typeof status === 'function';
|
|
3961
4078
|
} },
|
|
@@ -4023,10 +4140,23 @@ function _selfTestCases() {
|
|
|
4023
4140
|
return expOk && delegated && movedToLib;
|
|
4024
4141
|
} },
|
|
4025
4142
|
{ name: '10th 외부평가 Sonnet P2: rule add flag/경로 break(_parseAddTitle) — trigger 값/경로 흡수 차단 (1.9.426)', run: () => {
|
|
4026
|
-
|
|
4027
|
-
|
|
4143
|
+
// 낡은 소스-리터럴(디스패처 한 줄 그대로)은 1.30.4/1.9.445 리팩터로 썩어 자기참조 공허참이 됐다.
|
|
4144
|
+
// 대신 **실행 검사**: rule add 를 실제 spawn 해 (a) 제목이 --trigger 값/후행 경로를 흡수하지 않는지,
|
|
4145
|
+
// (b) positional path 가 root 로 쓰이고 cwd 는 오염되지 않는지 확인한다.
|
|
4028
4146
|
const m = require('../lib/pure-utils');
|
|
4029
4147
|
const u = m._parseAddTitle(['rule', 'add', '세션', '점검', '--trigger', 'every-session', '/p'], 2) === '세션 점검';
|
|
4148
|
+
const proj = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_ruleproj_'));
|
|
4149
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_rulecwd_'));
|
|
4150
|
+
let wired = false;
|
|
4151
|
+
try {
|
|
4152
|
+
for (const d of [proj, cwd]) { fs.mkdirSync(path.join(d, '.harness'), { recursive: true }); fs.writeFileSync(path.join(d, '.harness', 'HARNESS_VERSION'), VERSION); }
|
|
4153
|
+
const r = cp.spawnSync(process.execPath, [__filename, 'rule', 'add', '세션', '점검', '--trigger', 'every-session', proj, '--json'], { encoding: 'utf8', cwd, timeout: 30000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1' } });
|
|
4154
|
+
const line = (r.stdout || '').split(/\r?\n/).map(x => x.trim()).filter(x => x.startsWith('{')).pop();
|
|
4155
|
+
const j = JSON.parse(line);
|
|
4156
|
+
const titleOk = j.ok === true && j.rule === '세션 점검' && j.trigger === 'every-session';
|
|
4157
|
+
const rootOk = fs.existsSync(path.join(proj, '.harness', 'rules.md')) && !fs.existsSync(path.join(cwd, '.harness', 'rules.md'));
|
|
4158
|
+
wired = titleOk && rootOk;
|
|
4159
|
+
} catch (e) { wired = false; } finally { for (const d of [proj, cwd]) { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} } }
|
|
4030
4160
|
return wired && u;
|
|
4031
4161
|
} },
|
|
4032
4162
|
{ name: '클린룸 (UR-0184): feature add/show/link/impact positional-path 와이어 + 미초기화 게이트 + _taskPositionalPath 값-플래그 skip (1.36.2)', run: () => {
|
|
@@ -4310,7 +4440,24 @@ function _selfTestCases() {
|
|
|
4310
4440
|
{ name: 'UR-0151: decision/lesson/rule add positional path 지원(_taskPositionalPath 재사용, cwd 오염 차단) (1.9.445)', run: () => {
|
|
4311
4441
|
const src = read(__filename);
|
|
4312
4442
|
// 1.12.1 (UR-0008): 멀티라인 exact-string includes 는 공백/줄바꿈/환경에 취약(클린룸 selftest false-alarm) → 공백 유연 정규식(\s+)으로 견고화.
|
|
4313
|
-
|
|
4443
|
+
// 1.36.80 (가드 부패 수리): 낡은 exact-literal "…, _parseAddTitle(args, 2))" 은 1.30.4 리팩터(_desc 변수 추출)로 제품 코드에서
|
|
4444
|
+
// 사라졌고 이 가드 줄 자신에만 남아 includes 가 영원히 참이었다(자기참조 false-pass — dispatch 를 통째로 process.cwd() 로 바꿔도 초록).
|
|
4445
|
+
// → 소스 문자열 대신 실제 CLI 실행으로 "positional 경로에 기록 + cwd 미오염"을 행위 검증한다.
|
|
4446
|
+
let rule = false;
|
|
4447
|
+
const _rt = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_ur0151_'));
|
|
4448
|
+
try {
|
|
4449
|
+
const _proj = path.join(_rt, 'proj'), _cwdDir = path.join(_rt, 'cwd');
|
|
4450
|
+
fs.mkdirSync(path.join(_proj, '.harness'), { recursive: true });
|
|
4451
|
+
fs.writeFileSync(path.join(_proj, '.harness', 'HARNESS_VERSION'), VERSION); // init 게이트 통과용 최소 마커
|
|
4452
|
+
fs.mkdirSync(_cwdDir, { recursive: true }); // 미초기화 — cwd 로 새면 write 자체가 차단됨
|
|
4453
|
+
const _rtitle = 'UR-0151 positional path 회귀가드';
|
|
4454
|
+
cp.spawnSync(process.execPath, [__filename, 'rule', 'add', _rtitle, _proj, '--trigger', 'every-session', '--json'],
|
|
4455
|
+
{ cwd: _cwdDir, encoding: 'utf8', timeout: 60000, maxBuffer: 8 * 1024 * 1024 });
|
|
4456
|
+
const _inProj = readRules(_proj).some(r => r.rule === _rtitle && r.status === 'active');
|
|
4457
|
+
const _cwdRules = path.join(_cwdDir, '.harness', 'rules.md');
|
|
4458
|
+
const _inCwd = exists(_cwdRules) && read(_cwdRules).includes(_rtitle);
|
|
4459
|
+
rule = _inProj && !_inCwd;
|
|
4460
|
+
} catch {} finally { try { fs.rmSync(_rt, { recursive: true, force: true }); } catch {} }
|
|
4314
4461
|
const lesson = /if \(cmd === 'lesson'\) \{\s+const root = absRoot\(arg\('--path', null\) \|\| _taskPositionalPath\(args, 2\) \|\| process\.cwd\(\)\)/.test(src);
|
|
4315
4462
|
const decision = /if \(cmd === 'decision'\) \{\s+const root = absRoot\(arg\('--path', null\) \|\| _taskPositionalPath\(args, 2\) \|\| process\.cwd\(\)\)/.test(src);
|
|
4316
4463
|
// rule add 의 --trigger 값은 경로 아님(path-like 아님) + 값-플래그 제외
|
|
@@ -4442,8 +4589,15 @@ function _selfTestCases() {
|
|
|
4442
4589
|
// 1) done 기본 claimsChecked(--strict-claims 옵트인 아님) 2) --json 에 claimsConsistent+gitCrossCheck 3) --lenient 탈출 4) optimism 전체스캔(walk root) 5) git strongMismatch 는 advisory(기본 게이트 제외)
|
|
4443
4590
|
const defaultGate = src.includes("const claimsChecked = !lenient && (isDoneClaim || has('--require-evidence') || has('--strict-claims'));");
|
|
4444
4591
|
const jsonExposed = src.includes('claimsConsistent: !claimsChecked ? null : strictOk') && src.includes('gitCrossCheck: !gitApplicable ? null : !gitStrongMismatch');
|
|
4445
|
-
|
|
4446
|
-
|
|
4592
|
+
// 1.36.82 (공허한 가드 스윕 High): 이 리터럴은 리팩터로 사라진 옛 표현이라 **자기 줄만 매칭**하고 있었다 —
|
|
4593
|
+
// verify-claim 의 --json exit 게이트가 어떤 selftest 로도 보호되지 않았다(게이트를 통째로 지워도 초록).
|
|
4594
|
+
// 현재 구현(reasons 기반 단일 판정 + exit 전파)을 검사한다.
|
|
4595
|
+
const jsonGate = src.includes("if (claimsChecked && !strictOk) out.reasons.push('optimism/honesty')")
|
|
4596
|
+
&& src.includes('out.ok = out.reasons.length === 0;') && src.includes('if (!out.ok) return process.exit(1);');
|
|
4597
|
+
// 1.36.82: 끝의 `;` 때문에 실제 overallFail 라인과 어긋나 **자기 줄만 매칭**하고 있었다(리팩터로 뒤에 항이 추가됨).
|
|
4598
|
+
// 현재 라인과 일치시키고, 1.36.82 에 추가한 unverifiableClaim 게이팅이 human 경로에 배선됐는지도 함께 단언한다.
|
|
4599
|
+
const overall = src.includes('(claimsChecked && !strictOk) || !evidenceQualityOk || !gitClaimOk')
|
|
4600
|
+
&& /const overallFail = unverifiableClaim \|\|/.test(src);
|
|
4447
4601
|
const wholeScan = src.includes('function _scanCodeForPatterns(root)') && src.includes('walk(root, 0);');
|
|
4448
4602
|
const gitAdvisory = src.includes("const gitClaimOk = !(has('--strict-claims') && gitStrongMismatch);");
|
|
4449
4603
|
return defaultGate && jsonExposed && jsonGate && overall && wholeScan && gitAdvisory;
|
|
@@ -4463,7 +4617,12 @@ function _selfTestCases() {
|
|
|
4463
4617
|
const src = read(__filename);
|
|
4464
4618
|
const coreDef = src.match(/function _verifyClaimsAll\(root\) \{[\s\S]*?\n\}/); // 함수 본문만 캡처(첫 줄머리 } 까지)
|
|
4465
4619
|
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
|
-
|
|
4620
|
+
// 1.36.82: 헤더 문자열을 **정확 리터럴**로 잡으면 그 줄을 만질 때마다 자기 줄만 매칭해 조용히 통과한다
|
|
4621
|
+
// (이번 라운드에만 두 번 그렇게 됐다). 표현이 아니라 **불변식**을 본다:
|
|
4622
|
+
// 헤더가 withClaims 로 5/6 을 계산하고, --claims 일 때만 verify-claims 단계가 붙는다.
|
|
4623
|
+
const gateOptIn = src.includes("const withClaims = has('--claims');")
|
|
4624
|
+
&& /# leerness gate \(\$\{[^}]*withClaims \? 6 : 5[^}]*\} checks\)/.test(src)
|
|
4625
|
+
&& src.includes("if (withClaims) step('verify-claims', () => { const r = _verifyClaimsAll(root);");
|
|
4467
4626
|
const reuse = src.includes('const res = _verifyClaimsAll(root);'); // CLI 도 코어 공유(분기 없음)
|
|
4468
4627
|
const tools = require('../lib/mcp-tools');
|
|
4469
4628
|
const def = tools.find(t => t.name === 'leerness_verify_claim_all');
|
|
@@ -4520,8 +4679,31 @@ function _selfTestCases() {
|
|
|
4520
4679
|
} },
|
|
4521
4680
|
{ name: '15th 잔여 클러스터 (UR-0017~0021): api-skill CRLF + shell-guard 공백없는&& + stat-before-read + 중첩skip + requirements 디렉티브 (1.12.5)', run: () => {
|
|
4522
4681
|
const src = read(__filename);
|
|
4523
|
-
|
|
4524
|
-
|
|
4682
|
+
// 1.36.80 (가드 부패 수리): 낡은 exact-literal 두 개(_loadAPISkill 손상-fallback 객체 리터럴 / 스캔 루프의 1MB stat 한 줄)는
|
|
4683
|
+
// 각각 1.36.74(손상 frontmatter 표시 필드 추가) · 1.36.56(stat 1회화 리팩터)로 제품 코드에서 사라졌고 이 가드 줄 자신에만 남아
|
|
4684
|
+
// includes 가 영원히 참이었다(자기참조 false-pass). → CRLF 정규화/손상 fallback 은 _loadAPISkill 실호출,
|
|
4685
|
+
// 1MB 상한은 _collectSecretFindings 실호출로 행위 검증하고, "stat 이 read 보다 먼저"라는 순서만 불변식 정규식으로 확인한다.
|
|
4686
|
+
let apiCrlf = false, statBeforeRead = false;
|
|
4687
|
+
const _t15 = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_g15_'));
|
|
4688
|
+
try {
|
|
4689
|
+
const _sk = path.join(_t15, '.harness', 'api-skills');
|
|
4690
|
+
fs.mkdirSync(_sk, { recursive: true });
|
|
4691
|
+
fs.writeFileSync(path.join(_sk, 'crlf.md'), '---\r\nid: crlf\r\nname: CRLF Skill\r\nurls:\r\n - https://ex.test/doc\r\n---\r\n본문\r\n');
|
|
4692
|
+
fs.writeFileSync(path.join(_sk, 'broken.md'), '---\nid: broken\n닫는 구분자 없음 — frontmatter 손상\n');
|
|
4693
|
+
const _s1 = _loadAPISkill(_t15, 'crlf'); // CRLF: raw read 였다면 frontmatter 전부 유실
|
|
4694
|
+
const _s2 = _loadAPISkill(_t15, 'broken'); // 손상: body 없으면 _matchAPISkills 의 s.body.slice 크래시
|
|
4695
|
+
apiCrlf = !!_s1 && _s1.name === 'CRLF Skill' && _s1.urls.length === 1 && _s1.urls[0] === 'https://ex.test/doc' && !/\r/.test(String(_s1.body))
|
|
4696
|
+
&& !!_s2 && typeof _s2.body === 'string' && _s2.body.length > 0 && Array.isArray(_s2.urls) && _s2.name === 'broken';
|
|
4697
|
+
const _sec = 'module.exports={apiKey:"sk-test-1234567890abcdefghijklmnopqrstuvwxyz"};';
|
|
4698
|
+
fs.writeFileSync(path.join(_t15, 'small.js'), _sec);
|
|
4699
|
+
fs.writeFileSync(path.join(_t15, 'big.js'), '// ' + 'x'.repeat(1024 * 1024) + '\n' + _sec);
|
|
4700
|
+
const _hits = _collectSecretFindings(_t15).findings || [];
|
|
4701
|
+
const _capOk = _hits.some(f => /small\.js/.test(f.file)) && !_hits.some(f => /big\.js/.test(f.file)); // 1MB 초과는 스캔 제외
|
|
4702
|
+
const _order = /statSync\(file\)[\s\S]{0,240}?size > 1024 \* 1024\)\s*continue;[\s\S]{0,600}?read(?:FileSync)?\(file\)/.test(src)
|
|
4703
|
+
&& /statSync\(file\)\.size > 5 \* 1024 \* 1024\)\s*continue;[\s\S]{0,240}?readBuf\(file\)/.test(src)
|
|
4704
|
+
&& /statSync\(fp2\)\.size > budget\)\s*continue;[\s\S]{0,160}?read\(fp2\)/.test(src);
|
|
4705
|
+
statBeforeRead = _capOk && _order;
|
|
4706
|
+
} catch {} finally { try { fs.rmSync(_t15, { recursive: true, force: true }); } catch {} }
|
|
4525
4707
|
const nestedSkip = src.includes('segs.some(s => SCAN_SKIP_DIRS.has(s))');
|
|
4526
4708
|
const an = require('../lib/analyzers');
|
|
4527
4709
|
const sg = an._shellGuardAnalyze('npm run build&&npm test', { shell: 'powershell', psVersion: 5 });
|
|
@@ -4543,11 +4725,33 @@ function _selfTestCases() {
|
|
|
4543
4725
|
} },
|
|
4544
4726
|
{ name: 'Karpathy 가이드라인4 (UR-0032): plan --done-when 검증가능 완료조건 저장/파싱/표시 (1.14.2)', run: () => {
|
|
4545
4727
|
const src = read(__filename);
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4728
|
+
// 1.36.80 (가드 부패 수리): planAdd 의 doneWhen 기본값 한 줄을 통째로 박아둔 낡은 exact-literal 은 1.36.63 다국어화로
|
|
4729
|
+
// 제품 코드에서 사라졌고 이 가드 줄 자신에만 남아 includes 가 영원히 참이었다(자기참조 false-pass — --done-when 을 통째로 무시해도 초록).
|
|
4730
|
+
// 아래 dw 계산도 제품 파서가 아니라 이 케이스가 새로 쓴 정규식이라 아무것도 지키지 못했다.
|
|
4731
|
+
// → plan add 를 실제 실행해 저장(plan.md)과 파싱(planListCmd)을 행위 검증. 표시/기본값은 언어별이라 불변식 정규식으로.
|
|
4732
|
+
let wired = false;
|
|
4733
|
+
const _tp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_dw_'));
|
|
4734
|
+
try {
|
|
4735
|
+
fs.mkdirSync(path.join(_tp, '.harness'), { recursive: true });
|
|
4736
|
+
fs.writeFileSync(path.join(_tp, '.harness', 'HARNESS_VERSION'), VERSION);
|
|
4737
|
+
const _cond = '로그인 e2e 테스트 통과';
|
|
4738
|
+
const _mtitle = 'Done-When 회귀가드';
|
|
4739
|
+
cp.spawnSync(process.execPath, [__filename, 'plan', 'add', _mtitle, '--path', _tp, '--done-when', _cond, '--json'],
|
|
4740
|
+
{ cwd: _tp, encoding: 'utf8', timeout: 60000, maxBuffer: 8 * 1024 * 1024 });
|
|
4741
|
+
// 저장: milestone 블록에 Done-When 줄 + 제목이 --done-when 값을 흡수하지 않음(nonFlagArgs withValue 회귀가드)
|
|
4742
|
+
const _pm = read(planPath(_tp));
|
|
4743
|
+
const _stored = new RegExp('^### M-\\d{4,}\\. ' + _mtitle + '$[\\s\\S]*?^Done-When: ' + _cond + '$', 'm').test(_pm);
|
|
4744
|
+
// 파싱: 제품 파서(planListCmd)가 doneWhen 을 실제로 되돌려줌
|
|
4745
|
+
let _out = ''; const _wr = process.stdout.write;
|
|
4746
|
+
try { process.stdout.write = x => { _out += x; return true; }; planListCmd(_tp, { json: true }); } finally { process.stdout.write = _wr; }
|
|
4747
|
+
let _ms = []; try { _ms = JSON.parse(_out).milestones || []; } catch {}
|
|
4748
|
+
const _parsed = _ms.length === 1 && _ms[0].doneWhen === _cond && _ms[0].title === _mtitle;
|
|
4749
|
+
// 표시 + 미지정 기본값: 1.36.63 다국어화로 값이 언어별 → exact 리터럴 대신 불변식
|
|
4750
|
+
const _shown = /완료기준\(Done-When\): \$\{m\.doneWhen/.test(src);
|
|
4751
|
+
const _hasDefault = /const doneWhen = _lineSafe\(arg\('--done-when', ''\)\s*\|\|/.test(src);
|
|
4752
|
+
wired = _stored && _parsed && _shown && _hasDefault;
|
|
4753
|
+
} catch {} finally { try { fs.rmSync(_tp, { recursive: true, force: true }); } catch {} }
|
|
4754
|
+
return wired;
|
|
4551
4755
|
} },
|
|
4552
4756
|
{ name: '16th 버그헌트 F1/F2: scan secrets 패턴당 멀티매치(break 제거) + task/rule list 파이프 셀안전 (1.15.1)', run: () => {
|
|
4553
4757
|
const src = read(__filename);
|
|
@@ -4557,8 +4761,18 @@ function _selfTestCases() {
|
|
|
4557
4761
|
} },
|
|
4558
4762
|
{ name: '외부클린룸 C2/C3/C4: gate --json 단일객체 + memory search --json + about .harness 정합 (1.16.1)', run: () => {
|
|
4559
4763
|
const src = read(__filename);
|
|
4560
|
-
|
|
4561
|
-
|
|
4764
|
+
// 자기참조 제거: 낡은 리터럴("const jsonMode = has('--json'); // 외부리뷰 C2")은 리팩터로 제품 코드에서 사라져 가드 자기 줄에서만 매치했다.
|
|
4765
|
+
// gate() 함수 소스만(셀프테스트 영역 제외) 불변식 정규식으로 검사 — 단일 집계 객체 + 단계 출력 억제.
|
|
4766
|
+
const _gateSrc = gate.toString();
|
|
4767
|
+
const c2 = /const jsonMode = has\('--json'\)/.test(_gateSrc)
|
|
4768
|
+
&& /if \(jsonMode\) process\.stdout\.write = \(\) => true;/.test(_gateSrc)
|
|
4769
|
+
&& /if \(jsonMode\) \{ log\(JSON\.stringify\(\{[^\n}]*ok: bad === 0, total: checks\.length, failed: bad, checks\b/.test(_gateSrc);
|
|
4770
|
+
// 자기참조 제거: 낡은 리터럴('JSON.stringify({ version: VERSION, query, total, includeCode')은 필드 추가 리팩터로
|
|
4771
|
+
// 제품 코드에서 사라졌다(가드 자기 줄만 매치). memorySearch() 함수 소스만 불변식 정규식으로 검사.
|
|
4772
|
+
const _msSrc = memorySearch.toString();
|
|
4773
|
+
const c3 = /const jsonMode = has\('--json'\)/.test(_msSrc)
|
|
4774
|
+
&& /if \(!jsonMode\) log\(/.test(_msSrc)
|
|
4775
|
+
&& /if \(jsonMode\) \{ log\(JSON\.stringify\(\{ version: VERSION, query,[^\n]*\btotal,[^\n]*\bincludeCode:[^\n]*\bresults\b/.test(_msSrc);
|
|
4562
4776
|
const _badDir = '.leern' + 'ess/ 에 영속화 (state start'; // 자기참조 회피: 분할 — about state 줄이 .leerness 로 남아있으면 감지
|
|
4563
4777
|
const c4 = src.includes('상태/결정/진행을 .harness/ 에 영속화 (task/decision') && !src.includes('상태/결정/진행을 ' + _badDir);
|
|
4564
4778
|
return c2 && c3 && c4;
|
|
@@ -4772,9 +4986,13 @@ function _selfTestCases() {
|
|
|
4772
4986
|
&& src.includes("else if (surface === 'lessons') _saveLessons(root, _parseLessonEntries(read(activePath)))");
|
|
4773
4987
|
// P2-4: plan remove 가 모든 헤딩(#{1,3} )을 경계로 split (사용자 h2 섹션 흡수 방지)
|
|
4774
4988
|
const planOk = src.includes('const blocks = text.split(/\\n(?=#{1,3} )/);');
|
|
4775
|
-
// P2-5:
|
|
4776
|
-
|
|
4777
|
-
|
|
4989
|
+
// P2-5: 숫자로 시작하는 실파일('123.js'·'2fa.ts')이 추출돼야 한다 — 안 되면 존재검사가 공허통과했다.
|
|
4990
|
+
// 1.36.82: 정규식 **조각 문자열**을 검사하던 것을 **행위 검사**로 교체 — 추출기를 단일 출처(lib/analyzers)로
|
|
4991
|
+
// 옮기자 조각이 사라져 가드가 자기 줄만 매칭할 뻔했다(정확 리터럴 가드의 전형적 부패).
|
|
4992
|
+
const _re = require('../lib/analyzers')._EVIDENCE_FILE_RE('g');
|
|
4993
|
+
const _hit = (s) => Array.from(String(s).matchAll(_re), m => m[0]);
|
|
4994
|
+
const fileReOk = _hit('123.js 수정').includes('123.js') && _hit('2fa.ts 추가').includes('2fa.ts');
|
|
4995
|
+
const analyzersOk = require('../lib/analyzers')._evidenceQuality('123.js 수정, tests: 1/1 passed').hasFile === true;
|
|
4778
4996
|
// P2-3: archive 쓰기에 _lineSafe (개행 델리미터 위조 차단)
|
|
4779
4997
|
const archiveOk = src.includes('`\\n## 제거 ${today()} (target: "${_lineSafe(target)}")\\n${archiveBlocks}\\n`');
|
|
4780
4998
|
// P2-7: drop/remove not-found 가 failJson (--json 에러 구조화)
|
|
@@ -4934,6 +5152,98 @@ function _selfTestCases() {
|
|
|
4934
5152
|
while ((m = re.exec(self)) !== null) { if (m[1].includes(NL)) offenders++; }
|
|
4935
5153
|
return offenders === 0;
|
|
4936
5154
|
} },
|
|
5155
|
+
{ name: '자기참조 소스가드 확산 금지 (1.36.82, 공허한 가드 스윕): 제품에 없는 코드 리터럴을 붙잡은 selftest 가 늘어나면 실패 (메타가드)', run: () => {
|
|
5156
|
+
// 사고: 소스가드가 `read(__filename)` 로 자기 파일을 읽는데, 검사하려는 코드 리터럴이 리팩터로 사라지면
|
|
5157
|
+
// 그 문자열은 **가드 자신의 줄에만** 남는다 → includes 는 계속 참 → 기능을 통째로 지워도 초록.
|
|
5158
|
+
// 실제로 verify-claim 의 --json exit 게이트와 개인키 파일 스캔 강제포함 가드가 이 상태였다.
|
|
5159
|
+
// 판정: selftest 영역 밖(제품 코드 = bin 나머지 + lib/ + scripts/)에 존재하지 않는 **코드형** 리터럴.
|
|
5160
|
+
// 데이터 문자열(파일명·placeholder 등)이 제품에 없는 건 정상이므로 코드 신호가 있는 것만 본다(false-BLOCK 회피).
|
|
5161
|
+
const src = read(__filename);
|
|
5162
|
+
// (검수 High#6) 1차판은 `'selfref' + '-guard-own'` 을 MARK 로 써서 **소스 본문에는 결합된 문자열이 없어**
|
|
5163
|
+
// 자기 제외가 한 번도 동작하지 않았다(자기 자신을 안 센 건 우연이었다). 소스에 그대로 존재하는 표식을 쓴다.
|
|
5164
|
+
const MARK = 'SELFREF_METAGUARD_OWN_CASE';
|
|
5165
|
+
const s = src.indexOf('function _selfTestCases(');
|
|
5166
|
+
if (s < 0) return false;
|
|
5167
|
+
const nextFn = src.indexOf('\nfunction ', s + 10);
|
|
5168
|
+
const e = nextFn < 0 ? src.length : nextFn;
|
|
5169
|
+
const binOutside = src.slice(0, s) + src.slice(e); // bin 에서 selftest 영역만 제외한 부분
|
|
5170
|
+
let outside = binOutside;
|
|
5171
|
+
for (const sub of ['lib', 'scripts']) {
|
|
5172
|
+
const dir = path.join(path.dirname(__filename), '..', sub);
|
|
5173
|
+
if (!exists(dir)) continue;
|
|
5174
|
+
for (const f of fs.readdirSync(dir)) if (f.endsWith('.js')) outside += '\n' + read(path.join(dir, f));
|
|
5175
|
+
}
|
|
5176
|
+
const region = src.slice(s, e);
|
|
5177
|
+
// (검수 High#6) 1차판의 종결자 기반 정규식(`\n } },`)은 337건 중 170건만 매치했다 —
|
|
5178
|
+
// 여러 케이스를 하나로 뭉치거나(마지막 무쉼표 케이스 등) 통째로 놓쳐 가드 자신이 반쯤 공허했다.
|
|
5179
|
+
// 케이스 **시작 경계**로 분할한다(줄머리 ` { name:`) — 종결 형태에 의존하지 않는다.
|
|
5180
|
+
const chunks = region.split(/\n(?= \{ name: )/).slice(1);
|
|
5181
|
+
// 백틱 리터럴도 스캔한다(1차판은 따옴표만 봐서 `.includes(\`…\`)` 로 우회 가능했다).
|
|
5182
|
+
// 1.36.82 (자체 발견): **파일-인지** 판정 — 리터럴이 "제품 어딘가"에 있으면 통과시키던 것을 고친다.
|
|
5183
|
+
// 가드가 read(__filename)(=bin) 을 검사하는데 그 구현이 lib/ 로 옮겨갔다면 그 가드는 여전히 자기 줄만
|
|
5184
|
+
// 매칭한다(실측 3건: drift 최신 Last generated · decision 필드 파싱 · constraints 호출부).
|
|
5185
|
+
// bin 을 읽는 변수의 리터럴은 **bin 안에서만** 찾고, 다른 변수(lib 파일 등)는 전체에서 찾는다.
|
|
5186
|
+
// 수신 변수 매칭에 단어 경계는 필수 — 없으면 `s.includes` 가 `tps.includes` 에 걸려 오탐이 난다(자체 실측).
|
|
5187
|
+
const litRe = /(\w+)\.includes\((['"`])((?:\\.|(?!\2)[\s\S])*?)\2\)/g;
|
|
5188
|
+
const codeish = (L) => /[(){}=;]|=>|\|\||&&/.test(L) && /[a-zA-Z_$]{3,}/.test(L);
|
|
5189
|
+
let offenders = 0;
|
|
5190
|
+
for (const body of chunks) {
|
|
5191
|
+
if (body.includes(MARK)) continue; // 이 메타가드 자신 (표식은 소스에 그대로 존재)
|
|
5192
|
+
if (!/read\(__filename\)/.test(body)) continue;
|
|
5193
|
+
const binVars = new Set([...body.matchAll(/(?:const|let|var)\s+(\w+)\s*=\s*read\(__filename\)/g)].map(m => m[1]));
|
|
5194
|
+
litRe.lastIndex = 0;
|
|
5195
|
+
let lm;
|
|
5196
|
+
while ((lm = litRe.exec(body)) !== null) {
|
|
5197
|
+
const L = lm[3].replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
5198
|
+
if (L.length < 20 || !codeish(L)) continue;
|
|
5199
|
+
// 템플릿 보간은 **백틱에서만** 일어난다 — 백틱 needle 의 `${...}` 는 런타임 치환이라 원문 비교가 불가능하므로
|
|
5200
|
+
// 제외한다(정상 가드 오탐 1건 실측). 그러나 따옴표 리터럴의 '${...}' 는 그냥 문자열이므로 반드시 검사한다.
|
|
5201
|
+
// (검수 High#2) 1차판이 따옴표까지 싸잡아 제외해 16개 가드가 무방비가 됐다 — 실측 우회 재현됨.
|
|
5202
|
+
if (lm[2] === '`' && L.includes('${')) continue;
|
|
5203
|
+
const haystack = binVars.has(lm[1]) ? binOutside : outside;
|
|
5204
|
+
if (!haystack.includes(L)) { offenders++; break; }
|
|
5205
|
+
}
|
|
5206
|
+
}
|
|
5207
|
+
// 1.36.83: 잔여 부채 **0** — 1.36.82 가 baseline 12 로 유예했던 것을 이 라운드에서 전부 수리했다
|
|
5208
|
+
// (각 가드는 지키던 동작을 실제로 깨뜨렸을 때 실패함을 변이로 증명). 이제 하나라도 생기면 즉시 실패한다.
|
|
5209
|
+
// 유예 수치를 남겨두면 그 안에서 조용히 썩으므로, 갚은 뒤에는 반드시 0 으로 조인다.
|
|
5210
|
+
return offenders === 0;
|
|
5211
|
+
} },
|
|
5212
|
+
{ name: '동봉 문서의 낡을 수치 주장 금지 (1.36.82): README/docs 의 MCP 도구·selftest 케이스 수가 실측과 불일치 시 실패 (메타가드)', run: () => {
|
|
5213
|
+
// 사고: docs/interoperability.md 가 "86 도구"(3릴리스 낡음), README.ko.md 가 "selftest 210 케이스"(실제 335) 를
|
|
5214
|
+
// **현재 상태**로 주장한 채 npm 에 실려 나갔다. README.md 는 readmeSync 관리구간이라 동적으로 맞았지만
|
|
5215
|
+
// 관리구간 밖 문서는 아무도 맞춰주지 않는다 — leerness 자신이 "측정 안 된 수치 주장 금지"를 강제하는 도구인데 자기 문서가 어겼다.
|
|
5216
|
+
// 범위(정직하게 좁힘): 현재 상태를 서술하는 문서만. AGENTS.md/CLAUDE.md/CHANGELOG.md 는 **버전별 과거 라운드 로그**라
|
|
5217
|
+
// 과거 수치가 정당하므로 제외한다(포함하면 false-BLOCK 이 된다).
|
|
5218
|
+
const root = path.join(path.dirname(__filename), '..');
|
|
5219
|
+
// (검수 Medium) npm tarball 에 실리는 루트 문서를 빠짐없이 — CONTRIBUTING/SECURITY 도 대상이다.
|
|
5220
|
+
const targets = ['README.md', 'README.ko.md', 'CONTRIBUTING.md', 'SECURITY.md'].map(f => path.join(root, f));
|
|
5221
|
+
const docsDir = path.join(root, 'docs');
|
|
5222
|
+
if (exists(docsDir)) for (const f of fs.readdirSync(docsDir)) if (f.endsWith('.md')) targets.push(path.join(docsDir, f));
|
|
5223
|
+
const mcpN = _mcpToolCount();
|
|
5224
|
+
const selfN = _selfTestCases().length;
|
|
5225
|
+
let coreN = 0; try { coreN = (require('../lib/mcp-tools').CORE || []).length; } catch {}
|
|
5226
|
+
for (const f of targets) {
|
|
5227
|
+
if (!exists(f)) continue;
|
|
5228
|
+
for (const raw of read(f).split('\n')) {
|
|
5229
|
+
// 버전 표기는 카운트 주장이 아니다 — 지우고 본다(1.36.81 을 "36 도구"로 오독하는 것 방지)
|
|
5230
|
+
const line = raw.replace(/\bv?\d+\.\d+(?:\.\d+)?\b/g, ' ');
|
|
5231
|
+
// (검수 Medium) core 프로필 문맥의 수치는 full 과 비교하면 거짓 차단이다 — 그 줄은 core 기준으로 본다.
|
|
5232
|
+
const coreCtx = /\bcore\b|코어|프로필/i.test(line);
|
|
5233
|
+
const expectMcp = coreCtx ? coreN : mcpN;
|
|
5234
|
+
// "80+" 같은 하한 표현은 정확 수치 주장이 아니다 — 대상에서 제외(false-BLOCK 회피).
|
|
5235
|
+
if (/\d\s*\+/.test(line)) continue;
|
|
5236
|
+
// (검수 Medium FN) 어순 양방향: "89 도구" 와 "MCP tools: 89" 둘 다 잡는다.
|
|
5237
|
+
// 주의: JS \b 는 ASCII 기준이라 한글 뒤에서 성립하지 않는다 → 경계는 영문 tools 에만 붙인다.
|
|
5238
|
+
let m = /\b(\d{2,3})\s*(?:개\s*)?(?:MCP\s*)?(?:도구|tools?\b)/i.exec(line)
|
|
5239
|
+
|| /(?:MCP[- ]?tools?|MCP\s*도구)\s*[:=]?\s*(\d{2,3})\b/i.exec(line);
|
|
5240
|
+
if (m && expectMcp && Number(m[1]) !== expectMcp) return false;
|
|
5241
|
+
m = /selftest[^\n]{0,16}?\b(\d{2,4})\b/i.exec(line) || /\b(\d{2,4})\s*selftest/i.exec(line);
|
|
5242
|
+
if (m && Number(m[1]) !== selfN) return false;
|
|
5243
|
+
}
|
|
5244
|
+
}
|
|
5245
|
+
return true;
|
|
5246
|
+
} },
|
|
4937
5247
|
{ name: '출하 소스 생 NUL 금지 (1.36.81, 클린룸 실측): bin/lib 전수 0 — 구분자를 이스케이프로 (메타가드)', run: () => {
|
|
4938
5248
|
// 1.36.80 사고: 지문/ID 구분자를 소스에 **생 NUL 바이트**로 써서 도구가 파일을 바이너리로 취급했다(grep 차단).
|
|
4939
5249
|
// 정규화 도구가 조용히 지우면 해시 입력이 바뀌어 전 프로젝트의 캘리브레이션/ID 가 한꺼번에 어긋난다.
|
|
@@ -4973,8 +5283,22 @@ function _selfTestCases() {
|
|
|
4973
5283
|
} },
|
|
4974
5284
|
{ name: 'CLI 영어화 Phase 3 (1.21.2, UR-0010): verify-claim 출력 t() 경유 + 한국어 기본 보존 (소스 가드)', run: () => {
|
|
4975
5285
|
const src = read(__filename);
|
|
4976
|
-
|
|
4977
|
-
|
|
5286
|
+
// 1.36.83 (검수 Medium#1, 실측 확정): '## File check' / '## Test count' 는 **가드 자신의 줄에만** 존재했다 —
|
|
5287
|
+
// 실제 출력은 이모지가 들어간 '## 📂 File check (0 claimed)' / '## 🧪 Test count' 라 정확 리터럴이 어긋난다.
|
|
5288
|
+
// → 실제 CLI 를 en 으로 돌려 헤딩이 영어로 렌더되는지 **행위**로 확인하고, ko 보존은 소스로 확인한다.
|
|
5289
|
+
let en = false;
|
|
5290
|
+
{
|
|
5291
|
+
const _ed = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_en3_'));
|
|
5292
|
+
try {
|
|
5293
|
+
cp.spawnSync(process.execPath, [__filename, 'init', _ed, '--yes', '--no-env', '--no-stale-check', '--language', 'en'], { encoding: 'utf8', timeout: 90000 });
|
|
5294
|
+
cp.spawnSync(process.execPath, [__filename, 'task', 'add', 'probe', '--path', _ed], { encoding: 'utf8', timeout: 60000 });
|
|
5295
|
+
const _md = read(path.join(_ed, '.harness', 'progress-tracker.md'));
|
|
5296
|
+
const _tid = [...String(_md).matchAll(/\|\s*(T-\d{4})\s*\|/g)].map(m => m[1]).pop();
|
|
5297
|
+
cp.spawnSync(process.execPath, [__filename, 'progress', 'update', _tid, '--status', 'done', '--evidence', 'bin/leerness.js', '--path', _ed], { encoding: 'utf8', timeout: 60000 });
|
|
5298
|
+
const _o = (cp.spawnSync(process.execPath, [__filename, 'verify-claim', _tid, '--path', _ed, '--language', 'en'], { encoding: 'utf8', timeout: 120000, maxBuffer: 16 * 1024 * 1024 }).stdout) || '';
|
|
5299
|
+
en = /^##[^\n]*File check/m.test(_o) && /^##[^\n]*Test count/m.test(_o) && /^##[^\n]*Summary/m.test(_o) && !/[가-힣]/.test(_o.split('\n').filter(l => /^##/.test(l)).join('\n'));
|
|
5300
|
+
} catch { en = false; } finally { try { fs.rmSync(_ed, { recursive: true, force: true }); } catch {} }
|
|
5301
|
+
}
|
|
4978
5302
|
const koPreserved = src.includes('## 종합') && src.includes('구현 실체 (done 기본)'); // ko 원문이 t() ko 인자로 남아 e2e(ko) 무회귀
|
|
4979
5303
|
return en && koPreserved;
|
|
4980
5304
|
} },
|
|
@@ -5016,7 +5340,13 @@ function _selfTestCases() {
|
|
|
5016
5340
|
const sc = read(path.join(path.dirname(__filename), '..', 'lib', 'session-close.js'));
|
|
5017
5341
|
const rowsEn = sc.includes("t('- 없음', '- none')") && sc.includes('_retroOneLine(agg, uiLang)');
|
|
5018
5342
|
const retroEn = bin.includes('function _retroOneLine(agg, lang)') && bin.includes('`done ${done}/${total}') && bin.includes('decisions ${agg.decisionBlocks} accumulated');
|
|
5019
|
-
|
|
5343
|
+
// 자기참조 제거: 1.36.30 에서 자동 생성 대상이 roadmap.html → leerness.html(온톨로지 그래프)로 바뀌며
|
|
5344
|
+
// 'roadmap.html auto-updated (${trigger})' 는 제품 코드에서 사라졌다(가드 자기 줄만 매치).
|
|
5345
|
+
// _autoRoadmap() 함수 소스만 검사 — en/ko 두 분기가 모두 살아있는지(영어화 정직성).
|
|
5346
|
+
const _arSrc = _autoRoadmap.toString();
|
|
5347
|
+
const roadmapEn = /_uiLang\(root\) === 'en'/.test(_arSrc)
|
|
5348
|
+
&& /ontology graph auto-updated \(\${trigger}\)/.test(_arSrc)
|
|
5349
|
+
&& /온톨로지 그래프 자동 갱신 \(\${trigger}\)/.test(_arSrc);
|
|
5020
5350
|
const koPreserved = bin.includes('완료 ${done}/${total}') && bin.includes('온톨로지 그래프 자동 갱신 (${trigger})') && sc.includes("t('- 없음', '- none')"); // ko 인자 보존
|
|
5021
5351
|
return rowsEn && retroEn && roadmapEn && koPreserved;
|
|
5022
5352
|
} },
|
|
@@ -5043,7 +5373,10 @@ function _selfTestCases() {
|
|
|
5043
5373
|
const bin = read(__filename);
|
|
5044
5374
|
const cat = read(path.join(path.dirname(__filename), '..', 'lib', 'catalogs.js'));
|
|
5045
5375
|
const pu = read(path.join(path.dirname(__filename), '..', 'lib', 'pure-utils.js'));
|
|
5046
|
-
|
|
5376
|
+
// 1.36.82 (공허한 가드 스윕): `!isKeyFile) continue;` 는 1.36.56 리팩터로 사라진 코드다 —
|
|
5377
|
+
// 그 문자열이 이 가드 자신의 줄에만 남아 **자기참조로 통과**하고 있었다(개인키 스캔을 꺼도 초록).
|
|
5378
|
+
// 현재 구현(_known 강제 포함)을 검사하도록 교체 + 행위로도 확인한다.
|
|
5379
|
+
const keyFile = bin.includes('const isKeyFile =') && bin.includes('|| isEnvFamily || isKeyFile');
|
|
5047
5380
|
const dbVg = /DB connection string[^\n]*valueGroup: 1/.test(cat);
|
|
5048
5381
|
const phPlaceholder = pu.includes('root|admin|user|username|yourpassword');
|
|
5049
5382
|
const retroGuard = bin.includes("failJson(has('--json'), 'invalid_arg'") && bin.includes('Math.min(days, 36500)');
|
|
@@ -5304,8 +5637,12 @@ function _selfTestCases() {
|
|
|
5304
5637
|
// F-07: 단수형 별칭 매핑 존재
|
|
5305
5638
|
const aliasOk = s.includes("({ decision: 'decisions', lesson: 'lessons', plans: 'plan' })[surface]");
|
|
5306
5639
|
// F-02: 비율형 완전-통과 → declaredTestCount 승격 + 신뢰 경계 필드
|
|
5307
|
-
|
|
5308
|
-
|
|
5640
|
+
// 자기참조 제거: 낡은 리터럴('...denom) declaredTestCount = declaredPass.denom')은 블록화 리팩터
|
|
5641
|
+
// ({ declaredTestCount = ...; _ratioPromoted = true; })로 제품 코드에서 사라졌다(가드 자기 줄만 매치).
|
|
5642
|
+
// verifyClaimCmd() 함수 소스만 불변식 정규식으로 검사 — 전부-통과(N/N)만 승격.
|
|
5643
|
+
const _vcSrc = verifyClaimCmd.toString();
|
|
5644
|
+
const ratioOk = /declaredTestCount == null && declaredPass && declaredPass\.num === declaredPass\.denom\)[\s\S]{0,40}?declaredTestCount = declaredPass\.denom/.test(_vcSrc)
|
|
5645
|
+
&& /semanticVerified: false/.test(_vcSrc) && /\? 'executed' : 'static'/.test(_vcSrc);
|
|
5309
5646
|
// F-08: Unix .env 0600
|
|
5310
5647
|
const envOk = /mergeEnvFile[\s\S]{0,600}chmodSync\(p, 0o600\)/.test(s);
|
|
5311
5648
|
return langOk && aliasOk && ratioOk && envOk;
|
|
@@ -12984,7 +13321,12 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
12984
13321
|
// codex 버그헌트 P2: basename 첫 글자를 [A-Za-z] 로만 잡아 '123.js'·'2fa.ts' 같은 숫자시작 실파일이
|
|
12985
13322
|
// 추출 안 돼 존재검사가 vacuous-pass → 없는 파일 인용한 done 주장이 통과(gate FN). 첫 글자에 숫자 허용.
|
|
12986
13323
|
// (prev-separator 가드가 UNC/드라이브/URL 꼬리 오탐은 계속 차단하므로 FP 억제 유지)
|
|
12987
|
-
|
|
13324
|
+
// 1.36.82 (검수 High#2): 추출기와 `_evidenceQuality` 의 "파일 언급" 판정이 각자 따로였다(주석만 "정합").
|
|
13325
|
+
// 같은 명령 안에서 정의가 갈리면 정직한 주장이 사유 없이 거부되므로 **단일 출처**(lib/analyzers)를 쓴다.
|
|
13326
|
+
// 해소된 사각지대: 다단계 경로의 bare 파일(ops/containers/Dockerfile) · 선두 점 디렉토리(.github/workflows/ci.yml
|
|
13327
|
+
// 가 github/… 로 잘리던 것) · 점파일(.gitignore) · Dockerfile.dev 가 Dockerfile 로 잘려 "없는 파일"이 되던 것.
|
|
13328
|
+
void FILE_EXTS; // (구 상수 — 위 단일 출처로 대체. 주변 주석의 이력 보존용)
|
|
13329
|
+
const FILE_RE = require('../lib/analyzers')._EVIDENCE_FILE_RE('g');
|
|
12988
13330
|
// 1.36.11 (클린룸 P1-B): 매치 직전 문자가 경로 구분자면 더 긴 경로의 "꼬리"만 잡은 것(UNC \\\\server\\..., 드라이브 X:\\...,
|
|
12989
13331
|
// 숫자 시작 선두 세그먼트 등) — 잘린 꼬리를 루트-상대 경로로 검증하면 무관한 로컬 파일이 통과(우회성). 그런 매치는 버린다.
|
|
12990
13332
|
const filePatterns = [];
|
|
@@ -13148,14 +13490,32 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
13148
13490
|
// 1.9.309: 기본 임계는 비례적 — 근거 일부(파일·테스트·로그 중 하나)만 있으면 통과(Opus 의 "증거 0" 만 차단).
|
|
13149
13491
|
// --require-evidence(명시) 는 엄격(파일+테스트 모두, 1.9.287). 과탐 방지.
|
|
13150
13492
|
const evidenceQualityOk = !mustHaveEvidence || (has('--require-evidence') ? evq.ok : (evq.hasFile || evq.hasTest || evq.hasLog));
|
|
13151
|
-
//
|
|
13493
|
+
// filesAllExist 는 "주장 파일들이 실제 존재하는가" — 주장 파일이 0건이면 공허참(빈 배열의 every)이다.
|
|
13152
13494
|
const filesAllExist = fileChecks.every(c => c.exists);
|
|
13495
|
+
// 1.36.82 (공허한 가드 스윕 High #1, 실측 재현): **아무것도 대조하지 못한 주장을 통과로 보고하지 않는다.**
|
|
13496
|
+
// 이전 주석은 "공허참은 evidenceQualityOk 가 차단한다"고 적혀 있었으나 전제가 거짓이었다 —
|
|
13497
|
+
// 기본 모드의 evidenceQualityOk 는 hasFile||hasTest||hasLog 라 evidence 에 "tests: 32/32 passed" 문자열
|
|
13498
|
+
// 하나만 있어도 통과한다. 그 결과 파일 0건·테스트 0개 프로젝트에서 done 을 선언하면
|
|
13499
|
+
// filesAllExist(빈배열)=true · implementationSubstance(검사대상 0)=true · gitApplicable(files>0 요구)=false 로
|
|
13500
|
+
// **전 판정이 공허참**이 되어 ok:true/exit 0 이 나왔다.
|
|
13501
|
+
// 단조성이 뒤집혀 있었다: 파일명을 적어 검증 가능하게 쓸수록 FAIL, 아무것도 안 적고 날조할수록 PASS.
|
|
13502
|
+
// 탈출구는 두 가지뿐 — evidence 에 수정 파일 경로를 적거나, --run-tests 로 실제 실행 결과를 붙인다.
|
|
13503
|
+
// 기존 opt-out(--lenient)이 그대로 적용되므로 의도된 예외는 사용자가 되돌릴 수 있다.
|
|
13504
|
+
// (같은 저장소의 `contract verify` 가 이미 빈 계약을 empty_contract 로 거부하는 것과 대칭.)
|
|
13505
|
+
// 계산은 claimsChecked 선언 이후에 둔다(아래) — 여기서 참조하면 TDZ 다.
|
|
13153
13506
|
|
|
13154
13507
|
// 1.11.2 (14th 버그헌트 P1-CRITICAL, UR-0175): 허위완료 차단을 "기본값"에서 작동 — done 주장은 optimism(evidence 주장↔코드 호출 흔적) + git 교차검증을 기본 게이팅.
|
|
13155
13508
|
// 이전엔 --strict-claims 옵트인일 때만 잡아 기본 모드(verify-claim --run-tests)가 허위완료를 통과(exit 0)시켰고, --json 은 git 신호조차 누락(머신 경로가 더 위험)했음.
|
|
13156
13509
|
// 범위: done/완료 주장(또는 --require-evidence/--strict-claims). opt-out: --lenient. --json/human 양 경로가 동일 계산을 공유.
|
|
13157
13510
|
const lenient = has('--lenient');
|
|
13158
13511
|
const claimsChecked = !lenient && (isDoneClaim || has('--require-evidence') || has('--strict-claims'));
|
|
13512
|
+
// 1.36.82 (High #1): "아무것도 대조하지 못함" — 위 주석 참조. 세 경로(collect/--json/human)가 이 하나를 공유한다.
|
|
13513
|
+
// (검수 High#2-d, 실측 재현) 처음엔 `runResult.parsed` 를 요구했는데 그건 **같은 역전을 다시 만든 것**이었다:
|
|
13514
|
+
// `--test-cmd 'node -e "process.exit(0)"'` 처럼 실제로 실행해 exit 0 을 받았어도 출력이 파싱 불가면 차단되고,
|
|
13515
|
+
// 반대로 evidence 에 "1/1 passed" 문자열만 적은 날조는 통과했다. 커스텀 리포터·go test 등 정직한 실행이 벌을 받는다.
|
|
13516
|
+
// 판정 기준은 **실행 확증**이어야 한다 — 실제로 돌았고 실패하지 않았는가(allPassed = exit 0 이며 파싱됐다면 전부 통과).
|
|
13517
|
+
const _runProven = !!(runResult && !runResult.skipped && runResult.allPassed);
|
|
13518
|
+
const unverifiableClaim = claimsChecked && files.length === 0 && !_runProven;
|
|
13159
13519
|
let optimismSuspects = [], honestyFindings = [], strictOk = true;
|
|
13160
13520
|
if (claimsChecked) {
|
|
13161
13521
|
const codeText = _scanCodeForPatterns(root);
|
|
@@ -13183,6 +13543,7 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
13183
13543
|
const _tcMatch = declaredTestCount == null ? null : (!testMeasured ? null : actualTestCount >= declaredTestCount);
|
|
13184
13544
|
const _runFail = !!(runResult && !runResult.skipped && !runResult.allPassed);
|
|
13185
13545
|
const reasons = [];
|
|
13546
|
+
if (unverifiableClaim) reasons.push('unverifiable-claim'); // 1.36.82 (High #1): 대조된 것이 하나도 없음
|
|
13186
13547
|
if (!filesAllExist) reasons.push('files-missing');
|
|
13187
13548
|
if (_tcMatch === false) reasons.push('test-count-short');
|
|
13188
13549
|
if (!evidenceQualityOk) reasons.push('evidence-incomplete');
|
|
@@ -13231,6 +13592,7 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
13231
13592
|
// 1.35.7 (UR-0013 GPT5.5평가 개선②): top-level ok/reasons — CI 가 verdict 필드를 재조합하지 않아도 되도록 collect 경로와 동일 어휘.
|
|
13232
13593
|
// exit 는 reasons 기반 단일 판정(ok↔exit 불일치 원천 차단) — 기존 게이트(1.11.2/1.17.3/1.17.4) 동일 + declared-pass-mismatch 추가.
|
|
13233
13594
|
out.reasons = [];
|
|
13595
|
+
if (unverifiableClaim) out.reasons.push('unverifiable-claim'); // 1.36.82 (High #1)
|
|
13234
13596
|
if (!filesAllExist) out.reasons.push('files-missing');
|
|
13235
13597
|
if (out.verdict.testCountMatch === false) out.reasons.push('test-count-short');
|
|
13236
13598
|
if (!evidenceQualityOk) out.reasons.push('evidence-incomplete');
|
|
@@ -13328,7 +13690,10 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
13328
13690
|
}
|
|
13329
13691
|
// 1.9.309 (UR-0048): done 주장 evidence 완전성 — 기본 강제(상단 pre-compute). --lenient 로 opt-out.
|
|
13330
13692
|
if (mustHaveEvidence) {
|
|
13331
|
-
|
|
13693
|
+
// 1.36.82 (스윕 medium): 기본 모드의 통과 조건은 hasFile||hasTest||hasLog 인데 라벨은 늘 "파일+테스트 근거 있음"이라
|
|
13694
|
+
// hasFile:false 인 같은 실행의 JSON 과 정면 모순하는 **거짓 표기**였다. 실제로 확인된 근거만 나열한다.
|
|
13695
|
+
const _evParts = [evq.hasFile && t('파일 경로', 'file path'), evq.hasTest && t('테스트', 'test'), evq.hasLog && t('실행 로그', 'run log')].filter(Boolean);
|
|
13696
|
+
log(` - ${t('evidence 완전성 (done 기본 강제)', 'evidence completeness (done default)')}: ${evidenceQualityOk ? `✓ pass (${_evParts.join(' + ') || t('근거 없음', 'none')})` : `✗ FAIL ${t(`(누락: ${evq.missing.join(', ')})`, `(missing: ${evq.missing.join(', ')})`)}`}`);
|
|
13332
13697
|
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')}`);
|
|
13333
13698
|
}
|
|
13334
13699
|
// 1.17.3 (UR-0046): 구현 실체(스텁) + 테스트-구현 연결 — Attack C(주석뿐 구현+assert(true)) 차단.
|
|
@@ -13342,7 +13707,12 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
13342
13707
|
} else if (testLinkOk === true && claimsChecked) {
|
|
13343
13708
|
log(` - ${t('테스트-구현 연결', 'test-impl link')}: ${t('✓ pass (테스트가 구현을 참조)', '✓ pass (test references the impl)')}`);
|
|
13344
13709
|
}
|
|
13345
|
-
|
|
13710
|
+
// 1.36.82 (High #1): 대조된 것이 하나도 없으면 "일치"가 아니라 **검증 불가**다 — 통과로 보고하지 않는다.
|
|
13711
|
+
if (unverifiableClaim) {
|
|
13712
|
+
log(` - ${t('검증 가능성', 'verifiability')}: ${t('✗ FAIL — 이 주장에서 실제로 대조된 것이 없습니다(주장 파일 0건 · 실행 확증 없음)', '✗ FAIL — nothing in this claim was actually checked (0 claimed files · no run evidence)')}`);
|
|
13713
|
+
log(` · ${t('evidence 에 수정 파일 경로를 적거나 --run-tests 로 실제 실행 결과를 붙이세요. 의도한 예외면 --lenient.', 'add changed-file paths to evidence, or attach a real run with --run-tests. If intended, use --lenient.')}`);
|
|
13714
|
+
}
|
|
13715
|
+
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
|
|
13346
13716
|
// 1.9.287: 정직한 한계 고지 — 테스트 통과 ≠ 의미적 구현 정확성
|
|
13347
13717
|
if (claimsChecked || mustHaveEvidence) {
|
|
13348
13718
|
log('');
|
|
@@ -15362,7 +15732,9 @@ function gate(root) {
|
|
|
15362
15732
|
// 1.34.1 (16th리뷰 정직화): 기본 5체크(특히 lazy detect)는 워크스페이스-상태(handoff/test-run/evidence 부재) 신호로 거짓완료를 잡지, 콘텐츠(파일/카운트/스텁)는 검사하지 않음.
|
|
15363
15733
|
// --claims 는 콘텐츠-레벨 검증을 추가 — 워크스페이스가 깨끗한 성숙 프로젝트에선 기본 5체크가 통과(exit 0)해도 --claims 만 콘텐츠 거짓을 잡아(exit 1) README 약속("claims fail → cannot merge")을 문자 그대로 강제. (실증 가드: e2e B(1.34.1))
|
|
15364
15734
|
const withClaims = has('--claims');
|
|
15365
|
-
|
|
15735
|
+
const _ref1 = _argAllStrict('--require-referee'); // 1.36.82 (High #2): 안내만 하고 구현이 없던 플래그 — 실제 게이팅
|
|
15736
|
+
const _refIds = _ref1.values, _refMissing = _ref1.missing;
|
|
15737
|
+
if (!jsonMode) log(`# leerness gate (${(withClaims ? 6 : 5) + _refIds.length + _refMissing} checks)`);
|
|
15366
15738
|
function step(label, fn) {
|
|
15367
15739
|
const code0 = process.exitCode || 0;
|
|
15368
15740
|
if (!jsonMode) log(`\n## ${label}`);
|
|
@@ -15373,9 +15745,13 @@ function gate(root) {
|
|
|
15373
15745
|
const failed = threw != null || !!(process.exitCode && process.exitCode !== code0);
|
|
15374
15746
|
if (threw && !jsonMode) fail(`${label} threw: ${threw.message}`);
|
|
15375
15747
|
if (failed) bad++;
|
|
15376
|
-
|
|
15748
|
+
// 1.36.82 (검수 Medium): --json 에서 모든 실패가 {name, ok:false} 로만 축약돼
|
|
15749
|
+
// referee_not_found / referee_stale / timeout / spawn 오류가 구분되지 않았다 — 단계가 남긴 진단을 그대로 싣는다.
|
|
15750
|
+
checks.push({ name: label, ok: !failed, ...(threw ? { error: threw.message } : {}), ..._stepDetail });
|
|
15751
|
+
_stepDetail = {};
|
|
15377
15752
|
process.exitCode = 0;
|
|
15378
15753
|
}
|
|
15754
|
+
let _stepDetail = {}; // step 내부에서 채우면 그 단계의 checks[] 항목에 병합된다
|
|
15379
15755
|
step('verify', () => verify(root));
|
|
15380
15756
|
step('audit', () => audit(root));
|
|
15381
15757
|
step('scan secrets', () => scanSecrets(root));
|
|
@@ -15383,6 +15759,38 @@ function gate(root) {
|
|
|
15383
15759
|
step('lazy detect', () => lazyDetect(root));
|
|
15384
15760
|
// 1.33.3: opt-in 정밀 per-claim 검증. 코어(_verifyClaimsAll)는 exit 안 하고 결과만 반환 → step 의 exitCode 감지로 실패 집계. 비-json 모드는 불일치 task 를 fail() 로 표면화.
|
|
15385
15761
|
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(', ')}`)); } });
|
|
15762
|
+
// 1.36.82 (공허한 가드 스윕 High #2, 실측 확인): `referee verify` 성공 시 툴이 사용자에게
|
|
15763
|
+
// "이제 gate --require-referee <id> 가 이 검증기를 신뢰합니다" 라고 **안내하는데 gate 는 이 플래그를 읽지도 않았다**.
|
|
15764
|
+
// 사용자는 안내받은 명령을 CI 에 넣고 검증기가 머지를 막는다고 믿지만 아무 일도 일어나지 않았고,
|
|
15765
|
+
// 무효/부재 referee 여도 exit 0 이라 오배선을 알아챌 신호조차 없었다 — 안내가 곧 거짓 약속이었다.
|
|
15766
|
+
// 이제 실제로 게이팅한다: 미검증/무효/부재 검증기는 실행 전에 거부하고, 검증기가 현재 상태를 거부하면 게이트가 실패한다.
|
|
15767
|
+
// (검수 High#4) 값 누락은 조용히 버리지 않는다 — CI 의 빈 환경변수가 게이팅을 무력화하는 fail-open 을 막는다.
|
|
15768
|
+
for (let i = 0; i < _refMissing; i++) {
|
|
15769
|
+
step('referee:(값 없음)', () => {
|
|
15770
|
+
process.exitCode = 1;
|
|
15771
|
+
_stepDetail = { code: 'referee_missing_value' };
|
|
15772
|
+
if (!jsonMode) fail('--require-referee 에 검증기 id 가 없습니다 (예: --require-referee build-check). 값이 비면 게이팅이 사라지므로 실패로 처리합니다.');
|
|
15773
|
+
});
|
|
15774
|
+
}
|
|
15775
|
+
// (검수 Low) 같은 id 반복은 같은 명령을 중복 실행할 뿐 — 순서를 보존해 중복만 제거한다.
|
|
15776
|
+
for (const _rid of Array.from(new Set(_refIds))) {
|
|
15777
|
+
step(`referee:${_rid}`, () => {
|
|
15778
|
+
const g = _ref.requireReferee(root, _rid);
|
|
15779
|
+
if (!g.ok) { process.exitCode = 1; _stepDetail = { code: g.code }; if (!jsonMode) fail(`${g.code}: ${g.message}`); return; }
|
|
15780
|
+
// (검수 Medium) 캘리브레이션과 동일한 시간 정책 — referee verify --timeout 으로 늘린 검증기를
|
|
15781
|
+
// gate 에서만 10분에 끊으면 정직한 검증기가 거짓 차단된다. 저장된 값이 있으면 그것을 쓴다.
|
|
15782
|
+
const _to = Math.max(1000, Number(g.referee.timeoutMs) || 600000);
|
|
15783
|
+
const rr = cp.spawnSync(g.referee.check, { cwd: root, shell: true, encoding: 'utf8', timeout: _to, maxBuffer: 8 * 1024 * 1024 });
|
|
15784
|
+
const code = rr.status == null ? 1 : rr.status;
|
|
15785
|
+
const timedOut = !!(rr.error && /ETIMEDOUT|timed? ?out/i.test(String(rr.error.message || rr.error.code || '')));
|
|
15786
|
+
if (code !== 0) {
|
|
15787
|
+
process.exitCode = 1;
|
|
15788
|
+
const why = rr.error ? ` (${rr.error.code || rr.error.message})` : '';
|
|
15789
|
+
_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 } : {}) };
|
|
15790
|
+
if (!jsonMode) fail(`referee ${_rid} 가 현재 상태를 거부(exit ${code})${why}: ${g.referee.check}`);
|
|
15791
|
+
}
|
|
15792
|
+
});
|
|
15793
|
+
}
|
|
15386
15794
|
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; }
|
|
15387
15795
|
log(`\n# gate summary: ${bad} 단계 실패`);
|
|
15388
15796
|
if (bad) process.exitCode = 1;
|