leerness 1.9.415 → 1.9.417
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 +37 -0
- package/README.md +5 -5
- package/bin/harness.js +39 -12
- package/lib/pure-utils.js +31 -1
- package/package.json +1 -1
- package/scripts/e2e.js +51 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.417 — 2026-06-07 — contract verify field 검증 범용화: ## Fields 섹션 불릿 인식 (9번째 외부평가, UR-0123)
|
|
4
|
+
|
|
5
|
+
**🔧 `contract verify` 의 필드 계약 검증이 `tick.` 프리픽스 전용으로 하드코딩돼 범용 spec 에서 무력화되던 것(Opus 발견) 보강.**
|
|
6
|
+
|
|
7
|
+
### 배경
|
|
8
|
+
Opus: `_parseContractSpec` 의 필드 추출이 `matchAll(/tick\.(name)/g)` 뿐(원래 TICK_SPEC 예제 잔재) → `## Fields` 아래 `- userId` 같은 일반 필드를 전혀 인식 못 해 "모든 필드 존재"로 항상 통과. 범용 명세↔구현 필드 검증이 사실상 불능.
|
|
9
|
+
|
|
10
|
+
### 수정
|
|
11
|
+
- `_parseContractSpec`: **`## Fields`(또는 `## 필드`) 섹션 한정**으로 불릿(`- name` / `* name: type` / `- name (설명)`)을 필드로 인식. 섹션 한정이라 산문 불릿 오탐 없음. 식별자 직후 `(` 면 함수로 보아 제외.
|
|
12
|
+
- 기존 `tick.X` 인식 + 함수 불릿(declared) 동작은 그대로(회귀 0).
|
|
13
|
+
|
|
14
|
+
### 검증 (회귀 0)
|
|
15
|
+
- **selftest 162→163 PASS** (Fields 섹션 인식 + Functions 불릿 필드 오인 방지 + `## 필드` 한글 + tick. 회귀).
|
|
16
|
+
- **E2E 416→417 PASS** (## Fields 누락 감지 exit 1 + 충족 시 통과).
|
|
17
|
+
|
|
18
|
+
### 9번째 외부평가 진행
|
|
19
|
+
UR-0121(handoff false-OK) ✅ · UR-0122(add류 일관성) ✅ · **UR-0123(contract field) ✅** · 잔여: team positional path / status 라벨 / init 프로파일·명령 계층화(UR-0124).
|
|
20
|
+
|
|
21
|
+
## 1.9.416 — 2026-06-07 — add류 CLI 인자 일관성: 경로 흡수 차단 + 빈 입력 거부 (9번째 외부평가, UR-0122)
|
|
22
|
+
|
|
23
|
+
**🧩 9번째 외부평가의 최강 UX 발견(3모델 공통: CLI 인자 규칙 불일치) 보강 — `task add`/`requests add`/`decision add` 의 제목 파싱을 단일 출처로 통일.**
|
|
24
|
+
|
|
25
|
+
### 배경
|
|
26
|
+
Sonnet P1 + Codex: `task add "제목" /some/path` 가 경로를 제목에 흡수("제목 /some/path")하던 반면 `decision add` 는 1.9.351(UR-0064)에서 이미 경로형 positional 을 차단. 같은 add 패러다임에서 동작이 갈려 혼란.
|
|
27
|
+
|
|
28
|
+
### 수정
|
|
29
|
+
- **`_parseAddTitle(args, startIdx)` 순수 헬퍼**(pure-utils): positional 을 join 하되 첫 `--flag` 또는 경로형 토큰(`/x`, `C:\x`, `./x`, `../x`)에서 멈춤 — **단일 출처**.
|
|
30
|
+
- `task add` / `requests add` / `decision add` 모두 이 헬퍼 사용 → 경로 흡수 일관 차단.
|
|
31
|
+
- **빈/경로-only 제목 거부**: `task add ""`, `task add /path` 등 → `failJson`(--json 시 `{ok:false,code:"empty_title"}`) + **exit 1**(기존엔 빈 task 생성). decision/requests add 동일.
|
|
32
|
+
|
|
33
|
+
### 검증 (회귀 0)
|
|
34
|
+
- **selftest 161→162 PASS** (_parseAddTitle 4 단위 + task/requests 와이어).
|
|
35
|
+
- **E2E 415→416 PASS** (경로 흡수 차단 + 빈/경로-only exit 1 + --json + requests 경로 break).
|
|
36
|
+
|
|
37
|
+
### 후속 백로그 (다음 라운드)
|
|
38
|
+
team add/show positional path 일관성(UR-0122 잔여) · status/health "healthy" 라벨(UR-0121 잔여) · contract field 범용화(UR-0123) · init 프로파일/명령 계층화(UR-0124).
|
|
39
|
+
|
|
3
40
|
## 1.9.415 — 2026-06-07 — 정직성 수정: handoff 보안 헤드라인 false-OK + scan/encoding/contract --json (9번째 외부평가, UR-0121)
|
|
4
41
|
|
|
5
42
|
**🚨 9번째 외부 멀티모델 리뷰(Codex GPT-5.5 + Claude Sonnet/Opus 4.8, README 미참조 클린룸)에서 발견한 "의미적 false-OK"를 수정 — leerness 정체성(정직/anti-laziness)과 정면 충돌하던 결함.**
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
|
|
4
4
|
> **A CLI harness that stops AI coding agents from faking completion, duplicating work, forgetting context, and colliding.**
|
|
5
5
|
|
|
6
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []() []() []() []()
|
|
7
7
|
|
|
8
8
|
```
|
|
9
9
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -471,7 +471,7 @@ MIT — © leerness contributors
|
|
|
471
471
|
<!-- leerness:project-readme:start -->
|
|
472
472
|
## Leerness Project Harness
|
|
473
473
|
|
|
474
|
-
이 프로젝트는 Leerness v1.9.
|
|
474
|
+
이 프로젝트는 Leerness v1.9.417 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
475
475
|
|
|
476
476
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
477
477
|
|
|
@@ -525,7 +525,7 @@ leerness memory restore decision <date|title>
|
|
|
525
525
|
|
|
526
526
|
### MCP server (외부 AI 통합)
|
|
527
527
|
|
|
528
|
-
Leerness v1.9.
|
|
528
|
+
Leerness v1.9.417는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
|
|
529
529
|
|
|
530
530
|
```jsonc
|
|
531
531
|
// 카테고리별
|
|
@@ -546,7 +546,7 @@ Leerness v1.9.415는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
546
546
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
547
547
|
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) 다음 라운드 예약.
|
|
548
548
|
|
|
549
|
-
현재 누적: **70 라운드 (1.9.40 → 1.9.
|
|
549
|
+
현재 누적: **70 라운드 (1.9.40 → 1.9.417)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
550
550
|
|
|
551
551
|
### 성능 가이드 (1.9.140 측정)
|
|
552
552
|
|
|
@@ -584,6 +584,6 @@ leerness release pack --close --auto-main-push
|
|
|
584
584
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
585
585
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
586
586
|
|
|
587
|
-
Last synced by Leerness v1.9.
|
|
587
|
+
Last synced by Leerness v1.9.417: 2026-06-07
|
|
588
588
|
<!-- leerness:project-readme:end -->
|
|
589
589
|
|
package/bin/harness.js
CHANGED
|
@@ -25,13 +25,13 @@ const { _isSecretKey, _isPlaceholderSecret, _looksSecretLike, _mergeLines, _merg
|
|
|
25
25
|
_withBuiltinSource, _esc, _roadmapTokenStyles, _parseSkillMd,
|
|
26
26
|
_migrationGuideText, _parseContractSpec, _gitignoreMatch,
|
|
27
27
|
_featureGraphTemplate, _parseFeatureGraph, _nextFeatureId, _featureBlock, _featureImpactBfs,
|
|
28
|
-
_parseChangelogBetween, _cellSafe, _cellUnescape, _lineSafe, _parseLimit } = require('../lib/pure-utils'); // 1.9.318~
|
|
28
|
+
_parseChangelogBetween, _cellSafe, _cellUnescape, _lineSafe, _parseLimit, _parseAddTitle } = require('../lib/pure-utils'); // 1.9.318~416 (UR-0025/0053/0075/0086/0087/0104/0122): 순수 유틸 모듈 분리
|
|
29
29
|
// 1.9.304 (UR-0025): 순수 분석/검증 함수 모듈 분리.
|
|
30
30
|
const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInGit, _epistemicHonestyCheck } = require('../lib/analyzers');
|
|
31
31
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
32
32
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_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 분리 (MERGE_OVERWRITE_FILES/MINIMAL_SKIP_KEYS 포함)
|
|
33
33
|
|
|
34
|
-
const VERSION = '1.9.
|
|
34
|
+
const VERSION = '1.9.417';
|
|
35
35
|
|
|
36
36
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
37
37
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -3025,6 +3025,30 @@ function _selfTestCases() {
|
|
|
3025
3025
|
{ name: '8번째 버그헌트 (UR-0115): lazy detect --auto-track 단일 RMW 배치(O(T×N)→O(N+T)) (1.9.411)', run: () => { const src = read(__filename); const batched = src.includes("8번째 버그헌트, UR-0115") && /has\('--auto-track'\)[\s\S]{0,500}?_withLock\(progressPath\(root\), \(\) => \{[\s\S]{0,1200}?writeProgressRows/.test(src); const noPerTodoUpsert = !/for \(const t of newTodos\) \{\s*const id = nextId\(root, 'T'\);/.test(src); return batched && noPerTodoUpsert; } },
|
|
3026
3026
|
{ name: '6번째 외부평가 Opus P1 (UR-0100): list-family(decision/feature/plan/runs/team list) positional path 지원 (조용한 cwd 오독 차단) (1.9.412)', run: () => { const src = read(__filename); const L = '_resolveRoot('; const decOk = src.includes("decisionListCmd(absRoot(" + L + "args[2]))"); const planOk = src.includes("planListCmd(absRoot(" + L + "args[2]))"); const featOk = src.includes("featureListCmd(absRoot(" + L + "args[2]))"); const runsOk = src.includes("runsListCmd(absRoot(" + L + "args[2]))"); const teamOk = src.includes(L + "args[1] === 'list' ? args[2] : null)"); return decOk && planOk && featOk && runsOk && teamOk; } },
|
|
3027
3027
|
{ name: '6번째 외부평가 codex P2 (UR-0101): action 명령(task/decision/rule/lesson add) --json 구조화 출력 (1.9.413)', run: () => { const src = read(__filename); const taskJ = src.includes("log(JSON.stringify({ ok: true, id, status: arg('--status', 'requested'), request: text }))"); const decJ = src.includes("log(JSON.stringify({ ok: true, title }))"); const lesJ = src.includes("log(JSON.stringify({ ok: true, text, tag: tag || null }))"); const ruleJ = src.includes("skipped: !!result.skip"); return taskJ && decJ && lesJ && ruleJ; } },
|
|
3028
|
+
{ name: '9th 외부평가 Opus (UR-0123): contract field 범용화 — ## Fields 섹션 불릿 인식(tick. 회귀 보존) (1.9.417)', run: () => {
|
|
3029
|
+
const m = require('../lib/pure-utils');
|
|
3030
|
+
const r1 = m._parseContractSpec('# S\n\n## Fields\n- userId\n- expiresAt: string\n');
|
|
3031
|
+
const f1 = r1.fields.includes('userId') && r1.fields.includes('expiresAt');
|
|
3032
|
+
const r2 = m._parseContractSpec('# S\n\n## Functions\n- loginUser(id)\n- notField\n');
|
|
3033
|
+
const f2 = r2.fields.length === 0 && r2.declared.includes('loginUser');
|
|
3034
|
+
const r3 = m._parseContractSpec('tick.legacy\n');
|
|
3035
|
+
const f3 = r3.fields.includes('legacy');
|
|
3036
|
+
const r4 = m._parseContractSpec('# S\n\n## 필드\n- 항목은영문만\n- userName\n');
|
|
3037
|
+
const f4 = r4.fields.includes('userName');
|
|
3038
|
+
return f1 && f2 && f3 && f4;
|
|
3039
|
+
} },
|
|
3040
|
+
{ name: '9th 외부평가 Sonnet/Codex (UR-0122): add류 _parseAddTitle(flag/경로 break) + 빈 입력 거부 와이어 (1.9.416)', run: () => {
|
|
3041
|
+
const m = require('../lib/pure-utils');
|
|
3042
|
+
if (typeof m._parseAddTitle !== 'function' || m._parseAddTitle !== _parseAddTitle) return false;
|
|
3043
|
+
const u1 = m._parseAddTitle(['add', '제목', '/x/y'], 1) === '제목';
|
|
3044
|
+
const u2 = m._parseAddTitle(['add', 'my', 'title', '--note', 'v'], 1) === 'my title';
|
|
3045
|
+
const u3 = m._parseAddTitle(['add', '/only'], 1) === '';
|
|
3046
|
+
const u4 = m._parseAddTitle(['add', './rel'], 1) === '';
|
|
3047
|
+
const src = read(__filename);
|
|
3048
|
+
const taskWired = src.includes('_parseAddTitle(args, 2)') && src.includes("'empty_title'");
|
|
3049
|
+
const reqWired = src.includes('_parseAddTitle(rest, 0)');
|
|
3050
|
+
return u1 && u2 && u3 && u4 && taskWired && reqWired;
|
|
3051
|
+
} },
|
|
3028
3052
|
{ name: '9th 외부평가 Codex P1 (UR-0121): handoff 보안 헤드라인 실제 스캔 기반 + scan/encoding --json + contract --json exit (1.9.415)', run: () => {
|
|
3029
3053
|
const src = read(__filename);
|
|
3030
3054
|
const handoffWired = src.includes('_collectSecretFindings(root)') && src.includes('🚨 ' + '시크릿 ');
|
|
@@ -4513,8 +4537,9 @@ function requestsCmd(root, sub, ...rest) {
|
|
|
4513
4537
|
}
|
|
4514
4538
|
|
|
4515
4539
|
if (sub === 'add') {
|
|
4516
|
-
|
|
4517
|
-
|
|
4540
|
+
// 1.9.416 (UR-0122): flag/경로 break(기존 filter 는 경로형 positional 을 text 에 흡수) + 빈 입력 거부
|
|
4541
|
+
const text = _parseAddTitle(rest, 0);
|
|
4542
|
+
if (!text) { failJson(has('--json'), 'empty_request', 'leerness requests add "<요청>" 필요 (빈/경로-only 거부)'); return process.exit(process.exitCode || 1); }
|
|
4518
4543
|
const entry = _recordUserRequest(root, text);
|
|
4519
4544
|
if (!entry) { console.error('failed to record'); process.exit(1); }
|
|
4520
4545
|
if (has('--json')) { log(JSON.stringify(entry, null, 2)); return; }
|
|
@@ -20996,7 +21021,12 @@ async function main() {
|
|
|
20996
21021
|
if (cmd === 'task') {
|
|
20997
21022
|
const root = absRoot(arg('--path', process.cwd())); const sub = args[1] || 'list';
|
|
20998
21023
|
if (sub==='list') return taskList(root);
|
|
20999
|
-
if (sub==='add')
|
|
21024
|
+
if (sub==='add') {
|
|
21025
|
+
// 1.9.416 (UR-0122): flag/경로 break + 빈 입력 거부 (기존: args.slice(2).join(' ') 가 경로 흡수 + 빈 task 생성)
|
|
21026
|
+
const title = _parseAddTitle(args, 2);
|
|
21027
|
+
if (!title) { failJson(has('--json'), 'empty_title', 'task add "<제목>" 필요 (빈/경로-only 제목 거부)'); return process.exit(process.exitCode || 1); }
|
|
21028
|
+
return taskAdd(root, title);
|
|
21029
|
+
}
|
|
21000
21030
|
if (sub==='update') return taskUpdate(root, args[2]);
|
|
21001
21031
|
if (sub==='drop') return taskDrop(root, args[2]);
|
|
21002
21032
|
if (sub==='fix-evidence') return taskFixEvidence(root);
|
|
@@ -21052,13 +21082,10 @@ async function main() {
|
|
|
21052
21082
|
const root = absRoot(arg('--path', process.cwd())); const sub = args[1] || '';
|
|
21053
21083
|
if (sub === 'add') {
|
|
21054
21084
|
// args[2..] 가 title (단, --flag 또는 경로형 positional 이 시작되기 전까지)
|
|
21055
|
-
|
|
21056
|
-
|
|
21057
|
-
|
|
21058
|
-
|
|
21059
|
-
titleParts.push(args[i]);
|
|
21060
|
-
}
|
|
21061
|
-
return decisionAdd(root, titleParts.join(' '));
|
|
21085
|
+
// 1.9.351 (UR-0064) → 1.9.416 (UR-0122): 공유 헬퍼 _parseAddTitle 로 단일화(flag/경로 break) + 빈 입력 거부
|
|
21086
|
+
const title = _parseAddTitle(args, 2);
|
|
21087
|
+
if (!title) { failJson(has('--json'), 'empty_title', 'decision add "<제목>" 필요'); return process.exit(process.exitCode || 1); }
|
|
21088
|
+
return decisionAdd(root, title);
|
|
21062
21089
|
}
|
|
21063
21090
|
if (sub === 'list') {
|
|
21064
21091
|
return decisionListCmd(absRoot(_resolveRoot(args[2])), { json: has('--json') }); // 1.9.412 (UR-0100): positional path 지원(add 의 args[2]=title 와 분리)
|
package/lib/pure-utils.js
CHANGED
|
@@ -962,7 +962,9 @@ module.exports = {
|
|
|
962
962
|
// 1.9.402 (7번째 버그헌트 P1-A 잔여, UR-0108): MD projection 라인 안전화(개행→공백)
|
|
963
963
|
_lineSafe,
|
|
964
964
|
// 1.9.407 (8번째 버그헌트, UR-0111): --limit 안전 파싱(NaN/음수/0 → 기본값)
|
|
965
|
-
_parseLimit
|
|
965
|
+
_parseLimit,
|
|
966
|
+
// 1.9.416 (9th 외부평가, UR-0122): add 류 제목 파싱(flag/경로 break) 단일 출처
|
|
967
|
+
_parseAddTitle
|
|
966
968
|
};
|
|
967
969
|
|
|
968
970
|
// 1.9.355 (UR-0075 Phase A): AI 에이전트용 크로스버전 마이그레이션 안전 워크플로 가이드 (순수 텍스트). 임시설치 + --path + 백업 + diff 검증.
|
|
@@ -1019,6 +1021,20 @@ function _parseContractSpec(specText) {
|
|
|
1019
1021
|
for (const m of s.matchAll(/^\s*(?:[-*+]|\d+\.)\s+([A-Za-z_$][\w$]*)\(/gm)) declared.add(m[1]);
|
|
1020
1022
|
for (const m of s.matchAll(/`([A-Za-z_$][\w$]*)\s*\(/g)) mentioned.add(m[1]);
|
|
1021
1023
|
for (const m of s.matchAll(/tick\.([A-Za-z_$][\w$]*)/g)) fields.add(m[1]);
|
|
1024
|
+
// 1.9.417 (9th 외부평가 Opus, UR-0123): `## Fields`(또는 `## 필드`) 섹션 불릿도 필드로 인식.
|
|
1025
|
+
// 기존엔 tick. 프리픽스 전용이라 범용 spec 의 필드 계약이 무력화(원래 TICK_SPEC 예제 잔재). 섹션 한정 파싱이라 산문 오탐 없음.
|
|
1026
|
+
// 불릿 식별자 추출: "- userId" / "* userId: string" / "- userId (설명)" → userId. 식별자 직후 ( 면 함수라 제외(:|공백|줄끝만 허용).
|
|
1027
|
+
{
|
|
1028
|
+
const lines = s.split(/\r\n?|\n/);
|
|
1029
|
+
let inFields = false;
|
|
1030
|
+
for (const line of lines) {
|
|
1031
|
+
const h = line.match(/^#{1,6}\s+(.+?)\s*$/);
|
|
1032
|
+
if (h) { const t = h[1].trim().toLowerCase(); inFields = t === 'fields' || t.startsWith('fields ') || h[1].trim().startsWith('필드'); continue; }
|
|
1033
|
+
if (!inFields) continue;
|
|
1034
|
+
const b = line.match(/^\s*(?:[-*+]|\d+\.)\s+([A-Za-z_$][\w$]*)\s*(?::|\s|$)/);
|
|
1035
|
+
if (b) fields.add(b[1]);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1022
1038
|
return { declared: [...declared], mentioned: [...mentioned], fields: [...fields] };
|
|
1023
1039
|
}
|
|
1024
1040
|
|
|
@@ -1208,3 +1224,17 @@ function _cellUnescape(s) { return String(s == null ? '' : s).replace(/\\\|/g, '
|
|
|
1208
1224
|
function _lineSafe(s) { return String(s == null ? '' : s).replace(/\r\n|\r|\n/g, ' '); }
|
|
1209
1225
|
// 1.9.407 (8번째 버그헌트, UR-0111): --limit 안전 파싱 — NaN(예: '--limit abc')/음수/0 은 slice(0,NaN)=[] 로 모든 결과를 조용히 숨김 → 기본값으로 폴백.
|
|
1210
1226
|
function _parseLimit(raw, def) { const n = parseInt(raw, 10); return (Number.isFinite(n) && n > 0) ? n : def; }
|
|
1227
|
+
|
|
1228
|
+
// 1.9.416 (9th 외부평가 Sonnet/Codex, UR-0122): add 류(task/requests/decision) 제목 파싱 단일 출처.
|
|
1229
|
+
// positional 을 join 하되 첫 --flag 또는 경로형 토큰(/x, C:\x, ./x, ../x)에서 멈춤 →
|
|
1230
|
+
// `task add "제목" /some/path` 가 경로를 제목에 흡수하던 오염(decision add 는 이미 차단)을 일관 적용.
|
|
1231
|
+
function _parseAddTitle(args, startIdx = 0) {
|
|
1232
|
+
const parts = [];
|
|
1233
|
+
for (let i = startIdx; i < (args || []).length; i++) {
|
|
1234
|
+
const a = args[i];
|
|
1235
|
+
if (typeof a !== 'string') break;
|
|
1236
|
+
if (a.startsWith('--') || /^([A-Za-z]:[\\/]|\/|\.\.?[\\/])/.test(a)) break;
|
|
1237
|
+
parts.push(a);
|
|
1238
|
+
}
|
|
1239
|
+
return parts.join(' ').trim();
|
|
1240
|
+
}
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -5957,5 +5957,56 @@ total++;
|
|
|
5957
5957
|
if (!ok) failed++;
|
|
5958
5958
|
}
|
|
5959
5959
|
|
|
5960
|
+
// 1.9.416 회귀 (9th 외부평가 Sonnet/Codex, UR-0122): add류 제목 경로흡수 차단 + 빈 입력 거부
|
|
5961
|
+
total++;
|
|
5962
|
+
{
|
|
5963
|
+
let ok = false;
|
|
5964
|
+
try {
|
|
5965
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-addtitle-'));
|
|
5966
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
5967
|
+
// (1) task add "제목" <경로> → 경로가 title 에 흡수되지 않음
|
|
5968
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'add', '인증 구현', d, '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
5969
|
+
const tl = cp.spawnSync(process.execPath, [CLI, 'task', 'list', d, '--path', d], { encoding: 'utf8', timeout: 15000 }).stdout || '';
|
|
5970
|
+
const noPathPollution = /인증 구현/.test(tl) && !new RegExp(d.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).test(tl.split('인증 구현')[1] || '');
|
|
5971
|
+
// (2) task add 빈/경로-only 거부 exit 1
|
|
5972
|
+
const empty = cp.spawnSync(process.execPath, [CLI, 'task', 'add', '', '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
5973
|
+
const pathOnly = cp.spawnSync(process.execPath, [CLI, 'task', 'add', d, '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
5974
|
+
const rejectOk = empty.status === 1 && pathOnly.status === 1;
|
|
5975
|
+
// (3) --json 빈 거부 구조화
|
|
5976
|
+
const ej = cp.spawnSync(process.execPath, [CLI, 'task', 'add', '', '--path', d, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
5977
|
+
let jsonOk = false; try { const j = JSON.parse(ej.stdout); jsonOk = j.ok === false && j.code === 'empty_title'; } catch {}
|
|
5978
|
+
// (4) requests add 경로 break
|
|
5979
|
+
cp.spawnSync(process.execPath, [CLI, 'requests', 'add', '다크모드 지원', d, '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
5980
|
+
const rl = cp.spawnSync(process.execPath, [CLI, 'requests', 'list', '--path', d], { encoding: 'utf8', timeout: 15000 }).stdout || '';
|
|
5981
|
+
const reqClean = /다크모드 지원/.test(rl) && !new RegExp(d.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).test(rl.split('다크모드 지원')[1] || '');
|
|
5982
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
5983
|
+
ok = noPathPollution && rejectOk && jsonOk && reqClean;
|
|
5984
|
+
} catch {}
|
|
5985
|
+
console.log(ok ? '✓ B(1.9.416) 9th외부평가: add류 제목 경로흡수 차단 + 빈 입력 거부 exit1 + --json (UR-0122)' : '✗ add류 제목 일관성 실패');
|
|
5986
|
+
if (!ok) failed++;
|
|
5987
|
+
}
|
|
5988
|
+
|
|
5989
|
+
// 1.9.417 회귀 (9th 외부평가 Opus, UR-0123): contract verify field 범용화 — ## Fields 섹션 불릿 누락 감지
|
|
5990
|
+
total++;
|
|
5991
|
+
{
|
|
5992
|
+
let ok = false;
|
|
5993
|
+
try {
|
|
5994
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-cfield-'));
|
|
5995
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
5996
|
+
fs.writeFileSync(path.join(d, 'f.md'), '# S\n\n## Fields\n- userId\n- expiresAt\n');
|
|
5997
|
+
fs.writeFileSync(path.join(d, 'f.js'), 'const x={userId:1};\nmodule.exports={x};\n');
|
|
5998
|
+
const cj = cp.spawnSync(process.execPath, [CLI, 'contract', 'verify', path.join(d, 'f.md'), path.join(d, 'f.js'), '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
5999
|
+
let detectOk = false; try { const j = JSON.parse(cj.stdout); detectOk = j.specFields.includes('userId') && j.specFields.includes('expiresAt') && j.missingFields.includes('expiresAt') && !j.missingFields.includes('userId') && j.ok === false && cj.status === 1; } catch {}
|
|
6000
|
+
// 회귀: 모든 필드 충족 시 통과
|
|
6001
|
+
fs.writeFileSync(path.join(d, 'g.js'), 'const x={userId:1,expiresAt:2};\nmodule.exports={x};\n');
|
|
6002
|
+
const cg = cp.spawnSync(process.execPath, [CLI, 'contract', 'verify', path.join(d, 'f.md'), path.join(d, 'g.js'), '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
6003
|
+
let passOk = false; try { const j = JSON.parse(cg.stdout); passOk = j.missingFields.length === 0 && j.ok === true; } catch {}
|
|
6004
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
6005
|
+
ok = detectOk && passOk;
|
|
6006
|
+
} catch {}
|
|
6007
|
+
console.log(ok ? '✓ B(1.9.417) 9th외부평가: contract field 범용화(## Fields 불릿 누락 감지 + 충족 통과) (UR-0123)' : '✗ contract field 범용화 실패');
|
|
6008
|
+
if (!ok) failed++;
|
|
6009
|
+
}
|
|
6010
|
+
|
|
5960
6011
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
5961
6012
|
if (failed > 0) process.exit(1);
|