leerness 1.36.34 → 1.36.36
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 +15 -0
- package/README.md +4 -4
- package/bin/leerness.js +117 -28
- package/lib/pure-utils.js +40 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.36 — 2026-07-15 — `leerness anchors` — 정체성앵커 초안 합성 (도그푸딩 재실측 후속)
|
|
4
|
+
|
|
5
|
+
**배경(실측)**: 1.36.19 에서 앵커 미작성 감지를 출하했지만, 실프로젝트 7곳 재순회 결과 **brief 미작성 4/7 · plan Goal 미작성 6/7** — 감지·노출만으론 전환이 안 된다(반례: AEGIRINE 0/0 은 전환 가능함의 증거). 마찰 지점은 "빈 문서를 처음부터 써야 함" — 실신호 초안으로 제거.
|
|
6
|
+
|
|
7
|
+
- **`leerness anchors [status|draft [--apply]] [path]`**: package.json description · README 첫 산문 · milestones · 활성 task 만으로 Purpose/Goal 초안 합성(순수 `_draftAnchors`). **발명 금지** — 신호가 없으면 `no_signal` 로 거부(빈 프로젝트에 그럴듯한 거짓 목적을 써주지 않음).
|
|
8
|
+
- **안전**: `--apply` 는 해당 섹션이 **미작성일 때만** 교체(`_replaceMdSection` — 그 섹션만, 나머지 불변). 작성된 앵커는 절대 불클로버(재실행 실측). 초안 표식 주석 명시 — AI/사용자 검토 후 주석 제거로 확정.
|
|
9
|
+
- **handoff 힌트 배선**: 앵커 미작성 헤드라인에 `→ leerness anchors draft` 액션 부착 — 다음 세션 AI 가 한 명령으로 전환.
|
|
10
|
+
- **검증**: selftest 306/306(합성/무신호 거부/섹션 교체/배선), E2E 사슬 실측(status→draft→apply→감지 해소→불클로버), exit 전파형 게이트, 클린룸. (부수: audit 마커와의 finding-code 충돌을 selftest 가 잡아 코드명 분리 — 자기참조 트랩 주석 변형 포함 5번째.)
|
|
11
|
+
|
|
12
|
+
## 1.36.35 — 2026-07-15 — deps --run-tests 정직화 + #7 의도적 DEFER (codex 3차 잔여 소진)
|
|
13
|
+
|
|
14
|
+
- **#6 `deps <cap> --run-tests`**: (a) `--json` 이 테스트 스윕 **전에** return → 스윕이 무언 무시되고 exit 0(false-green) — json/human 이 같은 `_runImpactSweep` 를 쓰도록 공용화, json 은 `tests[]` 필드 + 실패 시 exit 1. (b) 프로젝트를 basename 으로 역추적해 **동명 프로젝트(a/service, b/service) 중 하나만 테스트**되고 실패쪽이 조용히 생략 — 절대경로 키로 교정. 실측: 동명 2 프로젝트 모두 실행(PASS/FAIL 각각), json/human 동등, 실패 exit 1.
|
|
15
|
+
- **#7 (주석 트레이스 오인) 의도적 DEFER — 정직 기록**: 주석 속 `fetch('/users')` 가 낙관검사 트레이스로 인정되는 건 사실이나, 교정에는 JS 주석 마스킹이 필요하다. 이 마스커 계열은 1.36.18 에서 7회 반증 끝에 리버트한 전력이 있고(0-deps 제약 하에 파서 없이 안전 불가), 잘못 벗기면 verify-claim `--strict-claims` 가 **false-BLOCK**(허위 실패로 머지 차단) 하는 방향이라 게이트 편향 원칙(false-PASS 편향)에 반한다. 현 한계는 문서화된 known-limitation 으로 유지: 낙관검사는 "주장 vs 텍스트 흔적" 수준의 advisory 신호다.
|
|
16
|
+
- **검증**: selftest 305/305(스윕 공용+절대경로+exit 배선; 자기참조 트랩 4번째 재발 → split-literal), 동명 충돌 원 재현 before/after, exit 전파형 게이트 e2e, 클린룸. **codex 3차 헌트 10건 전건 처분 완료(수정 9 + 정직 DEFER 1).**
|
|
17
|
+
|
|
3
18
|
## 1.36.34 — 2026-07-15 — 판정 정직화 5종 — 도구가 내리는 판정(허용/만료/최신/집계)이 거짓말하지 않도록 (codex 3차 #4/#5/#8/#9/#10)
|
|
4
19
|
|
|
5
20
|
- **#8 policy check**: 차단 판정인데 exit 0 → **exit 1**(CI/스크립트가 판정 신뢰 가능). 손상 policy.json 을 기본값 폴백으로 오답하던 것 → `policy_corrupt` 구조화 에러.
|
package/README.md
CHANGED
|
@@ -121,7 +121,7 @@ MIT
|
|
|
121
121
|
<!-- leerness:project-readme:start -->
|
|
122
122
|
## Leerness Project Harness
|
|
123
123
|
|
|
124
|
-
이 프로젝트는 Leerness v1.36.
|
|
124
|
+
이 프로젝트는 Leerness v1.36.36 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
125
125
|
|
|
126
126
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
127
127
|
|
|
@@ -175,7 +175,7 @@ leerness memory restore decision <date|title>
|
|
|
175
175
|
|
|
176
176
|
### MCP server (외부 AI 통합)
|
|
177
177
|
|
|
178
|
-
Leerness v1.36.
|
|
178
|
+
Leerness v1.36.36는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
179
179
|
|
|
180
180
|
```jsonc
|
|
181
181
|
// 카테고리별
|
|
@@ -196,7 +196,7 @@ Leerness v1.36.34는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
196
196
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
197
197
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push → 7) session close → 8) 다음 라운드 예약.
|
|
198
198
|
|
|
199
|
-
현재 누적: **70 라운드 (1.9.40 → 1.36.
|
|
199
|
+
현재 누적: **70 라운드 (1.9.40 → 1.36.36)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
200
200
|
|
|
201
201
|
### 성능 가이드
|
|
202
202
|
|
|
@@ -234,6 +234,6 @@ leerness release pack --close --auto-main-push
|
|
|
234
234
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
235
235
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
236
236
|
|
|
237
|
-
Last synced by Leerness v1.36.
|
|
237
|
+
Last synced by Leerness v1.36.36: 2026-07-15
|
|
238
238
|
<!-- leerness:project-readme:end -->
|
|
239
239
|
|
package/bin/leerness.js
CHANGED
|
@@ -26,7 +26,7 @@ const { _isSecretKey, _isPlaceholderSecret, _looksSecretLike, _mergeLines, _merg
|
|
|
26
26
|
_migrationGuideText, _parseContractSpec, _gitignoreMatch,
|
|
27
27
|
_featureGraphTemplate, _parseFeatureGraph, _nextFeatureId, _featureBlock, _featureImpactBfs,
|
|
28
28
|
_parseChangelogBetween, _cellSafe, _cellUnescape, _lineSafe, _parseLimit, _parseAddTitle, _parseImplExports, _taskPositionalPath, _completionClaimAllowed, _minorKey, _shouldPublishNpm,
|
|
29
|
-
_matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd, _briefUnfilled, _planGoalUnfilled } = require('../lib/pure-utils'); // 1.9.318~1.11.4 (UR-0025/.../0007 glossary): 순수 유틸 모듈 분리
|
|
29
|
+
_matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd, _briefUnfilled, _planGoalUnfilled, _draftAnchors, _replaceMdSection } = require('../lib/pure-utils'); // 1.9.318~1.11.4 (UR-0025/.../0007 glossary): 순수 유틸 모듈 분리 · 1.36.36 anchors
|
|
30
30
|
// 1.9.304 (UR-0025): 순수 분석/검증 함수 모듈 분리.
|
|
31
31
|
const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInGit, _epistemicHonestyCheck } = require('../lib/analyzers');
|
|
32
32
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
@@ -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.36';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -3168,6 +3168,34 @@ function _selfTestCases() {
|
|
|
3168
3168
|
const tarOk = s.includes('const _qw = a =>') && s.includes("read(path.join(root, '.harness', 'HARNESS_VERSION')).trim(); if (_hv) nextLeerness = _hv;");
|
|
3169
3169
|
return semverOk && cacheOk && policyOk && credsOk && closeOk && tarOk;
|
|
3170
3170
|
} },
|
|
3171
|
+
{ name: 'deps --run-tests 정직화 (1.36.35, codex 3차 #6): json/human 스윕 공용 + 절대경로 키(동명 프로젝트 오식별 방지) + 실패 시 exit1 (소스가드)', run: () => {
|
|
3172
|
+
const s = read(__filename);
|
|
3173
|
+
const sweepShared = s.includes('function _runImpactSweep(quietLog)') && s.includes("const tests = has('--run-tests') ? _runImpactSweep(true) : undefined;");
|
|
3174
|
+
const absKey = s.includes('fromProjectPath: p,') && s.includes('const impactedPaths = new Set(directImpact.map(e => e.fromProjectPath)');
|
|
3175
|
+
const jsonExit = s.includes('if (tests && tests.some(t => t.passed === false)) process.exitCode = 1;');
|
|
3176
|
+
const humanShared = s.includes('const results = _runImpactSweep(false);');
|
|
3177
|
+
// 구식 basename 역추적이 사라졌는지 (오식별 코드 부재) — split-literal: 이 줄 자신 매치 방지 (자기참조 트랩 4번째)
|
|
3178
|
+
const oldGone = !s.includes('const projPath = allPaths.find(' + 'p => path.basename(p) === projName);');
|
|
3179
|
+
return sweepShared && absKey && jsonExit && humanShared && oldGone;
|
|
3180
|
+
} },
|
|
3181
|
+
{ name: 'anchors draft (1.36.36, 도그푸딩 후속): 실신호 초안 합성 + 미작성만 교체 + 불클로버 — 순수 행위검사', run: () => {
|
|
3182
|
+
const p = require('../lib/pure-utils');
|
|
3183
|
+
if (typeof p._draftAnchors !== 'function' || typeof p._replaceMdSection !== 'function') return false;
|
|
3184
|
+
const d = p._draftAnchors({ pkgDescription: '결제 정산 서비스', readmeText: '# T\n\n웹훅 백엔드.\n', milestones: [{ id: 'M-0001', title: '베타', status: 'planned' }], tasks: [] });
|
|
3185
|
+
const draftOk = d.hasSignal && d.purpose.some(l => l.includes('결제 정산 서비스')) && d.goal.some(l => l.includes('M-0001')) && d.purpose[0].includes('draft:');
|
|
3186
|
+
// 무신호 시 발명 금지
|
|
3187
|
+
const empty = p._draftAnchors({});
|
|
3188
|
+
const noInvent = !empty.hasSignal && empty.purpose.length === 0 && empty.goal.length === 0;
|
|
3189
|
+
// 섹션 교체는 대상 섹션만, 나머지 불변 + 섹션 없으면 원문 그대로
|
|
3190
|
+
const md = '# B\n\n## Purpose\n- placeholder\n\n## Users\n- real users\n';
|
|
3191
|
+
const rep = p._replaceMdSection(md, 'Purpose', ['- new']);
|
|
3192
|
+
const secOk = rep.includes('- new') && !rep.includes('- placeholder') && rep.includes('- real users');
|
|
3193
|
+
const noSec = p._replaceMdSection(md, 'Nope', ['- x']) === md;
|
|
3194
|
+
// CLI 배선 + handoff 힌트
|
|
3195
|
+
const s = read(__filename);
|
|
3196
|
+
const wired = typeof anchorsCmd === 'function' && s.includes("cmd === 'anchors'") && s.includes('→ leerness anchors draft');
|
|
3197
|
+
return draftOk && noInvent && secOk && noSec && wired;
|
|
3198
|
+
} },
|
|
3171
3199
|
{ name: 'debug 렌즈 (1.36.27, obra/superpowers systematic-debugging): 자기질문 6문항 ko/en 락스텝 + affects 유효 + route bugfix 힌트 + 파일매핑 미확장 — 행위검사', run: () => {
|
|
3172
3200
|
const d = LENS_CATALOG.debug;
|
|
3173
3201
|
if (!d) return false;
|
|
@@ -4843,6 +4871,58 @@ function shellGuardCmd(root, cmd, opts = {}) {
|
|
|
4843
4871
|
if (analysis.issues.some(i => i.severity === 'error')) process.exitCode = 1;
|
|
4844
4872
|
}
|
|
4845
4873
|
|
|
4874
|
+
// 1.36.36 (도그푸딩 실측 후속): `leerness anchors [status|draft [--apply]]` — 정체성앵커(brief Purpose / plan Goal) 미작성 전환 지원.
|
|
4875
|
+
// 실측: 1.36.19 감지 출하 후에도 실프로젝트 brief 4/7 · Goal 6/7 미작성 — 노출만으론 전환 안 됨. 초안 합성으로 마찰 제거.
|
|
4876
|
+
// 안전: --apply 는 해당 섹션이 "미작성일 때만" 교체(작성된 내용 절대 불클로버), 초안 표식 주석 명시.
|
|
4877
|
+
function anchorsCmd(root, sub) {
|
|
4878
|
+
root = absRoot(root);
|
|
4879
|
+
const json = has('--json');
|
|
4880
|
+
if (!exists(path.join(root, '.harness'))) { failJson(json, 'harness_missing', `leerness 미설치: ${root}`); return; } // 코드명 주의: audit 고유 finding kind(초기화 미완 마커)는 selftest 가드가 bin 부재를 단언하므로 여기서 사용 금지
|
|
4881
|
+
const bf = path.join(root, '.harness', 'project-brief.md');
|
|
4882
|
+
const pf = planPath(root);
|
|
4883
|
+
const briefTxt = exists(bf) ? read(bf) : '';
|
|
4884
|
+
const planTxt = exists(pf) ? read(pf) : '';
|
|
4885
|
+
const naM = '<!-- leerness:na';
|
|
4886
|
+
const briefGap = !!briefTxt && !briefTxt.includes(naM) && _briefUnfilled(briefTxt);
|
|
4887
|
+
const planGap = !!planTxt && !planTxt.includes(naM) && _planGoalUnfilled(planTxt);
|
|
4888
|
+
if (!sub || sub === 'status') {
|
|
4889
|
+
if (json) { log(JSON.stringify({ briefUnfilled: briefGap, planGoalUnfilled: planGap }, null, 2)); return; }
|
|
4890
|
+
log(`# leerness anchors — 정체성앵커 상태`);
|
|
4891
|
+
log(` project-brief Purpose: ${briefGap ? '📋 미작성' : '✓ 작성됨'}`);
|
|
4892
|
+
log(` plan Goal: ${planGap ? '📋 미작성' : '✓ 작성됨'}`);
|
|
4893
|
+
if (briefGap || planGap) log(`\n 💡 초안 생성: leerness anchors draft · 적용: leerness anchors draft --apply (미작성 섹션만 채움)`);
|
|
4894
|
+
return;
|
|
4895
|
+
}
|
|
4896
|
+
if (sub === 'draft') {
|
|
4897
|
+
// 실신호 수집 (전부 read-only)
|
|
4898
|
+
let pkgDescription = '';
|
|
4899
|
+
try { const pj = JSON.parse(read(path.join(root, 'package.json'))); pkgDescription = pj.description || ''; } catch {}
|
|
4900
|
+
let readmeText = '';
|
|
4901
|
+
try { if (exists(path.join(root, 'README.md'))) readmeText = read(path.join(root, 'README.md')).slice(0, 4000); } catch {}
|
|
4902
|
+
let milestones = [], tasks = [];
|
|
4903
|
+
try { const rd = _roadmapData(root); milestones = rd.milestones || []; tasks = rd.tasks || []; } catch {}
|
|
4904
|
+
const draft = _draftAnchors({ pkgDescription, readmeText, milestones, tasks });
|
|
4905
|
+
if (!draft.hasSignal) { failJson(json, 'no_signal', '초안을 만들 실신호가 없음 (package.json description / README 산문 / milestone / 활성 task 모두 부재) — 직접 작성 필요'); return; }
|
|
4906
|
+
const apply = has('--apply');
|
|
4907
|
+
const applied = [];
|
|
4908
|
+
if (briefGap && draft.purpose.length) {
|
|
4909
|
+
if (apply) { writeUtf8(bf, _replaceMdSection(briefTxt, 'Purpose', draft.purpose)); applied.push('project-brief.md#Purpose'); }
|
|
4910
|
+
}
|
|
4911
|
+
if (planGap && draft.goal.length) {
|
|
4912
|
+
if (apply) { writeUtf8(pf, _replaceMdSection(planTxt, 'Goal', draft.goal)); applied.push('plan.md#Goal'); }
|
|
4913
|
+
}
|
|
4914
|
+
if (json) { log(JSON.stringify({ briefUnfilled: briefGap, planGoalUnfilled: planGap, draft: { purpose: draft.purpose, goal: draft.goal }, applied, dryRun: !apply }, null, 2)); return; }
|
|
4915
|
+
log(`# leerness anchors draft ${apply ? '(적용)' : '(dry-run — 적용: --apply)'}`);
|
|
4916
|
+
if (briefGap) { log(`\n## project-brief Purpose ${apply ? '← 적용됨' : '초안'}`); draft.purpose.forEach(l => log(' ' + l)); }
|
|
4917
|
+
else log(`\n ✓ project-brief Purpose 는 이미 작성됨 — 건드리지 않음`);
|
|
4918
|
+
if (planGap) { log(`\n## plan Goal ${apply ? '← 적용됨' : '초안'}`); draft.goal.forEach(l => log(' ' + l)); }
|
|
4919
|
+
else log(` ✓ plan Goal 은 이미 작성됨 — 건드리지 않음`);
|
|
4920
|
+
if (apply && applied.length) log(`\n✓ 적용: ${applied.join(', ')} (초안 표식 주석 포함 — 검토 후 주석 제거로 확정)`);
|
|
4921
|
+
return;
|
|
4922
|
+
}
|
|
4923
|
+
fail(`알 수 없는 하위명령: ${sub} (가능: status, draft [--apply])`);
|
|
4924
|
+
}
|
|
4925
|
+
|
|
4846
4926
|
// 1.36.28 (codex 미검토표면 헌트 #3 — 데이터 유실 클래스): 손상된 JSON 스토어를 "빈 값"으로 오인해 덮어쓰는 fail-open 방지.
|
|
4847
4927
|
// 파일이 "없음"과 "있으나 손상"을 구분 — 후자는 파싱 실패를 던져 변경(저장)을 거부하고 원본을 보존한다.
|
|
4848
4928
|
// 설계: LOAD 가 아니라 SAVE/변경 진입점에서만 던진다(handoff 등 비변경 읽기 경로는 resilient 유지). state 는
|
|
@@ -9796,7 +9876,7 @@ function handoff(root) {
|
|
|
9796
9876
|
const _planTxt = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
9797
9877
|
const briefGap = !_briefTxt.includes(_naM) && _briefUnfilled(_briefTxt);
|
|
9798
9878
|
const planGap = !!_planTxt && !_planTxt.includes(_naM) && _planGoalUnfilled(_planTxt);
|
|
9799
|
-
if (briefGap && planGap) parts.push(t('📋 정체성앵커 미작성 (brief+plan)', '📋 identity anchors unfilled (brief+plan)'));
|
|
9879
|
+
if (briefGap && planGap) parts.push(t('📋 정체성앵커 미작성 (brief+plan) → leerness anchors draft', '📋 identity anchors unfilled (brief+plan) → leerness anchors draft'));
|
|
9800
9880
|
else if (briefGap) parts.push(t('📋 project-brief 미작성', '📋 project-brief unfilled'));
|
|
9801
9881
|
else if (planGap) parts.push(t('📋 plan Goal 미작성', '📋 plan Goal unfilled'));
|
|
9802
9882
|
} catch {}
|
|
@@ -12338,7 +12418,7 @@ function depsImpactCmd(root, targetCapability) {
|
|
|
12338
12418
|
for (const e of entries) {
|
|
12339
12419
|
allEntries.push({ project: path.basename(p), projectPath: p, entry: e });
|
|
12340
12420
|
for (const dep of e.dependsOn) {
|
|
12341
|
-
allEdges.push({ fromProject: path.basename(p), fromCap: e.capability, toCap: dep });
|
|
12421
|
+
allEdges.push({ fromProject: path.basename(p), fromProjectPath: p, fromCap: e.capability, toCap: dep }); // 1.36.35 (#6): 절대경로 동반 — basename 충돌 시 프로젝트 오식별 방지
|
|
12342
12422
|
}
|
|
12343
12423
|
}
|
|
12344
12424
|
}
|
|
@@ -12348,6 +12428,7 @@ function depsImpactCmd(root, targetCapability) {
|
|
|
12348
12428
|
const targetLower = target.toLowerCase();
|
|
12349
12429
|
const directImpact = allEdges.filter(e => e.toCap.toLowerCase() === targetLower);
|
|
12350
12430
|
const impactedProjects = new Set(directImpact.map(e => e.fromProject));
|
|
12431
|
+
const impactedPaths = new Set(directImpact.map(e => e.fromProjectPath).filter(Boolean)); // 1.36.35 (#6): 절대경로 기준 집합
|
|
12351
12432
|
|
|
12352
12433
|
// 2단계 전이: 영향받은 capability를 또 의존하는 것들 (2-hop)
|
|
12353
12434
|
const transitiveImpact = [];
|
|
@@ -12356,6 +12437,7 @@ function depsImpactCmd(root, targetCapability) {
|
|
|
12356
12437
|
if (e2.toCap.toLowerCase() === e1.fromCap.toLowerCase()) {
|
|
12357
12438
|
transitiveImpact.push({ via: e1.fromCap, ...e2 });
|
|
12358
12439
|
impactedProjects.add(e2.fromProject);
|
|
12440
|
+
if (e2.fromProjectPath) impactedPaths.add(e2.fromProjectPath);
|
|
12359
12441
|
}
|
|
12360
12442
|
}
|
|
12361
12443
|
}
|
|
@@ -12363,13 +12445,39 @@ function depsImpactCmd(root, targetCapability) {
|
|
|
12363
12445
|
// target capability 자체가 어디 등록됐는지
|
|
12364
12446
|
const definedAt = allEntries.filter(e => e.entry.capability.toLowerCase() === targetLower);
|
|
12365
12447
|
|
|
12448
|
+
// 1.36.35 (#6): 테스트 스윕 — json/human 공용 (종전엔 json 이 스윕 전에 return 해 --run-tests 가 무언 무시됐고,
|
|
12449
|
+
// basename 으로 경로를 역추적해 동명 프로젝트 중 하나만 테스트됐다). 절대경로 순회로 둘 다 교정.
|
|
12450
|
+
function _runImpactSweep(quietLog) {
|
|
12451
|
+
const results = [];
|
|
12452
|
+
for (const projPath of impactedPaths) {
|
|
12453
|
+
const projName = path.basename(projPath);
|
|
12454
|
+
const pkgPath = path.join(projPath, 'package.json');
|
|
12455
|
+
if (!exists(pkgPath)) { if (!quietLog) log(` ⚠ ${projName}: package.json 없음 — skip`); results.push({ project: projName, projectPath: projPath, skipped: 'no_package_json' }); continue; }
|
|
12456
|
+
let pkg = null;
|
|
12457
|
+
try { pkg = JSON.parse(read(pkgPath)); } catch {}
|
|
12458
|
+
if (!pkg?.scripts?.test) { if (!quietLog) log(` ⚠ ${projName}: scripts.test 없음 — skip`); results.push({ project: projName, projectPath: projPath, skipped: 'no_test_script' }); continue; }
|
|
12459
|
+
const t0 = Date.now();
|
|
12460
|
+
const r = runCommandSafe('npm test', [], { cwd: projPath, root: projPath, encoding: 'utf8', allowShell: true, scrubSecrets: true, timeout: 5 * 60 * 1000, kind: 'reuse_impact_test' });
|
|
12461
|
+
const elapsed = Date.now() - t0;
|
|
12462
|
+
const out = (r.stdout || '') + (r.stderr || '');
|
|
12463
|
+
const passed = r.status === 0;
|
|
12464
|
+
results.push({ project: projName, projectPath: projPath, passed, status: r.status, elapsedMs: elapsed });
|
|
12465
|
+
if (!quietLog) log(` ${passed ? '✓' : '✗'} ${projName}: ${passed ? 'PASS' : `FAIL (exit ${r.status})`} · ${Math.round(elapsed / 1000)}s`);
|
|
12466
|
+
}
|
|
12467
|
+
return results;
|
|
12468
|
+
}
|
|
12469
|
+
|
|
12366
12470
|
if (has('--json')) {
|
|
12471
|
+
const tests = has('--run-tests') ? _runImpactSweep(true) : undefined;
|
|
12472
|
+
if (tests && tests.some(t => t.passed === false)) process.exitCode = 1;
|
|
12367
12473
|
log(JSON.stringify({
|
|
12368
12474
|
target,
|
|
12369
12475
|
definedAt: definedAt.map(d => ({ project: d.project, element: d.entry.element })),
|
|
12370
12476
|
directImpact,
|
|
12371
12477
|
transitiveImpact,
|
|
12372
|
-
impactedProjects: Array.from(impactedProjects)
|
|
12478
|
+
impactedProjects: Array.from(impactedProjects),
|
|
12479
|
+
impactedProjectPaths: Array.from(impactedPaths),
|
|
12480
|
+
...(tests ? { tests } : {})
|
|
12373
12481
|
}, null, 2));
|
|
12374
12482
|
return;
|
|
12375
12483
|
}
|
|
@@ -12402,32 +12510,12 @@ function depsImpactCmd(root, targetCapability) {
|
|
|
12402
12510
|
if (has('--run-tests')) {
|
|
12403
12511
|
log('');
|
|
12404
12512
|
log(`## 🚦 자동 회귀 sweep (--run-tests)`);
|
|
12405
|
-
const results =
|
|
12406
|
-
for (const projName of impactedProjects) {
|
|
12407
|
-
const projPath = allPaths.find(p => path.basename(p) === projName);
|
|
12408
|
-
if (!projPath) continue;
|
|
12409
|
-
const pkgPath = path.join(projPath, 'package.json');
|
|
12410
|
-
if (!exists(pkgPath)) { log(` ⚠ ${projName}: package.json 없음 — skip`); continue; }
|
|
12411
|
-
let pkg = null;
|
|
12412
|
-
try { pkg = JSON.parse(read(pkgPath)); } catch {}
|
|
12413
|
-
if (!pkg?.scripts?.test) { log(` ⚠ ${projName}: scripts.test 없음 — skip`); continue; }
|
|
12414
|
-
const t0 = Date.now();
|
|
12415
|
-
// 1.9.299 (UR-0039): 영향 프로젝트 npm test → runCommandSafe + scrubSecrets (시크릿 노출 차단). root=projPath 라 cwd jail 통과.
|
|
12416
|
-
const r = runCommandSafe('npm test', [], { cwd: projPath, root: projPath, encoding: 'utf8', allowShell: true, scrubSecrets: true, timeout: 5 * 60 * 1000, kind: 'reuse_impact_test' });
|
|
12417
|
-
const elapsed = Date.now() - t0;
|
|
12418
|
-
const out = (r.stdout || '') + (r.stderr || '');
|
|
12419
|
-
const m = out.match(/(\d+)\s*\/\s*(\d+)\s*(?:passed|통과|pass|passing)/i);
|
|
12420
|
-
const passed = r.status === 0;
|
|
12421
|
-
results.push({ project: projName, passed, exit: r.status, elapsed, parsed: m ? { num: parseInt(m[1], 10), denom: parseInt(m[2], 10) } : null });
|
|
12422
|
-
const tag = passed ? '✓' : '✗';
|
|
12423
|
-
const ratio = m ? ` (${m[1]}/${m[2]})` : '';
|
|
12424
|
-
log(` ${tag} ${projName}: exit=${r.status}${ratio} ${elapsed}ms`);
|
|
12425
|
-
}
|
|
12513
|
+
const results = _runImpactSweep(false); // 1.36.35 (#6): json 과 동일한 절대경로 스윕 공용
|
|
12426
12514
|
log('');
|
|
12427
12515
|
const pass = results.filter(r => r.passed).length;
|
|
12428
|
-
const fail = results.
|
|
12516
|
+
const fail = results.filter(r => r.passed === false).length;
|
|
12429
12517
|
log(`## 종합`);
|
|
12430
|
-
log(` - 영향받는 프로젝트 ${
|
|
12518
|
+
log(` - 영향받는 프로젝트 ${impactedPaths.size}개 중 ${pass}개 통과, ${fail}개 실패`);
|
|
12431
12519
|
if (fail > 0) {
|
|
12432
12520
|
log(` ⚠ ${target} 변경이 ${fail}개 프로젝트에 회귀 발생 가능 — 해당 프로젝트 testing 우선`);
|
|
12433
12521
|
return process.exit(1);
|
|
@@ -21700,6 +21788,7 @@ async function main() {
|
|
|
21700
21788
|
if (cmd === 'milestones') return milestonesCmd(_resolveRoot(args[1]));
|
|
21701
21789
|
// 1.9.231: leerness pulse — 한 줄 종합 요약 (10 핵심 지표)
|
|
21702
21790
|
if (cmd === 'pulse') return pulseCmd(_resolveRoot(args[1]));
|
|
21791
|
+
if (cmd === 'anchors') return anchorsCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd(), args[1] && !args[1].startsWith('-') ? args[1] : null); // 1.36.36: 정체성앵커 초안
|
|
21703
21792
|
if (cmd === 'toggle') return _tgl.toggleCmd(arg('--path', process.cwd()), args[1], args[2], args[3], { has, VERSION }); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동)
|
|
21704
21793
|
if (cmd === 'lens') return lensCmd(args[1]); // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈
|
|
21705
21794
|
// 1.9.233: leerness commands — 카테고리화된 전체 CLI 명령 목록
|
package/lib/pure-utils.js
CHANGED
|
@@ -1145,8 +1145,47 @@ function _sectionUnfilled(content, heading, placeholderRe) {
|
|
|
1145
1145
|
function _briefUnfilled(content) { return _sectionUnfilled(content, 'Purpose', _BRIEF_PLACEHOLDER_RE); }
|
|
1146
1146
|
function _planGoalUnfilled(content) { return _sectionUnfilled(content, 'Goal', _PLAN_GOAL_PLACEHOLDER_RE); }
|
|
1147
1147
|
|
|
1148
|
+
// 1.36.36 (도그푸딩 실측 후속): 정체성앵커 초안 합성 — 실신호(package.json/README/milestones/tasks)만으로 순수 조합.
|
|
1149
|
+
// 배경: 1.36.19 감지 출하 후에도 실프로젝트 7곳 재실측에서 brief 미작성 4/7 · plan Goal 미작성 6/7 — 노출만으론 전환 안 됨.
|
|
1150
|
+
// 원칙: 발명 금지(신호에 없는 내용은 쓰지 않음) + 초안 표식 명시(확정은 AI/사용자 몫) — 과장 방지.
|
|
1151
|
+
function _draftAnchors(sig = {}) {
|
|
1152
|
+
const purpose = [];
|
|
1153
|
+
const goal = [];
|
|
1154
|
+
const desc = String(sig.pkgDescription || '').trim();
|
|
1155
|
+
// README 첫 문단: 제목/배지/빈줄 건너뛰고 첫 산문 줄
|
|
1156
|
+
let readmeLead = '';
|
|
1157
|
+
for (const line of String(sig.readmeText || '').split(/\r?\n/)) {
|
|
1158
|
+
const t = line.trim();
|
|
1159
|
+
if (!t || t.startsWith('#') || t.startsWith('![') || t.startsWith('[!') || t.startsWith('<')) continue;
|
|
1160
|
+
readmeLead = t; break;
|
|
1161
|
+
}
|
|
1162
|
+
if (desc) purpose.push(`- ${desc}`);
|
|
1163
|
+
if (readmeLead && readmeLead !== desc) purpose.push(`- ${readmeLead.slice(0, 200)}`);
|
|
1164
|
+
const ms = (sig.milestones || []).filter(m => m && m.title).slice(0, 3);
|
|
1165
|
+
const activeTasks = (sig.tasks || []).filter(t => t && /in-progress|planned|requested/.test(String(t.status || ''))).slice(0, 3);
|
|
1166
|
+
if (desc || readmeLead) goal.push(`- ${(desc || readmeLead).slice(0, 160)} 를(을) 동작하는 제품으로 유지·발전시킨다`);
|
|
1167
|
+
for (const m of ms) goal.push(`- [${m.id || 'M'}] ${String(m.title).slice(0, 120)}${m.status ? ` (${m.status})` : ''}`);
|
|
1168
|
+
if (!goal.length && activeTasks.length) for (const t of activeTasks) goal.push(`- ${t.id}: ${String(t.request || '').slice(0, 100)}`);
|
|
1169
|
+
const marker = '<!-- draft: leerness anchors — 실신호 기반 초안. AI/사용자가 검토·수정 후 이 주석을 지우면 확정 -->';
|
|
1170
|
+
return {
|
|
1171
|
+
hasSignal: purpose.length > 0 || goal.length > 0,
|
|
1172
|
+
purpose: purpose.length ? [marker, ...purpose] : [],
|
|
1173
|
+
goal: goal.length ? [marker, ...goal] : [],
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// 섹션 본문 교체(## <heading> 다음부터 다음 ## 전까지) — 파일 나머지는 불변. 대상 섹션 없으면 원문 그대로.
|
|
1178
|
+
function _replaceMdSection(content, heading, newBodyLines) {
|
|
1179
|
+
const lines = String(content || '').replace(/\r\n/g, '\n').split('\n');
|
|
1180
|
+
const start = lines.findIndex(l => l.trim().replace(/^#+\s*/, '').toLowerCase() === String(heading).toLowerCase() && /^#+\s/.test(l.trim()));
|
|
1181
|
+
if (start < 0) return content;
|
|
1182
|
+
let end = lines.length;
|
|
1183
|
+
for (let i = start + 1; i < lines.length; i++) if (/^##\s/.test(lines[i])) { end = i; break; }
|
|
1184
|
+
return [...lines.slice(0, start + 1), '', ...newBodyLines, '', ...lines.slice(end)].join('\n');
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1148
1187
|
module.exports = {
|
|
1149
|
-
_parseImplExports, _briefUnfilled, _planGoalUnfilled, _mdSectionBody,
|
|
1188
|
+
_parseImplExports, _briefUnfilled, _planGoalUnfilled, _mdSectionBody, _draftAnchors, _replaceMdSection,
|
|
1150
1189
|
_matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd, GLOSSARY_START, GLOSSARY_END,
|
|
1151
1190
|
_isSecretKey, compareVer, parseHarnessVersion,
|
|
1152
1191
|
_isPlaceholderSecret, _looksSecretLike,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.36",
|
|
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",
|