leerness 1.9.398 β†’ 1.9.400

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,42 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.400 β€” 2026-06-07 πŸŽ‰ β€” anti-laziness λͺ…λ Ή --json μ—λŸ¬ ꡬ쑰화 (7번째 λ²„κ·Έν—ŒνŠΈ P1-B, UR-0105)
4
+
5
+ **πŸ”Œ verify-claim / optimism-check / honesty-check 의 `--json` μ—λŸ¬λ„ ꡬ쑰화 JSON β€” AI μ—μ΄μ „νŠΈ self-gate 검증 λͺ…λ Ήμ˜ μžλ™ν™” μ‹ λ’°μ„±.**
6
+
7
+ ### λ°°κ²½ (7번째 λ²„κ·Έν—ŒνŠΈ P1-B Β· failJson νŒ¨ν„΄ ν™•λŒ€)
8
+ λ²„κ·Έν—ŒνŠΈ: μœ„ 3개 anti-laziness 검증 λͺ…λ Ή(AI μ—μ΄μ „νŠΈκ°€ 자기 검증에 μ‚¬μš©)이 μ—†λŠ” T-ID 에 `--json` 으둜 호좜 μ‹œ `βœ— ... μ—†μŒ` ν…μŠ€νŠΈλ₯Ό stdout 에 좜λ ₯ β†’ JSON.parse ν¬λž˜μ‹œ. 1.9.398 의 failJson 헬퍼λ₯Ό 이듀에 ν™•λŒ€ 적용.
9
+
10
+ ### κ΅¬ν˜„
11
+ - verify-claim / optimism-check: missing_args / not_found λ₯Ό `failJson(_j, ...)` 둜.
12
+ - honesty-check: T-ID not_found λ₯Ό failJson 둜.
13
+ - μ‚¬λžŒμš© 좜λ ₯(--json 없을 λ•Œ) 무변경.
14
+
15
+ ### 검증 (νšŒκ·€ 0)
16
+ - **selftest 145β†’146 PASS** (3개 λͺ…λ Ή failJson 와이어 μ†ŒμŠ€ 확인).
17
+ - **E2E 338β†’339 PASS** (3개 --json μ—λŸ¬ β†’ {ok:false,code:not_found} exit1 + μ‚¬λžŒμš© βœ— ν…μŠ€νŠΈ 보쑴).
18
+
19
+ ## 1.9.399 β€” 2026-06-07 β€” ν…Œμ΄λΈ”μ…€ injection 차단: task/rule νŒŒμ΄ν”„Β·κ°œν–‰ (7번째 λ²„κ·Έν—ŒνŠΈ P1-A, UR-0104)
20
+
21
+ **πŸ›‘ 데이터 무결성 β€” task/rule ν…μŠ€νŠΈμ˜ νŒŒμ΄ν”„(|)Β·κ°œν–‰(\\n)이 progress-tracker/rules.md ν‘œλ₯Ό μ†μƒΒ·κ°€μ§œν–‰ μ£Όμž…Β·λ©±λ“±μ„± 무λ ₯ν™”ν•˜λ˜ 것을 차단(μ…€ μ΄μŠ€μΌ€μ΄ν”„).**
22
+
23
+ ### λ°°κ²½ (7번째 β€” λ‚΄λΆ€ λ©€ν‹°μ—μ΄μ „νŠΈ λ²„κ·Έν—ŒνŠΈ, 외뢀리뷰 μ΄ˆμ›”)
24
+ 38 μ—μ΄μ „νŠΈ 포괄 λ²„κ·Έν—ŒνŠΈ(31후보→confirmed 30/refuted 1)κ°€ μ™ΈλΆ€ 리뷰(codex/Opus)κ°€ λͺ» λ³Έ **ν…Œμ΄λΈ”μ…€ injection 클래슀**λ₯Ό 발견. 직접 μž¬ν˜„ 검증:
25
+ - `task add 'fix login | bypass'` β†’ progress-tracker ν–‰ 컬럼 μ‹œν”„νŠΈ, update μ‹œ 영ꡬ μ ˆλ‹¨(JSON λ°±μ—… μ—†μŒ).
26
+ - `task add '...\\n| T-9999 | done | ...'` β†’ κ°€μ§œ done ν–‰ μ£Όμž… + μ‹€μ œ task 무성 μ†Œμ‹€.
27
+ - `rule add 'a | b'` β†’ rules.md 컬럼 λ°€λ¦Ό + dedup(1.9.212) 무λ ₯ν™”λ‘œ 쀑볡 λ£° λ¬΄ν•œ 생성.
28
+
29
+ ### κ΅¬ν˜„
30
+ - **`_cellSafe`/`_cellUnescape`** 순수 헬퍼(lib/pure-utils.js): μ“°κΈ° μ‹œ κ°œν–‰β†’κ³΅λ°± + `|`β†’`\\|`, 읽기 μ‹œ λΉ„μ΄μŠ€μΌ€μ΄ν”„ νŒŒμ΄ν”„(`/(?<!\\)\\|/`)μ—μ„œλ§Œ 뢄리 ν›„ `\\|`β†’`|` 볡원.
31
+ - 적용: `writeProgressRows`/`readProgressRows`(task) + `writeRules`/`readRules`(rule). μ‚¬μš©μž ν…μŠ€νŠΈμ˜ νŒŒμ΄ν”„λŠ” **보쑴**(볡원), κ°œν–‰μ€ 곡백화(ν–‰ μ£Όμž… 차단).
32
+
33
+ ### 검증 (νšŒκ·€ 0)
34
+ - **selftest 144β†’145 PASS** (cellSafe/Unescape reference-equality + νŒŒμ΄ν”„ round-trip + κ°œν–‰ 제거 + 와이어).
35
+ - **E2E 337β†’338 PASS** (task νŒŒμ΄ν”„ 보쑴/κ°œν–‰ κ°€μ§œν–‰ 차단 + rule νŒŒμ΄ν”„ 보쑴/λ©±λ“± 회볡/status λΉ„μ˜€μ—Ό).
36
+ - μ‹€μΈ‘: `task add 'fix login | bypass'` 원본 보쑴, κ°œν–‰ μ£Όμž… T-9999 차단, rule 쀑볡 add β†’ skip(λ©±λ“±).
37
+
38
+ ### λ‹€μŒ(같은 ν΄λŸ¬μŠ€ν„°): decisions/lessons MD projection μ…€ μ•ˆμ „ν™”(UR-0104 μž”μ—¬, JSON canonical 은 이미 μ•ˆμ „).
39
+
3
40
  ## 1.9.398 β€” 2026-06-07 β€” --json μ—λŸ¬ 경둜 ꡬ쑰화 (6번째 외뢀평가 P1-C, UR-0099)
