leerness 1.9.212 β†’ 1.9.213

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,80 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.213 β€” 2026-05-22
4
+
5
+ **🎯 intent inference + scope expansion 게이트 (μ‚¬μš©μž λͺ…μ‹œ) β€” λ§ˆμ§€λ§‰ pending task μ™„μ„±.**
6
+
7
+ ### μ‚¬μš©μž λͺ…μ‹œ
8
+ > *"μ‚¬μš©μžκ°€ λ§ν•œ λ‚΄μš©λ§Œ μˆ˜μ •ν•΄μ•Όν•˜λŠ” κ²½μš°λ„ μžˆμ§€λ§Œ, κ΄€λ ¨μžˆλŠ”κ²ƒμ΄λ‚˜ λ³΄κ°•ν•˜λ©΄ 쒋을 뢀뢄을 νŒŒμ•…ν•˜μ—¬ μ§„ν–‰ / κ²Œμž„ κ°œλ°œμ—μ„œ λ§΅+캐릭터+κΈ°λ³ΈκΈ°λŠ₯ μš”μ²­ μ‹œ μ˜λ„ νŒŒμ•…"*
9
+
10
+ ### 1. 3원칙 μ•ˆμ „ 섀계
11
+ 1. **Always-Off, Opt-In** β€” κΈ°λ³Έ λΉ„ν™œμ„±, λͺ…μ‹œμ  호좜 (`leerness intent expand`) λ˜λŠ” review-request 톡합 μ‹œλ§Œ μž‘λ™
12
+ 2. **Dry-run κΈ°λ³Έ** β€” μ‹€μ œ task add **μ ˆλŒ€ X**, λͺ¨λ“  좜λ ₯은 후보 보고만
13
+ 3. **λͺ…μ‹œ vs μΆ”λ‘  뢄리 라벨링** β€” `πŸ‘€ μ‚¬μš©μž λͺ…μ‹œ` vs `πŸ€– AI μΆ”λ‘  ν™•μž₯` 항상 ꡬ뢄
14
+
15
+ ### 2. `_classifyIntent(text)` β€” 3단계 λΆ„λ₯˜
16
+ - **precise** ("μ •ν™•νžˆ", "κ·Έκ²ƒλ§Œ", "κ·ΈλŒ€λ‘œ", "λ§ν•œλŒ€λ‘œ", "only") β†’ AI μΆ”λ‘  ν™•μž₯ **λΉ„ν™œμ„±**
17
+ - **broad** ("κΈ°λ³Έ", "포괄적", "λ“±λ“±", "λ‹€μ–‘ν•œ", "전체", "ν•„μš”ν•œ") β†’ ν™•μž₯ 후보 N건 dry-run μ œμ‹œ
18
+ - **default** (λͺ…μ‹œ μ—†μŒ) β†’ λͺ…μ‹œ μš°μ„ , ν™•μž₯ 후보 κ²€ν† μš©μœΌλ‘œ ν‘œμ‹œ
19
+
20
+ ### 3. `.harness/domain-catalog.json` β€” 5 default + user νŽΈμ§‘ κ°€λŠ₯
21
+ - **game** (10 μ»΄ν¬λ„ŒνŠΈ): map, character, gameLoop, collision, camera, hud, audio, save, menu, input
22
+ - **web** (8 μ»΄ν¬λ„ŒνŠΈ): routing, state, auth, api, db, ui, test, deploy
23
+ - **api** (7 μ»΄ν¬λ„ŒνŠΈ): endpoint, auth, rate-limit, validation, error, logging, docs
24
+ - **cli** (6 μ»΄ν¬λ„ŒνŠΈ): argParser, help, config, output, error, completion
25
+ - **data** (6 μ»΄ν¬λ„ŒνŠΈ): ingest, transform, storage, query, validation, lineage
26
+
27
+ ### 4. `_inferScopeExpansion(text, root)` β€” ν™•μž₯ 후보 dry-run
28
+ - 도메인 μžλ™ 탐지 (alias λ§€μΉ­)
29
+ - μ‚¬μš©μž λͺ…μ‹œ mention μΆ”μΆœ
30
+ - λ‚˜λ¨Έμ§€ β†’ AI μΆ”λ‘  ν™•μž₯ 후보
31
+ - **mode: 'dry-run' κ°•μ œ** (μ‹€ν–‰ X)
32
+
33
+ ### 5. `leerness intent <classify|expand|domains>` CLI
34
+ - `classify "<req>"` β€” μ˜λ„ λΆ„λ₯˜ + μ‹ ν˜Έ
35
+ - `expand "<req>"` β€” 도메인 탐지 + ν™•μž₯ 후보 dry-run
36
+ - `domains` β€” catalog 좜λ ₯
37
+ - λͺ¨λ‘ `--json` 지원
38
+
39
+ ### 6. μ‚¬μš©μž κ²Œμž„ μ˜ˆμ‹œ 검증
40
+ ```
41
+ $ leerness intent expand "λ§΅κ³Ό 캐릭터 + κΈ°λ³Έ κ²Œμž„ κΈ°λŠ₯ ν¬κ΄„μ μœΌλ‘œ λ§Œλ“€μ–΄μ€˜"
42
+ 🌐 intent: broad πŸ“¦ domain: game (matched: "κ²Œμž„")
43
+
44
+ ## πŸ‘€ μ‚¬μš©μž λͺ…μ‹œ (2)
45
+ β€’ character β€” 캐릭터/μŠ€ν”„λΌμ΄νŠΈ + μ• λ‹ˆλ©”μ΄μ…˜ μƒνƒœλ¨Έμ‹ 
46
+ β€’ gameLoop β€” κ²Œμž„ 루프 (tick/render/update)
47
+
48
+ ## πŸ€– AI μΆ”λ‘  ν™•μž₯ 후보 (8, dry-run)
49
+ [1] map / [2] collision / [3] camera / [4] hud / [5] audio / [6] save / [7] menu / [8] input
50
+
51
+ β†’ μ§„ν–‰ μ‹œ λͺ…μ‹œ 승인 ν•„μš”: leerness task add "<μ„ νƒν•œ 후보 μš”μ²­>"
52
+ β†’ μ „λΆ€ λ¬΄μ‹œν•˜λ €λ©΄ μΆ”κ°€ task add 없이 λͺ…μ‹œ μš”μ²­λ§Œ μ§„ν–‰
53
+ ```
54
+
55
+ ### 7. precise μ˜λ„ 보호 검증
56
+ ```
57
+ $ leerness intent expand "μ •ν™•νžˆ κ²Œμž„μ˜ 맡만 μˆ˜μ •ν•΄μ€˜ κ·Έκ²ƒλ§Œ"
58
+ πŸ›‘ intent: precise β€” AI μΆ”λ‘  ν™•μž₯ λΉ„ν™œμ„± (μ‚¬μš©μž μ˜λ„ 보호)
59
+ ```
60
+
61
+ ### 8. λˆ„μ  νšŒκ·€ (1.9.200~212) β€” λͺ¨λ‘ μœ μ§€
62
+
63
+ ### 9. stress-v158 β€” 19/19 PASS
64
+ - 1.9.213 (10) + μ„±λŠ₯ (2) + λˆ„μ  νšŒκ·€ (7)
65
+ - κ²Œμž„ μ˜ˆμ‹œ broad β†’ 8 ν™•μž₯ 후보 / precise β†’ 0 ν™•μž₯ (μ˜λ„ 보호) / web 도메인 탐지
66
+ - μ„±λŠ₯: --version cold start avg 388ms Β· MCP 54 도ꡬ 434ms
67
+
68
+ ### 10. μžλ™ release (75 λΌμš΄λ“œ main-push streak Β· 36 λΌμš΄λ“œ npm publish streak)
69
+
70
+ ### 11. πŸŽ‰ μ‚¬μš©μž λͺ…μ‹œ task 백둜그 **μ™„μ „ μ†Œμ§„** (#307κΉŒμ§€ λͺ¨λ‘ completed)
71
+ - 1.9.207~213 7 λΌμš΄λ“œ 연속 μ‚¬μš©μž λͺ…μ‹œ μš”μ²­ 처리:
72
+ - 207 user-requests audit / 208 platform constraints / 209 pre-wake audit
73
+ - 210 adaptive wakeup / 211 .harness→.leerness migration / 212 idempotency dedup
74
+ - 213 intent inference + scope expansion 게이트
75
+
76
+ ---
77
+
3
78
  ## 1.9.212 β€” 2026-05-22
