leerness 1.36.117 → 1.36.118
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 +19 -3
- package/lib/audit.js +27 -6
- package/lib/pure-utils.js +32 -2
- package/package.json +1 -1
- package/scripts/e2e.js +65 -4
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.118 하네스를 사용합니다. 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.118는 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.117는 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.118 릴리스 태그 이력** (수백 라운드) · _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.118: 2026-08-14
|
|
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.118';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -23879,7 +23879,15 @@ function adapterCmd(root, tool, opts = {}) {
|
|
|
23879
23879
|
const _mf = path.join(root, '.harness', 'manifest.json');
|
|
23880
23880
|
if (exists(_mf)) {
|
|
23881
23881
|
_assertStoreParsable(_mf, '.harness/manifest.json');
|
|
23882
|
-
|
|
23882
|
+
// 1.36.118 (검수 P1): `_assertStoreParsable` 은 **문법**만 본다. `null`·`[]`·`false`·`"x"` 는 유효 JSON 이라
|
|
23883
|
+
// 통과한 뒤 `.mode` 가 없어 조용히 standard 로 떨어졌다 — minimal 설치가 다시 뒤집힌다.
|
|
23884
|
+
// "손상이면 중단" 계약은 형상까지 봐야 지켜진다.
|
|
23885
|
+
let _mo = null;
|
|
23886
|
+
try { _mo = JSON.parse(read(_mf)); } catch { _mo = undefined; }
|
|
23887
|
+
if (!_mo || typeof _mo !== 'object' || Array.isArray(_mo)) {
|
|
23888
|
+
throw Object.assign(new Error(`.harness/manifest.json 이 객체가 아님(${Array.isArray(_mo) ? 'array' : typeof _mo}) — 설치 등급을 읽을 수 없어 중단합니다: ${_mf}`), { code: 'E_STORE_CORRUPT', file: _mf });
|
|
23889
|
+
}
|
|
23890
|
+
_instMode = _mo.mode || null;
|
|
23883
23891
|
}
|
|
23884
23892
|
}
|
|
23885
23893
|
// ⚠ 옵션이 둘로 갈린다: `minimal:true` 는 **어떤 키를 만들지**, `mode:'minimal'` 은 **템플릿을 줄일지** 다.
|
|
@@ -23916,7 +23924,15 @@ function adapterCmd(root, tool, opts = {}) {
|
|
|
23916
23924
|
// 1.36.117 (검수 P1): 깨진 항목을 보존했을 때도 "직접 호출 가능" 이라고 말하고 있었다 — 그건 거짓 성공이다.
|
|
23917
23925
|
const _brk = results.filter(r => r.broken);
|
|
23918
23926
|
if (_brk.length) _brk.forEach(r => warn(`${r.file} 의 leerness 항목에 command 가 없어 서버가 뜨지 않습니다 — 사용자 설정이라 보존했습니다`));
|
|
23919
|
-
|
|
23927
|
+
// 1.36.118 (자체 헌트): **보존**했을 때도 "leerness MCP verb 를 직접 호출 가능" 이라고 말하고 있었다.
|
|
23928
|
+
// 보존한 항목이 leerness 를 띄우는지는 우리가 확인하지 않았다(사용자가 다른 서버를 적어 뒀을 수도 있다) —
|
|
23929
|
+
// 우리가 쓴 항목이 하나라도 있을 때만 그렇게 말한다. 그 밖에는 무엇을 보존했는지만 알린다.
|
|
23930
|
+
const _wrote = results.filter(r => r.action === 'created' || r.action === 'updated');
|
|
23931
|
+
const _kept = results.filter(r => r.preserved && !r.broken);
|
|
23932
|
+
if (a.mcp && _wrote.length) log(` ℹ MCP 등록 — 이 도구가 leerness MCP verb(state_show/start/record/verify/handoff)를 직접 호출 가능`);
|
|
23933
|
+
_kept.forEach(r => log(` ℹ ${r.file} 의 기존 leerness 항목을 그대로 두었습니다 — 그 항목이 leerness MCP 를 띄우는지는 확인하지 않았습니다`));
|
|
23934
|
+
// 보존은 안전하지만 **막다른 길이면 안 된다** — 우리 것이 망가졌을 때 되돌릴 방법을 알려 준다(자체 헌트).
|
|
23935
|
+
if (_kept.length || _brk.length) log(` → 표준 설정으로 되돌리려면 그 파일의 "leerness" 항목을 지우고 이 명령을 다시 실행하세요`);
|
|
23920
23936
|
return;
|
|
23921
23937
|
}
|
|
23922
23938
|
|
package/lib/audit.js
CHANGED
|
@@ -56,11 +56,18 @@ function _collectGuardInputs(root) {
|
|
|
56
56
|
// 죽은 wrapper 가 `test` 보다 위에 있으면 둘이 같은 파일을 불러도 '죽은 참조' 로 고정돼
|
|
57
57
|
// 그 파일이 부르는 정상 가드가 고아로 나왔다(실측 재현). 참조한 스크립트를 전부 모은다.
|
|
58
58
|
const runners = [];
|
|
59
|
+
// ⚠ 출처가 **무조건**(CI 설정이 직접 부름)인 사실을 따로 기록한다. `viaScripts` 배열만 두면,
|
|
60
|
+
// CI 가 부르는 파일을 죽은 스크립트도 참조할 때 그 스크립트가 배열에 붙으면서 **무조건 출처가 사라지고**
|
|
61
|
+
// 러너 전체가 그 죽은 스크립트에 종속된다 → 정상 배선이 고아로 뒤집힌다(검수 지적, 실측 재현).
|
|
59
62
|
const push = (file, viaScript) => {
|
|
60
63
|
const found = runners.find(r => r.file === file);
|
|
61
|
-
if (found) {
|
|
64
|
+
if (found) {
|
|
65
|
+
if (!viaScript) found.unconditional = true;
|
|
66
|
+
else if (!found.viaScripts.includes(viaScript)) found.viaScripts.push(viaScript);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
62
69
|
const t = rd(path.join(root, file));
|
|
63
|
-
if (t != null) runners.push({ file, text: t, viaScript: viaScript || null, viaScripts: viaScript ? [viaScript] : [] });
|
|
70
|
+
if (t != null) runners.push({ file, text: t, viaScript: viaScript || null, viaScripts: viaScript ? [viaScript] : [], unconditional: !viaScript });
|
|
64
71
|
};
|
|
65
72
|
try { for (const f of fs.readdirSync(path.join(root, '.github', 'workflows'))) if (/\.ya?ml$/.test(f)) push(`.github/workflows/${f}`); } catch {}
|
|
66
73
|
// 1.36.116 (검수 #5): 정확히 `Makefile` 과 훅 2개만 읽었다 — `GNUmakefile`·소문자 `makefile`·`commit-msg` 를
|
|
@@ -72,15 +79,27 @@ function _collectGuardInputs(root) {
|
|
|
72
79
|
// 러너가 부르는 스크립트 파일도 러너다 — 동적 열거자가 대개 거기 있다(그걸 못 보면 오탐이 폭발한다).
|
|
73
80
|
// 1.36.116 (검수 #5): 경로에 `scripts/` 가 있어야만 수집했다 — `tools/run-ci.mjs`·`ci/run.mjs`·`bin/run-checks.js`
|
|
74
81
|
// 같은 흔한 배치를 놓치고, 그 안의 동적 열거자를 못 보면 다시 오탐이 폭발한다. 디렉터리 이름을 넓힌다.
|
|
75
|
-
|
|
82
|
+
// 경로 구분자는 `/`·`\` 둘 다 받는다 — Windows CI 는 `.\bin\cli.js` 로 적는다(검수 P2 미탐).
|
|
83
|
+
const _FILE_RE = /[\w./\\-]*(?:scripts|tools|ci|bin|tasks)[/\\][\w.-]+\.(?:m?js|cjs|ts|sh)/g;
|
|
84
|
+
const _norm = (x) => String(x).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
76
85
|
// CI 설정 파일이 부르는 것은 출처 null(무조건 유효한 러너), npm 스크립트가 부르는 것은 그 스크립트가 출처다.
|
|
77
86
|
// ⚠ 자기 진입점 제외는 **CI 설정이 직접 부르는 경우까지** 막으면 안 된다(검수 P2) — 그 파일이 실제로
|
|
78
87
|
// 가드를 배선하고 있으면 그걸 못 봐서 정상 가드가 고아로 뒤집힌다. CI 가 부르면 그건 러너가 맞다.
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
// ⚠ 단 **주석 줄에서 수집하면 안 된다**: `# debug: node bin/cli.js` 한 줄로 제품 진입점이 러너가 되고
|
|
89
|
+
// 그 안의 문자열이 진짜 고아를 덮는다(검수 P2 — 자기참조가 주석 경로로 되살아난다).
|
|
90
|
+
const _noComment = (s) => String(s).split('\n').filter(l => !/^\s*(#|\/\/|--|;)/.test(l)).join('\n');
|
|
91
|
+
for (const r of runners.slice()) {
|
|
92
|
+
const txt = _noComment(r.text);
|
|
93
|
+
for (const m of txt.matchAll(_FILE_RE)) push(_norm(m[0]), null);
|
|
94
|
+
// 디렉터리 접두가 없는 진입점(`"main": "index.js"`)은 위 정규식이 못 잡는다 — 선언된 bin/main 만 이름으로 확인한다.
|
|
95
|
+
for (const own of ownEntries) {
|
|
96
|
+
if (/[/\\]/.test(own)) continue;
|
|
97
|
+
const esc = own.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
98
|
+
if (new RegExp(`(^|[^\\w.\\-/\\\\])\\.?[/\\\\]?${esc}([^\\w.-]|$)`, 'm').test(txt)) push(own, null);
|
|
99
|
+
}
|
|
81
100
|
}
|
|
82
101
|
for (const [name, body] of Object.entries(packageScripts)) for (const m of String(body || '').matchAll(_FILE_RE)) {
|
|
83
|
-
const rel = m[0]
|
|
102
|
+
const rel = _norm(m[0]);
|
|
84
103
|
if (!ownEntries.has(rel)) push(rel, name); // 자기 진입점은 러너가 아니다 (위 주석)
|
|
85
104
|
}
|
|
86
105
|
// scripts/ 아래 가드처럼 보이는 파일(2단계까지)
|
|
@@ -203,6 +222,8 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
203
222
|
log(` → CI 워크플로나 집계 스크립트(예: test:ci)에 배선하거나, 더 이상 안 쓰면 지우세요`);
|
|
204
223
|
_finding('orphan_guard', 'warn', 'checks exist but no runner invokes them', {
|
|
205
224
|
orphanScripts: g.orphanScripts, orphanFiles: g.orphanFiles, scanIncomplete: g.scanIncomplete,
|
|
225
|
+
// 1.36.118 (검수 P3): 고아가 **있을 때**도 범위 정보가 필요하다 — 0건 분기에만 넣어 계약이 갈렸다.
|
|
226
|
+
monorepoOutOfScope: g.monorepoOutOfScope, deferred: g.deferred,
|
|
206
227
|
guardCount: g.guardCount, dynamicRunners: g.dynamicRunners, enumPrefixes: g.enumPrefixes, reason: g.reason,
|
|
207
228
|
});
|
|
208
229
|
} else if (g.scanIncomplete || g.deferred || g.monorepoOutOfScope) {
|
package/lib/pure-utils.js
CHANGED
|
@@ -1187,6 +1187,7 @@ function _detectOrphanGuards(input) {
|
|
|
1187
1187
|
};
|
|
1188
1188
|
// 출처가 여럿이면 **하나라도 살아 있으면** 그 러너는 실제로 돈다. 하나만 보면 선언 순서가 판정을 바꾼다(실측).
|
|
1189
1189
|
const _liveRunner = (r) => {
|
|
1190
|
+
if (r.unconditional) return true; // CI 설정이 직접 부르는 파일 — 어떤 스크립트에도 종속되지 않는다
|
|
1190
1191
|
const via = Array.isArray(r.viaScripts) && r.viaScripts.length ? r.viaScripts : (r.viaScript ? [r.viaScript] : []);
|
|
1191
1192
|
return via.length === 0 || via.some(n => reachable.has(n) || coveredByEnum(n));
|
|
1192
1193
|
};
|
|
@@ -1206,8 +1207,22 @@ function _detectOrphanGuards(input) {
|
|
|
1206
1207
|
// ⚠ 줄 필터를 **CI 설정 파일에까지** 걸면 matrix/여러 줄 배선을 놓친다(검수 P2):
|
|
1207
1208
|
// `script: [check:a, check:b]` 은 `npm` 이 다른 줄에 있다. CI 설정은 그 자체가 배선이므로 전문을 본다.
|
|
1208
1209
|
// 줄 필터는 **코드 파일**에만 건다 — 거기서만 이름이 데이터로 등장한다(우리 e2e.js 가 그랬다).
|
|
1210
|
+
// ⚠ CI 설정을 전문으로 보되 **설명용 필드는 뺀다**: `- name: TODO check:leaf 를 붙이자`,
|
|
1211
|
+
// `env: { FOO: check:x }`, artifact 이름 같은 것이 그것만으로 '호출됨' 이 됐다(자체 헌트 + 검수 재현).
|
|
1212
|
+
// 금지 목록은 유계가 아니므로(라벨 키는 얼마든지 새로 생긴다) **허용 목록**으로 뒤집는다:
|
|
1213
|
+
// `key: value` 꼴은 key 가 실행 키일 때만 보고, 키가 없는 줄(목록 항목·연속 줄)과
|
|
1214
|
+
// 실행 토큰이 있는 줄은 그대로 본다. 진짜 배선이면 `run:` 줄이 반드시 있다.
|
|
1215
|
+
const _KEY_RE = /^\s*-?\s*([A-Za-z_][\w.-]*)\s*:\s/;
|
|
1216
|
+
const _RUN_KEYS = /^(?:run|cmd|command|commands|entrypoint|args|script|scripts|shell|uses|with)$/i;
|
|
1217
|
+
const _INVOKE = /\b(?:npm|npx|yarn|pnpm|make|exec)\b/;
|
|
1218
|
+
const _configLineOk = (l) => {
|
|
1219
|
+
const m = _KEY_RE.exec(l);
|
|
1220
|
+
if (!m) return true; // 목록 항목(`- check:a`)·연속 줄
|
|
1221
|
+
return _RUN_KEYS.test(m[1]) || _INVOKE.test(l);
|
|
1222
|
+
};
|
|
1209
1223
|
const _raw = _dropCommentLines(String(r.text || ''));
|
|
1210
|
-
const t = _isConfigRunner(r.file)
|
|
1224
|
+
const t = _isConfigRunner(r.file)
|
|
1225
|
+
? _raw.split('\n').filter(_configLineOk).join('\n')
|
|
1211
1226
|
: _raw.split('\n').filter(l => /\b(?:npm|npx|yarn|pnpm|make|run|exec)\b/.test(l)).join('\n');
|
|
1212
1227
|
for (const n of names) if (!roots.has(n) && _has(t, n)) { roots.add(n); grew = true; }
|
|
1213
1228
|
grew = true;
|
|
@@ -1235,9 +1250,24 @@ function _detectOrphanGuards(input) {
|
|
|
1235
1250
|
// `b/check.js` 까지 덮어 버린다. basename 은 그 이름이 유일할 때만 근거로 쓴다.
|
|
1236
1251
|
const _baseCount = new Map();
|
|
1237
1252
|
for (const f of scriptFiles) { const b = String(f).split(/[\\/]/).pop(); _baseCount.set(b, (_baseCount.get(b) || 0) + 1); }
|
|
1253
|
+
// ⚠ 그런데 유일성만으로 끄면 **해석 가능한 상대 호출**까지 잃는다(검수 P2): `scripts/a/run.js` 안의
|
|
1254
|
+
// `require('./check.js')` 는 `scripts/a/check.js` 를 가리키는데, 동명 파일이 있다는 이유로 그것까지 고아가 됐다.
|
|
1255
|
+
// 참조한 러너의 디렉터리를 기준으로 상대 경로를 **해석해서** 덮인 집합에 넣는다.
|
|
1256
|
+
const _resolved = new Set();
|
|
1257
|
+
for (const r of runners.filter(_liveRunner)) {
|
|
1258
|
+
const dir = String(r.file || '').replace(/\\/g, '/').split('/').slice(0, -1);
|
|
1259
|
+
for (const m of String(r.text || '').matchAll(/(\.{1,2}(?:\/[\w.-]+)+\.(?:m?js|cjs|ts|sh))/g)) {
|
|
1260
|
+
const segs = dir.slice();
|
|
1261
|
+
for (const s of m[1].split('/')) {
|
|
1262
|
+
if (s === '.' || s === '') continue;
|
|
1263
|
+
if (s === '..') segs.pop(); else segs.push(s);
|
|
1264
|
+
}
|
|
1265
|
+
_resolved.add(segs.join('/'));
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1238
1268
|
const orphanFiles = unknownOrchestrator ? [] : scriptFiles.filter(f => {
|
|
1239
1269
|
const full = String(f).replace(/\\/g, '/');
|
|
1240
|
-
if (allText.includes(full)) return false;
|
|
1270
|
+
if (allText.includes(full) || _resolved.has(full)) return false;
|
|
1241
1271
|
const base = full.split('/').pop();
|
|
1242
1272
|
if (_baseCount.get(base) === 1 && allText.includes(base)) return false;
|
|
1243
1273
|
return true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.118",
|
|
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
|
@@ -11337,6 +11337,11 @@ total++;
|
|
|
11337
11337
|
const runnable = !!(val && typeof val === 'object' && val.command);
|
|
11338
11338
|
if (!runnable && expect === 'preserve' && !/command 가 없어/.test(out)) bad.push(`_ours:${label}에_경고없음`);
|
|
11339
11339
|
if (!runnable && expect === 'preserve' && /직접 호출 가능/.test(out)) bad.push(`_ours:${label}에_거짓성공`);
|
|
11340
|
+
// (자체 헌트) **보존**했을 때도 "leerness MCP verb 를 직접 호출 가능" 이라고 말하고 있었다 —
|
|
11341
|
+
// 보존한 항목이 leerness 를 띄우는지 우리는 확인하지 않았다. 우리가 쓴 항목이 있을 때만 그렇게 말한다.
|
|
11342
|
+
if (expect === 'preserve' && /직접 호출 가능/.test(out)) bad.push(`_ours:${label}_보존인데_호출가능주장`);
|
|
11343
|
+
if (expect === 'preserve' && !/되돌리려면/.test(out)) bad.push(`_ours:${label}_복구경로_안내없음`);
|
|
11344
|
+
if (expect === 'update' && !/직접 호출 가능/.test(out)) bad.push(`_ours:${label}_갱신인데_안내없음`);
|
|
11340
11345
|
}
|
|
11341
11346
|
}
|
|
11342
11347
|
// ⑧ (검수 P3) Desktop 상태·영문 표면을 **실제로** 검증한다 — 지금까지는 필드 존재만 봤다.
|
|
@@ -11347,19 +11352,31 @@ total++;
|
|
|
11347
11352
|
const hKo = String(cp.spawnSync(process.execPath, [CLI, 'mcp', 'install', '--path', ko], { cwd: ko, encoding: 'utf8', timeout: 120000, env: ENV }).stdout || '');
|
|
11348
11353
|
if (/[가-힣]/.test(hEn)) bad.push(`desktop:영문표면에한국어(${(hEn.match(/[가-힣]+/g) || [])[0]})`);
|
|
11349
11354
|
if (!/[가-힣]/.test(hKo)) bad.push('desktop:한국어표면이비어있음');
|
|
11355
|
+
// 한국어 없음만 보면 **영문 안내를 비워도** 통과한다(검수 P3) — 영문 문구가 실제로 있는지 본다.
|
|
11356
|
+
if (!/Config file/.test(hEn) || !/mcpServers/.test(hEn) || !/save the whole file|add just this one entry/i.test(hEn)) bad.push('desktop:영문안내_내용없음');
|
|
11350
11357
|
// `mcpServers` 키가 없는 파일을 위한 안내가 있어야 한다(ⓐ 를 따르면 기존 설정이 날아간다)
|
|
11351
11358
|
if (!/mcpServers[^\n]*(키가 없|no .*key)/i.test(hKo)) bad.push('desktop:mcpServers키없는경우_안내없음');
|
|
11352
11359
|
// `registeredState` **네 상태**를 격리해서 실제로 잰다(검수 P3: 지금까지는 필드 존재만 봤다 —
|
|
11353
11360
|
// `_regState` 를 상수 'none' 으로 바꾸는 변이가 생존했다). 사용자의 진짜 설정은 건드리지 않는다:
|
|
11354
11361
|
// Windows 는 APPDATA, Linux 는 XDG_CONFIG_HOME 으로 앱 설정 경로를 임시 디렉터리에 묶는다.
|
|
11355
11362
|
const _cfgVar = process.platform === 'win32' ? 'APPDATA' : (process.platform === 'linux' ? 'XDG_CONFIG_HOME' : null);
|
|
11363
|
+
// macOS 는 경로가 homedir 고정이라 격리할 수 없다 → 그 플랫폼에선 이 깊은 검사가 통째로 건너뛰어진다(검수 P3).
|
|
11364
|
+
// 건너뛸 때도 **공허하게 통과하지는 않도록** 최소 계약(4상태 중 하나)은 잰다.
|
|
11365
|
+
if (!_cfgVar) {
|
|
11366
|
+
const rr = cp.spawnSync(process.execPath, [CLI, 'mcp', 'install', '--path', ko, '--json'], { cwd: ko, encoding: 'utf8', timeout: 120000, env: ENV });
|
|
11367
|
+
let jj = null; try { jj = JSON.parse(String(rr.stdout || '').trim()); } catch {}
|
|
11368
|
+
if (!jj || !['ok', 'broken', 'none', 'unreadable'].includes(jj.registeredState)) bad.push('desktop:상태값이_계약밖');
|
|
11369
|
+
}
|
|
11356
11370
|
if (_cfgVar) {
|
|
11357
11371
|
const states = [
|
|
11358
11372
|
[{ mcpServers: { leerness: { command: 'npx', args: ['-y', 'leerness', 'mcp', 'serve'] } } }, 'ok'],
|
|
11359
11373
|
[{ mcpServers: { leerness: {} } }, 'broken'],
|
|
11360
11374
|
[{ mcpServers: { leerness: [] } }, 'broken'],
|
|
11361
11375
|
[{ mcpServers: { leerness: { command: ' ' } } }, 'broken'],
|
|
11376
|
+
[{ mcpServers: [] }, 'broken'], // 컨테이너가 배열 — 'none' 으로 새면 안 된다(검수 P3)
|
|
11377
|
+
[{ mcpServers: false }, 'broken'],
|
|
11362
11378
|
[{ mcpServers: { other: { command: 'x' } } }, 'none'],
|
|
11379
|
+
[{}, 'none'],
|
|
11363
11380
|
['{ broken', 'unreadable'],
|
|
11364
11381
|
];
|
|
11365
11382
|
for (const [content, expect] of states) {
|
|
@@ -11734,6 +11751,20 @@ total++;
|
|
|
11734
11751
|
const rr = (vi.runners.find(r => /tools[\\/]x\.js$/.test(r.file)) || {});
|
|
11735
11752
|
if (!(rr.viaScripts || []).includes('test')) bad.push('수집기가_출처를_하나만_담음');
|
|
11736
11753
|
if (PU._detectOrphanGuards(vi).orphanScripts.length) bad.push('실측_선언순서_오탐');
|
|
11754
|
+
// (검수) CI 설정이 **직접 부르는** 파일은 어떤 스크립트에도 종속되지 않아야 한다.
|
|
11755
|
+
// `viaScripts` 배열만 두면 죽은 스크립트가 그 배열에 붙으면서 무조건 출처가 사라져
|
|
11756
|
+
// 정상 배선이 고아로 뒤집혔다(오탐).
|
|
11757
|
+
const d5 = path.join(sb4, 'ci');
|
|
11758
|
+
fs.mkdirSync(path.join(d5, 'tools'), { recursive: true });
|
|
11759
|
+
fs.mkdirSync(path.join(d5, '.github', 'workflows'), { recursive: true });
|
|
11760
|
+
fs.writeFileSync(path.join(d5, 'package.json'), JSON.stringify({ name: 'c', version: '0.1.0',
|
|
11761
|
+
scripts: { 'dead-wrapper': 'node tools/ci.js', test: 'node t.js', 'check:leaf': 'node l.js' } }));
|
|
11762
|
+
fs.writeFileSync(path.join(d5, '.github', 'workflows', 'ci.yml'), 'jobs:\n a:\n steps:\n - run: node tools/ci.js\n');
|
|
11763
|
+
fs.writeFileSync(path.join(d5, 'tools', 'ci.js'), 'sh("npm run check:leaf")\n');
|
|
11764
|
+
const ci = AUD._collectGuardInputs(d5);
|
|
11765
|
+
const cr = ci.runners.find(r => /tools[\\/]ci\.js$/.test(r.file)) || {};
|
|
11766
|
+
if (!cr.unconditional) bad.push('CI직접호출_출처_유실');
|
|
11767
|
+
if (PU._detectOrphanGuards(ci).orphanScripts.length) bad.push('CI직접호출인데_고아로_오탐');
|
|
11737
11768
|
} finally { try { fs.rmSync(sb4, { recursive: true, force: true }); } catch {} }
|
|
11738
11769
|
}
|
|
11739
11770
|
// ⑰ (검수 P2) 와일드카드가 있다고 실행은 아니다 — `echo "test:*"` 는 아무것도 안 돌린다.
|
|
@@ -11780,12 +11811,38 @@ total++;
|
|
|
11780
11811
|
scriptFiles: [],
|
|
11781
11812
|
});
|
|
11782
11813
|
if (r19d.orphanScripts.length) bad.push('산_열거자를_무시:' + r19d.orphanScripts.join(','));
|
|
11814
|
+
// 고정점의 **다단 전파**: 열거자 A 가 붙여 준 접두 덕에 열거자 B 가 나중에 live 가 되는 순서.
|
|
11815
|
+
// 한 번만 훑는 구현으로 바꾸는 변이가 여기서 죽는다(검수 P3: 지금까지는 1단계만 봤다).
|
|
11816
|
+
const r19e = PU._detectOrphanGuards({
|
|
11817
|
+
packageScripts: { test: 'node scripts/enum-a.mjs', 'check:enum-b': 'node scripts/enum-b.mjs', 'verify:leaf': 'node v.js', 'scan:dead': 'node d.js' },
|
|
11818
|
+
runners: [
|
|
11819
|
+
{ file: '.github/workflows/ci.yml', text: 'run: npm test', viaScript: null, viaScripts: [], unconditional: true },
|
|
11820
|
+
{ file: 'scripts/enum-a.mjs', text: `const pkg=require("./package.json");\nfor (const n of ${_ENUM_LIT}) { if (n.startsWith("check:")) run(n); }`, viaScript: 'test', viaScripts: ['test'] },
|
|
11821
|
+
{ file: 'scripts/enum-b.mjs', text: `const pkg=require("./package.json");\nfor (const n of ${_ENUM_LIT}) { if (n.startsWith("verify:")) run(n); }`, viaScript: 'check:enum-b', viaScripts: ['check:enum-b'] },
|
|
11822
|
+
],
|
|
11823
|
+
scriptFiles: [],
|
|
11824
|
+
});
|
|
11825
|
+
if (r19e.orphanScripts.includes('verify:leaf')) bad.push('다단_열거자_전파_안됨');
|
|
11826
|
+
if (!r19e.enumPrefixes.includes('verify:')) bad.push('2단계_열거자_접두_미수집');
|
|
11827
|
+
// 판별력 — 어느 열거자도 안 태우는 것은 여전히 고아여야 한다(전파가 전부를 덮으면 안 된다)
|
|
11828
|
+
if (!r19e.orphanScripts.includes('scan:dead')) bad.push('다단전파가_범위밖까지_덮음');
|
|
11783
11829
|
// ⑳ (검수 P2) npm 이 스스로 돌리는 생명주기 스크립트(postinstall 등)는 루트다 — 그 잎을 고아라 하면 오탐이다.
|
|
11784
11830
|
const r20 = PU._detectOrphanGuards({
|
|
11785
11831
|
packageScripts: { postinstall: 'npm run check:generated', 'check:generated': 'node g.js' },
|
|
11786
11832
|
runners: [], scriptFiles: [],
|
|
11787
11833
|
});
|
|
11788
11834
|
if (r20.orphanScripts.length) bad.push('생명주기_잎_오탐:' + r20.orphanScripts.join(','));
|
|
11835
|
+
// ㉑ (자체 헌트) CI 설정을 **전문**으로 보게 바꾼 대가 — 사람이 읽는 필드(`name:`)에 이름이 있으면
|
|
11836
|
+
// 그것만으로 '호출됨' 이 됐다. 설명용 키는 빼되, 진짜 배선(`run:`·matrix)은 여전히 잡혀야 한다.
|
|
11837
|
+
for (const [label, text, expectOrphan] of [
|
|
11838
|
+
['산문만', 'jobs:\n a:\n steps:\n - name: TODO check:leaf 붙이기\n run: echo skip\n', true],
|
|
11839
|
+
['name 뒤 실제 run', 'jobs:\n a:\n steps:\n - name: Run check:leaf\n run: npm run check:leaf\n', false],
|
|
11840
|
+
['matrix', 'jobs:\n a:\n strategy:\n matrix:\n script: [check:leaf]\n steps:\n - run: npm run ${{ matrix.script }}\n', false],
|
|
11841
|
+
]) {
|
|
11842
|
+
const rr = PU._detectOrphanGuards({ packageScripts: { test: 'node t.js', 'check:leaf': 'node l.js' },
|
|
11843
|
+
runners: [{ file: '.github/workflows/ci.yml', text, viaScript: null, viaScripts: [] }], scriptFiles: [] });
|
|
11844
|
+
if (rr.orphanScripts.includes('check:leaf') !== expectOrphan) bad.push(`CI설명키:${label}(고아=${!expectOrphan})`);
|
|
11845
|
+
}
|
|
11789
11846
|
// ⑮ 자기참조 재발 가드. 이 저장소 **자신**을 수집해 보면 `scripts/e2e.js` 가 러너로 담긴다.
|
|
11790
11847
|
// 그 안에 열거자 모양의 픽스처 문자열을 그대로 쓰면 leerness 가 자기 테스트를 진짜 열거자로 읽고
|
|
11791
11848
|
// 자기 자신의 고아 판정을 통째로 무력화한다(이번 라운드에 두 번 발생). 리터럴 쪼개기가 유지되는지 실측으로 잰다.
|
|
@@ -11812,10 +11869,10 @@ total++;
|
|
|
11812
11869
|
const selfNoE2e = PU._detectOrphanGuards(Object.assign({}, selfNoBin,
|
|
11813
11870
|
{ runners: selfNoBin.runners.filter(r => !/e2e\.js$/.test(r.file)) }));
|
|
11814
11871
|
// ⚠ 비교 대상을 `orphanScripts` 하나로 두면 약한 불변식이다(검수 P3) — 판정 전체를 비교한다.
|
|
11815
|
-
// `orphanFiles`
|
|
11816
|
-
//
|
|
11817
|
-
//
|
|
11818
|
-
|
|
11872
|
+
// `orphanFiles` 를 통째로 빼면 자기참조 회귀를 못 잡는다(검수 P3) — **측정된 차이만 허용**한다.
|
|
11873
|
+
// 허용치: `scripts/e2e-core.js`. 근거는 실측이다 — e2e.js 가 그 경로를 언급하고, 그 파일은
|
|
11874
|
+
// 고아인 `test:core` 만 실행하므로 e2e.js 를 빼면 고아로 뜬다. 그 밖의 차이는 자기참조 회귀다.
|
|
11875
|
+
const _FILE_ALLOW = new Set(['scripts/e2e-core.js']);
|
|
11819
11876
|
const _shape = (x) => JSON.stringify({
|
|
11820
11877
|
s: x.orphanScripts.slice().sort(),
|
|
11821
11878
|
d: x.dynamicRunners.slice().sort(), p: x.enumPrefixes.slice().sort(),
|
|
@@ -11824,6 +11881,10 @@ total++;
|
|
|
11824
11881
|
if (_shape(selfBase) !== _shape(selfNoE2e)) {
|
|
11825
11882
|
bad.push(`자기참조:테스트파일이_판정을_바꿈(${selfBase.orphanScripts.join(',')} vs ${selfNoE2e.orphanScripts.join(',')})`);
|
|
11826
11883
|
}
|
|
11884
|
+
const _fdiff = [...new Set([...selfBase.orphanFiles, ...selfNoE2e.orphanFiles])]
|
|
11885
|
+
.filter(f => selfBase.orphanFiles.includes(f) !== selfNoE2e.orphanFiles.includes(f))
|
|
11886
|
+
.filter(f => !_FILE_ALLOW.has(String(f).replace(/\\/g, '/')));
|
|
11887
|
+
if (_fdiff.length) bad.push(`자기참조:테스트파일이_파일판정을_바꿈(${_fdiff.join(',')})`);
|
|
11827
11888
|
// 셋업 유효성 — e2e.js 가 실제로 live 러너여야 이 불변식이 의미가 있다(비어 있으면 공허하게 통과한다)
|
|
11828
11889
|
if (!selfNoBin.runners.some(r => /e2e\.js$/.test(r.file))) bad.push('자기참조:불변식이_공허(e2e.js가입력에없음)');
|
|
11829
11890
|
}
|