leerness 1.36.99 → 1.36.100
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 +14 -0
- package/README.md +4 -4
- package/bin/leerness.js +2 -2
- package/lib/clarify.js +82 -2
- package/package.json +1 -1
- package/scripts/e2e.js +53 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.100 — 2026-08-05 — 승인된 시안을 계약으로 고정하고, 구현이 지켰는지 기계로 대조한다 (P-0012 후반)
|
|
4
|
+
|
|
5
|
+
사용자가 고른 방식의 후반부다: *"승인 시 그대로 적용"* 을 **"시안을 계약으로 고정 + 구현 후 자동 대조"** 로 구현한다.
|
|
6
|
+
|
|
7
|
+
프레임워크 프로젝트에서 렌더된 마크업을 컴포넌트 소스로 되돌릴 수는 없다. 그래서 **"자동 적용" 이라고 부르지 않는다.** 대신 시안이 약속한 것을 저장해 두고, 구현이 끝난 뒤 그 약속이 지켜졌는지 센다.
|
|
8
|
+
|
|
9
|
+
- 시안에 `<!-- leerness:draw-start -->` / `draw-end` 마커를 넣었다. AI 는 그 **사이만** 그리고, 승인 시 그 구간의 클래스·`var(--토큰)`·요소가 계약으로 고정된다(스캐폴드 자신의 장식은 계약에서 제외).
|
|
10
|
+
- `leerness preview verify <P-ID> --files a,b` — 구현 파일에서 계약된 클래스·토큰이 나타나는지 대조하고 **누락 목록과 존재율**을 낸다. 실측: 계약을 무시한 구현 8% · 계약대로 구현 100%.
|
|
11
|
+
- 아직 안 그린 시안을 승인하면 *"계약이 비어 대조가 무의미합니다"* 라고 말한다 — 빈 계약을 조용히 만들지 않는다.
|
|
12
|
+
|
|
13
|
+
**한계를 매번 말한다.** 이건 **존재 검사**지 시각적 일치 검사가 아니다. 클래스가 있다고 같아 보이는 건 아니다 — 그건 사람이 시안과 화면을 나란히 봐야 한다. 이 구분을 흐리면 1.36.5 에서 고쳤던 과장(휴리스틱을 측정된 판정처럼 광고)을 그대로 되풀이하게 된다. 그래서 출력·JSON·도움말 모두에 `presence-only` 를 박았고, e2e 가 그 문구의 존재까지 단언한다.
|
|
14
|
+
|
|
15
|
+
구현 중 판정 하나가 틀렸다. "아직 안 그렸다" 를 **글자 수**로 봤는데 도구가 넣어 둔 안내 문구만으로도 120자를 넘어 경고가 안 떴다 — 손대지 않았다는 확실한 신호는 길이가 아니라 **그 자리에 placeholder 가 그대로 있다는 것**이었다.
|
|
16
|
+
|
|
3
17
|
## 1.36.99 — 2026-08-05 — 시안을 빈 종이가 아니라 이 앱의 재료로 시작시킨다 (P-0012)
|
|
4
18
|
|
|
5
19
|
사용자 지적: *"mockup.html 로 미리보기 시안의 정확도가 높지 않은데 — 개발하는 프로젝트 언어 등을 참고해서, 수정하려는 페이지나 요소를 복제해 높은 정확도의 시안을 보여줄 수 있나?"*
|
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ MIT
|
|
|
122
122
|
<!-- leerness:project-readme:start -->
|
|
123
123
|
## Leerness Project Harness
|
|
124
124
|
|
|
125
|
-
이 프로젝트는 Leerness v1.36.
|
|
125
|
+
이 프로젝트는 Leerness v1.36.100 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
126
126
|
|
|
127
127
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
128
128
|
|
|
@@ -176,7 +176,7 @@ leerness memory restore decision <date|title>
|
|
|
176
176
|
|
|
177
177
|
### MCP server (외부 AI 통합)
|
|
178
178
|
|
|
179
|
-
Leerness v1.36.
|
|
179
|
+
Leerness v1.36.100는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
|
|
180
180
|
|
|
181
181
|
```jsonc
|
|
182
182
|
// 카테고리별
|
|
@@ -197,7 +197,7 @@ Leerness v1.36.99는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
197
197
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
198
198
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
199
199
|
|
|
200
|
-
현재 누적: **v1.9.x → 1.36.
|
|
200
|
+
현재 누적: **v1.9.x → 1.36.100 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
201
201
|
|
|
202
202
|
### 성능 가이드
|
|
203
203
|
|
|
@@ -235,5 +235,5 @@ leerness release pack --close --auto-main-push
|
|
|
235
235
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
236
236
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
237
237
|
|
|
238
|
-
Last synced by Leerness v1.36.
|
|
238
|
+
Last synced by Leerness v1.36.100: 2026-08-05
|
|
239
239
|
<!-- leerness:project-readme:end -->
|
package/bin/leerness.js
CHANGED
|
@@ -34,7 +34,7 @@ const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE
|
|
|
34
34
|
const { tokenizeForRank: _tokenizeForRank, expandQuery: _expandQuery, scoreHits: _scoreHits, suggestTerms: _suggestTerms } = require('../lib/search-core'); // 1.36.23: memory search 랭킹 코어(순수·0-deps)
|
|
35
35
|
const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
|
|
36
36
|
|
|
37
|
-
const VERSION = '1.36.
|
|
37
|
+
const VERSION = '1.36.100';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -8033,7 +8033,7 @@ function commandsCmd(root) {
|
|
|
8033
8033
|
{ cmd: 'tech [--json]', desc: '기술 프로필 — 개발 언어·연결 서비스 자동 감지 + 마이그레이션/언어전환 이력, 그래프 🛠 탭 표시 — 1.36.53 UR-0062' },
|
|
8034
8034
|
{ cmd: 'integrity check [--repair] [--json]', desc: 'managed 정책-문서 12종 무결성(부재/H1 상실/절단) 점검 — --repair: archive 대피 후 템플릿 재생성 — 1.36.57 감사 F-04' },
|
|
8035
8035
|
{ cmd: 'referee add|verify|list|show|drop', desc: '검증기 캘리브레이션 — 신뢰 전에 탐지력 증명(known-good 통과 + 망가뜨린 known-bad 를 기대 사유로 거부). verify-claim --referee / gate 게이팅 — 1.36.80 P-0001' },
|
|
8036
|
-
{ cmd: 'preview add|list|show|approve|revise|mockup', desc: '신규 기능 미리보기 승인 워크플로 — approve 전 코드 작성 금지 계약. mockup <P-ID> [--force]:
|
|
8036
|
+
{ cmd: 'preview add|list|show|approve|revise|mockup|verify', desc: '신규 기능 미리보기 승인 워크플로 — approve 전 코드 작성 금지 계약. mockup <P-ID> [--target <파일>] [--force]: 프로젝트의 실제 토큰·컴포넌트·대상 화면 클래스를 심은 시안 · approve 시 그 시안이 계약으로 고정됨 · verify <P-ID> --files a,b: 구현이 계약을 지켰는지 존재 대조(시각적 일치는 보증하지 않음) — 1.36.51 UR-0061 · 1.36.75 UR-0066 · 1.36.99/1.36.100 P-0012' },
|
|
8037
8037
|
{ cmd: 'dashboard [path] [--port N] [--timeout SEC] [--json]', desc: '.harness 읽기 전용 대시보드 — 토글·task·마일스톤·미리보기·라우팅기록·bugfix probe 를 localhost 에서 열람. 설정 변경·게이트 판정은 하지 않는다(CLI 가 판정). --json 은 서버 없이 같은 스냅샷 출력. 정적 단일파일 뷰(leerness.html)는 leerness graph --html 로 그대로 — 1.36.90 P-0006' },
|
|
8038
8038
|
{ cmd: 'agents route "<작업>" [--tier tiny|normal|high-risk] [--confirm --approved-by "<승인자>"] [--log]', desc: '작업 난이도 판정(결정적 규칙) → 추상 역할(architect/commander/coder/reviewer) 배치 제안. 모델명 하드코딩 없이 `roles set` 매핑을 쓰고, 실행은 명시 확인 필요. 토글 기본 OFF: leerness toggle set difficulty-routing on — 1.36.89 P-0007' },
|
|
8039
8039
|
{ cmd: 'bugfix start|receipt|drop|list', desc: 'bugfix 완료 영수증 — 수정 전 재현 probe 를 등록(2회 연속 실패해야 등록)하고, done 전이에서 통과 + 근본원인/형제범위 영수증을 요구. 토글 기본 OFF: leerness toggle set bugfix-receipt on — 1.36.87 P-0005' },
|
package/lib/clarify.js
CHANGED
|
@@ -128,6 +128,30 @@ function _targetSkeleton(root, rel) {
|
|
|
128
128
|
while ((m = TAG.exec(txt))) { const t = m[1]; if (!tseen.has(t)) { tseen.add(t); tags.push(t); } }
|
|
129
129
|
return { file: rel, classes: classes.slice(0, 40), tags: tags.slice(0, 40), lines: txt.split('\n').length };
|
|
130
130
|
}
|
|
131
|
+
// 1.36.100 (P-0012 후반): 승인된 시안을 '계약' 으로 고정한다.
|
|
132
|
+
// 프레임워크 프로젝트에서 렌더된 마크업을 컴포넌트 소스로 되돌릴 수는 없다 — 그래서 "자동 적용" 이라 부르지 않는다.
|
|
133
|
+
// 대신 시안이 약속한 **클래스와 토큰**을 저장해 두고, 구현이 끝난 뒤 그 약속이 지켜졌는지 기계로 대조한다.
|
|
134
|
+
// 한계는 분명히 적는다: 이건 '존재' 검사지 '시각적 일치' 검사가 아니다(있다고 해서 같아 보이는 건 아니다).
|
|
135
|
+
const DRAW_START = 'leerness:draw-start';
|
|
136
|
+
const DRAW_END = 'leerness:draw-end';
|
|
137
|
+
function _extractContract(html) {
|
|
138
|
+
const s = String(html || '');
|
|
139
|
+
const a = s.indexOf(DRAW_START), b = s.indexOf(DRAW_END);
|
|
140
|
+
if (a < 0 || b < 0 || b < a) return { ok: false, reason: 'draw 마커를 찾지 못함 — 시안에서 마커를 지웠거나 구버전 시안입니다' };
|
|
141
|
+
const area = s.slice(s.indexOf('-->', a) + 3, b);
|
|
142
|
+
const classes = new Set(); const tokens = new Set(); const tags = new Set();
|
|
143
|
+
let m;
|
|
144
|
+
const CLS = /class\s*=\s*["']([^"']{1,400})["']/g;
|
|
145
|
+
while ((m = CLS.exec(area))) for (const c of m[1].split(/\s+/)) if (c && !/^(?:placeholder|mock|notes|chips|sw|stack|meta)$/.test(c)) classes.add(c);
|
|
146
|
+
const TOK = /var\(\s*(--[\w-]+)\s*\)/g;
|
|
147
|
+
while ((m = TOK.exec(area))) tokens.add(m[1]);
|
|
148
|
+
const TAG = /<([a-zA-Z][\w-]*)/g;
|
|
149
|
+
while ((m = TAG.exec(area))) tags.add(m[1].toLowerCase());
|
|
150
|
+
// '아직 안 그렸다' 는 길이로 판정하면 안 된다 — 도구가 넣어 둔 안내 문구만으로도 120자를 넘는다(실측에서 이 판정이 틀렸다).
|
|
151
|
+
// 손대지 않았다는 확실한 신호는 그 자리에 도구의 placeholder 가 그대로 있다는 것이다.
|
|
152
|
+
const untouched = /class\s*=\s*["']placeholder["']/.test(area) || classes.size === 0;
|
|
153
|
+
return { ok: true, classes: [...classes], tokens: [...tokens], tags: [...tags], drawn: area.replace(/\s+/g, ' ').trim().length, untouched };
|
|
154
|
+
}
|
|
131
155
|
function _mockupScaffold(p, ctx) {
|
|
132
156
|
const feats = (p.features || []).map(f => ` <li>${_esc(f)}</li>`).join('\n');
|
|
133
157
|
const c = ctx || { stack: [], tokens: { cssVars: [], utilities: [] }, components: [], target: null, notes: [] };
|
|
@@ -202,8 +226,10 @@ ${feats || ' <li>(없음 — --features "a,b" 로 기록)</li>'}
|
|
|
202
226
|
${notes ? `<p class="notes" style="margin-top:10px">인벤토리 한계</p><ul class="notes">${notes}</ul>` : ''}
|
|
203
227
|
</section>
|
|
204
228
|
<section class="mock">
|
|
205
|
-
<h2>화면 시안 — ⬇
|
|
229
|
+
<h2>화면 시안 — ⬇ 아래 두 마커 <b>사이</b>만 실제 레이아웃 초안으로 교체하세요</h2>
|
|
230
|
+
<!-- leerness:draw-start · 승인 시 이 구간의 클래스와 var(--토큰) 이 '계약' 으로 고정된다. 마커는 지우지 마세요. -->
|
|
206
231
|
<div class="placeholder">위의 <b>실제 토큰·컴포넌트·대상 화면 클래스</b>를 재료로 삼아 그립니다 — 새 색/새 간격을 발명하지 마세요.<br>외부 리소스 없이(오프라인) 이 파일 하나로 열리게 유지하세요.</div>
|
|
232
|
+
<!-- leerness:draw-end -->
|
|
207
233
|
</section>
|
|
208
234
|
</main>
|
|
209
235
|
<!-- leerness:mockup ${_esc(p.id)} — 이 파일은 시안입니다. 승인(leerness preview approve ${_esc(p.id)}) 전 실제 코드 작성 금지. -->
|
|
@@ -322,6 +348,44 @@ function previewCmd(root, sub, rest, deps = {}) {
|
|
|
322
348
|
return;
|
|
323
349
|
}
|
|
324
350
|
|
|
351
|
+
// 1.36.100 (P-0012 후반): `preview verify <P-ID> --files a,b` — 승인 시 고정한 계약과 구현을 대조.
|
|
352
|
+
// 무엇을 하는가: 시안이 약속한 클래스/토큰이 구현 파일에 '나타나는가' 를 센다.
|
|
353
|
+
// 무엇을 하지 않는가: 시각적 일치를 보증하지 않는다. 클래스가 있다고 같아 보이는 건 아니다 —
|
|
354
|
+
// 이 구분을 흐리면 1.36.5 에서 고쳤던 과장을 되풀이하게 된다. 그래서 출력에 한계를 매번 적는다.
|
|
355
|
+
if (sub === 'verify') {
|
|
356
|
+
if (!id) { failJson(json, 'id_required', `preview verify <P-ID> --files a,b 필요`); return; }
|
|
357
|
+
if (!p) { failJson(json, 'not_found', `preview 없음: ${id}`); return; }
|
|
358
|
+
if (!p.contract) { failJson(json, 'no_contract', `${id} 에 계약이 없습니다 — 시안을 만들고(preview mockup) 승인(preview approve)해야 고정됩니다`); return; }
|
|
359
|
+
const filesArg = (arg && arg('--files', '')) || '';
|
|
360
|
+
const files = filesArg.split(',').map(s => s.trim()).filter(Boolean);
|
|
361
|
+
if (!files.length) { failJson(json, 'files_required', `--files src/a.tsx,src/b.css 로 구현 파일을 지정하세요`); return; }
|
|
362
|
+
const base = absRoot(root);
|
|
363
|
+
let text = ''; const read_ = []; const missingFiles = [];
|
|
364
|
+
for (const f of files) {
|
|
365
|
+
const fp = path.resolve(base, f);
|
|
366
|
+
if (fp !== base && !fp.startsWith(base + path.sep)) { missingFiles.push(`${f} (루트 밖 — 읽지 않음)`); continue; }
|
|
367
|
+
if (!exists(fp)) { missingFiles.push(`${f} (없음)`); continue; }
|
|
368
|
+
try { const st = fs.statSync(fp); if (st.size > 1024 * 1024) { missingFiles.push(`${f} (1MB 초과 — 건너뜀)`); continue; } text += '\n' + read(fp); read_.push(f); } catch (e) { missingFiles.push(`${f} (${e.message})`); }
|
|
369
|
+
}
|
|
370
|
+
const cls = p.contract.classes || [], tok = p.contract.tokens || [];
|
|
371
|
+
const clsFound = cls.filter(c => text.includes(c));
|
|
372
|
+
const tokFound = tok.filter(t => text.includes(t));
|
|
373
|
+
const total = cls.length + tok.length;
|
|
374
|
+
const found = clsFound.length + tokFound.length;
|
|
375
|
+
const pct = total ? Math.round(found * 100 / total) : 0;
|
|
376
|
+
const missCls = cls.filter(c => !clsFound.includes(c)), missTok = tok.filter(t => !tokFound.includes(t));
|
|
377
|
+
if (json) { log(JSON.stringify({ ok: true, id, filesRead: read_, filesSkipped: missingFiles, classes: { total: cls.length, found: clsFound.length, missing: missCls }, tokens: { total: tok.length, found: tokFound.length, missing: missTok }, presencePct: pct, limit: 'presence-only — 시각적 일치를 보증하지 않음' }, null, 2)); return; }
|
|
378
|
+
log(`# preview verify ${id} — 시안 계약 ↔ 구현 대조`);
|
|
379
|
+
log(` 읽은 파일 ${read_.length}개${missingFiles.length ? ` · 건너뜀 ${missingFiles.length}개` : ''}`);
|
|
380
|
+
missingFiles.forEach(x => warn(` ${x}`));
|
|
381
|
+
log(` 클래스 ${clsFound.length}/${cls.length} · 토큰 ${tokFound.length}/${tok.length} → 존재율 ${pct}%`);
|
|
382
|
+
if (missCls.length) log(` 누락 클래스: ${missCls.slice(0, 20).join(' ')}${missCls.length > 20 ? ` …외 ${missCls.length - 20}` : ''}`);
|
|
383
|
+
if (missTok.length) log(` 누락 토큰: ${missTok.slice(0, 20).join(' ')}${missTok.length > 20 ? ` …외 ${missTok.length - 20}` : ''}`);
|
|
384
|
+
if (!read_.length) warn(' 읽은 구현 파일이 없어 존재율은 의미가 없습니다');
|
|
385
|
+
log(` ⓘ 한계: 이건 '존재' 검사입니다 — 클래스·토큰이 나타나는지만 봅니다. 같아 보이는지는 보지 않습니다(그건 사람이 시안과 화면을 나란히 봐야 합니다).`);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
325
389
|
if (sub === 'show') {
|
|
326
390
|
if (json) { log(JSON.stringify({ ok: true, ...p }, null, 2)); return; }
|
|
327
391
|
log(`# ${p.id} 「${p.title}」 [${p.status}]`);
|
|
@@ -340,10 +404,26 @@ function previewCmd(root, sub, rest, deps = {}) {
|
|
|
340
404
|
ok(`${p.id} 이미 승인됨 — 변경 없음`); return;
|
|
341
405
|
}
|
|
342
406
|
p.status = 'approved'; (p.history = p.history || []).push({ at: now(), event: 'approved' });
|
|
407
|
+
// 1.36.100 (P-0012 후반): 승인 순간의 시안을 계약으로 고정한다 — 나중에 구현이 그 약속을 지켰는지 대조하기 위해서다.
|
|
408
|
+
// 시안이 없거나 마커가 지워졌으면 계약을 만들지 않는다(없는 계약을 있는 척 하지 않는다).
|
|
409
|
+
let _contractNote = '';
|
|
410
|
+
if (p.mockupPath) {
|
|
411
|
+
const mp = path.resolve(root, p.mockupPath);
|
|
412
|
+
if (exists(mp)) {
|
|
413
|
+
const ex = _extractContract(read(mp));
|
|
414
|
+
if (ex.ok) {
|
|
415
|
+
p.contract = { at: now(), classes: ex.classes, tokens: ex.tokens, tags: ex.tags, drawnChars: ex.drawn, untouched: !!ex.untouched };
|
|
416
|
+
_contractNote = `계약 고정: 클래스 ${ex.classes.length} · 토큰 ${ex.tokens.length} · 요소 ${ex.tags.length}`;
|
|
417
|
+
if (ex.untouched) _contractNote += ' ⚠ 시안을 아직 안 그렸습니다(placeholder 그대로) — 계약이 비어 대조가 무의미합니다';
|
|
418
|
+
} else _contractNote = `계약 미고정: ${ex.reason}`;
|
|
419
|
+
} else _contractNote = '계약 미고정: 시안 파일 없음';
|
|
420
|
+
} else _contractNote = '계약 미고정: 시안 없음 (leerness preview mockup 으로 먼저 생성)';
|
|
343
421
|
if (!_savePreviews(root, list, json)) return;
|
|
344
422
|
try { if (typeof deps._onResolved === 'function') deps._onResolved(p.id); } catch {} // 1.36.80 (UR-0067): 확인 완료 → 임시 워크스페이스 정리
|
|
345
|
-
if (json) { log(JSON.stringify({ ok: true, id: p.id, status: p.status }, null, 2)); return; }
|
|
423
|
+
if (json) { log(JSON.stringify({ ok: true, id: p.id, status: p.status, contract: p.contract || null, contractNote: _contractNote }, null, 2)); return; }
|
|
346
424
|
ok(`${p.id} 승인 — 이제 구현을 시작해도 됩니다.`);
|
|
425
|
+
log(` ${_contractNote}`);
|
|
426
|
+
if (p.contract) log(` → 구현 후 대조: leerness preview verify ${p.id} --files src/a.tsx,src/b.css`);
|
|
347
427
|
return;
|
|
348
428
|
}
|
|
349
429
|
if (sub === 'revise') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.100",
|
|
4
4
|
"description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
package/scripts/e2e.js
CHANGED
|
@@ -3744,7 +3744,11 @@ total++;
|
|
|
3744
3744
|
{
|
|
3745
3745
|
let ok = false;
|
|
3746
3746
|
try {
|
|
3747
|
-
|
|
3747
|
+
// 1.36.100: 이 호출만 30초로 남아 있었다 — doctor 는 selftest 를 내장해 격리 실측 31s(게시된 1.36.98 도 동일)라
|
|
3748
|
+
// 전체 스위트 부하에서 간헐 초과했고, 실제로 게이트 한 번을 실패시켰다(내 변경 때문이 아니라 잠복 결함).
|
|
3749
|
+
// 같은 명령의 다른 호출(4644·6955)은 1.36.87 에서 이미 120초로 올려 뒀다 — 여기만 빠져 있었다.
|
|
3750
|
+
// 검사 대상은 속도가 아니라 동작이다.
|
|
3751
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'doctor', '--json'], { encoding: 'utf8', timeout: 120000 });
|
|
3748
3752
|
let j = null; try { j = JSON.parse(r.stdout); } catch {}
|
|
3749
3753
|
const doctorOk = j && j.version && typeof j.mcpTools === 'number' && j.mcpTools >= 80 && j.selftest && j.selftest.total > 0 && j.healthy === true && r.status === 0;
|
|
3750
3754
|
// commands 요약 + banner 가 실제 MCP 수 노출 (하드코딩 65/46 아님)
|
|
@@ -9567,6 +9571,54 @@ total++;
|
|
|
9567
9571
|
if (!ok) failed++;
|
|
9568
9572
|
}
|
|
9569
9573
|
|
|
9574
|
+
// ── 1.36.100 (P-0012 후반): 승인 시 시안을 계약으로 고정 → 구현 후 존재 대조. 과장하지 않는 것까지 함께 건다.
|
|
9575
|
+
{
|
|
9576
|
+
total++;
|
|
9577
|
+
let ok = false; const dbg = {};
|
|
9578
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-ct100-'));
|
|
9579
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a], { cwd: d, encoding: 'utf8', timeout: 120000 });
|
|
9580
|
+
const out = (r) => (r.stdout || '') + (r.stderr || '');
|
|
9581
|
+
try {
|
|
9582
|
+
fs.writeFileSync(path.join(d, 'package.json'), JSON.stringify({ name: 'shop', version: '0.1.0', dependencies: { react: '18', tailwindcss: '3' } }));
|
|
9583
|
+
R(['init', d, '--yes', '--language', 'ko', '--skills', 'recommended']);
|
|
9584
|
+
const W = (rel, t) => { const p = path.join(d, rel); fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, t); };
|
|
9585
|
+
W('src/theme.css', ':root { --brand-500: #2563eb; --surface-1: #0f1115; }\n');
|
|
9586
|
+
W('src/components/Button.tsx', 'export function Button(){ return <button className="px-3 py-1 rounded-lg">go</button> }\n');
|
|
9587
|
+
const id = (/(P-\d{4,})/.exec(out(R(['preview', 'add', '장바구니', '--design', 'x']))) || [])[1];
|
|
9588
|
+
R(['preview', 'mockup', id]);
|
|
9589
|
+
const mp = path.join(d, '.harness', 'previews', id + '-mockup.html');
|
|
9590
|
+
// 안 그린 시안을 승인하면 계약이 비었다고 말한다(조용히 빈 계약을 만들지 않는다)
|
|
9591
|
+
const ap0 = out(R(['preview', 'approve', id]));
|
|
9592
|
+
const warnsEmpty = /아직 안 그렸습니다/.test(ap0);
|
|
9593
|
+
// AI 가 마커 사이를 채운 뒤 재승인 → 계약 고정
|
|
9594
|
+
R(['preview', 'revise', id, '--note', '초안 반영']);
|
|
9595
|
+
let html = fs.readFileSync(mp, 'utf8');
|
|
9596
|
+
const drawn = '\n<header class="topbar p-4 rounded-lg" style="background:var(--surface-1)"><h1 class="text-xl">장바구니</h1></header>\n<ul class="cart-list gap-2"><li class="cart-item px-3 py-1" style="border-color:var(--brand-500)">항목</li></ul>\n';
|
|
9597
|
+
fs.writeFileSync(mp, html.replace(/(leerness:draw-start[^>]*-->)[\s\S]*?(<!-- leerness:draw-end)/, `$1${drawn}$2`));
|
|
9598
|
+
const ap = out(R(['preview', 'approve', id]));
|
|
9599
|
+
const fixed = /계약 고정: 클래스 [1-9]/.test(ap);
|
|
9600
|
+
const store = JSON.parse(fs.readFileSync(path.join(d, '.harness', 'previews.json'), 'utf8'));
|
|
9601
|
+
const rec = ((store.previews || store) || []).find(x => x.id === id);
|
|
9602
|
+
const stored = !!(rec && rec.contract && rec.contract.classes.includes('topbar') && rec.contract.tokens.includes('--brand-500'))
|
|
9603
|
+
&& !rec.contract.classes.some(c => ['mock', 'notes', 'chips', 'placeholder'].includes(c)); // 스캐폴드 장식은 계약이 아니다
|
|
9604
|
+
// 미준수 구현 → 낮은 존재율 · 준수 구현 → 100%
|
|
9605
|
+
W('src/pages/Cart.tsx', 'export function Cart(){ return <main className="p-4">x</main> }\n');
|
|
9606
|
+
const low = Number((/존재율 (\d+)%/.exec(out(R(['preview', 'verify', id, '--files', 'src/pages/Cart.tsx']))) || [, '100'])[1]);
|
|
9607
|
+
W('src/pages/Cart.tsx', 'import "./cart.css";\nexport function Cart(){ return (<main><header className="topbar p-4 rounded-lg"><h1 className="text-xl">장바구니</h1></header><ul className="cart-list gap-2"><li className="cart-item px-3 py-1">항목</li></ul></main>) }\n');
|
|
9608
|
+
W('src/pages/cart.css', '.topbar{background:var(--surface-1)} .cart-item{border-color:var(--brand-500)}\n');
|
|
9609
|
+
const v2 = out(R(['preview', 'verify', id, '--files', 'src/pages/Cart.tsx,src/pages/cart.css']));
|
|
9610
|
+
const high = Number((/존재율 (\d+)%/.exec(v2) || [, '0'])[1]);
|
|
9611
|
+
// 과장 금지: '존재' 검사라는 한계를 매번 말한다(1.36.5 에서 고쳤던 과장의 재발 방지)
|
|
9612
|
+
const honest = /'존재' 검사입니다/.test(v2) && /같아 보이는지는 보지 않습니다/.test(v2);
|
|
9613
|
+
const guarded = /루트 밖/.test(out(R(['preview', 'verify', id, '--files', '../../etc/passwd'])));
|
|
9614
|
+
Object.assign(dbg, { warnsEmpty, fixed, stored, low, high, honest, guarded });
|
|
9615
|
+
ok = warnsEmpty && fixed && stored && low < 50 && high === 100 && honest && guarded;
|
|
9616
|
+
} catch (e) { dbg.err = String(e && e.message).slice(0, 160); } finally { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} }
|
|
9617
|
+
console.log(ok ? '✓ B(1.36.100/P-0012) 승인 시 시안→계약 고정 + 구현 존재 대조(미준수 저율/준수 100%) + 한계 고지 + 루트밖 거부 + 빈 시안 경고'
|
|
9618
|
+
: '✗ 1.36.100 시안 계약 실패 ' + JSON.stringify(dbg));
|
|
9619
|
+
if (!ok) failed++;
|
|
9620
|
+
}
|
|
9621
|
+
|
|
9570
9622
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
9571
9623
|
if (failed > 0) process.exit(1);
|
|
9572
9624
|
|