leerness 1.35.16 → 1.36.2
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 +68 -0
- package/README.md +4 -4
- package/bin/leerness.js +69 -6
- package/lib/audit.js +28 -9
- package/lib/feature.js +4 -1
- package/lib/health.js +14 -0
- package/lib/pure-utils.js +2 -2
- package/lib/state-integrity.js +38 -0
- package/package.json +1 -1
- package/scripts/e2e-core.js +95 -0
- package/scripts/e2e.js +73 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.2 — 2026-07-07 — 클린룸 후속: `feature add|show|link|impact` trailing positional path 존중 (조용한 cwd 오독 + stray `.harness` scaffold 차단)
|
|
4
|
+
|
|
5
|
+
**독립 클린룸 리뷰(1.35.17 게시본)가 재현한 P2 결함 소진.** 비-프로젝트 디렉토리에서 `leerness feature add "이름" <프로젝트경로>` 를 실행하면 성공 메시지는 프로젝트 경로를 되울리지만(→ 처리된 척), 실제 feature 는 **CWD** 에 떨어지고 그 자리에 **stray `.harness` 가 조용히 scaffold** 됐다. 즉 지정한 프로젝트가 아니라 아무 폴더나 오염시켰다. `--path <project>` 는 정상 동작했다.
|
|
6
|
+
|
|
7
|
+
### 원인
|
|
8
|
+
- 디스패치(`bin/leerness.js`)가 `feature add` 를 `featureAddCmd(arg('--path', cwd), args.slice(2).filter(비-flag).join(' '))` 로 라우팅 — **모든 비-flag positional 이 NAME 으로 join** 되어 경로가 이름에 흡수되고 root 는 언제나 CWD. `feature list` 는 1.9.412(UR-0100)에서 `_resolveRoot(args[2])` 로 positional-path 를 이미 지원했지만 `add/show/link/impact` 는 갱신되지 않았다.
|
|
9
|
+
- `featureAddCmd` 의 `_ensureFeatureGraph(root)` 가 **무조건** `.harness/feature-graph.md` 를 생성 → 미초기화 폴더도 조용히 반쪽 상태로 오염.
|
|
10
|
+
|
|
11
|
+
### 수정 — 두 겹 방어 (a) positional path 인식 (b) 미초기화 게이트
|
|
12
|
+
- **(a) trailing positional path 존중** — `add/show/link/impact` 모두 `--path > path-like positional(_taskPositionalPath) > cwd` 로 root 해석(기존 `task`/`rule add`/`feature list` 와 동일 규약). `add` 의 NAME 은 `_parseAddTitle(args, 2)` 로 첫 경로/플래그에서 절단해 경로 흡수를 차단(내부 슬래시 제목 `auth/login` 등은 선행 구분자만 경로로 보므로 보호).
|
|
13
|
+
- **(b) 미초기화 scaffold 게이트** — `featureAddCmd` 가 `_ensureFeatureGraph` 전에 `_requireInit(root, 'feature add')` 통과를 요구(=`task add` 와 동일). 워크스페이스 마커(HARNESS_VERSION/guideline.md/AGENTS.md) 없는 디렉토리는 **scaffold 대신 exit 1 + 안내**, `--force` 우회. `show/link/impact` 는 read-only(그래프 없으면 not-found/빈결과 — scaffold 안 함).
|
|
14
|
+
- **`_taskPositionalPath` 값-플래그 확장** — feature 값-플래그(`--files`/`--depends-on`/`--affects`/`--co-changes-with`)의 값이 path-like(`./src/x.js`)여도 root 로 오인하지 않도록 `_TASK_VALUE_FLAGS` 에 포함(예: `feature add "이름" --files ./src/a.js` 에서 `./src/a.js` 는 files 값이지 경로 아님).
|
|
15
|
+
|
|
16
|
+
### 검증
|
|
17
|
+
- selftest **275**(+1: `_featRoot`/`_parseAddTitle` 디스패치 배선 + `_requireInit` 게이트 + `_taskPositionalPath` 값-플래그 skip; split-literal 앵커로 자기참조 false-pass 회피). e2e-core **+5**(positional 등록·이름 clean / stray scaffold 없음 / 미초기화 게이트 / show positional / `--path` 우선). full e2e **+1 회귀 블록**(add/show/link/impact positional + 미초기화 게이트 + `--files` path-like 값 오인 없음). 클린룸 재현(비-프로젝트 cwd)으로 fix 전/후 대조 확정 → 게시본 재실증.
|
|
18
|
+
|
|
19
|
+
## 1.36.1 — 2026-07-07 — 클린룸 후속: 상태파일 JSON 무결성 체크 (health/check/audit false-negative 차단)
|
|
20
|
+
|
|
21
|
+
**1.36.0 클린룸 리뷰가 남긴 후속 백로그 소진** — 독립 리뷰어 A(P2, fails-safe)가 재현한 false-negative: `.harness/manifest.json` 을 깨진 JSON(`{ this is : not valid json ]]]`)으로 덮어써도 `health`/`doctor`/`check`/`audit` 가 전부 **"healthy" / exit 0** 을 반환. 원인은 모든 상태파일 로더가 `try{JSON.parse}catch{빈 상태}` 로 그레이스풀 폴백해(=크래시 없음, 좋은 성질) **손상을 조용히 빈 상태로 대체**하는데, 어떤 진단 명령도 상태 JSON 무결성을 별도로 검증하지 않던 갭.
|
|
22
|
+
|
|
23
|
+
### 수정 — `.harness/*.json` JSON 무결성 검증 (신규 공유 헬퍼 `lib/state-integrity.js`)
|
|
24
|
+
- **단일출처 `findCorruptedStateJson(root)`** — `.harness` 바로 아래 `*.json`(manifest/decisions/lessons/skills-lock/active-wakeups/user-requests/…)을 열거해 `JSON.parse` 시도, 실패 파일 목록 반환. 비-재귀(archive/api-skills/cache 제외)·존재 파일만·빈/공백 파일 무오탐·BOM strip·**비-크래시**(파서 예외 흡수).
|
|
25
|
+
- **3 진단 표면에 배선** (심각도는 표면 성격에 맞춤):
|
|
26
|
+
- **`audit`** — `corrupted_state_json` finding(**warning**, `--strict` 시 failure 승격).
|
|
27
|
+
- **`health`** — `checks.stateIntegrity` + `issues` → `healthy:false`(**degraded 신호**, `--strict` 시 exit 1).
|
|
28
|
+
- **`check`** — pre-action 게이트이므로 **하드 실패(exit 1)**. 손상 상태를 통과시키지 않음.
|
|
29
|
+
- **`doctor` 는 무변경** — root 를 받지 않는 설치/환경 진단(selftest)이라 워크스페이스 상태 검증 범위 밖(설계상 분리).
|
|
30
|
+
|
|
31
|
+
### 검증
|
|
32
|
+
- selftest **274**(+1: `findCorruptedStateJson` 탐지/무오탐/공유 배선 직접 검증). e2e-core **37**(+6: 클린 무오탐 + 손상 manifest → audit/health/check 3표면 표면화). full e2e **385**(+1 회귀 블록: 클린 무오탐 + 손상 표면화 + `audit --strict` 승격). 재현 확정 → 게시본 재실증.
|
|
33
|
+
|
|
34
|
+
## 1.36.0 — 2026-07-07 — 🏁 안정 minor: gate 표면 전체 적대적 헌트 완주 (1.35.14~1.35.17 통합) + 독립 클린룸 검증
|
|
35
|
+
|
|
36
|
+
**codex 협업 7라운드로 gate 임베드 플래그십의 모든 체크 표면을 적대적 헌트 완주한 안정 릴리스**, 마지막으로 내 프레이밍 없는 **독립 블라인드 리뷰어 2명**으로 게시본(1.35.17)을 교차검증. 각 헌트 라운드 = 자체 프로브 + codex(gpt-5.5 xhigh) 독립 헌터 → 맹신 X 재현으로 확정만 수정 → FP-safe → 양-레벨 회귀가드(selftest+e2e-core) → 게시본 재실증.
|
|
37
|
+
|
|
38
|
+
### 이 minor 에 묶인 gate 표면 sweep (전부 이미 patch 배포됨 — 안정 marker + 통합)
|
|
39
|
+
- **scan secrets (1.35.14):** 복합키(`DB_PASSWORD=`)/JSON 따옴표키(`"password":"x"`)/Django `SECRET_KEY`/개인키 변종 미탐 + 로컬 DB 기본자격·사전-단어 FP 억제.
|
|
40
|
+
- **encoding check (1.35.15):** 🔴 파괴적 `--apply` 가 CP949 파일에 BOM 만 붙여 손상시키던 것 차단(유효-UTF-8 만 BOM) + 순수-ASCII `.bat` FP + `.cmd` 미스캔 FN + BOM+깨진본문 은폐 FN.
|
|
41
|
+
- **lazy detect (1.35.16):** 안티-치트 우회 3종(status=completed evidence 회피, 아포스트로피/멀티라인 TODO 오판, 쓰레기 evidence) + 비-JS 테스트러너 FP.
|
|
42
|
+
- **audit (1.35.17):** 유저-프로젝트 FP 4종(`readme_synced` category error, `.env*` glob 무지, "rest" 오인, threshold-0 footgun).
|
|
43
|
+
|
|
44
|
+
### 독립 클린룸 리뷰 반영 (블라인드 리뷰어 2명, 게시본 1.35.17 → 맹신 X 재현)
|
|
45
|
+
- **`--json` 성공 경로 무결성 (리뷰어 B, P2).** `task update`/`plan add`/`rule verify`/`reuse find` 가 `--json` 이어도 **성공 시 평문** 출력(에러 경로는 이미 구조화) → JSON 소비 스크립트가 성공 경로에서만 크래시. `task add`(1.9.413) 와 동일 emitter 로 4곳 보강. 재현 확정.
|
|
46
|
+
- **`migrate --force` 문구 정직화 (리뷰어 A, P2).** `--guide` 가 `--force` 를 "비파괴, 기존 내용 보존"이라 표기했으나 실제로는 관리 `.md` 를 템플릿으로 **교체**(기존 내용은 `.harness/archive` 백업, in-place 보존 아님) → 문구를 정직하게 수정(커스텀 보존은 `--force` 없이 migrate/`update --yes` 안내). 재현 확정.
|
|
47
|
+
|
|
48
|
+
### 클린룸 발견 — 1.36.1 후속 백로그 (재현 확정했으나 behavior 변경이라 안정 릴리스에 미포함)
|
|
49
|
+
- **`feature add|show|link|impact` 가 positional `[path]` 무시 + CWD 에 stray `.harness` 스캐폴딩 (리뷰어 A, P2).** name 이 positional 이라 경로가 name 으로 흡수됨(`feature list` 는 1.9.412 로 이미 positional path 지원). arg-parsing 변경이라 focused 후속에서 처리.
|
|
50
|
+
- **`health`/`doctor`/`check` 가 손상된 `.harness/*.json` 에도 healthy 보고 (리뷰어 A, P2, fails-safe).** 상태파일 JSON 무결성 미검증(크래시는 없음 — graceful) → 진단 명령에 무결성 체크 추가는 신규 기능이라 후속.
|
|
51
|
+
|
|
52
|
+
### 검증
|
|
53
|
+
- selftest 273. e2e-core **31**(플래그십 행위 + 클린룸 회귀). full e2e 384. 독립 리뷰어 2명이 확증: 에러경로 --json 무결성·exit code·injection·비파괴 마이그레이션(--force 제외)·team deploy 이중게이트 모두 **PASS**. 게시본 1.36.0 재실증. **R-0006 안정 marker.**
|
|
54
|
+
|
|
55
|
+
## 1.35.17 — 2026-07-07 — codex 협업 라운드 ⑦: audit 적대적 헌트 — 유저-프로젝트 FP 4종 (gate 표면 sweep 완결)
|
|
56
|
+
|
|
57
|
+
**사용자 지시 "codex와 협업" 라운드 ⑦ — gate 임베드 마지막 미헌트 표면 `audit`.** audit 은 유저의 실제 프로젝트를 점검하므로 FP 가 `--strict` gate 를 거짓 실패시킴. 자체 프로브 + codex(gpt-5.5 xhigh) 교차. 재현으로 확정한 유저-프로젝트 FP 4종 수정. **이로써 gate 5체크(verify/audit/scan secrets/encoding/lazy detect) + gate 구성 + contract 전 표면 적대적 헌트 완주.**
|
|
58
|
+
|
|
59
|
+
### 확정 수정 (전부 유저-프로젝트 FP)
|
|
60
|
+
- **`readme_synced_version_stale` 카테고리 오류.** README 관리블록의 "Last synced by Leerness vX" 는 readme sync 가 **leerness 도구 버전**(`v${VERSION}`)으로 쓰는데, audit 은 이를 **프로젝트의 package.json 버전**과 비교 → 도구≠프로젝트 버전인 **모든 유저 프로젝트에서 항상 오탐** + `--fix` 가 pkg.version 을 써 다음 readme sync(VERSION 기록)와 영구 충돌. → **현재 실행 중 leerness VERSION 과 비교**(= "오래된 leerness 로 sync 됨 → 재sync" 정확 감지, leerness 자기 repo 는 VERSION==pkg 라 무변화). 재현 확정.
|
|
61
|
+
- **`gitignore_missing_secrets` glob 무지 (codex #11).** 필수 시크릿 패턴(`.env`/`.env.local`/…)을 **정확-일치**로만 검사 → 흔한 `.env*`/`.env.*` 광역 패턴(git 이 실제로 .env 패밀리 전체 ignore)을 쓰면 "누락" 오탐(`--strict` 시 failure). trailing-star prefix 커버리지 인식 추가(git 동작 일치, 신규 FN 0). 재현 확정.
|
|
62
|
+
- **`api_skill_missing` 이 영어 단어 "rest" 오인 (codex #15).** 키워드 정규식이 `/…|REST|…/i` 라 대소문자 무관 → **"clean up the rest of the module"** 같은 평범한 task 를 REST API 로 오판. API/REST 약어를 대소문자-민감으로 분리(서술형 endpoint/graphql/oauth/webhook/url 은 무관 유지). 재현 확정.
|
|
63
|
+
- **`strict_promoted` threshold 0 footgun (codex #12).** `--strict --threshold 0`(또는 음수)이면 `warnings>=0` 이 항상 참 → **경고 0인 clean 프로젝트도 실패**. `warnings>0 &&` 가드 추가.
|
|
64
|
+
|
|
65
|
+
### 맹신 X / 미채택
|
|
66
|
+
- codex "malformed package.json crash" → **반증**: 해당 블록이 `try{…JSON.parse…}catch{}` 로 감싸져 크래시 아님(조용히 skip). badge 다중매칭(#6)/design-system 템플릿 포맷(#5)/npm_cve --strict 노이즈(#4)/env local-only(#11)/milestone future(#10)/current-state 타임존(#7)은 저빈도·by-design·--fix 상호작용 복잡으로 미채택(문서화).
|
|
67
|
+
|
|
68
|
+
### 검증
|
|
69
|
+
- selftest 273. e2e-core **27**(신규 audit 픽스처 3: readme_synced/gitignore-glob/rest-word). full e2e **384**. 게시본 1.35.17 재실증.
|
|
70
|
+
|
|
3
71
|
## 1.35.16 — 2026-07-07 — codex 협업 라운드 ⑥: lazy detect(안티-치트) 적대적 헌트 — 우회/FP/FN 5종 수정
|
|
4
72
|
|
|
5
73
|
**사용자 지시 "codex와 협업" 라운드 ⑥ — gate 임베드 `lazy detect`(안티-게으름/안티-치트) 적대적 헌트.** 이 체크의 주 사용자는 AI 에이전트 자신이라 **우회 인센티브가 직접적**. 자체 프로브 + codex(gpt-5.5 xhigh) 독립 헌터로 바이패스(FN)+FP 양방향 사냥. 재현으로 확정한 것만 수정.
|
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.
|
|
128
|
+
이 프로젝트는 Leerness v1.36.2 하네스를 사용합니다. 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.
|
|
182
|
+
Leerness v1.36.2는 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.35.16는 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.
|
|
203
|
+
현재 누적: **70 라운드 (1.9.40 → 1.36.2)** · 매 라운드 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.
|
|
241
|
+
Last synced by Leerness v1.36.2: 2026-07-07
|
|
242
242
|
<!-- leerness:project-readme:end -->
|
|
243
243
|
|
package/bin/leerness.js
CHANGED
|
@@ -31,8 +31,9 @@ const { _isSecretKey, _isPlaceholderSecret, _looksSecretLike, _mergeLines, _merg
|
|
|
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
|
+
const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
|
|
34
35
|
|
|
35
|
-
const VERSION = '1.
|
|
36
|
+
const VERSION = '1.36.2';
|
|
36
37
|
|
|
37
38
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
38
39
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -3193,6 +3194,24 @@ function _selfTestCases() {
|
|
|
3193
3194
|
const u = m._parseAddTitle(['rule', 'add', '세션', '점검', '--trigger', 'every-session', '/p'], 2) === '세션 점검';
|
|
3194
3195
|
return wired && u;
|
|
3195
3196
|
} },
|
|
3197
|
+
{ name: '클린룸 (UR-0184): feature add/show/link/impact positional-path 와이어 + 미초기화 게이트 + _taskPositionalPath 값-플래그 skip (1.36.2)', run: () => {
|
|
3198
|
+
const src = read(__filename);
|
|
3199
|
+
// split-literal 앵커 — selftest 자기참조 false-pass 회피(자기참조 트랩); 실제 디스패치/주입에만 매칭
|
|
3200
|
+
const featRootDef = 'const _feat' + 'Root = () => absRoot(';
|
|
3201
|
+
const addWire = 'featureAddCmd(_feat' + 'Root(), _parseAddTitle(args, 2))';
|
|
3202
|
+
const showWire = 'featureShowCmd(_feat' + 'Root(), args[2])';
|
|
3203
|
+
const depInject = 'arg, has, _require' + 'Init }'; // _featureDeps 에 _requireInit 주입
|
|
3204
|
+
const wired = src.includes(featRootDef) && src.includes(addWire) && src.includes(showWire) && src.includes(depInject);
|
|
3205
|
+
// lib/feature.js: featureAddCmd 가 미초기화 dir scaffold 대신 _requireInit 게이트
|
|
3206
|
+
const featSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'feature.js'));
|
|
3207
|
+
const gateWired = featSrc.includes('_require' + "Init(root, 'feature add')");
|
|
3208
|
+
// 순수 동작: _taskPositionalPath 가 feature 값-플래그(--files 등)의 path-like 값을 root 로 오인 안 함 + 실제 경로/드라이브만 추출(id 는 제외)
|
|
3209
|
+
const m = require('../lib/pure-utils');
|
|
3210
|
+
const p1 = m._taskPositionalPath(['feature', 'add', 'Name', '--files', './src/x.js'], 2) === null;
|
|
3211
|
+
const p2 = m._taskPositionalPath(['feature', 'add', 'Name', '/proj'], 2) === '/proj';
|
|
3212
|
+
const p3 = m._taskPositionalPath(['feature', 'show', 'F-0001', 'C:\\proj'], 2) === 'C:\\proj';
|
|
3213
|
+
return wired && gateWired && p1 && p2 && p3;
|
|
3214
|
+
} },
|
|
3196
3215
|
{ name: 'UR-0025 큰핸들러 모듈화 8번째: healthCmd → lib/health.js + DI 위임 + 동작 (1.9.423)', run: () => {
|
|
3197
3216
|
const m = require('../lib/health');
|
|
3198
3217
|
const expOk = typeof m.healthCmd === 'function';
|
|
@@ -4081,6 +4100,26 @@ function _selfTestCases() {
|
|
|
4081
4100
|
const hasAdopt = bin.includes("'ado" + "pt'") && bin.includes('parent ado' + 'pt --apply');
|
|
4082
4101
|
const nonDestructive = bin.includes('inherited-from-pa' + 'rent.md') && bin.includes('PARENT_LI' + 'NK.json');
|
|
4083
4102
|
return hasAdopt && nonDestructive;
|
|
4103
|
+
} },
|
|
4104
|
+
{ name: 'lib/state-integrity: findCorruptedStateJson 손상 JSON 탐지 + 유효/빈파일 무오탐 + audit/health 공유 (클린룸 리뷰 FN 1.36.1)', run: () => {
|
|
4105
|
+
const m = require('../lib/state-integrity');
|
|
4106
|
+
if (typeof m.findCorruptedStateJson !== 'function') return false;
|
|
4107
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_si_'));
|
|
4108
|
+
try {
|
|
4109
|
+
const hd = path.join(tmp, '.harness'); fs.mkdirSync(hd, { recursive: true });
|
|
4110
|
+
fs.writeFileSync(path.join(hd, 'valid.json'), '{"a":1}');
|
|
4111
|
+
fs.writeFileSync(path.join(hd, 'empty.json'), ''); // 빈 파일 = 그레이스풀 빈 상태(무오탐)
|
|
4112
|
+
fs.writeFileSync(path.join(hd, 'manifest.json'), '{ bad : json ]]]');
|
|
4113
|
+
const r = m.findCorruptedStateJson(tmp);
|
|
4114
|
+
const onlyBad = r.length === 1 && r[0].file === '.harness/manifest.json' && !!r[0].error;
|
|
4115
|
+
// .harness 부재 → 빈 배열(비-크래시)
|
|
4116
|
+
const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_si2_'));
|
|
4117
|
+
const noHarness = m.findCorruptedStateJson(emptyDir).length === 0;
|
|
4118
|
+
try { fs.rmSync(emptyDir, { recursive: true, force: true }); } catch {}
|
|
4119
|
+
// audit/health 가 헬퍼를 require 로 공유(DI 아님 — 래퍼 시그니처 무변경)
|
|
4120
|
+
const wired = read(path.join(__dirname, '..', 'lib', 'audit.js')).includes("require('./state-integrity')") && read(path.join(__dirname, '..', 'lib', 'health.js')).includes("require('./state-integrity')");
|
|
4121
|
+
return onlyBad && noHarness && wired;
|
|
4122
|
+
} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
4084
4123
|
} }
|
|
4085
4124
|
];
|
|
4086
4125
|
}
|
|
@@ -7339,6 +7378,8 @@ function planAdd(root, text) {
|
|
|
7339
7378
|
upsertProgress(root, { id: tid, status, request: text, evidence: `plan:${id}`, nextAction });
|
|
7340
7379
|
return { id, tid };
|
|
7341
7380
|
});
|
|
7381
|
+
// 1.36.0 (클린룸 리뷰 B, --json 무결성): plan add 성공 경로 --json 보강(에러 경로는 이미 구조화).
|
|
7382
|
+
if (has('--json')) { log(JSON.stringify({ ok: true, milestone: id, task: tid, text })); return; }
|
|
7342
7383
|
ok(`plan added: ${id} → progress: ${tid}`);
|
|
7343
7384
|
_autoRoadmap(absRoot(root), 'data-change');
|
|
7344
7385
|
}
|
|
@@ -7593,6 +7634,8 @@ function taskUpdate(root, id) {
|
|
|
7593
7634
|
if (arg('--next') !== null) patch.nextAction = arg('--next');
|
|
7594
7635
|
if (arg('--note')) patch.request = arg('--note');
|
|
7595
7636
|
upsertProgress(root, patch);
|
|
7637
|
+
// 1.36.0 (클린룸 리뷰 B, --json 무결성): task add(1.9.413) 는 --json 성공 출력이 있으나 task update 는 누락 — 성공 시 평문만 내보내 JSON 소비 스크립트가 성공 경로에서만 크래시(에러 경로는 이미 구조화). 동일 패턴으로 보강.
|
|
7638
|
+
if (has('--json')) { log(JSON.stringify({ ok: true, ...patch })); return; }
|
|
7596
7639
|
ok(`task updated: ${id}`);
|
|
7597
7640
|
_autoRoadmap(absRoot(root), 'data-change');
|
|
7598
7641
|
}
|
|
@@ -8553,6 +8596,17 @@ function preCheck(root) {
|
|
|
8553
8596
|
const pfOk = pf.includes('AGENTS.md');
|
|
8554
8597
|
checks.push({ name: 'protected-files.md AGENTS.md', kind: 'integrity', ok: pfOk });
|
|
8555
8598
|
if (!pfOk) { issues++; if (!json) fail('protected-files.md missing AGENTS.md'); }
|
|
8599
|
+
// 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태파일 JSON 무결성 — 손상 시 하드 실패(exit 1). check 는 pre-action 게이트이므로 손상 상태를 통과시키지 않는다.
|
|
8600
|
+
// (audit=warning / health=degraded 와 달리 check 는 차단 신호. 비-크래시: 헬퍼가 파서 예외를 흡수.)
|
|
8601
|
+
try {
|
|
8602
|
+
const corrupted = _findCorruptedStateJson(root);
|
|
8603
|
+
if (corrupted.length) {
|
|
8604
|
+
for (const c of corrupted) { checks.push({ name: c.file, kind: 'json-integrity', ok: false, error: c.error }); issues++; if (!json) fail(`corrupted JSON: ${c.file} (${c.error})`); }
|
|
8605
|
+
} else {
|
|
8606
|
+
checks.push({ name: 'state JSON integrity', kind: 'json-integrity', ok: true });
|
|
8607
|
+
if (!json) ok('state JSON integrity OK (.harness/*.json)');
|
|
8608
|
+
}
|
|
8609
|
+
} catch {}
|
|
8556
8610
|
if (json) { log(JSON.stringify({ root, healthy: issues === 0, issues, checks }, null, 2)); if (issues) process.exitCode = 1; return; }
|
|
8557
8611
|
if (issues === 0) ok('pre-action check passed');
|
|
8558
8612
|
else { process.exitCode = 1; }
|
|
@@ -14302,6 +14356,8 @@ function verifyRules(root) {
|
|
|
14302
14356
|
function ruleVerifyCmd(root) {
|
|
14303
14357
|
root = absRoot(root);
|
|
14304
14358
|
const results = verifyRules(root);
|
|
14359
|
+
// 1.36.0 (클린룸 리뷰 B, --json 무결성): rule verify 성공 경로 --json 보강 — 기존엔 --json 이어도 평문 표/텍스트만 출력.
|
|
14360
|
+
if (has('--json')) { log(JSON.stringify({ ok: true, count: results.length, results })); return; }
|
|
14305
14361
|
if (!results.length) return ok('활성 룰 없음');
|
|
14306
14362
|
log('# Rules verification');
|
|
14307
14363
|
log('| ID | Trigger | Rule | Verified | Note |');
|
|
@@ -15116,7 +15172,7 @@ function _writeFeatureGraph(root, nodes) {
|
|
|
15116
15172
|
// 1.9.391 (UR-0025 큰 핸들러 모듈화 3번째): feature add/link/impact/list/show 핸들러를 lib/feature.js 로 분리.
|
|
15117
15173
|
// _featureImpactBfs 는 pure-utils 로 이동(handoff/audit 공유). feature-graph I/O 헬퍼(_readFeatureGraph 등)는 공유라 harness 유지+주입.
|
|
15118
15174
|
const _feature = require('../lib/feature');
|
|
15119
|
-
function _featureDeps() { return { _ensureFeatureGraph, _readFeatureGraph, _writeFeatureGraph, arg, has }; }
|
|
15175
|
+
function _featureDeps() { return { _ensureFeatureGraph, _readFeatureGraph, _writeFeatureGraph, arg, has, _requireInit }; } // 1.36.2: _requireInit 주입 — feature add 미초기화 dir scaffold 게이트
|
|
15120
15176
|
function featureAddCmd(root, title) { return _feature.featureAddCmd(root, title, _featureDeps()); }
|
|
15121
15177
|
function featureLinkCmd(root, fromId) { return _feature.featureLinkCmd(root, fromId, _featureDeps()); }
|
|
15122
15178
|
function featureImpactCmd(root, fromId) { return _feature.featureImpactCmd(root, fromId, _featureDeps()); }
|
|
@@ -15226,6 +15282,8 @@ function reuseFind(root, query) {
|
|
|
15226
15282
|
if (exportRe.test(lines[i])) matches.push({ source: rel(root, f), line: i + 1, text: lines[i].trim().slice(0, 120) });
|
|
15227
15283
|
}
|
|
15228
15284
|
}
|
|
15285
|
+
// 1.36.0 (클린룸 리뷰 B, --json 무결성): reuse find 성공 경로 --json 보강 — 기존엔 --json 이어도 사람용 텍스트만.
|
|
15286
|
+
if (has('--json')) { log(JSON.stringify({ ok: true, query, count: matches.length, matches: matches.slice(0, _parseLimit(arg('--limit', '20'), 20)) })); return; }
|
|
15229
15287
|
log(`# reuse find: "${query}"`);
|
|
15230
15288
|
if (!matches.length) return ok('기존 자원 없음 — 새로 만드는 것이 최선의 선택일 수 있음');
|
|
15231
15289
|
log(`${matches.length}개 후보:`);
|
|
@@ -20773,11 +20831,16 @@ async function main() {
|
|
|
20773
20831
|
if (cmd === 'release' && args[1] === 'channel') return releaseChannelCmd((args[2] && !args[2].startsWith('-')) ? args[2] : arg('--path', process.cwd()));
|
|
20774
20832
|
if (cmd === 'release' && args[1] === 'cadence') return releaseCadenceCmd((args[2] && !args[2].startsWith('-')) ? args[2] : arg('--path', process.cwd())); // 1.9.374 (UR-0074): 릴리스 빈도 진단
|
|
20775
20833
|
// 1.9.141: feature causality graph
|
|
20776
|
-
|
|
20777
|
-
|
|
20778
|
-
|
|
20834
|
+
// 1.36.2 (clean-room, UR-0184): add/show/link/impact 도 trailing positional path 인식 — list(1.9.412)만 지원하던 것을 일관 확대.
|
|
20835
|
+
// 기존엔 add 가 모든 non-flag positional 을 NAME 으로 join → 경로가 이름에 흡수 + cwd 에 stray .harness scaffold(조용한 오독).
|
|
20836
|
+
// 해석: --path > path-like positional(_taskPositionalPath) > cwd. add 의 NAME 은 _parseAddTitle 로 첫 경로/플래그에서 절단(경로 흡수 차단).
|
|
20837
|
+
// add 는 featureAddCmd 내부 _requireInit 게이트로 미초기화 dir scaffold 대신 에러(--force 우회). show/link/impact 는 read-only(scaffold 안 함).
|
|
20838
|
+
const _featRoot = () => absRoot(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd());
|
|
20839
|
+
if (cmd === 'feature' && args[1] === 'add') return featureAddCmd(_featRoot(), _parseAddTitle(args, 2));
|
|
20840
|
+
if (cmd === 'feature' && args[1] === 'link') return featureLinkCmd(_featRoot(), args[2]);
|
|
20841
|
+
if (cmd === 'feature' && args[1] === 'impact') return featureImpactCmd(_featRoot(), args[2]);
|
|
20779
20842
|
if (cmd === 'feature' && args[1] === 'list') return featureListCmd(absRoot(_resolveRoot(args[2]))); // 1.9.412 (UR-0100): positional path 지원(조용한 cwd 오독 방지)
|
|
20780
|
-
if (cmd === 'feature' && args[1] === 'show') return featureShowCmd(
|
|
20843
|
+
if (cmd === 'feature' && args[1] === 'show') return featureShowCmd(_featRoot(), args[2]);
|
|
20781
20844
|
if (cmd === 'impact') return impactCmd(arg('--path', process.cwd()), args[1]);
|
|
20782
20845
|
if (cmd === 'reuse' && args[1] === 'find') return reuseFind(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
|
|
20783
20846
|
if (cmd === 'reuse' && args[1] === 'register') return reuseRegister(arg('--path', process.cwd()), args[2]);
|
package/lib/audit.js
CHANGED
|
@@ -6,6 +6,7 @@ const cp = require('child_process');
|
|
|
6
6
|
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
|
+
const { findCorruptedStateJson } = require('./state-integrity'); // 1.36.1 (클린룸 리뷰 FN): 상태 JSON 무결성
|
|
9
10
|
|
|
10
11
|
function audit(root, opts = {}, deps = {}) {
|
|
11
12
|
const { VERSION, arg, has, planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills, _collectSecretFindings } = deps;
|
|
@@ -32,6 +33,19 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
32
33
|
process.exitCode = 1;
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
36
|
+
// 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태파일 JSON 무결성 — 깨진 JSON 을 그레이스풀 폴백(빈 상태)이 "healthy" 로 감추던 false-negative 차단.
|
|
37
|
+
// 손상 파일을 warning + corrupted_state_json finding 으로 표면화(--strict 시 failure 로 승격). 비-크래시(헬퍼가 파서 예외를 흡수).
|
|
38
|
+
try {
|
|
39
|
+
const corrupted = findCorruptedStateJson(root);
|
|
40
|
+
if (corrupted.length) {
|
|
41
|
+
warnings++;
|
|
42
|
+
warn(`상태파일 JSON 손상 ${corrupted.length}건: ${corrupted.map(c => c.file).join(', ')} (수동 복구 또는 leerness init 필요)`);
|
|
43
|
+
corrupted.slice(0, 6).forEach(c => log(` ${c.file}: ${c.error}`));
|
|
44
|
+
_finding('corrupted_state_json', 'warn', '.harness 상태파일 JSON 파싱 실패 (손상)', { count: corrupted.length, files: corrupted.map(c => c.file), sample: corrupted.slice(0, 10) });
|
|
45
|
+
} else {
|
|
46
|
+
ok('상태파일 JSON 무결성 OK (.harness/*.json)');
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
35
49
|
const designCands = ['designguide.md','design-guide.md','docs/designguide.md','docs/design-guide.md','.harness/designguide.md'];
|
|
36
50
|
const dups = designCands.filter(f => exists(path.join(root,f)));
|
|
37
51
|
if (dups.length) { warnings++; warn(`design guide duplicates outside canonical: ${dups.join(', ')} (run: leerness consistency merge-design-guide)`); _finding('design_dup', 'warn', 'design guide duplicates outside canonical', { duplicates: dups }); }
|
|
@@ -113,14 +127,14 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
113
127
|
}
|
|
114
128
|
}
|
|
115
129
|
// 1.18.4 (GPT-5.5 평가 #7, UR-0006): 배지뿐 아니라 관리블록의 "Last synced by Leerness vX" 도 검사.
|
|
116
|
-
//
|
|
130
|
+
// 1.35.17 (audit 헌트 FP): 이 라인은 readme sync 가 `Last synced by Leerness v${VERSION}`(leerness 도구 버전)으로 쓴다. 기존엔 이를 pkg.version(프로젝트 버전)과 비교해 유저 프로젝트마다(도구≠프로젝트 버전) 항상 오탐 + --fix 가 pkg.version 을 써 다음 readme sync 가 되돌리는 영구 충돌. → 현재 실행 중 leerness VERSION 과 비교(= "오래된 leerness 로 sync 됨 → 재sync" 를 정확히 감지, leerness 자기 repo 는 VERSION==pkg.version 이라 무변화). --fix 도 VERSION 으로 기록(readme sync 와 정합).
|
|
117
131
|
const sm = readmeText.match(/Last synced by Leerness v(\d+\.\d+\.\d+)/);
|
|
118
|
-
if (
|
|
132
|
+
if (VERSION && sm && sm[1] !== VERSION) {
|
|
119
133
|
warnings++;
|
|
120
|
-
warn(`README.md managed-block
|
|
121
|
-
_finding('readme_synced_version_stale', 'warn', 'README.md managed-block synced version
|
|
134
|
+
warn(`README.md managed-block synced by older Leerness: README=v${sm[1]} vs current=v${VERSION} (run: leerness readme sync)`);
|
|
135
|
+
_finding('readme_synced_version_stale', 'warn', 'README.md managed-block synced by older Leerness version', { readme: sm[1], leerness: VERSION });
|
|
122
136
|
if (fix) {
|
|
123
|
-
const updated2 = read(readmePath).replace(/Last synced by Leerness v\d+\.\d+\.\d+/g, `Last synced by Leerness v${
|
|
137
|
+
const updated2 = read(readmePath).replace(/Last synced by Leerness v\d+\.\d+\.\d+/g, `Last synced by Leerness v${VERSION}`);
|
|
124
138
|
writeUtf8(readmePath, updated2);
|
|
125
139
|
ok(' ↳ fixed: README.md 관리블록 synced 버전 갱신');
|
|
126
140
|
fixed++;
|
|
@@ -169,7 +183,9 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
169
183
|
const giLines = giText.split('\n').map(l => l.trim());
|
|
170
184
|
// 필수 보안 패턴 (글로벌 룰 .gitignore 보안 체크리스트)
|
|
171
185
|
const SECRET_PATTERNS = ['.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json'];
|
|
172
|
-
|
|
186
|
+
// 1.35.17 (audit 헌트 FP): 정확-일치만 보던 것을 광역 glob 커버리지 인식으로 완화 — 흔한 `.env*`/`.env.*`(git 이 .env 패밀리 전체를 실제로 ignore) 를 쓰면 필수 패턴이 '누락' 오탐 나던 것 차단. trailing-star prefix 매칭(`.env*`→`.env` 접두 커버)은 git 동작과 일치라 신규 FN 0(더 관대해질 뿐).
|
|
187
|
+
const _covered = (p) => giLines.some(l => { const s = l.replace(/^\//, ''); return s === p || (s.endsWith('*') && p.startsWith(s.slice(0, -1))); });
|
|
188
|
+
const missing = SECRET_PATTERNS.filter(p => !_covered(p));
|
|
173
189
|
if (missing.length) {
|
|
174
190
|
warnings++;
|
|
175
191
|
warn(`.gitignore에 시크릿 패턴 ${missing.length}건 누락: ${missing.slice(0, 4).join(', ')}${missing.length > 4 ? ' …' : ''}`);
|
|
@@ -300,8 +316,10 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
300
316
|
const ip = rows.find(r => r.status === 'in-progress');
|
|
301
317
|
if (ip) {
|
|
302
318
|
const taskText = (ip.request || '') + ' ' + (ip.nextAction || '') + ' ' + (ip.evidence || '');
|
|
303
|
-
|
|
304
|
-
|
|
319
|
+
// 1.35.17 (audit 헌트 FP, codex): API/REST 는 대소문자-민감(약어)로 분리 — 기존 /…|REST|…/i 는 영어 단어 "rest"("clean up the rest of …")를 매칭해 api_skill 오탐. 서술형 토큰(endpoint/graphql/oauth/webhook/url)만 대소문자-무관 유지.
|
|
320
|
+
const apiKeywords = /endpoint|GraphQL|OAuth|webhook|https?:\/\/[^\s]+/i;
|
|
321
|
+
const apiAcronym = /\bAPI\b|\bREST\b/; // 대문자 약어만
|
|
322
|
+
if (apiKeywords.test(taskText) || apiAcronym.test(taskText)) {
|
|
305
323
|
const matched = _matchAPISkills(root, taskText);
|
|
306
324
|
const allSkills = _listAPISkills(root);
|
|
307
325
|
if (matched.length === 0) {
|
|
@@ -323,7 +341,8 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
323
341
|
// 1.9.63: --strict — warnings ≥ threshold 시 failures로 승격 (CI 친화)
|
|
324
342
|
if (has('--strict')) {
|
|
325
343
|
const threshold = parseInt(arg('--threshold', '1'), 10);
|
|
326
|
-
|
|
344
|
+
// 1.35.17 (audit 헌트 FP, codex): warnings>0 가드 추가 — `--threshold 0` (또는 음수)이면 `warnings>=0` 이 항상 참이라 경고 0인 clean 프로젝트도 실패시키던 footgun. 실제 경고가 있을 때만 승격.
|
|
345
|
+
if (warnings > 0 && warnings >= threshold) {
|
|
327
346
|
failures++;
|
|
328
347
|
warn(`--strict 활성: warnings ${warnings} ≥ threshold ${threshold} → failures 승격`);
|
|
329
348
|
_finding('strict_promoted', 'fail', `warnings ${warnings} ≥ threshold ${threshold} → failures 승격`, { warnings, threshold });
|
package/lib/feature.js
CHANGED
|
@@ -8,9 +8,12 @@ const { absRoot, log, ok, warn, fail, failJson } = require('./io');
|
|
|
8
8
|
const { _nextFeatureId, _featureImpactBfs } = require('./pure-utils');
|
|
9
9
|
|
|
10
10
|
function featureAddCmd(root, title, deps = {}) {
|
|
11
|
-
const { _ensureFeatureGraph, _readFeatureGraph, _writeFeatureGraph, arg } = deps;
|
|
11
|
+
const { _ensureFeatureGraph, _readFeatureGraph, _writeFeatureGraph, arg, _requireInit } = deps;
|
|
12
12
|
root = absRoot(root);
|
|
13
13
|
if (!title) return fail('feature add: title 필요 — leerness feature add "<title>"');
|
|
14
|
+
// 1.36.2 (clean-room, UR-0184): 미초기화 dir 에 stray .harness scaffold 대신 에러 — task add 와 동일 게이트(_requireInit, --force 우회).
|
|
15
|
+
// 기존엔 _ensureFeatureGraph 가 무조건 .harness/feature-graph.md 생성 → 비-프로젝트 폴더 조용히 오염. (_requireInit 미주입 시 fail-open: 구버전/직접호출 호환)
|
|
16
|
+
if (typeof _requireInit === 'function' && !_requireInit(root, 'feature add')) return;
|
|
14
17
|
_ensureFeatureGraph(root);
|
|
15
18
|
const { nodes } = _readFeatureGraph(root);
|
|
16
19
|
if (nodes.some(n => n.title.toLowerCase() === title.toLowerCase())) {
|
package/lib/health.js
CHANGED
|
@@ -8,6 +8,7 @@ const path = require('path');
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const { log, ok, warn, fail, failJson, today, now, absRoot, exists, read, readBuf, mkdirp, writeUtf8, append, rel } = require('./io');
|
|
10
10
|
const { _parseArchiveBlocks } = require('./pure-utils');
|
|
11
|
+
const { findCorruptedStateJson } = require('./state-integrity'); // 1.36.1 (클린룸 리뷰 FN): 상태 JSON 무결성
|
|
11
12
|
|
|
12
13
|
function healthCmd(root, deps = {}) {
|
|
13
14
|
const { VERSION, STATUSES, has, arg, uiLang, harnessPath, listAllSkills, planPath, readProgressRows, readRules, envDiff, _collectSecretFindings, _readUsageStats, _loadDecisions, _loadLessons, _loadShellFailures, _readFeatureGraph, _scanShellScriptsEncoding, _shellEnvDrift, _computeMilestones, _computeRecentChanges, _computeRoundHistory, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _listAPISkills, _matchAPISkills, _mcpToolCount } = deps;
|
|
@@ -78,6 +79,11 @@ function healthCmd(root, deps = {}) {
|
|
|
78
79
|
for (const r of rows) byStatus[r.status] = (byStatus[r.status] || 0) + 1;
|
|
79
80
|
out.checks.tasks = { total: rows.length, byStatus };
|
|
80
81
|
} catch { out.checks.tasks = { error: 'tasks 점검 실패' }; }
|
|
82
|
+
// 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태파일 JSON 무결성 — 손상 JSON 을 그레이스풀 폴백이 healthy 로 위조하던 false-negative 를 degraded 신호로 표면화.
|
|
83
|
+
try {
|
|
84
|
+
const corrupted = findCorruptedStateJson(root);
|
|
85
|
+
out.checks.stateIntegrity = { ok: corrupted.length === 0, corruptedCount: corrupted.length, corrupted: corrupted.map(c => ({ file: c.file, error: c.error })) };
|
|
86
|
+
} catch { out.checks.stateIntegrity = { error: 'stateIntegrity 점검 실패' }; }
|
|
81
87
|
// 1.9.123: memorySurface 통합 (handoff --json 1.9.115 / session close --json 1.9.122 와 동일 패턴)
|
|
82
88
|
try {
|
|
83
89
|
const rows = readProgressRows(root);
|
|
@@ -292,6 +298,7 @@ function healthCmd(root, deps = {}) {
|
|
|
292
298
|
if (out.checks.security?.hasDotEnv && out.checks.security?.envInGitignore === false) issues.push(t('🚨 .env가 .gitignore에 누락 (보안 CRITICAL)', '🚨 .env missing from .gitignore (security CRITICAL)'));
|
|
293
299
|
if (out.checks.security?.envExampleMissing?.length) issues.push(t(`.env→.env.example 누락 ${out.checks.security.envExampleMissing.length}건`, `.env→.env.example missing ${out.checks.security.envExampleMissing.length}`));
|
|
294
300
|
if (out.checks.security?.gitignoreMissingSecrets?.length) issues.push(t(`.gitignore 시크릿 누락 ${out.checks.security.gitignoreMissingSecrets.length}건`, `.gitignore missing secret patterns ${out.checks.security.gitignoreMissingSecrets.length}`));
|
|
301
|
+
if (out.checks.stateIntegrity?.corruptedCount > 0) issues.push(t(`🗄 상태파일 JSON 손상 ${out.checks.stateIntegrity.corruptedCount}건 (${out.checks.stateIntegrity.corrupted.map(c => c.file).join(', ')}) — 수동 복구/leerness init 필요`, `🗄 ${out.checks.stateIntegrity.corruptedCount} corrupted state JSON file(s) (${out.checks.stateIntegrity.corrupted.map(c => c.file).join(', ')}) — repair or leerness init`)); // 1.36.1 (클린룸 리뷰 FN)
|
|
295
302
|
out.issues = issues;
|
|
296
303
|
out.healthy = issues.length === 0;
|
|
297
304
|
|
|
@@ -330,6 +337,13 @@ function healthCmd(root, deps = {}) {
|
|
|
330
337
|
log(`## tasks`);
|
|
331
338
|
const tb = out.checks.tasks?.byStatus || {};
|
|
332
339
|
log(t(` 총 ${out.checks.tasks?.total || 0}건: ${Object.entries(tb).map(([s, n]) => `${s}=${n}`).join(', ') || '없음'}`, ` total ${out.checks.tasks?.total || 0}: ${Object.entries(tb).map(([s, n]) => `${s}=${n}`).join(', ') || 'none'}`));
|
|
340
|
+
// 1.36.1 (클린룸 리뷰 FN): 상태파일 JSON 무결성 표면화
|
|
341
|
+
log('');
|
|
342
|
+
log(t(`## 상태파일 무결성`, `## state integrity`));
|
|
343
|
+
const _si = out.checks.stateIntegrity || {};
|
|
344
|
+
if (_si.error) log(` n/a (${_si.error})`);
|
|
345
|
+
else if (_si.corruptedCount > 0) log(t(` ✗ JSON 손상 ${_si.corruptedCount}건: ${_si.corrupted.map(c => c.file).join(', ')}`, ` ✗ ${_si.corruptedCount} corrupted: ${_si.corrupted.map(c => c.file).join(', ')}`));
|
|
346
|
+
else log(t(` ✓ .harness/*.json 파싱 정상`, ` ✓ .harness/*.json parse OK`));
|
|
333
347
|
// 1.9.163: 5능력 매트릭스 — 1.9.155 sub-agent 점검의 코드 기반 자동 평가
|
|
334
348
|
if (out.capabilityMatrix && !out.capabilityMatrix.error) {
|
|
335
349
|
log('');
|
package/lib/pure-utils.js
CHANGED
|
@@ -1142,7 +1142,7 @@ function _migrationGuideText(version) {
|
|
|
1142
1142
|
'',
|
|
1143
1143
|
'## 3. 마이그레이션 적용 (임시설치 = npx 캐시, 격리)',
|
|
1144
1144
|
' npx leerness@latest update --yes --path <project> # 자동 마이그레이션 (.harness/archive 백업 + 신 스키마 반영)',
|
|
1145
|
-
' # 또는: npx leerness@latest migrate <project> --force # 강제 재스캐폴딩(
|
|
1145
|
+
' # 또는: npx leerness@latest migrate <project> --force # 강제 재스캐폴딩 — 관리 .md 를 템플릿으로 교체(기존 내용은 .harness/archive 로 백업, in-place 보존 아님). 커스텀 편집 보존은 --force 없이 migrate / update --yes 사용',
|
|
1146
1146
|
'',
|
|
1147
1147
|
'## 4. 검증 (필수)',
|
|
1148
1148
|
' git -C <project> diff # 생성/수정 파일 전수 확인 (예상치 못한 변경 점검)',
|
|
@@ -1418,7 +1418,7 @@ function _parseAddTitle(args, startIdx = 0) {
|
|
|
1418
1418
|
// 1.9.442 (12th 외부평가 Sonnet UR-0141): task 계열 positional path 안전 추출.
|
|
1419
1419
|
// _parseAddTitle 과 동일한 path-like 판정(선행 구분자 / ./ ../ C:\)으로 제목/ID/맨이름은 경로로 오인 안 함(src/auth 같은 내부 슬래시 제목 보호).
|
|
1420
1420
|
// 값-취하는 플래그(--evidence /abs/log 등)의 값은 root 후보에서 제외(직전 토큰이 값-플래그면 skip) → 오탐 차단. 첫 path-like positional 만 반환, 없으면 null.
|
|
1421
|
-
const _TASK_VALUE_FLAGS = new Set(['--status', '--evidence', '--priority', '--note', '--reason', '--title', '--desc', '--summary', '--id', '--limit', '--from', '--to', '--trigger', '--tag']); // 1.9.445 (UR-0151): rule/lesson add 값-플래그(--trigger/--tag) 포함
|
|
1421
|
+
const _TASK_VALUE_FLAGS = new Set(['--status', '--evidence', '--priority', '--note', '--reason', '--title', '--desc', '--summary', '--id', '--limit', '--from', '--to', '--trigger', '--tag', '--files', '--depends-on', '--affects', '--co-changes-with']); // 1.9.445 (UR-0151): rule/lesson add 값-플래그(--trigger/--tag) 포함. 1.36.2 (UR-0184): feature add 값-플래그(--files 등) 값이 path-like 여도 root 로 오인 안 하게 포함
|
|
1422
1422
|
function _taskPositionalPath(args, startIdx = 2) {
|
|
1423
1423
|
const a = args || [];
|
|
1424
1424
|
for (let i = startIdx; i < a.length; i++) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// lib/state-integrity.js — .harness/*.json 상태파일 JSON 무결성 검사 (단일출처, 1.36.1).
|
|
2
|
+
// 배경(클린룸 리뷰 FN): health/doctor/check/audit 는 상태 JSON 을 try{JSON.parse}catch{빈 상태} 로 그레이스풀
|
|
3
|
+
// 폴백하느라, .harness/manifest.json 등이 깨진 JSON 이어도 전부 "healthy"/exit 0 을 반환하던 false-negative 가 있었다.
|
|
4
|
+
// → 어떤 명령도 워크스페이스 상태파일의 JSON 무결성을 검증하지 않던 갭을 이 헬퍼로 메운다(audit/health/check 공유).
|
|
5
|
+
'use strict';
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
// .harness 바로 아래의 *.json 상태파일을 열거해 JSON.parse 를 시도, 파싱 실패 파일 목록을 반환.
|
|
10
|
+
// 반환: [{ file: '.harness/<name>.json', error: '<message>' }, …] (파일명 오름차순 — 결정적/테스트 안정).
|
|
11
|
+
// 설계:
|
|
12
|
+
// - 존재하는 파일만 검사(부재는 무결성 문제 아님 — 그레이스풀 빈 상태로 취급).
|
|
13
|
+
// - 비-재귀(archive/api-skills/cache 하위는 코어 상태파일 아님 → 오탐 방지) · 파일만(디렉토리 skip).
|
|
14
|
+
// - 빈/공백-only 파일은 손상 아님(그레이스풀 빈 상태) — 오탐 차단.
|
|
15
|
+
// - BOM strip 후 파싱(io.read 와 동일 — Windows PowerShell Out-File 등의 BOM 대응).
|
|
16
|
+
// - 비-크래시: .harness 부재/디렉토리 읽기 오류 → 빈 배열, 개별 파일 읽기 오류 → skip.
|
|
17
|
+
function findCorruptedStateJson(root) {
|
|
18
|
+
const dir = path.join(root, '.harness');
|
|
19
|
+
let entries;
|
|
20
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
21
|
+
catch { return []; } // .harness 없으면 검사 대상 0 (미초기화는 별도 체크가 담당)
|
|
22
|
+
const corrupted = [];
|
|
23
|
+
for (const e of entries) {
|
|
24
|
+
if (!e.isFile() || !e.name.endsWith('.json')) continue;
|
|
25
|
+
const p = path.join(dir, e.name);
|
|
26
|
+
let text;
|
|
27
|
+
try { text = fs.readFileSync(p, 'utf8'); }
|
|
28
|
+
catch { continue; } // 읽기 불가(권한 등)는 JSON 무결성과 별개 문제 — skip
|
|
29
|
+
if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1); // BOM strip
|
|
30
|
+
if (text.trim() === '') continue; // 빈 파일 = 그레이스풀 빈 상태(손상 아님)
|
|
31
|
+
try { JSON.parse(text); }
|
|
32
|
+
catch (err) { corrupted.push({ file: '.harness/' + e.name, error: String((err && err.message) || err).slice(0, 200) }); }
|
|
33
|
+
}
|
|
34
|
+
corrupted.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
|
|
35
|
+
return corrupted;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { findCorruptedStateJson };
|
package/package.json
CHANGED
package/scripts/e2e-core.js
CHANGED
|
@@ -165,6 +165,101 @@ console.log('# leerness core (test:core) — flagship behavioral guarantees');
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
// (9) audit: 유저-프로젝트 FP 가드 (1.35.17)
|
|
169
|
+
{
|
|
170
|
+
const VER = (cp.spawnSync(process.execPath, [CLI, '--version'], { encoding: 'utf8' }).stdout || '').trim();
|
|
171
|
+
const auditJson = (d) => { const r = cp.spawnSync(process.execPath, [CLI, 'audit', d, '--json'], { encoding: 'utf8', timeout: 40000, env: { ...process.env, LEERNESS_OFFLINE: '1' } }); try { return JSON.parse(r.stdout); } catch { return null; } };
|
|
172
|
+
const hasK = (j, k) => !!(j && j.findings.some(f => f.kind === k));
|
|
173
|
+
// A: 관리블록이 현재 도구버전으로 sync 됨(유저 pkg 버전 다름) → readme_synced FP 없음
|
|
174
|
+
{
|
|
175
|
+
const d = fresh();
|
|
176
|
+
fs.writeFileSync(path.join(d, 'package.json'), JSON.stringify({ name: 'user-app', version: '2.3.0' }));
|
|
177
|
+
fs.writeFileSync(path.join(d, 'README.md'), '# A\n<!-- leerness:project-readme:start -->\nLast synced by Leerness v' + VER + ': x\n<!-- leerness:project-readme:end -->\n');
|
|
178
|
+
assert('audit: managed-block synced by CURRENT leerness → no readme_synced FP (user pkg 2.3.0)', !hasK(auditJson(d), 'readme_synced_version_stale'));
|
|
179
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
// B: .gitignore .env* 광역 패턴이 .env 패밀리 커버 → 오탐 없음
|
|
182
|
+
{
|
|
183
|
+
const d = fresh();
|
|
184
|
+
fs.writeFileSync(path.join(d, '.env'), 'K=xxxxxxxx\n');
|
|
185
|
+
fs.writeFileSync(path.join(d, '.gitignore'), '.env*\n*.pem\ncredentials.json\n');
|
|
186
|
+
assert('audit: .gitignore .env* glob covers .env family → no gitignore_missing_secrets FP', !hasK(auditJson(d), 'gitignore_missing_secrets'));
|
|
187
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
// C: 영어 단어 "rest" 는 REST API 로 오인 안 함
|
|
190
|
+
{
|
|
191
|
+
const d = fresh();
|
|
192
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'clean up the rest of the module', '--path', d], { encoding: 'utf8' });
|
|
193
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'update', 'T-0002', '--status', 'in-progress', '--path', d], { encoding: 'utf8' });
|
|
194
|
+
assert('audit: task word "rest" → no api_skill_missing FP (REST acronym case-sensitive)', !hasK(auditJson(d), 'api_skill_missing'));
|
|
195
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// (10) --json 무결성: 변형/조회 명령이 성공 경로에서도 유효 JSON (1.36.0, 클린룸 리뷰 B)
|
|
200
|
+
{
|
|
201
|
+
const d = fresh();
|
|
202
|
+
const okJson = (r) => { try { JSON.parse((r.stdout || '').trim()); return true; } catch { return false; } };
|
|
203
|
+
run(d, ['task', 'add', 'sample']);
|
|
204
|
+
assert('json: task update --status done --json → valid JSON (was plain text)', okJson(run(d, ['task', 'update', 'T-0002', '--status', 'done', '--json'])));
|
|
205
|
+
assert('json: plan add --json → valid JSON', okJson(run(d, ['plan', 'add', 'a milestone', '--json'])));
|
|
206
|
+
assert('json: rule verify --json → valid JSON', okJson(run(d, ['rule', 'verify', '--json'])));
|
|
207
|
+
assert('json: reuse find --json → valid JSON', okJson(run(d, ['reuse', 'find', 'button', '--json'])));
|
|
208
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// (11) 상태파일 JSON 무결성: 손상 .harness/*.json 을 audit/health/check 가 표면화 (클린룸 리뷰 FN, 1.36.1)
|
|
212
|
+
// 회귀 배경: 그레이스풀 폴백(try{JSON.parse}catch{빈상태})이 깨진 상태 JSON 을 "healthy"/exit 0 으로 감추던 false-negative.
|
|
213
|
+
{
|
|
214
|
+
const d = fresh();
|
|
215
|
+
const auditJson = (dir) => { const r = cp.spawnSync(process.execPath, [CLI, 'audit', dir, '--json'], { encoding: 'utf8', timeout: 40000, env: { ...process.env, LEERNESS_OFFLINE: '1' } }); try { return JSON.parse(r.stdout); } catch { return null; } };
|
|
216
|
+
const healthJson = (dir) => { const r = cp.spawnSync(process.execPath, [CLI, 'health', dir, '--json'], { encoding: 'utf8', timeout: 40000 }); try { return JSON.parse(r.stdout); } catch { return null; } };
|
|
217
|
+
const hasK = (j, k) => !!(j && (j.findings || []).some(f => f.kind === k));
|
|
218
|
+
// FP 가드: 클린(유효 JSON) 프로젝트 → 손상 finding 없음 + check exit 0 + health.stateIntegrity.ok
|
|
219
|
+
assert('state-integrity: clean project → no corrupted_state_json (FP guard)', !hasK(auditJson(d), 'corrupted_state_json'));
|
|
220
|
+
assert('state-integrity: clean project → check exit 0', run(d, ['check']).status === 0);
|
|
221
|
+
{
|
|
222
|
+
const hc = healthJson(d);
|
|
223
|
+
assert('state-integrity: clean project → health.stateIntegrity.ok', !!(hc && hc.checks && hc.checks.stateIntegrity && hc.checks.stateIntegrity.ok === true));
|
|
224
|
+
}
|
|
225
|
+
// 손상 주입: manifest.json 을 깨진 JSON 으로 덮어씀 (리뷰 재현과 동일)
|
|
226
|
+
fs.writeFileSync(path.join(d, '.harness', 'manifest.json'), '{ this is : not valid json ]]]');
|
|
227
|
+
assert('state-integrity: corrupted manifest.json → audit corrupted_state_json finding', hasK(auditJson(d), 'corrupted_state_json'));
|
|
228
|
+
assert('state-integrity: corrupted manifest.json → check exit 1 (hard gate)', run(d, ['check']).status === 1);
|
|
229
|
+
{
|
|
230
|
+
const hc = healthJson(d);
|
|
231
|
+
const flagged = !!(hc && hc.healthy === false && hc.checks && hc.checks.stateIntegrity && hc.checks.stateIntegrity.corruptedCount === 1 && (hc.checks.stateIntegrity.corrupted || []).some(c => c.file === '.harness/manifest.json'));
|
|
232
|
+
assert('state-integrity: corrupted manifest.json → health degraded (healthy=false + stateIntegrity)', flagged);
|
|
233
|
+
}
|
|
234
|
+
// 비-크래시 확인: 손상 상태에서도 audit/health/check 는 예외 없이 종료(위 spawn 이 이미 parse 됨 → 크래시 아님)
|
|
235
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// (12) feature add/show 가 trailing positional path 를 존중 — cwd 조용한 오독 + stray .harness scaffold 차단 (클린룸 리뷰 UR-0184, 1.36.2)
|
|
239
|
+
// 회귀 배경: add 가 모든 non-flag positional 을 NAME 으로 join → 경로가 이름에 흡수 + 비-프로젝트 cwd 에 stray .harness scaffold(조용한 오염).
|
|
240
|
+
{
|
|
241
|
+
const target = fresh(); // 초기화된 타깃 프로젝트
|
|
242
|
+
const outsider = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-nonproj-')); // 비-프로젝트 cwd (init 안 함)
|
|
243
|
+
const at = (dir, args) => cp.spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8', timeout: 40000, cwd: dir });
|
|
244
|
+
const graphOf = (dir) => { try { return fs.readFileSync(path.join(dir, '.harness', 'feature-graph.md'), 'utf8'); } catch { return ''; } };
|
|
245
|
+
// (a) 비-프로젝트 cwd 에서 `feature add "이름" <타깃경로>` → 타깃에 등록 + 이름에 경로 흡수 없음(node heading title clean)
|
|
246
|
+
at(outsider, ['feature', 'add', 'PosPathFeat', target]);
|
|
247
|
+
assert('feature add: positional path → 타깃 등록 + 이름 clean(경로 흡수 없음)', /^## F-\d{4} PosPathFeat\s*$/m.test(graphOf(target)));
|
|
248
|
+
// (b) 비-프로젝트 cwd 에 stray .harness scaffold 안 함 (조용한 오염 차단)
|
|
249
|
+
assert('feature add: positional path → 비-프로젝트 cwd 에 .harness scaffold 안 함', !fs.existsSync(path.join(outsider, '.harness')));
|
|
250
|
+
// (c) 경로 미지정 + 비-프로젝트 dir → init 게이트로 exit 1 + scaffold 없음 (option b)
|
|
251
|
+
const orphan = at(outsider, ['feature', 'add', 'OrphanFeat']);
|
|
252
|
+
assert('feature add: 미초기화 dir(경로 미지정) → init 게이트 exit 1 + scaffold 없음', orphan.status === 1 && !fs.existsSync(path.join(outsider, '.harness')));
|
|
253
|
+
// (d) show 도 positional path 존중 — 타깃 노드 조회(cwd 아님)
|
|
254
|
+
const showP = at(outsider, ['feature', 'show', 'F-0001', target]);
|
|
255
|
+
assert('feature show: positional path → 타깃 노드 조회(cwd 아님)', showP.status === 0 && /PosPathFeat/.test(showP.stdout || ''));
|
|
256
|
+
// (e) --path 는 여전히 우선(회귀 없음)
|
|
257
|
+
at(outsider, ['feature', 'add', 'ViaFlag', '--path', target]);
|
|
258
|
+
assert('feature add: --path 우선 보존(회귀 없음)', /^## F-\d{4} ViaFlag\s*$/m.test(graphOf(target)) && !fs.existsSync(path.join(outsider, '.harness')));
|
|
259
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
260
|
+
fs.rmSync(outsider, { recursive: true, force: true });
|
|
261
|
+
}
|
|
262
|
+
|
|
168
263
|
const dur = ((Date.now() - t0) / 1000).toFixed(1);
|
|
169
264
|
console.log(`\nCore result: ${total - failed}/${total} passed · ${dur}s`);
|
|
170
265
|
if (failed > 0) process.exit(1);
|
package/scripts/e2e.js
CHANGED
|
@@ -5881,6 +5881,45 @@ total++;
|
|
|
5881
5881
|
if (!ok) failed++;
|
|
5882
5882
|
}
|
|
5883
5883
|
|
|
5884
|
+
// 1.36.2 회귀 (클린룸 리뷰, UR-0184): feature add/show/link/impact 도 trailing positional path 인식 —
|
|
5885
|
+
// 기존엔 add 가 모든 non-flag positional 을 NAME 으로 join → 경로가 이름에 흡수 + 비-프로젝트 cwd 에 stray .harness scaffold(조용한 오독).
|
|
5886
|
+
// fix: --path > path-like positional > cwd; add 의 NAME 은 _parseAddTitle 로 절단; 미초기화 dir 은 _requireInit 게이트(scaffold 대신 에러).
|
|
5887
|
+
total++;
|
|
5888
|
+
{
|
|
5889
|
+
let ok = false;
|
|
5890
|
+
try {
|
|
5891
|
+
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-featpos-t-'));
|
|
5892
|
+
const outsider = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-featpos-x-')); // 비-프로젝트 cwd
|
|
5893
|
+
cp.spawnSync(process.execPath, [CLI, 'init', target, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
5894
|
+
const at = (dir, a) => cp.spawnSync(process.execPath, [CLI, ...a], { encoding: 'utf8', timeout: 20000, cwd: dir });
|
|
5895
|
+
const graphOf = (dir) => { try { return fs.readFileSync(path.join(dir, '.harness', 'feature-graph.md'), 'utf8'); } catch { return ''; } };
|
|
5896
|
+
// (a) 비-프로젝트 cwd 에서 positional path → 타깃 등록 + 이름 clean(경로 흡수 없음)
|
|
5897
|
+
at(outsider, ['feature', 'add', 'PosFeat', target]);
|
|
5898
|
+
const addedClean = /^## F-\d{4} PosFeat\s*$/m.test(graphOf(target));
|
|
5899
|
+
// (b) 비-프로젝트 cwd 에 stray .harness scaffold 안 함
|
|
5900
|
+
const noStray = !fs.existsSync(path.join(outsider, '.harness'));
|
|
5901
|
+
// (c) 경로 미지정 + 미초기화 dir → init 게이트 exit 1 + scaffold 없음
|
|
5902
|
+
const orphan = at(outsider, ['feature', 'add', 'Orphan']);
|
|
5903
|
+
const gated = orphan.status === 1 && !fs.existsSync(path.join(outsider, '.harness'));
|
|
5904
|
+
// (d) show/link/impact 도 positional path 존중 (cwd 아님)
|
|
5905
|
+
at(outsider, ['feature', 'add', 'PosFeat2', target]);
|
|
5906
|
+
const showP = at(outsider, ['feature', 'show', 'F-0001', target]);
|
|
5907
|
+
const showOk = showP.status === 0 && /PosFeat/.test(showP.stdout || '');
|
|
5908
|
+
at(outsider, ['feature', 'link', 'F-0001', '--affects', 'F-0002', target]);
|
|
5909
|
+
const impactP = at(outsider, ['feature', 'impact', 'F-0001', target]);
|
|
5910
|
+
const impactOk = impactP.status === 0 && /F-0002/.test(impactP.stdout || '');
|
|
5911
|
+
// (e) --path 우선 보존(회귀 없음) + --files 의 path-like 값이 root 로 오인 안 됨
|
|
5912
|
+
at(outsider, ['feature', 'add', 'ViaFlag', '--files', './src/x.js', '--path', target]);
|
|
5913
|
+
const flagOk = /^## F-\d{4} ViaFlag\s*$/m.test(graphOf(target)) && !fs.existsSync(path.join(outsider, '.harness'));
|
|
5914
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
5915
|
+
fs.rmSync(outsider, { recursive: true, force: true });
|
|
5916
|
+
ok = addedClean && noStray && gated && showOk && impactOk && flagOk;
|
|
5917
|
+
if (!ok) console.log(` [featpos 디버그] clean=${addedClean} noStray=${noStray} gated=${gated} show=${showOk} impact=${impactOk} flag=${flagOk}`);
|
|
5918
|
+
} catch {}
|
|
5919
|
+
console.log(ok ? '✓ B(1.36.2) 클린룸: feature add/show/link/impact positional path 인식 + 미초기화 scaffold 게이트 (UR-0184)' : '✗ feature positional path 실패');
|
|
5920
|
+
if (!ok) failed++;
|
|
5921
|
+
}
|
|
5922
|
+
|
|
5884
5923
|
// 1.9.413 회귀 (6th외부평가 codex P2, UR-0101): action 명령 --json 구조화 출력 + 데이터 영속 + 사람용 보존
|
|
5885
5924
|
total++;
|
|
5886
5925
|
{
|
|
@@ -6919,5 +6958,39 @@ total++;
|
|
|
6919
6958
|
if (!ok) failed++;
|
|
6920
6959
|
}
|
|
6921
6960
|
|
|
6961
|
+
// 1.36.1 회귀 (클린룸 리뷰 FN): 상태파일 JSON 무결성 — 손상 .harness/*.json 을 audit(warning)/health(degraded)/check(exit 1) 가 표면화.
|
|
6962
|
+
// 배경: 그레이스풀 폴백이 깨진 상태 JSON 을 "healthy"/exit 0 으로 감추던 false-negative(health/doctor/check 전부). 클린(유효 JSON)엔 무오탐.
|
|
6963
|
+
total++;
|
|
6964
|
+
{
|
|
6965
|
+
let ok = false;
|
|
6966
|
+
try {
|
|
6967
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-si-'));
|
|
6968
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
6969
|
+
const auditJson = () => { const r = cp.spawnSync(process.execPath, [CLI, 'audit', d, '--json'], { encoding: 'utf8', timeout: 40000, env: { ...process.env, LEERNESS_OFFLINE: '1' } }); try { return JSON.parse(r.stdout); } catch { return null; } };
|
|
6970
|
+
const healthJson = () => { const r = cp.spawnSync(process.execPath, [CLI, 'health', d, '--json'], { encoding: 'utf8', timeout: 40000 }); try { return JSON.parse(r.stdout); } catch { return null; } };
|
|
6971
|
+
const checkExit = () => cp.spawnSync(process.execPath, [CLI, 'check', d], { encoding: 'utf8', timeout: 40000 }).status;
|
|
6972
|
+
const hasK = (j, k) => !!(j && (j.findings || []).some(f => f.kind === k));
|
|
6973
|
+
// 클린: 무오탐 + check exit 0 + health.stateIntegrity.ok
|
|
6974
|
+
const cleanAudit = !hasK(auditJson(), 'corrupted_state_json');
|
|
6975
|
+
const cleanCheck = checkExit() === 0;
|
|
6976
|
+
const hc0 = healthJson();
|
|
6977
|
+
const cleanHealth = !!(hc0 && hc0.checks && hc0.checks.stateIntegrity && hc0.checks.stateIntegrity.ok === true);
|
|
6978
|
+
// 손상 주입(리뷰 재현): manifest.json → 깨진 JSON
|
|
6979
|
+
fs.writeFileSync(path.join(d, '.harness', 'manifest.json'), '{ this is : not valid json ]]]');
|
|
6980
|
+
const badAudit = hasK(auditJson(), 'corrupted_state_json');
|
|
6981
|
+
const badCheck = checkExit() === 1;
|
|
6982
|
+
const hc1 = healthJson();
|
|
6983
|
+
const badHealth = !!(hc1 && hc1.healthy === false && hc1.checks.stateIntegrity && hc1.checks.stateIntegrity.corruptedCount === 1 && (hc1.checks.stateIntegrity.corrupted || []).some(c => c.file === '.harness/manifest.json'));
|
|
6984
|
+
// audit --strict: warning 승격 → exit 1 (게이트 친화)
|
|
6985
|
+
const strictExit = cp.spawnSync(process.execPath, [CLI, 'audit', d, '--strict', '--no-npm-audit'], { encoding: 'utf8', timeout: 40000, env: { ...process.env, LEERNESS_OFFLINE: '1' } }).status;
|
|
6986
|
+
const strictBlocks = strictExit === 1;
|
|
6987
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
6988
|
+
ok = cleanAudit && cleanCheck && cleanHealth && badAudit && badCheck && badHealth && strictBlocks;
|
|
6989
|
+
if (!ok) console.log(` [si 디버그] cleanA=${cleanAudit} cleanC=${cleanCheck} cleanH=${cleanHealth} badA=${badAudit} badC=${badCheck} badH=${badHealth} strict=${strictBlocks}`);
|
|
6990
|
+
} catch (e) {}
|
|
6991
|
+
console.log(ok ? '✓ B(1.36.1) 클린룸 FN: 상태 JSON 손상 표면화(audit finding/health degraded/check exit 1 + --strict 승격) + 클린 무오탐' : '✗ 상태 JSON 무결성 회귀가드 실패');
|
|
6992
|
+
if (!ok) failed++;
|
|
6993
|
+
}
|
|
6994
|
+
|
|
6922
6995
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
6923
6996
|
if (failed > 0) process.exit(1);
|