leerness 1.36.78 → 1.36.80
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 +36 -0
- package/README.ko.md +1 -1
- package/README.md +6 -6
- package/bin/leerness.js +173 -15
- package/lib/audit.js +19 -0
- package/lib/catalogs.js +2 -9
- package/lib/clarify.js +4 -0
- package/lib/mcp-tools.js +4 -2
- package/lib/preview-serve.js +254 -0
- package/lib/pure-utils.js +11 -2
- package/lib/referee.js +0 -0
- package/lib/review-request.js +43 -1
- package/lib/tech-profile.js +12 -3
- package/package.json +1 -1
- package/scripts/e2e.js +207 -20
package/scripts/e2e.js
CHANGED
|
@@ -153,12 +153,12 @@ total++;
|
|
|
153
153
|
run('route planning', ['route', 'planning']);
|
|
154
154
|
run('route bugfix', ['route', 'bugfix']);
|
|
155
155
|
run('skill list', ['skill', 'list']);
|
|
156
|
-
run('skill info', ['skill', 'info', '
|
|
156
|
+
run('skill info', ['skill', 'info', 'feature-implementation']);
|
|
157
157
|
|
|
158
158
|
// 1.9.2: 스킬 학습 사이클 회귀
|
|
159
159
|
run('skill learn (new)', ['skill', 'learn', 'open-meteo', '--doc', 'https://open-meteo.com/en/docs', '--command', 'fetch hourly+daily JSON', '--capability', 'http fetch', '--capability', 'cache', '--note', 'e2e learn', '--display', 'Open-Meteo 날씨 스킬', '--path', tmp]);
|
|
160
160
|
run('skill use (new)', ['skill', 'use', 'open-meteo', '--note', 'first call', '--path', tmp]);
|
|
161
|
-
run('skill use (catalog)', ['skill', 'use', '
|
|
161
|
+
run('skill use (catalog)', ['skill', 'use', 'feature-implementation', '--note', 'catalog skill materialize', '--path', tmp]);
|
|
162
162
|
run('skill optimize', ['skill', 'optimize', 'open-meteo', '--before', 'no cache', '--after', 'If-Modified-Since', '--note', 'e2e opt', '--path', tmp]);
|
|
163
163
|
|
|
164
164
|
total++;
|
|
@@ -1183,7 +1183,7 @@ total++;
|
|
|
1183
1183
|
} else {
|
|
1184
1184
|
const jsonR = fn(JSON.stringify({ skills: [{ name: 'a', description: 'A' }] }), null);
|
|
1185
1185
|
const rssR = fn('<rss><channel><item><title>X</title><link>http://x.com/s.md</link></item></channel></rss>', null);
|
|
1186
|
-
const mdR = fn('- [
|
|
1186
|
+
const mdR = fn('- [feature-implementation](f.md) — Feature\n- [project-roadmap-generator](r.md) — Roadmap', null);
|
|
1187
1187
|
const urlR = fn('https://x.com/foo/SKILL.md', null);
|
|
1188
1188
|
const ok = jsonR[0].format === 'json' && rssR[0].format === 'rss'
|
|
1189
1189
|
&& mdR[0].format === 'markdown' && urlR[0].format === 'urls';
|
|
@@ -1234,14 +1234,13 @@ total++;
|
|
|
1234
1234
|
{
|
|
1235
1235
|
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-match-'));
|
|
1236
1236
|
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'all'], { stdio: 'ignore', timeout: 30000 });
|
|
1237
|
-
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'match', '
|
|
1237
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'match', '기능 구현 재사용 검사', '--path', tmpC, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
1238
1238
|
let parsed = null;
|
|
1239
1239
|
try { parsed = JSON.parse(r.stdout); } catch {}
|
|
1240
1240
|
const ok = parsed
|
|
1241
1241
|
&& parsed.top
|
|
1242
|
-
&& parsed.top.length > 0
|
|
1243
|
-
|
|
1244
|
-
console.log(ok ? '✓ B(1.9.45) skill match: jaccard 매칭 → office 최상위' : `✗ skill match 실패`);
|
|
1242
|
+
&& parsed.top.length > 0; // 1.36.80: 내장 카탈로그 축소 — 특정 id 고정 대신 매칭 존재만 단언
|
|
1243
|
+
console.log(ok ? '✓ B(1.9.45) skill match: jaccard 매칭 → 최상위 후보 반환' : `✗ skill match 실패`);
|
|
1245
1244
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1246
1245
|
}
|
|
1247
1246
|
|
|
@@ -1276,7 +1275,7 @@ total++;
|
|
|
1276
1275
|
const ok = r.status === 0
|
|
1277
1276
|
&& fs.existsSync(publishDir)
|
|
1278
1277
|
&& manifest
|
|
1279
|
-
&& manifest.skills && manifest.skills.length >=
|
|
1278
|
+
&& manifest.skills && manifest.skills.length >= 2 // 1.36.80: 내장 카탈로그 축소
|
|
1280
1279
|
&& manifest.format === 'agentskills.io';
|
|
1281
1280
|
console.log(ok ? `✓ B(1.9.47) skill publish: ${manifest ? manifest.skills.length : 0} skill + manifest 생성` : `✗ skill publish 실패`);
|
|
1282
1281
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
@@ -1308,7 +1307,7 @@ total++;
|
|
|
1308
1307
|
const exportDir = path.join(tmpC, '.harness', 'skills-export');
|
|
1309
1308
|
const exists2 = fs.existsSync(exportDir);
|
|
1310
1309
|
const count = exists2 ? fs.readdirSync(exportDir).length : 0;
|
|
1311
|
-
const ok = r.status === 0 && exists2 && count >=
|
|
1310
|
+
const ok = r.status === 0 && exists2 && count >= 2; // 1.36.80: 내장 카탈로그 축소(도메인 7종 제거)
|
|
1312
1311
|
console.log(ok ? `✓ B(1.9.43) skill export-all: ${count}개 skill 일괄 SKILL.md 생성` : `✗ export-all 실패 (count=${count})`);
|
|
1313
1312
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1314
1313
|
}
|
|
@@ -1393,13 +1392,13 @@ total++;
|
|
|
1393
1392
|
// skill export → SKILL.md frontmatter 정확 생성
|
|
1394
1393
|
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-skex-'));
|
|
1395
1394
|
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1396
|
-
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'export', '
|
|
1397
|
-
const skillFile = path.join(tmpC, '.harness', 'skills-export', '
|
|
1395
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'export', 'feature-implementation', '--path', tmpC], { encoding: 'utf8', timeout: 15000 });
|
|
1396
|
+
const skillFile = path.join(tmpC, '.harness', 'skills-export', 'feature-implementation', 'SKILL.md');
|
|
1398
1397
|
const exists2 = fs.existsSync(skillFile);
|
|
1399
1398
|
const body = exists2 ? fs.readFileSync(skillFile, 'utf8') : '';
|
|
1400
1399
|
const ok = r.status === 0
|
|
1401
1400
|
&& exists2
|
|
1402
|
-
&& /^---\nname:
|
|
1401
|
+
&& /^---\nname: feature-implementation\ndescription:/.test(body)
|
|
1403
1402
|
&& /\n---\n/.test(body);
|
|
1404
1403
|
console.log(ok ? '✓ B(1.9.42) skill export: agentskills.io 표준 SKILL.md frontmatter 생성' : `✗ export 실패`);
|
|
1405
1404
|
if (!ok) { failed++; console.log(body.slice(0, 300) || r.stdout.slice(0, 300)); }
|
|
@@ -1899,7 +1898,7 @@ total++;
|
|
|
1899
1898
|
// 비-TTY + --yes 시 multi-select prompt 안 띄움 → 통상 init 흐름
|
|
1900
1899
|
const ok = r.status === 0
|
|
1901
1900
|
&& /Leerness v/.test(r.stdout)
|
|
1902
|
-
&& /Skills:
|
|
1901
|
+
&& /Skills: (feature-implementation|project-roadmap-generator)/.test(r.stdout); // 1.36.80
|
|
1903
1902
|
console.log(ok ? '✓ B(1.9.34) multi-select 비-TTY 폴백: --yes로 default 사용' : `✗ multi-select 폴백 실패`);
|
|
1904
1903
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
1905
1904
|
}
|
|
@@ -2380,7 +2379,7 @@ total++;
|
|
|
2380
2379
|
encoding: 'utf8',
|
|
2381
2380
|
env: Object.assign({}, process.env, { LEERNESS_SKILLPACK_PATH: '' })
|
|
2382
2381
|
});
|
|
2383
|
-
const ok = r.status === 0 && /builtin fallback/.test(r.stdout) && /\| builtin \|/.test(r.stdout);
|
|
2382
|
+
const ok = r.status === 0 && /builtin fallback/.test(r.stdout) && /\| (?:builtin|catalog\+local|catalog) \|/.test(r.stdout);
|
|
2384
2383
|
console.log(ok ? '✓ B(1.9.10) builtin fallback (skillpack 없을 때)' : '✗ builtin fallback 실패');
|
|
2385
2384
|
if (!ok) failed++;
|
|
2386
2385
|
}
|
|
@@ -4401,10 +4400,10 @@ total++;
|
|
|
4401
4400
|
try {
|
|
4402
4401
|
const c = require(path.resolve(__dirname, '..', 'lib', 'catalogs.js'));
|
|
4403
4402
|
const m = require(path.resolve(__dirname, '..', 'lib', 'pure-utils.js'));
|
|
4404
|
-
const catOk = c.BUILTIN_CATALOG && Object.keys(c.BUILTIN_CATALOG).length ===
|
|
4403
|
+
const catOk = c.BUILTIN_CATALOG && Object.keys(c.BUILTIN_CATALOG).length === 2 && c.BUILTIN_CATALOG['feature-implementation'] && c.BUILTIN_CATALOG['feature-implementation'].version === '1.0.0';
|
|
4405
4404
|
const out = m._withBuiltinSource(c.BUILTIN_CATALOG);
|
|
4406
|
-
const work = catOk && typeof m._withBuiltinSource === 'function' && Object.keys(out).length ===
|
|
4407
|
-
&& Object.values(out).every(v => v._source === 'builtin') && Array.isArray(out.
|
|
4405
|
+
const work = catOk && typeof m._withBuiltinSource === 'function' && Object.keys(out).length === 2
|
|
4406
|
+
&& Object.values(out).every(v => v._source === 'builtin') && Array.isArray(out['feature-implementation'].capabilities)
|
|
4408
4407
|
&& Object.keys(m._withBuiltinSource(null)).length === 0;
|
|
4409
4408
|
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4410
4409
|
const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
|
|
@@ -4416,7 +4415,7 @@ total++;
|
|
|
4416
4415
|
cp.spawnSync(process.execPath, [CLI, 'init', sd, '--yes', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
|
|
4417
4416
|
const sr = cp.spawnSync(process.execPath, [CLI, 'skill', 'list', '--path', sd], { encoding: 'utf8', timeout: 20000 });
|
|
4418
4417
|
const listOut = (sr.stdout || '') + (sr.stderr || '');
|
|
4419
|
-
const cmdOk = /
|
|
4418
|
+
const cmdOk = /feature-implementation|roadmap/i.test(listOut);
|
|
4420
4419
|
ok = work && movedOut && cmdOk;
|
|
4421
4420
|
fs.rmSync(sd, { recursive: true, force: true });
|
|
4422
4421
|
} catch {}
|
|
@@ -5094,7 +5093,7 @@ total++;
|
|
|
5094
5093
|
fs.rmSync(d, { recursive: true, force: true });
|
|
5095
5094
|
const d2 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rec-'));
|
|
5096
5095
|
cp.spawnSync(process.execPath, [CLI, 'init', d2, '--yes', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
|
|
5097
|
-
const recInstalled = fs.existsSync(path.join(d2, '.harness', 'skills', '
|
|
5096
|
+
const recInstalled = fs.existsSync(path.join(d2, '.harness', 'skills', 'feature-implementation')); // recommended → office 포함
|
|
5098
5097
|
fs.rmSync(d2, { recursive: true, force: true });
|
|
5099
5098
|
ok = skipAbsent && corePresent && recInstalled;
|
|
5100
5099
|
} catch {}
|
|
@@ -7317,7 +7316,7 @@ total++;
|
|
|
7317
7316
|
const i2 = cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'en', '--skills', 'recommended', '--no-stale-check', '--no-env'], { encoding: 'utf8', timeout: 40000 });
|
|
7318
7317
|
const dirSet = fs.readdirSync(path.join(d, '.harness', 'skills'), { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
7319
7318
|
const lockSet = Object.keys(JSON.parse(fs.readFileSync(path.join(d, '.harness', 'skills-lock.json'), 'utf8')).installedSkills).sort();
|
|
7320
|
-
const f12Ok = i1.status === 0 && i2.status === 0 && dirSet.length
|
|
7319
|
+
const f12Ok = i1.status === 0 && i2.status === 0 && dirSet.length >= 2 && JSON.stringify(dirSet) === JSON.stringify(lockSet); // 1.36.80: 내장 축소 — 개수 하한 대신 집합 일치가 본질
|
|
7321
7320
|
// F9: 실 CLI migrate 경유 — 구판 KO canonical 을 en 프로젝트 CLAUDE.md 에 심고 migrate
|
|
7322
7321
|
const d9 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-f9-')); _d.push(d9);
|
|
7323
7322
|
const i3 = cp.spawnSync(process.execPath, [CLI, 'init', d9, '--yes', '--language', 'en', '--no-stale-check', '--no-env'], { encoding: 'utf8', timeout: 40000 });
|
|
@@ -7619,5 +7618,193 @@ total++;
|
|
|
7619
7618
|
if (!ok) failed++;
|
|
7620
7619
|
}
|
|
7621
7620
|
|
|
7621
|
+
// 1.36.79 (실 프로젝트 60개 도그푸딩 P1 5건): progress 행 보존 · handoff --json 순수 · parent path · tech 언어 순위 · audit=integrity 정합
|
|
7622
|
+
total++;
|
|
7623
|
+
{
|
|
7624
|
+
let ok = false;
|
|
7625
|
+
const _d = [];
|
|
7626
|
+
try {
|
|
7627
|
+
// P1-A: 비-T/M/D 행(R-/UR-)이 쓰기 경로에서 소실되면 안 된다 + 클래스 가드
|
|
7628
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-a-')); _d.push(d);
|
|
7629
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7630
|
+
const pt = path.join(d, '.harness', 'progress-tracker.md');
|
|
7631
|
+
fs.writeFileSync(pt, fs.readFileSync(pt, 'utf8').trimEnd() + '\n| R-0001 | done | 사용자 이력 | 증거 | - | 2026-05-07 |\n| UR-0002 | done | 다른 요청 | 증거2 | - | 2026-05-07 |\n');
|
|
7632
|
+
const rowsOf = () => (fs.readFileSync(pt, 'utf8').match(/^\|\s*[A-Z]{1,3}-\d{3,}\s*\|/gm) || []).length;
|
|
7633
|
+
const beforeRows = rowsOf();
|
|
7634
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'brand new', '--path', d], { encoding: 'utf8', timeout: 20000 });
|
|
7635
|
+
const rowOk = rowsOf() === beforeRows + 1
|
|
7636
|
+
&& fs.readFileSync(pt, 'utf8').includes('R-0001') && fs.readFileSync(pt, 'utf8').includes('UR-0002');
|
|
7637
|
+
// 클래스 가드: 파서가 못 읽는 행-모양 라인(셀 부족)이 있으면 쓰기 중단 + 원본 보존 (자체 재설계 후 실동작)
|
|
7638
|
+
const dG = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-a2-')); _d.push(dG);
|
|
7639
|
+
cp.spawnSync(process.execPath, [CLI, 'init', dG, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7640
|
+
const ptG = path.join(dG, '.harness', 'progress-tracker.md');
|
|
7641
|
+
fs.appendFileSync(ptG, '| R-0007 | done | 셀부족행 |\n');
|
|
7642
|
+
const beforeG = fs.readFileSync(ptG, 'utf8');
|
|
7643
|
+
const guardR = cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'x', '--path', dG], { encoding: 'utf8', timeout: 20000 });
|
|
7644
|
+
const guardOk = guardR.status === 1 && fs.readFileSync(ptG, 'utf8') === beforeG;
|
|
7645
|
+
// (검수 #2) 무관한 참조표는 task 로 흡수되지 않고, 표 뒤 사용자 콘텐츠는 재작성에도 보존
|
|
7646
|
+
const dS = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-a3-')); _d.push(dS);
|
|
7647
|
+
cp.spawnSync(process.execPath, [CLI, 'init', dS, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7648
|
+
const ptS = path.join(dS, '.harness', 'progress-tracker.md');
|
|
7649
|
+
fs.appendFileSync(ptS, '\n## 참고표\n\n| Code | Owner | Note |\n|---|---|---|\n| API-123 | owner | 무관 참조 |\n');
|
|
7650
|
+
let absorbed = true;
|
|
7651
|
+
try { absorbed = (JSON.parse(cp.spawnSync(process.execPath, [CLI, 'task', 'list', '--path', dS, '--json'], { encoding: 'utf8', timeout: 20000 }).stdout).tasks || []).some(t => t.id === 'API-123'); } catch {}
|
|
7652
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'x', '--path', dS], { encoding: 'utf8', timeout: 20000 });
|
|
7653
|
+
const suffixOk = !absorbed && fs.readFileSync(ptS, 'utf8').includes('| API-123 | owner |') && fs.readFileSync(ptS, 'utf8').includes('## 참고표');
|
|
7654
|
+
// P1-B: stale 프로젝트에서도 handoff --json 은 순수 JSON (사람용 drift 배너 미오염)
|
|
7655
|
+
const d2 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-b-')); _d.push(d2);
|
|
7656
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d2, '--yes', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7657
|
+
const hp = path.join(d2, '.harness', 'session-handoff.md');
|
|
7658
|
+
if (fs.existsSync(hp)) fs.writeFileSync(hp, fs.readFileSync(hp, 'utf8').replace(/Last generated:.*/, 'Last generated: 2026-01-01T00:00:00.000Z'));
|
|
7659
|
+
const pt2 = path.join(d2, '.harness', 'progress-tracker.md');
|
|
7660
|
+
if (fs.existsSync(pt2)) fs.writeFileSync(pt2, fs.readFileSync(pt2, 'utf8').replace(/2026-\d\d-\d\d/g, '2026-01-01'));
|
|
7661
|
+
const hj = cp.spawnSync(process.execPath, [CLI, 'handoff', d2, '--json'], { encoding: 'utf8', timeout: 40000, maxBuffer: 32 * 1024 * 1024 });
|
|
7662
|
+
let jsonPure = false; try { JSON.parse(hj.stdout); jsonPure = true; } catch {}
|
|
7663
|
+
// 텍스트 모드에서는 배너 유지(무회귀)
|
|
7664
|
+
const ht = cp.spawnSync(process.execPath, [CLI, 'handoff', d2], { encoding: 'utf8', timeout: 40000, maxBuffer: 32 * 1024 * 1024 });
|
|
7665
|
+
const bannerKept = /drift 감지|drift detected/.test(ht.stdout);
|
|
7666
|
+
// P1-C: parent detect 가 positional path 를 존중
|
|
7667
|
+
const d3 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-c-')); _d.push(d3);
|
|
7668
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d3, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7669
|
+
let parentOk = false;
|
|
7670
|
+
try { const j = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'parent', 'detect', d3, '--json'], { encoding: 'utf8', timeout: 20000, cwd: path.dirname(path.dirname(CLI)) }).stdout); parentOk = String(j.root || j.child || '').includes(path.basename(d3)); } catch {}
|
|
7671
|
+
// P1-D: 소스 파일 수가 마커 파일보다 우선 (Python 모노레포 오라벨 방지) + 진짜 JS 무회귀
|
|
7672
|
+
const d4 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-d-')); _d.push(d4);
|
|
7673
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d4, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7674
|
+
fs.mkdirSync(path.join(d4, 'src'), { recursive: true });
|
|
7675
|
+
for (let i = 0; i < 30; i++) fs.writeFileSync(path.join(d4, 'src', `m${i}.py`), 'def f():\n return 1\n');
|
|
7676
|
+
fs.writeFileSync(path.join(d4, 'requirements.txt'), 'requests\n');
|
|
7677
|
+
fs.writeFileSync(path.join(d4, 'package.json'), JSON.stringify({ name: 'x', devDependencies: { tailwindcss: '^3' } }));
|
|
7678
|
+
let techOk = false;
|
|
7679
|
+
try { techOk = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'tech', '--json', '--path', d4], { encoding: 'utf8', timeout: 25000 }).stdout).current.languages[0].id === 'python'; } catch {}
|
|
7680
|
+
const d5 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-d2-')); _d.push(d5);
|
|
7681
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d5, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7682
|
+
fs.mkdirSync(path.join(d5, 'src'), { recursive: true });
|
|
7683
|
+
for (let i = 0; i < 20; i++) fs.writeFileSync(path.join(d5, 'src', `m${i}.js`), 'export const a = 1;\n');
|
|
7684
|
+
fs.writeFileSync(path.join(d5, 'package.json'), JSON.stringify({ name: 'js', dependencies: { react: '^18' } }));
|
|
7685
|
+
let jsOk = false;
|
|
7686
|
+
try { jsOk = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'tech', '--json', '--path', d5], { encoding: 'utf8', timeout: 25000 }).stdout).current.languages[0].id === 'javascript'; } catch {}
|
|
7687
|
+
// P1-E: 코어 문서가 대량 사라진 설치를 audit 이 healthy 로 보고하면 안 된다(integrity 와 정합)
|
|
7688
|
+
const d6 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-e-')); _d.push(d6);
|
|
7689
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d6, '--yes', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7690
|
+
const hd = path.join(d6, '.harness');
|
|
7691
|
+
const keep = new Set(['progress-tracker.md', 'HARNESS_VERSION', 'plan.md']);
|
|
7692
|
+
for (const f of fs.readdirSync(hd)) { if (!keep.has(f)) { try { fs.rmSync(path.join(hd, f), { recursive: true, force: true }); } catch {} } }
|
|
7693
|
+
let auditOk = false;
|
|
7694
|
+
try {
|
|
7695
|
+
const au = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'audit', d6, '--json'], { encoding: 'utf8', timeout: 30000 }).stdout);
|
|
7696
|
+
const ic = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'integrity', 'check', d6, '--json'], { encoding: 'utf8', timeout: 30000 }).stdout);
|
|
7697
|
+
auditOk = au.healthy === false && au.failures >= 1 && ic.ok === false; // 두 명령 판정 일치
|
|
7698
|
+
} catch {}
|
|
7699
|
+
// (검수 #3) parent 없는 경로 거부
|
|
7700
|
+
const parentBadOk = cp.spawnSync(process.execPath, [CLI, 'parent', 'detect', 'C:/no/such/dir', '--json'], { encoding: 'utf8', timeout: 20000 }).status === 1;
|
|
7701
|
+
ok = rowOk && guardOk && suffixOk && jsonPure && bannerKept && parentOk && parentBadOk && techOk && jsOk && auditOk;
|
|
7702
|
+
if (!ok) console.log(` [dogfood 디버그] row=${rowOk} guard=${guardOk} suffix=${suffixOk} json=${jsonPure} banner=${bannerKept} parent=${parentOk} parentBad=${parentBadOk} tech=${techOk} js=${jsOk} audit=${auditOk}`);
|
|
7703
|
+
} catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
|
|
7704
|
+
console.log(ok ? '✓ B(1.36.79) 도그푸딩 P1×5: progress 행 보존 · handoff --json 순수(텍스트 배너 유지) · parent path · tech 파일수 우선 · audit=integrity 정합' : '✗ 도그푸딩 P1 수정 실패');
|
|
7705
|
+
if (!ok) failed++;
|
|
7706
|
+
}
|
|
7707
|
+
|
|
7708
|
+
// 1.36.80 (P-0001 검증기 캘리브레이션): 검증기를 신뢰하기 전에 탐지력을 실행으로 증명 — 적대적 FP 프로브 포함
|
|
7709
|
+
total++;
|
|
7710
|
+
{
|
|
7711
|
+
let ok = false;
|
|
7712
|
+
const _d = [];
|
|
7713
|
+
try {
|
|
7714
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-ref-')); _d.push(d);
|
|
7715
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7716
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { encoding: 'utf8', timeout: 60000, maxBuffer: 16 * 1024 * 1024 });
|
|
7717
|
+
const J = (r) => { try { return JSON.parse(r.stdout); } catch { return null; } };
|
|
7718
|
+
const N = (js) => `node -e "${js}"`;
|
|
7719
|
+
const add = (id, check, good, bad, expect) => { const a = ['referee', 'add', id, '--check', check, '--good', good, '--bad', bad]; if (expect) a.push('--expect-bad', expect); return R(a); };
|
|
7720
|
+
const verify = (id) => J(R(['referee', 'verify', id, '--json']));
|
|
7721
|
+
// 정상: good 통과 + bad 를 기대 사유로 거부 → 캘리브레이션 통과
|
|
7722
|
+
add('okref', N('process.exit(0)'), N('process.exit(0)'), N("console.log('AssertionError: x'); process.exit(1)"), 'AssertionError');
|
|
7723
|
+
const good = verify('okref');
|
|
7724
|
+
const happyOk = good && good.ok === true && good.goodExit === 0 && good.badExit === 1 && good.expectMatched === true;
|
|
7725
|
+
// FP 프로브 ①: known-good 이 이미 빨간 상태 → 거부(기준선 무효)
|
|
7726
|
+
add('redbase', N('process.exit(0)'), N('process.exit(3)'), N('process.exit(1)'), '');
|
|
7727
|
+
const p1 = verify('redbase');
|
|
7728
|
+
// FP 프로브 ②: known-bad 가 통과 → 탐지력 없음 거부
|
|
7729
|
+
add('nopower', N('process.exit(0)'), N('process.exit(0)'), N('process.exit(0)'), '');
|
|
7730
|
+
const p2 = verify('nopower');
|
|
7731
|
+
// FP 프로브 ③: bad 가 무관한 이유로 실패 → 사유 불일치 거부
|
|
7732
|
+
add('wrongreason', N('process.exit(0)'), N('process.exit(0)'), N("console.log('Error: cannot find module'); process.exit(1)"), 'AssertionError');
|
|
7733
|
+
const p3 = verify('wrongreason');
|
|
7734
|
+
const probesOk = p1 && p1.ok === false && p2 && p2.ok === false && p3 && p3.ok === false && p3.expectMatched === false;
|
|
7735
|
+
// 지문(stale): 캘리브레이션 후 명령이 바뀌면 신뢰 철회
|
|
7736
|
+
const store = path.join(d, '.harness', 'referees.json');
|
|
7737
|
+
const js = JSON.parse(fs.readFileSync(store, 'utf8'));
|
|
7738
|
+
js.find(x => x.id === 'okref').bad = N('process.exit(1)');
|
|
7739
|
+
fs.writeFileSync(store, JSON.stringify(js, null, 2));
|
|
7740
|
+
const shown = J(R(['referee', 'show', 'okref', '--json']));
|
|
7741
|
+
const staleOk = shown && shown.stale === true && shown.calibrated === false;
|
|
7742
|
+
// verify-claim 게이팅: 미검증/없는 referee 는 거부, 검증기 실패도 거부
|
|
7743
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'add', '작업', '--path', d], { encoding: 'utf8', timeout: 20000 });
|
|
7744
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'update', 'T-0002', '--status', 'done', '--evidence', 'lib/referee.js 구현', '--path', d], { encoding: 'utf8', timeout: 20000 });
|
|
7745
|
+
const missing = R(['verify-claim', 'T-0002', '--referee', 'nope', '--json']);
|
|
7746
|
+
const staleUse = R(['verify-claim', 'T-0002', '--referee', 'okref', '--json']); // 위에서 stale 로 만든 것
|
|
7747
|
+
add('failing', N('process.exit(2)'), N('process.exit(0)'), N("console.log('AssertionError'); process.exit(1)"), 'AssertionError');
|
|
7748
|
+
verify('failing');
|
|
7749
|
+
const refFail = R(['verify-claim', 'T-0002', '--referee', 'failing', '--json']);
|
|
7750
|
+
const gateOk = missing.status === 1 && (J(missing) || {}).code === 'referee_not_found'
|
|
7751
|
+
&& staleUse.status === 1 && (J(staleUse) || {}).code === 'referee_stale'
|
|
7752
|
+
&& refFail.status === 1 && (J(refFail) || {}).code === 'referee_failed';
|
|
7753
|
+
// 스토어 규율: 손상 위 변경 거부 + --json 순수
|
|
7754
|
+
const listPure = (() => { try { JSON.parse(R(['referee', 'list', '--json']).stdout); return true; } catch { return false; } })();
|
|
7755
|
+
fs.writeFileSync(store, '{ broken');
|
|
7756
|
+
const corrupt = R(['referee', 'add', 'x9', '--check', 'a', '--good', 'b', '--bad', 'c', '--json']);
|
|
7757
|
+
const storeOk = listPure && corrupt.status === 1 && (J(corrupt) || {}).code === 'store_invalid';
|
|
7758
|
+
ok = happyOk && probesOk && staleOk && gateOk && storeOk;
|
|
7759
|
+
if (!ok) console.log(` [referee 디버그] happy=${happyOk} probes=${probesOk} stale=${staleOk} gate=${gateOk} store=${storeOk}`);
|
|
7760
|
+
} catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
|
|
7761
|
+
console.log(ok ? '✓ B(1.36.80) referee 캘리브레이션: 탐지력 증명(good통과+bad를 기대사유로 거부) · FP프로브 3종 거부 · 명령변경 stale · verify-claim 게이팅 · 스토어 규율' : '✗ referee 캘리브레이션 실패');
|
|
7762
|
+
if (!ok) failed++;
|
|
7763
|
+
}
|
|
7764
|
+
|
|
7765
|
+
// 1.36.80 (UR-0067 라이브 미리보기 + P-0003 사다리 + 기본 스킬 정리)
|
|
7766
|
+
total++;
|
|
7767
|
+
{
|
|
7768
|
+
let ok = false;
|
|
7769
|
+
const _d = [];
|
|
7770
|
+
try {
|
|
7771
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-pv80-')); _d.push(d);
|
|
7772
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7773
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { encoding: 'utf8', timeout: 30000, maxBuffer: 16 * 1024 * 1024 });
|
|
7774
|
+
const J = (r) => { try { return JSON.parse(r.stdout); } catch { return null; } };
|
|
7775
|
+
// 기본 스킬: 도메인 7종 제거 · 남은 2종 · 근거 없는 'passed' 주장 금지
|
|
7776
|
+
const CAT = require(path.resolve(path.dirname(CLI), '..', 'lib', 'catalogs.js')).BUILTIN_CATALOG;
|
|
7777
|
+
const skillOk = Object.keys(CAT).length === 2 && !CAT['commerce-api'] && CAT['feature-implementation']
|
|
7778
|
+
&& Object.values(CAT).every(v => v.verification !== 'passed');
|
|
7779
|
+
// 기존 설치분 보존(비파괴): 카탈로그에서 빠진 스킬도 락에 남는다
|
|
7780
|
+
fs.mkdirSync(path.join(d, '.harness', 'skills', 'commerce-api'), { recursive: true });
|
|
7781
|
+
fs.writeFileSync(path.join(d, '.harness', 'skills', 'commerce-api', 'skill.json'), '{"name":"commerce-api"}');
|
|
7782
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--skills', 'recommended', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7783
|
+
let keepOk = false;
|
|
7784
|
+
try { keepOk = Object.keys(JSON.parse(fs.readFileSync(path.join(d, '.harness', 'skills-lock.json'), 'utf8')).installedSkills).includes('commerce-api') && fs.existsSync(path.join(d, '.harness', 'skills', 'commerce-api')); } catch {}
|
|
7785
|
+
// 라이브 미리보기: mode 미설정 → 질문 노출, self 기본
|
|
7786
|
+
R(['preview', 'add', '새 페이지', '--design', '밝게']);
|
|
7787
|
+
R(['preview', 'mockup', 'P-0001']);
|
|
7788
|
+
const modeShow = R(['preview', 'mode']);
|
|
7789
|
+
const modeOk = /미리보기 모드가 아직/.test(modeShow.stdout) && (J(R(['preview', 'mode', '--json'])) || {}).effective === 'self';
|
|
7790
|
+
// project 모드(옵트인): 정적 디렉토리 배치 + 루트 밖 거부
|
|
7791
|
+
R(['preview', 'mode', 'project', '--static-dir', 'public', '--dev-url', 'http://localhost:5173']);
|
|
7792
|
+
const proj = J(R(['preview', 'serve', 'P-0001', '--json']));
|
|
7793
|
+
const projOk = proj && proj.mode === 'project' && /public\/leerness-preview-P-0001\.html/.test(proj.file || '')
|
|
7794
|
+
&& fs.existsSync(path.join(d, 'public', 'leerness-preview-P-0001.html'))
|
|
7795
|
+
&& R(['preview', 'serve', 'P-0001', '--static-dir', '../outside', '--json']).status === 1;
|
|
7796
|
+
// 사다리: 옵트인 + proceed 무영향 + 근거 없으면 unknown
|
|
7797
|
+
const base = J(R(['review-request', '테스트 요청', '--json']));
|
|
7798
|
+
const lad = J(R(['review-request', '테스트 요청', '--ladder', '--json']));
|
|
7799
|
+
const ladderOk = base && base.ladder === undefined && lad && lad.ladder && lad.ladder.rungs.length === 7
|
|
7800
|
+
&& lad.ladder.rungs.every(r => ['confirmed', 'candidate', 'unknown', 'not-applicable'].includes(r.status))
|
|
7801
|
+
&& lad.proceed === base.proceed && Array.isArray(lad.ladder.preserved) && lad.ladder.preserved.length >= 4;
|
|
7802
|
+
ok = skillOk && keepOk && modeOk && projOk && ladderOk;
|
|
7803
|
+
if (!ok) console.log(` [pv80 디버그] skill=${skillOk} keep=${keepOk} mode=${modeOk} proj=${projOk} ladder=${ladderOk}`);
|
|
7804
|
+
} catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
|
|
7805
|
+
console.log(ok ? '✓ B(1.36.80) 기본스킬 정리(도메인7 제거·passed 주장 금지·기존설치 보존) · 라이브 미리보기 mode(self기본/project옵트인·루트밖 거부) · 사다리 옵트인(proceed 무영향)' : '✗ 1.36.80 미리보기/스킬/사다리 실패');
|
|
7806
|
+
if (!ok) failed++;
|
|
7807
|
+
}
|
|
7808
|
+
|
|
7622
7809
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
7623
7810
|
if (failed > 0) process.exit(1);
|