4
79
 
5
80
  **πŸ” λ©±λ“±μ„± 감사 + ruleAdd/taskAdd dedup 보강 (μ‚¬μš©μž λͺ…μ‹œ).**
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **AI μ½”λ”© μ—μ΄μ „νŠΈμ˜ κ±°μ§“ μ™„λ£ŒΒ·μ€‘λ³΅Β·λ§κ°Β·μΆ©λŒμ„ λ§‰μ•„μ£ΌλŠ” κ²€μˆ˜Β·κΈ°μ–΅Β·ν˜‘μ—… CLI ν•˜λ„€μŠ€.**
4
4
 
5
- [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.212-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v157-17%2F17-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-54-brightgreen)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-142-blueviolet)]() [![main-push](https://img.shields.io/badge/release--main--push-74_rounds-success)]() [![goal-matrix](https://img.shields.io/badge/λͺ©μ _맀트릭슀-98%2F100_(5μΆ•)-brightgreen)]() [![idempotency](https://img.shields.io/badge/idempotency-audit_4μ˜μ—­-success)]() [![perf](https://img.shields.io/badge/cold_start-395ms-success)]() [![install](https://img.shields.io/badge/npm_i_leerness-μ¦‰μ‹œ_μ‚¬μš©_κ°€λŠ₯-success)]() [![sandbox](https://img.shields.io/badge/runCommandSafe-cwd_jail%2Benv_scrub-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
5
+ [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.213-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v158-19%2F19-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-54-brightgreen)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-143-blueviolet)]() [![main-push](https://img.shields.io/badge/release--main--push-75_rounds-success)]() [![goal-matrix](https://img.shields.io/badge/λͺ©μ _맀트릭슀-98%2F100_(5μΆ•)-brightgreen)]() [![intent](https://img.shields.io/badge/intent-inference_5도메인-success)]() [![perf](https://img.shields.io/badge/cold_start-388ms-success)]() [![install](https://img.shields.io/badge/npm_i_leerness-μ¦‰μ‹œ_μ‚¬μš©_κ°€λŠ₯-success)]() [![sandbox](https://img.shields.io/badge/runCommandSafe-cwd_jail%2Benv_scrub-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
6
 
7
7
  ```
8
8
  ╔══════════════════════════════════════════════════════════════╗
@@ -12,9 +12,9 @@
12
12
  β•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β• β–ˆβ–ˆβ•”β•β•β• β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β• β•šβ•β•β•β•β–ˆβ–ˆβ•‘ β•‘
13
13
  β•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘ β•‘
14
14
  β•‘ β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β•β•šβ•β• β•šβ•β•β•šβ•β• β•šβ•β•β•β•β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β• β•‘
15
- β•‘ v1.9.212 AI Agent Reliability Harness + Sandbox β•‘
15
+ β•‘ v1.9.213 AI Agent Reliability Harness + Sandbox β•‘
16
16
  β•‘ verify Β· remember Β· orchestrate Β· audit Β· sandbox Β· drift β•‘
17
- β•‘ πŸ” λ©±λ“±μ„± 감사 + ruleAdd/taskAdd dedup (μ‚¬μš©μž λͺ…μ‹œ) β•‘
17
+ β•‘ 🎯 intent inference + scope expansion 5도메인 (μ‚¬μš©μž λͺ…μ‹œ) β•‘
18
18
  β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
19
19
  ```
20
20
 
package/bin/harness.js CHANGED
@@ -7,7 +7,7 @@ const cp = require('child_process');
7
7
  const os = require('os'); // 1.9.178: _publishToNpm μ—μ„œ os.tmpdir() μ‚¬μš© (μ „μ—­ import)
8
8
  const readline = require('readline');
9
9
 
10
- const VERSION = '1.9.212';
10
+ const VERSION = '1.9.213';
11
11
 
12
12
  // 1.9.184: DEP0190 (child_process shell: true) deprecation warning μ–΅μ œ (μ‚¬μš©μž λͺ…μ‹œ).
13
13
  // leerness λŠ” cross-platform PATH resolution 을 μœ„ν•΄ shell: true λ₯Ό μ˜λ„μ μœΌλ‘œ μ‚¬μš© (claude.cmd / ollama.cmd λ“± Windows .cmd 처리).
@@ -2526,6 +2526,175 @@ function _buildWorkspaceReferenceGuide(root, dirName) {
2526
2526
  return lines.join('\n');
2527
2527
  }
2528
2528
 
2529
+ // 1.9.213: intent inference + scope expansion 게이트 (μ‚¬μš©μž λͺ…μ‹œ)
2530
+ // "μ‚¬μš©μžκ°€ λ§ν•œ λ‚΄μš©λ§Œ μˆ˜μ •ν•΄μ•Όν•˜λŠ” κ²½μš°λ„ μžˆμ§€λ§Œ, κ΄€λ ¨μžˆλŠ”κ²ƒμ΄λ‚˜ λ³΄κ°•ν•˜λ©΄ 쒋을 뢀뢄을 νŒŒμ•…ν•˜μ—¬ μ§„ν–‰"
2531
+ // "κ²Œμž„ κ°œλ°œμ—μ„œ λ§΅+캐릭터+κΈ°λ³ΈκΈ°λŠ₯ μš”μ²­ μ‹œ μ˜λ„ νŒŒμ•…"
2532
+ // 3원칙: (1) Always-Off Opt-In, (2) Dry-run κΈ°λ³Έ (μ‹€ν–‰ X), (3) λͺ…μ‹œ vs μΆ”λ‘  뢄리 라벨링
2533
+ // .harness/domain-catalog.json μ‚¬μš©μž νŽΈμ§‘ κ°€λŠ₯ + default catalog 5μ’… (game/web/api/cli/data)
2534
+ function _domainCatalogPath(root) { return path.join(root, '.harness', 'domain-catalog.json'); }
2535
+ const _DEFAULT_DOMAIN_CATALOG = {
2536
+ version: '1.9.213',
2537
+ domains: {
2538
+ game: {
2539
+ aliases: ['κ²Œμž„', 'game', 'unity', 'unreal', 'godot', 'phaser', 'gamedev'],
2540
+ components: [
2541
+ { key: 'map', desc: 'λ§΅/타일 μ‹œμŠ€ν…œ + μ˜μ—­/슀폰' },
2542
+ { key: 'character', desc: '캐릭터/μŠ€ν”„λΌμ΄νŠΈ + μ• λ‹ˆλ©”μ΄μ…˜ μƒνƒœλ¨Έμ‹ ' },
2543
+ { key: 'gameLoop', desc: 'κ²Œμž„ 루프 (tick/render/update)' },
2544
+ { key: 'collision', desc: '좩돌 감지 (AABB/SAT/grid)' },
2545
+ { key: 'camera', desc: '카메라 follow + μ˜μ—­ μ œν•œ' },
2546
+ { key: 'hud', desc: 'HUD/UI (HP/score/inventory)' },
2547
+ { key: 'audio', desc: 'μ‚¬μš΄λ“œ λ§€λ‹ˆμ € (BGM/SFX)' },
2548
+ { key: 'save', desc: 'μ €μž₯/λ‘œλ“œ (slot μ‹œμŠ€ν…œ)' },
2549
+ { key: 'menu', desc: '메뉴 (메인/μΌμ‹œμ •μ§€/μ„€μ •)' },
2550
+ { key: 'input', desc: 'μž…λ ₯ ν•Έλ“€λŸ¬ (ν‚€λ³΄λ“œ/νŒ¨λ“œ/ν„°μΉ˜)' }
2551
+ ]
2552
+ },
2553
+ web: {
2554
+ aliases: ['μ›Ή', 'web', 'website', 'webapp', 'nextjs', 'react', 'vue', 'svelte', 'frontend'],
2555
+ components: [
2556
+ { key: 'routing', desc: 'λΌμš°νŒ… (path/dynamic/nested)' },
2557
+ { key: 'state', desc: 'μƒνƒœ 관리 (context/redux/zustand)' },
2558
+ { key: 'auth', desc: '인증 (OAuth/JWT/session)' },
2559
+ { key: 'api', desc: 'API ν΄λΌμ΄μ–ΈνŠΈ (fetch/axios/tanstack-query)' },
2560
+ { key: 'db', desc: 'DB 연동 (ORM/migration/seed)' },
2561
+ { key: 'ui', desc: 'UI μ»΄ν¬λ„ŒνŠΈ 라이브러리' },
2562
+ { key: 'test', desc: 'ν…ŒμŠ€νŠΈ (unit/e2e/visual)' },
2563
+ { key: 'deploy', desc: '배포 (Vercel/Netlify/Cloudflare)' }
2564
+ ]
2565
+ },
2566
+ api: {
2567
+ aliases: ['api', 'rest', 'graphql', 'endpoint', 'backend', 'server'],
2568
+ components: [
2569
+ { key: 'endpoint', desc: 'μ—”λ“œν¬μΈνŠΈ λΌμš°νŒ… + HTTP method' },
2570
+ { key: 'auth', desc: '인증/인가 (API key/OAuth/JWT)' },
2571
+ { key: 'rate-limit', desc: 'rate limit (RPS/RPM/token bucket)' },
2572
+ { key: 'validation', desc: 'μž…λ ₯ 검증 (zod/joi/yup)' },
2573
+ { key: 'error', desc: 'μ—λŸ¬ 핸듀링 + 응닡 ν˜•μ‹' },
2574
+ { key: 'logging', desc: 'λ‘œκΉ… + λͺ¨λ‹ˆν„°λ§ (structured logs)' },
2575
+ { key: 'docs', desc: 'API λ¬Έμ„œ (OpenAPI/Swagger)' }
2576
+ ]
2577
+ },
2578
+ cli: {
2579
+ aliases: ['cli', 'command-line', 'tool', 'utility', 'shell'],
2580
+ components: [
2581
+ { key: 'argParser', desc: '인자 νŒŒμ‹± (yargs/commander/clipanion)' },
2582
+ { key: 'help', desc: 'help / man / examples ν…μŠ€νŠΈ' },
2583
+ { key: 'config', desc: 'μ„€μ • 파일 + env λ³€μˆ˜' },
2584
+ { key: 'output', desc: '좜λ ₯ (TTY 색상/JSON/quiet)' },
2585
+ { key: 'error', desc: 'μ—λŸ¬ 처리 + exit code κ·œμ•½' },
2586
+ { key: 'completion', desc: 'shell completion (bash/zsh/fish)' }
2587
+ ]
2588
+ },
2589
+ data: {
2590
+ aliases: ['data', 'pipeline', 'etl', 'analytics', 'ingest'],
2591
+ components: [
2592
+ { key: 'ingest', desc: '데이터 μˆ˜μ§‘ (file/API/stream)' },
2593
+ { key: 'transform', desc: 'λ³€ν™˜ (cleaning/normalization/joining)' },
2594
+ { key: 'storage', desc: 'μ €μž₯μ†Œ (parquet/db/blob)' },
2595
+ { key: 'query', desc: '쿼리/뢄석 (SQL/aggregations)' },
2596
+ { key: 'validation', desc: '데이터 검증 (schema/contracts)' },
2597
+ { key: 'lineage', desc: '데이터 lineage 좔적' }
2598
+ ]
2599
+ }
2600
+ }
2601
+ };
2602
+ function _loadDomainCatalog(root) {
2603
+ try {
2604
+ const fp = _domainCatalogPath(root);
2605
+ if (!exists(fp)) return _DEFAULT_DOMAIN_CATALOG;
2606
+ const j = JSON.parse(read(fp));
2607
+ // user catalog merge β€” user override μš°μ„ 
2608
+ const merged = JSON.parse(JSON.stringify(_DEFAULT_DOMAIN_CATALOG));
2609
+ if (j.domains) {
2610
+ for (const k of Object.keys(j.domains)) merged.domains[k] = j.domains[k];
2611
+ }
2612
+ return merged;
2613
+ } catch { return _DEFAULT_DOMAIN_CATALOG; }
2614
+ }
2615
+ function _writeDomainCatalog(root, catalog) {
2616
+ try {
2617
+ mkdirp(path.join(root, '.harness'));
2618
+ writeUtf8(_domainCatalogPath(root), JSON.stringify({ ...catalog, updatedAt: new Date().toISOString() }, null, 2));
2619
+ return true;
2620
+ } catch { return false; }
2621
+ }
2622
+ // intent λΆ„λ₯˜: precise / broad / default
2623
+ function _classifyIntent(text) {
2624
+ if (!text || typeof text !== 'string') return { intent: 'default', signals: [] };
2625
+ const signals = [];
2626
+ // precise μ‹ ν˜Έ: "μ •ν™•νžˆ / κ·Έκ²ƒλ§Œ / κ·ΈλŒ€λ‘œ / only / just / 만"
2627
+ const preciseKws = ['μ •ν™•νžˆ', 'κ·Έκ²ƒλ§Œ', 'κ·ΈλŒ€λ‘œ', 'only', 'just only', 'λ§ν•œλŒ€λ‘œ', 'λ§ν•œ κ·ΈλŒ€λ‘œ'];
2628
+ for (const kw of preciseKws) {
2629
+ if (text.toLowerCase().includes(kw.toLowerCase())) signals.push({ kind: 'precise', match: kw });
2630
+ }
2631
+ // broad μ‹ ν˜Έ: "κΈ°λ³Έ / 포괄적 / λ“±λ“± / λ‹€μ–‘ν•œ / 전체 / 기본적인 / etc / overall"
2632
+ const broadKws = ['κΈ°λ³Έ', '포괄적', 'λ“±λ“±', 'λ‹€μ–‘ν•œ', '전체', '기본적인', 'etc', 'overall', 'ν•„μš”ν•œ', 'κ΄€λ ¨', 'comprehensive', 'including'];
2633
+ for (const kw of broadKws) {
2634
+ if (text.toLowerCase().includes(kw.toLowerCase())) signals.push({ kind: 'broad', match: kw });
2635
+ }
2636
+ // κ²°μ •
2637
+ const preciseCount = signals.filter(s => s.kind === 'precise').length;
2638
+ const broadCount = signals.filter(s => s.kind === 'broad').length;
2639
+ let intent;
2640
+ if (preciseCount > broadCount && preciseCount >= 1) intent = 'precise';
2641
+ else if (broadCount >= 1) intent = 'broad';
2642
+ else intent = 'default';
2643
+ return { intent, signals, preciseCount, broadCount };
2644
+ }
2645
+ function _detectDomain(text, root) {
2646
+ if (!text) return { domain: null, alias: null };
2647
+ const lower = text.toLowerCase();
2648
+ const catalog = _loadDomainCatalog(root);
2649
+ for (const [domain, info] of Object.entries(catalog.domains)) {
2650
+ for (const a of info.aliases || []) {
2651
+ if (lower.includes(a.toLowerCase())) {
2652
+ return { domain, alias: a, components: info.components };
2653
+ }
2654
+ }
2655
+ }
2656
+ return { domain: null, alias: null };
2657
+ }
2658
+ function _inferScopeExpansion(text, root) {
2659
+ const classify = _classifyIntent(text);
2660
+ const det = _detectDomain(text, root);
2661
+ const result = {
2662
+ text,
2663
+ intent: classify.intent,
2664
+ intentSignals: classify.signals,
2665
+ domain: det.domain,
2666
+ matchedAlias: det.alias,
2667
+ explicitMentions: [],
2668
+ expansionCandidates: [],
2669
+ mode: 'dry-run' // 항상 dry-run β€” μ‹€ν–‰ X
2670
+ };
2671
+ if (!det.domain || !det.components) {
2672
+ result.note = '도메인 미탐지 β€” λͺ…μ‹œ μš”μ²­λ§Œ μ§„ν–‰ (ν™•μž₯ 후보 μ—†μŒ)';
2673
+ return result;
2674
+ }
2675
+ // μ‚¬μš©μž ν…μŠ€νŠΈμ—μ„œ component key 직접 μ–ΈκΈ‰ μΆ”μΆœ
2676
+ const lowerText = text.toLowerCase();
2677
+ for (const c of det.components) {
2678
+ const mentionPatterns = [c.key.toLowerCase(), ...(c.desc.split(/[\s/()]/).filter(w => w.length >= 2).map(w => w.toLowerCase()))];
2679
+ const mentioned = mentionPatterns.some(p => lowerText.includes(p));
2680
+ if (mentioned) {
2681
+ result.explicitMentions.push({ key: c.key, desc: c.desc, source: 'user-explicit' });
2682
+ } else {
2683
+ result.expansionCandidates.push({ key: c.key, desc: c.desc, source: 'ai-inference' });
2684
+ }
2685
+ }
2686
+ // intent === precise λ©΄ expansion λΉ„μ–΄μžˆκ²Œ (μ˜λ„ 보호)
2687
+ if (classify.intent === 'precise') {
2688
+ result.expansionCandidates = [];
2689
+ result.note = `intent: precise β€” λͺ…μ‹œ μš”μ²­λ§Œ μ§„ν–‰, AI μΆ”λ‘  ν™•μž₯ λΉ„ν™œμ„± (μ‚¬μš©μž μ˜λ„ 보호)`;
2690
+ } else if (classify.intent === 'broad') {
2691
+ result.note = `intent: broad β€” ${result.expansionCandidates.length} ν™•μž₯ 후보 μ œμ‹œ (--expand-all λ˜λŠ” --select N 으둜 λͺ…μ‹œ 승인 ν•„μš”)`;
2692
+ } else {
2693
+ result.note = `intent: default β€” λͺ…μ‹œ ${result.explicitMentions.length}건만 μš°μ„ , ν™•μž₯ 후보 ${result.expansionCandidates.length}건 κ²€ν† μš©`;
2694
+ }
2695
+ return result;
2696
+ }
2697
+
2529
2698
  // 1.9.203: μžλ™ λΌμš΄λ“œ plan 정리 β€” μ‚¬μš©μž λͺ…μ‹œ
2530
2699
  // "λ°±κ·ΈλΌμš΄λ“œμ— λ‹€μŒ μž‘μ—…μ΄ μ‹œμž‘κ°€λŠ₯ν•œ μ˜ˆμƒ μ‹œκ°„ + μΌμ–΄λ‚¬μ„λ•Œ ν•΄μ•Όν•˜λŠ” 일을 정리"
2531
2700
  // λΌμš΄λ“œ 마무리 μ‹œ .harness/auto-resume-plan.json μžλ™ μ €μž₯ β†’ λ‹€μŒ wakeup μ‹œ μ¦‰μ‹œ μ‹€ν–‰ κ°€λŠ₯
@@ -3317,6 +3486,99 @@ function idempotencyCmd(root, sub) {
3317
3486
  process.exit(1);
3318
3487
  }
3319
3488
 
3489
+ // 1.9.213: leerness intent <classify|expand|domains> CLI β€” intent inference + scope expansion (μ‚¬μš©μž λͺ…μ‹œ)
3490
+ function intentCmd(root, sub, ...rest) {
3491
+ root = absRoot(root);
3492
+ const isTty = process.stdout && process.stdout.isTTY;
3493
+ const cyan = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
3494
+ const grn = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
3495
+ const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
3496
+ const red = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
3497
+ const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
3498
+
3499
+ if (!sub || sub === 'help' || sub === '--help') {
3500
+ log(`# leerness intent (1.9.213) β€” μ‚¬μš©μž μ˜λ„ νŒŒμ•… + scope expansion 게이트`);
3501
+ log('');
3502
+ log(` classify "<request>" β†’ intent λΆ„λ₯˜ (precise/broad/default) + μ‹ ν˜Έ (--json κ°€λŠ₯)`);
3503
+ log(` expand "<request>" β†’ 도메인 탐지 + ν™•μž₯ 후보 dry-run (--json κ°€λŠ₯)`);
3504
+ log(` domains β†’ λ“±λ‘λœ 도메인 catalog 좜λ ₯ (game/web/api/cli/data)`);
3505
+ log('');
3506
+ log(dim(` 3원칙: (1) Always-Off Opt-In (2) Dry-run κΈ°λ³Έ (μ‹€ν–‰ X) (3) λͺ…μ‹œ vs μΆ”λ‘  뢄리 라벨링`));
3507
+ log(dim(` 예: leerness intent expand "λ§΅κ³Ό 캐릭터 + κΈ°λ³Έ κ²Œμž„ κΈ°λŠ₯ λ§Œλ“€μ–΄μ€˜"`));
3508
+ return;
3509
+ }
3510
+
3511
+ if (sub === 'classify') {
3512
+ const text = rest.filter(x => !x.startsWith('-')).join(' ');
3513
+ if (!text) { console.error('Usage: leerness intent classify "<request text>"'); process.exit(1); }
3514
+ const result = _classifyIntent(text);
3515
+ if (has('--json')) { log(JSON.stringify({ text, ...result }, null, 2)); return; }
3516
+ log(cyan(`# leerness intent classify (1.9.213)`));
3517
+ log(` text: ${text.slice(0, 100)}${text.length > 100 ? '…' : ''}`);
3518
+ log('');
3519
+ const icon = result.intent === 'precise' ? '🎯' : (result.intent === 'broad' ? '🌐' : 'πŸ”');
3520
+ log(` ${icon} intent: ${grn(result.intent)} (precise=${result.preciseCount} / broad=${result.broadCount})`);
3521
+ if (result.signals.length) {
3522
+ log('');
3523
+ log(` πŸ“‘ μ‹ ν˜Έ:`);
3524
+ result.signals.forEach(s => log(dim(` β€’ [${s.kind}] "${s.match}"`)));
3525
+ }
3526
+ return;
3527
+ }
3528
+
3529
+ if (sub === 'expand') {
3530
+ const text = rest.filter(x => !x.startsWith('-')).join(' ');
3531
+ if (!text) { console.error('Usage: leerness intent expand "<request text>"'); process.exit(1); }
3532
+ const result = _inferScopeExpansion(text, root);
3533
+ if (has('--json')) { log(JSON.stringify(result, null, 2)); return; }
3534
+ log(cyan(`# leerness intent expand (1.9.213) [DRY-RUN]`));
3535
+ log(` text: ${text.slice(0, 100)}${text.length > 100 ? '…' : ''}`);
3536
+ log('');
3537
+ const iIcon = result.intent === 'precise' ? '🎯' : (result.intent === 'broad' ? '🌐' : 'πŸ”');
3538
+ log(` ${iIcon} intent: ${grn(result.intent)}` + (result.domain ? ` πŸ“¦ domain: ${grn(result.domain)}` + (result.matchedAlias ? dim(` (matched: "${result.matchedAlias}")`) : '') : ` ${dim('domain 미탐지')}`));
3539
+ log('');
3540
+ if (result.explicitMentions.length) {
3541
+ log(grn(`## πŸ‘€ μ‚¬μš©μž λͺ…μ‹œ (${result.explicitMentions.length})`));
3542
+ result.explicitMentions.forEach(m => log(` β€’ ${m.key} β€” ${m.desc}`));
3543
+ log('');
3544
+ }
3545
+ if (result.expansionCandidates.length) {
3546
+ log(yel(`## πŸ€– AI μΆ”λ‘  ν™•μž₯ 후보 (${result.expansionCandidates.length}, dry-run)`));
3547
+ result.expansionCandidates.forEach((c, i) => log(` [${i + 1}] ${c.key} β€” ${c.desc}`));
3548
+ log('');
3549
+ log(dim(` β†’ μ§„ν–‰ μ‹œ λͺ…μ‹œ 승인 ν•„μš”: leerness task add "<μ„ νƒν•œ 후보 μš”μ²­>"`));
3550
+ log(dim(` β†’ μ „λΆ€ λ¬΄μ‹œν•˜λ €λ©΄ μΆ”κ°€ task add 없이 λͺ…μ‹œ μš”μ²­λ§Œ μ§„ν–‰`));
3551
+ } else if (result.domain && result.intent === 'precise') {
3552
+ log(red(` πŸ›‘ intent: precise β€” AI μΆ”λ‘  ν™•μž₯ λΉ„ν™œμ„± (μ‚¬μš©μž μ˜λ„ 보호)`));
3553
+ }
3554
+ if (result.note) {
3555
+ log('');
3556
+ log(dim(` β„Ή ${result.note}`));
3557
+ }
3558
+ return;
3559
+ }
3560
+
3561
+ if (sub === 'domains') {
3562
+ const catalog = _loadDomainCatalog(root);
3563
+ if (has('--json')) { log(JSON.stringify(catalog, null, 2)); return; }
3564
+ log(cyan(`# leerness intent domains (1.9.213)`));
3565
+ log(` total domains: ${Object.keys(catalog.domains).length}`);
3566
+ log('');
3567
+ for (const [name, info] of Object.entries(catalog.domains)) {
3568
+ log(grn(` πŸ“¦ ${name}`) + dim(` aliases: ${(info.aliases || []).join(', ')}`));
3569
+ for (const c of info.components || []) {
3570
+ log(` β€’ ${c.key.padEnd(12)} ${dim(c.desc)}`);
3571
+ }
3572
+ log('');
3573
+ }
3574
+ return;
3575
+ }
3576
+
3577
+ console.error(`Unknown subcommand: ${sub}`);
3578
+ console.error(`Run: leerness intent help`);
3579
+ process.exit(1);
3580
+ }
3581
+
3320
3582
  // 1.9.199: wakeup miss μžλ™ 회볡 κ°•ν™” β€” last-handoff timestamp 기둝 + 25min κΈ°λŒ€ interval μ •λ°€ μΈ‘μ •
3321
3583
  // μ‚¬μš©μž λͺ…μ‹œ (1.9.198 후속): "22:01에 λΌμš΄λ“œμ— μžλ™μ§„μž…λ˜μ§€μ•Šμ•˜μ–΄"
3322
3584
  // 1.9.196 task-log mtime 기반 detector λŠ” task-log κ°€ 자주 μ•ˆ μ—…λ°μ΄νŠΈλ˜λ©΄ false positive.
@@ -16760,6 +17022,8 @@ async function main() {
16760
17022
  if (cmd === 'migrate-workspace-dir') return migrateWorkspaceDirCmd(arg('--path', process.cwd()));
16761
17023
  // 1.9.212: leerness idempotency audit β€” λ©±λ“±μ„± μœ„λ°˜ 탐지 (μ‚¬μš©μž λͺ…μ‹œ)
16762
17024
  if (cmd === 'idempotency') return idempotencyCmd(arg('--path', process.cwd()), args[1]);
17025
+ // 1.9.213: leerness intent <classify|expand|domains> β€” intent inference + scope expansion (μ‚¬μš©μž λͺ…μ‹œ)
17026
+ if (cmd === 'intent') return intentCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
16763
17027
  if (cmd === 'rule' && args[1] === 'add') return ruleAdd(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
16764
17028
  if (cmd === 'rule' && args[1] === 'list') return ruleList(arg('--path', process.cwd()));
16765
17029
  if (cmd === 'rule' && args[1] === 'remove') return ruleRemove(arg('--path', process.cwd()), args[2]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.212",
3
+ "version": "1.9.213",
4
4
  "description": "Leerness: λΉ„νŒŒκ΄΄ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜, μžλ™ 버전 κ°μ§€Β·μ—…λ°μ΄νŠΈ, κ³„νš/μ§„ν–‰/ν•Έλ“œμ˜€ν”„ μžλ™ν™”, κ²ŒμœΌλ¦„Β·μ‹œν¬λ¦ΏΒ·μΈμ½”λ”© μžλ™ κ°€λ“œ, Claude Code μŠ¬λž˜μ‹œ 톡합을 κ°–μΆ˜ ν•œκ΅­μ–΄ μš°μ„  AI 개발 ν•˜λ„€μŠ€.",
5
5
  "keywords": [
6
6
  "leerness",