4
41
 
5
42
  **πŸ”Œ `--json` λͺ¨λ“œμ—μ„œ μ—λŸ¬λ„ ꡬ쑰화 JSON(`{ok:false,error,code}`) β€” AI μ—μ΄μ „νŠΈκ°€ μ—λŸ¬ κ²½λ‘œμ—μ„œ JSON.parse μ‹€νŒ¨ν•˜μ§€ μ•Šλ„λ‘.**
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.398-green)]() [![tests](https://img.shields.io/badge/e2e-337%2F337-success)]() [![selftest](https://img.shields.io/badge/selftest-144-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.400-green)]() [![tests](https://img.shields.io/badge/e2e-339%2F339-success)]() [![selftest](https://img.shields.io/badge/selftest-146-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.398 ν•˜λ„€μŠ€λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€. AI μ—μ΄μ „νŠΈλŠ” μž‘μ—… μ „ `leerness handoff`둜 μ»¨ν…μŠ€νŠΈλ₯Ό μ μž¬ν•˜κ³ , μž‘μ—… ν›„ `leerness check`/`leerness audit`/`leerness session close`λ₯Ό μˆ˜ν–‰ν•΄μ•Ό ν•©λ‹ˆλ‹€.
474
+ 이 ν”„λ‘œμ νŠΈλŠ” Leerness v1.9.400 ν•˜λ„€μŠ€λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€. 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.398λŠ” stdio JSON-RPC MCP serverλ₯Ό λ‚΄μž₯ν•©λ‹ˆλ‹€ β€” Claude Code Β· Cursor Β· Codex CLI λ“± μ™ΈλΆ€ AI에 **85개 도ꡬ**λ₯Ό λ…ΈμΆœ:
528
+ Leerness v1.9.400λŠ” 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.398λŠ” 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.398)** Β· λ§€ λΌμš΄λ“œ GitHub release/νƒœκ·Έ 생성 Β· _reports/λŠ” λΉ„κ³΅κ°œ 보쑴.
549
+ ν˜„μž¬ λˆ„μ : **70 λΌμš΄λ“œ (1.9.40 β†’ 1.9.400)** Β· λ§€ λΌμš΄λ“œ 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.398: 2026-06-06
587
+ Last synced by Leerness v1.9.400: 2026-06-06
588
588
  <!-- leerness:project-readme:end -->
589
589
 
package/bin/harness.js CHANGED
@@ -25,13 +25,13 @@ const { _isSecretKey, _isPlaceholderSecret, _looksSecretLike, _mergeLines, _merg
25
25
  _withBuiltinSource, _esc, _roadmapTokenStyles, _parseSkillMd,
26
26
  _migrationGuideText, _parseContractSpec, _gitignoreMatch,
27
27
  _featureGraphTemplate, _parseFeatureGraph, _nextFeatureId, _featureBlock, _featureImpactBfs,
28
- _parseChangelogBetween } = require('../lib/pure-utils'); // 1.9.318~393 (UR-0025/0053/0075/0086/0087): 순수 μœ ν‹Έ λͺ¨λ“ˆ 뢄리
28
+ _parseChangelogBetween, _cellSafe, _cellUnescape } = require('../lib/pure-utils'); // 1.9.318~399 (UR-0025/0053/0075/0086/0087/0104): 순수 μœ ν‹Έ λͺ¨λ“ˆ 뢄리
29
29
  // 1.9.304 (UR-0025): 순수 뢄석/검증 ν•¨μˆ˜ λͺ¨λ“ˆ 뢄리.
