leerness 1.9.339 → 1.9.341
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 +35 -0
- package/README.md +5 -5
- package/bin/harness.js +62 -76
- package/lib/catalogs.js +16 -1
- package/lib/pure-utils.js +20 -2
- package/package.json +1 -1
- package/scripts/e2e.js +76 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.341 — 2026-06-05 — UR-0025(심층): BUILTIN_CATALOG 서브시스템 분리
|
|
4
|
+
|
|
5
|
+
**🧩 내장 스킬 catalog(9종) + 순수 _source 변환을 모듈로 분리 — 동형 추출 7번째.** (UR-0025 심층)
|
|
6
|
+
|
|
7
|
+
### 배경 (방향 판단)
|
|
8
|
+
"rules canonical 로 트리오 완성"을 검토했으나, rules 는 이미 **구조화된 MD 테이블**(`| R-XXXX | trigger | rule | added | status | lastVerified |`)로 `readRules`/`writeRules` round-trip 이 깔끔 → decisions/lessons 의 free-form 블록 drift 부류가 아니고, 6개 변이 명령 + 매세션 자동검증에 쓰이는 행동 핵심이라 변환 위험 대비 가치 낮음 → **보류**(결정매트릭스: 안정성·기존 패턴 우선). 대신 검증된 저위험 catalog 추출 계속.
|
|
9
|
+
|
|
10
|
+
### 구현 (UR-0025 심층)
|
|
11
|
+
1. **`BUILTIN_CATALOG`**(9 내장 스킬: office/commerce-api/crawling/firebase/ads-analytics/appstore-review/ai-verified-skill-publisher/feature-implementation/project-roadmap-generator) → `lib/catalogs.js`.
|
|
12
|
+
2. **`_withBuiltinSource(catalog)`**(순수: catalog → `_source:'builtin'` 부여 맵) → `lib/pure-utils.js`. `_loadSkillCatalog` 의 builtin fallback = `_withBuiltinSource(BUILTIN_CATALOG)` 박막. import 가 로드타임 `_loadSkillCatalog()` 호출보다 앞서므로 순서 안전. 긴 단일라인 블록은 일회용 마이그레이션 스크립트로 결정적 이동 후 삭제.
|
|
13
|
+
3. selftest 88→89 · e2e 285→286.
|
|
14
|
+
|
|
15
|
+
### 검증
|
|
16
|
+
- **selftest 89/89 PASS** · **E2E 286/286 PASS** (회귀 0).
|
|
17
|
+
- 실측: catalog 9종(office.version 보존) · `_withBuiltinSource` 9 entries 전부 _source=builtin·capabilities 보존·null guard({}) · `skill list` 명령 builtin catalog 정상 노출(office/firebase/...).
|
|
18
|
+
|
|
19
|
+
## 1.9.340 — 2026-06-05 — UR-0058: lessons canonical = JSON, MD = projection (Codex 위임·검증)
|
|
20
|
+
|
|
21
|
+
**🤖 UR-0053 decisions canonical 패턴을 lessons 로 확장 — Codex CLI(gpt-5.5) 위임 후 하네스 독립 검증. 상태 저장 단일 진실소스 일반화.** (직전 1.9.339 decisions 의 정밀 미러)
|
|
22
|
+
|
|
23
|
+
### 배경 (위임 결정)
|
|
24
|
+
1.9.339 에서 decisions canonical(JSON 진실소스 + MD projection)을 확립 → lessons 도 동일 MD-parse drift 부류이고 `_parseLessonEntries`(순수, `{date,text,tag}`)가 이미 존재 → 동형 작업을 Codex 에 위임("일부 Codex" 로테이션). 위임 ≠ 무검증: 1.9.339 구현을 참조 명세로 작성 → `codex exec` → 하네스가 독립 검증(범위·구문·round-trip·백필·count 사이트·save/list/drop end-to-end).
|
|
25
|
+
|
|
26
|
+
### 구현 (Codex 수행)
|
|
27
|
+
1. **canonical write path**: `lesson save`/`drop` → `lessons.json`(canonical) + `lessons.md`(projection) 동시 기록 (`_saveLessons`).
|
|
28
|
+
2. **순수 렌더** `_renderLessonsMd(lessons)` → `lib/pure-utils.js` (tag 없으면 Tag 줄 생략 → `_parseLessonEntries` 와 round-trip 멱등). 기존 `_parseLessonEntries` 가 canonical 파서.
|
|
29
|
+
3. **canonical 로더** `_loadLessons(root)`: lessons.json 우선 → 없으면 `_parseLessonEntries(lessons.md)` (읽기 무부작용). 손상 JSON 시 MD fallback.
|
|
30
|
+
4. **단일 진실소스 라우팅**: lesson save/list(+tag/query)/drop + pulse/memorySurface/memory-status(lessonHeaders/latest)/handoff count 를 `_loadLessons` 로 전환.
|
|
31
|
+
5. **비파괴/백필**: 기존 MD-only → 첫 write 시 MD→JSON 백필(무손실). 읽기 무부작용. fuzzy restore/retro/today-heuristic 은 projection MD 유지(일관).
|
|
32
|
+
|
|
33
|
+
### 검증 (하네스 독립)
|
|
34
|
+
- **변경 범위**: bin/harness.js + lib/pure-utils.js 만 (VERSION/e2e/README/CHANGELOG/package/catalogs 미변경 — 명세 준수).
|
|
35
|
+
- **무결성**: node -c ×2 · selftest 88/88 PASS (Codex 가 self-reference-safe 케이스 추가) · **E2E 285/285 PASS** (회귀 0).
|
|
36
|
+
- **동작 실측**: save→lessons.json(tag=security·null)+lessons.md(projection, Tag 줄) · list 2·tag filter 1 · drop→1건+archive · memory-status L count=1 · **백필**: MD-only→list 2(읽기 무부작용)·첫 save 시 기존2+신규1=3 보존 · round-trip 멱등.
|
|
37
|
+
|
|
3
38
|
## 1.9.339 — 2026-06-05 — UR-0053: decisions canonical = JSON, MD = projection (아키텍처)
|
|
4
39
|
|
|
5
40
|
**🏛 상태 저장 단일 진실소스 — decisions 를 canonical JSON 으로 전환, decisions.md 는 projection(렌더 뷰). count drift 근본 해소.** (UR-0053 full, "UR-0053+UR-0025 둘 다 진행" 중 Round B)
|
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
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []() []() []() []()
|
|
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.
|
|
474
|
+
이 프로젝트는 Leerness v1.9.341 하네스를 사용합니다. 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.
|
|
528
|
+
Leerness v1.9.341는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **83개 도구**를 노출:
|
|
529
529
|
|
|
530
530
|
```jsonc
|
|
531
531
|
// 카테고리별
|
|
@@ -546,7 +546,7 @@ Leerness v1.9.339는 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.
|
|
549
|
+
현재 누적: **70 라운드 (1.9.40 → 1.9.341)** · 매 라운드 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.
|
|
587
|
+
Last synced by Leerness v1.9.341: 2026-06-05
|
|
588
588
|
<!-- leerness:project-readme:end -->
|
|
589
589
|
|
package/bin/harness.js
CHANGED
|
@@ -20,13 +20,14 @@ const { _isSecretKey, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel,
|
|
|
20
20
|
_detectLspLang, _matchLspSymbols,
|
|
21
21
|
_detectOptimism: _puDetectOptimism, _computeConfidence: _puComputeConfidence,
|
|
22
22
|
_personaSummaries, _translate,
|
|
23
|
-
_decisionsFromMd, _renderDecisionsMd
|
|
23
|
+
_decisionsFromMd, _renderDecisionsMd, _renderLessonsMd,
|
|
24
|
+
_withBuiltinSource } = require('../lib/pure-utils'); // 1.9.318~341 (UR-0025/0053): 순수 유틸 모듈 분리
|
|
24
25
|
// 1.9.304 (UR-0025): 순수 분석/검증 함수 모듈 분리.
|
|
25
26
|
const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInGit, _epistemicHonestyCheck } = require('../lib/analyzers');
|
|
26
27
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
27
|
-
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 } = require('../lib/catalogs'); // 1.9.
|
|
28
|
+
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 } = require('../lib/catalogs'); // 1.9.341 (UR-0025): BUILTIN_CATALOG 분리
|
|
28
29
|
|
|
29
|
-
const VERSION = '1.9.
|
|
30
|
+
const VERSION = '1.9.341';
|
|
30
31
|
|
|
31
32
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
32
33
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -123,23 +124,10 @@ function _loadSkillCatalog() {
|
|
|
123
124
|
return out;
|
|
124
125
|
}
|
|
125
126
|
SKILLPACK_SOURCE = 'builtin';
|
|
126
|
-
|
|
127
|
-
for (const [k, v] of Object.entries(BUILTIN_CATALOG)) out[k] = { ...v, _source: 'builtin' };
|
|
128
|
-
return out;
|
|
127
|
+
return _withBuiltinSource(BUILTIN_CATALOG); // 1.9.341 (UR-0025): 순수 _withBuiltinSource (lib/pure-utils)
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
|
|
132
|
-
'office': { displayNameKo: '마이크로소프트 오피스 자동화 스킬 라이브러리', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['Word/Excel/PowerPoint 문서 자동화', '템플릿 기반 문서 생성', '표/차트/요약 문서화', '민감정보 제외 규칙 적용'] },
|
|
133
|
-
'commerce-api': { displayNameKo: '커머스 API 연동 스킬 라이브러리', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['쿠팡·롯데온·스마트스토어 API 연동 설계', '주문/상품/매출 동기화', '환경변수 기반 인증 분리', '레이트리밋/재시도/오류 처리'] },
|
|
134
|
-
'crawling': { displayNameKo: '크롤링·브라우저 자동화 스킬 라이브러리', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['Playwright 기반 자동화', '다운로드/로그인 세션 처리', '스크린샷 기반 실패 진단', '약관/권한/차단 위험 점검'] },
|
|
135
|
-
'firebase': { displayNameKo: 'Firebase·Cloud Functions 스킬 라이브러리', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['Firebase Functions 배포 구조', '환경변수/시크릿 분리', '권한/IAM 점검', '로컬 에뮬레이터 검증'] },
|
|
136
|
-
'ads-analytics': { displayNameKo: '광고·GA4 분석 스킬 라이브러리', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['GA4 이벤트/전환 점검', '광고 데이터 수집 구조화', '소스/매체 분석', '리포트 자동화'] },
|
|
137
|
-
'appstore-review': { displayNameKo: '앱스토어 심사 대응 스킬 라이브러리', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['심사 문구 분석', '개인정보 라벨 점검', '리젝 대응 초안', '웹뷰/앱 데이터 수집 구분'] },
|
|
138
|
-
'ai-verified-skill-publisher': { displayNameKo: 'AI 검증 스킬 업로드·라이브러리화 스킬', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['검증된 스킬 정규화', '민감정보 스캔', 'AI 검증 메타데이터 작성', 'npm/git 업로드 dry-run 및 실행 게이트'] },
|
|
139
|
-
'feature-implementation': { displayNameKo: '기능 구현 표준 스킬', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['feature-contracts 작성', '재사용 우선 검사', '테스트 증거 수집', '핸드오프 트리거'] },
|
|
140
|
-
// 1.9.11: 기본 내장 — 로드맵 자동 생성 스킬
|
|
141
|
-
'project-roadmap-generator': { displayNameKo: '프로젝트 로드맵 자동 생성 스킬', version: '0.2.0', lastUpdated: '2026-05-12', verification: 'passed', capabilities: ['leerness .harness/* 통합 파싱 (plan/progress/skills/rules/decisions/handoff/current-state)', '좌→우 수평 트리 + 상하 중앙정렬 SVG', '7개 상태 색상 (완료/진행/보류/검토/예정/미완료/오류)', 'design-system + CSS variables 자동 주입', '화이트보드 panning/zoom + 더블클릭 reset', '단일 HTML 출력 (외부 의존성 0)'] }
|
|
142
|
-
};
|
|
130
|
+
// 1.9.341 (UR-0025 심층): BUILTIN_CATALOG (9 내장 스킬 catalog) 는 lib/catalogs.js 로 이전 (import).
|
|
143
131
|
|
|
144
132
|
// 1.9.10: skillCatalog는 skillpack 우선, fallback builtin. _loadSkillCatalog 호출은 BUILTIN_CATALOG 정의 후.
|
|
145
133
|
const skillCatalog = _loadSkillCatalog();
|
|
@@ -3045,7 +3033,7 @@ function _selfTestCases() {
|
|
|
3045
3033
|
{ name: '텔레메트리 분리: 내부 auto-call(LEERNESS_INTERNAL) usage 집계 제외 + 주요 spawn 마킹 (UR-0051 설치리뷰 1.9.317)', run: () => { const src = read(__filename); const guard = src.includes("process.env.LEERNESS_INTERNAL !== '1'"); const marked = (src.match(/LEERNESS_INTERNAL: '1'/g) || []).length >= 10; const reviewMarked = /'review-request'[\s\S]{0,200}LEERNESS_INTERNAL: '1'/.test(src); return guard && marked && reviewMarked; } },
|
|
3046
3034
|
{ name: 'lib/pure-utils: HTML 파싱 유틸 3종 모듈 분리 + 동작 + 인라인 제거 (UR-0025 1.9.318)', run: () => { const m = require('../lib/pure-utils'); const fnOk = typeof m._htmlToText === 'function' && typeof m._extractTitle === 'function' && typeof m._extractLinks === 'function'; const work = m._htmlToText('<p>Hello <b>World</b></p>') === 'Hello World' && m._extractTitle('<html><title>My & Page</title></html>') === 'My & Page' && m._extractLinks('<a href="/a">A</a><a href="https://other.com/b">B</a>', 'https://x.com/').length === 1; const moved = m._htmlToText === _htmlToText && !/^function _htmlToText\(html\) \{/m.test(read(__filename)); return fnOk && work && moved; } },
|
|
3047
3035
|
{ name: 'MCP ToolRegistry 일치성: 모든 도구 def 가 dispatch case 보유 + 고아 case 0 + requiredTier 완비 (UR-0044 1.9.319)', run: () => { const tools = require('../lib/mcp-tools'); const src = read(__filename); const missing = tools.filter(t => !src.includes("case '" + t.name + "':")); const cases = [...src.matchAll(/case '(leerness_[a-z_]+)':/g)].map(m => m[1]); const defNames = new Set(tools.map(t => t.name)); const orphans = [...new Set(cases)].filter(c => !defNames.has(c)); const tierOk = tools.every(t => typeof t.requiredTier === 'string' && PERMISSION_TIERS.includes(t.requiredTier)); return tools.length >= 83 && missing.length === 0 && orphans.length === 0 && tierOk; } },
|
|
3048
|
-
{ name: 'count drift 수정: _countDatedBlocks 코드펜스(템플릿) 제외 (UR-0053 1.9.320;
|
|
3036
|
+
{ name: 'count drift 수정: _countDatedBlocks 코드펜스(템플릿) 제외 (UR-0053 1.9.320; memory count canonical 전환 후 legacy parser 보존)', run: () => { const f = _countDatedBlocks; const withTpl = '# D\n\n```md\n### 2026-01-01 — Decision 제목\n- Decision:\n```\n\n### 2026-06-04 — 실제\n- Decision: 실제\n'; const c1 = f(withTpl) === 1; const c0 = f('```md\n### 2026-01-01 — x\n```\n') === 0; const c2 = f('### 2026-01-01 — A\n### 2026-02-02 — B\n') === 2; return typeof f === 'function' && c1 && c0 && c2; } },
|
|
3049
3037
|
{ name: 'decision/lesson 필드 파싱: 빈 필드가 다음 줄로 안 샘 ([ \\t]* 사용) (UR-0053 1.9.321)', run: () => { const block = '### 2026-06-05 — X\n- Decision: X\n- Reason: r\n- Alternatives: \n- Impact: 보안\n'; const alt = block.match(/- Alternatives:[ \t]*(.+)/); const imp = block.match(/- Impact:[ \t]*(.+)/); const altNoBleed = !alt || !/Impact/.test(alt[1]); const impOk = !!imp && imp[1].trim() === '보안'; const src = read(__filename); const fixedOk = src.includes('- Alternatives:[ \\t]*(.+)') && src.includes('- Lesson:[ \\t]*(.+)') && src.includes('- Impact:[ \\t]*(.+)'); return altNoBleed && impOk && fixedOk; } },
|
|
3050
3038
|
{ name: 'MCP handler 통합: _mcpToCliArgs 단일 함수 + mcpServeCmd 호출 + 인라인 switch 단일화 (UR-0044 1.9.322)', run: () => { const src = read(__filename); const fnDef = /function _mcpToCliArgs\(name, args, targetPath\) \{/.test(src); const called = src.includes('cliArgs = _mcpToCliArgs(name, args, targetPath)'); const switchCount = (src.match(/switch \(name\) \{/g) || []).length; const nullPath = src.includes('if (cliArgs === null) return send('); return fnDef && called && switchCount === 1 && nullPath; } },
|
|
3051
3039
|
{ name: 'fresh-init gate 통과: lazy detect 부재신호(handoff/test/progress) done-work 없으면 비차단 (UR-0054 ⑥ 1.9.323)', run: () => { const src = read(__filename); const doneWork = src.includes("const _hasDoneWork = rows.some(r => /^(done|completed|verified)$/i.test(r.status))"); const advisory = src.includes('_ADVISORY_KINDS') && src.includes("'handoff_never_generated'") && src.includes("'no_test_run'"); const blocking = src.includes('const blockingIssues = Math.max(0, issues - advisoryCount)') && src.includes('if (blockingIssues > 0) process.exitCode = 1'); return doneWork && advisory && blocking; } },
|
|
@@ -3057,7 +3045,7 @@ function _selfTestCases() {
|
|
|
3057
3045
|
{ name: 'lib/pure-utils: roadmap MD 파서 분리(_roadmapMapStatus/Milestones/Tokens) + 인라인 제거 (UR-0025 1.9.329)', run: () => { const m = require('../lib/pure-utils'); const work = m._roadmapMapStatus('REQUESTED') === 'planned' && m._roadmapMapStatus('done') === 'done' && m._roadmapParseMilestones('### M-0001. 로그인\nStatus: in-progress\nProgress: 40%')[0].progress === 40 && m._roadmapParseTokens('| color | #fff |').color === '#fff'; const src = read(__filename); const moved = m._roadmapMapStatus === _roadmapMapStatus && !/^function _roadmapMapStatus\(/m.test(src) && !/^function _roadmapParseMilestones\(/m.test(src) && !/^function _roadmapParseTokens\(/m.test(src); return work && moved; } },
|
|
3058
3046
|
{ name: 'lib/pure-utils: project-brief config 분리(_BRIEF_FIELDS/_briefFilled) + 인라인 제거 (UR-0025 1.9.330)', run: () => { const m = require('../lib/pure-utils'); const cfgOk = Array.isArray(m._BRIEF_FIELDS) && m._BRIEF_FIELDS.length === 10 && m._BRIEF_FIELDS[0].key === 'intro'; const work = m._briefFilled({ intro: 'x', features: ['a'] }) === 2 && m._briefFilled({}) === 0; const src = read(__filename); const moved = m._briefFilled === _briefFilled && m._BRIEF_FIELDS === _BRIEF_FIELDS && !/^const _BRIEF_FIELDS = \[/m.test(src) && !/^function _briefFilled\(/m.test(src); return cfgOk && work && moved; } },
|
|
3059
3047
|
{ name: 'lib/pure-utils: brief 빌더 분리(_briefReadmeBlock/_briefBlueprint + BRIEF 마커, VERSION 주입) (UR-0025 1.9.331)', run: () => { const m = require('../lib/pure-utils'); const fnOk = typeof m._briefReadmeBlock === 'function' && typeof m._briefBlueprint === 'function' && m.BRIEF_START.includes('project-brief:start'); const b = { project: 'X', intro: 'i', features: ['f1'] }; const rb = m._briefReadmeBlock(b); const bp = m._briefBlueprint(b, '9.9.9'); const work = rb.includes(m.BRIEF_START) && rb.includes(m.BRIEF_END) && /f1/.test(rb) && /Blueprint/.test(bp) && /leerness v9\.9\.9/.test(bp); const src = read(__filename); const moved = m._briefBlueprint === _briefBlueprint && m.BRIEF_START === BRIEF_START && !/^function _briefReadmeBlock\(/m.test(src) && !/^function _briefBlueprint\(/m.test(src) && !/^const BRIEF_START =/m.test(src); return fnOk && work && moved; } },
|
|
3060
|
-
{ name: 'lib/pure-utils: lessons.md 파서 분리(_parseLessonEntries) + 인라인 제거 (UR-0025 1.9.332)', run: () => { const m = require('../lib/pure-utils'); const r = m._parseLessonEntries('### 2026-06-05\n- Lesson: A\n- Tag: t\n\n### 2026-06-04\n- Lesson: B'); const work = r.length === 2 && r[0].text === 'A' && r[0].tag === 't' && r[1].tag === null && r[0].date === '2026-06-05'; const src = read(__filename); const moved = m._parseLessonEntries === _parseLessonEntries && !/^function _parseLessonEntries\(/m.test(src) && src.includes('
|
|
3048
|
+
{ name: 'lib/pure-utils: lessons.md 파서 분리(_parseLessonEntries) + 인라인 제거 (UR-0025 1.9.332)', run: () => { const m = require('../lib/pure-utils'); const r = m._parseLessonEntries('### 2026-06-05\n- Lesson: A\n- Tag: t\n\n### 2026-06-04\n- Lesson: B'); const work = r.length === 2 && r[0].text === 'A' && r[0].tag === 't' && r[1].tag === null && r[0].date === '2026-06-05'; const src = read(__filename); const moved = m._parseLessonEntries === _parseLessonEntries && !/^function _parseLessonEntries\(/m.test(src) && src.includes('_parseLessonEntries(read(mp))'); return work && moved; } },
|
|
3061
3049
|
{ name: 'UR-0025 심층: constraints catalog→lib/catalogs + _matchConstraints→pure-utils 분리 (1.9.333)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c._DEFAULT_PLATFORM_CONSTRAINTS && Object.keys(c._DEFAULT_PLATFORM_CONSTRAINTS.platforms).length === 6 && !!c._DEFAULT_PLATFORM_CONSTRAINTS.platforms.stripe; const r = m._matchConstraints(c._DEFAULT_PLATFORM_CONSTRAINTS, 'stripe 결제'); const work = r.matched.length === 1 && r.matched[0].platform === 'stripe' && r.totalPlatforms === 6 && m._matchConstraints(null, 'x').matched.length === 0; const src = read(__filename); const moved = _DEFAULT_PLATFORM_CONSTRAINTS === c._DEFAULT_PLATFORM_CONSTRAINTS && _matchConstraints === m._matchConstraints && !/const _DEFAULT_PLATFORM_CONSTRAINTS = \{/.test(src) && src.includes('_matchConstraints(_loadPlatformConstraints(root), text)'); return catOk && work && moved; } },
|
|
3062
3050
|
{ name: 'UR-0025 심층(Codex 위임·검증): intent domain catalog→lib/catalogs + _matchDomain→pure-utils 분리 (1.9.334)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c._DEFAULT_DOMAIN_CATALOG && Object.keys(c._DEFAULT_DOMAIN_CATALOG.domains).length === 5 && !!c._DEFAULT_DOMAIN_CATALOG.domains.game; const r = m._matchDomain(c._DEFAULT_DOMAIN_CATALOG, 'unity 게임'); const work = r.domain === 'game' && Array.isArray(r.components) && m._matchDomain(c._DEFAULT_DOMAIN_CATALOG, 'zzz없음').domain === null && m._matchDomain(null, 'x').domain === null; const src = read(__filename); const moved = _DEFAULT_DOMAIN_CATALOG === c._DEFAULT_DOMAIN_CATALOG && _matchDomain === m._matchDomain && !/const _DEFAULT_DOMAIN_CATALOG = \{/.test(src) && src.includes('_matchDomain(_loadDomainCatalog(root), text)'); return catOk && work && moved; } },
|
|
3063
3051
|
{ name: 'UR-0025 심층: LSP catalog→lib/catalogs(_LSP_LANG_PATTERNS) + _detectLspLang/_matchLspSymbols→pure-utils 분리 (1.9.335)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c._LSP_LANG_PATTERNS && Object.keys(c._LSP_LANG_PATTERNS).length === 5 && Array.isArray(c._LSP_LANG_PATTERNS.javascript); const langOk = m._detectLspLang('a.py') === 'python' && m._detectLspLang('b.go') === 'go' && m._detectLspLang('c.md') === 'javascript'; const sy = m._matchLspSymbols(c._LSP_LANG_PATTERNS, 'function alpha(){}\nclass Beta{}', 'javascript'); const work = sy.length === 2 && sy[0].name === 'alpha' && sy[0].kind === 'function' && sy[1].kind === 'class' && m._matchLspSymbols(null, 'x', 'javascript').length === 0; const src = read(__filename); const moved = _LSP_LANG_PATTERNS === c._LSP_LANG_PATTERNS && _detectLspLang === m._detectLspLang && _matchLspSymbols === m._matchLspSymbols && !/const _LSP_LANG_PATTERNS = \{/.test(src) && !/function _detectLspLang\(/.test(src); return catOk && langOk && work && moved; } },
|
|
@@ -3065,6 +3053,8 @@ function _selfTestCases() {
|
|
|
3065
3053
|
{ name: 'UR-0025 심층: persona catalog→lib/catalogs(BUILT_IN_PERSONAS) + _personaSummaries→pure-utils 분리 (1.9.337)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c.BUILT_IN_PERSONAS && Object.keys(c.BUILT_IN_PERSONAS).length === 5 && c.BUILT_IN_PERSONAS.security && typeof c.BUILT_IN_PERSONAS.security.body === 'string'; const sm = m._personaSummaries(c.BUILT_IN_PERSONAS); const work = Array.isArray(sm) && sm.length === 5 && sm[0].id === 'security' && sm[0].body === undefined && typeof sm[0].name === 'string' && m._personaSummaries(null).length === 0; const src = read(__filename); const moved = BUILT_IN_PERSONAS === c.BUILT_IN_PERSONAS && _personaSummaries === m._personaSummaries && !/const BUILT_IN_PERSONAS = \{/.test(src); return catOk && work && moved; } },
|
|
3066
3054
|
{ name: 'UR-0025 심층: i18n STRINGS catalog→lib/catalogs + _translate→pure-utils 분리 (1.9.338)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c.STRINGS && typeof c.STRINGS['common.ready'] === 'object' && c.STRINGS['common.ready'].en === 'Ready'; const work = m._translate(c.STRINGS, 'common.ready', 'en') === 'Ready' && m._translate(c.STRINGS, 'common.ready', 'ko') === '준비 완료' && m._translate(c.STRINGS, 'no.such.key', 'en') === 'no.such.key' && m._translate(null, 'x', 'ko') === 'x' && m._translate({ k: { ko: '케이' } }, 'k', 'en') === '케이'; const src = read(__filename); const moved = STRINGS === c.STRINGS && _translate === m._translate && !/const STRINGS = \{/.test(src); return catOk && work && moved; } },
|
|
3067
3055
|
{ name: 'UR-0053: decisions canonical JSON 레이어(_loadDecisions/_saveDecisions/decisionsJsonPath) + pure 파서/렌더 round-trip (1.9.339)', run: () => { const m = require('../lib/pure-utils'); const md = '# Decisions\n\n### 2026-06-05 — A\n- Decision: a\n- Reason: r\n- Alternatives: alt\n- Impact: imp\n\n### 2026-06-04 — B\n- Decision: b\n- Alternatives:\n'; const objs = m._decisionsFromMd(md); const parseOk = objs.length === 2 && objs[0].alternatives === 'alt' && objs[0].impact === 'imp' && objs[1].alternatives === null && objs[1].title === 'B'; const rt = m._decisionsFromMd(m._renderDecisionsMd(objs)); const rtOk = JSON.stringify(rt) === JSON.stringify(objs); const tplOk = m._decisionsFromMd(m._renderDecisionsMd([])).length === 0; const layerOk = typeof _loadDecisions === 'function' && typeof _saveDecisions === 'function' && typeof decisionsJsonPath === 'function' && _decisionsFromMd === m._decisionsFromMd; return parseOk && rtOk && tplOk && layerOk; } },
|
|
3056
|
+
{ name: 'UR-0058: lessons canonical JSON 레이어(_loadLessons/_saveLessons/lessonsJsonPath) + pure 파서/렌더 round-trip', run: () => { const m = require('../lib/pure-utils'); const objs = [{ date: '2026-06-05', text: 'A', tag: 't' }, { date: '2026-06-04', text: 'B', tag: null }]; const rt = m._parseLessonEntries(m._renderLessonsMd(objs)); const rtOk = JSON.stringify(rt) === JSON.stringify(objs); const tplOk = m._parseLessonEntries(m._renderLessonsMd([])).length === 0; const layerOk = typeof _loadLessons === 'function' && typeof _saveLessons === 'function' && typeof lessonsJsonPath === 'function' && _renderLessonsMd === m._renderLessonsMd; return rtOk && tplOk && layerOk; } },
|
|
3057
|
+
{ name: 'UR-0025 심층: BUILTIN_CATALOG→lib/catalogs + _withBuiltinSource→pure-utils 분리 (1.9.341)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c.BUILTIN_CATALOG && Object.keys(c.BUILTIN_CATALOG).length === 9 && c.BUILTIN_CATALOG.office && c.BUILTIN_CATALOG.office.version === '1.0.0'; const out = m._withBuiltinSource(c.BUILTIN_CATALOG); const work = Object.keys(out).length === 9 && Object.values(out).every(v => v._source === 'builtin') && out.office.version === '1.0.0' && Array.isArray(out.office.capabilities) && Object.keys(m._withBuiltinSource(null)).length === 0; const src = read(__filename); const moved = BUILTIN_CATALOG === c.BUILTIN_CATALOG && _withBuiltinSource === m._withBuiltinSource && !/const BUILTIN_CATALOG = \{/.test(src); return catOk && work && moved; } },
|
|
3068
3058
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3069
3059
|
];
|
|
3070
3060
|
}
|
|
@@ -3549,8 +3539,7 @@ function pulseCmd(root) {
|
|
|
3549
3539
|
const rulesActive = readRules(root).filter(r => r.status === 'active').length;
|
|
3550
3540
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
3551
3541
|
const milestonesCnt = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
3552
|
-
const
|
|
3553
|
-
const lessonsCount = (lm.match(/^### \d{4}-\d{2}-\d{2}[^\n]*/gm) || []).length;
|
|
3542
|
+
const lessonsCount = _loadLessons(root).length;
|
|
3554
3543
|
data.memorySurface = `T${tasksInProgress}/D${decisionCount}/R${rulesActive}/P${milestonesCnt}/L${lessonsCount}`;
|
|
3555
3544
|
} catch {}
|
|
3556
3545
|
// 마일스톤 + ETA
|
|
@@ -4429,7 +4418,7 @@ function _buildAutoResumePlan(root, opts) {
|
|
|
4429
4418
|
tasksInProgress: rows.filter(r => r.status === 'in-progress').length,
|
|
4430
4419
|
decisions: _loadDecisions(root).length, // 1.9.339 (UR-0053): canonical 단일 진실소스
|
|
4431
4420
|
rulesActive: readRules(root).filter(r => r.status === 'active').length,
|
|
4432
|
-
lessons:
|
|
4421
|
+
lessons: _loadLessons(root).length
|
|
4433
4422
|
};
|
|
4434
4423
|
return {
|
|
4435
4424
|
nextRoundVersion: opts.nextRoundVersion || `next after ${currentVersion}`,
|
|
@@ -5877,6 +5866,21 @@ function _saveDecisions(root, decisions) {
|
|
|
5877
5866
|
}
|
|
5878
5867
|
// 1.9.112: 전용 lessons.md (Memory Write Surface 5번째)
|
|
5879
5868
|
const lessonsPath = root => path.join(root, '.harness/lessons.md');
|
|
5869
|
+
const lessonsJsonPath = root => path.join(root, '.harness/lessons.json');
|
|
5870
|
+
function _loadLessons(root) {
|
|
5871
|
+
const jp = lessonsJsonPath(root);
|
|
5872
|
+
if (exists(jp)) {
|
|
5873
|
+
try { const arr = JSON.parse(read(jp)); if (Array.isArray(arr)) return arr; } catch {}
|
|
5874
|
+
}
|
|
5875
|
+
const mp = lessonsPath(root);
|
|
5876
|
+
return exists(mp) ? _parseLessonEntries(read(mp)) : [];
|
|
5877
|
+
}
|
|
5878
|
+
function _saveLessons(root, lessons) {
|
|
5879
|
+
const arr = Array.isArray(lessons) ? lessons : [];
|
|
5880
|
+
mkdirp(path.dirname(lessonsJsonPath(root)));
|
|
5881
|
+
writeUtf8(lessonsJsonPath(root), JSON.stringify(arr, null, 2) + '\n');
|
|
5882
|
+
writeUtf8(lessonsPath(root), _renderLessonsMd(arr));
|
|
5883
|
+
}
|
|
5880
5884
|
|
|
5881
5885
|
function nextId(root, prefix) {
|
|
5882
5886
|
const sources = [planPath(root), progressPath(root)].map(p => exists(p) ? read(p) : '').join('\n');
|
|
@@ -6240,10 +6244,10 @@ function memoryStatusCmd(root, opts = {}) {
|
|
|
6240
6244
|
const milestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
6241
6245
|
// plan milestones in-progress: progress-tracker에서 plan:M-XXXX evidence 있고 in-progress 인 row
|
|
6242
6246
|
const planInProgress = rows.filter(r => /plan:M-/.test(r.evidence || '') && r.status === 'in-progress').length;
|
|
6243
|
-
// Lessons (
|
|
6244
|
-
const
|
|
6245
|
-
const lessonHeaders =
|
|
6246
|
-
const lessonsLatest =
|
|
6247
|
+
// Lessons (UR-0058: canonical _loadLessons — JSON 단일 진실소스, MD projection fallback)
|
|
6248
|
+
const _les = _loadLessons(root);
|
|
6249
|
+
const lessonHeaders = _les.map(l => '### ' + l.date);
|
|
6250
|
+
const lessonsLatest = _les.length ? (_les[_les.length - 1].text || '').slice(0, 100) : null;
|
|
6247
6251
|
// 1.9.130: DELETE 5종 archive entry counts
|
|
6248
6252
|
const archiveCounts = { decisions: 0, lessons: 0, plan: 0, total: 0 };
|
|
6249
6253
|
try {
|
|
@@ -6444,20 +6448,19 @@ function lessonListCmd(root, opts = {}) {
|
|
|
6444
6448
|
// 1.9.139: --query 필터 (lesson text case-insensitive 매칭)
|
|
6445
6449
|
const queryFilter = arg('--query', null);
|
|
6446
6450
|
const queryRe = queryFilter ? new RegExp(queryFilter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i') : null;
|
|
6447
|
-
const
|
|
6448
|
-
if (!
|
|
6451
|
+
const all = _loadLessons(root);
|
|
6452
|
+
if (!all.length) {
|
|
6449
6453
|
if (jsonMode) {
|
|
6450
6454
|
const empty = { version: VERSION, root, total: 0, lessons: [], tag: tagFilter };
|
|
6451
6455
|
if (queryFilter) empty.query = queryFilter;
|
|
6452
6456
|
process.stdout.write(JSON.stringify(empty, null, 2) + '\n');
|
|
6453
6457
|
return;
|
|
6454
6458
|
}
|
|
6455
|
-
return ok('lessons
|
|
6459
|
+
return ok('lessons 비어있음 — leerness lesson save "<text>" 로 첫 lesson 영구화');
|
|
6456
6460
|
}
|
|
6457
|
-
const text = read(lp);
|
|
6458
6461
|
const lessons = [];
|
|
6459
|
-
//
|
|
6460
|
-
for (const lesson of
|
|
6462
|
+
// UR-0058: lessons.json canonical 로드(JSON 우선, MD fallback). tag/query 필터는 명령에 유지.
|
|
6463
|
+
for (const lesson of all) {
|
|
6461
6464
|
if (tagFilter && lesson.tag !== tagFilter) continue;
|
|
6462
6465
|
// 1.9.139: query 필터 — lesson text 또는 tag 매칭
|
|
6463
6466
|
if (queryRe && !queryRe.test(lesson.text) && !queryRe.test(lesson.tag || '')) continue;
|
|
@@ -6487,32 +6490,24 @@ function lessonListCmd(root, opts = {}) {
|
|
|
6487
6490
|
function lessonDropCmd(root, target) {
|
|
6488
6491
|
root = absRoot(root);
|
|
6489
6492
|
if (!target) return fail('lesson drop <date|text-substring> 필요. 예: leerness lesson drop "2026-05-20" 또는 leerness lesson drop "JWT"');
|
|
6490
|
-
const lp = lessonsPath(root);
|
|
6491
|
-
if (!exists(lp)) return fail('lessons.md 없음');
|
|
6492
|
-
const text = read(lp);
|
|
6493
|
-
const blocks = text.split(/\n(?=### )/);
|
|
6494
|
-
let removed = 0;
|
|
6495
6493
|
const kept = [];
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
}
|
|
6513
|
-
if (removed === 0) return fail(`매칭 lesson 없음: "${target}"`);
|
|
6514
|
-
writeUtf8(lp, kept.join('\n'));
|
|
6515
|
-
ok(`lesson dropped: ${removed}건 (보존: .harness/lessons.archive.md)`);
|
|
6494
|
+
const removed = [];
|
|
6495
|
+
const all = _loadLessons(root);
|
|
6496
|
+
if (!all.length) return fail('lessons 없음');
|
|
6497
|
+
for (const l of all) {
|
|
6498
|
+
if (l.date === target || (l.text || '').includes(target)) removed.push(l);
|
|
6499
|
+
else kept.push(l);
|
|
6500
|
+
}
|
|
6501
|
+
if (!removed.length) return fail(`매칭 lesson 없음: "${target}"`);
|
|
6502
|
+
// archive 보존 — lessons.archive.md 에 projection MD 블록 형태로 추가
|
|
6503
|
+
const archivePath = path.join(root, '.harness/lessons.archive.md');
|
|
6504
|
+
const archiveHeader = exists(archivePath) ? '' : '# Lessons archive\n\n';
|
|
6505
|
+
const archiveBlocks = removed.map(l =>
|
|
6506
|
+
`\n### ${l.date}\n- Lesson: ${l.text}\n${l.tag ? `- Tag: ${l.tag}\n` : ''}`
|
|
6507
|
+
).join('');
|
|
6508
|
+
append(archivePath, archiveHeader + `\n## 제거 ${today()} (target: "${target}")\n${archiveBlocks}\n`);
|
|
6509
|
+
_saveLessons(root, kept);
|
|
6510
|
+
ok(`lesson dropped: ${removed.length}건 (보존: .harness/lessons.archive.md)`);
|
|
6516
6511
|
_autoRoadmap(absRoot(root), 'data-change');
|
|
6517
6512
|
}
|
|
6518
6513
|
|
|
@@ -6521,15 +6516,10 @@ function lessonSave(root, text) {
|
|
|
6521
6516
|
root = absRoot(root);
|
|
6522
6517
|
if (!_requireInit(root, 'lesson save')) return; // 1.9.311 (UR-0047): init 가드
|
|
6523
6518
|
if (!text) return fail('lesson text required. 예: leerness lesson save "JWT는 refresh token도 짧게 (15분 권장)"');
|
|
6524
|
-
const lp = lessonsPath(root);
|
|
6525
6519
|
const tag = arg('--tag', '');
|
|
6526
|
-
const
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
writeUtf8(lp, `# Lessons (1.9.112)\n\n과거 실수/통찰/패턴 영구 기록 — handoff 자동 회수와 통합.\n${block}`);
|
|
6530
|
-
} else {
|
|
6531
|
-
append(lp, block);
|
|
6532
|
-
}
|
|
6520
|
+
const all = _loadLessons(root);
|
|
6521
|
+
all.push({ date: today(), text, tag: tag || null });
|
|
6522
|
+
_saveLessons(root, all);
|
|
6533
6523
|
ok(`lesson saved`);
|
|
6534
6524
|
_autoRoadmap(absRoot(root), 'data-change');
|
|
6535
6525
|
}
|
|
@@ -7415,8 +7405,7 @@ function handoff(root) {
|
|
|
7415
7405
|
const rulesActive = rules.filter(r => r.status === 'active').length;
|
|
7416
7406
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
7417
7407
|
const milestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
7418
|
-
const
|
|
7419
|
-
const lessonsCount = (lm.match(/^### \d{4}-\d{2}-\d{2}[^\n]*/gm) || []).length;
|
|
7408
|
+
const lessonsCount = _loadLessons(root).length;
|
|
7420
7409
|
// 1.9.130: archive 카운트 통합
|
|
7421
7410
|
const archiveCountsH = { decisions: 0, lessons: 0, plan: 0, total: 0 };
|
|
7422
7411
|
try {
|
|
@@ -7686,7 +7675,7 @@ function handoff(root) {
|
|
|
7686
7675
|
const rulesActive = readRules(root).filter(r => r.status === 'active').length;
|
|
7687
7676
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
7688
7677
|
const planMilestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
7689
|
-
const lessons =
|
|
7678
|
+
const lessons = _loadLessons(root).length;
|
|
7690
7679
|
parts.push(`🧠 mem T${inProgressTasks}/D${decisions}/R${rulesActive}/P${planMilestones}/L${lessons}`);
|
|
7691
7680
|
} catch {}
|
|
7692
7681
|
// 8) 1.9.152: 활성 외부 AI CLI 카운트 (1.9.151 복수 선택 결과 반영) — 메인 에이전트가 sub-agent 분배 가능성 즉시 인지
|
|
@@ -12024,8 +12013,7 @@ function sessionClose(root, opts = {}) {
|
|
|
12024
12013
|
const rulesActive0 = rules0.filter(r => r.status === 'active').length;
|
|
12025
12014
|
const planText0 = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
12026
12015
|
const milestones0 = (planText0.match(/^### M-\d{4}\./gm) || []).length;
|
|
12027
|
-
const
|
|
12028
|
-
const lessonsCount0 = (lm0.match(/^### \d{4}-\d{2}-\d{2}[^\n]*/gm) || []).length;
|
|
12016
|
+
const lessonsCount0 = _loadLessons(root).length;
|
|
12029
12017
|
// 1.9.130: archive 카운트 통합
|
|
12030
12018
|
const archiveCountsS = { decisions: 0, lessons: 0, plan: 0, total: 0 };
|
|
12031
12019
|
try {
|
|
@@ -12362,8 +12350,7 @@ function sessionClose(root, opts = {}) {
|
|
|
12362
12350
|
const rActive = readRules(root).filter(r => r.status === 'active').length;
|
|
12363
12351
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
12364
12352
|
const pCnt = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
12365
|
-
const
|
|
12366
|
-
const lCnt = (lm.match(/^### \d{4}-\d{2}-\d{2}[^\n]*/gm) || []).length;
|
|
12353
|
+
const lCnt = _loadLessons(root).length;
|
|
12367
12354
|
const mem = `T${tIn}/D${dCnt}/R${rActive}/P${pCnt}/L${lCnt}`;
|
|
12368
12355
|
let pulseLine = `📍 v${VERSION} · 🔄 R${rh.roundCount} · 🧠 ${mem}`;
|
|
12369
12356
|
if (ms.next) {
|
|
@@ -18001,7 +17988,7 @@ function contextCmd(root, opts = {}) {
|
|
|
18001
17988
|
tasksInProgress: rows.filter(r => r.status === 'in-progress').length,
|
|
18002
17989
|
decisions: decisionCount,
|
|
18003
17990
|
rulesActive: activeRules.length,
|
|
18004
|
-
lessons:
|
|
17991
|
+
lessons: _loadLessons(root).length
|
|
18005
17992
|
};
|
|
18006
17993
|
// 프로젝트 청사진 (1.9.308 UR-0055 2단계): brief 에서 의도/개요/방향이력 회수 (단일 출처)
|
|
18007
17994
|
const brief = _loadBrief(root);
|
|
@@ -19606,8 +19593,7 @@ function healthCmd(root) {
|
|
|
19606
19593
|
const rulesActive = rules.filter(r => r.status === 'active').length;
|
|
19607
19594
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
19608
19595
|
const milestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
19609
|
-
const
|
|
19610
|
-
const lessonsCount = (lm.match(/^### \d{4}-\d{2}-\d{2}[^\n]*/gm) || []).length;
|
|
19596
|
+
const lessonsCount = _loadLessons(root).length;
|
|
19611
19597
|
out.memorySurface = {
|
|
19612
19598
|
tasks: { inProgress: tasksInProgress, total: rows.length, byStatus: tasksByStatus },
|
|
19613
19599
|
decisions: { count: decisionsCount },
|
package/lib/catalogs.js
CHANGED
|
@@ -342,4 +342,19 @@ const STRINGS = {
|
|
|
342
342
|
'common.ready': { ko: '준비 완료', en: 'Ready' }
|
|
343
343
|
};
|
|
344
344
|
|
|
345
|
-
|
|
345
|
+
|
|
346
|
+
// 1.9.341 (UR-0025 심층): 내장 스킬 catalog (9종) — harness 에서 분리. _loadSkillCatalog 의 builtin fallback.
|
|
347
|
+
const BUILTIN_CATALOG = {
|
|
348
|
+
'office': { displayNameKo: '마이크로소프트 오피스 자동화 스킬 라이브러리', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['Word/Excel/PowerPoint 문서 자동화', '템플릿 기반 문서 생성', '표/차트/요약 문서화', '민감정보 제외 규칙 적용'] },
|
|
349
|
+
'commerce-api': { displayNameKo: '커머스 API 연동 스킬 라이브러리', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['쿠팡·롯데온·스마트스토어 API 연동 설계', '주문/상품/매출 동기화', '환경변수 기반 인증 분리', '레이트리밋/재시도/오류 처리'] },
|
|
350
|
+
'crawling': { displayNameKo: '크롤링·브라우저 자동화 스킬 라이브러리', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['Playwright 기반 자동화', '다운로드/로그인 세션 처리', '스크린샷 기반 실패 진단', '약관/권한/차단 위험 점검'] },
|
|
351
|
+
'firebase': { displayNameKo: 'Firebase·Cloud Functions 스킬 라이브러리', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['Firebase Functions 배포 구조', '환경변수/시크릿 분리', '권한/IAM 점검', '로컬 에뮬레이터 검증'] },
|
|
352
|
+
'ads-analytics': { displayNameKo: '광고·GA4 분석 스킬 라이브러리', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['GA4 이벤트/전환 점검', '광고 데이터 수집 구조화', '소스/매체 분석', '리포트 자동화'] },
|
|
353
|
+
'appstore-review': { displayNameKo: '앱스토어 심사 대응 스킬 라이브러리', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['심사 문구 분석', '개인정보 라벨 점검', '리젝 대응 초안', '웹뷰/앱 데이터 수집 구분'] },
|
|
354
|
+
'ai-verified-skill-publisher': { displayNameKo: 'AI 검증 스킬 업로드·라이브러리화 스킬', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['검증된 스킬 정규화', '민감정보 스캔', 'AI 검증 메타데이터 작성', 'npm/git 업로드 dry-run 및 실행 게이트'] },
|
|
355
|
+
'feature-implementation': { displayNameKo: '기능 구현 표준 스킬', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['feature-contracts 작성', '재사용 우선 검사', '테스트 증거 수집', '핸드오프 트리거'] },
|
|
356
|
+
// 1.9.11: 기본 내장 — 로드맵 자동 생성 스킬
|
|
357
|
+
'project-roadmap-generator': { displayNameKo: '프로젝트 로드맵 자동 생성 스킬', version: '0.2.0', lastUpdated: '2026-05-12', verification: 'passed', capabilities: ['leerness .harness/* 통합 파싱 (plan/progress/skills/rules/decisions/handoff/current-state)', '좌→우 수평 트리 + 상하 중앙정렬 SVG', '7개 상태 색상 (완료/진행/보류/검토/예정/미완료/오류)', 'design-system + CSS variables 자동 주입', '화이트보드 panning/zoom + 더블클릭 reset', '단일 HTML 출력 (외부 의존성 0)'] }
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
module.exports = { 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 };
|
package/lib/pure-utils.js
CHANGED
|
@@ -375,6 +375,22 @@ function _parseLessonEntries(text) {
|
|
|
375
375
|
return out;
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
// UR-0058: canonical lessons 객체 배열 → lessons.md projection. _parseLessonEntries 와 round-trip 안전.
|
|
379
|
+
function _renderLessonsMd(lessons) {
|
|
380
|
+
const preamble = '# Lessons (1.9.112)\n\n과거 실수/통찰/패턴 영구 기록 — handoff 자동 회수와 통합.\n';
|
|
381
|
+
const body = (lessons || []).map(l =>
|
|
382
|
+
`\n### ${l.date}\n- Lesson: ${l.text}\n${l.tag ? `- Tag: ${l.tag}\n` : ''}`
|
|
383
|
+
).join('');
|
|
384
|
+
return preamble + body;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 1.9.341 (UR-0025 심층): 내장 스킬 catalog → _source:'builtin' 부여 맵 (skillpack fallback 순수 변환).
|
|
388
|
+
function _withBuiltinSource(catalog) {
|
|
389
|
+
const out = {};
|
|
390
|
+
for (const [k, v] of Object.entries(catalog || {})) out[k] = { ...v, _source: 'builtin' };
|
|
391
|
+
return out;
|
|
392
|
+
}
|
|
393
|
+
|
|
378
394
|
// 1.9.333 (UR-0025 심층): 순수 플랫폼 제약 매칭 — catalog + 텍스트 → 매칭 플랫폼/제약/제안 (fs 의존 0, catalog 주입).
|
|
379
395
|
function _matchConstraints(catalog, text) {
|
|
380
396
|
if (!text || typeof text !== 'string' || !catalog || !catalog.platforms) return { matched: [], suggestions: [] };
|
|
@@ -571,8 +587,10 @@ module.exports = {
|
|
|
571
587
|
_BRIEF_FIELDS, _briefFilled,
|
|
572
588
|
// 1.9.331 (UR-0025): project-brief 텍스트 빌더 + 마커
|
|
573
589
|
BRIEF_START, BRIEF_END, _briefReadmeBlock, _briefBlueprint,
|
|
574
|
-
// 1.9.332
|
|
575
|
-
_parseLessonEntries,
|
|
590
|
+
// 1.9.332/UR-0058: 순수 lessons.md 파서 + canonical projection renderer
|
|
591
|
+
_parseLessonEntries, _renderLessonsMd,
|
|
592
|
+
// 1.9.341 (UR-0025 심층): 내장 스킬 catalog _source 부여
|
|
593
|
+
_withBuiltinSource,
|
|
576
594
|
// 1.9.333 (UR-0025 심층): 순수 플랫폼 제약 매칭
|
|
577
595
|
_matchConstraints,
|
|
578
596
|
// 1.9.333 패턴 적용: 순수 도메인 매칭
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -4323,5 +4323,81 @@ total++;
|
|
|
4323
4323
|
if (!ok) failed++;
|
|
4324
4324
|
}
|
|
4325
4325
|
|
|
4326
|
+
// 1.9.340 회귀 (UR-0058, Codex 위임·검증): lessons canonical = JSON, MD = projection. save→json+md / list+tag / drop+archive / MD-only 백필
|
|
4327
|
+
total++;
|
|
4328
|
+
{
|
|
4329
|
+
let ok = false;
|
|
4330
|
+
try {
|
|
4331
|
+
const m = require(path.resolve(__dirname, '..', 'lib', 'pure-utils.js'));
|
|
4332
|
+
// 순수 round-trip (tag null 포함)
|
|
4333
|
+
const objs = [{ date: '2026-06-05', text: 'A', tag: 't' }, { date: '2026-06-04', text: 'B', tag: null }];
|
|
4334
|
+
const pureOk = JSON.stringify(m._parseLessonEntries(m._renderLessonsMd(objs))) === JSON.stringify(objs)
|
|
4335
|
+
&& m._parseLessonEntries(m._renderLessonsMd([])).length === 0;
|
|
4336
|
+
// end-to-end: save → lessons.json(canonical) + lessons.md(projection)
|
|
4337
|
+
const ld = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-les-'));
|
|
4338
|
+
cp.spawnSync(process.execPath, [CLI, 'init', ld, '--yes', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
|
|
4339
|
+
cp.spawnSync(process.execPath, [CLI, 'lesson', 'save', 'JWT 짧게', '--tag', 'security', '--path', ld], { encoding: 'utf8', timeout: 20000 });
|
|
4340
|
+
cp.spawnSync(process.execPath, [CLI, 'lesson', 'save', '캐시 TTL', '--path', ld], { encoding: 'utf8', timeout: 20000 });
|
|
4341
|
+
const jsonExists = fs.existsSync(path.join(ld, '.harness', 'lessons.json'));
|
|
4342
|
+
const canon = jsonExists ? JSON.parse(fs.readFileSync(path.join(ld, '.harness', 'lessons.json'), 'utf8')) : [];
|
|
4343
|
+
const mdProj = fs.existsSync(path.join(ld, '.harness', 'lessons.md')) ? fs.readFileSync(path.join(ld, '.harness', 'lessons.md'), 'utf8') : '';
|
|
4344
|
+
const writeOk = jsonExists && canon.length === 2 && canon[0].tag === 'security' && canon[1].tag === null
|
|
4345
|
+
&& mdProj.includes('JWT 짧게') && mdProj.includes('캐시 TTL') && mdProj.includes('- Tag: security');
|
|
4346
|
+
// list + tag filter
|
|
4347
|
+
let listOk = false, tagOk = false;
|
|
4348
|
+
const lr = cp.spawnSync(process.execPath, [CLI, 'lesson', 'list', '--path', ld, '--json'], { encoding: 'utf8', timeout: 20000 });
|
|
4349
|
+
try { listOk = JSON.parse(lr.stdout).total === 2; } catch {}
|
|
4350
|
+
const tr = cp.spawnSync(process.execPath, [CLI, 'lesson', 'list', '--tag', 'security', '--path', ld, '--json'], { encoding: 'utf8', timeout: 20000 });
|
|
4351
|
+
try { tagOk = JSON.parse(tr.stdout).total === 1; } catch {}
|
|
4352
|
+
// drop + archive
|
|
4353
|
+
cp.spawnSync(process.execPath, [CLI, 'lesson', 'drop', 'JWT', '--path', ld], { encoding: 'utf8', timeout: 20000 });
|
|
4354
|
+
const afterDrop = JSON.parse(fs.readFileSync(path.join(ld, '.harness', 'lessons.json'), 'utf8'));
|
|
4355
|
+
const dropOk = afterDrop.length === 1 && afterDrop[0].text === '캐시 TTL' && fs.existsSync(path.join(ld, '.harness', 'lessons.archive.md'));
|
|
4356
|
+
fs.rmSync(ld, { recursive: true, force: true });
|
|
4357
|
+
// 백필: MD-only(JSON 없음) → list 가 MD 파싱, 읽기 무부작용
|
|
4358
|
+
const bd = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-lbf-'));
|
|
4359
|
+
cp.spawnSync(process.execPath, [CLI, 'init', bd, '--yes', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
|
|
4360
|
+
fs.rmSync(path.join(bd, '.harness', 'lessons.json'), { force: true });
|
|
4361
|
+
fs.writeFileSync(path.join(bd, '.harness', 'lessons.md'), '# Lessons\n\n### 2026-06-01\n- Lesson: 기존1\n- Tag: t1\n\n### 2026-06-02\n- Lesson: 기존2\n', 'utf8');
|
|
4362
|
+
const br = cp.spawnSync(process.execPath, [CLI, 'lesson', 'list', '--path', bd, '--json'], { encoding: 'utf8', timeout: 20000 });
|
|
4363
|
+
let backfillOk = false;
|
|
4364
|
+
try { backfillOk = JSON.parse(br.stdout).total === 2 && !fs.existsSync(path.join(bd, '.harness', 'lessons.json')); } catch {}
|
|
4365
|
+
fs.rmSync(bd, { recursive: true, force: true });
|
|
4366
|
+
ok = pureOk && writeOk && listOk && tagOk && dropOk && backfillOk;
|
|
4367
|
+
} catch {}
|
|
4368
|
+
console.log(ok ? '✓ B(1.9.340) UR-0058(Codex 위임·검증): lessons canonical JSON + MD projection + 백필 (UR-0058)' : '✗ lessons canonical JSON 실패');
|
|
4369
|
+
if (!ok) failed++;
|
|
4370
|
+
}
|
|
4371
|
+
|
|
4372
|
+
// 1.9.341 회귀 (UR-0025 심층): BUILTIN_CATALOG→lib/catalogs + _withBuiltinSource→pure-utils + skill list 회귀
|
|
4373
|
+
total++;
|
|
4374
|
+
{
|
|
4375
|
+
let ok = false;
|
|
4376
|
+
try {
|
|
4377
|
+
const c = require(path.resolve(__dirname, '..', 'lib', 'catalogs.js'));
|
|
4378
|
+
const m = require(path.resolve(__dirname, '..', 'lib', 'pure-utils.js'));
|
|
4379
|
+
const catOk = c.BUILTIN_CATALOG && Object.keys(c.BUILTIN_CATALOG).length === 9 && c.BUILTIN_CATALOG.office && c.BUILTIN_CATALOG.office.version === '1.0.0';
|
|
4380
|
+
const out = m._withBuiltinSource(c.BUILTIN_CATALOG);
|
|
4381
|
+
const work = catOk && typeof m._withBuiltinSource === 'function' && Object.keys(out).length === 9
|
|
4382
|
+
&& Object.values(out).every(v => v._source === 'builtin') && Array.isArray(out.office.capabilities)
|
|
4383
|
+
&& Object.keys(m._withBuiltinSource(null)).length === 0;
|
|
4384
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
|
|
4385
|
+
const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
|
|
4386
|
+
const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
|
|
4387
|
+
const movedOut = !/const BUILTIN_CATALOG = \{/.test(harnessSrc) && _catImp.includes('BUILTIN_CATALOG') && _puImp.includes('_withBuiltinSource')
|
|
4388
|
+
&& harnessSrc.includes('_withBuiltinSource(BUILTIN_CATALOG)');
|
|
4389
|
+
// 소비 명령 회귀: skill list (builtin catalog 노출)
|
|
4390
|
+
const sd = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-skl-'));
|
|
4391
|
+
cp.spawnSync(process.execPath, [CLI, 'init', sd, '--yes', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
|
|
4392
|
+
const sr = cp.spawnSync(process.execPath, [CLI, 'skill', 'list', '--path', sd], { encoding: 'utf8', timeout: 20000 });
|
|
4393
|
+
const listOut = (sr.stdout || '') + (sr.stderr || '');
|
|
4394
|
+
const cmdOk = /office|firebase|feature-implementation|roadmap/i.test(listOut);
|
|
4395
|
+
ok = work && movedOut && cmdOk;
|
|
4396
|
+
fs.rmSync(sd, { recursive: true, force: true });
|
|
4397
|
+
} catch {}
|
|
4398
|
+
console.log(ok ? '✓ B(1.9.341) UR-0025 심층: BUILTIN_CATALOG/_withBuiltinSource 분리 + skill list 회귀 (UR-0025)' : '✗ BUILTIN_CATALOG 분리 실패');
|
|
4399
|
+
if (!ok) failed++;
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4326
4402
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
4327
4403
|
if (failed > 0) process.exit(1);
|