leerness 1.36.102 → 1.36.103
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/README.md +4 -4
- package/bin/leerness.js +309 -13
- package/lib/io.js +39 -12
- package/package.json +1 -1
- package/scripts/e2e.js +127 -0
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ MIT
|
|
|
122
122
|
<!-- leerness:project-readme:start -->
|
|
123
123
|
## Leerness Project Harness
|
|
124
124
|
|
|
125
|
-
이 프로젝트는 Leerness v1.36.
|
|
125
|
+
이 프로젝트는 Leerness v1.36.103 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
126
126
|
|
|
127
127
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
128
128
|
|
|
@@ -176,7 +176,7 @@ leerness memory restore decision <date|title>
|
|
|
176
176
|
|
|
177
177
|
### MCP server (외부 AI 통합)
|
|
178
178
|
|
|
179
|
-
Leerness v1.36.
|
|
179
|
+
Leerness v1.36.103는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
|
|
180
180
|
|
|
181
181
|
```jsonc
|
|
182
182
|
// 카테고리별
|
|
@@ -197,7 +197,7 @@ Leerness v1.36.102는 stdio JSON-RPC MCP server를 내장합니다 — Claude Co
|
|
|
197
197
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
198
198
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
199
199
|
|
|
200
|
-
현재 누적: **v1.9.x → 1.36.
|
|
200
|
+
현재 누적: **v1.9.x → 1.36.103 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
201
201
|
|
|
202
202
|
### 성능 가이드
|
|
203
203
|
|
|
@@ -235,5 +235,5 @@ leerness release pack --close --auto-main-push
|
|
|
235
235
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
236
236
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
237
237
|
|
|
238
|
-
Last synced by Leerness v1.36.
|
|
238
|
+
Last synced by Leerness v1.36.103: 2026-08-05
|
|
239
239
|
<!-- leerness:project-readme:end -->
|
package/bin/leerness.js
CHANGED
|
@@ -34,7 +34,7 @@ const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE
|
|
|
34
34
|
const { tokenizeForRank: _tokenizeForRank, expandQuery: _expandQuery, scoreHits: _scoreHits, suggestTerms: _suggestTerms } = require('../lib/search-core'); // 1.36.23: memory search 랭킹 코어(순수·0-deps)
|
|
35
35
|
const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
|
|
36
36
|
|
|
37
|
-
const VERSION = '1.36.
|
|
37
|
+
const VERSION = '1.36.103';
|
|
38
38
|
|
|
39
39
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
40
40
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -236,9 +236,49 @@ function _resolveRoot(positional) {
|
|
|
236
236
|
if (positional && !String(positional).startsWith('-')) return positional;
|
|
237
237
|
return process.cwd();
|
|
238
238
|
}
|
|
239
|
+
// 1.36.103: 값-플래그 목록이 자기 소비자보다 45개 뒤처져 있었다.
|
|
240
|
+
// 미등록 값-플래그는 토큰만 건너뛰고 **값이 positional 로 떨어져** 사용자 데이터를 오염시킨다. 실측:
|
|
241
|
+
// leerness preview add "my page" --select ZZ → 저장된 제목 「my page ZZ」
|
|
242
|
+
// leerness review --persona security sample.js → "✗ 파일 없음: <cwd>\security" (값이 파일 경로로 소비)
|
|
243
|
+
// 같은 사고가 개별 등록으로 네 번 반복됐다 — --done-when(1.14.2) · --test-cmd(1.17.2) ·
|
|
244
|
+
// --require-referee(1.36.82, "툴이 안내하는 문법 그대로 쓰면 깨졌다") · 1.36.80/81 소급.
|
|
245
|
+
// 목록에 한 줄 더 넣는 대신 구조를 바꾼다: 목록을 모듈 스코프로 올려 selftest 가 소비자(arg('--x'))와
|
|
246
|
+
// 대조해 **완전성을 강제**하고(누락 시 게이트 실패), 어느 집합에도 없는 플래그는 오타로 보고 stderr 로 알린다.
|
|
247
|
+
// 동작은 바꾸지 않는다 — 미분류 부울 리터럴이 47개 남아 있어 '모르는 플래그의 값을 건너뛰기' 로 기본값을
|
|
248
|
+
// 뒤집으면 그쪽에서 positional 을 삼키는 새 회귀가 난다(가드는 false-BLOCK 이 아니라 false-PASS 쪽으로).
|
|
249
|
+
const _VALUE_FLAGS = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--to','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score','--include','--days','--gh-pages-src','--roadmap','--since','--agents','--model','--timeout','--retry-on-fail','--label','--score','--tokens','--alternatives','--impact','--tag','--surface','--depends-on','--affects','--co-changes-with','--files','--branch','--remote','--task-add','--next-action','--role','--provider','--env-var','--deploy','--token-lifetime-hours','--port','--secret','--keep','--shell','--ps-version','--done-when','--test-cmd','--require-referee','--referee','--expect-bad','--good','--bad','--static-dir','--dev-url','--repro','--root-cause','--siblings','--siblings-na','--previous-gap',
|
|
250
|
+
// 1.36.103 일괄 등록 — 소스의 arg()/argAll() 소비자에서 기계 추출한 미등록 45종(각각이 위 사고와 같은 오염 지점이었다)
|
|
251
|
+
'--alias','--bin','--cmd','--constraint','--desc','--design','--direction','--emit','--env-flag','--exit','--features','--file','--github','--in','--install-hint','--kind','--lang','--max','--measure','--min','--mockup','--name','--npm-otp','--npm-tag','--ollama-url','--persona','--preset','--presets','--profile','--project','--result','--scenario','--select','--selector','--source','--summary','--task','--text','--tier','--url','--version-args','--where','--window','--x','--y',
|
|
252
|
+
// bin/ 만 훑었을 때 놓친 lib/*.js 소비자 — 특히 --priority 는 _TASK_VALUE_FLAGS 에만 있고 여기 없어 값이 제목에 붙었다
|
|
253
|
+
'--files-changed','--files-read','--members','--only','--personas','--priority','--purpose','--schedule',
|
|
254
|
+
// --approved-by 는 `agents route` 가 값으로 쓴다(자체 인라인 목록에만 있었다) — 부울로 오분류하면 값이 토큰으로 샌다.
|
|
255
|
+
// --title 도 같다(_TASK_VALUE_FLAGS 소속). 둘 다 완전성 가드가 잡아냈다 — 손으로 분류한 것은 손으로 틀린다.
|
|
256
|
+
'--approved-by', '--title']);
|
|
257
|
+
// 1.36.103 (codex 검수 #10/#11): `brief set` 은 플래그를 정적 리터럴이 아니라 레지스트리에서 만든다
|
|
258
|
+
// (`arg('--' + f.flag)`). 정적 추출로는 보이지 않아 `brief set --intro hello` 가 "알 수 없는 플래그" 오탐을 냈다.
|
|
259
|
+
// 손으로 10개를 베껴 적으면 그 목록이 또 뒤처진다 — 레지스트리에서 **유도**한다(이 라운드의 요지 그대로).
|
|
260
|
+
for (const _bf of _BRIEF_FIELDS) { if (_bf && _bf.flag) _VALUE_FLAGS.add('--' + _bf.flag); }
|
|
261
|
+
// 부울 플래그(값 없음) — 오타 경고의 대조군. 여기 있거나 _VALUE_FLAGS 에 있으면 '실재하는 플래그' 로 본다.
|
|
262
|
+
// 판정에만 쓰이고 파싱 동작에는 관여하지 않으므로, 넓게 잡아도 안전하다(과다 포함 = 경고 억제).
|
|
263
|
+
const _BOOL_FLAGS = new Set(['--ai','--all','--all-apps','--all-presets','--allow-empty','--allow-unrelated','--apply','--audit','--auto','--auto-apply-delivered','--auto-cleanup-branches','--auto-fix','--auto-fix-bom','--auto-fix-encoding','--auto-main-push','--auto-recover','--auto-track','--banner','--bench','--build','--bundle-only','--claims','--close','--compact','--detect','--dry-run','--dry-run-npm','--embedding','--enforce','--fail-on-candidates','--fail-on-violation','--force','--gh-pages','--gh-release','--git-push','--global','--guide','--help','--html','--include-code','--interactive','--json','--last','--lenient','--major','--minimal','--minor','--no-animate','--no-auto-roadmap','--no-auto-update','--no-banner','--no-brainstorm-hits','--no-cache','--no-context-inject','--no-crawl','--no-drift-check','--no-enforce','--no-env','--no-env-detect','--no-feature-impact','--no-headline','--no-init-check','--no-interactive-select','--no-lazy-warn','--no-lessons','--no-longterm-recall','--no-mem-delta','--no-multiagent-hint','--no-next-actions','--no-npm','--no-official-skills','--no-on-every-change','--no-pre-wake','--no-preserve-link','--no-readme-sync','--no-record','--no-repl','--no-review','--no-save','--no-security-check','--no-security-summary','--no-setup-agents','--no-skill-suggest','--no-stale-check','--no-suggest','--no-synonyms','--no-team-reminders','--no-wakeup-miss','--no-workflow-guide','--no-write','--npm-publish','--offline','--on-every-change','--pack','--parent-migrate','--publish-npm','--pulse','--quiet','--record','--refresh','--remove','--repair','--repl','--require-evidence','--run-tests','--skeleton','--strict','--strict-claims','--strict-elements','--strict-exit','--suggest','--version','--yes',
|
|
264
|
+
// 소스에 리터럴로만 존재해 소비 형태를 기계 추출하지 못한 것들 — 실재하는 플래그이므로 오타로 오인하면 안 된다
|
|
265
|
+
'--commands','--decision','--errors','--expand-all','--fix','--list','--notes','--print','--raw','--select-all','--skip-git-repo-check','--tests','--verbose','--yolo',
|
|
266
|
+
// lib/*.js 의 부울 소비자 — bin/ 만 훑었다면 `agents route --confirm` 같은 실재 문법에 오탐 경고를 냈을 것이다
|
|
267
|
+
'--confirm','--dispatch','--execute','--ladder','--log','--multi','--no-env-check','--no-feature-check','--no-gitignore-check','--no-npm-audit','--no-secret-scan','--readonly','--write']);
|
|
268
|
+
// 1.36.103: 하위명령이 '선택' 인 명령에서 `<cmd> <경로>` 의 경로가 하위명령 자리로 읽히던 것 차단.
|
|
269
|
+
// tech/library 는 경로가 args[1] 인데 root 해석이 _taskPositionalPath(args, 2) 로 한 칸 늦게 시작해
|
|
270
|
+
// positional 을 통째로 건너뛰고 cwd 에 썼다(실측: `tech <TARGET>` 이 CWD 에 tech-profile.json 생성).
|
|
271
|
+
// 더 나쁜 건 1.36.101 path-strict 가드가 그 경로의 존재를 **검증해 준 뒤** 버렸다는 것이다 —
|
|
272
|
+
// 도구가 "그 경로 맞다" 고 확인하고 다른 곳에 쓴다. 경로형 토큰이면 하위명령이 아니라고 본다.
|
|
273
|
+
function _optSub(a) {
|
|
274
|
+
const v = a && a[1];
|
|
275
|
+
if (typeof v !== 'string' || !v) return undefined;
|
|
276
|
+
if (/^([A-Za-z]:[\\/]|\/|\.\.?[\\/])/.test(v)) return undefined; // 선행 구분자 path-like → 하위명령 아님
|
|
277
|
+
return v;
|
|
278
|
+
}
|
|
239
279
|
function nonFlagArgs() {
|
|
240
280
|
const out = [];
|
|
241
|
-
const withValue =
|
|
281
|
+
const withValue = _VALUE_FLAGS;
|
|
242
282
|
const a = process.argv.slice(2);
|
|
243
283
|
for (let i = 0; i < a.length; i++) {
|
|
244
284
|
const x = a[i];
|
|
@@ -868,7 +908,15 @@ function createBackup(root, reason, files, dry = false) {
|
|
|
868
908
|
candidates
|
|
869
909
|
}, null, 2) + '\n');
|
|
870
910
|
// CV-4/UR-0079: 새 스냅샷 기록 후 retention 적용 (기본 10, --keep N 조정) — 무한 누적 차단.
|
|
871
|
-
|
|
911
|
+
// 1.36.103: 여기도 `|| 10` 이라 --keep 0 이 조용히 10 이 됐다. 유효하지 않은 값은 기본값을 쓰되 그렇다고 말하고,
|
|
912
|
+
// 0(=직전에 쓴 스냅샷만 남김)은 최소 1 로 고정한다 — 방금 만든 백업까지 지우는 것은 이 명령의 의도가 아니다.
|
|
913
|
+
const _keepRaw = arg('--keep', null);
|
|
914
|
+
let keep = 10;
|
|
915
|
+
if (_keepRaw !== null) {
|
|
916
|
+
const n = Number(_keepRaw);
|
|
917
|
+
if (!Number.isInteger(n) || n < 0) warn(`--keep 무시: 0 이상의 정수가 아닙니다 (받은 값: ${_keepRaw}) — 기본 10 적용`);
|
|
918
|
+
else keep = Math.max(1, n);
|
|
919
|
+
}
|
|
872
920
|
const pruned = _pruneArchives(root, keep);
|
|
873
921
|
return { archiveDir: ar, candidates, pruned };
|
|
874
922
|
}
|
|
@@ -2674,6 +2722,17 @@ function agentModeCmd(root, sub) {
|
|
|
2674
2722
|
}
|
|
2675
2723
|
if (sub === 'tick') {
|
|
2676
2724
|
// 가벼움 우선 — pulse 만 호출
|
|
2725
|
+
// 1.36.103: --json 일 때 헤더 문구 + 사람용 pulse 출력을 그대로 흘려 stdout 이 파싱 불가였다(게시본 동일).
|
|
2726
|
+
// tick 은 pulse 의 얇은 래퍼이므로 자식에게 --json 을 넘겨 그 페이로드를 그대로 통과시킨다.
|
|
2727
|
+
if (has('--json')) {
|
|
2728
|
+
const rj = spawnChild(['pulse', '--path', root, '--json']);
|
|
2729
|
+
const raw = (rj.stdout || '').trim();
|
|
2730
|
+
let parsed = null; try { parsed = JSON.parse(raw); } catch { parsed = null; }
|
|
2731
|
+
if (!parsed) { failJson(true, 'pulse_failed', `agent-mode tick: pulse 출력을 읽지 못했습니다 (exit ${rj.status})`); return; }
|
|
2732
|
+
log(JSON.stringify(Object.assign({ ok: true, mode: 'tick' }, parsed), null, 2));
|
|
2733
|
+
if (rj.status !== 0) process.exitCode = 1;
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2677
2736
|
log(cy(`# leerness agent-mode tick (1.9.239) — 가벼운 상태 확인`));
|
|
2678
2737
|
const r = spawnChild(['pulse', '--path', root]);
|
|
2679
2738
|
process.stdout.write(r.stdout || '');
|
|
@@ -6174,7 +6233,11 @@ function _selfTestCases() {
|
|
|
6174
6233
|
const tps = read(path.join(__dirname, '..', 'lib', 'tech-profile.js'));
|
|
6175
6234
|
const guards = cl.includes('_withLock(_previewsPath(root)') && cl.includes("failJson(json, 'note_required'")
|
|
6176
6235
|
&& tps.includes('prev = null') && tps.includes('_mergeManifests(root, 2)')
|
|
6177
|
-
|
|
6236
|
+
// 1.36.103: #7b(성공 exit 엔 폴백 오류 JSON 금지)는 소스 문자열이 아니라 행위로 단언한다.
|
|
6237
|
+
// 훅을 _installJsonExitFallback 으로 옮기자 예전 정규식이 형태만 보고 깨졌다 — 보호 대상은 그대로였는데.
|
|
6238
|
+
// 지금은 _p0103JsonOk() 가 양방향으로 검사한다: 실패 경로는 ok:false JSON 을 내고, 성공 경로(pulse --json,
|
|
6239
|
+
// exit 0)는 폴백이 끼어들지 않아 자기 페이로드가 그대로 나온다.
|
|
6240
|
+
&& io2.includes('!_jsonErrEmitted && t && (t[0]') && io2.includes('_installJsonExitFallback') && io2.includes('.corrupt-${Date.now()}-${process.pid}-')
|
|
6178
6241
|
&& s.includes('declaredTestCount != null && runResult && !runResult.skipped && runResult.allPassed');
|
|
6179
6242
|
// 1.36.98 (P-0013): UI 스택 감지 — 라이브러리 인벤토리/시안 생성기가 스택별 전략을 고르는 근거라 오판이 곧 잘못된 산출물이 된다.
|
|
6180
6243
|
// 실측(실제 12개 프로젝트)에서 Tailwind 프로젝트는 CSS 변수가 0~5개뿐이고 유틸 어휘가 74~121종이라 토큰 출처가 정반대다.
|
|
@@ -6199,7 +6262,8 @@ function _selfTestCases() {
|
|
|
6199
6262
|
&& (JSON.parse(read(path.join(nextApp, '.harness', 'tech-profile.json'))).current.ui || []).some(u => u.id === 'tailwind');
|
|
6200
6263
|
} catch { uiOk = false; } finally { try { fs.rmSync(tmp3, { recursive: true, force: true }); } catch {} }
|
|
6201
6264
|
return clar && shape && noDbUrl && reqOk && guards && uiOk && _p0013LibraryOk() && _p0088CurrentStatePreserved()
|
|
6202
|
-
&& _p0101SurfaceOk() && _p0101SkillIdOk() && _p0101PathStrictOk() && _p0102HonestyOk()
|
|
6265
|
+
&& _p0101SurfaceOk() && _p0101SkillIdOk() && _p0101PathStrictOk() && _p0102HonestyOk()
|
|
6266
|
+
&& _p0103FlagsOk() && _p0103JsonOk() && _p0103RootOk();
|
|
6203
6267
|
} },
|
|
6204
6268
|
{ name: '시크릿 스캐너 F-06 (1.36.56, 외부감사): 무명 확장자 소형 텍스트 스캔(이진 NUL 제외) + 같은 토큰 중복 보고 dedupe — 행위검사', run: () => {
|
|
6205
6269
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_sc56_'));
|
|
@@ -7787,6 +7851,149 @@ function _p0101PathStrictOk() {
|
|
|
7787
7851
|
// 표본이 조용히 줄어들면 가드가 약해진 걸 모른다 — 목록/표본 크기도 함께 못박는다.
|
|
7788
7852
|
return bad.length === 0 && controlOk && _sample.length >= 7 && _PATH_STRICT_CMDS.length >= 20;
|
|
7789
7853
|
}
|
|
7854
|
+
// 1.36.103: 검증한 경로를 실제로 쓴다 — `<cmd> <경로>` 의 positional 이 root 가 되는가.
|
|
7855
|
+
// tech/library 는 1.36.101 path-strict 가 args[1] 의 존재를 검증하는데 root 해석은 index 2 부터 시작해
|
|
7856
|
+
// 그 경로를 건너뛰고 cwd 에 썼다. 관측값은 출력 문구가 아니라 **파일시스템 부작용**이다(tech-profile.json 위치).
|
|
7857
|
+
// 대조군 두 개가 필수다: 인자 없으면 cwd 그대로 · 하위명령은 여전히 하위명령(경로로 오인 금지).
|
|
7858
|
+
function _p0103RootOk() {
|
|
7859
|
+
const os2 = require('os');
|
|
7860
|
+
const cp2 = require('child_process');
|
|
7861
|
+
const arena = fs.mkdtempSync(path.join(os2.tmpdir(), 'leerness-p103r-'));
|
|
7862
|
+
try {
|
|
7863
|
+
const cwdDir = path.join(arena, 'CWDPROJ'), target = path.join(arena, 'TARGETPROJ');
|
|
7864
|
+
for (const d of [cwdDir, target]) { fs.mkdirSync(d, { recursive: true }); fs.writeFileSync(path.join(d, 'package.json'), '{"name":"x","version":"0.1.0"}'); }
|
|
7865
|
+
const env2 = Object.assign({}, process.env, { LEERNESS_OFFLINE: '1', LEERNESS_NO_AUTO_ROADMAP: '1' });
|
|
7866
|
+
const R = (a, cwd) => cp2.spawnSync(process.execPath, [__filename, ...a], { cwd: cwd || cwdDir, encoding: 'utf8', timeout: 180000, env: env2 });
|
|
7867
|
+
R(['init', cwdDir, '--yes'], cwdDir); R(['init', target, '--yes'], target);
|
|
7868
|
+
const tp = (d) => path.join(d, '.harness', 'tech-profile.json');
|
|
7869
|
+
const clear = () => { for (const d of [cwdDir, target]) { try { fs.unlinkSync(tp(d)); } catch { /* 없으면 그만 */ } } };
|
|
7870
|
+
// ① 준 경로에 쓴다
|
|
7871
|
+
clear(); R(['tech', target]);
|
|
7872
|
+
const wroteTarget = exists(tp(target)) && !exists(tp(cwdDir));
|
|
7873
|
+
// ② 대조군 A — 인자가 없으면 예전처럼 cwd
|
|
7874
|
+
clear(); R(['tech']);
|
|
7875
|
+
const bareIsCwd = exists(tp(cwdDir)) && !exists(tp(target));
|
|
7876
|
+
// ③ 하위명령은 경로로 오인되지 않는다. 1.36.101 의 path-strict 가 args[1] 을 무조건 경로로 봐서
|
|
7877
|
+
// `leerness library page` 가 "경로 없음: <cwd>\page" 로 거부됐다 — 게시된 1.36.102 에서 확인한 회귀다.
|
|
7878
|
+
const htmlT = path.join(target, 'leerness-library.html'), htmlC = path.join(cwdDir, 'leerness-library.html');
|
|
7879
|
+
for (const h of [htmlT, htmlC]) { try { fs.unlinkSync(h); } catch { /* 없으면 그만 */ } }
|
|
7880
|
+
R(['library', 'page']); // 경로 없이도 동작해야 한다(제품이 안내하는 형태)
|
|
7881
|
+
const pageBare = exists(htmlC);
|
|
7882
|
+
R(['library', 'page', target]);
|
|
7883
|
+
const pageWithPath = exists(htmlT);
|
|
7884
|
+
// ④ library 도 준 경로를 본다 — 헤더가 그 디렉토리 이름을 말한다
|
|
7885
|
+
const libOut = ((R(['library', target]).stdout) || '');
|
|
7886
|
+
const libTarget = libOut.includes('TARGETPROJ') && !libOut.includes('CWDPROJ');
|
|
7887
|
+
// ⑤ 좁히면서 false-PASS 를 만들지 않았는가 — 맨이름 오타는 여전히 거부한다(가능한 하위명령을 알려주면서)
|
|
7888
|
+
const typo = R(['library', 'NOPEZZ']);
|
|
7889
|
+
const typoRejected = typo.status !== 0 && /NOPEZZ/.test((typo.stdout || '') + (typo.stderr || ''));
|
|
7890
|
+
const typoTech = R(['tech', 'NOPEZZ']).status !== 0;
|
|
7891
|
+
return wroteTarget && bareIsCwd && pageBare && pageWithPath && libTarget && typoRejected && typoTech;
|
|
7892
|
+
} catch { return false; } finally { try { fs.rmSync(arena, { recursive: true, force: true }); } catch { /* 정리 실패는 판정과 무관 */ } }
|
|
7893
|
+
}
|
|
7894
|
+
// 1.36.103: --json 계약 회귀 가드 — "실패해도 stdout 은 항상 파싱 가능한 JSON 하나".
|
|
7895
|
+
// 실측 위반 5건이 서로 다른 이유로 났다: 경고가 오류보다 먼저 나와 stdout 오염(reuse-map/retro/insights),
|
|
7896
|
+
// 실패 분기에 JSON 갈래가 없음(release cleanup 비-git), 그리고 stdout 을 삼키는 구간에서 표식만 서서
|
|
7897
|
+
// 폴백이 막힘(gate). 그래서 한 자리가 아니라 계약 자체를 단언한다.
|
|
7898
|
+
function _p0103JsonOk() {
|
|
7899
|
+
const os2 = require('os');
|
|
7900
|
+
const cp2 = require('child_process');
|
|
7901
|
+
const arena = fs.mkdtempSync(path.join(os2.tmpdir(), 'leerness-p103j-'));
|
|
7902
|
+
try {
|
|
7903
|
+
const proj = path.join(arena, 'proj'); fs.mkdirSync(proj, { recursive: true });
|
|
7904
|
+
fs.writeFileSync(path.join(proj, 'package.json'), '{"name":"p","version":"0.1.0"}');
|
|
7905
|
+
const R = (a, cwd) => cp2.spawnSync(process.execPath, [__filename, ...a], { cwd: cwd || proj, encoding: 'utf8', timeout: 180000, env: Object.assign({}, process.env, { LEERNESS_OFFLINE: '1', LEERNESS_NO_AUTO_ROADMAP: '1' }) });
|
|
7906
|
+
R(['init', proj, '--yes']);
|
|
7907
|
+
const parse = (r) => { const t = ((r.stdout) || '').trim(); if (!t) return null; try { return JSON.parse(t); } catch { return null; } };
|
|
7908
|
+
// ① 실패 경로 3종 — 각각 다른 원인이었다
|
|
7909
|
+
const gateJ = parse(R(['gate', 'no-such-gate', '--json'])); // stdout 을 삼키는 구간
|
|
7910
|
+
const relJ = parse(R(['release', 'cleanup', '--json'])); // 실패 분기에 JSON 없음 (비-git)
|
|
7911
|
+
const incJ = parse(R(['reuse-map', '--include', 'no-such-project-zz', '--json'])); // 경고가 stdout 을 선점
|
|
7912
|
+
const failuresOk = !!gateJ && gateJ.ok === false && !!relJ && relJ.ok === false && !!incJ;
|
|
7913
|
+
// ② 성공 경로는 폴백이 끼어들지 않는다 (대조군 — 없으면 "항상 ok:false" 로도 통과한다)
|
|
7914
|
+
const pulseR = R(['pulse', '--json']);
|
|
7915
|
+
const pulseJ = parse(pulseR);
|
|
7916
|
+
const successOk = pulseR.status === 0 && !!pulseJ && pulseJ.ok !== false;
|
|
7917
|
+
// ③ --keep 은 준 값을 조용히 바꾸지 않는다
|
|
7918
|
+
const keepBad = R(['release', 'cleanup', '--keep', 'abc', '--json']);
|
|
7919
|
+
const keepBadJ = parse(keepBad);
|
|
7920
|
+
// --keep 은 소비처가 둘이었고 서로 다르게 굴었다 — 진입점에서 통일했으므로 init 쪽도 함께 건다(codex 검수 #9).
|
|
7921
|
+
const keepInit = R(['init', path.join(arena, 'K'), '--yes', '--keep', 'abc', '--json']);
|
|
7922
|
+
let keepInitJ = null; try { keepInitJ = JSON.parse(((keepInit.stdout) || '').trim()); } catch { keepInitJ = null; }
|
|
7923
|
+
const keepOk = !!keepBadJ && keepBadJ.code === 'invalid_keep' && keepBad.status !== 0
|
|
7924
|
+
&& !!keepInitJ && keepInitJ.code === 'invalid_keep' && keepInit.status !== 0;
|
|
7925
|
+
// ④ --json 이 아예 구현되지 않아 사람용 텍스트를 내던 표면(게시본 3/30) — 계약 스윕
|
|
7926
|
+
const sweepBad = [['consistency', 'check', '--json'], ['agent-mode', 'tick', '--json'], ['graph', '--json']]
|
|
7927
|
+
.filter(a => !parse(R(a)));
|
|
7928
|
+
return failuresOk && successOk && keepOk && sweepBad.length === 0;
|
|
7929
|
+
} catch { return false; } finally { try { fs.rmSync(arena, { recursive: true, force: true }); } catch { /* 정리 실패는 판정과 무관 */ } }
|
|
7930
|
+
}
|
|
7931
|
+
// 1.36.103: 값-플래그 등록 완전성 + 오타 가시성 회귀 가드.
|
|
7932
|
+
// 이 클래스는 개별 등록으로 네 번 재발했다(--done-when/--test-cmd/--require-referee/소급 2종).
|
|
7933
|
+
// 그래서 '고쳤다' 가 아니라 '뒤처질 수 없다' 를 단언한다 — 소스의 소비자(arg/argAll)를 열거해
|
|
7934
|
+
// _VALUE_FLAGS 를 **데이터로** 대조하므로, 새 값-플래그를 등록 없이 추가하면 여기서 게이트가 붉어진다.
|
|
7935
|
+
function _p0103FlagsOk() {
|
|
7936
|
+
const os2 = require('os');
|
|
7937
|
+
const cp2 = require('child_process');
|
|
7938
|
+
// ① 완전성 — 값으로 소비되는 플래그는 전부 등록돼 있어야 한다.
|
|
7939
|
+
// 범위는 bin + lib 전체다. bin/ 만 훑었을 때 --priority(_TASK_VALUE_FLAGS 에만 있던 것)와
|
|
7940
|
+
// lib 의 --purpose/--personas/--members/--only/--schedule/--files-read 를 통째로 놓쳤다.
|
|
7941
|
+
const libDir = path.join(path.dirname(__filename), '..', 'lib');
|
|
7942
|
+
let src = read(__filename);
|
|
7943
|
+
try { for (const f of fs.readdirSync(libDir)) if (f.endsWith('.js')) src += '\n' + read(path.join(libDir, f)); } catch { return false; }
|
|
7944
|
+
const IGNORE = new Set(['--definitely-not-real-xyz']); // selftest 전용 리터럴(실재 플래그 아님)
|
|
7945
|
+
const consumed = new Set();
|
|
7946
|
+
let m; const re = /\b_?arg(?:All(?:Strict)?)?\(\s*'(--[a-z0-9-]+)'/g;
|
|
7947
|
+
while ((m = re.exec(src))) if (!IGNORE.has(m[1])) consumed.add(m[1]);
|
|
7948
|
+
// 다른 기제로 값을 취하는 집합도 같은 계약을 진다. (_TASK_VALUE_FLAGS 는 pure-utils 내부 상수라
|
|
7949
|
+
// export 되지 않으므로, 이어붙인 소스에서 읽는다 — 임의 결합 대신 정의 위치를 그대로 참조한다.)
|
|
7950
|
+
const tvf = src.match(/const _TASK_VALUE_FLAGS = new Set\(\[([\s\S]*?)\]\)/);
|
|
7951
|
+
if (!tvf) return false;
|
|
7952
|
+
for (const q of (tvf[1].match(/'(--[a-z0-9-]+)'/g) || [])) consumed.add(q.slice(1, -1));
|
|
7953
|
+
const unregistered = [...consumed].filter(f => !_VALUE_FLAGS.has(f));
|
|
7954
|
+
// 열거 자체가 죽으면 공허참이 된다 — 표본 하한과 알려진 항목 존재를 함께 단언한다.
|
|
7955
|
+
const enumeratedOk = consumed.size >= 100 && consumed.has('--persona') && consumed.has('--select')
|
|
7956
|
+
&& consumed.has('--require-referee') && consumed.has('--priority') && consumed.has('--purpose');
|
|
7957
|
+
if (unregistered.length || !enumeratedOk) return false;
|
|
7958
|
+
|
|
7959
|
+
// ② 행위 — 등록된 값-플래그의 값이 더 이상 본문/경로로 흡수되지 않는다.
|
|
7960
|
+
const arena = fs.mkdtempSync(path.join(os2.tmpdir(), 'leerness-p103-'));
|
|
7961
|
+
try {
|
|
7962
|
+
const proj = path.join(arena, 'proj'); fs.mkdirSync(proj, { recursive: true });
|
|
7963
|
+
fs.writeFileSync(path.join(proj, 'package.json'), '{"name":"p","version":"0.1.0"}');
|
|
7964
|
+
fs.writeFileSync(path.join(proj, 'sample.js'), 'function f(){ return 1; }\n');
|
|
7965
|
+
const R = (a) => cp2.spawnSync(process.execPath, [__filename, ...a], { cwd: proj, encoding: 'utf8', timeout: 180000, env: Object.assign({}, process.env, { LEERNESS_OFFLINE: '1', LEERNESS_NO_AUTO_ROADMAP: '1' }) });
|
|
7966
|
+
R(['init', proj, '--yes']);
|
|
7967
|
+
R(['preview', 'add', 'my page', '--select', 'ZZABSORB']);
|
|
7968
|
+
const pv = exists(path.join(proj, '.harness', 'previews.json')) ? read(path.join(proj, '.harness', 'previews.json')) : '';
|
|
7969
|
+
const titleClean = pv.includes('my page') && !pv.includes('ZZABSORB');
|
|
7970
|
+
// 제품이 스스로 안내하는 문법(persona list 의 "leerness review <file> --persona <id>") 이 순서와 무관하게 동작해야 한다
|
|
7971
|
+
const rev = R(['review', '--persona', 'security', 'sample.js']);
|
|
7972
|
+
const revOut = (rev.stdout || '') + (rev.stderr || '');
|
|
7973
|
+
const pathClean = !/security['"]?\s*$/m.test(revOut) && !revOut.includes(path.join(proj, 'security'));
|
|
7974
|
+
|
|
7975
|
+
// ③ 등호형(--flag=value)은 값을 자기가 들고 있다 — 다음 토큰까지 먹으면 위치인자가 사라진다.
|
|
7976
|
+
// 인라인 목록을 _VALUE_FLAGS 로 바꾸며 멤버십을 split('=')[0] 로 본 탓에 내가 만든 회귀다(codex 검수 #5~#8).
|
|
7977
|
+
R(['preview', 'add', '--select=ZZEQ', 'equals form title']);
|
|
7978
|
+
const pv2 = exists(path.join(proj, '.harness', 'previews.json')) ? read(path.join(proj, '.harness', 'previews.json')) : '';
|
|
7979
|
+
const eqOk = pv2.includes('equals form title') && !pv2.includes('ZZEQ');
|
|
7980
|
+
const rtEq = R(['agents', 'route', '--tier=normal', 'fix the parser']);
|
|
7981
|
+
const eqRoute = !/비어 있|empty_task/.test((rtEq.stdout || '') + (rtEq.stderr || ''));
|
|
7982
|
+
// ④ 동적 레지스트리 플래그(brief set 은 arg('--'+f.flag) 로 만든다)에 오탐 경고가 없어야 한다 — 판별 대조군 포함
|
|
7983
|
+
const W2 = /알 수 없는 플래그|Unknown flag/;
|
|
7984
|
+
const dynOk = !W2.test((R(['brief', 'set', '--intro', 'hello']).stderr) || '')
|
|
7985
|
+
&& W2.test((R(['brief', 'set', '--introo', 'x']).stderr) || '');
|
|
7986
|
+
// ⑤ 오타 가시성 — 판별 대조군: 실재 플래그는 조용하고, 없는 플래그만 경고한다.
|
|
7987
|
+
const good = R(['pulse', '--json']);
|
|
7988
|
+
const typo = R(['pulse', '--json', '--nosuchflagzz']);
|
|
7989
|
+
const quietOnReal = !/알 수 없는 플래그|Unknown flag/.test(good.stderr || '');
|
|
7990
|
+
const warnsOnTypo = /알 수 없는 플래그/.test(typo.stderr || '') && (typo.stderr || '').includes('--nosuchflagzz');
|
|
7991
|
+
// 경고는 stderr 전용 — stdout(기계 소비자) 은 오염되지 않는다
|
|
7992
|
+
const stdoutClean = !/알 수 없는 플래그|Unknown flag/.test(typo.stdout || '');
|
|
7993
|
+
let jsonStillValid = false; try { JSON.parse((typo.stdout || '').trim()); jsonStillValid = true; } catch { jsonStillValid = false; }
|
|
7994
|
+
return titleClean && pathClean && eqOk && eqRoute && dynOk && quietOnReal && warnsOnTypo && stdoutClean && jsonStillValid;
|
|
7995
|
+
} catch { return false; } finally { try { fs.rmSync(arena, { recursive: true, force: true }); } catch { /* 정리 실패는 판정과 무관 */ } }
|
|
7996
|
+
}
|
|
7790
7997
|
// 1.36.102 (명령 표면 감사 P1 2건): 회귀 가드.
|
|
7791
7998
|
// ① update --check 가 레지스트리 조회에 실패하고도 "up to date" 라고 단정했다 —
|
|
7792
7999
|
// 바로 윗줄에 "(unavailable — 판정 보류)" 를 찍어 놓고 아랫줄에서 결론을 뒤집는 자기모순이었다.
|
|
@@ -17021,6 +17228,9 @@ function consistencyCheck(root) {
|
|
|
17021
17228
|
root = absRoot(root);
|
|
17022
17229
|
const cands = ['designguide.md','design-guide.md','.harness/designguide.md','docs/designguide.md','docs/design-guide.md'];
|
|
17023
17230
|
const found = cands.filter(f => exists(path.join(root,f)));
|
|
17231
|
+
// 1.36.103: --json 이 전역 플래그로 광고되는데 여기선 구현이 없어 사람용 텍스트가 나갔다(게시본에서도 동일).
|
|
17232
|
+
// 기계 소비자에겐 파싱 불가 = 미구현과 같다 — 같은 데이터를 JSON 으로 낸다.
|
|
17233
|
+
if (has('--json')) { log(JSON.stringify({ ok: true, root, canonical: '.harness/design-system.md', mergeCandidates: found }, null, 2)); return; }
|
|
17024
17234
|
log('Canonical design file: .harness/design-system.md');
|
|
17025
17235
|
if (found.length) { warn('merge candidates found:'); found.forEach(x => log('- ' + x)); }
|
|
17026
17236
|
else ok('no duplicate design guide candidates');
|
|
@@ -18701,10 +18911,25 @@ function releaseCleanupCmd(root) {
|
|
|
18701
18911
|
const rd = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
|
|
18702
18912
|
const dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
18703
18913
|
const apply = has('--apply');
|
|
18704
|
-
const
|
|
18914
|
+
const jsonMode = has('--json');
|
|
18915
|
+
// 1.36.103: `parseInt(...) || 5` 는 사용자가 준 0 을 조용히 5 로 바꿨다(실측: `--keep 0` → "merged 0 ≤ keep 5").
|
|
18916
|
+
// 0 은 "merged 전부 정리" 라는 명확한 의사표시다. 반대로 `--keep -3`/`--keep abc` 는 그대로 통과하거나
|
|
18917
|
+
// 조용히 기본값이 됐다 — 준 값을 무시하려면 최소한 그렇다고 말해야 한다.
|
|
18918
|
+
const _keepRaw = arg('--keep', null);
|
|
18919
|
+
let keep = 5;
|
|
18920
|
+
if (_keepRaw !== null) {
|
|
18921
|
+
const n = Number(_keepRaw);
|
|
18922
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
18923
|
+
failJson(jsonMode, 'invalid_keep', `--keep 은 0 이상의 정수여야 합니다 (받은 값: ${_keepRaw})`);
|
|
18924
|
+
return;
|
|
18925
|
+
}
|
|
18926
|
+
keep = n;
|
|
18927
|
+
}
|
|
18705
18928
|
// 1) git 저장소 확인
|
|
18706
18929
|
const headR = cp.spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root, encoding: 'utf8' });
|
|
18707
18930
|
if (headR.status !== 0) {
|
|
18931
|
+
// 1.36.103: --json 이 사람용 텍스트를 내던 자리 — 기계 소비자는 여기서 JSON.parse 에 실패했다.
|
|
18932
|
+
if (jsonMode) { log(JSON.stringify({ ok: false, error: 'git 저장소가 아닙니다', code: 'not_a_git_repo', root, deleted: [], candidates: [] }, null, 2)); process.exitCode = 1; return; }
|
|
18708
18933
|
log(rd('# leerness release cleanup (1.9.235) — git 저장소 아님'));
|
|
18709
18934
|
return;
|
|
18710
18935
|
}
|
|
@@ -19530,6 +19755,13 @@ function graphCmd(root) {
|
|
|
19530
19755
|
for (const e of edges) lines.push(` "${e.src}" --> "${e.dst}"`);
|
|
19531
19756
|
lines.push('```');
|
|
19532
19757
|
const md = `# Code dependency graph\n\n생성: ${now()}\n노드: ${nodeSet.size}, 엣지: ${edges.length}\n\n` + lines.join('\n') + '\n';
|
|
19758
|
+
// 1.36.103: --json 미구현이라 mermaid 마크다운이 stdout 으로 나갔다 — 그래프야말로 기계가 읽을 데이터다.
|
|
19759
|
+
if (has('--json')) {
|
|
19760
|
+
const payload = { ok: true, root, generatedAt: now(), nodes: [...nodeSet], edges, counts: { nodes: nodeSet.size, edges: edges.length } };
|
|
19761
|
+
if (out) { writeUtf8(path.resolve(root, out), md); payload.written = out; }
|
|
19762
|
+
log(JSON.stringify(payload, null, 2));
|
|
19763
|
+
return;
|
|
19764
|
+
}
|
|
19533
19765
|
if (out) {
|
|
19534
19766
|
writeUtf8(path.resolve(root, out), md);
|
|
19535
19767
|
ok(`graph 저장: ${out}`);
|
|
@@ -25003,6 +25235,42 @@ async function main() {
|
|
|
25003
25235
|
return log(VERSION);
|
|
25004
25236
|
}
|
|
25005
25237
|
if (has('--help') || has('-h')) return help();
|
|
25238
|
+
// 1.36.103: 실재하지 않는 플래그(오타)를 조용히 삼키지 않는다.
|
|
25239
|
+
// 값-플래그 미등록 45종은 _VALUE_FLAGS 등록으로 닫았지만, 사용자 오타(--prio, --resaon)는 여전히
|
|
25240
|
+
// 그 값이 본문에 붙는다(실측: `decision add "use redis" --resaon faster` → 저장된 제목 "use redis faster").
|
|
25241
|
+
// 파싱 기본값을 뒤집으면 미분류 부울 리터럴 쪽에서 positional 을 삼키는 새 회귀가 나므로, 동작 대신
|
|
25242
|
+
// 가시성을 준다 — stderr 로만 알리고 stdout/--json/exit code 는 건드리지 않는다(기계 소비자 무영향).
|
|
25243
|
+
{
|
|
25244
|
+
const _unknownFlags = [];
|
|
25245
|
+
for (const _tok of process.argv.slice(2)) {
|
|
25246
|
+
if (typeof _tok !== 'string' || !_tok.startsWith('--') || _tok === '--') continue;
|
|
25247
|
+
const _name = _tok.split('=')[0];
|
|
25248
|
+
if (_VALUE_FLAGS.has(_name) || _BOOL_FLAGS.has(_name)) continue;
|
|
25249
|
+
if (!_unknownFlags.includes(_name)) _unknownFlags.push(_name);
|
|
25250
|
+
}
|
|
25251
|
+
if (_unknownFlags.length) {
|
|
25252
|
+
const _L = _uiLang(process.cwd());
|
|
25253
|
+
console.error(_L === 'en'
|
|
25254
|
+
? `⚠ Unknown flag: ${_unknownFlags.join(' ')} — no such flag in leerness. Its value can be absorbed into the text or path argument. Check: leerness --help`
|
|
25255
|
+
: `⚠ 알 수 없는 플래그: ${_unknownFlags.join(' ')} — leerness 에 없는 이름입니다. 그 값이 본문이나 경로 인자에 섞일 수 있습니다. 확인: leerness --help`);
|
|
25256
|
+
}
|
|
25257
|
+
}
|
|
25258
|
+
// 1.36.103 (codex 검수 #9): --keep 은 소비처가 둘인데 한쪽(release cleanup)은 거부하고 다른 쪽(아카이브 retention)은
|
|
25259
|
+
// 조용히 기본값 10 이 됐다 — `init . --keep abc --json` 이 exit 0 + {ok:true} 로 끝났다.
|
|
25260
|
+
// "준 값을 조용히 바꾸지 않는다" 를 소비처마다 반복하지 않고 진입점 한 곳에서 강제한다.
|
|
25261
|
+
{
|
|
25262
|
+
const _k = arg('--keep', null);
|
|
25263
|
+
if (_k !== null) {
|
|
25264
|
+
const _kn = Number(_k);
|
|
25265
|
+
if (!Number.isInteger(_kn) || _kn < 0) {
|
|
25266
|
+
const _enK = _uiLang(process.cwd()) === 'en';
|
|
25267
|
+
failJson(has('--json'), 'invalid_keep', _enK
|
|
25268
|
+
? `--keep must be an integer >= 0 (got: ${_k})`
|
|
25269
|
+
: `--keep 은 0 이상의 정수여야 합니다 (받은 값: ${_k})`);
|
|
25270
|
+
return;
|
|
25271
|
+
}
|
|
25272
|
+
}
|
|
25273
|
+
}
|
|
25006
25274
|
// 1.36.101 (명령 표면 감사): '없는 경로에 초록 보고' 클래스를 디스패처 한 곳에서 막는다.
|
|
25007
25275
|
// 실측 — 32개 조사 대상 중 16개가 존재하지 않는 디렉토리에 exit 0 과 정상 보고를 냈다
|
|
25008
25276
|
// (pulse/round-history/milestones/session-resume 는 "✓ 비정상 종료 신호 없음 · R0 · 정상 운영 중" 까지 찍었다 — 오타가 안심 보고가 된다).
|
|
@@ -25019,7 +25287,32 @@ async function main() {
|
|
|
25019
25287
|
const _isStrict = _strict.has(cmd) || (_strictPair[cmd] && args[1] === _strictPair[cmd]);
|
|
25020
25288
|
if (_isStrict) {
|
|
25021
25289
|
// 사용자가 '경로를 명시했을 때만' 검사한다 — 미지정(cwd)은 항상 존재하므로 검사할 것이 없다.
|
|
25022
|
-
|
|
25290
|
+
// 1.36.103 (자기 회귀 수리): 이 가드는 args[1] 을 무조건 경로로 봤다. 하위명령을 가진 명령에서는 그게 틀린다 —
|
|
25291
|
+
// `leerness library page` 가 "경로 없음: <cwd>\page" 로 거부됐다(1.36.101/102 에 게시된 상태로 확인).
|
|
25292
|
+
// 그건 library 가 자기 출력에서 안내하는 명령이고, P-0013 사람용 페이지의 유일한 진입점이다.
|
|
25293
|
+
// 하위명령을 받는 명령에 한해 **선행 구분자 path-like 일 때만** 경로로 본다. 나머지(하위명령 없는 명령)는
|
|
25294
|
+
// 맨이름 오타도 경로로 잡아야 하므로 1.36.101 동작을 그대로 둔다.
|
|
25295
|
+
// tech 는 sub 를 받기만 하고 쓰지 않는다(하위명령 없음) → args[1] 은 언제나 경로다. library 만 'page' 를 갖는다.
|
|
25296
|
+
// 알려진 하위명령도 아니고 경로형도 아니면 그건 오타다 — 조용히 cwd 에서 초록 보고를 내는 대신 그렇다고 말한다
|
|
25297
|
+
// (좁히다가 false-BLOCK 을 false-PASS 로 바꾸면 1.36.101 이 잡던 바로 그 증상이 되돌아온다).
|
|
25298
|
+
const _SUBS = { library: new Set(['page']) };
|
|
25299
|
+
const _looksPath = (a) => /^([A-Za-z]:[\\/]|\/|\.\.?[\\/])/.test(a);
|
|
25300
|
+
let _cands = [args[1], args[2]];
|
|
25301
|
+
if (_SUBS[cmd] && args[1] && !args[1].startsWith('-')) {
|
|
25302
|
+
if (_SUBS[cmd].has(args[1])) _cands = [null, args[2]]; // 하위명령 — 경로 아님
|
|
25303
|
+
// codex 검수 #4 는 "존재하는 디렉토리면 맨이름도 경로로 받아 달라" 였다 — 받지 않는다.
|
|
25304
|
+
// root 해석(_taskPositionalPath)은 선행 구분자만 경로로 인정한다(제목 'src/auth' 를 경로로 오인하지 않기 위한 계약).
|
|
25305
|
+
// 여기서만 통과시키면 검증은 하고 root 는 cwd 로 가는, 이 라운드가 고치고 있는 바로 그 형태가 된다.
|
|
25306
|
+
// 그래서 거부하되 무엇을 치면 되는지(./name) 를 문장에 넣는다.
|
|
25307
|
+
else if (!_looksPath(args[1])) {
|
|
25308
|
+
const _en0 = _uiLang(process.cwd()) === 'en';
|
|
25309
|
+
failJson(has('--json'), 'unknown_subcommand', _en0
|
|
25310
|
+
? `unknown ${cmd} subcommand: ${args[1]} (available: ${[..._SUBS[cmd]].join(', ')}) — for a path use an explicit form like ./${args[1]}`
|
|
25311
|
+
: `알 수 없는 ${cmd} 하위명령: ${args[1]} (가능: ${[..._SUBS[cmd]].join(', ')}) — 경로라면 ./${args[1]} 처럼 명시하세요`);
|
|
25312
|
+
return;
|
|
25313
|
+
}
|
|
25314
|
+
}
|
|
25315
|
+
const _explicit = arg('--path', null) || _cands.find(a => a && !a.startsWith('-') && a !== _strictPair[cmd]);
|
|
25023
25316
|
if (_explicit) {
|
|
25024
25317
|
const _abs = absRoot(_explicit);
|
|
25025
25318
|
if (!exists(_abs) || !fs.statSync(_abs).isDirectory()) {
|
|
@@ -25141,7 +25434,10 @@ async function main() {
|
|
|
25141
25434
|
if (cmd === 'agents' && args[1] === 'route') {
|
|
25142
25435
|
const _rtRaw = process.argv.slice(2); const _rtI = _rtRaw.indexOf('route'); const _rtToks = [];
|
|
25143
25436
|
for (let i = _rtI + 1; i < _rtRaw.length && _rtI >= 0; i++) {
|
|
25144
|
-
|
|
25437
|
+
// 1.36.103: 자체 인라인 목록(--tier/--approved-by/--path)을 단일 출처 _VALUE_FLAGS 로 교체.
|
|
25438
|
+
// 같은 클래스의 허용목록이 다섯 벌 있었고 각각 따로 뒤처졌다 — 목록이 늘어나면 갭도 같이 늘어난다.
|
|
25439
|
+
// 등호형(--tier=normal)은 값을 자기가 들고 있다 — 다음 토큰까지 건너뛰면 위치인자를 먹는다(codex 검수 #6 재현).
|
|
25440
|
+
if (_rtRaw[i].startsWith('--')) { if (!_rtRaw[i].includes('=') && _VALUE_FLAGS.has(_rtRaw[i]) && _rtRaw[i + 1] && !_rtRaw[i + 1].startsWith('--')) i++; continue; }
|
|
25145
25441
|
_rtToks.push(_rtRaw[i]);
|
|
25146
25442
|
}
|
|
25147
25443
|
// _providerAuth (1.36.91): 인증 축을 라우팅이 소비한다 — 확인 가능한 provider 만 ok/no, 나머지는 unknown.
|
|
@@ -25280,9 +25576,9 @@ async function main() {
|
|
|
25280
25576
|
if (cmd === 'anchors') return anchorsCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd(), args[1] && !args[1].startsWith('-') ? args[1] : null); // 1.36.36: 정체성앵커 초안
|
|
25281
25577
|
if (cmd === 'toggle') return _tgl.toggleCmd(arg('--path', process.cwd()), args[1], args[2], args[3], { has, VERSION }); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동)
|
|
25282
25578
|
// 1.36.53 (UR-0062): 기술 프로필 · 1.36.67 (F15): 변경 시 기존 leerness.html 동반 갱신(있을 때만)
|
|
25283
|
-
if (cmd === 'tech') return _tech.techCmd(arg('--path', null) || _taskPositionalPath(args,
|
|
25579
|
+
if (cmd === 'tech') return _tech.techCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd(), _optSub(args), { has, regenGraph: (r) => { if (exists(path.join(r, 'leerness.html'))) _graph.graphHtmlCmd(r, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, _loadTechProfile: _tech.loadTechProfile, quiet: true }); } });
|
|
25284
25580
|
// 1.36.98 (P-0013): 재사용 인벤토리 — 새 화면 조각을 만들기 전에 '이미 있는 것' 을 먼저 본다(reuse-map 의 UI 판).
|
|
25285
|
-
if (cmd === 'library') return require('../lib/library').libraryCmd(arg('--path', null) || _taskPositionalPath(args,
|
|
25581
|
+
if (cmd === 'library') return require('../lib/library').libraryCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd(), _optSub(args), { has, log, ok, warn, failJson });
|
|
25286
25582
|
if (cmd === 'integrity') { // 1.36.57 (감사 F-04): managed 문서 무결성 점검/복구
|
|
25287
25583
|
if (args[1] && args[1] !== 'check') { failJson(has('--json'), 'unknown_subcommand', `알 수 없는 integrity 하위명령: ${args[1]} (가능: check [--repair] [--force])`); return process.exit(process.exitCode || 1); }
|
|
25288
25584
|
return integrityCmd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd());
|
|
@@ -25293,7 +25589,7 @@ async function main() {
|
|
|
25293
25589
|
// 미지 플래그(--design/--features/--note)의 값이 positional 로 새는 것 차단 — 원시 argv 에서 플래그+값 스킵 (1.36.49 release note 패턴)
|
|
25294
25590
|
const _pvRaw = process.argv.slice(2); const _pvI = _pvRaw.indexOf('preview'); const _pvToks = [];
|
|
25295
25591
|
for (let i = _pvI + 1; i < _pvRaw.length && _pvI >= 0; i++) {
|
|
25296
|
-
if (_pvRaw[i].startsWith('--')) { if (['
|
|
25592
|
+
if (_pvRaw[i].startsWith('--')) { if (!_pvRaw[i].includes('=') && _VALUE_FLAGS.has(_pvRaw[i]) && _pvRaw[i + 1]) i++; continue; } // 1.36.75 (검수 #2): --mockup 값이 제목에 흡수되던 것. 1.36.103: 인라인 목록 → _VALUE_FLAGS (--select 등이 제목에 붙던 것) + 등호형은 다음 토큰을 먹지 않음(codex 검수 #5 재현)
|
|
25297
25593
|
_pvToks.push(_pvRaw[i]);
|
|
25298
25594
|
}
|
|
25299
25595
|
// 1.36.80 (UR-0067): 라이브 미리보기 — serve(로컬 서버) / mode(self|project) 는 별도 모듈로 위임
|
|
@@ -25306,7 +25602,7 @@ async function main() {
|
|
|
25306
25602
|
if (cmd === 'referee') {
|
|
25307
25603
|
const _rvRaw = process.argv.slice(2); const _rvI = _rvRaw.indexOf('referee'); const _rvToks = [];
|
|
25308
25604
|
for (let i = _rvI + 1; i < _rvRaw.length && _rvI >= 0; i++) {
|
|
25309
|
-
if (_rvRaw[i].startsWith('--')) { if (['
|
|
25605
|
+
if (_rvRaw[i].startsWith('--')) { if (!_rvRaw[i].includes('=') && _VALUE_FLAGS.has(_rvRaw[i]) && _rvRaw[i + 1]) i++; continue; } // 1.36.103: 인라인 목록 → _VALUE_FLAGS · 등호형 제외(codex 검수 #7)
|
|
25310
25606
|
_rvToks.push(_rvRaw[i]);
|
|
25311
25607
|
}
|
|
25312
25608
|
return refereeCmd(arg('--path', process.cwd()), _rvToks[0], _rvToks.slice(1));
|
|
@@ -25315,7 +25611,7 @@ async function main() {
|
|
|
25315
25611
|
if (cmd === 'bugfix') {
|
|
25316
25612
|
const _bfRaw = process.argv.slice(2); const _bfI = _bfRaw.indexOf('bugfix'); const _bfToks = [];
|
|
25317
25613
|
for (let i = _bfI + 1; i < _bfRaw.length && _bfI >= 0; i++) {
|
|
25318
|
-
if (_bfRaw[i].startsWith('--')) { if (['
|
|
25614
|
+
if (_bfRaw[i].startsWith('--')) { if (!_bfRaw[i].includes('=') && _VALUE_FLAGS.has(_bfRaw[i]) && _bfRaw[i + 1]) i++; continue; } // 1.36.103: 인라인 목록 → _VALUE_FLAGS · 등호형 제외(codex 검수 #8)
|
|
25319
25615
|
_bfToks.push(_bfRaw[i]);
|
|
25320
25616
|
}
|
|
25321
25617
|
// 1.36.88 (출하전 헌트 #7): `bugfix list <path>` 가 위치 경로를 조용히 무시하고 cwd 를 읽어,
|
package/lib/io.js
CHANGED
|
@@ -18,13 +18,21 @@ function log(s = '') {
|
|
|
18
18
|
// 1.36.54 (codex 7차 #7a): JSON 통과는 지연-fail(폴백 대기) 경로 전용 — failJson 이 이미 오류 JSON 을
|
|
19
19
|
// 방출했다면(_jsonErrEmitted) 추가 JSON 은 이중 문서가 되므로 stderr 강등.
|
|
20
20
|
const t = typeof s === 'string' ? s.trim() : '';
|
|
21
|
-
if (!_jsonErrEmitted && t && (t[0] === '{' || t[0] === '[')) { try { JSON.parse(t); console.log(s);
|
|
21
|
+
if (!_jsonErrEmitted && t && (t[0] === '{' || t[0] === '[')) { try { JSON.parse(t); console.log(s); return; } catch {} }
|
|
22
22
|
console.error(s); return;
|
|
23
23
|
}
|
|
24
|
-
console.log(s);
|
|
24
|
+
console.log(s);
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// 1.36.103: _stdoutWrote 는 'log() 를 불렀다' 가 아니라 '실제로 stdout 에 썼다' 여야 한다.
|
|
27
|
+
// 여기서 직접 세우면, gate 처럼 하위 출력을 삼키려고 process.stdout.write 를 no-op 으로 바꾼 구간에서
|
|
28
|
+
// 바이트는 한 개도 안 나갔는데 표식만 서서 폴백 오류 JSON 이 막혔다(실측: gate <없는경로> --json → stdout 0바이트).
|
|
29
|
+
// 추적은 stdout 래퍼 한 곳에만 둔다 — 래퍼가 교체된 동안의 출력은 정의상 stdout 에 도달하지 않은 것이다.
|
|
30
|
+
// 1.36.103: --json 모드에서 ✓/⚠ 는 절대 JSON 페이로드의 일부가 될 수 없다 — stdout 이 아니라 stderr 로 낸다.
|
|
31
|
+
// 기존에는 fail() 이 래치된 뒤에만 강등돼, 경고가 오류보다 먼저 나오면 stdout 을 오염시켰다.
|
|
32
|
+
// 실측: `reuse-map|retro|insights --include <없는프로젝트> --json` → stdout 이 "⚠ --include 무시: …" 한 줄뿐이라
|
|
33
|
+
// JSON.parse 가 실패한다(그리고 _stdoutWrote 가 서 버려 폴백 오류 JSON 도 막힌다).
|
|
34
|
+
function ok(s) { if (_argvJsonMode() && !_quiet) { console.error('✓ ' + s); return; } log('✓ ' + s); }
|
|
35
|
+
function warn(s) { if (_argvJsonMode() && !_quiet) { console.error('⚠ ' + s); return; } log('⚠ ' + s); }
|
|
28
36
|
// fail() 은 오류 신호 → exit code 1 설정 (CI/MCP/에이전트가 실패를 성공으로 오판 방지, UR-0045).
|
|
29
37
|
// 1.36.50 (codex 스윕 B, 40+ 실측): --json 요청 시 fail() 자체가 단일 JSON 계약을 이행 —
|
|
30
38
|
// 첫 오류를 {ok:false,error,code:'error'} 로 stdout 에 내고 이후 사람용 출력(log/ok/warn·추가 fail)은
|
|
@@ -39,14 +47,34 @@ let _failLatched = false; // fail() json 모드 발화 표식 (사람용 lo
|
|
|
39
47
|
let _firstFail = null;
|
|
40
48
|
let _stdoutWrote = false;
|
|
41
49
|
let _exitHooked = false;
|
|
50
|
+
// 1.36.103: 폴백 JSON 은 '지금의' process.stdout.write 가 아니라 로드 시점의 원본 fd 로 쓴다.
|
|
51
|
+
// gate 는 JSON 모드에서 하위 단계 출력을 삼키려고 write 를 no-op 으로 갈아끼우는데(bin 17162),
|
|
52
|
+
// 그 구간에서 process.exit() 가 나면 finally 가 돌지 않아 복원되지 않는다 → console.log 가 통째로 삼켜져
|
|
53
|
+
// `--json` 인데 stdout 0바이트 + exit 1 이 됐다(실측). 원본을 잡아두면 그 상태에서도 계약을 지킬 수 있다.
|
|
54
|
+
let _rawStdoutWrite = null;
|
|
42
55
|
let _latchArgv = null; // 래치는 argv 정체성 스코프 — 새 명령(REPL/인프로세스 테스트가 argv 교체) 시 자동 해제
|
|
43
56
|
// stdout 기록 추적은 스트림 레벨 — 일부 명령(audit 등)이 process.stdout.write 를 직접 캡처/사용해 log() 를 우회한다.
|
|
44
57
|
// 모듈 로드 시 1회 투명 래핑: 이후 어떤 경로(직접 write 포함)로든 stdout 에 쓰면 exit 폴백이 이중 JSON 을 내지 않는다.
|
|
45
58
|
{
|
|
46
|
-
|
|
47
|
-
process.stdout.write = function (...a) { _stdoutWrote = true; return
|
|
59
|
+
_rawStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
60
|
+
process.stdout.write = function (...a) { _stdoutWrote = true; return _rawStdoutWrite(...a); };
|
|
48
61
|
}
|
|
49
62
|
function _argvJsonMode() { try { return process.argv.includes('--json'); } catch { return false; } }
|
|
63
|
+
// 1.36.103: 폴백 오류 JSON 을 fail() 안이 아니라 모듈 로드 시 설치한다.
|
|
64
|
+
// fail() 을 거치지 않고 실패하는 경로(하위 점검이 직접 console.error 후 exit)에서는 훅이 아예 걸리지 않아
|
|
65
|
+
// `--json` 인데 stdout 이 완전히 비고 exit 1 인 상태가 나왔다(실측: gate <없는경로> --json → stdout 0바이트).
|
|
66
|
+
// 기계 소비자에게 '빈 stdout' 은 파싱 불가와 같다 — --json 은 실패해도 반드시 JSON 하나를 남긴다.
|
|
67
|
+
function _installJsonExitFallback() {
|
|
68
|
+
if (_exitHooked) return;
|
|
69
|
+
_exitHooked = true;
|
|
70
|
+
process.on('exit', (code) => {
|
|
71
|
+
if (code === 0 || _stdoutWrote || !_argvJsonMode()) return;
|
|
72
|
+
const payload = JSON.stringify({ ok: false, error: _firstFail != null ? _firstFail : 'command failed', code: 'error' }, null, 2) + '\n';
|
|
73
|
+
if (_rawStdoutWrite) { try { _rawStdoutWrite(payload); return; } catch { /* 원본 write 실패 시 console 로 폴백 */ } }
|
|
74
|
+
console.log(payload.trimEnd());
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
_installJsonExitFallback();
|
|
50
78
|
function _unlatchIfNewCmd() {
|
|
51
79
|
if ((_jsonErrEmitted || _failLatched) && _latchArgv !== process.argv) { _jsonErrEmitted = false; _failLatched = false; _firstFail = null; _quiet = false; _latchArgv = null; }
|
|
52
80
|
}
|
|
@@ -56,11 +84,9 @@ function fail(s) {
|
|
|
56
84
|
if (_firstFail == null) _firstFail = s;
|
|
57
85
|
_failLatched = true; _latchArgv = process.argv;
|
|
58
86
|
console.error('✗ ' + s);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
process.on('exit', (code) => { if (code !== 0 && _failLatched && !_stdoutWrote && _firstFail != null) console.log(JSON.stringify({ ok: false, error: _firstFail, code: 'error' }, null, 2)); });
|
|
63
|
-
}
|
|
87
|
+
// 1.36.54 (#7b): 최종 exit 이 0(명령이 회복)이면 폴백 오류 JSON 을 내지 않는다 — 성공 exit 에 ok:false 모순 방지.
|
|
88
|
+
// 1.36.103: 훅 설치는 모듈 로드 시점으로 옮겼다(_installJsonExitFallback) — fail() 을 안 거치는 실패 경로도 덮기 위해.
|
|
89
|
+
_installJsonExitFallback();
|
|
64
90
|
process.exitCode = 1; return;
|
|
65
91
|
}
|
|
66
92
|
console.log('✗ ' + s); process.exitCode = 1; // quiet 무시(오류는 항상 노출)
|
|
@@ -70,7 +96,8 @@ function fail(s) {
|
|
|
70
96
|
function failJson(jsonMode, code, msg) {
|
|
71
97
|
_unlatchIfNewCmd();
|
|
72
98
|
if (jsonMode) {
|
|
73
|
-
|
|
99
|
+
// 1.36.103: _stdoutWrote 는 래퍼가 세운다 — 여기서 세우면 삼켜진 출력을 '썼다' 고 오인한다.
|
|
100
|
+
if (!_jsonErrEmitted) { console.log(JSON.stringify({ ok: false, error: msg, code }, null, 2)); _jsonErrEmitted = true; _failLatched = true; _latchArgv = process.argv; }
|
|
74
101
|
else console.error('✗ ' + msg);
|
|
75
102
|
process.exitCode = 1;
|
|
76
103
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.103",
|
|
4
4
|
"description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
package/scripts/e2e.js
CHANGED
|
@@ -9623,6 +9623,133 @@ total++;
|
|
|
9623
9623
|
if (!ok) failed++;
|
|
9624
9624
|
}
|
|
9625
9625
|
|
|
9626
|
+
// ── 1.36.103 A: 값-플래그의 값이 사용자 데이터/경로로 흡수되지 않는다.
|
|
9627
|
+
// 허용목록이 소비자보다 45개 뒤처져 있었고, 같은 클래스의 목록이 다섯 벌로 갈라져 각자 따로 뒤처졌다.
|
|
9628
|
+
// 실측 재현: `preview add "제목" --select X` → 저장 제목 「제목 X」 · `review --persona security f.js` → 파일이 "security".
|
|
9629
|
+
{
|
|
9630
|
+
total++;
|
|
9631
|
+
let ok = false; const dbg = {};
|
|
9632
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-fl103-'));
|
|
9633
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a], { cwd: d, encoding: 'utf8', timeout: 120000 });
|
|
9634
|
+
const out = (r) => (r.stdout || '') + (r.stderr || '');
|
|
9635
|
+
try {
|
|
9636
|
+
fs.writeFileSync(path.join(d, 'package.json'), JSON.stringify({ name: 'fl', version: '0.1.0' }));
|
|
9637
|
+
fs.writeFileSync(path.join(d, 'sample.js'), 'function f(){ return 1; }\n');
|
|
9638
|
+
R(['init', d, '--yes']);
|
|
9639
|
+
const allText = () => { let s = ''; (function w(y) { for (const e of fs.readdirSync(y, { withFileTypes: true })) { const p = path.join(y, e.name); if (e.isDirectory()) { if (!/node_modules|\.git$/.test(e.name)) w(p); } else { try { s += fs.readFileSync(p, 'utf8'); } catch {} } } })(d); return s; };
|
|
9640
|
+
R(['preview', 'add', 'cart page', '--select', 'ZZA1']);
|
|
9641
|
+
R(['task', 'add', 'do work', '--priority', 'ZZA2']);
|
|
9642
|
+
R(['requests', 'add', 'add mode', '--summary', 'ZZA3']);
|
|
9643
|
+
const txt = allText();
|
|
9644
|
+
dbg.absorbed = ['ZZA1', 'ZZA2', 'ZZA3'].filter(m => txt.includes(m));
|
|
9645
|
+
// 제품이 스스로 안내하는 문법(persona list 의 "leerness review <file> --persona <id>")은 순서와 무관해야 한다
|
|
9646
|
+
const rev = R(['review', '--persona', 'security', 'sample.js']);
|
|
9647
|
+
dbg.revOk = rev.status === 0 && /sample\.js/.test(rev.stdout || '');
|
|
9648
|
+
// 오타는 조용히 삼키지 않고 stderr 로 알린다 — 판별 대조군: 실재 플래그엔 침묵
|
|
9649
|
+
const W = /알 수 없는 플래그|Unknown flag/;
|
|
9650
|
+
dbg.typoWarns = W.test(R(['task', 'add', 'x', '--prio', 'high']).stderr || '');
|
|
9651
|
+
dbg.realQuiet = !W.test(R(['task', 'list', '--json']).stderr || '') && !W.test(R(['audit', '--no-secret-scan']).stderr || '');
|
|
9652
|
+
// 경고가 stdout/JSON 을 오염시키지 않는다
|
|
9653
|
+
const jr = R(['pulse', '--json', '--nosuchflagzz']);
|
|
9654
|
+
let jsonOk = false; try { JSON.parse((jr.stdout || '').trim()); jsonOk = true; } catch {}
|
|
9655
|
+
dbg.jsonOk = jsonOk && !W.test(jr.stdout || '');
|
|
9656
|
+
// 등호형(--flag=value)이 다음 토큰까지 먹지 않는다 — 인라인 목록을 공유 집합으로 바꾸며 내가 만든 회귀(codex 검수 #5~#8)
|
|
9657
|
+
R(['preview', 'add', '--select=ZZEQ', 'equals title']);
|
|
9658
|
+
const pvp = path.join(d, '.harness', 'previews.json');
|
|
9659
|
+
const pvt = fs.existsSync(pvp) ? fs.readFileSync(pvp, 'utf8') : '';
|
|
9660
|
+
dbg.equals = pvt.includes('equals title') && !pvt.includes('ZZEQ');
|
|
9661
|
+
const rEq = R(['agents', 'route', '--tier=normal', 'fix the parser']);
|
|
9662
|
+
dbg.equalsRoute = !/비어 있|empty_task/.test((rEq.stdout || '') + (rEq.stderr || ''));
|
|
9663
|
+
// 동적 레지스트리(brief set 은 arg('--'+f.flag))에 오탐 경고 없음 + 진짜 오타는 경고(판별 대조군)
|
|
9664
|
+
dbg.dynamic = !W.test((R(['brief', 'set', '--intro', 'hello']).stderr) || '') && W.test((R(['brief', 'set', '--introo', 'x']).stderr) || '');
|
|
9665
|
+
ok = dbg.absorbed.length === 0 && dbg.revOk && dbg.typoWarns && dbg.realQuiet && dbg.jsonOk
|
|
9666
|
+
&& dbg.equals && dbg.equalsRoute && dbg.dynamic;
|
|
9667
|
+
} catch (e) { dbg.err = String(e && e.message).slice(0, 160); } finally { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} }
|
|
9668
|
+
console.log(ok ? '✓ A(1.36.103) 값-플래그 흡수 차단(제목/경로) + 등호형 무흡수 + 동적 레지스트리 무오탐 + 오타만 stderr 경고 + JSON 무오염'
|
|
9669
|
+
: '✗ 1.36.103 플래그 계약 실패 ' + JSON.stringify(dbg));
|
|
9670
|
+
if (!ok) failed++;
|
|
9671
|
+
}
|
|
9672
|
+
|
|
9673
|
+
// ── 1.36.103 B: --json 은 실패해도 stdout 에 파싱 가능한 JSON 하나를 남긴다.
|
|
9674
|
+
// 위반 5건이 서로 다른 이유였다 — 경고가 stdout 선점 / 실패 분기에 JSON 없음 / 출력을 삼키는 구간에서 표식만 서서 폴백 차단.
|
|
9675
|
+
{
|
|
9676
|
+
total++;
|
|
9677
|
+
let ok = false; const dbg = {};
|
|
9678
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-js103-'));
|
|
9679
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a], { cwd: d, encoding: 'utf8', timeout: 180000 });
|
|
9680
|
+
try {
|
|
9681
|
+
fs.writeFileSync(path.join(d, 'package.json'), JSON.stringify({ name: 'js', version: '0.1.0' }));
|
|
9682
|
+
R(['init', d, '--yes']);
|
|
9683
|
+
const J = (a) => { const r = R(a); const t = ((r.stdout) || '').trim(); let j = null; try { j = JSON.parse(t); } catch {} return { r, j, empty: !t }; };
|
|
9684
|
+
const g = J(['gate', 'no-such-gate', '--json']);
|
|
9685
|
+
const rc = J(['release', 'cleanup', '--json']);
|
|
9686
|
+
const inc = J(['reuse-map', '--include', 'no-such-project-zz', '--json']);
|
|
9687
|
+
dbg.gate = !!g.j && g.j.ok === false && !g.empty;
|
|
9688
|
+
dbg.release = !!rc.j && rc.j.code === 'not_a_git_repo';
|
|
9689
|
+
dbg.include = !!inc.j;
|
|
9690
|
+
// 대조군 — 성공 경로에 폴백이 끼어들면 "항상 ok:false" 로도 통과한다
|
|
9691
|
+
const p = J(['pulse', '--json']);
|
|
9692
|
+
dbg.success = p.r.status === 0 && !!p.j && p.j.ok !== false && p.j.version !== undefined;
|
|
9693
|
+
// --keep 은 준 값을 조용히 바꾸지 않는다 (0 은 유효, abc/-3 은 거부)
|
|
9694
|
+
const kb = J(['release', 'cleanup', '--keep', 'abc', '--json']);
|
|
9695
|
+
const kn = J(['release', 'cleanup', '--keep', '-3', '--json']);
|
|
9696
|
+
const ki = J(['init', path.join(d, 'KEEPTEST'), '--yes', '--keep', 'abc', '--json']); // 소비처 둘이 서로 다르게 굴던 것(codex 검수 #9)
|
|
9697
|
+
const k0 = J(['release', 'cleanup', '--keep', '0', '--json']); // 대조군: 0 은 유효
|
|
9698
|
+
dbg.keep = !!kb.j && kb.j.code === 'invalid_keep' && !!kn.j && kn.j.code === 'invalid_keep'
|
|
9699
|
+
&& !!ki.j && ki.j.code === 'invalid_keep' && !!k0.j && k0.j.code !== 'invalid_keep';
|
|
9700
|
+
// 개별 케이스가 아니라 계약을 건다 — 표면을 훑어 "stdout 은 언제나 JSON 문서 하나" 를 단언한다.
|
|
9701
|
+
// 여기 셋(consistency check · agent-mode tick · graph)은 --json 구현 자체가 없어 사람용 텍스트를 냈다
|
|
9702
|
+
// (게시된 1.36.102 에서 3/30 위반으로 확인 → 구현 후 0/30).
|
|
9703
|
+
const SWEEP = [
|
|
9704
|
+
['consistency', 'check', '--json'], ['agent-mode', 'tick', '--json'], ['graph', '--json'],
|
|
9705
|
+
['anchors', '--json'], ['capabilities', '--json'], ['verify', '--json'], ['status', '--json'],
|
|
9706
|
+
['enforce', 'status', '--json'], ['constraints', 'list', '--json'], ['next-action', 'list', '--json'],
|
|
9707
|
+
];
|
|
9708
|
+
const swept = SWEEP.map(a => ({ a, r: J(a) })).filter(x => !x.r.j);
|
|
9709
|
+
dbg.sweepBad = swept.map(x => x.a.join(' '));
|
|
9710
|
+
ok = dbg.gate && dbg.release && dbg.include && dbg.success && dbg.keep && swept.length === 0;
|
|
9711
|
+
} catch (e) { dbg.err = String(e && e.message).slice(0, 160); } finally { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} }
|
|
9712
|
+
console.log(ok ? '✓ B(1.36.103) --json 실패 경로 3종이 유효 JSON + 성공 경로 무간섭(대조군) + --keep 값 무시 금지 + 10표면 스윕'
|
|
9713
|
+
: '✗ 1.36.103 JSON 계약 실패 ' + JSON.stringify(dbg));
|
|
9714
|
+
if (!ok) failed++;
|
|
9715
|
+
}
|
|
9716
|
+
|
|
9717
|
+
// ── 1.36.103 C: 검증한 경로를 실제로 쓴다 + 하위명령을 경로로 오인하지 않는다.
|
|
9718
|
+
// 1.36.101 의 path-strict 가 args[1] 을 무조건 경로로 봐서 `leerness library page` 를 거부했고(게시된 1.36.102
|
|
9719
|
+
// 에서 확인), 같은 두 명령이 root 해석은 index 2 부터 시작해 검증된 경로를 건너뛰고 cwd 에 썼다.
|
|
9720
|
+
// 관측값은 파일시스템이다 — 문구가 아니라 '어디에 썼는지'.
|
|
9721
|
+
{
|
|
9722
|
+
total++;
|
|
9723
|
+
let ok = false; const dbg = {};
|
|
9724
|
+
const arena = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rt103-'));
|
|
9725
|
+
try {
|
|
9726
|
+
const cwdDir = path.join(arena, 'CWDPROJ'), target = path.join(arena, 'TARGETPROJ');
|
|
9727
|
+
for (const x of [cwdDir, target]) { fs.mkdirSync(x, { recursive: true }); fs.writeFileSync(path.join(x, 'package.json'), '{"name":"x","version":"0.1.0"}'); }
|
|
9728
|
+
const R = (a, cwd) => cp.spawnSync(process.execPath, [CLI, ...a], { cwd: cwd || cwdDir, encoding: 'utf8', timeout: 120000 });
|
|
9729
|
+
R(['init', cwdDir, '--yes'], cwdDir); R(['init', target, '--yes'], target);
|
|
9730
|
+
const tp = (x) => path.join(x, '.harness', 'tech-profile.json');
|
|
9731
|
+
const clear = (...ps) => { for (const p of ps) { try { fs.unlinkSync(p); } catch {} } };
|
|
9732
|
+
clear(tp(cwdDir), tp(target)); R(['tech', target]);
|
|
9733
|
+
dbg.wroteTarget = fs.existsSync(tp(target)) && !fs.existsSync(tp(cwdDir));
|
|
9734
|
+
clear(tp(cwdDir), tp(target)); R(['tech']);
|
|
9735
|
+
dbg.bareIsCwd = fs.existsSync(tp(cwdDir)) && !fs.existsSync(tp(target));
|
|
9736
|
+
const htmlC = path.join(cwdDir, 'leerness-library.html'), htmlT = path.join(target, 'leerness-library.html');
|
|
9737
|
+
clear(htmlC, htmlT);
|
|
9738
|
+
R(['library', 'page']); dbg.pageBare = fs.existsSync(htmlC);
|
|
9739
|
+
R(['library', 'page', target]); dbg.pageWithPath = fs.existsSync(htmlT);
|
|
9740
|
+
const lo = (R(['library', target]).stdout) || '';
|
|
9741
|
+
dbg.libTarget = lo.includes('TARGETPROJ') && !lo.includes('CWDPROJ');
|
|
9742
|
+
// 좁히면서 false-PASS 를 만들지 않았는가 — 맨이름 오타는 여전히 거부
|
|
9743
|
+
dbg.typo = R(['library', 'NOPEZZ']).status !== 0 && R(['tech', 'NOPEZZ']).status !== 0;
|
|
9744
|
+
// 1.36.101 무회귀 — 경로형 오타는 그대로 거부
|
|
9745
|
+
dbg.strictKept = R(['pulse', path.join(arena, 'NOPE-zz')]).status !== 0 && R(['library', './NOPE-zz']).status !== 0;
|
|
9746
|
+
ok = dbg.wroteTarget && dbg.bareIsCwd && dbg.pageBare && dbg.pageWithPath && dbg.libTarget && dbg.typo && dbg.strictKept;
|
|
9747
|
+
} catch (e) { dbg.err = String(e && e.message).slice(0, 160); } finally { try { fs.rmSync(arena, { recursive: true, force: true }); } catch {} }
|
|
9748
|
+
console.log(ok ? '✓ C(1.36.103) 준 경로에 적용(파일시스템 관측) + library page 회귀 수리 + 오타 거부 유지(1.36.101 무회귀)'
|
|
9749
|
+
: '✗ 1.36.103 root/하위명령 계약 실패 ' + JSON.stringify(dbg));
|
|
9750
|
+
if (!ok) failed++;
|
|
9751
|
+
}
|
|
9752
|
+
|
|
9626
9753
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
9627
9754
|
if (failed > 0) process.exit(1);
|
|
9628
9755
|
|