30
30
  const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInGit, _epistemicHonestyCheck } = require('../lib/analyzers');
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.398';
34
+ const VERSION = '1.9.400';
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') μ‹œ 호슀트 ν”„λ‘œμ„ΈμŠ€ μ˜€μ—Ό.
@@ -3003,6 +3003,8 @@ function _selfTestCases() {
3003
3003
  { name: '6번째 외뢀평가/codex P1-B: task drop μ‘΄μž¬ν™•μΈ κ°€λ“œ β€” μ—†λŠ” ID κ°€μ§œ row λ°©μ§€ (1.9.396)', run: () => { const src = read(__filename); const i = src.indexOf('function taskDrop(root, id)'); if (i < 0) return false; const body = src.slice(i, i + 700); return body.includes('not found in progress-tracker.md') && body.includes('rows.find(r => r.id === id)') && body.includes('_requireInit'); } },
3004
3004
  { name: '6번째 외뢀평가/codex P1-A (UR-0098): install-safety λ ˆμ‹œν”Ό μ…Έ-무관 + hardeningNote (1.9.397)', run: () => { if (typeof installSafetyCmd !== 'function') return false; const save = process.argv; const _w = process.stdout.write; let out = ''; try { process.argv = ['node', 'h', 'install-safety', '--json']; process.stdout.write = s => { out += s; return true; }; installSafetyCmd({ json: true }); } catch {} finally { process.stdout.write = _w; process.argv = save; } let j; try { j = JSON.parse(out); } catch {} const noPosixPrefix = !!j && Array.isArray(j.safeInstall) && !j.safeInstall.some(x => /^npm_config_\w+=/.test(String(x).trim())); const crossShell = !!j && j.safeInstall.filter(x => String(x).includes('npx --yes')).length >= 2; const noteOk = !!j && typeof j.hardeningNote === 'string' && j.hardeningNote.includes('PowerShell'); return noPosixPrefix && crossShell && noteOk; } },
3005
3005
  { name: '6번째 외뢀평가/codex P1-C (UR-0099): --json μ—λŸ¬ 경둜 ꡬ쑰화 failJson + 와이어 (1.9.398)', run: () => { const io = require('../lib/io'); if (io.failJson !== failJson) return false; const _w = process.stdout.write; const saved = process.exitCode; let jOut = '', hOut = ''; let jExit = 0; try { process.stdout.write = s => { jOut += s; return true; }; process.exitCode = 0; failJson(true, 'tc', 'm'); jExit = process.exitCode; process.stdout.write = s => { hOut += s; return true; }; process.exitCode = 0; failJson(false, 'c', 'humanmsg'); } catch {} finally { process.stdout.write = _w; process.exitCode = saved; } let pj; try { pj = JSON.parse(jOut); } catch {} const jsonOk = !!pj && pj.ok === false && pj.code === 'tc' && pj.error === 'm' && jExit === 1; const humanOk = hOut.includes('βœ—') && hOut.includes('humanmsg') && !hOut.includes('{'); const src = read(__filename); const wired = src.includes("failJson(_j, 'missing_args'") && src.includes("failJson(_j, 'spec_not_found'"); return jsonOk && humanOk && wired; } },
3006
+ { name: '7번째 λ²„κ·Έν—ŒνŠΈ P1-A (UR-0104): ν…Œμ΄λΈ”μ…€ μ•ˆμ „ν™” _cellSafe/_cellUnescape (νŒŒμ΄ν”„/κ°œν–‰ injection 차단) (1.9.399)', run: () => { const m = require('../lib/pure-utils'); if (m._cellSafe !== _cellSafe || m._cellUnescape !== _cellUnescape) return false; const safe = _cellSafe('fix | bug\nrow2'); const noRaw = !/(?<!\\)\|/.test(safe) && !/[\r\n]/.test(safe); const pipeRt = _cellUnescape(_cellSafe('a | b | c')) === 'a | b | c'; const nlGone = _cellSafe('a\nb') === 'a b'; const src = read(__filename); const wired = src.includes('_cellSafe(r.request)') && src.includes('_cellSafe(r.rule)'); return noRaw && pipeRt && nlGone && wired; } },
3007
+ { 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,400}?failJson\(_j, 'not_found'/.test(src); const oc = /function optimismCheckCmd[\s\S]{0,400}?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; } },
3006
3008
  { name: 'VERSION ν˜•μ‹ (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3007
3009
  ];
