leerness 1.35.15 → 1.35.17
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 +33 -0
- package/README.md +4 -4
- package/bin/leerness.js +41 -26
- package/lib/audit.js +14 -9
- package/package.json +1 -1
- package/scripts/e2e-core.js +65 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.35.17 — 2026-07-07 — codex 협업 라운드 ⑦: audit 적대적 헌트 — 유저-프로젝트 FP 4종 (gate 표면 sweep 완결)
|
|
4
|
+
|
|
5
|
+
**사용자 지시 "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 전 표면 적대적 헌트 완주.**
|
|
6
|
+
|
|
7
|
+
### 확정 수정 (전부 유저-프로젝트 FP)
|
|
8
|
+
- **`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 라 무변화). 재현 확정.
|
|
9
|
+
- **`gitignore_missing_secrets` glob 무지 (codex #11).** 필수 시크릿 패턴(`.env`/`.env.local`/…)을 **정확-일치**로만 검사 → 흔한 `.env*`/`.env.*` 광역 패턴(git 이 실제로 .env 패밀리 전체 ignore)을 쓰면 "누락" 오탐(`--strict` 시 failure). trailing-star prefix 커버리지 인식 추가(git 동작 일치, 신규 FN 0). 재현 확정.
|
|
10
|
+
- **`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 은 무관 유지). 재현 확정.
|
|
11
|
+
- **`strict_promoted` threshold 0 footgun (codex #12).** `--strict --threshold 0`(또는 음수)이면 `warnings>=0` 이 항상 참 → **경고 0인 clean 프로젝트도 실패**. `warnings>0 &&` 가드 추가.
|
|
12
|
+
|
|
13
|
+
### 맹신 X / 미채택
|
|
14
|
+
- 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 상호작용 복잡으로 미채택(문서화).
|
|
15
|
+
|
|
16
|
+
### 검증
|
|
17
|
+
- selftest 273. e2e-core **27**(신규 audit 픽스처 3: readme_synced/gitignore-glob/rest-word). full e2e **384**. 게시본 1.35.17 재실증.
|
|
18
|
+
|
|
19
|
+
## 1.35.16 — 2026-07-07 — codex 협업 라운드 ⑥: lazy detect(안티-치트) 적대적 헌트 — 우회/FP/FN 5종 수정
|
|
20
|
+
|
|
21
|
+
**사용자 지시 "codex와 협업" 라운드 ⑥ — gate 임베드 `lazy detect`(안티-게으름/안티-치트) 적대적 헌트.** 이 체크의 주 사용자는 AI 에이전트 자신이라 **우회 인센티브가 직접적**. 자체 프로브 + codex(gpt-5.5 xhigh) 독립 헌터로 바이패스(FN)+FP 양방향 사냥. 재현으로 확정한 것만 수정.
|
|
22
|
+
|
|
23
|
+
### 확정 수정
|
|
24
|
+
- **FP — `no_test_run` 이 비-JS 프로젝트를 오탐.** `hasTestRun` 정규식이 JS+pytest 중심 → **Go/Rust/Ruby/PHP/.NET/Java/Elixir/Swift/Dart/C 프로젝트가 `cargo test`/`go test`/`rspec`/`phpunit`/`dotnet test`/`mvn`/`gradle` 등으로 실제 테스트를 돌려도** no_test_run 발화. done 작업이 있는 active 프로젝트에선 **blocking → gate 거짓 실패**. 러너 목록 확장(negative finding 억제라 FP-safe). 재현 확정.
|
|
25
|
+
- **FN #6 + FP #10 — TODO 스캐너 따옴표 상태 오판.** 구 `isInsideQuote`(per-line 따옴표 패리티)가 (a) 큰따옴표 안의 아포스트로피(`"don't"`)를 여는-따옴표로 세어 **뒤의 진짜 TODO 를 스킵(FN·고의 우회 벡터)**, (b) **여러 줄 템플릿 리터럴 안의 TODO 를 문자열 밖으로 오판(FP·blocking)**. 파일 전체를 문자 단위로 걸으며 실제 문자열 경계를 추적(백틱은 줄 넘김, `'`/`"`는 개행 종료)하는 상태머신으로 교체 — 문자열 밖 TODO 만 카운트. 둘 다 재현 확정.
|
|
26
|
+
- **우회 #3 — `evidence_missing` 이 status 를 `=== 'done'` 로만 검사.** `_hasDoneWork` 는 `/^(done|completed|verified)$/i` 인데 evidence 검사는 정확-`'done'` 만 → **`status=completed`(유효한 CLI 값)로 두면 done-work 로는 세면서 evidence 는 검사 안 하던 우회**. 동일 done-집합(대소문자무관)으로 정합. 재현 확정.
|
|
27
|
+
- **우회 #4 — 쓰레기 evidence 통과.** trivial evidence 토큰(`n/a`/`tbd`/`todo`/`wip`/`none`/`x`/점·물음표만) 추가 — 비-empty 껍데기 문자열로 통과하던 구멍 축소(lazy 는 lenient, verify-claim 이 strict 담당이므로 최소한만).
|
|
28
|
+
- **우회 #9 — blocked 행 좁은 검사.** status 대소문자무관(`Blocked`) + nextAction trivial 토큰(빈값/`-`/`n/a`/`none`/`tbd`)도 `blocker_no_next_action` 으로 감지.
|
|
29
|
+
|
|
30
|
+
### 미채택 (by-design / 저빈도)
|
|
31
|
+
- 대소문자 TODO(`todo`/`@todo`)는 `const todo` 등 FP 위험으로 대소문자-민감 유지(codex #7 동의). 무-done 회피(#5)는 fresh-프로젝트 leniency(1.9.323)의 의도된 트레이드오프. TODO 파일경로 substring 억제(#8)는 coarse-but-intended.
|
|
32
|
+
|
|
33
|
+
### 검증
|
|
34
|
+
- selftest 273. e2e-core **24**(신규 lazy 픽스처 4). full e2e **384**. 게시본 1.35.16 재실증.
|
|
35
|
+
|
|
3
36
|
## 1.35.15 — 2026-07-07 — codex 협업 라운드 ⑤: encoding check 적대적 헌트 — 파괴적 --apply + FP/FN 5종 수정
|
|
4
37
|
|
|
5
38
|
**사용자 지시 "codex와 협업" 라운드 ⑤ — gate 임베드 `encoding check` + 변형 `env encoding-check --apply` 적대적 헌트.** 한국어-우선 사용자에게 CP949 오손은 실사용 영향이 큼. 자체 프로브 + 실제 cmd.exe 런타임 실측 + codex(gpt-5.5 xhigh, 웹검색 포함) 독립 헌터 교차.
|
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.35.
|
|
128
|
+
이 프로젝트는 Leerness v1.35.17 하네스를 사용합니다. 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.35.
|
|
182
|
+
Leerness v1.35.17는 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.15는 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.35.
|
|
203
|
+
현재 누적: **70 라운드 (1.9.40 → 1.35.17)** · 매 라운드 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.35.
|
|
241
|
+
Last synced by Leerness v1.35.17: 2026-07-07
|
|
242
242
|
<!-- leerness:project-readme:end -->
|
|
243
243
|
|
package/bin/leerness.js
CHANGED
|
@@ -32,7 +32,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
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
|
|
|
35
|
-
const VERSION = '1.35.
|
|
35
|
+
const VERSION = '1.35.17';
|
|
36
36
|
|
|
37
37
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
38
38
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -8401,9 +8401,15 @@ function lazyDetect(root, opts = {}) {
|
|
|
8401
8401
|
const rows = readProgressRows(root);
|
|
8402
8402
|
// 1.9.1 P6: evidence가 단독 plan:M-XXXX 한 줄일 때만 검증 부족 처리.
|
|
8403
8403
|
// "tests:32/32 (plan:M-0002)" 같이 검증 키워드를 같이 적은 경우는 통과.
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8404
|
+
// 1.35.16 (lazy 헌트 우회 #3+#4): (1) evidence 검사를 _hasDoneWork 와 동일한 done-집합(done|completed|verified, 대소문자무관)으로 정합 — 기존 `=== 'done'` 은 status=completed/verified/Done 을 놓쳐, done-work 로는 세면서 evidence 는 안 보던 우회. (2) trivial evidence 토큰(n/a, tbd, todo, wip, none, x, 점만/물음표만) 추가 — 비-empty 쓰레기 문자열로 통과하던 구멍 축소.
|
|
8405
|
+
for (const r of rows) {
|
|
8406
|
+
if (!/^(done|completed|verified)$/i.test(r.status || '')) continue;
|
|
8407
|
+
const _ev = (r.evidence || '').trim();
|
|
8408
|
+
const _trivial = !_ev || /^(user-request|n\/?a|tbd|todo|wip|none|nil|x|-+|\.+|\?+)$/i.test(_ev) || /^plan:M-\d{4}$/i.test(_ev);
|
|
8409
|
+
if (_trivial) {
|
|
8410
|
+
issues++; _warn(`done row without verifiable evidence: ${r.id} (${r.request})`,
|
|
8411
|
+
{ kind: 'evidence_missing', severity: 'warn', taskId: r.id, request: r.request });
|
|
8412
|
+
}
|
|
8407
8413
|
}
|
|
8408
8414
|
if (rows.length === 0) { issues++; _warn('progress-tracker is empty (no tasks tracked)',
|
|
8409
8415
|
{ kind: 'progress_empty', severity: 'warn' }); }
|
|
@@ -8417,17 +8423,10 @@ function lazyDetect(root, opts = {}) {
|
|
|
8417
8423
|
{ kind: 'handoff_empty', severity: 'warn' });
|
|
8418
8424
|
}
|
|
8419
8425
|
const ev = exists(evidencePath(root)) ? read(evidencePath(root)) : '';
|
|
8420
|
-
|
|
8426
|
+
// 1.35.16 (lazy 헌트 FP): JS+pytest 중심 목록에 비-JS 러너 추가 — Go/Rust/Ruby/PHP/.NET/Java/Elixir/Swift/Dart/C 등이 실제 테스트를 돌려도 no_test_run 오탐(active 프로젝트에선 blocking → gate 거짓 실패)하던 것 차단. (negative finding 억제라 추가는 FP-safe.)
|
|
8427
|
+
const hasTestRun = /\b(npm test|pnpm test|yarn test|pytest|jest|vitest|tsc|eslint|playwright|cypress|go test|cargo test|cargo nextest|rspec|rake test|rake spec|phpunit|dotnet test|mvn test|mvn verify|gradle test|gradlew test|bun test|deno test|mocha|ctest|make test|make check|mix test|swift test|flutter test|dart test|tox|unittest|nose2)\b/i.test(ev);
|
|
8421
8428
|
if (!hasTestRun) { issues++; _warn('review-evidence.md has no recorded test/typecheck/lint run',
|
|
8422
8429
|
{ kind: 'no_test_run', severity: 'warn' }); }
|
|
8423
|
-
// 1.9.4 C: TODO/FIXME가 string literal 안에 있으면 제외 (정규식 패턴 자체 등 false positive).
|
|
8424
|
-
function isInsideQuote(line, idx) {
|
|
8425
|
-
const pre = line.slice(0, idx);
|
|
8426
|
-
const sq = (pre.match(/(?<!\\)'/g) || []).length;
|
|
8427
|
-
const dq = (pre.match(/(?<!\\)"/g) || []).length;
|
|
8428
|
-
const bq = (pre.match(/(?<!\\)`/g) || []).length;
|
|
8429
|
-
return (sq % 2 === 1) || (dq % 2 === 1) || (bq % 2 === 1);
|
|
8430
|
-
}
|
|
8431
8430
|
// 1.9.7 C: TODO 자동 추적 강화 — 위치+텍스트 캡처, known-todos 비교, --auto-track 등록
|
|
8432
8431
|
const knownPath = path.join(root, '.harness/known-todos.json');
|
|
8433
8432
|
let knownList = [];
|
|
@@ -8444,18 +8443,33 @@ function lazyDetect(root, opts = {}) {
|
|
|
8444
8443
|
if (/[\\/]bin[\\/]harness\.js$/.test(file)) continue;
|
|
8445
8444
|
let text; try { text = read(file); } catch { continue; }
|
|
8446
8445
|
const lines = text.split('\n');
|
|
8447
|
-
const
|
|
8446
|
+
const fileRel = rel(root, file);
|
|
8447
|
+
// 1.35.16 (lazy 헌트 FN #6 + FP #10): 구 isInsideQuote(per-line 따옴표 패리티)는 (a) 큰따옴표 안의 아포스트로피("don't")를 여는-따옴표로 오인해 뒤의 진짜 TODO 를 스킵(FN·고의 우회 벡터), (b) 여러 줄 템플릿 리터럴 안의 TODO 를 문자열 밖으로 오판(FP·blocking). 파일 전체를 문자 단위로 걸으며 실제 문자열 상태를 추적(백틱 문자열은 줄 넘김, '/"는 개행에서 종료)해 '문자열 밖' TODO/FIXME/XXX 만 카운트.
|
|
8448
|
+
let open = null; // 현재 열린 문자열 종류 (null | ' | " | `)
|
|
8448
8449
|
for (let i = 0; i < lines.length; i++) {
|
|
8449
|
-
|
|
8450
|
-
let
|
|
8451
|
-
|
|
8452
|
-
if (
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
if (
|
|
8450
|
+
const line = lines[i];
|
|
8451
|
+
for (let k = 0; k < line.length; k++) {
|
|
8452
|
+
const c = line[k];
|
|
8453
|
+
if (open) {
|
|
8454
|
+
if (c === '\\') { k++; continue; } // 문자열 내부 이스케이프
|
|
8455
|
+
if (c === open) open = null;
|
|
8456
|
+
continue;
|
|
8457
|
+
}
|
|
8458
|
+
if (c === '\\') { k++; continue; }
|
|
8459
|
+
if (c === '"' || c === "'" || c === '`') { open = c; continue; }
|
|
8460
|
+
// 문자열 밖: 이 위치에서 TODO/FIXME/XXX 시작 + 앞쪽 word-boundary
|
|
8461
|
+
if ((c === 'T' || c === 'F' || c === 'X') && (k === 0 || !/\w/.test(line[k - 1]))) {
|
|
8462
|
+
const mm = /^(TODO|FIXME|XXX)\b/.exec(line.slice(k));
|
|
8463
|
+
if (mm) {
|
|
8464
|
+
todoCount++;
|
|
8465
|
+
const txt = line.trim().slice(0, 120);
|
|
8466
|
+
const key = `${fileRel}:${i + 1}:${txt}`;
|
|
8467
|
+
if (!knownSet.has(key)) newTodos.push({ file: fileRel, line: i + 1, text: txt });
|
|
8468
|
+
k += mm[1].length - 1;
|
|
8469
|
+
}
|
|
8470
|
+
}
|
|
8458
8471
|
}
|
|
8472
|
+
if (open === '"' || open === "'") open = null; // 비-템플릿 문자열은 개행에서 종료(JS 문법상 개행 불가)
|
|
8459
8473
|
}
|
|
8460
8474
|
}
|
|
8461
8475
|
if (todoCount > 0) {
|
|
@@ -8493,11 +8507,12 @@ function lazyDetect(root, opts = {}) {
|
|
|
8493
8507
|
}
|
|
8494
8508
|
}
|
|
8495
8509
|
}
|
|
8496
|
-
|
|
8497
|
-
|
|
8510
|
+
// 1.35.16 (lazy 헌트 우회 #9): status 대소문자무관(Blocked) + nextAction trivial 토큰(빈값/-/n/a/none/tbd/점만) 도 blocker_no_next_action 으로 감지(기존 '없음'/'다음 액션 작성' 만).
|
|
8511
|
+
const blockers = rows.filter(r => /^blocked$/i.test(r.status || ''));
|
|
8512
|
+
for (const b of blockers) { const _na = (b.nextAction || '').trim(); if (!_na || _na === '없음' || /다음 액션 작성/.test(b.nextAction) || /^(-+|n\/?a|none|nil|tbd|todo|wip|x|\.+|\?+)$/i.test(_na)) {
|
|
8498
8513
|
issues++; _warn(`blocker without nextAction: ${b.id}`,
|
|
8499
8514
|
{ kind: 'blocker_no_next_action', severity: 'warn', taskId: b.id });
|
|
8500
|
-
}
|
|
8515
|
+
} }
|
|
8501
8516
|
// 1.9.323 (UR-0054 ⑥, 설치리뷰): fresh/무작업 프로젝트 gate 통과 — 작업 흔적(done/completed/verified) 없으면
|
|
8502
8517
|
// "부재" 신호(progress empty / handoff 미생성·빈 / 테스트 미실행)는 비차단(어드바이저리). active 프로젝트는 기존대로 차단.
|
|
8503
8518
|
// 배경: leerness init 직후 leerness gate 가 빈 트래커·미생성 handoff 때문에 즉시 실패하던 UX 결함.
|
package/lib/audit.js
CHANGED
|
@@ -113,14 +113,14 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
// 1.18.4 (GPT-5.5 평가 #7, UR-0006): 배지뿐 아니라 관리블록의 "Last synced by Leerness vX" 도 검사.
|
|
116
|
-
//
|
|
116
|
+
// 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
117
|
const sm = readmeText.match(/Last synced by Leerness v(\d+\.\d+\.\d+)/);
|
|
118
|
-
if (
|
|
118
|
+
if (VERSION && sm && sm[1] !== VERSION) {
|
|
119
119
|
warnings++;
|
|
120
|
-
warn(`README.md managed-block
|
|
121
|
-
_finding('readme_synced_version_stale', 'warn', 'README.md managed-block synced version
|
|
120
|
+
warn(`README.md managed-block synced by older Leerness: README=v${sm[1]} vs current=v${VERSION} (run: leerness readme sync)`);
|
|
121
|
+
_finding('readme_synced_version_stale', 'warn', 'README.md managed-block synced by older Leerness version', { readme: sm[1], leerness: VERSION });
|
|
122
122
|
if (fix) {
|
|
123
|
-
const updated2 = read(readmePath).replace(/Last synced by Leerness v\d+\.\d+\.\d+/g, `Last synced by Leerness v${
|
|
123
|
+
const updated2 = read(readmePath).replace(/Last synced by Leerness v\d+\.\d+\.\d+/g, `Last synced by Leerness v${VERSION}`);
|
|
124
124
|
writeUtf8(readmePath, updated2);
|
|
125
125
|
ok(' ↳ fixed: README.md 관리블록 synced 버전 갱신');
|
|
126
126
|
fixed++;
|
|
@@ -169,7 +169,9 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
169
169
|
const giLines = giText.split('\n').map(l => l.trim());
|
|
170
170
|
// 필수 보안 패턴 (글로벌 룰 .gitignore 보안 체크리스트)
|
|
171
171
|
const SECRET_PATTERNS = ['.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json'];
|
|
172
|
-
|
|
172
|
+
// 1.35.17 (audit 헌트 FP): 정확-일치만 보던 것을 광역 glob 커버리지 인식으로 완화 — 흔한 `.env*`/`.env.*`(git 이 .env 패밀리 전체를 실제로 ignore) 를 쓰면 필수 패턴이 '누락' 오탐 나던 것 차단. trailing-star prefix 매칭(`.env*`→`.env` 접두 커버)은 git 동작과 일치라 신규 FN 0(더 관대해질 뿐).
|
|
173
|
+
const _covered = (p) => giLines.some(l => { const s = l.replace(/^\//, ''); return s === p || (s.endsWith('*') && p.startsWith(s.slice(0, -1))); });
|
|
174
|
+
const missing = SECRET_PATTERNS.filter(p => !_covered(p));
|
|
173
175
|
if (missing.length) {
|
|
174
176
|
warnings++;
|
|
175
177
|
warn(`.gitignore에 시크릿 패턴 ${missing.length}건 누락: ${missing.slice(0, 4).join(', ')}${missing.length > 4 ? ' …' : ''}`);
|
|
@@ -300,8 +302,10 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
300
302
|
const ip = rows.find(r => r.status === 'in-progress');
|
|
301
303
|
if (ip) {
|
|
302
304
|
const taskText = (ip.request || '') + ' ' + (ip.nextAction || '') + ' ' + (ip.evidence || '');
|
|
303
|
-
|
|
304
|
-
|
|
305
|
+
// 1.35.17 (audit 헌트 FP, codex): API/REST 는 대소문자-민감(약어)로 분리 — 기존 /…|REST|…/i 는 영어 단어 "rest"("clean up the rest of …")를 매칭해 api_skill 오탐. 서술형 토큰(endpoint/graphql/oauth/webhook/url)만 대소문자-무관 유지.
|
|
306
|
+
const apiKeywords = /endpoint|GraphQL|OAuth|webhook|https?:\/\/[^\s]+/i;
|
|
307
|
+
const apiAcronym = /\bAPI\b|\bREST\b/; // 대문자 약어만
|
|
308
|
+
if (apiKeywords.test(taskText) || apiAcronym.test(taskText)) {
|
|
305
309
|
const matched = _matchAPISkills(root, taskText);
|
|
306
310
|
const allSkills = _listAPISkills(root);
|
|
307
311
|
if (matched.length === 0) {
|
|
@@ -323,7 +327,8 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
323
327
|
// 1.9.63: --strict — warnings ≥ threshold 시 failures로 승격 (CI 친화)
|
|
324
328
|
if (has('--strict')) {
|
|
325
329
|
const threshold = parseInt(arg('--threshold', '1'), 10);
|
|
326
|
-
|
|
330
|
+
// 1.35.17 (audit 헌트 FP, codex): warnings>0 가드 추가 — `--threshold 0` (또는 음수)이면 `warnings>=0` 이 항상 참이라 경고 0인 clean 프로젝트도 실패시키던 footgun. 실제 경고가 있을 때만 승격.
|
|
331
|
+
if (warnings > 0 && warnings >= threshold) {
|
|
327
332
|
failures++;
|
|
328
333
|
warn(`--strict 활성: warnings ${warnings} ≥ threshold ${threshold} → failures 승격`);
|
|
329
334
|
_finding('strict_promoted', 'fail', `warnings ${warnings} ≥ threshold ${threshold} → failures 승격`, { warnings, threshold });
|
package/package.json
CHANGED
package/scripts/e2e-core.js
CHANGED
|
@@ -131,6 +131,71 @@ console.log('# leerness core (test:core) — flagship behavioral guarantees');
|
|
|
131
131
|
fs.rmSync(d, { recursive: true, force: true });
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// (8) lazy detect: 안티-치트 우회/FP 가드 (1.35.16)
|
|
135
|
+
{
|
|
136
|
+
const BT = String.fromCharCode(96);
|
|
137
|
+
const jget = (d) => { const r = run(d, ['lazy', 'detect', '--json']); try { return JSON.parse(r.stdout); } catch { return null; } };
|
|
138
|
+
const hasK = (j, k) => !!(j && j.findings.some(f => f.kind === k));
|
|
139
|
+
// FP: cargo test 기록 → no_test_run 오탐 아님 (비-JS 러너 인식)
|
|
140
|
+
{
|
|
141
|
+
const d = fresh();
|
|
142
|
+
run(d, ['task', 'add', 'rust']); run(d, ['task', 'update', 'T-0002', '--status', 'done', '--evidence', 'src/lib.rs done']);
|
|
143
|
+
fs.appendFileSync(path.join(d, '.harness/review-evidence.md'), '\n```\ncargo test --all\n42 passed; 0 failed\n```\n');
|
|
144
|
+
assert('lazy: cargo test recognized (no false no_test_run)', !hasK(jget(d), 'no_test_run'));
|
|
145
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
// 우회 #3/#4: status=completed + trivial evidence → evidence_missing 발화
|
|
148
|
+
{
|
|
149
|
+
const d = fresh();
|
|
150
|
+
run(d, ['task', 'add', 'y']); run(d, ['task', 'update', 'T-0002', '--status', 'completed', '--evidence', 'n/a']);
|
|
151
|
+
assert('lazy: completed + n/a evidence → evidence_missing (status-consistency + trivial)', hasK(jget(d), 'evidence_missing'));
|
|
152
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
// FN #6: 아포스트로피 문자열 뒤 TODO 카운트 + FP #10: 멀티라인 템플릿 내 TODO 미카운트
|
|
155
|
+
{
|
|
156
|
+
const d = fresh();
|
|
157
|
+
fs.writeFileSync(path.join(d, 'a.js'), 'const m = "don' + String.fromCharCode(39) + 't"; // TODO: real\n');
|
|
158
|
+
const j1 = jget(d);
|
|
159
|
+
assert('lazy: TODO after apostrophe-string counted (FN fix)', j1 && j1.todoCount === 1);
|
|
160
|
+
fs.rmSync(path.join(d, 'a.js'), { force: true });
|
|
161
|
+
fs.writeFileSync(path.join(d, 'b.js'), 'const s = ' + BT + '\n TODO: inside template\n' + BT + ';\n');
|
|
162
|
+
const j2 = jget(d);
|
|
163
|
+
assert('lazy: TODO inside multi-line template NOT counted (FP fix)', j2 && j2.todoCount === 0);
|
|
164
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
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
|
+
|
|
134
199
|
const dur = ((Date.now() - t0) / 1000).toFixed(1);
|
|
135
200
|
console.log(`\nCore result: ${total - failed}/${total} passed · ${dur}s`);
|
|
136
201
|
if (failed > 0) process.exit(1);
|