leerness 1.36.68 → 1.36.70
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 +20 -0
- package/README.md +4 -4
- package/bin/leerness.js +13 -8
- package/lib/diagnostics.js +25 -2
- package/lib/mcp-tools.js +1 -1
- package/package.json +1 -1
- package/scripts/e2e.js +66 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.70 — 2026-07-24 — 성장 한계 클래스 2: migrate 보고 상한 (88KB → 13.5KB) — R-0001 검수 13회전
|
|
4
|
+
|
|
5
|
+
1.36.69 whats-new 상한과 동일 규율을 migrate 보고서의 "AI must re-read" 섹션에 적용. 큰 버전 점프(602 릴리스)에서 보고서가 ~88KB 로 폭주해 AI 컨텍스트를 태우던 것.
|
|
6
|
+
|
|
7
|
+
- **헤드라인 최신 30 + 명령/플래그/파일 목록 40 캡**, 각 헤더에 실제 총수 표기 + 정직한 "… 외 N개" 오버플로 라인 + `whats-new --from X --all/--limit N` 탈출구. 작은 점프에선 상한 비가시(전량 출력).
|
|
8
|
+
- (검수 #1 Medium) **광고된 `--limit N` 이 텍스트 모드에서 조용히 무시되던 것 수정** — 명시적 `--limit` 존중 + 오버플로 라인(기본 텍스트 출력은 전체 유지, 계약 무변경). 종전엔 보고서가 권하는 명령이 602줄을 그대로 뱉었다.
|
|
9
|
+
- (검수 #2) e2e 픽스처 `--no-stale-check`(빈 캐시 npm 조회 8초 지연 차단). (검수 #3) e2e 단언을 헤딩 파싱 기반 정확 일치(shown===30·행수·오버플로 수치)로 강화.
|
|
10
|
+
- 검수 실측(codex): 30/602 헤드라인 + 외 572 · 명령 40/170+130 · 플래그 40/133+93 · 파일 40/43+3 — 전 수치 독립 파싱과 일치. 소점프 무영향, 마크다운 정합, fromV 엣지(동일/쓰레기/미래) 전부 통과.
|
|
11
|
+
- 검증: selftest 334, e2e +1.
|
|
12
|
+
|
|
13
|
+
## 1.36.69 — 2026-07-22 — 성장 한계 클래스: whats-new --json 응답 상한 (804KB → 30KB) — R-0001 검수 12회전
|
|
14
|
+
|
|
15
|
+
직전 게이트가 우연히 드러낸 누적 성장 문제를 픽스처 우회가 아니라 제품에서 해결. CHANGELOG 1MB 돌파로 `--json` 이 804KB 를 뱉어 AI 소비자 컨텍스트를 폭파시키고 기본 stdout 버퍼에서 절단됐다.
|
|
16
|
+
|
|
17
|
+
- **기본 최신 30개 버전 + 정직한 절단 표기**(`totalVersions`/`shown`/`truncated`/`hint`), 전체는 `--all`, 개수는 `--limit N`. 텍스트 모드 무변경. 실측 804KB → 30KB.
|
|
18
|
+
- (검수 #1 Medium) **MCP 계약 동반 갱신**: `leerness_whats_new` 에 `limit`/`all` 인자 노출 + 전달 — 종전 그대로면 MCP 소비자는 30개로 조용히 잘리고 조절 수단이 없었다(실측 확인 후 수정).
|
|
19
|
+
- (검수 #2) 무효/비양수 `--limit`(0/-5/abc) 는 기본값 폴백(종전 -5 → 1). (검수 #3) 절단 힌트 UI 언어 준수. (검수 #4) e2e 단언을 `shown === 30` 정확 일치로 + 무효 limit·비절단 hint 부재·MCP 전달 케이스 추가.
|
|
20
|
+
- **후속 계측(검수 제공)**: migrate stdout ~88KB · memory search(--limit huge) ~54KB · retro/insights ~40KB · task list ~33KB — 다음 상한 후보로 기록(현재는 안전 범위).
|
|
21
|
+
- 검증: selftest 334, e2e +1(크기 회귀 가드 포함).
|
|
22
|
+
|
|
3
23
|
## 1.36.68 — 2026-07-22 — F14 재구현: 마감 보고의 미승인 미리보기 (8차 헌트 이연분 종결) — R-0001 검수 11회전
|
|
4
24
|
|
|
5
25
|
1.36.67 에서 이연했던 F14 를 원인 규명 후 재구현.
|
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.70 하네스를 사용합니다. 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.70는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
180
180
|
|
|
181
181
|
```jsonc
|
|
182
182
|
// 카테고리별
|
|
@@ -197,7 +197,7 @@ Leerness v1.36.68는 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.70 릴리스 태그 이력** (수백 라운드) · _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.70: 2026-07-24
|
|
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.70';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -929,17 +929,22 @@ function writeMigrationReport(root, backup, actions, opts = {}) {
|
|
|
929
929
|
v.newFiles.forEach(f => allFiles.add(f));
|
|
930
930
|
}
|
|
931
931
|
if (diff.length) {
|
|
932
|
+
// 1.36.70 (성장 한계 클래스, whats-new 1.36.69 와 동일 규율): 큰 버전 점프(601+ 릴리스)에서
|
|
933
|
+
// 보고서가 ~88KB 로 폭주해 AI 컨텍스트를 태우던 것 — 목록별 상한 + 정직한 "외 N개" 표기.
|
|
934
|
+
const _capList = (set, cap, fmt) => { const a = [...set]; return a.slice(0, cap).map(fmt).join('\n') + (a.length > cap ? `\n- … 외 ${a.length - cap}개 (전체: \`leerness whats-new --from ${fromV} --all\`)` : ''); };
|
|
932
935
|
aiReadBlock = `\n## 🤖 AI must re-read (1.9.41 차분 안내)\n\n`;
|
|
933
|
-
aiReadBlock += `이 migrate는 ${fromV} → ${VERSION}
|
|
934
|
-
if (allCommands.size) aiReadBlock += `**📌 신규 명령** (이전엔 없던
|
|
935
|
-
if (allFlags.size) aiReadBlock += `**🚩 신규
|
|
936
|
-
if (allFiles.size) aiReadBlock += `**📄 신규/변경 파일** (반드시
|
|
937
|
-
|
|
938
|
-
|
|
936
|
+
aiReadBlock += `이 migrate는 ${fromV} → ${VERSION} 점프입니다 (${diff.length}개 릴리스). 메인 AI 에이전트는 다음을 인지하고 우선 활용:\n\n`;
|
|
937
|
+
if (allCommands.size) aiReadBlock += `**📌 신규 명령** (이전엔 없던 것, ${allCommands.size}개):\n${_capList(allCommands, 40, c => `- \`leerness ${c}\``)}\n\n`;
|
|
938
|
+
if (allFlags.size) aiReadBlock += `**🚩 신규 플래그** (${allFlags.size}개):\n${_capList(allFlags, 40, f => `- \`${f}\``)}\n\n`;
|
|
939
|
+
if (allFiles.size) aiReadBlock += `**📄 신규/변경 파일** (반드시 재독, ${allFiles.size}개):\n${_capList(allFiles, 40, f => `- \`${f}\``)}\n\n`;
|
|
940
|
+
const _hlCap = 30;
|
|
941
|
+
aiReadBlock += `**버전별 헤드라인** (최신 ${Math.min(_hlCap, diff.length)}/${diff.length}):\n`;
|
|
942
|
+
for (const v of diff.slice(0, _hlCap)) {
|
|
939
943
|
const firstLine = (v.body.match(/^\*\*([^*]+)\*\*/) || [])[1]
|
|
940
944
|
|| (v.body.split('\n').find(l => l.trim() && !l.startsWith('##')) || '').trim().slice(0, 120);
|
|
941
945
|
aiReadBlock += `- ${v.version} — ${firstLine || '(no headline)'}\n`;
|
|
942
946
|
}
|
|
947
|
+
if (diff.length > _hlCap) aiReadBlock += `- … 외 ${diff.length - _hlCap}개 버전 — \`leerness whats-new --from ${fromV} --limit N\` 으로 조회\n`;
|
|
943
948
|
aiReadBlock += `\n**권장 행동**:\n1. 위 신규 명령을 \`--help\`로 확인\n2. \`AGENTS.md\` / \`CLAUDE.md\` / \`.harness/session-workflow.md\` 재독 (다음 \`leerness handoff\` 호출 시 자동 안내)\n3. 이전 청크의 기억 무효 — 새 도구 우선 시도\n4. 상세: \`leerness whats-new --from ${fromV}\`\n`;
|
|
944
949
|
}
|
|
945
950
|
}
|
|
@@ -18666,7 +18671,7 @@ function _mcpToCliArgs(name, args, targetPath) {
|
|
|
18666
18671
|
case 'leerness_contract_verify': cliArgs = ['contract', 'verify', args.spec, args.impl]; break;
|
|
18667
18672
|
case 'leerness_agents_list': cliArgs = ['agents', 'list', '--json']; break;
|
|
18668
18673
|
case 'leerness_reuse_map': cliArgs = ['reuse-map', targetPath, ...(args.allApps ? ['--all-apps'] : []), ...(args.strictElements ? ['--strict-elements'] : []), '--json']; break;
|
|
18669
|
-
case 'leerness_whats_new': cliArgs = ['whats-new', '--path', targetPath, ...(args.from ? ['--from', args.from] : []), ...(args.to ? ['--to', args.to] : []), '--json']; break;
|
|
18674
|
+
case 'leerness_whats_new': cliArgs = ['whats-new', '--path', targetPath, ...(args.from ? ['--from', args.from] : []), ...(args.to ? ['--to', args.to] : []), ...(args.all ? ['--all'] : []), ...(args.limit ? ['--limit', String(args.limit)] : []), '--json']; break; // 1.36.69 (검수 #1): limit/all 전달 — MCP 소비자도 상한 조절 가능
|
|
18670
18675
|
case 'leerness_usage_stats': cliArgs = ['usage', 'stats', targetPath, '--json']; break;
|
|
18671
18676
|
case 'leerness_session_close': cliArgs = ['session', 'close', targetPath, '--json']; break;
|
|
18672
18677
|
case 'leerness_skill_suggest': cliArgs = ['skill', 'suggest', '--path', targetPath, '--json', ...(args.min ? ['--min', String(args.min)] : []), ...(args.days ? ['--days', String(args.days)] : [])]; break;
|
package/lib/diagnostics.js
CHANGED
|
@@ -161,7 +161,25 @@ function whatsNewCmd(root, deps = {}) {
|
|
|
161
161
|
return process.exit(1);
|
|
162
162
|
}
|
|
163
163
|
const diff = _parseChangelogBetween(read(changelogPath), fromV, toV);
|
|
164
|
-
if (has('--json')) {
|
|
164
|
+
if (has('--json')) {
|
|
165
|
+
// 1.36.69 (성장 한계 클래스): CHANGELOG 누적(1MB+)으로 --json 이 800KB 를 넘겨 AI 소비자의 컨텍스트를 폭파시켰다.
|
|
166
|
+
// 기본은 최신 30개 버전으로 제한(에이전트가 실제로 읽는 범위) + 절단 사실을 정직하게 표기. 전체는 --all/--limit 로 옵트인.
|
|
167
|
+
// (검수 #2) 무효/비양수 limit 은 기본값으로 폴백 — `--limit -5` 가 1 이 되던 비일관 제거
|
|
168
|
+
const _rawLim = parseInt(arg('--limit', '30'), 10);
|
|
169
|
+
const _lim = has('--all') ? diff.length : (Number.isFinite(_rawLim) && _rawLim > 0 ? _rawLim : 30);
|
|
170
|
+
const _shown = diff.slice(0, _lim);
|
|
171
|
+
const _truncated = _shown.length < diff.length;
|
|
172
|
+
log(JSON.stringify({
|
|
173
|
+
from: fromV, to: toV,
|
|
174
|
+
versions: _shown,
|
|
175
|
+
totalVersions: diff.length,
|
|
176
|
+
shown: _shown.length,
|
|
177
|
+
truncated: _truncated,
|
|
178
|
+
// (검수 #3) 힌트도 UI 언어 준수
|
|
179
|
+
...(_truncated ? { hint: t(`최신 ${_shown.length}개만 표시 — 전체는 --all, 개수 조절은 --limit N`, `showing newest ${_shown.length} only — use --all for everything, --limit N to adjust`) } : {})
|
|
180
|
+
}, null, 2));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
165
183
|
if (!diff.length) {
|
|
166
184
|
log(`# leerness whats-new (1.9.41)`);
|
|
167
185
|
log(t(`현재 ${fromV} ↔ 대상 ${toV}: 새 항목 없음 (또는 CHANGELOG에 기록 안 됨)`, `current ${fromV} ↔ target ${toV}: no new entries (or not recorded in CHANGELOG)`));
|
|
@@ -186,12 +204,17 @@ function whatsNewCmd(root, deps = {}) {
|
|
|
186
204
|
if (allFiles.size) log(t(` 📄 신규 파일: ${[...allFiles].join(', ')}`, ` 📄 new files: ${[...allFiles].join(', ')}`));
|
|
187
205
|
log('');
|
|
188
206
|
log(t(`## 📜 버전별 헤드라인`, `## 📜 per-version headlines`));
|
|
189
|
-
|
|
207
|
+
// 1.36.70 (검수 #1): migrate 보고서가 광고하는 `--limit N` 이 텍스트 모드에서 조용히 무시되던 것 —
|
|
208
|
+
// 명시적 --limit 만 존중(기본 텍스트 출력은 전체 유지, 계약 무변경).
|
|
209
|
+
const _txtRawLim = parseInt(arg('--limit', ''), 10);
|
|
210
|
+
const _txtLim = (has('--limit') && Number.isFinite(_txtRawLim) && _txtRawLim > 0) ? _txtRawLim : diff.length;
|
|
211
|
+
for (const v of diff.slice(0, _txtLim)) {
|
|
190
212
|
// body 첫 줄(또는 strong header) 추출
|
|
191
213
|
const firstLine = (v.body.match(/^\*\*([^*]+)\*\*/) || [])[1]
|
|
192
214
|
|| (v.body.split('\n').find(l => l.trim() && !l.startsWith('##')) || '').trim().slice(0, 120);
|
|
193
215
|
log(` • ${v.version}${v.date ? ` (${v.date})` : ''} — ${firstLine || '(no headline)'}`);
|
|
194
216
|
}
|
|
217
|
+
if (_txtLim < diff.length) log(t(` … 외 ${diff.length - _txtLim}개 버전 (전체: --all 또는 --limit 생략)`, ` … ${diff.length - _txtLim} more version(s) (drop --limit or use --all for everything)`));
|
|
195
218
|
log('');
|
|
196
219
|
log(t(`## 💡 권장 행동`, `## 💡 recommended actions`));
|
|
197
220
|
log(t(` 1. 위 신규 명령들을 시도해 보세요 (예: \`leerness <명령> --help\`)`, ` 1. Try the new commands above (e.g. \`leerness <command> --help\`)`));
|
package/lib/mcp-tools.js
CHANGED
|
@@ -12,7 +12,7 @@ module.exports = [
|
|
|
12
12
|
{ name: 'leerness_contract_verify', requiredTier: 'read-only', description: '명세 ↔ 구현 함수/필드 일치 자동 검사', inputSchema: { type: 'object', properties: { spec: { type: 'string' }, impl: { type: 'string' } }, required: ['spec', 'impl'] } },
|
|
13
13
|
{ name: 'leerness_agents_list', requiredTier: 'read-only', description: '외부 AI CLI 가용성 표 (claude/codex/agy/copilot 상태 + 환경변수 활성화 여부)', inputSchema: { type: 'object', properties: {} } },
|
|
14
14
|
{ name: 'leerness_reuse_map', requiredTier: 'read-only', description: '워크스페이스 중복 함수/capability 자동 감지 (--all-apps + fuzzy 매칭)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, allApps: { type: 'boolean' }, strictElements: { type: 'boolean' } } } },
|
|
15
|
-
{ name: 'leerness_whats_new', requiredTier: 'read-only', description: 'CHANGELOG 차분 자동 추출 (from → to 사이 신규 명령/플래그/파일)', inputSchema: { type: 'object', properties: { from: { type: 'string' }, to: { type: 'string' } } } },
|
|
15
|
+
{ name: 'leerness_whats_new', requiredTier: 'read-only', description: 'CHANGELOG 차분 자동 추출 (from → to 사이 신규 명령/플래그/파일). 1.36.69: 기본 최신 30개 버전만 반환(응답 폭주 방지) — 응답의 totalVersions/shown/truncated 로 절단 여부 확인, 더 필요하면 limit(개수) 또는 all=true(전체, 대용량 주의) 지정.', inputSchema: { type: 'object', properties: { from: { type: 'string' }, to: { type: 'string' }, limit: { type: 'number' }, all: { type: 'boolean' } } } },
|
|
16
16
|
{ name: 'leerness_usage_stats', requiredTier: 'read-only', description: 'leerness 명령별 누적 호출 통계 + drift 통계', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
|
|
17
17
|
{ name: 'leerness_session_close', requiredTier: 'read-only', description: '1.9.103 — 세션 마감 JSON (handoff/current-state/task-log 갱신 + taskCounts + rules + skillCandidates + drift + topCommands + mcpStats). 외부 AI가 마감 통계 자동 회수', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
|
|
18
18
|
{ name: 'leerness_skill_suggest', requiredTier: 'read-only', description: '1.9.53 — 사용 패턴 자동 분석 → 새 skill 후보 제안 (Hermes-style 자동 학습)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, min: { type: 'number' }, days: { type: 'number' } } } },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.70",
|
|
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
|
@@ -1444,8 +1444,8 @@ total++;
|
|
|
1444
1444
|
total++;
|
|
1445
1445
|
{
|
|
1446
1446
|
// whats-new --from 큰 점프 → 신규 명령 추출
|
|
1447
|
-
// 1.36.
|
|
1448
|
-
const r = cp.spawnSync(process.execPath, [CLI, 'whats-new', '--from', '1.9.33', '--json'], { encoding: 'utf8', timeout: 20000, maxBuffer: 32 * 1024 * 1024 });
|
|
1447
|
+
// 1.36.69: 기본 응답은 최신 30개로 제한됨 — 전체 차분 검증은 --all (대용량이라 maxBuffer 명시)
|
|
1448
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'whats-new', '--from', '1.9.33', '--json', '--all'], { encoding: 'utf8', timeout: 20000, maxBuffer: 32 * 1024 * 1024 });
|
|
1449
1449
|
let parsed = null;
|
|
1450
1450
|
try { parsed = JSON.parse(r.stdout); } catch {}
|
|
1451
1451
|
const ok = parsed
|
|
@@ -7174,5 +7174,69 @@ total++;
|
|
|
7174
7174
|
if (!ok) failed++;
|
|
7175
7175
|
}
|
|
7176
7176
|
|
|
7177
|
+
// 1.36.69 (성장 한계 클래스): whats-new --json 기본 상한 — AI 컨텍스트 보호 + 정직한 절단 표기 + --all/--limit 옵트인
|
|
7178
|
+
total++;
|
|
7179
|
+
{
|
|
7180
|
+
let ok = false;
|
|
7181
|
+
try {
|
|
7182
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, 'whats-new', ...a], { encoding: 'utf8', timeout: 25000, maxBuffer: 32 * 1024 * 1024 });
|
|
7183
|
+
const def = JSON.parse(R(['--from', '1.9.33', '--json']).stdout);
|
|
7184
|
+
// (검수 #4) 정확 일치로 — shown<=30 은 기본값이 1로 퇴화해도 통과하던 느슨한 단언
|
|
7185
|
+
const defOk = def.shown === 30 && def.versions.length === def.shown && def.totalVersions > def.shown && def.truncated === true && typeof def.hint === 'string';
|
|
7186
|
+
// 무효 limit 은 기본값 폴백 (0/-5/abc)
|
|
7187
|
+
const badLim = ['0', '-5', 'abc'].every(v => { try { return JSON.parse(R(['--from', '1.9.33', '--json', '--limit', v]).stdout).shown === 30; } catch { return false; } });
|
|
7188
|
+
// 절단 없을 때 hint 부재
|
|
7189
|
+
const small = JSON.parse(R(['--from', '1.36.60', '--json']).stdout);
|
|
7190
|
+
const smallOk = small.truncated === false && small.hint === undefined && small.shown === small.totalVersions;
|
|
7191
|
+
// MCP 인자 전달 (limit)
|
|
7192
|
+
const mcpReq = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'leerness_whats_new', arguments: { from: '1.9.33', limit: 2 } } });
|
|
7193
|
+
const mcpR = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 25000, input: mcpReq + '\n', maxBuffer: 32 * 1024 * 1024 });
|
|
7194
|
+
let mcpOk = false;
|
|
7195
|
+
try { const txt = JSON.parse(mcpR.stdout.split('\n').filter(Boolean)[0]).result.content[0].text; mcpOk = JSON.parse(txt).shown === 2; } catch {}
|
|
7196
|
+
const all = JSON.parse(R(['--from', '1.9.33', '--json', '--all']).stdout);
|
|
7197
|
+
const allOk = all.shown === all.totalVersions && all.truncated === false && all.versions.length === def.totalVersions;
|
|
7198
|
+
const lim = JSON.parse(R(['--from', '1.9.33', '--json', '--limit', '3']).stdout);
|
|
7199
|
+
const limOk = lim.shown === 3 && lim.versions.length === 3;
|
|
7200
|
+
// 크기 회귀 가드: 기본 응답이 200KB 를 넘지 않아야(에이전트 컨텍스트 보호)
|
|
7201
|
+
const defRaw = R(['--from', '1.9.33', '--json']).stdout;
|
|
7202
|
+
const sizeOk = defRaw.length < 200 * 1024;
|
|
7203
|
+
ok = defOk && allOk && limOk && sizeOk && badLim && smallOk && mcpOk;
|
|
7204
|
+
if (!ok) console.log(` [wn 디버그] def=${defOk} all=${allOk} lim=${limOk} size=${sizeOk}(${Math.round(defRaw.length / 1024)}KB) bad=${badLim} small=${smallOk} mcp=${mcpOk}`);
|
|
7205
|
+
} catch (e) {}
|
|
7206
|
+
console.log(ok ? '✓ B(1.36.69) whats-new --json 상한 30(정확) + truncated/hint + --all/--limit + 무효limit 폴백 + MCP 전달 + 200KB 미만' : '✗ whats-new 상한 실패');
|
|
7207
|
+
if (!ok) failed++;
|
|
7208
|
+
}
|
|
7209
|
+
|
|
7210
|
+
// 1.36.70 (성장 한계 클래스): migrate 보고서 상한 — 큰 버전 점프에서 ~88KB 폭주하던 AI must re-read 를 목록별 캡 + "외 N개" 정직 표기
|
|
7211
|
+
total++;
|
|
7212
|
+
{
|
|
7213
|
+
let ok = false;
|
|
7214
|
+
const _d = [];
|
|
7215
|
+
try {
|
|
7216
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-migcap-')); _d.push(d);
|
|
7217
|
+
// (검수 #2) --no-stale-check: 빈 캐시에서 npm 조회로 최대 8초 지연되던 것 차단
|
|
7218
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7219
|
+
fs.writeFileSync(path.join(d, '.harness', 'HARNESS_VERSION'), '1.9.33');
|
|
7220
|
+
cp.spawnSync(process.execPath, [CLI, 'migrate', d], { encoding: 'utf8', timeout: 60000, maxBuffer: 32 * 1024 * 1024 });
|
|
7221
|
+
const rep = fs.readFileSync(path.join(d, '.harness', 'migration-report.md'), 'utf8');
|
|
7222
|
+
const sizeOk = rep.length < 30 * 1024;
|
|
7223
|
+
// (검수 #3) 헤딩을 파싱해 shown/total 정합 + 헤드라인 행수 + 오버플로 수치 정확 일치로 단언 강화
|
|
7224
|
+
const hm = rep.match(/최신 (\d+)\/(\d+)/);
|
|
7225
|
+
const shown = hm ? parseInt(hm[1], 10) : 0, totalV = hm ? parseInt(hm[2], 10) : 0;
|
|
7226
|
+
const hlRows = (rep.match(/^- 1\.\d+\.\d+ — /gm) || []).length;
|
|
7227
|
+
const om = rep.match(/외 (\d+)개 버전/);
|
|
7228
|
+
const capOk = shown === 30 && totalV > 30 && hlRows === 30 && !!om && parseInt(om[1], 10) === totalV - 30;
|
|
7229
|
+
const escapeOk = rep.includes('whats-new --from 1.9.33'); // 전체가 필요한 소비자를 위한 탈출구
|
|
7230
|
+
// (검수 #1) 광고된 --limit 이 텍스트 모드에서도 실제 동작
|
|
7231
|
+
const limTxt = cp.spawnSync(process.execPath, [CLI, 'whats-new', '--from', '1.9.33', '--limit', '5'], { encoding: 'utf8', timeout: 20000, maxBuffer: 32 * 1024 * 1024 });
|
|
7232
|
+
const limRows = (limTxt.stdout.match(/^ {2}• 1\.\d+\.\d+/gm) || []).length;
|
|
7233
|
+
const limOk = limRows === 5 && /외 \d+개 버전|more version/.test(limTxt.stdout);
|
|
7234
|
+
ok = sizeOk && capOk && escapeOk && limOk;
|
|
7235
|
+
if (!ok) console.log(` [migcap 디버그] size=${sizeOk}(${Math.round(rep.length / 1024)}KB) cap=${capOk}(${shown}/${totalV},rows=${hlRows}) escape=${escapeOk} lim=${limOk}(${limRows})`);
|
|
7236
|
+
} catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
|
|
7237
|
+
console.log(ok ? '✓ B(1.36.70) migrate 보고 상한: 88KB→<30KB + 외N개 정직 표기 + whats-new 탈출구' : '✗ migrate 보고 상한 실패');
|
|
7238
|
+
if (!ok) failed++;
|
|
7239
|
+
}
|
|
7240
|
+
|
|
7177
7241
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
7178
7242
|
if (failed > 0) process.exit(1);
|