3008
3010
  }
@@ -5832,7 +5834,8 @@ function readProgressRows(root) {
5832
5834
  const rows = [];
5833
5835
  for (const line of text.split('\n')) {
5834
5836
  if (!/^\| (?:T|M|D)-\d{4} \|/.test(line)) continue;
5835
- const cells = line.split('|').slice(1, -1).map(s => s.trim());
5837
+ // 1.9.399 (7번째 λ²„κ·Έν—ŒνŠΈ P1-A, UR-0104): λΉ„μ΄μŠ€μΌ€μ΄ν”„ νŒŒμ΄ν”„μ—μ„œλ§Œ 뢄리 + μ…€ 볡원 β€” μ‚¬μš©μž ν…μŠ€νŠΈμ˜ '|'(μ΄μŠ€μΌ€μ΄ν”„λ¨)이 μ»¬λŸΌμ„ κΉ¨μ§€ μ•ŠμŒ.
5838
+ const cells = line.split(/(?<!\\)\|/).slice(1, -1).map(s => _cellUnescape(s).trim());
5836
5839
  if (cells.length < 6) continue;
5837
5840
  const [id, status, request, evidence, nextAction, updated] = cells;
5838
5841
  rows.push({ id, status, request, evidence, nextAction, updated });
@@ -5853,8 +5856,9 @@ function progressHeader(root) {
5853
5856
  return text.slice(0, text.indexOf('\n', idx)).trimEnd();
5854
5857
  }
5855
5858
  function writeProgressRows(root, header, rows) {
5859
+ // 1.9.399 (7번째 λ²„κ·Έν—ŒνŠΈ P1-A, UR-0104): μ…€ μ½˜ν…μΈ  μ•ˆμ „ν™” β€” κ°œν–‰β†’κ³΅λ°±, '|'β†’'\|'. μ‚¬μš©μž ν…μŠ€νŠΈμ˜ νŒŒμ΄ν”„/κ°œν–‰μ΄ ν‘œ 행을 손상/μ£Όμž… λͺ» 함.
5856
5860
  const composed = header + '\n' +
5857
- rows.map(r => `| ${r.id} | ${r.status} | ${r.request} | ${r.evidence} | ${r.nextAction} | ${r.updated} |`).join('\n') +
5861
+ rows.map(r => `| ${_cellSafe(r.id)} | ${_cellSafe(r.status)} | ${_cellSafe(r.request)} | ${_cellSafe(r.evidence)} | ${_cellSafe(r.nextAction)} | ${_cellSafe(r.updated)} |`).join('\n') +
5858
5862
  (rows.length ? '\n' : '');
5859
5863
  writeUtf8(progressPath(root), composed);
5860
5864
  }
@@ -9232,10 +9236,11 @@ function _gitChangedFiles(root) {
9232
9236
  // _claimFileInGit β†’ lib/analyzers.js (1.9.304 UR-0025)
9233
9237
  function verifyClaimCmd(root, taskId) {
9234
9238
  root = absRoot(root);
9235
- if (!taskId) return fail('verify-claim <T-ID> ν•„μš”. 예: leerness verify-claim T-0008');
9239
+ const _j = has('--json'); // 1.9.400 (7번째 λ²„κ·Έν—ŒνŠΈ P1-B, UR-0105): --json μ—λŸ¬λ„ ꡬ쑰화
9240
+ if (!taskId) return failJson(_j, 'missing_args', 'verify-claim <T-ID> ν•„μš”. 예: leerness verify-claim T-0008');
9236
9241
  const rows = readProgressRows(root);
9237
9242
  const row = rows.find(r => r.id === taskId);
9238
- if (!row) return fail(`progress-tracker.md에 ${taskId} μ—†μŒ.`);
9243
+ if (!row) return failJson(_j, 'not_found', `progress-tracker.md에 ${taskId} μ—†μŒ.`);
9239
9244
 
9240
9245
  const evidence = row.evidence || '';
9241
9246
  // 1.9.20: 파일 경둜 μΆ”μΆœ β€” 도메인 폴더 μžλ™ 인식 + 루트 λ©”νƒ€νŒŒμΌ
@@ -9991,7 +9996,7 @@ function honestyCheckCmd(root, arg1) {
9991
9996
  if (textArg) { subject = String(textArg); sourceLabel = '--text'; }
9992
9997
  else if (arg1 && !arg1.startsWith('-')) {
9993
9998
  const row = readProgressRows(root).find(r => r.id === arg1);
9994
- if (!row) { fail(`progress-tracker.md에 ${arg1} μ—†μŒ.`); process.exitCode = 1; return; }
9999
+ if (!row) { failJson(has('--json'), 'not_found', `progress-tracker.md에 ${arg1} μ—†μŒ.`); return; } // 1.9.400 (UR-0105): --json μ—λŸ¬ ꡬ쑰화
9995
10000
  subject = row.evidence || ''; sourceLabel = `${arg1} evidence`;
9996
10001
  } else { fail('μ‚¬μš©λ²•: leerness honesty-check <T-ID> λ˜λŠ” leerness honesty-check --text "<μ£Όμž₯>"'); process.exitCode = 1; return; }
9997
10002
  const r = _epistemicHonestyCheck(subject);
@@ -10009,10 +10014,11 @@ function honestyCheckCmd(root, arg1) {
10009
10014
  }
10010
10015
  function optimismCheckCmd(root, taskId) {
10011
10016
  root = absRoot(root || process.cwd());
10012
- if (!taskId) return fail('optimism-check <T-ID> ν•„μš”. 예: leerness optimism-check T-0001');
10017
+ const _j = has('--json'); // 1.9.400 (7번째 λ²„κ·Έν—ŒνŠΈ P1-B, UR-0105): --json μ—λŸ¬λ„ ꡬ쑰화
10018
+ if (!taskId) return failJson(_j, 'missing_args', 'optimism-check <T-ID> ν•„μš”. 예: leerness optimism-check T-0001');
10013
10019
  const rows = readProgressRows(root);
10014
10020
  const row = rows.find(r => r.id === taskId);
10015
- if (!row) return fail(`progress-tracker.md에 ${taskId} μ—†μŒ.`);
10021
+ if (!row) return failJson(_j, 'not_found', `progress-tracker.md에 ${taskId} μ—†μŒ.`);
10016
10022
 
10017
10023
  const codeText = _scanCodeForPatterns(root);
10018
10024
  const suspects = _detectOptimism(row.evidence || '', codeText);
@@ -13565,7 +13571,8 @@ function readRules(root) {
13565
13571
  const rules = [];
13566
13572
  for (const line of read(f).split('\n')) {
13567
13573
  if (!/^\| R-\d{4} \|/.test(line)) continue;
13568
- const cells = line.split('|').slice(1, -1).map(s => s.trim());
13574
+ // 1.9.399 (7번째 λ²„κ·Έν—ŒνŠΈ P1-A, UR-0104): λΉ„μ΄μŠ€μΌ€μ΄ν”„ νŒŒμ΄ν”„ 뢄리 + 볡원 β€” rule ν…μŠ€νŠΈμ˜ '|'κ°€ 컬럼 λ°€λ¦Ό/λ©±λ“±μ„± 무λ ₯ν™”λ₯Ό λͺ» μΌμœΌν‚΄.
13575
+ const cells = line.split(/(?<!\\)\|/).slice(1, -1).map(s => _cellUnescape(s).trim());
13569
13576
  if (cells.length < 6) continue;
13570
13577
  rules.push({ id: cells[0], trigger: cells[1], rule: cells[2], added: cells[3], status: cells[4], lastVerified: cells[5] });
13571
13578
  }
@@ -13573,7 +13580,8 @@ function readRules(root) {
13573
13580
  }
13574
13581
 
13575
13582
  function writeRules(root, rules) {
13576
- const body = rules.map(r => `| ${r.id} | ${r.trigger} | ${r.rule} | ${r.added} | ${r.status} | ${r.lastVerified || '-'} |`).join('\n');
13583
+ // 1.9.399 (7번째 λ²„κ·Έν—ŒνŠΈ P1-A, UR-0104): μ…€ μ•ˆμ „ν™” β€” rule ν…μŠ€νŠΈμ˜ νŒŒμ΄ν”„/κ°œν–‰ 차단(컬럼 밀림·쀑볡 λ£° λ¬΄ν•œμƒμ„± λ°©μ§€).
13584
+ const body = rules.map(r => `| ${_cellSafe(r.id)} | ${_cellSafe(r.trigger)} | ${_cellSafe(r.rule)} | ${_cellSafe(r.added)} | ${_cellSafe(r.status)} | ${_cellSafe(r.lastVerified || '-')} |`).join('\n');
13577
13585
  writeUtf8(rulesPath(root), _rulesHeader() + '\n' + body + (body ? '\n' : ''));
13578
13586
  }
13579
13587
 
package/lib/pure-utils.js CHANGED
@@ -935,7 +935,9 @@ module.exports = {
935
935
  // 1.9.391 (UR-0025): feature 영ν–₯ BFS (순수, 곡유)
936
936
  _featureImpactBfs,
937
937
  // 1.9.393 (UR-0025): CHANGELOG 버전 ꡬ간 μ°¨λΆ„ νŒŒμ„œ (순수, 곡유)
938
- _parseChangelogBetween
938
+ _parseChangelogBetween,
939
+ // 1.9.399 (7번째 λ²„κ·Έν—ŒνŠΈ P1-A, UR-0104): markdown ν…Œμ΄λΈ” μ…€ μ•ˆμ „ν™”(νŒŒμ΄ν”„/κ°œν–‰ injection 차단)
940
+ _cellSafe, _cellUnescape
939
941
  };
940
942
 
941
943
  // 1.9.355 (UR-0075 Phase A): AI μ—μ΄μ „νŠΈμš© ν¬λ‘œμŠ€λ²„μ „ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ μ•ˆμ „ μ›Œν¬ν”Œλ‘œ κ°€μ΄λ“œ (순수 ν…μŠ€νŠΈ). μž„μ‹œμ„€μΉ˜ + --path + λ°±μ—… + diff 검증.
@@ -1167,3 +1169,8 @@ function _parseChangelogBetween(changelogText, fromV, toV) {
1167
1169
  }
1168
1170
  return ranged;
1169
1171
  }
1172
+ // 1.9.399 (7번째 λ²„κ·Έν—ŒνŠΈ P1-A, UR-0104): markdown ν…Œμ΄λΈ” μ…€ μ•ˆμ „ν™” β€” κ°œν–‰(ν–‰ μ£Όμž…)Β·νŒŒμ΄ν”„(컬럼 μ‹œν”„νŠΈ) 차단.
1173
+ // _cellSafe: μ“°κΈ° μ‹œ κ°œν–‰β†’κ³΅λ°±, '|'β†’'\|'(μ΄μŠ€μΌ€μ΄ν”„). _cellUnescape: 읽기 μ‹œ '\|'β†’'|' 볡원.
1174
+ // table νŒŒμ„œλŠ” split(/(?<!\\)\|/) 둜 λΉ„μ΄μŠ€μΌ€μ΄ν”„ νŒŒμ΄ν”„μ—μ„œλ§Œ 뢄리 β†’ μ‚¬μš©μž ν…μŠ€νŠΈμ˜ νŒŒμ΄ν”„/κ°œν–‰μ΄ 데이터 μ†μƒΒ·κ°€μ§œν–‰ μ£Όμž…μ„ λͺ» μΌμœΌν‚΄.
1175
+ function _cellSafe(s) { return String(s == null ? '' : s).replace(/\r\n|\r|\n/g, ' ').replace(/\|/g, '\\|'); }
1176
+ function _cellUnescape(s) { return String(s == null ? '' : s).replace(/\\\|/g, '|'); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.398",
3
+ "version": "1.9.400",
4
4
  "description": "Leerness: λΉ„νŒŒκ΄΄ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜, μžλ™ 버전 κ°μ§€Β·μ—…λ°μ΄νŠΈ, κ³„νš/μ§„ν–‰/ν•Έλ“œμ˜€ν”„ μžλ™ν™”, κ²ŒμœΌλ¦„Β·μ‹œν¬λ¦ΏΒ·μΈμ½”λ”© μžλ™ κ°€λ“œ, Claude Code μŠ¬λž˜μ‹œ 톡합을 κ°–μΆ˜ ν•œκ΅­μ–΄ μš°μ„  AI 개발 ν•˜λ„€μŠ€.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -5558,5 +5558,49 @@ total++;
5558
5558
  if (!ok) failed++;
5559
5559
  }
5560
5560
 
5561
+ // 1.9.399 νšŒκ·€ (7번째 λ²„κ·Έν—ŒνŠΈ P1-A, UR-0104): ν…Œμ΄λΈ”μ…€ injection 차단 β€” task/rule ν…μŠ€νŠΈμ˜ νŒŒμ΄ν”„(|) 보쑴 + κ°œν–‰ κ°€μ§œν–‰ μ£Όμž… 차단 + rule λ©±λ“±μ„±
5562
+ total++;
5563
+ {
5564
+ let ok = false;
5565
+ try {
5566
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-cellinj-'));
5567
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
5568
+ cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'fix login | bypass', '--path', d, '--no-review'], { encoding: 'utf8', timeout: 15000 });
5569
+ cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'real\n| T-9999 | done | x | y | z | w |', '--path', d, '--no-review'], { encoding: 'utf8', timeout: 15000 });
5570
+ const tl = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'task', 'list', '--path', d, '--json'], { encoding: 'utf8', timeout: 15000 }).stdout);
5571
+ const ts = tl.tasks || tl;
5572
+ const pipeOk = ts.some(t => t.request === 'fix login | bypass'); // νŒŒμ΄ν”„ 원본 보쑴
5573
+ const noInject = !ts.some(t => t.id === 'T-9999' || t.status === 'done'); // κ°œν–‰ κ°€μ§œν–‰ μ£Όμž… 차단
5574
+ // rule νŒŒμ΄ν”„ + λ©±λ“±μ„±(쀑볡 add β†’ skip)
5575
+ cp.spawnSync(process.execPath, [CLI, 'rule', 'add', 'lint | typecheck', '--trigger', 'every-commit', '--path', d], { encoding: 'utf8', timeout: 15000 });
5576
+ cp.spawnSync(process.execPath, [CLI, 'rule', 'add', 'lint | typecheck', '--trigger', 'every-commit', '--path', d], { encoding: 'utf8', timeout: 15000 });
5577
+ const rl = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'rule', 'list', '--path', d, '--json'], { encoding: 'utf8', timeout: 15000 }).stdout);
5578
+ const rs = rl.rules || rl;
5579
+ const ruleOk = rs.length === 1 && rs[0].rule === 'lint | typecheck' && rs[0].status === 'active'; // νŒŒμ΄ν”„ 보쑴 + λ©±λ“± + status λΉ„μ˜€μ—Ό
5580
+ fs.rmSync(d, { recursive: true, force: true });
5581
+ ok = pipeOk && noInject && ruleOk;
5582
+ } catch {}
5583
+ console.log(ok ? 'βœ“ B(1.9.399) 7thλ²„κ·Έν—ŒνŠΈ P1-A: ν…Œμ΄λΈ”μ…€ injection 차단(task/rule νŒŒμ΄ν”„ 보쑴+κ°œν–‰ κ°€μ§œν–‰ 차단+rule λ©±λ“±) (UR-0104)' : 'βœ— ν…Œμ΄λΈ”μ…€ injection 차단 μ‹€νŒ¨');
5584
+ if (!ok) failed++;
5585
+ }
5586
+
5587
+ // 1.9.400 νšŒκ·€ (7번째 λ²„κ·Έν—ŒνŠΈ P1-B, UR-0105): verify-claim/optimism-check/honesty-check --json μ—λŸ¬κ°€ ꡬ쑰화 JSON + μ‚¬λžŒμš© 보쑴
5588
+ total++;
5589
+ {
5590
+ let ok = false;
5591
+ try {
5592
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-antilazyjson-'));
5593
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
5594
+ const jsonErr = (cmd) => { const r = cp.spawnSync(process.execPath, [CLI, cmd, 'T-9999', '--path', d, '--json'], { encoding: 'utf8', timeout: 15000 }); try { const j = JSON.parse(r.stdout); return j.ok === false && j.code === 'not_found' && r.status === 1; } catch { return false; } };
5595
+ const allJson = jsonErr('verify-claim') && jsonErr('optimism-check') && jsonErr('honesty-check');
5596
+ const hr = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-9999', '--path', d], { encoding: 'utf8', timeout: 15000 });
5597
+ const humanOk = hr.status === 1 && /βœ—/.test(hr.stdout || '') && !/^\s*\{/.test(hr.stdout || '');
5598
+ fs.rmSync(d, { recursive: true, force: true });
5599
+ ok = allJson && humanOk;
5600
+ } catch {}
5601
+ console.log(ok ? 'βœ“ B(1.9.400) 7thλ²„κ·Έν—ŒνŠΈ P1-B: anti-laziness(verify-claim/optimism/honesty) --json μ—λŸ¬ ꡬ쑰화 + μ‚¬λžŒμš© 보쑴 (UR-0105)' : 'βœ— anti-laziness --json μ—λŸ¬ μ‹€νŒ¨');
5602
+ if (!ok) failed++;
5603
+ }
5604
+
5561
5605
  console.log(`\nE2E result: ${total - failed}/${total} passed Β· ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
5562
5606
  if (failed > 0) process.exit(1);