leerness 1.9.416 → 1.9.418

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 CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.418 — 2026-06-07 — health 보안 정직화 + status "healthy" 의미 명시 (9번째 외부평가 Codex P2, UR-0121 잔여)
4
+
5
+ **🩺 `health`/`status` 의 "healthy" 가 프로젝트 안전을 뜻하는 듯 오해되던 것(Codex P2) 보강 — 정직성 일관 적용.**
6
+
7
+ ### 배경
8
+ Codex: 같은 상태에서 `gate` 는 실패하는데 `health --json`/`status --json` 은 `healthy:true`. `health` 의 보안 점검은 `.env`/`.gitignore` 만 보고 하드코딩 시크릿을 무시(1.9.415 handoff 와 동일 근본원인), `status` 의 `healthy` 는 설치 파일 존재만 의미하나 라벨이 오해를 유발.
9
+
10
+ ### 수정
11
+ - **health 보안 점검 정직화**: `_collectSecretFindings`(1.9.415 공유 헬퍼)로 **커밋 대상 하드코딩 시크릿**을 반영 → `checks.security.committedSecrets`/`critical` + `issues` + 최상위 `healthy` 에 전파. 시크릿이 있으면 `health` 가 `healthy:false`. `.env` 없어도 소스 스캔(기존엔 .env 없으면 보안 점검 스킵).
12
+ - **status 의미 명시**: `--json` 에 `scope:"install"` + `healthyMeaning`("설치 파일 존재 여부 — 프로젝트 안전은 gate/scan secrets 사용") 추가. `healthy` 의미(설치 완료)는 보존(비파괴).
13
+
14
+ ### 검증 (회귀 0)
15
+ - **selftest 163→164 PASS** (health `_collectSecretFindings` 와이어 + status scope/meaning).
16
+ - **E2E 417→418 PASS** (시크릿 시 health healthy:false + committedSecrets + status scope:install + 클린 회귀).
17
+
18
+ ### 9번째 외부평가 — 확정 발견 전부 소진
19
+ UR-0121(handoff/scan/encoding/contract + **health/status 라벨**) ✅ · UR-0122(add류) ✅ · UR-0123(contract field) ✅. 잔여는 전략 항목 UR-0124(init 프로파일/명령 계층화/handoff 속도/--compact) + team positional path(소소).
20
+
21
+ ## 1.9.417 — 2026-06-07 — contract verify field 검증 범용화: ## Fields 섹션 불릿 인식 (9번째 외부평가, UR-0123)
22
+
23
+ **🔧 `contract verify` 의 필드 계약 검증이 `tick.` 프리픽스 전용으로 하드코딩돼 범용 spec 에서 무력화되던 것(Opus 발견) 보강.**
24
+
25
+ ### 배경
26
+ Opus: `_parseContractSpec` 의 필드 추출이 `matchAll(/tick\.(name)/g)` 뿐(원래 TICK_SPEC 예제 잔재) → `## Fields` 아래 `- userId` 같은 일반 필드를 전혀 인식 못 해 "모든 필드 존재"로 항상 통과. 범용 명세↔구현 필드 검증이 사실상 불능.
27
+
28
+ ### 수정
29
+ - `_parseContractSpec`: **`## Fields`(또는 `## 필드`) 섹션 한정**으로 불릿(`- name` / `* name: type` / `- name (설명)`)을 필드로 인식. 섹션 한정이라 산문 불릿 오탐 없음. 식별자 직후 `(` 면 함수로 보아 제외.
30
+ - 기존 `tick.X` 인식 + 함수 불릿(declared) 동작은 그대로(회귀 0).
31
+
32
+ ### 검증 (회귀 0)
33
+ - **selftest 162→163 PASS** (Fields 섹션 인식 + Functions 불릿 필드 오인 방지 + `## 필드` 한글 + tick. 회귀).
34
+ - **E2E 416→417 PASS** (## Fields 누락 감지 exit 1 + 충족 시 통과).
35
+
36
+ ### 9번째 외부평가 진행
37
+ UR-0121(handoff false-OK) ✅ · UR-0122(add류 일관성) ✅ · **UR-0123(contract field) ✅** · 잔여: team positional path / status 라벨 / init 프로파일·명령 계층화(UR-0124).
38
+
3
39
  ## 1.9.416 — 2026-06-07 — add류 CLI 인자 일관성: 경로 흡수 차단 + 빈 입력 거부 (9번째 외부평가, UR-0122)
4
40
 
5
41
  **🧩 9번째 외부평가의 최강 UX 발견(3모델 공통: CLI 인자 규칙 불일치) 보강 — `task add`/`requests add`/`decision add` 의 제목 파싱을 단일 출처로 통일.**
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
4
4
  > **A CLI harness that stops AI coding agents from faking completion, duplicating work, forgetting context, and colliding.**
5
5
 
6
- [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.416-green)]() [![tests](https://img.shields.io/badge/e2e-355%2F355-success)]() [![selftest](https://img.shields.io/badge/selftest-162-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-85-brightgreen)]() [![providers](https://img.shields.io/badge/AI_providers-10-brightgreen)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
+ [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.418-green)]() [![tests](https://img.shields.io/badge/e2e-357%2F357-success)]() [![selftest](https://img.shields.io/badge/selftest-164-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-85-brightgreen)]() [![providers](https://img.shields.io/badge/AI_providers-10-brightgreen)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
7
7
 
8
8
  ```
9
9
  ╔══════════════════════════════════════════════════════════════╗
@@ -471,7 +471,7 @@ MIT — © leerness contributors
471
471
  <!-- leerness:project-readme:start -->
472
472
  ## Leerness Project Harness
473
473
 
474
- 이 프로젝트는 Leerness v1.9.416 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
474
+ 이 프로젝트는 Leerness v1.9.418 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
475
475
 
476
476
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
477
477
 
@@ -525,7 +525,7 @@ leerness memory restore decision <date|title>
525
525
 
526
526
  ### MCP server (외부 AI 통합)
527
527
 
528
- Leerness v1.9.416는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
528
+ Leerness v1.9.418는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
529
529
 
530
530
  ```jsonc
531
531
  // 카테고리별
@@ -546,7 +546,7 @@ Leerness v1.9.416는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
546
546
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
547
547
  1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
548
548
 
549
- 현재 누적: **70 라운드 (1.9.40 → 1.9.416)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
549
+ 현재 누적: **70 라운드 (1.9.40 → 1.9.418)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
550
550
 
551
551
  ### 성능 가이드 (1.9.140 측정)
552
552
 
@@ -584,6 +584,6 @@ leerness release pack --close --auto-main-push
584
584
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
585
585
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
586
586
 
587
- Last synced by Leerness v1.9.416: 2026-06-07
587
+ Last synced by Leerness v1.9.418: 2026-06-07
588
588
  <!-- leerness:project-readme:end -->
589
589
 
package/bin/harness.js CHANGED
@@ -31,7 +31,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
31
31
  // 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
32
32
  const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 (MERGE_OVERWRITE_FILES/MINIMAL_SKIP_KEYS 포함)
33
33
 
34
- const VERSION = '1.9.416';
34
+ const VERSION = '1.9.418';
35
35
 
36
36
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
37
37
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -3025,6 +3025,24 @@ function _selfTestCases() {
3025
3025
  { 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; } },
3026
3026
  { 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; } },
3027
3027
  { name: '6번째 외부평가 codex P2 (UR-0101): action 명령(task/decision/rule/lesson add) --json 구조화 출력 (1.9.413)', run: () => { const src = read(__filename); const taskJ = src.includes("log(JSON.stringify({ ok: true, id, status: arg('--status', 'requested'), request: text }))"); const decJ = src.includes("log(JSON.stringify({ ok: true, title }))"); const lesJ = src.includes("log(JSON.stringify({ ok: true, text, tag: tag || null }))"); const ruleJ = src.includes("skipped: !!result.skip"); return taskJ && decJ && lesJ && ruleJ; } },
3028
+ { name: '9th 외부평가 Codex P2 (UR-0121 잔여): health 보안 정직화(커밋 시크릿 반영) + status scope:install (1.9.418)', run: () => {
3029
+ const src = read(__filename);
3030
+ const healthWired = src.includes('_collectSecretFindings(root)') && src.includes('committedSecrets') && src.includes('커밋 대상 하드코딩 시크릿');
3031
+ const statusScope = src.includes("scope: 'install'") && src.includes('healthyMeaning');
3032
+ return healthWired && statusScope && typeof healthCmd === 'function' && typeof status === 'function';
3033
+ } },
3034
+ { name: '9th 외부평가 Opus (UR-0123): contract field 범용화 — ## Fields 섹션 불릿 인식(tick. 회귀 보존) (1.9.417)', run: () => {
3035
+ const m = require('../lib/pure-utils');
3036
+ const r1 = m._parseContractSpec('# S\n\n## Fields\n- userId\n- expiresAt: string\n');
3037
+ const f1 = r1.fields.includes('userId') && r1.fields.includes('expiresAt');
3038
+ const r2 = m._parseContractSpec('# S\n\n## Functions\n- loginUser(id)\n- notField\n');
3039
+ const f2 = r2.fields.length === 0 && r2.declared.includes('loginUser');
3040
+ const r3 = m._parseContractSpec('tick.legacy\n');
3041
+ const f3 = r3.fields.includes('legacy');
3042
+ const r4 = m._parseContractSpec('# S\n\n## 필드\n- 항목은영문만\n- userName\n');
3043
+ const f4 = r4.fields.includes('userName');
3044
+ return f1 && f2 && f3 && f4;
3045
+ } },
3028
3046
  { name: '9th 외부평가 Sonnet/Codex (UR-0122): add류 _parseAddTitle(flag/경로 break) + 빈 입력 거부 와이어 (1.9.416)', run: () => {
3029
3047
  const m = require('../lib/pure-utils');
3030
3048
  if (typeof m._parseAddTitle !== 'function' || m._parseAddTitle !== _parseAddTitle) return false;
@@ -6729,7 +6747,8 @@ function status(root) {
6729
6747
  const files = Object.keys(coreFiles(root, lang, [], { minimal: isMinimal }));
6730
6748
  const missing = files.filter(f => !exists(path.join(root,f)));
6731
6749
  // 1.9.384 (5번째 외부평가/UR-0085): --json 일관성 — AI 에이전트용 구조화 출력.
6732
- if (has('--json')) { log(JSON.stringify({ version: ver, language: lang, minimal: isMinimal, total: files.length, present: files.length - missing.length, missing, healthy: missing.length === 0 }, null, 2)); return; }
6750
+ // 1.9.418 (9th 외부평가 Codex P2): healthy 의미를 명시(설치 파일 존재 프로젝트 안전). 프로젝트 안전은 gate/scan secrets 사용.
6751
+ if (has('--json')) { log(JSON.stringify({ version: ver, language: lang, minimal: isMinimal, scope: 'install', total: files.length, present: files.length - missing.length, missing, healthy: missing.length === 0, healthyMeaning: '설치 파일 존재 여부(프로젝트 안전 아님 — 보안/품질은 leerness gate / scan secrets 사용)' }, null, 2)); return; }
6733
6752
  log(`Leerness: ${ver}${isMinimal ? ' (minimal)' : ''}`);
6734
6753
  log(`Files: ${files.length - missing.length}/${files.length}`);
6735
6754
  if (missing.length) missing.forEach(x => warn('missing: ' + x));
@@ -19369,26 +19388,29 @@ function healthCmd(root) {
19369
19388
  const j = JSON.parse(r.stdout.trim());
19370
19389
  out.checks.drift = { level: j.level, score: j.score, firedCount: (j.fired || []).length };
19371
19390
  } catch { out.checks.drift = { error: 'drift check 실패' }; }
19372
- // 2) 보안 상태 (env + .gitignore)
19391
+ // 2) 보안 상태 (1.9.418, 9th 외부평가 Codex P2): .env/.gitignore + **실제 하드코딩 시크릿 스캔**.
19392
+ // 기존엔 .env 가 .gitignore 에 있으면 critical:false 라 커밋된 하드코딩 시크릿이 있어도 health 가 healthy:true 였음(false-OK).
19393
+ // handoff/scan secrets 와 동일하게 _collectSecretFindings 로 커밋 대상 시크릿을 반영(정직성).
19373
19394
  try {
19395
+ const sec = _collectSecretFindings(root);
19396
+ const committedSecrets = sec.committed.length;
19374
19397
  const envPath = path.join(root, '.env');
19375
- if (exists(envPath)) {
19398
+ const hasDotEnv = exists(envPath);
19399
+ const s = { hasDotEnv, committedSecrets };
19400
+ if (hasDotEnv) {
19376
19401
  const d = envDiff(root);
19377
19402
  const giText = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
19378
19403
  const giLines = giText.split('\n').map(l => l.trim());
19379
19404
  const envInGi = giLines.includes('.env') || giLines.includes('/.env');
19380
19405
  const SECRET_PATTERNS = ['.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json'];
19381
- const missingSecrets = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
19382
- out.checks.security = {
19383
- hasDotEnv: true,
19384
- envInGitignore: envInGi,
19385
- envExampleMissing: d.inEnvOnly,
19386
- gitignoreMissingSecrets: missingSecrets,
19387
- critical: !envInGi
19388
- };
19406
+ s.envInGitignore = envInGi;
19407
+ s.envExampleMissing = d.inEnvOnly;
19408
+ s.gitignoreMissingSecrets = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
19409
+ s.critical = !envInGi || committedSecrets > 0;
19389
19410
  } else {
19390
- out.checks.security = { hasDotEnv: false, ok: true };
19411
+ s.critical = committedSecrets > 0;
19391
19412
  }
19413
+ out.checks.security = s;
19392
19414
  } catch { out.checks.security = { error: '보안 점검 실패' }; }
19393
19415
  // 3) skill 수 + skill query 누적
19394
19416
  try {
@@ -19631,7 +19653,8 @@ function healthCmd(root) {
19631
19653
  // 6) issues 요약 (사용자 글로벌 룰 가시화)
19632
19654
  const issues = [];
19633
19655
  if (out.checks.drift?.level && !/healthy/.test(out.checks.drift.level)) issues.push(`drift ${out.checks.drift.level}`);
19634
- if (out.checks.security?.critical) issues.push('🚨 .env가 .gitignore에 누락 (보안 CRITICAL)');
19656
+ if (out.checks.security?.committedSecrets > 0) issues.push(`🚨 커밋 대상 하드코딩 시크릿 ${out.checks.security.committedSecrets}건 (보안 CRITICAL)`); // 1.9.418 (9th 외부평가 Codex P2)
19657
+ if (out.checks.security?.hasDotEnv && out.checks.security?.envInGitignore === false) issues.push('🚨 .env가 .gitignore에 누락 (보안 CRITICAL)');
19635
19658
  if (out.checks.security?.envExampleMissing?.length) issues.push(`.env→.env.example 누락 ${out.checks.security.envExampleMissing.length}건`);
19636
19659
  if (out.checks.security?.gitignoreMissingSecrets?.length) issues.push(`.gitignore 시크릿 누락 ${out.checks.security.gitignoreMissingSecrets.length}건`);
19637
19660
  out.issues = issues;
package/lib/pure-utils.js CHANGED
@@ -1021,6 +1021,20 @@ function _parseContractSpec(specText) {
1021
1021
  for (const m of s.matchAll(/^\s*(?:[-*+]|\d+\.)\s+([A-Za-z_$][\w$]*)\(/gm)) declared.add(m[1]);
1022
1022
  for (const m of s.matchAll(/`([A-Za-z_$][\w$]*)\s*\(/g)) mentioned.add(m[1]);
1023
1023
  for (const m of s.matchAll(/tick\.([A-Za-z_$][\w$]*)/g)) fields.add(m[1]);
1024
+ // 1.9.417 (9th 외부평가 Opus, UR-0123): `## Fields`(또는 `## 필드`) 섹션 불릿도 필드로 인식.
1025
+ // 기존엔 tick. 프리픽스 전용이라 범용 spec 의 필드 계약이 무력화(원래 TICK_SPEC 예제 잔재). 섹션 한정 파싱이라 산문 오탐 없음.
1026
+ // 불릿 식별자 추출: "- userId" / "* userId: string" / "- userId (설명)" → userId. 식별자 직후 ( 면 함수라 제외(:|공백|줄끝만 허용).
1027
+ {
1028
+ const lines = s.split(/\r\n?|\n/);
1029
+ let inFields = false;
1030
+ for (const line of lines) {
1031
+ const h = line.match(/^#{1,6}\s+(.+?)\s*$/);
1032
+ if (h) { const t = h[1].trim().toLowerCase(); inFields = t === 'fields' || t.startsWith('fields ') || h[1].trim().startsWith('필드'); continue; }
1033
+ if (!inFields) continue;
1034
+ const b = line.match(/^\s*(?:[-*+]|\d+\.)\s+([A-Za-z_$][\w$]*)\s*(?::|\s|$)/);
1035
+ if (b) fields.add(b[1]);
1036
+ }
1037
+ }
1024
1038
  return { declared: [...declared], mentioned: [...mentioned], fields: [...fields] };
1025
1039
  }
1026
1040
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.416",
3
+ "version": "1.9.418",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -5986,5 +5986,53 @@ total++;
5986
5986
  if (!ok) failed++;
5987
5987
  }
5988
5988
 
5989
+ // 1.9.417 회귀 (9th 외부평가 Opus, UR-0123): contract verify field 범용화 — ## Fields 섹션 불릿 누락 감지
5990
+ total++;
5991
+ {
5992
+ let ok = false;
5993
+ try {
5994
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-cfield-'));
5995
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
5996
+ fs.writeFileSync(path.join(d, 'f.md'), '# S\n\n## Fields\n- userId\n- expiresAt\n');
5997
+ fs.writeFileSync(path.join(d, 'f.js'), 'const x={userId:1};\nmodule.exports={x};\n');
5998
+ const cj = cp.spawnSync(process.execPath, [CLI, 'contract', 'verify', path.join(d, 'f.md'), path.join(d, 'f.js'), '--json'], { encoding: 'utf8', timeout: 15000 });
5999
+ let detectOk = false; try { const j = JSON.parse(cj.stdout); detectOk = j.specFields.includes('userId') && j.specFields.includes('expiresAt') && j.missingFields.includes('expiresAt') && !j.missingFields.includes('userId') && j.ok === false && cj.status === 1; } catch {}
6000
+ // 회귀: 모든 필드 충족 시 통과
6001
+ fs.writeFileSync(path.join(d, 'g.js'), 'const x={userId:1,expiresAt:2};\nmodule.exports={x};\n');
6002
+ const cg = cp.spawnSync(process.execPath, [CLI, 'contract', 'verify', path.join(d, 'f.md'), path.join(d, 'g.js'), '--json'], { encoding: 'utf8', timeout: 15000 });
6003
+ let passOk = false; try { const j = JSON.parse(cg.stdout); passOk = j.missingFields.length === 0 && j.ok === true; } catch {}
6004
+ fs.rmSync(d, { recursive: true, force: true });
6005
+ ok = detectOk && passOk;
6006
+ } catch {}
6007
+ console.log(ok ? '✓ B(1.9.417) 9th외부평가: contract field 범용화(## Fields 불릿 누락 감지 + 충족 통과) (UR-0123)' : '✗ contract field 범용화 실패');
6008
+ if (!ok) failed++;
6009
+ }
6010
+
6011
+ // 1.9.418 회귀 (9th 외부평가 Codex P2, UR-0121 잔여): health 보안 정직화 + status scope
6012
+ total++;
6013
+ {
6014
+ let ok = false;
6015
+ try {
6016
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-hlabel-'));
6017
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
6018
+ fs.writeFileSync(path.join(d, 'leak.js'), 'module.exports={apiKey:"sk-test-1234567890abcdefghijklmnopqrstuvwxyz"};');
6019
+ // (1) 시크릿 있으면 health healthy:false + committedSecrets>0
6020
+ const hj = cp.spawnSync(process.execPath, [CLI, 'health', d, '--json'], { encoding: 'utf8', timeout: 20000 });
6021
+ let secOk = false; try { const j = JSON.parse(hj.stdout); secOk = j.healthy === false && j.checks.security.critical === true && j.checks.security.committedSecrets >= 1; } catch {}
6022
+ // (2) status scope:install + healthyMeaning
6023
+ const sj = cp.spawnSync(process.execPath, [CLI, 'status', d, '--json'], { encoding: 'utf8', timeout: 15000 });
6024
+ let scopeOk = false; try { const j = JSON.parse(sj.stdout); scopeOk = j.scope === 'install' && typeof j.healthyMeaning === 'string' && j.healthyMeaning.length > 0; } catch {}
6025
+ // (3) 회귀: 클린 워크스페이스 health healthy:true
6026
+ const dc = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-hclean-'));
6027
+ cp.spawnSync(process.execPath, [CLI, 'init', dc, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
6028
+ const hc = cp.spawnSync(process.execPath, [CLI, 'health', dc, '--json'], { encoding: 'utf8', timeout: 20000 });
6029
+ let cleanOk = false; try { const j = JSON.parse(hc.stdout); cleanOk = j.checks.security.committedSecrets === 0; } catch {}
6030
+ fs.rmSync(d, { recursive: true, force: true }); fs.rmSync(dc, { recursive: true, force: true });
6031
+ ok = secOk && scopeOk && cleanOk;
6032
+ } catch {}
6033
+ console.log(ok ? '✓ B(1.9.418) 9th외부평가: health 보안 정직화(커밋 시크릿→healthy:false) + status scope:install (UR-0121 잔여)' : '✗ health/status 라벨 실패');
6034
+ if (!ok) failed++;
6035
+ }
6036
+
5989
6037
  console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
5990
6038
  if (failed > 0) process.exit(1);