leerness 1.36.52 → 1.36.54
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 +22 -0
- package/README.md +4 -4
- package/bin/leerness.js +86 -11
- package/lib/clarify.js +34 -10
- package/lib/graph.js +35 -18
- package/lib/io.js +8 -4
- package/lib/tech-profile.js +188 -0
- package/package.json +1 -1
- package/scripts/e2e.js +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.54 — 2026-07-21 — codex 7차 헌트: 신작 표면(1.36.49~53) 12건 전수 수정 (High 2 · Med 7 · Low 3)
|
|
4
|
+
|
|
5
|
+
직전 3개 릴리스에서 신설한 기능(clarify/preview/tech/그래프 탭/io v2)을 외부 적대 검증 — 12건 발견, 전수 독립 재현 후 전부 수정 (반박 0).
|
|
6
|
+
|
|
7
|
+
- **#1 High — preview 형상-무효 스토어 파괴/크래시**: 파싱되지만 무효(비배열 루트/null 항목)를 []로 오인해 덮어씀(6차 rules 와 동일 클래스 재발!) — 로더 형상검증 + 변경 진입점 store_invalid 거부.
|
|
8
|
+
- **#2 High — 동시 preview add lost-update**: 락 없는 read-modify-write — _withLock 직렬화(락 안 재로드). 실측: 동시 2건 모두 생존·고유 ID.
|
|
9
|
+
- **#3 — clarify FP/FN**: "A 또는 B 모두 테스트" 오질문(모두/둘다/both 억제) + "좀 더 좋게" 미감지(비교급 모호 패턴 추가).
|
|
10
|
+
- **#4 — DATABASE_URL→postgres 오단정**: DB 중립 키 제외(.env 값 안 읽는 원칙 유지). **#5 — 모노레포 하위 매니페스트 미감지**: 2단계 병합 스캔. **#6 — 스키마-무효 프로필 고착**: {} 를 이전 프로필로 오인해 영구 미갱신 — 부재 취급 재생성. **#12 — requirements 점-접두 오탐**(openai.fake→openai): 배포명 전체 일치.
|
|
11
|
+
- **#7 — io 계약 모순 2종**: failJson 후 log(JSON) 이중 문서(래치 후 JSON 통과는 지연-fail 전용) + fail 후 회복 exit 0 인데 폴백 오류 JSON(최종 exit≠0 일 때만 방출). **#8 — 같은 ms 백업명 충돌**: pid+시퀀스 접미.
|
|
12
|
+
- **#9 — 동등 주장의 표현별 판정 불일치**: 실행-우선(1.36.52)을 비율형만 적용해 "3 tests passed"와 "3/3 passed"가 갈렸다 — 카운트형에도 동일 적용(실행 미확증 정적 게이트는 유지).
|
|
13
|
+
- **#10 — approve 중복 이력/빈 revise**: 멱등 no-op + note_required. **#11 — 제목 개행**: 한 줄 정규화.
|
|
14
|
+
- 검증: 12/12 행위 재검 실측(동시성·형상·io 3종·양방향 verify-claim 포함), selftest 328(+1 통합), e2e.
|
|
15
|
+
|
|
16
|
+
## 1.36.53 — 2026-07-21 — 기술 프로필: 언어·서비스 자동 감지 + 마이그레이션 이력 + 그래프 🛠 탭 (사용자 요청 UR-0062)
|
|
17
|
+
|
|
18
|
+
사용자 요청: "어떤 언어로 개발 중인지·어떤 서비스에 연결돼 있는지(마이그레이션/언어 전환 반영) 온톨로지 그래프에 표시, 그래프의 토글 스위치는 제거하고 CLI 명령/사용가이드로 대체".
|
|
19
|
+
|
|
20
|
+
- **`leerness tech [--json]`** (lib/tech-profile.js 신설): 언어 9종(매니페스트+얕은 확장자 스캔) · 서비스 21종(deps 이름/.env 키 이름/설정파일 — 값은 절대 안 읽음, 네트워크 0) 감지. `.harness/tech-profile.json` 에 현재 상태 + **변경 이력 diff**(±언어/±서비스, cap 50) 누적 — 서비스 마이그레이션·언어 전환이 시점과 함께 남는다. 실측: stripe→firebase 교체 시 `+서비스 firebase · -서비스 stripe` 자동 기록.
|
|
21
|
+
- **handoff 자동 갱신**: 세션 시작마다 프로필 재감지 — AI 가 항상 최신 기술 스택을 컨텍스트로 갖는다.
|
|
22
|
+
- **온톨로지 그래프 🛠 기술 탭**: 언어/서비스(근거 포함)/변경 이력 표시. **⚙ 토글 스위치 UI 제거**(사용자 요청) → 토글 상태 표(ON/OFF 텍스트) + `leerness toggle list|set`·`leerness tech` 사용 가이드로 대체.
|
|
23
|
+
- 검증: selftest 327(+1 행위: 감지→이력 diff→탭 임베드·스위치 부재), e2e +1 라운드트립.
|
|
24
|
+
|
|
3
25
|
## 1.36.52 — 2026-07-21 — 외부 GPT 감사(1.36.47) P0 채택 (UR-0063)
|
|
4
26
|
|
|
5
27
|
웹 구독 GPT 의 독립 감사(격리 설치·오류 주입·패치 첨부, 10건) 중 재현 확정 2건 + 채택 판단 2건을 반영. 전 건 현행 소스에서 독립 재현/검증 후 수정.
|
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ MIT
|
|
|
122
122
|
<!-- leerness:project-readme:start -->
|
|
123
123
|
## Leerness Project Harness
|
|
124
124
|
|
|
125
|
-
이 프로젝트는 Leerness v1.36.
|
|
125
|
+
이 프로젝트는 Leerness v1.36.54 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
126
126
|
|
|
127
127
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
128
128
|
|
|
@@ -176,7 +176,7 @@ leerness memory restore decision <date|title>
|
|
|
176
176
|
|
|
177
177
|
### MCP server (외부 AI 통합)
|
|
178
178
|
|
|
179
|
-
Leerness v1.36.
|
|
179
|
+
Leerness v1.36.54는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
180
180
|
|
|
181
181
|
```jsonc
|
|
182
182
|
// 카테고리별
|
|
@@ -197,7 +197,7 @@ Leerness v1.36.52는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
197
197
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
198
198
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
199
199
|
|
|
200
|
-
현재 누적: **v1.9.x → 1.36.
|
|
200
|
+
현재 누적: **v1.9.x → 1.36.54 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
201
201
|
|
|
202
202
|
### 성능 가이드
|
|
203
203
|
|
|
@@ -235,6 +235,6 @@ leerness release pack --close --auto-main-push
|
|
|
235
235
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
236
236
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
237
237
|
|
|
238
|
-
Last synced by Leerness v1.36.
|
|
238
|
+
Last synced by Leerness v1.36.54: 2026-07-21
|
|
239
239
|
<!-- leerness:project-readme:end -->
|
|
240
240
|
|
package/bin/leerness.js
CHANGED
|
@@ -34,7 +34,7 @@ const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE
|
|
|
34
34
|
const { tokenizeForRank: _tokenizeForRank, expandQuery: _expandQuery, scoreHits: _scoreHits, suggestTerms: _suggestTerms } = require('../lib/search-core'); // 1.36.23: memory search 랭킹 코어(순수·0-deps)
|
|
35
35
|
const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
|
|
36
36
|
|
|
37
|
-
const VERSION = '1.36.
|
|
37
|
+
const VERSION = '1.36.54';
|
|
38
38
|
|
|
39
39
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
40
40
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -3143,7 +3143,7 @@ function _selfTestCases() {
|
|
|
3143
3143
|
const o = '<scr' + 'ipt>', c = '</scr' + 'ipt>';
|
|
3144
3144
|
const js = html.slice(html.indexOf(o) + o.length, html.lastIndexOf(c));
|
|
3145
3145
|
let synOk = false; try { new Function(js); synOk = true; } catch {}
|
|
3146
|
-
htmlOk = html.includes('data-v="roadmap"') && html.includes('data-v="
|
|
3146
|
+
htmlOk = html.includes('data-v="roadmap"') && html.includes('data-v="tech"') && html.includes('toggleRegistry') && synOk; // 1.36.53: ⚙토글 탭 → 🛠기술 탭(토글 상태표+CLI 가이드 포함)
|
|
3147
3147
|
} catch { behavOk = false; } finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
3148
3148
|
// 배선 소스가드: 자동생성이 leerness.html(온톨로지)로 전환 + gate/lens 토글 준수 + toggle dispatch
|
|
3149
3149
|
const s = read(__filename);
|
|
@@ -5056,6 +5056,76 @@ function _selfTestCases() {
|
|
|
5056
5056
|
// F-08: Unix .env 0600
|
|
5057
5057
|
const envOk = /mergeEnvFile[\s\S]{0,600}chmodSync\(p, 0o600\)/.test(s);
|
|
5058
5058
|
return langOk && aliasOk && ratioOk && envOk;
|
|
5059
|
+
} },
|
|
5060
|
+
{ name: '기술 프로필 (1.36.53, UR-0062): 언어·서비스 감지 + 마이그레이션 이력 diff + 그래프 🛠 탭 임베드 — 행위검사', run: () => {
|
|
5061
|
+
const tp = require('../lib/tech-profile');
|
|
5062
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_tech_'));
|
|
5063
|
+
const _w = process.stdout.write;
|
|
5064
|
+
try {
|
|
5065
|
+
process.stdout.write = () => true;
|
|
5066
|
+
fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
|
|
5067
|
+
fs.writeFileSync(path.join(tmp, 'package.json'), JSON.stringify({ name: 'x', dependencies: { stripe: '^1', '@supabase/supabase-js': '^2' } }));
|
|
5068
|
+
fs.writeFileSync(path.join(tmp, '.env'), 'OPENAI_API_KEY=placeholder\n');
|
|
5069
|
+
const d1 = tp.detectTechProfile(tmp);
|
|
5070
|
+
const ids = a => a.map(x => x.id);
|
|
5071
|
+
const det = ids(d1.languages).includes('javascript') && ['stripe', 'supabase', 'openai'].every(s2 => ids(d1.services).includes(s2));
|
|
5072
|
+
const r1 = tp.refreshTechProfile(tmp);
|
|
5073
|
+
const saved = fs.existsSync(tp._techPath(tmp)) && r1.profile.history.length === 0;
|
|
5074
|
+
// 마이그레이션: stripe 제거 + firebase 추가 → 이력 diff 기록
|
|
5075
|
+
fs.writeFileSync(path.join(tmp, 'package.json'), JSON.stringify({ name: 'x', dependencies: { firebase: '^10', '@supabase/supabase-js': '^2' } }));
|
|
5076
|
+
const r2 = tp.refreshTechProfile(tmp);
|
|
5077
|
+
const migr = r2.changed && r2.diff.addedServices.includes('firebase') && r2.diff.removedServices.includes('stripe') && tp.loadTechProfile(tmp).history.length === 1;
|
|
5078
|
+
// 그래프 🛠 탭: tech 데이터 임베드 + 스위치 UI 제거 + CLI 가이드
|
|
5079
|
+
const g = require('../lib/graph');
|
|
5080
|
+
const out = path.join(tmp, 'leerness.html');
|
|
5081
|
+
g.graphHtmlCmd(tmp, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadTechProfile: tp.loadTechProfile, quiet: true }, out);
|
|
5082
|
+
const html = fs.readFileSync(out, 'utf8');
|
|
5083
|
+
const tabOk = html.includes('data-v="tech"') && !html.includes('tgFlip') && html.includes('leerness toggle set') && html.includes('firebase');
|
|
5084
|
+
return det && saved && migr && tabOk;
|
|
5085
|
+
} finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
5086
|
+
} },
|
|
5087
|
+
{ name: 'codex 7차 헌트 12종 (1.36.54): preview 형상검증/락·clarify 억제/보강·tech DATABASE_URL 제외/모노레포/스키마검증/점-접두·io 이중JSON/폴백exit/백업충돌·count 실행우선 — 행위+소스가드', run: () => {
|
|
5088
|
+
const c = require('../lib/clarify');
|
|
5089
|
+
// #3 행위: 모두-표지 억제 + 비교급 모호 감지
|
|
5090
|
+
const r1 = c._clarifySignals('A 또는 B 케이스 모두 테스트해줘');
|
|
5091
|
+
const r2 = c._clarifySignals('좀 더 좋게 해줘');
|
|
5092
|
+
const clar = r1.ambiguous === false && r2.ambiguous === true;
|
|
5093
|
+
// #1 행위: 형상-무효 로더 판정
|
|
5094
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_pv54_'));
|
|
5095
|
+
let shape = false;
|
|
5096
|
+
try {
|
|
5097
|
+
fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
|
|
5098
|
+
fs.writeFileSync(c._previewsPath(tmp), '{"legacy":{"id":"P-0042"}}');
|
|
5099
|
+
const chk1 = c._loadPreviewsChecked(tmp);
|
|
5100
|
+
fs.writeFileSync(c._previewsPath(tmp), '[{"id":"P-0001","title":"ok"},null]');
|
|
5101
|
+
const chk2 = c._loadPreviewsChecked(tmp);
|
|
5102
|
+
fs.writeFileSync(c._previewsPath(tmp), '[{"id":"P-0001","title":"ok"}]');
|
|
5103
|
+
const chk3 = c._loadPreviewsChecked(tmp);
|
|
5104
|
+
shape = chk1.invalid && chk2.invalid && !chk3.invalid && chk3.list.length === 1;
|
|
5105
|
+
} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
5106
|
+
// #4/#12 행위: DATABASE_URL 제외 + 점-접두 오탐 차단
|
|
5107
|
+
const tp = require('../lib/tech-profile');
|
|
5108
|
+
const pgSig = tp.SERVICE_SIGNALS.find(s => s.id === 'postgres');
|
|
5109
|
+
const noDbUrl = !pgSig.env.includes('DATABASE_URL');
|
|
5110
|
+
const tmp2 = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_tp54_'));
|
|
5111
|
+
let reqOk = false;
|
|
5112
|
+
try {
|
|
5113
|
+
fs.mkdirSync(path.join(tmp2, '.harness'), { recursive: true });
|
|
5114
|
+
fs.writeFileSync(path.join(tmp2, 'requirements.txt'), 'openai.fake==1.0\ndiscord.js==2.0\n');
|
|
5115
|
+
const det = tp.detectTechProfile(tmp2);
|
|
5116
|
+
const ids = det.services.map(s => s.id);
|
|
5117
|
+
reqOk = !ids.includes('openai') && ids.includes('discord');
|
|
5118
|
+
} finally { try { fs.rmSync(tmp2, { recursive: true, force: true }); } catch {} }
|
|
5119
|
+
// 소스가드: 락 직렬화(#2)·스키마검증(#6)·이중JSON 차단(#7a)·폴백 exit 게이트(#7b)·백업 접미(#8)·count 실행우선(#9)·note_required(#10)
|
|
5120
|
+
const s = read(__filename);
|
|
5121
|
+
const cl = read(path.join(__dirname, '..', 'lib', 'clarify.js'));
|
|
5122
|
+
const io2 = read(path.join(__dirname, '..', 'lib', 'io.js'));
|
|
5123
|
+
const tps = read(path.join(__dirname, '..', 'lib', 'tech-profile.js'));
|
|
5124
|
+
const guards = cl.includes('_withLock(_previewsPath(root)') && cl.includes("failJson(json, 'note_required'")
|
|
5125
|
+
&& tps.includes('prev = null') && tps.includes('_mergeManifests(root, 2)')
|
|
5126
|
+
&& io2.includes('!_jsonErrEmitted && t && (t[0]') && /process\.on\('exit', \(code\) => \{ if \(code !== 0/.test(io2) && io2.includes('.corrupt-${Date.now()}-${process.pid}-')
|
|
5127
|
+
&& s.includes('declaredTestCount != null && runResult && !runResult.skipped && runResult.allPassed');
|
|
5128
|
+
return clar && shape && noDbUrl && reqOk && guards;
|
|
5059
5129
|
} }
|
|
5060
5130
|
];
|
|
5061
5131
|
}
|
|
@@ -6195,6 +6265,7 @@ function commandsCmd(root) {
|
|
|
6195
6265
|
{ cmd: 'slash-commands [agent] [--json --record --detect --refresh]', desc: 'CLI 에이전트 슬래시 명령 레지스트리 + --help probe 자동 갱신 (1.9.265~267, UR-0021)' },
|
|
6196
6266
|
{ cmd: 'review-request "<request>"', desc: '사용자 요청 사전 검토 (1.9.176)' },
|
|
6197
6267
|
{ cmd: 'clarify "<사용자 요청>" [--json]', desc: '요청 모호성 신호 감지 → 사용자에게 물을 질문 생성 (추측 구현 방지) — 1.36.51 UR-0061' },
|
|
6268
|
+
{ cmd: 'tech [--json]', desc: '기술 프로필 — 개발 언어·연결 서비스 자동 감지 + 마이그레이션/언어전환 이력, 그래프 🛠 탭 표시 — 1.36.53 UR-0062' },
|
|
6198
6269
|
{ cmd: 'preview add|list|show|approve|revise', desc: '신규 기능 미리보기 승인 워크플로 — approve 전 코드 작성 금지 계약 — 1.36.51 UR-0061' },
|
|
6199
6270
|
{ cmd: 'review <file> --persona <ids>', desc: '페르소나 리뷰 (1.9.29)' },
|
|
6200
6271
|
{ cmd: 'brainstorm "<topic>" [--include-code]', desc: '워크스페이스 회수 + 코드 grep' }
|
|
@@ -8586,6 +8657,7 @@ function _saveTeams(root, teams) {
|
|
|
8586
8657
|
const _team = require('../lib/team');
|
|
8587
8658
|
const _tgl = require('../lib/toggles'); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동 — gate/lens/auto-graph/delegation-brief)
|
|
8588
8659
|
const _clar = require('../lib/clarify'); // 1.36.51 (UR-0061): 모호성 질문 + 미리보기 승인 워크플로
|
|
8660
|
+
const _tech = require('../lib/tech-profile'); // 1.36.53 (UR-0062): 기술 프로필 (언어·서비스 감지 + 변경 이력)
|
|
8589
8661
|
function teamCmd(root, sub, id, opts = {}) { return _team.teamCmd(root, sub, id, opts, { VERSION, _loadTeams, _saveTeams, _detectShellCtx, arg, has, _withLock }); } // 1.36.31: add 경합 락
|
|
8590
8662
|
|
|
8591
8663
|
// 1.9.112: 전용 lessons.md (Memory Write Surface 5번째)
|
|
@@ -12330,10 +12402,12 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
12330
12402
|
// 1.9.309 (UR-0048, 설치리뷰 Opus critical): done 주장은 evidence(파일+테스트) 기본 강제 — 거짓완료 차단을 기본값·MCP·json 모두에서 작동.
|
|
12331
12403
|
// 이전: --require-evidence 플래그 시에만 검사 → 증거0 done 이 기본 통과(fileChecks.every([])=공허참)했고 MCP 는 도달 불가했음.
|
|
12332
12404
|
// 이제 done/완료 주장은 기본 강제. opt-out: --lenient. 비-done 주장은 영향 없음.
|
|
12333
|
-
// 1.36.52 (F-02 보정, 게이트 실측):
|
|
12334
|
-
// 실제 실행으로 확증하면
|
|
12335
|
-
//
|
|
12336
|
-
|
|
12405
|
+
// 1.36.52 (F-02 보정, 게이트 실측): 정적 선언-수 대조는 실행 확증이 없을 때의 휴리스틱 — --run-tests 가 주장 수
|
|
12406
|
+
// 이상 전부-통과를 실제 실행으로 확증하면 재적용하지 않는다(실행 증거 > 정적 휴리스틱).
|
|
12407
|
+
// 1.36.54 (codex 7차 #9): 비율형(_ratioPromoted)만이 아니라 카운트형("N tests passed")도 동일 우선순위 —
|
|
12408
|
+
// 같은 주장을 표현만 바꿨을 때 판정이 갈리던 비일관성 해소. void _ratioPromoted (표시용으로만 유지).
|
|
12409
|
+
void _ratioPromoted;
|
|
12410
|
+
if (declaredTestCount != null && runResult && !runResult.skipped && runResult.allPassed && runResult.parsed && runResult.parsed.num >= declaredTestCount) declaredTestCount = null;
|
|
12337
12411
|
const isDoneClaim = /done|완료|completed/i.test(row.status || '');
|
|
12338
12412
|
const evq = _evidenceQuality(evidence);
|
|
12339
12413
|
const mustHaveEvidence = !has('--lenient') && (isDoneClaim || has('--require-evidence'));
|
|
@@ -15684,7 +15758,7 @@ function _autoRoadmap(root, trigger) {
|
|
|
15684
15758
|
// 토글 auto-graph OFF 시 생성 안 함(⚙ 탭/`leerness toggle`).
|
|
15685
15759
|
if (!_tgl.toggleOn(root, 'auto-graph')) return false;
|
|
15686
15760
|
const outFile = path.resolve(cfg.outFile || path.join(root, 'leerness.html'));
|
|
15687
|
-
const s = _graph.graphHtmlCmd(root, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, quiet: true }, outFile);
|
|
15761
|
+
const s = _graph.graphHtmlCmd(root, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, _loadTechProfile: _tech.loadTechProfile, quiet: true }, outFile);
|
|
15688
15762
|
const _en = _uiLang(root) === 'en';
|
|
15689
15763
|
log(_en ? `✓ ontology graph auto-updated (${trigger}) — ${rel(root, outFile)} (graph+roadmap+toggles)` : `✓ 온톨로지 그래프 자동 갱신 (${trigger}) — ${rel(root, outFile)} (그래프+로드맵+토글 탭)`);
|
|
15690
15764
|
return true;
|
|
@@ -22171,14 +22245,14 @@ function doctorCmd(opts = {}) { return _diag.doctorCmd(opts, { VERSION, uiLang:
|
|
|
22171
22245
|
function whichCmd() { return _diag.whichCmd({ VERSION, uiLang: _uiLang(arg('--path', process.cwd())), has, harnessPath: __filename }); }
|
|
22172
22246
|
// 1.34.3 (T-0077): `graph --html` → lib/graph.js 온톨로지 HTML(leerness.html) 생성기 위임. 데이터는 in-process 로더 주입(자식 프로세스 셸링 X).
|
|
22173
22247
|
const _graph = require('../lib/graph');
|
|
22174
|
-
function graphHtmlCmd(root) { return _graph.graphHtmlCmd(root, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, has, arg }); } // 1.36.30: 로드맵 탭 + ⚙ 토글 탭 + canonical feature
|
|
22248
|
+
function graphHtmlCmd(root) { return _graph.graphHtmlCmd(root, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, _loadTechProfile: _tech.loadTechProfile, has, arg }); } // 1.36.30: 로드맵 탭 + ⚙ 토글 탭 + canonical feature
|
|
22175
22249
|
// 1.34.4 (T-0077 후속): handoff 시 leerness.html 자동 재생성 — opt-in(LEERNESS_AUTO_GRAPH=1, 기본 OFF / "Always-Off Opt-In"). 사용자 비전 "자동으로 작성되게" 충족. 비치명(try/catch) · 기본경로 무영향.
|
|
22176
22250
|
function _maybeAutoGraph(root) {
|
|
22177
22251
|
if (process.env.LEERNESS_AUTO_GRAPH !== '1') return;
|
|
22178
22252
|
try {
|
|
22179
22253
|
const r0 = absRoot(root || process.cwd());
|
|
22180
22254
|
if (!exists(path.join(r0, '.harness'))) return;
|
|
22181
|
-
const s = _graph.graphHtmlCmd(r0, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, quiet: true });
|
|
22255
|
+
const s = _graph.graphHtmlCmd(r0, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, _loadTechProfile: _tech.loadTechProfile, quiet: true });
|
|
22182
22256
|
if (!has('--json') && !has('--quiet') && !has('--compact')) log(`📊 ontology graph auto-regenerated: leerness.html (${s.nodes} nodes · ${s.edges} links) — LEERNESS_AUTO_GRAPH=1`); // --json 출력 오염 방지
|
|
22183
22257
|
} catch {}
|
|
22184
22258
|
}
|
|
@@ -22406,7 +22480,7 @@ async function main() {
|
|
|
22406
22480
|
if (cmd === 'lazy' && args[1] === 'detect') return lazyDetect(_resolveRoot(args[2]), { json: has('--json') });
|
|
22407
22481
|
if (cmd === 'memory' && args[1] === 'search') return memorySearch(arg('--path', process.cwd()), args.slice(2).join(' '));
|
|
22408
22482
|
if (cmd === 'hook' && args[1] === 'session-start') return hookSessionStartCmd(arg('--path', args[2] || process.cwd())); // 1.36.22: SessionStart 컨텍스트 주입(쓰기 0)
|
|
22409
|
-
if (cmd === 'handoff') { const _hp = arg('--path', args[1] || process.cwd()); const _hr = handoffCmd(_hp); _maybeAutoGraph(_hp); return _hr; }
|
|
22483
|
+
if (cmd === 'handoff') { const _hp = arg('--path', args[1] || process.cwd()); const _hr = handoffCmd(_hp); try { _tech.refreshTechProfile(_hp); } catch {} _maybeAutoGraph(_hp); return _hr; } // 1.36.53: 기술 프로필 자동 갱신(마이그레이션 이력) — 그래프 생성 전에
|
|
22410
22484
|
if (cmd === 'reuse-map') return reuseMapCmd(arg('--path', args[1] || process.cwd()));
|
|
22411
22485
|
if (cmd === 'verify-claim') { const _p = arg('--path', process.cwd()); if (args[1] === '--all' || has('--all')) return verifyClaimAllCmd(_p); return verifyClaimCmd(_p, args[1]); } // 1.33.2: --all → 모든 done 주장 일괄 검증
|
|
22412
22486
|
if (cmd === 'orchestrate') return await orchestrateCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('-')));
|
|
@@ -22532,6 +22606,7 @@ async function main() {
|
|
|
22532
22606
|
if (cmd === 'enforce') return enforceCmd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd(), args[1]); // 1.36.43: 사용 강제 (git pre-commit)
|
|
22533
22607
|
if (cmd === 'anchors') return anchorsCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd(), args[1] && !args[1].startsWith('-') ? args[1] : null); // 1.36.36: 정체성앵커 초안
|
|
22534
22608
|
if (cmd === 'toggle') return _tgl.toggleCmd(arg('--path', process.cwd()), args[1], args[2], args[3], { has, VERSION }); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동)
|
|
22609
|
+
if (cmd === 'tech') return _tech.techCmd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd(), args[1], { has }); // 1.36.53 (UR-0062): 기술 프로필
|
|
22535
22610
|
// 1.36.51 (사용자 요청 UR-0061): 모호성 질문 + 미리보기 승인 워크플로
|
|
22536
22611
|
if (cmd === 'clarify') return _clar.clarifyCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('-')).join(' '), { has });
|
|
22537
22612
|
if (cmd === 'preview') {
|
|
@@ -22541,7 +22616,7 @@ async function main() {
|
|
|
22541
22616
|
if (_pvRaw[i].startsWith('--')) { if (['--design', '--features', '--note', '--path'].includes(_pvRaw[i]) && _pvRaw[i + 1]) i++; continue; }
|
|
22542
22617
|
_pvToks.push(_pvRaw[i]);
|
|
22543
22618
|
}
|
|
22544
|
-
return _clar.previewCmd(arg('--path', process.cwd()), _pvToks[0], _pvToks.slice(1), { has, arg });
|
|
22619
|
+
return _clar.previewCmd(arg('--path', process.cwd()), _pvToks[0], _pvToks.slice(1), { has, arg, _withLock }); // 1.36.54 (#2): 변경은 락 직렬화
|
|
22545
22620
|
}
|
|
22546
22621
|
if (cmd === 'lens') return lensCmd(args[1]); // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈
|
|
22547
22622
|
// 1.9.233: leerness commands — 카테고리화된 전체 CLI 명령 목록
|
package/lib/clarify.js
CHANGED
|
@@ -10,11 +10,12 @@ const { absRoot, exists, read, writeUtf8, log, ok, warn, fail, failJson, now, to
|
|
|
10
10
|
// ── ① 모호성 신호 카탈로그 (kind → { re, q(match) }) ──────────────────────────
|
|
11
11
|
const CLARIFY_SIGNALS = [
|
|
12
12
|
// '잘'은 "잘 통과하는지" 같은 정상 문장에 흔해 뒤따르는 요청동사가 있을 때만 (false-PASS 편향)
|
|
13
|
-
{ kind: 'vague-quality', re: /(적당히|알아서|이쁘게|예쁘게|깔끔하게|멋지게|잘\s?(?:좀|만들|해줘|해 줘|부탁)|as appropriate|make it nice|make it pretty)/,
|
|
13
|
+
{ kind: 'vague-quality', re: /(적당히|알아서|이쁘게|예쁘게|깔끔하게|멋지게|잘\s?(?:좀|만들|해줘|해 줘|부탁)|(?:좀 ?)?더 (?:좋게|낫게|이쁘게|예쁘게|깔끔하게)|as appropriate|make it nice|make it pretty|make it better)/,
|
|
14
14
|
q: (m) => `「${m.trim()}」의 기대 수준이 불명확합니다 — 참고할 예시(스크린샷/사이트/기존 화면)나 구체 기준이 있나요?` },
|
|
15
15
|
{ kind: 'pronoun', re: /(그거|이거|저거|아까\s?그|그 부분|위에서 말한|the one before)/,
|
|
16
16
|
q: (m) => `「${m.trim()}」가 무엇을 가리키는지 확인이 필요합니다 — 대상(파일/화면/기능 이름)을 지정해 주세요.` },
|
|
17
|
-
|
|
17
|
+
// 1.36.54 (codex 7차 #3): "모두/둘 다/both" 가 이미 답을 준 문장은 선택지 질문 억제(suppressIf) — false-BLOCK 방지
|
|
18
|
+
{ kind: 'alternative', re: /(\S+)\s?(?:이나|나|또는|혹은)\s(\S+)|(\b\w+\b) or (\b\w+\b)/, suppressIf: /(모두|둘 ?다|양쪽|전부|both|all of)/,
|
|
18
19
|
q: (m) => `복수 선택지(${m.trim()})가 언급됐습니다 — 어느 쪽을 원하시나요, 아니면 둘 다인가요?` },
|
|
19
20
|
{ kind: 'vague-scope', re: /(전부 다|전부|모두 다|모든 걸|싹 다|everything|all of (?:it|them))/,
|
|
20
21
|
q: (m) => `「${m.trim()}」의 범위가 넓습니다 — 포함/제외할 대상을 구체적으로 확인해도 될까요?` },
|
|
@@ -30,7 +31,7 @@ function _clarifySignals(text) {
|
|
|
30
31
|
const signals = [];
|
|
31
32
|
for (const s of CLARIFY_SIGNALS) {
|
|
32
33
|
const m = t.match(s.re);
|
|
33
|
-
if (m) signals.push({ kind: s.kind, match: m[0], question: s.q(m[0]) });
|
|
34
|
+
if (m && !(s.suppressIf && s.suppressIf.test(t))) signals.push({ kind: s.kind, match: m[0], question: s.q(m[0]) });
|
|
34
35
|
}
|
|
35
36
|
return { ambiguous: signals.length > 0, signals, questions: signals.map(s => s.question) };
|
|
36
37
|
}
|
|
@@ -52,11 +53,18 @@ function clarifyCmd(root, text, deps = {}) {
|
|
|
52
53
|
// ── ② 미리보기 승인 스토어 ─────────────────────────────────────────────────
|
|
53
54
|
function _previewsPath(root) { return path.join(absRoot(root), '.harness', 'previews.json'); }
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
// 1.36.54 (codex 7차 #1 High): 파싱은 되지만 형상이 무효(루트 비배열/항목 null/비객체)인 스토어를
|
|
57
|
+
// []로 오인해 덮어쓰던 클래스(6차 rules R-id 와 동일) — 로더가 무효 여부를 함께 반환하고 변경 진입점은 거부.
|
|
58
|
+
function _loadPreviewsChecked(root) {
|
|
56
59
|
const f = _previewsPath(root);
|
|
57
|
-
if (!exists(f)) return [];
|
|
58
|
-
try {
|
|
60
|
+
if (!exists(f)) return { list: [], invalid: false };
|
|
61
|
+
try {
|
|
62
|
+
const j = JSON.parse(read(f));
|
|
63
|
+
if (!Array.isArray(j) || j.some(e => !e || typeof e !== 'object' || typeof e.id !== 'string')) return { list: [], invalid: true };
|
|
64
|
+
return { list: j, invalid: false };
|
|
65
|
+
} catch { return { list: [], invalid: true }; }
|
|
59
66
|
}
|
|
67
|
+
function _loadPreviews(root) { return _loadPreviewsChecked(root).list; }
|
|
60
68
|
|
|
61
69
|
function _savePreviews(root, list, json) {
|
|
62
70
|
const f = _previewsPath(root);
|
|
@@ -80,11 +88,20 @@ function previewCmd(root, sub, rest, deps = {}) {
|
|
|
80
88
|
root = absRoot(root);
|
|
81
89
|
const json = !!(has && has('--json'));
|
|
82
90
|
if (!exists(path.join(root, '.harness'))) { failJson(json, 'harness_missing', `leerness 미설치: ${root} — 먼저 leerness init`); return; }
|
|
83
|
-
const list = _loadPreviews(root);
|
|
84
91
|
sub = sub || 'list';
|
|
92
|
+
// 1.36.54 (#2 High): 변경 하위명령은 락 안에서 재로드→검증→저장 전체를 직렬화 — 동시 add 의 lost-update 차단.
|
|
93
|
+
const _mutating = ['add', 'approve', 'revise'].includes(sub);
|
|
94
|
+
if (_mutating && deps._withLock && !deps._locked) {
|
|
95
|
+
return deps._withLock(_previewsPath(root), () => previewCmd(root, sub, rest, Object.assign({}, deps, { _locked: true })));
|
|
96
|
+
}
|
|
97
|
+
const _chk = _loadPreviewsChecked(root);
|
|
98
|
+
// 1.36.54 (#1 High): 무효 형상 스토어 위 변경 거부 — 원본 보존 (읽기 명령은 빈 목록으로 관용)
|
|
99
|
+
if (_mutating && _chk.invalid) { failJson(json, 'store_invalid', `previews.json 형상 무효(비배열 루트 또는 null/ID 없는 항목) — 덮어쓰기 거부: ${_previewsPath(root)} (수동 복구 후 재시도)`); return; }
|
|
100
|
+
const list = _chk.list;
|
|
85
101
|
|
|
86
102
|
if (sub === 'add') {
|
|
87
|
-
|
|
103
|
+
// 1.36.54 (#11): 제목 개행 정규화 — 한 미리보기 = 목록 한 줄 불변식
|
|
104
|
+
const title = (rest || []).join(' ').replace(/\s*\r?\n\s*/g, ' ').trim();
|
|
88
105
|
if (!title) { failJson(json, 'title_required', 'preview add "<기능 제목>" 필요 (+ --design "설명" --features "a,b")'); return; }
|
|
89
106
|
const design = arg ? (arg('--design', '') || '') : '';
|
|
90
107
|
const features = (arg ? (arg('--features', '') || '') : '').split(',').map(s => s.trim()).filter(Boolean);
|
|
@@ -128,6 +145,11 @@ function previewCmd(root, sub, rest, deps = {}) {
|
|
|
128
145
|
return;
|
|
129
146
|
}
|
|
130
147
|
if (sub === 'approve') {
|
|
148
|
+
// 1.36.54 (#10): 재승인은 멱등 no-op — 중복 이력 방지
|
|
149
|
+
if (p.status === 'approved') {
|
|
150
|
+
if (json) { log(JSON.stringify({ ok: true, id: p.id, status: p.status, changed: false }, null, 2)); return; }
|
|
151
|
+
ok(`${p.id} 이미 승인됨 — 변경 없음`); return;
|
|
152
|
+
}
|
|
131
153
|
p.status = 'approved'; (p.history = p.history || []).push({ at: now(), event: 'approved' });
|
|
132
154
|
if (!_savePreviews(root, list, json)) return;
|
|
133
155
|
if (json) { log(JSON.stringify({ ok: true, id: p.id, status: p.status }, null, 2)); return; }
|
|
@@ -135,7 +157,9 @@ function previewCmd(root, sub, rest, deps = {}) {
|
|
|
135
157
|
return;
|
|
136
158
|
}
|
|
137
159
|
if (sub === 'revise') {
|
|
138
|
-
const note = arg ? (arg('--note', '') || '') : '';
|
|
160
|
+
const note = (arg ? (arg('--note', '') || '') : '').toString().trim();
|
|
161
|
+
// 1.36.54 (#10): 빈 노트 수정요구 거부 — 무엇을 고칠지 없는 revise 는 워크플로 신호가 아니다
|
|
162
|
+
if (!note) { failJson(json, 'note_required', `preview revise ${p.id} --note "<수정 요구 내용>" 필요`); return; }
|
|
139
163
|
p.status = 'revision-requested'; (p.history = p.history || []).push({ at: now(), event: 'revision-requested', note });
|
|
140
164
|
if (!_savePreviews(root, list, json)) return;
|
|
141
165
|
if (json) { log(JSON.stringify({ ok: true, id: p.id, status: p.status, note }, null, 2)); return; }
|
|
@@ -145,4 +169,4 @@ function previewCmd(root, sub, rest, deps = {}) {
|
|
|
145
169
|
failJson(json, 'unknown_subcommand', `알 수 없는 preview 하위명령: ${sub} (가능: add, list, show, approve, revise)`);
|
|
146
170
|
}
|
|
147
171
|
|
|
148
|
-
module.exports = { CLARIFY_SIGNALS, _clarifySignals, clarifyCmd, previewCmd, pendingPreviews, _previewsPath, _loadPreviews };
|
|
172
|
+
module.exports = { CLARIFY_SIGNALS, _clarifySignals, clarifyCmd, previewCmd, pendingPreviews, _previewsPath, _loadPreviews, _loadPreviewsChecked };
|
package/lib/graph.js
CHANGED
|
@@ -65,7 +65,7 @@ canvas{position:fixed;inset:0;top:46px}
|
|
|
65
65
|
</style></head><body>
|
|
66
66
|
<div id="bar">
|
|
67
67
|
<div class="ttl"><span class="dot"></span><span id="proj">leerness</span><span style="color:var(--mut);font-weight:400">/ ontology</span></div>
|
|
68
|
-
<div id="tabs"><span class="tab on" data-v="graph">그래프</span><span class="tab" data-v="roadmap">로드맵</span><span class="tab" data-v="
|
|
68
|
+
<div id="tabs"><span class="tab on" data-v="graph">그래프</span><span class="tab" data-v="roadmap">로드맵</span><span class="tab" data-v="tech">🛠 기술</span></div>
|
|
69
69
|
<span class="stat" id="stat"></span>
|
|
70
70
|
<input id="search" placeholder="search nodes…" autocomplete="off">
|
|
71
71
|
<div id="chips"></div>
|
|
@@ -75,7 +75,7 @@ canvas{position:fixed;inset:0;top:46px}
|
|
|
75
75
|
<div id="empty">No nodes — run <b>leerness handoff .</b> to populate the harness, then regenerate.</div>
|
|
76
76
|
<div id="hint">drag node · scroll zoom · drag bg pan · click node → details · search+Enter jump · f / dblclick fit · p export PNG · Esc close</div>
|
|
77
77
|
<div id="vroadmap" class="view"></div>
|
|
78
|
-
<div id="
|
|
78
|
+
<div id="vtech" class="view"></div>
|
|
79
79
|
<script>
|
|
80
80
|
var DATA = /*__DATA__*/null;
|
|
81
81
|
var COLORS={task:'#58a6ff',plan:'#d29922',decision:'#39d0d8',lesson:'#e3b341',rule:'#bc8cff',skill:'#2dd4bf',feature:'#6e7681'};
|
|
@@ -171,8 +171,8 @@ function exportPng(){ try{ var t=document.createElement('canvas'); t.width=cv.wi
|
|
|
171
171
|
window.addEventListener('keydown',function(ev){ if(ev.target&&ev.target.tagName==='INPUT')return; if(ev.key==='f'||ev.key==='F'){_fit=true;fitView();} else if(ev.key==='p'||ev.key==='P'){exportPng();} else if(ev.key==='Escape'){closePanel();} });
|
|
172
172
|
cv.addEventListener('dblclick',function(ev){ if(!hit(ev.offsetX,ev.offsetY)){_fit=true;fitView();} });
|
|
173
173
|
|
|
174
|
-
// ── 탭 (1.36.
|
|
175
|
-
var _views={roadmap:document.getElementById('vroadmap'),
|
|
174
|
+
// ── 탭 (1.36.53, UR-0062): 그래프 | 로드맵 | 🛠 기술 (토글 스위치 UI 제거 → CLI 가이드로 대체) ──
|
|
175
|
+
var _views={roadmap:document.getElementById('vroadmap'),tech:document.getElementById('vtech')};
|
|
176
176
|
document.querySelectorAll('.tab').forEach(function(t){t.onclick=function(){
|
|
177
177
|
document.querySelectorAll('.tab').forEach(function(x){x.classList.toggle('on',x===t);});
|
|
178
178
|
var v=t.getAttribute('data-v');
|
|
@@ -202,22 +202,37 @@ document.querySelectorAll('.tab').forEach(function(t){t.onclick=function(){
|
|
|
202
202
|
el.innerHTML=h;
|
|
203
203
|
})();
|
|
204
204
|
|
|
205
|
-
//
|
|
205
|
+
// 🛠 기술 뷰 (1.36.53, UR-0062) — DATA.tech: 개발 언어 + 연결 서비스 + 변경 이력(마이그레이션/언어 전환).
|
|
206
|
+
// 토글은 스위치 UI 대신 상태 표 + CLI 사용 가이드 (실제 적용은 항상 CLI — 정적 HTML 은 상태를 바꿀 수 없다).
|
|
206
207
|
(function(){
|
|
207
|
-
var
|
|
208
|
-
var
|
|
208
|
+
var el=_views.tech;var h='';
|
|
209
|
+
var tp=(DATA&&DATA.tech)||{};var cur=tp.current||{languages:[],services:[]};var hist=tp.history||[];
|
|
210
|
+
h+='<div class="sec">개발 언어 ('+cur.languages.length+')</div><div class="ms">';
|
|
211
|
+
if(!cur.languages.length)h+='<div class="note">감지된 언어 없음 — 소스/매니페스트가 생기면 자동 감지</div>';
|
|
212
|
+
cur.languages.forEach(function(l){h+='<div class="tk"><span class="st">'+esc(l.id)+'</span><span style="color:var(--mut)">'+esc(l.evidence||'')+'</span></div>';});
|
|
213
|
+
h+='</div><div class="sec">연결 서비스 ('+cur.services.length+')</div><div class="ms">';
|
|
214
|
+
if(!cur.services.length)h+='<div class="note">감지된 서비스 없음 — 의존성/.env 키/설정파일에서 자동 감지</div>';
|
|
215
|
+
cur.services.forEach(function(s){h+='<div class="tk"><span class="st">'+esc(s.id)+'</span><span style="color:var(--mut)">'+esc(s.evidence||'')+'</span></div>';});
|
|
216
|
+
h+='</div><div class="sec">변경 이력 ('+hist.length+') — 서비스 마이그레이션·언어 전환 자동 추적</div><div class="ms">';
|
|
217
|
+
if(!hist.length)h+='<div class="note">변경 이력 없음 — 언어/서비스가 바뀌면 leerness tech·handoff 가 자동 기록</div>';
|
|
218
|
+
hist.slice(-12).reverse().forEach(function(d){var ps=[];
|
|
219
|
+
(d.addedLanguages||[]).length&&ps.push('+언어 '+d.addedLanguages.join(','));
|
|
220
|
+
(d.removedLanguages||[]).length&&ps.push('-언어 '+d.removedLanguages.join(','));
|
|
221
|
+
(d.addedServices||[]).length&&ps.push('+서비스 '+d.addedServices.join(','));
|
|
222
|
+
(d.removedServices||[]).length&&ps.push('-서비스 '+d.removedServices.join(','));
|
|
223
|
+
h+='<div class="tk"><span class="st">'+esc(String(d.at||'').slice(0,16))+'</span><span>'+esc(ps.join(' · '))+'</span></div>';});
|
|
224
|
+
h+='</div>';
|
|
225
|
+
// 토글: 상태 표 + CLI 가이드 (1.36.53: 스위치 UI 제거 — 사용자 요청)
|
|
226
|
+
var tg=(DATA&&DATA.toggles)||{};var reg=(DATA&&DATA.toggleRegistry)||{};
|
|
227
|
+
h+='<div class="sec">기능 토글 상태 (변경은 CLI)</div><div class="ms">';
|
|
209
228
|
Object.keys(reg).forEach(function(id){var on=tg[id]!==false;var m=reg[id]||{};
|
|
210
|
-
h+='<div class="
|
|
211
|
-
|
|
212
|
-
|
|
229
|
+
h+='<div class="tk"><span class="st" style="color:'+(on?'#3fb950':'#8b949e')+'">'+(on?'ON ':'OFF')+'</span><span><b>'+esc(id)+'</b> — '+esc(m.desc||'')+'</span></div>';});
|
|
230
|
+
h+='</div><div class="sec">사용 가이드</div><div class="ms"><div class="note" style="line-height:1.8">'
|
|
231
|
+
+'· 토글 목록/상태: <b>leerness toggle list</b><br>'
|
|
232
|
+
+'· 켜기/끄기: <b>leerness toggle set <gate|lens|auto-graph|delegation-brief> on|off</b><br>'
|
|
233
|
+
+'· 기술 프로필 갱신: <b>leerness tech</b> (handoff 실행 시 자동 갱신·변경 이력 기록)<br>'
|
|
234
|
+
+'· 이 파일은 정적 HTML — 상태 변경은 터미널(또는 AI)에게 위 명령을 실행시키세요. AI 는 토글 상태를 자동 준수합니다.</div></div>';
|
|
213
235
|
el.innerHTML=h;
|
|
214
|
-
window._tg=tg;
|
|
215
|
-
window.tgFlip=function(id){var cur=window._tg[id]!==false;var nv=!cur;window._tg[id]=nv;
|
|
216
|
-
var row=document.getElementById('tg-'+id);row.querySelector('.sw2').classList.toggle('on',nv);
|
|
217
|
-
var c=document.getElementById('cmd-'+id);c.classList.add('show');
|
|
218
|
-
document.getElementById('cmdtext-'+id).textContent='leerness toggle set '+id+' '+(nv?'on':'off');};
|
|
219
|
-
window.tgCopy=function(id){var t=document.getElementById('cmdtext-'+id).textContent;
|
|
220
|
-
try{navigator.clipboard.writeText(t);}catch(e){var ta=document.createElement('textarea');ta.value=t;document.body.appendChild(ta);ta.select();document.execCommand('copy');ta.remove();}};
|
|
221
236
|
})();
|
|
222
237
|
</script></body></html>`;
|
|
223
238
|
|
|
@@ -297,8 +312,10 @@ function buildGraphData(root, deps = {}) {
|
|
|
297
312
|
rules: (rd.rules || []).map(r => ({ rule: _trunc(r.rule || r.text || '', 110), status: r.status })),
|
|
298
313
|
};
|
|
299
314
|
const toggles = (typeof deps._loadToggles === 'function' ? deps._loadToggles(root) : {}) || {};
|
|
315
|
+
// 1.36.53 (UR-0062): 기술 프로필 임베드 — 🛠 기술 탭 데이터
|
|
316
|
+
const tech = (typeof deps._loadTechProfile === 'function' ? (deps._loadTechProfile(root) || null) : null) || { current: { languages: [], services: [] }, history: [] };
|
|
300
317
|
const toggleRegistry = deps._toggleRegistry || {};
|
|
301
|
-
return { project: rd.project || path.basename(root), version: rd.version || '', root, counts, nodes, edges, roadmap, toggles, toggleRegistry };
|
|
318
|
+
return { project: rd.project || path.basename(root), version: rd.version || '', root, counts, nodes, edges, roadmap, toggles, toggleRegistry, tech };
|
|
302
319
|
}
|
|
303
320
|
|
|
304
321
|
// `leerness graph --html [path] [--out file] [--json]` 핸들러.
|
package/lib/io.js
CHANGED
|
@@ -14,9 +14,11 @@ function log(s = '') {
|
|
|
14
14
|
_unlatchIfNewCmd();
|
|
15
15
|
if (_quiet) return;
|
|
16
16
|
if (_failLatched || _jsonErrEmitted) {
|
|
17
|
-
// json-오류 래치 중: 명령 자신의 구조화 출력(JSON 페이로드)만 통과, 사람용 잔여 줄은 stderr
|
|
17
|
+
// json-오류 래치 중: 명령 자신의 구조화 출력(JSON 페이로드)만 통과, 사람용 잔여 줄은 stderr 강등.
|
|
18
|
+
// 1.36.54 (codex 7차 #7a): JSON 통과는 지연-fail(폴백 대기) 경로 전용 — failJson 이 이미 오류 JSON 을
|
|
19
|
+
// 방출했다면(_jsonErrEmitted) 추가 JSON 은 이중 문서가 되므로 stderr 강등.
|
|
18
20
|
const t = typeof s === 'string' ? s.trim() : '';
|
|
19
|
-
if (t && (t[0] === '{' || t[0] === '[')) { try { JSON.parse(t); console.log(s); _stdoutWrote = true; return; } catch {} }
|
|
21
|
+
if (!_jsonErrEmitted && t && (t[0] === '{' || t[0] === '[')) { try { JSON.parse(t); console.log(s); _stdoutWrote = true; return; } catch {} }
|
|
20
22
|
console.error(s); return;
|
|
21
23
|
}
|
|
22
24
|
console.log(s); _stdoutWrote = true;
|
|
@@ -56,7 +58,8 @@ function fail(s) {
|
|
|
56
58
|
console.error('✗ ' + s);
|
|
57
59
|
if (!_exitHooked) {
|
|
58
60
|
_exitHooked = true;
|
|
59
|
-
|
|
61
|
+
// 1.36.54 (#7b): 최종 exit 이 0(명령이 회복)이면 폴백 오류 JSON 을 내지 않는다 — 성공 exit 에 ok:false 모순 방지
|
|
62
|
+
process.on('exit', (code) => { if (code !== 0 && _failLatched && !_stdoutWrote && _firstFail != null) console.log(JSON.stringify({ ok: false, error: _firstFail, code: 'error' }, null, 2)); });
|
|
60
63
|
}
|
|
61
64
|
process.exitCode = 1; return;
|
|
62
65
|
}
|
|
@@ -102,7 +105,8 @@ function _rescueCorruptJson(p, s) {
|
|
|
102
105
|
let old;
|
|
103
106
|
try { old = read(p); } catch { return; }
|
|
104
107
|
try { JSON.parse(old); return; } catch {} // 기존이 정상이면 통상 덮어쓰기
|
|
105
|
-
|
|
108
|
+
// 1.36.54 (#8): 같은 ms 내 연속 대피의 백업명 충돌 방지 — pid+시퀀스 접미
|
|
109
|
+
const bak = `${p}.corrupt-${Date.now()}-${process.pid}-${++_writeSeq}`;
|
|
106
110
|
// 알림은 stderr — --json 명령의 stdout 단일-JSON 계약을 오염하지 않는다 (codex 스윕 B #1 실측)
|
|
107
111
|
try { fs.renameSync(p, bak); console.error(`⚠ 손상 JSON 감지 — 원본 대피 후 저장: ${path.basename(bak)} (수동 복구용)`); } catch {}
|
|
108
112
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// lib/tech-profile.js — 프로젝트 기술 프로필 (1.36.53, 사용자 요청 UR-0062).
|
|
2
|
+
// 무엇으로 개발 중인가(언어) + 어디에 연결돼 있는가(서비스)를 파일 신호로 자동 감지하고,
|
|
3
|
+
// 변경 이력(언어 전환/서비스 마이그레이션)을 .harness/tech-profile.json 에 누적한다.
|
|
4
|
+
// 신호는 전부 로컬 파일(매니페스트/의존성/.env 키 이름/설정파일) — 네트워크 0 · .env 값은 절대 읽지 않고 키 이름만.
|
|
5
|
+
'use strict';
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const { absRoot, exists, read, writeUtf8, log, ok, warn, failJson, now } = require('./io');
|
|
9
|
+
|
|
10
|
+
// 언어 신호: 매니페스트 파일 존재 (확장자 통계는 보조 — 얕은 스캔만)
|
|
11
|
+
const LANG_SIGNALS = [
|
|
12
|
+
{ id: 'typescript', files: ['tsconfig.json'] },
|
|
13
|
+
{ id: 'javascript', files: ['package.json'] },
|
|
14
|
+
{ id: 'python', files: ['pyproject.toml', 'requirements.txt', 'setup.py', 'Pipfile'] },
|
|
15
|
+
{ id: 'go', files: ['go.mod'] },
|
|
16
|
+
{ id: 'rust', files: ['Cargo.toml'] },
|
|
17
|
+
{ id: 'java', files: ['pom.xml', 'build.gradle', 'build.gradle.kts'] },
|
|
18
|
+
{ id: 'csharp', glob: /\.(csproj|sln)$/i },
|
|
19
|
+
{ id: 'php', files: ['composer.json'] },
|
|
20
|
+
{ id: 'ruby', files: ['Gemfile'] },
|
|
21
|
+
];
|
|
22
|
+
const EXT_LANG = { '.ts': 'typescript', '.tsx': 'typescript', '.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.jsx': 'javascript', '.py': 'python', '.go': 'go', '.rs': 'rust', '.java': 'java', '.cs': 'csharp', '.php': 'php', '.rb': 'ruby' };
|
|
23
|
+
|
|
24
|
+
// 서비스 신호: deps 이름 / .env 키 접두 / 설정 파일
|
|
25
|
+
const SERVICE_SIGNALS = [
|
|
26
|
+
{ id: 'supabase', deps: ['@supabase/supabase-js', 'supabase'], env: ['SUPABASE_'] },
|
|
27
|
+
{ id: 'firebase', deps: ['firebase', 'firebase-admin'], env: ['FIREBASE_'], files: ['firebase.json'] },
|
|
28
|
+
{ id: 'stripe', deps: ['stripe'], env: ['STRIPE_'] },
|
|
29
|
+
{ id: 'openai', deps: ['openai'], env: ['OPENAI_'] },
|
|
30
|
+
{ id: 'anthropic', deps: ['@anthropic-ai/sdk', 'anthropic'], env: ['ANTHROPIC_'] },
|
|
31
|
+
{ id: 'aws', deps: ['aws-sdk', '@aws-sdk/client-s3', '@aws-sdk/core'], env: ['AWS_'], depPrefix: '@aws-sdk/' },
|
|
32
|
+
{ id: 'gcp', deps: ['@google-cloud/storage'], env: ['GOOGLE_CLOUD_', 'GCP_'], depPrefix: '@google-cloud/' },
|
|
33
|
+
{ id: 'azure', deps: [], env: ['AZURE_'], depPrefix: '@azure/' },
|
|
34
|
+
{ id: 'cloudflare', deps: ['wrangler'], env: ['CLOUDFLARE_', 'CF_ACCOUNT'], files: ['wrangler.toml', 'wrangler.jsonc'] },
|
|
35
|
+
{ id: 'vercel', deps: ['vercel'], env: ['VERCEL_'], files: ['vercel.json'] },
|
|
36
|
+
{ id: 'netlify', deps: ['netlify-cli'], env: ['NETLIFY_'], files: ['netlify.toml'] },
|
|
37
|
+
{ id: 'docker', deps: [], env: [], files: ['docker-compose.yml', 'docker-compose.yaml', 'Dockerfile'] },
|
|
38
|
+
// 1.36.54 (codex 7차 #4): DATABASE_URL 은 DB 중립 키(mysql:// 일 수도) — postgres 양성 신호에서 제외 (.env 값은 안 읽는 원칙 유지)
|
|
39
|
+
{ id: 'postgres', deps: ['pg', 'postgres'], env: ['POSTGRES_', 'PG_'] },
|
|
40
|
+
{ id: 'mysql', deps: ['mysql', 'mysql2'], env: ['MYSQL_'] },
|
|
41
|
+
{ id: 'mongodb', deps: ['mongodb', 'mongoose'], env: ['MONGO_', 'MONGODB_'] },
|
|
42
|
+
{ id: 'redis', deps: ['redis', 'ioredis'], env: ['REDIS_'] },
|
|
43
|
+
{ id: 'prisma', deps: ['prisma', '@prisma/client'], files: ['prisma/schema.prisma'] },
|
|
44
|
+
{ id: 'telegram', deps: ['node-telegram-bot-api', 'telegraf'], env: ['TELEGRAM_'] },
|
|
45
|
+
{ id: 'discord', deps: ['discord.js'], env: ['DISCORD_'] },
|
|
46
|
+
{ id: 'github-api', deps: ['@octokit/rest', 'octokit'], env: ['GH_TOKEN', 'GITHUB_TOKEN'] },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
function _envKeys(root) {
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const f of ['.env', '.env.local', '.env.production']) {
|
|
52
|
+
const p = path.join(root, f);
|
|
53
|
+
if (!exists(p)) continue;
|
|
54
|
+
try { for (const line of read(p).split('\n')) { const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/); if (m) out.push(m[1]); } } catch {}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _shallowExts(root) {
|
|
60
|
+
const counts = {};
|
|
61
|
+
const scan = (dir, depth) => {
|
|
62
|
+
let entries = [];
|
|
63
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
64
|
+
for (const e of entries.slice(0, 400)) {
|
|
65
|
+
if (e.name.startsWith('.') || e.name === 'node_modules' || e.name === 'dist' || e.name === 'build') continue;
|
|
66
|
+
const p = path.join(dir, e.name);
|
|
67
|
+
if (e.isDirectory()) { if (depth > 0) scan(p, depth - 1); continue; }
|
|
68
|
+
const ext = path.extname(e.name).toLowerCase();
|
|
69
|
+
if (EXT_LANG[ext]) counts[EXT_LANG[ext]] = (counts[EXT_LANG[ext]] || 0) + 1;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
scan(absRoot(root), 2);
|
|
73
|
+
return counts;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 순수-ish 감지 코어 — { languages:[{id,evidence}], services:[{id,evidence}] }
|
|
77
|
+
function detectTechProfile(root) {
|
|
78
|
+
root = absRoot(root);
|
|
79
|
+
const languages = []; const seenL = new Set();
|
|
80
|
+
const addL = (id, evidence) => { if (!seenL.has(id)) { seenL.add(id); languages.push({ id, evidence }); } };
|
|
81
|
+
for (const s of LANG_SIGNALS) {
|
|
82
|
+
if (s.files) for (const f of s.files) if (exists(path.join(root, f))) { addL(s.id, f); break; }
|
|
83
|
+
if (s.glob) { try { if (fs.readdirSync(root).some(n => s.glob.test(n))) addL(s.id, 'project file'); } catch {} }
|
|
84
|
+
}
|
|
85
|
+
const extCounts = _shallowExts(root);
|
|
86
|
+
for (const [lang, n] of Object.entries(extCounts)) if (n >= 3) addL(lang, `${n} source files`);
|
|
87
|
+
// deps 수집 — 1.36.54 (#5): 모노레포 대응, 루트 + 얕은 하위(2단계) 매니페스트 병합 (vendor 류 제외)
|
|
88
|
+
let deps = {};
|
|
89
|
+
let reqTxt = '';
|
|
90
|
+
const _mergeManifests = (dir, depth) => {
|
|
91
|
+
try {
|
|
92
|
+
const pj = JSON.parse(read(path.join(dir, 'package.json')));
|
|
93
|
+
deps = Object.assign({}, deps, pj.dependencies, pj.devDependencies);
|
|
94
|
+
if (dir !== root) addL('javascript', rel_(dir) + '/package.json');
|
|
95
|
+
} catch {}
|
|
96
|
+
try { if (exists(path.join(dir, 'requirements.txt'))) reqTxt += '\n' + read(path.join(dir, 'requirements.txt')); } catch {}
|
|
97
|
+
if (depth <= 0) return;
|
|
98
|
+
let entries = [];
|
|
99
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
100
|
+
for (const e of entries.slice(0, 200)) {
|
|
101
|
+
if (!e.isDirectory() || e.name.startsWith('.') || ['node_modules', 'dist', 'build', 'vendor', 'coverage'].includes(e.name)) continue;
|
|
102
|
+
_mergeManifests(path.join(dir, e.name), depth - 1);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const rel_ = (d) => path.relative(root, d).replace(/\\/g, '/');
|
|
106
|
+
_mergeManifests(root, 2);
|
|
107
|
+
const envKeys = _envKeys(root);
|
|
108
|
+
const services = []; const seenS = new Set();
|
|
109
|
+
const addS = (id, evidence) => { if (!seenS.has(id)) { seenS.add(id); services.push({ id, evidence }); } };
|
|
110
|
+
for (const s of SERVICE_SIGNALS) {
|
|
111
|
+
// 1.36.54 (#12): requirements 는 배포명 전체 일치만 — `\b` 가 점을 경계로 봐 openai.fake 가 openai 에 오탐했다.
|
|
112
|
+
// 이름 뒤는 버전 연산자/extras/공백/행끝만 허용 (PEP 스타일: 대소문자·-/_ 동치).
|
|
113
|
+
for (const d of (s.deps || [])) {
|
|
114
|
+
const reqHit = new RegExp(`^\\s*${d.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/[-_]/g, '[-_]')}\\s*(?:[=<>!~;\\[]|$)`, 'mi').test(reqTxt);
|
|
115
|
+
if (deps[d] || reqHit) { addS(s.id, `dep: ${d}`); break; }
|
|
116
|
+
}
|
|
117
|
+
if (!seenS.has(s.id) && s.depPrefix) { const hit = Object.keys(deps).find(k => k.startsWith(s.depPrefix)); if (hit) addS(s.id, `dep: ${hit}`); }
|
|
118
|
+
if (!seenS.has(s.id)) for (const pre of (s.env || [])) { const hit = envKeys.find(k => k === pre || k.startsWith(pre)); if (hit) { addS(s.id, `env: ${hit}`); break; } }
|
|
119
|
+
if (!seenS.has(s.id)) for (const f of (s.files || [])) if (exists(path.join(root, f))) { addS(s.id, `file: ${f}`); break; }
|
|
120
|
+
}
|
|
121
|
+
return { languages, services };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function _techPath(root) { return path.join(absRoot(root), '.harness', 'tech-profile.json'); }
|
|
125
|
+
|
|
126
|
+
function loadTechProfile(root) {
|
|
127
|
+
const f = _techPath(root);
|
|
128
|
+
if (!exists(f)) return null;
|
|
129
|
+
try { return JSON.parse(read(f)); } catch { return null; }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 감지 → 저장 + 변경 이력(diff) 누적. 반환: { profile, changed, diff }
|
|
133
|
+
function refreshTechProfile(root, opts = {}) {
|
|
134
|
+
root = absRoot(root);
|
|
135
|
+
if (!exists(path.join(root, '.harness'))) return null;
|
|
136
|
+
const cur = detectTechProfile(root);
|
|
137
|
+
let prev = loadTechProfile(root);
|
|
138
|
+
// 1.36.54 (#6): 파싱은 되지만 스키마가 무효(current/languages 부재)인 프로필은 부재 취급 — 종전엔 {} 가
|
|
139
|
+
// "이전 프로필"로 오인돼 changed=false 로 고착, 영원히 재기록/이력 축적이 안 됐다. (스키마-무효는 재생성 대상)
|
|
140
|
+
if (prev && (!prev.current || !Array.isArray(prev.current.languages) || !Array.isArray(prev.current.services))) prev = null;
|
|
141
|
+
const ids = a => (a || []).map(x => x.id);
|
|
142
|
+
const dif = (a, b) => a.filter(x => !b.includes(x));
|
|
143
|
+
let history = (prev && Array.isArray(prev.history)) ? prev.history : [];
|
|
144
|
+
let changed = false; let diff = null;
|
|
145
|
+
if (prev && prev.current) {
|
|
146
|
+
const aL = dif(ids(cur.languages), ids(prev.current.languages)), rL = dif(ids(prev.current.languages), ids(cur.languages));
|
|
147
|
+
const aS = dif(ids(cur.services), ids(prev.current.services)), rS = dif(ids(prev.current.services), ids(cur.services));
|
|
148
|
+
if (aL.length || rL.length || aS.length || rS.length) {
|
|
149
|
+
changed = true;
|
|
150
|
+
diff = { at: now(), addedLanguages: aL, removedLanguages: rL, addedServices: aS, removedServices: rS };
|
|
151
|
+
history = [...history, diff].slice(-50);
|
|
152
|
+
}
|
|
153
|
+
} else if (!prev) { changed = true; }
|
|
154
|
+
const profile = { current: cur, updatedAt: now(), history };
|
|
155
|
+
if (changed || !prev) {
|
|
156
|
+
// 손상 스토어는 writeUtf8 의 .corrupt-* 대피가 원본을 보존한다 (1.36.50)
|
|
157
|
+
writeUtf8(_techPath(root), JSON.stringify(profile, null, 2) + '\n');
|
|
158
|
+
}
|
|
159
|
+
return { profile, changed, diff };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// `leerness tech [refresh] [--json]`
|
|
163
|
+
function techCmd(root, sub, deps = {}) {
|
|
164
|
+
const { has } = deps;
|
|
165
|
+
root = absRoot(root);
|
|
166
|
+
const json = !!(has && has('--json'));
|
|
167
|
+
if (!exists(path.join(root, '.harness'))) { failJson(json, 'harness_missing', `leerness 미설치: ${root} — 먼저 leerness init`); return; }
|
|
168
|
+
const r = refreshTechProfile(root); // show 도 항상 최신화(감지는 저비용·로컬)
|
|
169
|
+
const p = r.profile;
|
|
170
|
+
if (json) { log(JSON.stringify({ ok: true, ...p, changedNow: r.changed, diffNow: r.diff }, null, 2)); return; }
|
|
171
|
+
log(`# leerness tech — 프로젝트 기술 프로필 (${p.updatedAt.slice(0, 10)})`);
|
|
172
|
+
log(` 언어: ${p.current.languages.length ? p.current.languages.map(l => `${l.id} (${l.evidence})`).join(' · ') : '(감지 없음)'}`);
|
|
173
|
+
log(` 서비스: ${p.current.services.length ? p.current.services.map(s => `${s.id} (${s.evidence})`).join(' · ') : '(감지 없음)'}`);
|
|
174
|
+
if (p.history.length) {
|
|
175
|
+
log(`\n 변경 이력 (${p.history.length}건 — 마이그레이션/언어 전환 추적):`);
|
|
176
|
+
for (const h of p.history.slice(-8)) {
|
|
177
|
+
const parts = [];
|
|
178
|
+
if (h.addedLanguages.length) parts.push(`+언어 ${h.addedLanguages.join(',')}`);
|
|
179
|
+
if (h.removedLanguages.length) parts.push(`-언어 ${h.removedLanguages.join(',')}`);
|
|
180
|
+
if (h.addedServices.length) parts.push(`+서비스 ${h.addedServices.join(',')}`);
|
|
181
|
+
if (h.removedServices.length) parts.push(`-서비스 ${h.removedServices.join(',')}`);
|
|
182
|
+
log(` · ${h.at.slice(0, 16)} ${parts.join(' · ')}`);
|
|
183
|
+
}
|
|
184
|
+
} else log(` (변경 이력 없음 — 서비스/언어가 바뀌면 자동 기록)`);
|
|
185
|
+
log(`\n ⓘ 온톨로지 그래프(leerness.html) 🛠 기술 탭에 표시 · 갱신: leerness tech (handoff 가 자동 갱신)`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = { LANG_SIGNALS, SERVICE_SIGNALS, detectTechProfile, loadTechProfile, refreshTechProfile, techCmd, _techPath };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.54",
|
|
4
4
|
"description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
package/scripts/e2e.js
CHANGED
|
@@ -7028,5 +7028,33 @@ total++;
|
|
|
7028
7028
|
if (!ok) failed++;
|
|
7029
7029
|
}
|
|
7030
7030
|
|
|
7031
|
+
// 1.36.53 (사용자 요청 UR-0062): tech 프로필 — 언어·서비스 감지/이력 + 그래프 🛠 탭(스위치 제거·가이드) CLI 라운드트립
|
|
7032
|
+
total++;
|
|
7033
|
+
{
|
|
7034
|
+
let ok = false;
|
|
7035
|
+
try {
|
|
7036
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-tech-'));
|
|
7037
|
+
fs.writeFileSync(path.join(d, 'package.json'), JSON.stringify({ name: 'demo', dependencies: { stripe: '^1' } }));
|
|
7038
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { encoding: 'utf8', timeout: 30000 });
|
|
7039
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { encoding: 'utf8', timeout: 20000 });
|
|
7040
|
+
const t1 = JSON.parse(R(['tech', '--json']).stdout);
|
|
7041
|
+
const detOk = t1.current.languages.some(l => l.id === 'javascript') && t1.current.services.some(s => s.id === 'stripe');
|
|
7042
|
+
// 마이그레이션: stripe → supabase
|
|
7043
|
+
fs.writeFileSync(path.join(d, 'package.json'), JSON.stringify({ name: 'demo', dependencies: { '@supabase/supabase-js': '^2' } }));
|
|
7044
|
+
const t2 = JSON.parse(R(['tech', '--json']).stdout);
|
|
7045
|
+
const migOk = t2.changedNow === true && t2.diffNow.addedServices.includes('supabase') && t2.diffNow.removedServices.includes('stripe') && t2.history.length === 1;
|
|
7046
|
+
// handoff 자동 갱신(프로필) 후 그래프 재생성 → 🛠 탭 (스위치 UI 제거 + CLI 가이드 + tech 데이터)
|
|
7047
|
+
cp.spawnSync(process.execPath, [CLI, 'handoff', d], { encoding: 'utf8', timeout: 25000 });
|
|
7048
|
+
cp.spawnSync(process.execPath, [CLI, 'graph', d, '--html'], { encoding: 'utf8', timeout: 25000 });
|
|
7049
|
+
const html = fs.readFileSync(path.join(d, 'leerness.html'), 'utf8');
|
|
7050
|
+
const tabOk = html.includes('data-v="tech"') && !html.includes('data-v="toggles"') && !html.includes('tgFlip') && html.includes('leerness toggle set') && html.includes('supabase');
|
|
7051
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
7052
|
+
ok = detOk && migOk && tabOk;
|
|
7053
|
+
if (!ok) console.log(` [tech 디버그] det=${detOk} mig=${migOk} tab=${tabOk}`);
|
|
7054
|
+
} catch (e) {}
|
|
7055
|
+
console.log(ok ? '✓ B(1.36.53) UR-0062: tech 언어·서비스 감지 + 마이그레이션 이력 diff + handoff 자동갱신 + 그래프 🛠 탭(스위치 제거·CLI 가이드)' : '✗ tech 프로필 실패');
|
|
7056
|
+
if (!ok) failed++;
|
|
7057
|
+
}
|
|
7058
|
+
|
|
7031
7059
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
7032
7060
|
if (failed > 0) process.exit(1);
|