leerness 1.36.51 → 1.36.53
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 +20 -0
- package/README.md +4 -4
- package/bin/leerness.js +88 -6
- package/lib/graph.js +35 -18
- package/lib/tech-profile.js +164 -0
- package/package.json +1 -1
- package/scripts/e2e.js +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.53 — 2026-07-21 — 기술 프로필: 언어·서비스 자동 감지 + 마이그레이션 이력 + 그래프 🛠 탭 (사용자 요청 UR-0062)
|
|
4
|
+
|
|
5
|
+
사용자 요청: "어떤 언어로 개발 중인지·어떤 서비스에 연결돼 있는지(마이그레이션/언어 전환 반영) 온톨로지 그래프에 표시, 그래프의 토글 스위치는 제거하고 CLI 명령/사용가이드로 대체".
|
|
6
|
+
|
|
7
|
+
- **`leerness tech [--json]`** (lib/tech-profile.js 신설): 언어 9종(매니페스트+얕은 확장자 스캔) · 서비스 21종(deps 이름/.env 키 이름/설정파일 — 값은 절대 안 읽음, 네트워크 0) 감지. `.harness/tech-profile.json` 에 현재 상태 + **변경 이력 diff**(±언어/±서비스, cap 50) 누적 — 서비스 마이그레이션·언어 전환이 시점과 함께 남는다. 실측: stripe→firebase 교체 시 `+서비스 firebase · -서비스 stripe` 자동 기록.
|
|
8
|
+
- **handoff 자동 갱신**: 세션 시작마다 프로필 재감지 — AI 가 항상 최신 기술 스택을 컨텍스트로 갖는다.
|
|
9
|
+
- **온톨로지 그래프 🛠 기술 탭**: 언어/서비스(근거 포함)/변경 이력 표시. **⚙ 토글 스위치 UI 제거**(사용자 요청) → 토글 상태 표(ON/OFF 텍스트) + `leerness toggle list|set`·`leerness tech` 사용 가이드로 대체.
|
|
10
|
+
- 검증: selftest 327(+1 행위: 감지→이력 diff→탭 임베드·스위치 부재), e2e +1 라운드트립.
|
|
11
|
+
|
|
12
|
+
## 1.36.52 — 2026-07-21 — 외부 GPT 감사(1.36.47) P0 채택 (UR-0063)
|
|
13
|
+
|
|
14
|
+
웹 구독 GPT 의 독립 감사(격리 설치·오류 주입·패치 첨부, 10건) 중 재현 확정 2건 + 채택 판단 2건을 반영. 전 건 현행 소스에서 독립 재현/검증 후 수정.
|
|
15
|
+
|
|
16
|
+
- **F-01 (재현확정) — 재-init 언어 뒤집힘**: en 프로젝트 init 2회차에 1회차가 생성한 한국어 관리 블록을 콘텐츠 감지가 오인해 ko 로 전환되던 멱등성 버그. 저장된 manifest/LANGUAGE 언어 최우선, 콘텐츠 감지는 신규만 (감사 첨부 패치 검증 후 채택). 실측: en→en 유지, 명시 --language ko 는 전환.
|
|
17
|
+
- **F-07 (재현확정) — memory restore 단수형 실패**: 생성 문서 예시(decision)와 CLI(decisions) 불일치 — 단수형 별칭 수용.
|
|
18
|
+
- **F-02 — 비율형 증거 우회**: "5/5 passed"(비율형 완전-통과)가 명시 카운트("5 tests passed")와 달리 정적 선언-수 대조를 우회하던 것 — 동일 정규화(1.35.9 원칙 유지: N/N 만 승격). verify-claim --json 에 신뢰 경계 명시: `semanticVerified:false` + `evidenceLevel: static|executed`.
|
|
19
|
+
- **F-08 — .env 권한**: Unix 에서 생성/병합 시 0600 (gitignore 는 커밋만 막고 로컬 노출은 못 막음).
|
|
20
|
+
- 감사 나머지: F-03(enforce=사용 게이트, status 한계 명시 기존) / F-06(전용 스캐너 병행 — 1.35.8 문서 기존) 은 방향 일치 확인, F-04(managed 무결성 복구)/F-05(en 완전 번역)/F-09(MCP 프로필)/E2E 아티팩트 이식성은 백로그 편입.
|
|
21
|
+
- 검증: selftest 326(+1 통합 행위), e2e.
|
|
22
|
+
|
|
3
23
|
## 1.36.51 — 2026-07-21 — 모호성 질문 + 미리보기 승인 워크플로 (사용자 요청 UR-0061)
|
|
4
24
|
|
|
5
25
|
사용자 요청: "자연어에 판단 모호한 부분이 있으면 사용자에게 질문하고, 신규 기능은 코드 작성 전 디자인/기능 미리보기를 제시해 승인/수정을 받도록".
|
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.53 하네스를 사용합니다. 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.53는 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.51는 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.53 릴리스 태그 이력** (수백 라운드) · _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.53: 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.53';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -240,6 +240,21 @@ function detectProjectName(root) { try { const pkg = JSON.parse(read(path.join(r
|
|
|
240
240
|
function detectLanguageValue(root, value = 'auto') {
|
|
241
241
|
const v = String(value || 'auto').toLowerCase();
|
|
242
242
|
if (v === 'ko' || v === 'en') return v;
|
|
243
|
+
// 1.36.52 (외부 GPT 감사 F-01, 재현확정): 이미 초기화된 프로젝트는 저장된 명시 언어를 최우선 —
|
|
244
|
+
// 1회차 init 이 생성한 한국어 관리 블록을 2회차 콘텐츠 감지가 오인해 en→ko 로 뒤집던 멱등성 버그.
|
|
245
|
+
// 콘텐츠 자동 감지는 신규 프로젝트에만. (감사 첨부 패치 검증 후 채택)
|
|
246
|
+
try {
|
|
247
|
+
const mf = path.join(root, '.harness', 'manifest.json');
|
|
248
|
+
if (exists(mf)) {
|
|
249
|
+
const saved = String((JSON.parse(read(mf)) || {}).language || '').toLowerCase();
|
|
250
|
+
if (saved === 'ko' || saved === 'en') return saved;
|
|
251
|
+
}
|
|
252
|
+
const lf = path.join(root, '.harness', 'LANGUAGE');
|
|
253
|
+
if (exists(lf)) {
|
|
254
|
+
const saved = String(read(lf) || '').trim().toLowerCase();
|
|
255
|
+
if (saved === 'ko' || saved === 'en') return saved;
|
|
256
|
+
}
|
|
257
|
+
} catch {}
|
|
243
258
|
// ① 프로젝트 콘텐츠 한글 우선 — 콘텐츠가 있으면 그 의도를 존중 (기존 동작 보존).
|
|
244
259
|
const candidates = ['README.md', 'docs/guideline.md', '.harness/project-brief.md', '.harness/plan.md'];
|
|
245
260
|
let text = '';
|
|
@@ -744,6 +759,8 @@ function mergeLinesFile(p, lines) {
|
|
|
744
759
|
function mergeEnvFile(p, lines) {
|
|
745
760
|
const current = exists(p) ? read(p) : '';
|
|
746
761
|
writeUtf8(p, _mergeEnvLines(current, lines));
|
|
762
|
+
// 1.36.52 (외부 GPT 감사 F-08): Unix 에서 .env 는 소유자 전용(0600) — gitignore 는 커밋만 막지 로컬 노출은 못 막는다.
|
|
763
|
+
if (process.platform !== 'win32') { try { fs.chmodSync(p, 0o600); } catch {} }
|
|
747
764
|
}
|
|
748
765
|
|
|
749
766
|
function writeMigrationReport(root, backup, actions, opts = {}) {
|
|
@@ -3126,7 +3143,7 @@ function _selfTestCases() {
|
|
|
3126
3143
|
const o = '<scr' + 'ipt>', c = '</scr' + 'ipt>';
|
|
3127
3144
|
const js = html.slice(html.indexOf(o) + o.length, html.lastIndexOf(c));
|
|
3128
3145
|
let synOk = false; try { new Function(js); synOk = true; } catch {}
|
|
3129
|
-
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 가이드 포함)
|
|
3130
3147
|
} catch { behavOk = false; } finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
3131
3148
|
// 배선 소스가드: 자동생성이 leerness.html(온톨로지)로 전환 + gate/lens 토글 준수 + toggle dispatch
|
|
3132
3149
|
const s = read(__filename);
|
|
@@ -5019,6 +5036,53 @@ function _selfTestCases() {
|
|
|
5019
5036
|
const preserved = fs.readFileSync(c._previewsPath(tmp), 'utf8') === '[{"id":"P-0001",';
|
|
5020
5037
|
return added && pending1 && revised && approved && preserved;
|
|
5021
5038
|
} finally { process.stdout.write = _w; console.error = _ce; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
5039
|
+
} },
|
|
5040
|
+
{ name: '외부 GPT 감사 P0 (1.36.52, UR-0063): 재init 언어보존(F-01) + restore 별칭(F-07) + ratio 증거 정규화/evidenceLevel(F-02) + .env 0600(F-08) — 행위+소스가드', run: () => {
|
|
5041
|
+
// F-01: manifest 언어가 콘텐츠 감지보다 우선 (행위)
|
|
5042
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_lang_'));
|
|
5043
|
+
let langOk = false;
|
|
5044
|
+
try {
|
|
5045
|
+
fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
|
|
5046
|
+
fs.writeFileSync(path.join(tmp, '.harness', 'manifest.json'), '{"language":"en"}');
|
|
5047
|
+
fs.writeFileSync(path.join(tmp, 'README.md'), '# 한국어 콘텐츠 가득한 리드미'); // 종전엔 이것이 ko 로 뒤집었다
|
|
5048
|
+
langOk = detectLanguageValue(tmp, 'auto') === 'en' && detectLanguageValue(tmp, 'ko') === 'ko';
|
|
5049
|
+
} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
5050
|
+
const s = read(__filename);
|
|
5051
|
+
// F-07: 단수형 별칭 매핑 존재
|
|
5052
|
+
const aliasOk = s.includes("({ decision: 'decisions', lesson: 'lessons', plans: 'plan' })[surface]");
|
|
5053
|
+
// F-02: 비율형 완전-통과 → declaredTestCount 승격 + 신뢰 경계 필드
|
|
5054
|
+
const ratioOk = s.includes('declaredPass.num === declaredPass.denom) declaredTestCount = declaredPass.denom')
|
|
5055
|
+
&& s.includes('semanticVerified: false') && s.includes("? 'executed' : 'static'");
|
|
5056
|
+
// F-08: Unix .env 0600
|
|
5057
|
+
const envOk = /mergeEnvFile[\s\S]{0,600}chmodSync\(p, 0o600\)/.test(s);
|
|
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 {} }
|
|
5022
5086
|
} }
|
|
5023
5087
|
];
|
|
5024
5088
|
}
|
|
@@ -6158,6 +6222,7 @@ function commandsCmd(root) {
|
|
|
6158
6222
|
{ cmd: 'slash-commands [agent] [--json --record --detect --refresh]', desc: 'CLI 에이전트 슬래시 명령 레지스트리 + --help probe 자동 갱신 (1.9.265~267, UR-0021)' },
|
|
6159
6223
|
{ cmd: 'review-request "<request>"', desc: '사용자 요청 사전 검토 (1.9.176)' },
|
|
6160
6224
|
{ cmd: 'clarify "<사용자 요청>" [--json]', desc: '요청 모호성 신호 감지 → 사용자에게 물을 질문 생성 (추측 구현 방지) — 1.36.51 UR-0061' },
|
|
6225
|
+
{ cmd: 'tech [--json]', desc: '기술 프로필 — 개발 언어·연결 서비스 자동 감지 + 마이그레이션/언어전환 이력, 그래프 🛠 탭 표시 — 1.36.53 UR-0062' },
|
|
6161
6226
|
{ cmd: 'preview add|list|show|approve|revise', desc: '신규 기능 미리보기 승인 워크플로 — approve 전 코드 작성 금지 계약 — 1.36.51 UR-0061' },
|
|
6162
6227
|
{ cmd: 'review <file> --persona <ids>', desc: '페르소나 리뷰 (1.9.29)' },
|
|
6163
6228
|
{ cmd: 'brainstorm "<topic>" [--include-code]', desc: '워크스페이스 회수 + 코드 grep' }
|
|
@@ -8549,6 +8614,7 @@ function _saveTeams(root, teams) {
|
|
|
8549
8614
|
const _team = require('../lib/team');
|
|
8550
8615
|
const _tgl = require('../lib/toggles'); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동 — gate/lens/auto-graph/delegation-brief)
|
|
8551
8616
|
const _clar = require('../lib/clarify'); // 1.36.51 (UR-0061): 모호성 질문 + 미리보기 승인 워크플로
|
|
8617
|
+
const _tech = require('../lib/tech-profile'); // 1.36.53 (UR-0062): 기술 프로필 (언어·서비스 감지 + 변경 이력)
|
|
8552
8618
|
function teamCmd(root, sub, id, opts = {}) { return _team.teamCmd(root, sub, id, opts, { VERSION, _loadTeams, _saveTeams, _detectShellCtx, arg, has, _withLock }); } // 1.36.31: add 경합 락
|
|
8553
8619
|
|
|
8554
8620
|
// 1.9.112: 전용 lessons.md (Memory Write Surface 5번째)
|
|
@@ -9129,6 +9195,8 @@ function memoryArchiveListCmd(root, opts = {}) {
|
|
|
9129
9195
|
// 매칭 archive 블록을 active 파일 끝에 추가 + archive 에서 제거
|
|
9130
9196
|
function memoryRestoreCmd(root, surface, target) {
|
|
9131
9197
|
root = absRoot(root);
|
|
9198
|
+
// 1.36.52 (외부 GPT 감사 F-07): 단수형 별칭 수용 — 생성 문서 예시(decision)와 CLI(decisions) 불일치로 실패하던 것.
|
|
9199
|
+
surface = ({ decision: 'decisions', lesson: 'lessons', plans: 'plan' })[surface] || surface;
|
|
9132
9200
|
if (!surface || !['decisions', 'lessons', 'plan'].includes(surface)) {
|
|
9133
9201
|
return fail('memory restore <decisions|lessons|plan> <target> 필요 (target: date YYYY-MM-DD 또는 substring)');
|
|
9134
9202
|
}
|
|
@@ -12180,6 +12248,11 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
12180
12248
|
const m5 = evidence.match(/(\d+)\s*tests?\b/i);
|
|
12181
12249
|
if (m5) declaredTestCount = parseInt(m5[1], 10);
|
|
12182
12250
|
}
|
|
12251
|
+
// 1.36.52 (외부 GPT 감사 F-02, 재현확정): 비율형 완전-통과("5/5 passed")를 명시 카운트와 동일 정규화 —
|
|
12252
|
+
// 종전엔 declaredTestCount 로만 정적 선언-수 대조를 해서 비율형 주장이 그 게이트를 우회했다.
|
|
12253
|
+
// 1.35.9 원칙 유지: 전부-통과(N/N) 주장만 승격(부분 비율은 비-테스트 서술일 수 있어 미승격).
|
|
12254
|
+
let _ratioPromoted = false;
|
|
12255
|
+
if (declaredTestCount == null && declaredPass && declaredPass.num === declaredPass.denom) { declaredTestCount = declaredPass.denom; _ratioPromoted = true; }
|
|
12183
12256
|
|
|
12184
12257
|
// 실제 파일 존재 검사
|
|
12185
12258
|
const fileChecks = files.map(f => ({ file: f, exists: exists(path.join(root, f)) }));
|
|
@@ -12286,6 +12359,10 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
12286
12359
|
// 1.9.309 (UR-0048, 설치리뷰 Opus critical): done 주장은 evidence(파일+테스트) 기본 강제 — 거짓완료 차단을 기본값·MCP·json 모두에서 작동.
|
|
12287
12360
|
// 이전: --require-evidence 플래그 시에만 검사 → 증거0 done 이 기본 통과(fileChecks.every([])=공허참)했고 MCP 는 도달 불가했음.
|
|
12288
12361
|
// 이제 done/완료 주장은 기본 강제. opt-out: --lenient. 비-done 주장은 영향 없음.
|
|
12362
|
+
// 1.36.52 (F-02 보정, 게이트 실측): 비율형 승격은 "정적 검증 전용" — --run-tests 가 주장 수 이상 전부-통과를
|
|
12363
|
+
// 실제 실행으로 확증하면 정적 선언-수 대조를 재적용하지 않는다(실행 증거 > 정적 휴리스틱; jest/mocha 러너는
|
|
12364
|
+
// 선언을 소스-grep 으로 셀 수 없는 형식이 많아 실행 확증 케이스를 정적 게이트가 거짓 차단했다).
|
|
12365
|
+
if (_ratioPromoted && runResult && !runResult.skipped && runResult.allPassed && runResult.parsed && runResult.parsed.num >= declaredTestCount) declaredTestCount = null;
|
|
12289
12366
|
const isDoneClaim = /done|완료|completed/i.test(row.status || '');
|
|
12290
12367
|
const evq = _evidenceQuality(evidence);
|
|
12291
12368
|
const mustHaveEvidence = !has('--lenient') && (isDoneClaim || has('--require-evidence'));
|
|
@@ -12343,6 +12420,10 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
12343
12420
|
const out = {
|
|
12344
12421
|
project: path.basename(root),
|
|
12345
12422
|
taskId, row,
|
|
12423
|
+
// 1.36.52 (외부 GPT 감사 F-02): 신뢰 경계 명시 — 이 검증은 정적/실행 흔적 대조이지 의미론 보증이 아니다.
|
|
12424
|
+
// evidenceLevel: static(파일·문자열 대조만) | executed(--run-tests 실제 실행 포함). 소비자가 경계를 오인하지 않도록 스키마에 고정.
|
|
12425
|
+
semanticVerified: false,
|
|
12426
|
+
evidenceLevel: (runResult && !runResult.skipped) ? 'executed' : 'static',
|
|
12346
12427
|
declared: { files: files.length, pass: declaredPass, testCount: declaredTestCount },
|
|
12347
12428
|
actual: { fileChecks, testCount: actualTestCount },
|
|
12348
12429
|
verdict: {
|
|
@@ -15632,7 +15713,7 @@ function _autoRoadmap(root, trigger) {
|
|
|
15632
15713
|
// 토글 auto-graph OFF 시 생성 안 함(⚙ 탭/`leerness toggle`).
|
|
15633
15714
|
if (!_tgl.toggleOn(root, 'auto-graph')) return false;
|
|
15634
15715
|
const outFile = path.resolve(cfg.outFile || path.join(root, 'leerness.html'));
|
|
15635
|
-
const s = _graph.graphHtmlCmd(root, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, quiet: true }, outFile);
|
|
15716
|
+
const s = _graph.graphHtmlCmd(root, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, _loadTechProfile: _tech.loadTechProfile, quiet: true }, outFile);
|
|
15636
15717
|
const _en = _uiLang(root) === 'en';
|
|
15637
15718
|
log(_en ? `✓ ontology graph auto-updated (${trigger}) — ${rel(root, outFile)} (graph+roadmap+toggles)` : `✓ 온톨로지 그래프 자동 갱신 (${trigger}) — ${rel(root, outFile)} (그래프+로드맵+토글 탭)`);
|
|
15638
15719
|
return true;
|
|
@@ -22119,14 +22200,14 @@ function doctorCmd(opts = {}) { return _diag.doctorCmd(opts, { VERSION, uiLang:
|
|
|
22119
22200
|
function whichCmd() { return _diag.whichCmd({ VERSION, uiLang: _uiLang(arg('--path', process.cwd())), has, harnessPath: __filename }); }
|
|
22120
22201
|
// 1.34.3 (T-0077): `graph --html` → lib/graph.js 온톨로지 HTML(leerness.html) 생성기 위임. 데이터는 in-process 로더 주입(자식 프로세스 셸링 X).
|
|
22121
22202
|
const _graph = require('../lib/graph');
|
|
22122
|
-
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
|
|
22203
|
+
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
|
|
22123
22204
|
// 1.34.4 (T-0077 후속): handoff 시 leerness.html 자동 재생성 — opt-in(LEERNESS_AUTO_GRAPH=1, 기본 OFF / "Always-Off Opt-In"). 사용자 비전 "자동으로 작성되게" 충족. 비치명(try/catch) · 기본경로 무영향.
|
|
22124
22205
|
function _maybeAutoGraph(root) {
|
|
22125
22206
|
if (process.env.LEERNESS_AUTO_GRAPH !== '1') return;
|
|
22126
22207
|
try {
|
|
22127
22208
|
const r0 = absRoot(root || process.cwd());
|
|
22128
22209
|
if (!exists(path.join(r0, '.harness'))) return;
|
|
22129
|
-
const s = _graph.graphHtmlCmd(r0, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, quiet: true });
|
|
22210
|
+
const s = _graph.graphHtmlCmd(r0, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, _loadTechProfile: _tech.loadTechProfile, quiet: true });
|
|
22130
22211
|
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 출력 오염 방지
|
|
22131
22212
|
} catch {}
|
|
22132
22213
|
}
|
|
@@ -22354,7 +22435,7 @@ async function main() {
|
|
|
22354
22435
|
if (cmd === 'lazy' && args[1] === 'detect') return lazyDetect(_resolveRoot(args[2]), { json: has('--json') });
|
|
22355
22436
|
if (cmd === 'memory' && args[1] === 'search') return memorySearch(arg('--path', process.cwd()), args.slice(2).join(' '));
|
|
22356
22437
|
if (cmd === 'hook' && args[1] === 'session-start') return hookSessionStartCmd(arg('--path', args[2] || process.cwd())); // 1.36.22: SessionStart 컨텍스트 주입(쓰기 0)
|
|
22357
|
-
if (cmd === 'handoff') { const _hp = arg('--path', args[1] || process.cwd()); const _hr = handoffCmd(_hp); _maybeAutoGraph(_hp); return _hr; }
|
|
22438
|
+
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: 기술 프로필 자동 갱신(마이그레이션 이력) — 그래프 생성 전에
|
|
22358
22439
|
if (cmd === 'reuse-map') return reuseMapCmd(arg('--path', args[1] || process.cwd()));
|
|
22359
22440
|
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 주장 일괄 검증
|
|
22360
22441
|
if (cmd === 'orchestrate') return await orchestrateCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('-')));
|
|
@@ -22480,6 +22561,7 @@ async function main() {
|
|
|
22480
22561
|
if (cmd === 'enforce') return enforceCmd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd(), args[1]); // 1.36.43: 사용 강제 (git pre-commit)
|
|
22481
22562
|
if (cmd === 'anchors') return anchorsCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd(), args[1] && !args[1].startsWith('-') ? args[1] : null); // 1.36.36: 정체성앵커 초안
|
|
22482
22563
|
if (cmd === 'toggle') return _tgl.toggleCmd(arg('--path', process.cwd()), args[1], args[2], args[3], { has, VERSION }); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동)
|
|
22564
|
+
if (cmd === 'tech') return _tech.techCmd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd(), args[1], { has }); // 1.36.53 (UR-0062): 기술 프로필
|
|
22483
22565
|
// 1.36.51 (사용자 요청 UR-0061): 모호성 질문 + 미리보기 승인 워크플로
|
|
22484
22566
|
if (cmd === 'clarify') return _clar.clarifyCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('-')).join(' '), { has });
|
|
22485
22567
|
if (cmd === 'preview') {
|
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]` 핸들러.
|
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
{ id: 'postgres', deps: ['pg', 'postgres'], env: ['POSTGRES_', 'PG_', 'DATABASE_URL'] },
|
|
39
|
+
{ id: 'mysql', deps: ['mysql', 'mysql2'], env: ['MYSQL_'] },
|
|
40
|
+
{ id: 'mongodb', deps: ['mongodb', 'mongoose'], env: ['MONGO_', 'MONGODB_'] },
|
|
41
|
+
{ id: 'redis', deps: ['redis', 'ioredis'], env: ['REDIS_'] },
|
|
42
|
+
{ id: 'prisma', deps: ['prisma', '@prisma/client'], files: ['prisma/schema.prisma'] },
|
|
43
|
+
{ id: 'telegram', deps: ['node-telegram-bot-api', 'telegraf'], env: ['TELEGRAM_'] },
|
|
44
|
+
{ id: 'discord', deps: ['discord.js'], env: ['DISCORD_'] },
|
|
45
|
+
{ id: 'github-api', deps: ['@octokit/rest', 'octokit'], env: ['GH_TOKEN', 'GITHUB_TOKEN'] },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function _envKeys(root) {
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const f of ['.env', '.env.local', '.env.production']) {
|
|
51
|
+
const p = path.join(root, f);
|
|
52
|
+
if (!exists(p)) continue;
|
|
53
|
+
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 {}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _shallowExts(root) {
|
|
59
|
+
const counts = {};
|
|
60
|
+
const scan = (dir, depth) => {
|
|
61
|
+
let entries = [];
|
|
62
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
63
|
+
for (const e of entries.slice(0, 400)) {
|
|
64
|
+
if (e.name.startsWith('.') || e.name === 'node_modules' || e.name === 'dist' || e.name === 'build') continue;
|
|
65
|
+
const p = path.join(dir, e.name);
|
|
66
|
+
if (e.isDirectory()) { if (depth > 0) scan(p, depth - 1); continue; }
|
|
67
|
+
const ext = path.extname(e.name).toLowerCase();
|
|
68
|
+
if (EXT_LANG[ext]) counts[EXT_LANG[ext]] = (counts[EXT_LANG[ext]] || 0) + 1;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
scan(absRoot(root), 2);
|
|
72
|
+
return counts;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 순수-ish 감지 코어 — { languages:[{id,evidence}], services:[{id,evidence}] }
|
|
76
|
+
function detectTechProfile(root) {
|
|
77
|
+
root = absRoot(root);
|
|
78
|
+
const languages = []; const seenL = new Set();
|
|
79
|
+
const addL = (id, evidence) => { if (!seenL.has(id)) { seenL.add(id); languages.push({ id, evidence }); } };
|
|
80
|
+
for (const s of LANG_SIGNALS) {
|
|
81
|
+
if (s.files) for (const f of s.files) if (exists(path.join(root, f))) { addL(s.id, f); break; }
|
|
82
|
+
if (s.glob) { try { if (fs.readdirSync(root).some(n => s.glob.test(n))) addL(s.id, 'project file'); } catch {} }
|
|
83
|
+
}
|
|
84
|
+
const extCounts = _shallowExts(root);
|
|
85
|
+
for (const [lang, n] of Object.entries(extCounts)) if (n >= 3) addL(lang, `${n} source files`);
|
|
86
|
+
// deps 수집
|
|
87
|
+
let deps = {};
|
|
88
|
+
try { const pj = JSON.parse(read(path.join(root, 'package.json'))); deps = Object.assign({}, pj.dependencies, pj.devDependencies); } catch {}
|
|
89
|
+
let reqTxt = '';
|
|
90
|
+
try { if (exists(path.join(root, 'requirements.txt'))) reqTxt = read(path.join(root, 'requirements.txt')); } catch {}
|
|
91
|
+
const envKeys = _envKeys(root);
|
|
92
|
+
const services = []; const seenS = new Set();
|
|
93
|
+
const addS = (id, evidence) => { if (!seenS.has(id)) { seenS.add(id); services.push({ id, evidence }); } };
|
|
94
|
+
for (const s of SERVICE_SIGNALS) {
|
|
95
|
+
for (const d of (s.deps || [])) { if (deps[d] || new RegExp(`^${d.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'm').test(reqTxt)) { addS(s.id, `dep: ${d}`); break; } }
|
|
96
|
+
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}`); }
|
|
97
|
+
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; } }
|
|
98
|
+
if (!seenS.has(s.id)) for (const f of (s.files || [])) if (exists(path.join(root, f))) { addS(s.id, `file: ${f}`); break; }
|
|
99
|
+
}
|
|
100
|
+
return { languages, services };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _techPath(root) { return path.join(absRoot(root), '.harness', 'tech-profile.json'); }
|
|
104
|
+
|
|
105
|
+
function loadTechProfile(root) {
|
|
106
|
+
const f = _techPath(root);
|
|
107
|
+
if (!exists(f)) return null;
|
|
108
|
+
try { return JSON.parse(read(f)); } catch { return null; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 감지 → 저장 + 변경 이력(diff) 누적. 반환: { profile, changed, diff }
|
|
112
|
+
function refreshTechProfile(root, opts = {}) {
|
|
113
|
+
root = absRoot(root);
|
|
114
|
+
if (!exists(path.join(root, '.harness'))) return null;
|
|
115
|
+
const cur = detectTechProfile(root);
|
|
116
|
+
const prev = loadTechProfile(root);
|
|
117
|
+
const ids = a => (a || []).map(x => x.id);
|
|
118
|
+
const dif = (a, b) => a.filter(x => !b.includes(x));
|
|
119
|
+
let history = (prev && Array.isArray(prev.history)) ? prev.history : [];
|
|
120
|
+
let changed = false; let diff = null;
|
|
121
|
+
if (prev && prev.current) {
|
|
122
|
+
const aL = dif(ids(cur.languages), ids(prev.current.languages)), rL = dif(ids(prev.current.languages), ids(cur.languages));
|
|
123
|
+
const aS = dif(ids(cur.services), ids(prev.current.services)), rS = dif(ids(prev.current.services), ids(cur.services));
|
|
124
|
+
if (aL.length || rL.length || aS.length || rS.length) {
|
|
125
|
+
changed = true;
|
|
126
|
+
diff = { at: now(), addedLanguages: aL, removedLanguages: rL, addedServices: aS, removedServices: rS };
|
|
127
|
+
history = [...history, diff].slice(-50);
|
|
128
|
+
}
|
|
129
|
+
} else if (!prev) { changed = true; }
|
|
130
|
+
const profile = { current: cur, updatedAt: now(), history };
|
|
131
|
+
if (changed || !prev) {
|
|
132
|
+
// 손상 스토어는 writeUtf8 의 .corrupt-* 대피가 원본을 보존한다 (1.36.50)
|
|
133
|
+
writeUtf8(_techPath(root), JSON.stringify(profile, null, 2) + '\n');
|
|
134
|
+
}
|
|
135
|
+
return { profile, changed, diff };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// `leerness tech [refresh] [--json]`
|
|
139
|
+
function techCmd(root, sub, deps = {}) {
|
|
140
|
+
const { has } = deps;
|
|
141
|
+
root = absRoot(root);
|
|
142
|
+
const json = !!(has && has('--json'));
|
|
143
|
+
if (!exists(path.join(root, '.harness'))) { failJson(json, 'harness_missing', `leerness 미설치: ${root} — 먼저 leerness init`); return; }
|
|
144
|
+
const r = refreshTechProfile(root); // show 도 항상 최신화(감지는 저비용·로컬)
|
|
145
|
+
const p = r.profile;
|
|
146
|
+
if (json) { log(JSON.stringify({ ok: true, ...p, changedNow: r.changed, diffNow: r.diff }, null, 2)); return; }
|
|
147
|
+
log(`# leerness tech — 프로젝트 기술 프로필 (${p.updatedAt.slice(0, 10)})`);
|
|
148
|
+
log(` 언어: ${p.current.languages.length ? p.current.languages.map(l => `${l.id} (${l.evidence})`).join(' · ') : '(감지 없음)'}`);
|
|
149
|
+
log(` 서비스: ${p.current.services.length ? p.current.services.map(s => `${s.id} (${s.evidence})`).join(' · ') : '(감지 없음)'}`);
|
|
150
|
+
if (p.history.length) {
|
|
151
|
+
log(`\n 변경 이력 (${p.history.length}건 — 마이그레이션/언어 전환 추적):`);
|
|
152
|
+
for (const h of p.history.slice(-8)) {
|
|
153
|
+
const parts = [];
|
|
154
|
+
if (h.addedLanguages.length) parts.push(`+언어 ${h.addedLanguages.join(',')}`);
|
|
155
|
+
if (h.removedLanguages.length) parts.push(`-언어 ${h.removedLanguages.join(',')}`);
|
|
156
|
+
if (h.addedServices.length) parts.push(`+서비스 ${h.addedServices.join(',')}`);
|
|
157
|
+
if (h.removedServices.length) parts.push(`-서비스 ${h.removedServices.join(',')}`);
|
|
158
|
+
log(` · ${h.at.slice(0, 16)} ${parts.join(' · ')}`);
|
|
159
|
+
}
|
|
160
|
+
} else log(` (변경 이력 없음 — 서비스/언어가 바뀌면 자동 기록)`);
|
|
161
|
+
log(`\n ⓘ 온톨로지 그래프(leerness.html) 🛠 기술 탭에 표시 · 갱신: leerness tech (handoff 가 자동 갱신)`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
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.53",
|
|
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);
|