leerness 1.36.18 → 1.36.20
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 +73 -29
- package/lib/audit.js +9 -0
- package/lib/pure-utils.js +26 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.20 — 2026-07-14 — `--apply --json`이 실제로 적용하도록 수정 — mutation-before-output (codex fresh-QA #7, 3커맨드 재현확정)
|
|
4
|
+
|
|
5
|
+
codex fresh-surface QA(1.36.19 이연분)에서 확정한 **silent no-op** 버그 수정: `--json` 분기가 mutation "전"에 return 해, `--apply --json`이 적용을 보고하고도 실제로는 아무 변경도 안 하던 3개 커맨드.
|
|
6
|
+
|
|
7
|
+
- **`reuse autodetect --apply --json`** — 후보만 보고하고 reuse-map.md를 안 씀(bin:20307의 --json return이 20319의 --apply 앞). → apply를 출력 전에 수행하고 JSON에 `applied`/`added` 반영. 실측: `--apply --json` → `applied:true, added:2` + reuse-map 실제 기록(0→1건), `--apply`(no-json) 무회귀.
|
|
8
|
+
- **`release cleanup --apply --json`** — `apply:true, deleteCount:N`을 보고하고도 브랜치를 안 지움(15018의 --json return이 15051의 삭제 앞). → 삭제를 출력 전에 수행하고 JSON에 `deleted`/`deleteFailed` 반영 + 삭제 실패 시 `exitCode=1`. 실측: `--apply --keep 1 --json` → `deleted:3` + 브랜치 실제 삭제(4→1).
|
|
9
|
+
- **`env encoding --apply --json`** — read-only 파일 apply 실패를 `action:'failed'`로 기록만 하고 exit 0. → 실패 시 `process.exitCode=1`. 실측: read-only→exit 1, writable→exit 0.
|
|
10
|
+
- **검증**: selftest 290/290(신규 소스가드 — 3커맨드의 apply-before-output 구조 + 실패 exit 반영), 각 커맨드 재현확정(임시 프로젝트/git-repo/read-only 파일), 게이트 e2e, 게시본 클린룸.
|
|
11
|
+
- **이연(다음 라운드)**: codex #8(CRLF `plan.md` 태스크 파서 tasks:[] + 잘못된 JSON 스키마가 `--json` 경로 crash — 파서 서브시스템 별도 조사) + #6(`copyRec` lstatSync 심링크).
|
|
12
|
+
|
|
13
|
+
## 1.36.19 — 2026-07-14 — 전략 앵커(project-brief/plan Goal) 미작성 감지 — 인계 AI가 프로젝트 맥락을 못 받던 근본원인 표면화 (실사용 7 프로젝트 dogfood)
|
|
14
|
+
|
|
15
|
+
**현장 관찰**: leerness를 오래 적용한 실프로젝트에서, 인계받은 AI(codex 등)가 "leerness를 참조하나 프로젝트 맥락을 이해 못 하고" 작업하는 현상이 보고됨. 7개 실사용 프로젝트를 dogfood 조사한 결과 근본원인이 드러남 — **동적 상태(current-state 7/7, decisions)는 잘 유지되나, 정체성 앵커인 `project-brief.md` Purpose(5/7 미작성)와 `plan.md` Goal/Scope(7/7 미작성)이 템플릿 placeholder 그대로 방치됨**. 27개 결정을 쌓은 프로젝트조차 brief는 빈칸. 그 결과 인계 AI는 "최근 무슨 작업을 했는지"(동적)는 받지만 "이 프로젝트가 무엇이고 범위가 어디까지인지"(정체성)를 못 받아, 최근 스레드만 이어가며 전체 프레임을 놓침. 게다가 leerness 스스로 이 갭을 전혀 경고하지 않았음(감지 부재).
|
|
16
|
+
|
|
17
|
+
- **전략 앵커 미작성 감지 (신규)**: pure-utils에 `_briefUnfilled`/`_planGoalUnfilled` 순수 detector 추가 — `## Purpose`/`## Goal` 섹션이 템플릿 placeholder 문구이거나 실내용 0(빈 bullet)이면 미작성으로 판정. bullet/문단 무관(문단형 실작성도 정상 인식), 섹션 자체가 없으면 미플래그(FP 회피). 7 프로젝트 ground-truth + 컨트롤(placeholder/빈칸/bullet작성/문단작성/섹션없음)로 판별 정밀 검증.
|
|
18
|
+
- **`audit` finding 2종**: project-brief Purpose 미작성 → `project_brief_unfilled` 경고("인계받는 AI가 프로젝트 목적/맥락을 못 받습니다"), plan Goal 미작성 → `plan_goal_unfilled` 경고. `<!-- leerness:na -->` 마커로 의도적 스킵 가능(기존 design-system/reuse-map 관례와 동일).
|
|
19
|
+
- **handoff 헤드라인 표면화**: 세션 시작 헤드라인에 `📋 project-brief 미작성` / `📋 plan Goal 미작성` / `📋 정체성앵커 미작성 (brief+plan)` 노출 — 파묻힌 init task(T-0001) 대신 매 세션 가시화. 채워진 프로젝트엔 무신호(무노이즈).
|
|
20
|
+
- **검증**: selftest 289/289(신규 앵커 감지 행위검사 — placeholder/빈칸/bullet/문단/섹션없음 5판별 + audit/handoff 배선 가드), 실프로젝트 실증(Adzento=brief+plan 경고, cafe-macro/view-work=brief populated·plan만), 게이트 e2e, 게시본 클린룸.
|
|
21
|
+
- **이연(다음 라운드, codex fresh-surface QA)**: #7 `--json`이 mutation 전 early-return(`reuse autodetect`/`release cleanup`/`env encoding` `--apply --json`이 apply를 보고하나 실제 미적용) + 실패 시 exit 0, #8 CRLF `plan.md` tasks:[] & 잘못된 JSON 스키마가 `--json` 경로에서 crash(구조화 에러 아님), #6 `copyRec` lstatSync 심링크 미처리. 재현→수정 예정.
|
|
22
|
+
|
|
3
23
|
## 1.36.18 — 2026-07-14 — UR-0052: handoff --compact 단일경로 + ID 5자리+ 대응 (P2-6 주석 마스킹은 조사 후 이연)
|
|
4
24
|
|
|
5
25
|
1.36.17에서 백로그로 이연했던 UR-0052 3건 중 2건(P3-8·P1-2)을 재현·수정. P2-6(주석 마스킹)은 codex 7라운드 적대 재검 끝에 **파서 없이 안전 구현 불가**로 판단해 이연(하단 상세). 각 수정은 파일 스크립트 프로브 + selftest 케이스로 실증(맹신 X).
|
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ MIT
|
|
|
125
125
|
<!-- leerness:project-readme:start -->
|
|
126
126
|
## Leerness Project Harness
|
|
127
127
|
|
|
128
|
-
이 프로젝트는 Leerness v1.36.
|
|
128
|
+
이 프로젝트는 Leerness v1.36.20 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
129
129
|
|
|
130
130
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
131
131
|
|
|
@@ -179,7 +179,7 @@ leerness memory restore decision <date|title>
|
|
|
179
179
|
|
|
180
180
|
### MCP server (외부 AI 통합)
|
|
181
181
|
|
|
182
|
-
Leerness v1.36.
|
|
182
|
+
Leerness v1.36.20는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
183
183
|
|
|
184
184
|
```jsonc
|
|
185
185
|
// 카테고리별
|
|
@@ -200,7 +200,7 @@ Leerness v1.36.18는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
200
200
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
201
201
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
|
|
202
202
|
|
|
203
|
-
현재 누적: **70 라운드 (1.9.40 → 1.36.
|
|
203
|
+
현재 누적: **70 라운드 (1.9.40 → 1.36.20)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
204
204
|
|
|
205
205
|
### 성능 가이드 (1.9.140 측정)
|
|
206
206
|
|
|
@@ -238,6 +238,6 @@ leerness release pack --close --auto-main-push
|
|
|
238
238
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
239
239
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
240
240
|
|
|
241
|
-
Last synced by Leerness v1.36.
|
|
241
|
+
Last synced by Leerness v1.36.20: 2026-07-14
|
|
242
242
|
<!-- leerness:project-readme:end -->
|
|
243
243
|
|
package/bin/leerness.js
CHANGED
|
@@ -26,14 +26,14 @@ 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 } = require('../lib/pure-utils'); // 1.9.318~1.11.4 (UR-0025/.../0007 glossary): 순수 유틸 모듈 분리
|
|
29
|
+
_matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd, _briefUnfilled, _planGoalUnfilled } = require('../lib/pure-utils'); // 1.9.318~1.11.4 (UR-0025/.../0007 glossary): 순수 유틸 모듈 분리
|
|
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).
|
|
33
33
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _TOOL_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 · 1.11.4 (UR-0007): _TOOL_CATALOG
|
|
34
34
|
const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
|
|
35
35
|
|
|
36
|
-
const VERSION = '1.36.
|
|
36
|
+
const VERSION = '1.36.20';
|
|
37
37
|
|
|
38
38
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
39
39
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -2611,6 +2611,8 @@ function envCmd(root, sub) {
|
|
|
2611
2611
|
}
|
|
2612
2612
|
}
|
|
2613
2613
|
}
|
|
2614
|
+
// 1.36.20 (codex fresh-QA #7): apply 실패(read-only EPERM 등)를 exit code 에 반영 — 종전엔 action:'failed' 를 기록만 하고 exit 0.
|
|
2615
|
+
if (apply && (result.applied || []).some(a => a.action === 'failed')) process.exitCode = 1;
|
|
2614
2616
|
if (has('--json')) { log(JSON.stringify(result, null, 2)); return; }
|
|
2615
2617
|
log(cy(`# leerness env encoding-check (1.9.241/242, UR-0014) — 셸 스크립트 인코딩 위험 감지`));
|
|
2616
2618
|
log('');
|
|
@@ -2963,6 +2965,13 @@ function _selfTestCases() {
|
|
|
2963
2965
|
const realDecl = p._parseContractSpec('function realFn(){}\n').declared.includes('realFn'); // 회귀: 펜스 밖 선언 유지
|
|
2964
2966
|
return bracket && dotStill && fenceExcluded && realDecl;
|
|
2965
2967
|
} },
|
|
2968
|
+
{ name: 'apply-before-output mutation 순서 (1.36.20, codex fresh-QA #7): reuse/release --apply 가 --json return 전 수행 + 실패 exit code — 소스가드', run: () => {
|
|
2969
|
+
const s = read(__filename);
|
|
2970
|
+
const reuseOk = s.includes('project: path.basename(root), found, applied, added') && s.includes("failJson(has('--json'), 'no_reuse_map'"); // reuse: apply 먼저 → json 에 applied/added
|
|
2971
|
+
const releaseOk = s.includes('deleteCount: toDelete.length, deleted, deleteFailed') && s.includes('if (deleteFailed > 0) process.exitCode = 1'); // release: 삭제 먼저 → json 에 deleted
|
|
2972
|
+
const encOk = s.includes("(result.applied || []).some(a => a.action === 'failed')) process.exitCode = 1"); // encoding: 실패 exit1
|
|
2973
|
+
return reuseOk && releaseOk && encOk;
|
|
2974
|
+
} },
|
|
2966
2975
|
{ name: 'ID 리더 5자리+ 대응 (1.36.18, UR-0052 P1-2): \\d{4}→\\d{4,} — 10k+ ID(T-10000) truncation 방지, 날짜 연도는 exactly-4 보존 — 행위검사', run: () => {
|
|
2967
2976
|
// 행위: 5자리 ID 전체 캡처(구 \d{4}는 앞 4자리만 → truncation/충돌), 4자리 ID 무회귀, 날짜 연도 exactly-4 유지.
|
|
2968
2977
|
const taskRe = /\bT-(\d{4,})\b/;
|
|
@@ -2984,6 +2993,22 @@ function _selfTestCases() {
|
|
|
2984
2993
|
const libNoStale = !read(dir + '/feature.js').includes('F-\\d{4}$') && !read(dir + '/audit.js').includes('(M-\\d{4})');
|
|
2985
2994
|
return idFive && idFour && ruleFive && mileFive && dateExact4 && readersWidened && dateKept && libWidened && libNoStale;
|
|
2986
2995
|
} },
|
|
2996
|
+
{ name: '전략 앵커 미작성 감지 (1.36.19, 실사용 7 프로젝트 dogfood): project-brief Purpose / plan Goal placeholder·빈칸 감지 + 실작성(bullet/문단) 무오탐 — 행위검사', run: () => {
|
|
2997
|
+
const p = require('../lib/pure-utils');
|
|
2998
|
+
if (typeof p._briefUnfilled !== 'function' || typeof p._planGoalUnfilled !== 'function') return false;
|
|
2999
|
+
const phBrief = p._briefUnfilled('# Project Brief\n## Purpose\n- 이 프로젝트의 목적을 실제 내용으로 업데이트하세요.\n## Users\n-\n'); // 템플릿 placeholder
|
|
3000
|
+
const emptyBrief = p._briefUnfilled('## Purpose\n- \n## Users\n-\n'); // 빈 bullet
|
|
3001
|
+
const filledBullet = !p._briefUnfilled('## Purpose\n- 소셜 발행 자동화 MCP 서버\n'); // 실작성(bullet) 무오탐
|
|
3002
|
+
const filledPara = !p._briefUnfilled('## Purpose\n코드베이스 변화를 실시간 가시화한다\n'); // 실작성(문단) 무오탐
|
|
3003
|
+
const noSection = !p._briefUnfilled('# Project Brief\n## Project\nfoo\n'); // Purpose 섹션 없음 → 미플래그(FP 회피)
|
|
3004
|
+
const phPlan = p._planGoalUnfilled('# Plan\n## Goal\n- 사용자 목적을 기준으로 전체 계획을 유지합니다.\n## Scope\n-\n'); // plan 템플릿
|
|
3005
|
+
const filledPlan = !p._planGoalUnfilled('## Goal\n- 카드뉴스 발행 자동화 완성\n'); // 실작성 plan 무오탐
|
|
3006
|
+
// audit + handoff 배선 가드
|
|
3007
|
+
const s = read(__filename); const au = require('fs').readFileSync(require('path').join(require('path').dirname(__dirname), 'lib', 'audit.js'), 'utf8');
|
|
3008
|
+
const wired = au.includes("_finding('project_brief_unfilled'") && au.includes("_finding('plan_goal_unfilled'")
|
|
3009
|
+
&& s.includes('📋 project-brief 미작성');
|
|
3010
|
+
return phBrief && emptyBrief && filledBullet && filledPara && noSection && phPlan && filledPlan && wired;
|
|
3011
|
+
} },
|
|
2987
3012
|
{ name: '시크릿 스캐너 FN 헌트 (1.35.14): 하드코딩 자격증명 복합/JSON키 탐지 + Slack xapp + 사전단어 FP 억제 — 행위검사', run: () => {
|
|
2988
3013
|
const pats = require('../lib/catalogs').SECRET_PATTERNS;
|
|
2989
3014
|
const pu = require('../lib/pure-utils');
|
|
@@ -9325,6 +9350,19 @@ function handoff(root) {
|
|
|
9325
9350
|
}
|
|
9326
9351
|
}
|
|
9327
9352
|
} catch {}
|
|
9353
|
+
// 2b) 전략 앵커 미작성 (1.36.19, 실사용 7 프로젝트 dogfood): project-brief Purpose(5/7)·plan Goal(7/7)이 템플릿 그대로
|
|
9354
|
+
// 방치돼 인계받는 AI 가 "프로젝트가 무엇인지/범위"를 못 받던 근본원인 → 세션시작 헤드라인에 표면화(파묻힌 init task 대신 가시화).
|
|
9355
|
+
try {
|
|
9356
|
+
const _naM = '<!-- leerness:na';
|
|
9357
|
+
const _bf = path.join(root, '.harness/project-brief.md');
|
|
9358
|
+
const _briefTxt = exists(_bf) ? read(_bf) : '';
|
|
9359
|
+
const _planTxt = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
9360
|
+
const briefGap = !_briefTxt.includes(_naM) && _briefUnfilled(_briefTxt);
|
|
9361
|
+
const planGap = !!_planTxt && !_planTxt.includes(_naM) && _planGoalUnfilled(_planTxt);
|
|
9362
|
+
if (briefGap && planGap) parts.push(t('📋 정체성앵커 미작성 (brief+plan)', '📋 identity anchors unfilled (brief+plan)'));
|
|
9363
|
+
else if (briefGap) parts.push(t('📋 project-brief 미작성', '📋 project-brief unfilled'));
|
|
9364
|
+
else if (planGap) parts.push(t('📋 plan Goal 미작성', '📋 plan Goal unfilled'));
|
|
9365
|
+
} catch {}
|
|
9328
9366
|
// 3) MCP 활동 누적
|
|
9329
9367
|
try {
|
|
9330
9368
|
const stats = _readUsageStats(root);
|
|
@@ -14985,11 +15023,21 @@ function releaseCleanupCmd(root) {
|
|
|
14985
15023
|
});
|
|
14986
15024
|
const recent = mergedBranches.slice(0, keep);
|
|
14987
15025
|
const toDelete = mergedBranches.slice(keep).filter(b => b !== currentBranch);
|
|
15026
|
+
// 1.36.20 (codex fresh-QA #7): 삭제(mutation)를 출력(json/human) "전"에 수행. 종전엔 --json 분기가 삭제 전 return 해
|
|
15027
|
+
// `release cleanup --apply --json` 이 apply:true·deleteCount N 을 보고하고도 실제로 아무것도 안 지우던 silent no-op.
|
|
15028
|
+
let deleted = 0, deleteFailed = 0;
|
|
15029
|
+
if (apply) {
|
|
15030
|
+
for (const b of toDelete) {
|
|
15031
|
+
const r = cp.spawnSync('git', ['branch', '-d', b], { cwd: root, encoding: 'utf8' });
|
|
15032
|
+
if (r.status === 0) deleted++; else deleteFailed++;
|
|
15033
|
+
}
|
|
15034
|
+
if (deleteFailed > 0) process.exitCode = 1; // 실패 시 nonzero exit
|
|
15035
|
+
}
|
|
14988
15036
|
// 5) JSON
|
|
14989
15037
|
if (has('--json')) {
|
|
14990
15038
|
log(JSON.stringify({
|
|
14991
15039
|
apply, keep, total: allBranches.length, merged: mergedBranches.length,
|
|
14992
|
-
unmerged: unmergedBranches.length, deleteCount: toDelete.length,
|
|
15040
|
+
unmerged: unmergedBranches.length, deleteCount: toDelete.length, deleted, deleteFailed,
|
|
14993
15041
|
toDelete, recent: recent.slice(0, keep), unmergedSample: unmergedBranches.slice(0, 5)
|
|
14994
15042
|
}, null, 2));
|
|
14995
15043
|
return;
|
|
@@ -15020,13 +15068,7 @@ function releaseCleanupCmd(root) {
|
|
|
15020
15068
|
if (recent.length > 5) log(dm(` ... +${recent.length - 5}개`));
|
|
15021
15069
|
log('');
|
|
15022
15070
|
if (apply) {
|
|
15023
|
-
|
|
15024
|
-
for (const b of toDelete) {
|
|
15025
|
-
const r = cp.spawnSync('git', ['branch', '-d', b], { cwd: root, encoding: 'utf8' });
|
|
15026
|
-
if (r.status === 0) ok++;
|
|
15027
|
-
else fail++;
|
|
15028
|
-
}
|
|
15029
|
-
log(gr(` ✓ 삭제 완료: ${ok}/${toDelete.length}건`) + (fail > 0 ? rd(` · 실패 ${fail}`) : ''));
|
|
15071
|
+
log(gr(` ✓ 삭제 완료: ${deleted}/${toDelete.length}건`) + (deleteFailed > 0 ? rd(` · 실패 ${deleteFailed}`) : '')); // 1.36.20: 삭제는 출력 전 수행됨(#7)
|
|
15030
15072
|
} else {
|
|
15031
15073
|
log(dm(` → 적용: leerness release cleanup --apply (안전: merged만 삭제, 현재 branch 보호)`));
|
|
15032
15074
|
log(dm(` → --keep N: 최근 N개 유지 (default 5)`));
|
|
@@ -20275,37 +20317,39 @@ function reuseAutodetectCmd(root) {
|
|
|
20275
20317
|
}
|
|
20276
20318
|
}
|
|
20277
20319
|
}
|
|
20278
|
-
|
|
20279
|
-
|
|
20280
|
-
|
|
20281
|
-
|
|
20282
|
-
log(`# leerness reuse autodetect (1.9.35)`);
|
|
20283
|
-
log(`project: ${path.basename(root)}`);
|
|
20284
|
-
log(`발견된 capability 후보: ${found.length}건`);
|
|
20285
|
-
log('');
|
|
20286
|
-
log('| Capability | Where | Kind | Note |');
|
|
20287
|
-
log('|---|---|---|---|');
|
|
20288
|
-
for (const c of found) log(`| ${c.name} | ${c.file} | util | (autodetect from module.exports) |`);
|
|
20289
|
-
log('');
|
|
20320
|
+
// 1.36.20 (codex fresh-QA #7): --apply mutation 을 출력(human/json) "전"에 수행. 종전엔 --json 분기가 --apply 앞에서
|
|
20321
|
+
// return 해 `reuse autodetect --apply --json` 이 후보만 보고하고 실제 reuse-map 을 안 쓰던 silent no-op. 이제 먼저 mutate →
|
|
20322
|
+
// applied/added 를 human/json 양쪽에 반영. 에러도 --json 시 구조화(failJson).
|
|
20323
|
+
let applied = false, added = 0;
|
|
20290
20324
|
if (has('--apply')) {
|
|
20291
|
-
// reuse-map.md에 추가 (헤더 보존 + 후보 라인 append)
|
|
20292
20325
|
const reusePath = path.join(root, '.harness', 'reuse-map.md');
|
|
20293
20326
|
if (!exists(reusePath)) {
|
|
20294
|
-
|
|
20295
|
-
return process.exit(1);
|
|
20327
|
+
failJson(has('--json'), 'no_reuse_map', `.harness/reuse-map.md 없음 — leerness init 먼저 실행`);
|
|
20328
|
+
return process.exit(process.exitCode || 1);
|
|
20296
20329
|
}
|
|
20297
20330
|
let body = read(reusePath);
|
|
20298
|
-
let added = 0;
|
|
20299
20331
|
for (const c of found) {
|
|
20300
20332
|
if (body.includes(`| ${c.name} |`)) continue; // 이미 있음
|
|
20301
20333
|
body += `| ${c.name} | ${c.file} | util | autodetect 1.9.35 |\n`;
|
|
20302
20334
|
added++;
|
|
20303
20335
|
}
|
|
20304
20336
|
writeUtf8(reusePath, body);
|
|
20305
|
-
|
|
20306
|
-
} else {
|
|
20307
|
-
log(`(--apply 로 reuse-map.md에 자동 추가)`);
|
|
20337
|
+
applied = true;
|
|
20308
20338
|
}
|
|
20339
|
+
if (has('--json')) {
|
|
20340
|
+
log(JSON.stringify({ project: path.basename(root), found, applied, added }, null, 2));
|
|
20341
|
+
return;
|
|
20342
|
+
}
|
|
20343
|
+
log(`# leerness reuse autodetect (1.9.35)`);
|
|
20344
|
+
log(`project: ${path.basename(root)}`);
|
|
20345
|
+
log(`발견된 capability 후보: ${found.length}건`);
|
|
20346
|
+
log('');
|
|
20347
|
+
log('| Capability | Where | Kind | Note |');
|
|
20348
|
+
log('|---|---|---|---|');
|
|
20349
|
+
for (const c of found) log(`| ${c.name} | ${c.file} | util | (autodetect from module.exports) |`);
|
|
20350
|
+
log('');
|
|
20351
|
+
if (applied) log(`✓ ${added}건 reuse-map.md에 추가됨`);
|
|
20352
|
+
else log(`(--apply 로 reuse-map.md에 자동 추가)`);
|
|
20309
20353
|
}
|
|
20310
20354
|
|
|
20311
20355
|
// 1.9.165: leerness web — playwright bridge MVP (opt-in 의존성, 5능력 #1 보강)
|
package/lib/audit.js
CHANGED
|
@@ -7,6 +7,7 @@ const path = require('path');
|
|
|
7
7
|
const { log, ok, warn, fail, failJson, today, now, absRoot, exists, read, readBuf, mkdirp, writeUtf8, append, rel } = require('./io');
|
|
8
8
|
const { SECRET_PATTERNS } = require('./catalogs');
|
|
9
9
|
const { findCorruptedStateJson } = require('./state-integrity'); // 1.36.1 (클린룸 리뷰 FN): 상태 JSON 무결성
|
|
10
|
+
const { _briefUnfilled, _planGoalUnfilled } = require('./pure-utils'); // 1.36.19: 전략 앵커(project-brief/plan Goal) 미작성 감지
|
|
10
11
|
|
|
11
12
|
function audit(root, opts = {}, deps = {}) {
|
|
12
13
|
const { VERSION, arg, has, planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills, _collectSecretFindings } = deps;
|
|
@@ -61,7 +62,15 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
61
62
|
if (reuse.includes(naMarker)) ok('reuse-map.md marked NA (skipped)');
|
|
62
63
|
else if (reuseLines === 0) { warnings++; warn('reuse-map.md is empty (consider populating known reusable elements)'); _finding('reuse_map_empty', 'warn', 'reuse-map.md is empty'); }
|
|
63
64
|
else ok(`reuse-map.md has ${reuseLines} entries`);
|
|
65
|
+
// 1.36.19 (실사용 7 프로젝트 dogfood): 전략 앵커 미작성 — 동적 상태는 유지되나 project-brief Purpose(5/7)·plan Goal(7/7)이
|
|
66
|
+
// 템플릿 그대로라 인계받는 AI 가 "프로젝트가 무엇인지/범위"를 못 받음(관찰된 근본원인). naMarker 로 의도적 스킵 가능.
|
|
67
|
+
const briefFile = path.join(root, '.harness/project-brief.md');
|
|
68
|
+
const briefTxt = exists(briefFile) ? read(briefFile) : '';
|
|
69
|
+
if (briefTxt.includes(naMarker)) ok('project-brief.md marked NA (skipped)');
|
|
70
|
+
else if (_briefUnfilled(briefTxt)) { warnings++; warn('project-brief.md Purpose 미작성(템플릿 그대로) — 인계받는 AI가 프로젝트 목적/맥락을 못 받습니다 (project-brief.md 채우기)'); _finding('project_brief_unfilled', 'warn', 'project-brief.md Purpose still placeholder/empty — AI handoffs lack project context'); }
|
|
71
|
+
else ok('project-brief.md Purpose populated');
|
|
64
72
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
73
|
+
if (planText && !planText.includes(naMarker) && _planGoalUnfilled(planText)) { warnings++; warn('plan.md Goal 미작성(템플릿 그대로) — 프로젝트 목표/범위가 비어 인계 AI가 방향을 못 잡습니다 (plan.md Goal/Scope 채우기)'); _finding('plan_goal_unfilled', 'warn', 'plan.md Goal still placeholder/empty'); }
|
|
65
74
|
const milestoneIds = Array.from(planText.matchAll(/^### (M-\d{4,})\./gm)).map(m => m[1]);
|
|
66
75
|
const rows = readProgressRows(root);
|
|
67
76
|
// 1.9.6 수정: 한 row에 여러 plan:M-XXXX 링크가 있어도 모두 인식 (matchAll로 전부 추출)
|
package/lib/pure-utils.js
CHANGED
|
@@ -1043,8 +1043,33 @@ function _renderGlossaryMd(entries, opts = {}) {
|
|
|
1043
1043
|
return s + GLOSSARY_END + '\n';
|
|
1044
1044
|
}
|
|
1045
1045
|
|
|
1046
|
+
// 1.36.19 (실사용 7 프로젝트 dogfood): 전략 앵커(project-brief Purpose / plan Goal) 미작성 감지.
|
|
1047
|
+
// 현장 관찰 — 동적 상태(current-state/decisions)는 7/7 유지되나 정체성 앵커는 brief 5/7·plan 7/7 이 템플릿 placeholder
|
|
1048
|
+
// 그대로 방치됨(27개 결정 쌓은 프로젝트조차 brief 빈칸). 인계받은 AI 가 "프로젝트가 무엇인지"를 못 받는 근본원인 →
|
|
1049
|
+
// audit/handoff 가 표면화하도록 순수 detector. bullet/문단 무관, 빈 섹션·placeholder 문구를 모두 미작성으로 판정.
|
|
1050
|
+
// 섹션 자체가 없으면(구조 미상) 미플래그(FP 회피). `<!-- leerness:na -->` 스킵은 캘러(audit)가 처리.
|
|
1051
|
+
function _mdSectionBody(content, heading) {
|
|
1052
|
+
const lines = String(content || '').split('\n');
|
|
1053
|
+
const h = new RegExp('^##\\s*' + heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*$', 'i');
|
|
1054
|
+
const i = lines.findIndex(l => h.test(l));
|
|
1055
|
+
if (i < 0) return null;
|
|
1056
|
+
const body = [];
|
|
1057
|
+
for (let j = i + 1; j < lines.length; j++) { if (/^##\s/.test(lines[j])) break; body.push(lines[j]); }
|
|
1058
|
+
return body.join('\n');
|
|
1059
|
+
}
|
|
1060
|
+
const _BRIEF_PLACEHOLDER_RE = /실제\s*(?:내용|목적)\s*으로\s*업데이트|describe .*purpose here|update .*with (?:the )?real/i;
|
|
1061
|
+
const _PLAN_GOAL_PLACEHOLDER_RE = /전체\s*계획을\s*유지합니다|사용자\s*목적을\s*기준으로|state your goal here|maintain the overall plan/i;
|
|
1062
|
+
function _sectionUnfilled(content, heading, placeholderRe) {
|
|
1063
|
+
const body = _mdSectionBody(content, heading);
|
|
1064
|
+
if (body == null) return false; // 섹션 없음 → 미플래그
|
|
1065
|
+
if (placeholderRe.test(body)) return true; // 템플릿 문구 그대로
|
|
1066
|
+
return !body.split('\n').some(l => l.replace(/^\s*[-*]\s*/, '').trim().length > 0); // 실내용(bullet/문단) 0 → 빈칸
|
|
1067
|
+
}
|
|
1068
|
+
function _briefUnfilled(content) { return _sectionUnfilled(content, 'Purpose', _BRIEF_PLACEHOLDER_RE); }
|
|
1069
|
+
function _planGoalUnfilled(content) { return _sectionUnfilled(content, 'Goal', _PLAN_GOAL_PLACEHOLDER_RE); }
|
|
1070
|
+
|
|
1046
1071
|
module.exports = {
|
|
1047
|
-
_parseImplExports,
|
|
1072
|
+
_parseImplExports, _briefUnfilled, _planGoalUnfilled, _mdSectionBody,
|
|
1048
1073
|
_matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd, GLOSSARY_START, GLOSSARY_END,
|
|
1049
1074
|
_isSecretKey, compareVer, parseHarnessVersion,
|
|
1050
1075
|
_isPlaceholderSecret, _looksSecretLike,
|