leerness 1.36.119 → 1.36.120
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 +5 -2
- package/lib/audit.js +19 -5
- package/lib/pure-utils.js +22 -5
- package/package.json +1 -1
- package/scripts/e2e.js +112 -2
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.120 하네스를 사용합니다. 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.120는 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.119는 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.120 릴리스 태그 이력** (수백 라운드) · _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.120: 2026-08-15
|
|
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.120';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -23935,7 +23935,10 @@ function adapterCmd(root, tool, opts = {}) {
|
|
|
23935
23935
|
// 우리가 쓴 항목이 하나라도 있을 때만 그렇게 말한다. 그 밖에는 무엇을 보존했는지만 알린다.
|
|
23936
23936
|
const _wrote = mcpResults.filter(r => r.action === 'created' || r.action === 'updated');
|
|
23937
23937
|
const _kept = mcpResults.filter(r => r.preserved && !r.broken);
|
|
23938
|
-
|
|
23938
|
+
// 1.36.120 (검수·자체 재현): cursor 는 MCP 설정이 **둘**(루트 `.mcp.json` + `.cursor/mcp.json`)이다.
|
|
23939
|
+
// 한쪽만 우리가 썼는데 "이 도구가 직접 호출 가능" 이라고 하면, 정작 그 도구가 읽는 파일은 남의 설정일 수 있다.
|
|
23940
|
+
// 주장은 **파일 단위**로 한다 — 우리가 쓴 파일만 이름을 대고 말한다.
|
|
23941
|
+
_wrote.forEach(r => log(` ℹ ${r.file} 에 MCP 등록 — 이 파일을 읽는 도구는 leerness MCP verb(state_show/start/record/verify/handoff)를 직접 호출 가능`));
|
|
23939
23942
|
_kept.forEach(r => log(` ℹ ${r.file} 의 기존 leerness 항목을 그대로 두었습니다 — 그 항목이 leerness MCP 를 띄우는지는 확인하지 않았습니다`));
|
|
23940
23943
|
// 보존은 안전하지만 **막다른 길이면 안 된다** — 우리 것이 망가졌을 때 되돌릴 방법을 알려 준다(자체 헌트).
|
|
23941
23944
|
if (_kept.length || _brk.length) log(` → 표준 설정으로 되돌리려면 그 파일의 "leerness" 항목을 지우고 이 명령을 다시 실행하세요`);
|
package/lib/audit.js
CHANGED
|
@@ -91,9 +91,17 @@ function _collectGuardInputs(root) {
|
|
|
91
91
|
// ⚠ 단 **주석 줄에서 수집하면 안 된다**: `# debug: node bin/cli.js` 한 줄로 제품 진입점이 러너가 되고
|
|
92
92
|
// 그 안의 문자열이 진짜 고아를 덮는다(검수 P2 — 자기참조가 주석 경로로 되살아난다).
|
|
93
93
|
// CMD 스텝의 `REM`·`::` 도 주석이다(검수 재현: `REM debug: node .\bin\phantom.js` 한 줄로 제품 진입점이 러너가 됐다).
|
|
94
|
-
|
|
94
|
+
// ⚠ `--`·`;` 는 여기서 다루는 파일 종류(YAML·Makefile·셸·JS)에서 주석이 아니다 — 오히려 여러 줄 명령의
|
|
95
|
+
// 연속 인자(`--file=scripts/x.js`)를 지워 파일 참조를 잃는다. 주석 표기는 실제로 쓰이는 것만 둔다.
|
|
96
|
+
const _noComment = (s) => String(s).split('\n').filter(l => !/^\s*(#|\/\/|::|@?REM\s)/i.test(l)).join('\n');
|
|
95
97
|
// 설명 필드(`- name: archive index.js`)는 실행 증거가 아니다 — bare 진입점 매처에도 같은 규칙을 건다.
|
|
96
|
-
|
|
98
|
+
// 단 값이 **산문일 때만** 설명으로 본다(matrix 변수명이 `name` 일 수 있다 — 판정기와 같은 규칙).
|
|
99
|
+
const _noDescriptive = (s) => String(s).split('\n').filter(l => {
|
|
100
|
+
const m = /^\s*-?\s*(?:name|description|title|summary|comment|displayName|label)\s*:\s*(.*)$/i.exec(l);
|
|
101
|
+
if (!m) return true;
|
|
102
|
+
const v = String(m[1]).trim();
|
|
103
|
+
return !v || v.startsWith('[') || !/\s/.test(v);
|
|
104
|
+
}).join('\n');
|
|
97
105
|
for (const r of runners.slice()) {
|
|
98
106
|
const txt = _noComment(r.text);
|
|
99
107
|
for (const m of txt.matchAll(_FILE_RE)) push(_norm(m[0]), null);
|
|
@@ -142,7 +150,13 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
142
150
|
const findings = [];
|
|
143
151
|
const _finding = (kind, severity, message, details = {}) => findings.push({ kind, severity, message, ...details });
|
|
144
152
|
const _origWrite = process.stdout.write.bind(process.stdout);
|
|
145
|
-
|
|
153
|
+
// 1.36.120: `--json` 은 **stdout 만** 막고 있었다 — `warn()` 이 쓰는 stderr 로 사람용 문구가 그대로 샜다.
|
|
154
|
+
// 그 결과 JSON 계약 표면에 잡음이 남고, 그 잡음의 내용이 **프로젝트 상태에 따라** 달라져
|
|
155
|
+
// 게이트 판정이 저장소 상태에 종속됐다(같은 코드로 통과/실패가 갈렸다). json 모드에서는 양쪽을 다 막는다.
|
|
156
|
+
// (findings 배열이 그 정보를 이미 싣고 있으므로 사람용 문구는 중복이다.)
|
|
157
|
+
const _origErrWrite = process.stderr.write.bind(process.stderr);
|
|
158
|
+
const _restoreStreams = () => { process.stdout.write = _origWrite; process.stderr.write = _origErrWrite; };
|
|
159
|
+
if (jsonMode) { process.stdout.write = () => true; process.stderr.write = () => true; }
|
|
146
160
|
try {
|
|
147
161
|
// 외부리뷰 CV-3/UR-0078: 미초기화/존재하지 않는 경로를 healthy 로 오판하던 것 수정 — 필수 마커 부재 시 failure 승격(verify 와 일관).
|
|
148
162
|
if (!exists(root) || !exists(path.join(root, '.harness')) || !exists(path.join(root, 'AGENTS.md'))) {
|
|
@@ -151,7 +165,7 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
151
165
|
_finding('not_initialized', 'fail', 'uninitialized or missing path (.harness or AGENTS.md absent)', { root });
|
|
152
166
|
// 1.27.1 (13번째 외부리뷰 #2): 미초기화 시 후속 체크(design/reuse 등)를 없는 하네스에 대해 보고하던 모순 출력 차단 — 요약/JSON 으로 직행 후 종료(exit code/JSON 페이로드는 종전과 동일).
|
|
153
167
|
log(`Audit summary: warnings=${warnings} failures=${failures}`);
|
|
154
|
-
if (jsonMode) {
|
|
168
|
+
if (jsonMode) { _restoreStreams(); process.stdout.write(JSON.stringify({ version: VERSION, root, warnings, failures, fixed, healthy: false, fixApplied: fix, strict: has('--strict'), strictThreshold: has('--strict') ? parseInt(arg('--threshold', '1'), 10) : null, summary: `warnings=${warnings} failures=${failures}`, findings }, null, 2) + '\n'); }
|
|
155
169
|
process.exitCode = 1;
|
|
156
170
|
return;
|
|
157
171
|
}
|
|
@@ -549,7 +563,7 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
549
563
|
log(`Audit summary: warnings=${warnings} failures=${failures}${fix ? ` fixed=${fixed}` : ''}${has('--strict') ? ` strict-threshold=${arg('--threshold', '1')}` : ''}`);
|
|
550
564
|
} finally {
|
|
551
565
|
// 1.9.102: stdout 복원
|
|
552
|
-
if (jsonMode)
|
|
566
|
+
if (jsonMode) _restoreStreams();
|
|
553
567
|
}
|
|
554
568
|
// 1.9.102: JSON 모드 — 구조화 출력
|
|
555
569
|
if (jsonMode) {
|
package/lib/pure-utils.js
CHANGED
|
@@ -1216,8 +1216,16 @@ function _detectOrphanGuards(input) {
|
|
|
1216
1216
|
// 사람이 읽는 설명 필드만 빼고 나머지는 그대로 본다.
|
|
1217
1217
|
// 그 대가(측정됨): `env: { TARGET: check:x }`·`key: check:x-hash` 처럼 **데이터 값**에 이름이 있으면
|
|
1218
1218
|
// 그것을 호출로 보고 진짜 고아를 가린다. 그건 미탐이라 감수한다.
|
|
1219
|
-
|
|
1220
|
-
|
|
1219
|
+
// ⚠ `name:` 을 **무조건** 설명으로 보면 matrix 변수명이 `name` 인 정상 배선이 잘려 나간다(검수 재현):
|
|
1220
|
+
// `matrix: { name: [check:leaf] }` + `run: npm run ${{ matrix.name }}`. 값이 **산문일 때만** 설명으로 본다 —
|
|
1221
|
+
// 목록(`[...]`)이나 공백 없는 단일 토큰은 데이터일 수 있으므로 남긴다(편향은 과소 보고).
|
|
1222
|
+
const _DESCRIPTIVE = /^\s*-?\s*(?:name|description|title|summary|comment|displayName|label)\s*:\s*(.*)$/i;
|
|
1223
|
+
const _configLineOk = (l) => {
|
|
1224
|
+
const m = _DESCRIPTIVE.exec(l);
|
|
1225
|
+
if (!m) return true;
|
|
1226
|
+
const v = String(m[1]).trim();
|
|
1227
|
+
return !v || v.startsWith('[') || !/\s/.test(v);
|
|
1228
|
+
};
|
|
1221
1229
|
const _raw = _dropCommentLines(String(r.text || ''));
|
|
1222
1230
|
const t = _isConfigRunner(r.file)
|
|
1223
1231
|
? _raw.split('\n').filter(_configLineOk).join('\n')
|
|
@@ -1242,8 +1250,13 @@ function _detectOrphanGuards(input) {
|
|
|
1242
1250
|
// 1.36.116: 참조하는 쪽이 **도달 가능해야** 덮은 것으로 본다 — 아무도 안 부르는 스크립트가 파일을
|
|
1243
1251
|
// 언급해 봐야 그 파일도 안 도는 건 마찬가지다(스크립트 쪽 #7 과 같은 결함이 파일 쪽에도 있었다).
|
|
1244
1252
|
const liveScripts = Object.entries(scripts).filter(([n]) => isLive(n));
|
|
1245
|
-
|
|
1246
|
-
|
|
1253
|
+
// ⚠ 매칭 전에 경로 구분자를 정규화한다. Windows CI 는 `node scripts\x\check.js` 로 적는데
|
|
1254
|
+
// 후보 경로는 `/` 표기라 전체 경로가 안 맞고, basename 경계 판정은 앞의 `\` 때문에 걸러져
|
|
1255
|
+
// **실제로 실행되는 가드가 고아로 보고됐다**(자체 헌트 실측: POSIX 표기만 통과).
|
|
1256
|
+
// `scripts/./check.js` 같은 dot-segment 도 편다 — 전체 경로 비교가 정규화를 안 해서 실제 호출이
|
|
1257
|
+
// 고아로 보고됐다(검수 재현). basename 경계는 앞의 `/` 를 거부하므로 폴백으로도 안 잡혔다.
|
|
1258
|
+
const allText = (runners.filter(_liveRunner).map(r => String(r.text || '')).join('\n') + '\n'
|
|
1259
|
+
+ liveScripts.map(([, b]) => String(b || '')).join('\n')).replace(/\\/g, '/').split('/./').join('/');
|
|
1247
1260
|
// ⚠ basename 만으로 덮였다고 보면 **동명 파일이 서로를 살린다**(검수 P2): `a/check.js` 를 부르는 배선이
|
|
1248
1261
|
// `b/check.js` 까지 덮어 버린다. basename 은 그 이름이 유일할 때만 근거로 쓴다.
|
|
1249
1262
|
const _baseCount = new Map();
|
|
@@ -1256,7 +1269,10 @@ function _detectOrphanGuards(input) {
|
|
|
1256
1269
|
// 구분자는 `\` 도 받는다(`require(".\\a\\check.js")`).
|
|
1257
1270
|
const _resolved = new Set();
|
|
1258
1271
|
for (const r of runners.filter(_liveRunner)) {
|
|
1259
|
-
|
|
1272
|
+
// ⚠ 기준 디렉터리가 다르다: **CI 설정의 명령은 프로젝트 루트에서 실행**된다(`run: node ./scripts/x.js`).
|
|
1273
|
+
// 설정 파일 위치(`.github/workflows`) 기준으로 풀면 `.github/workflows/scripts/x.js` 가 돼 실제 호출을 놓친다(검수 재현).
|
|
1274
|
+
// 코드 파일 안의 `require('./x')` 는 그 파일 기준이 맞다.
|
|
1275
|
+
const dir = _isConfigRunner(r.file) ? [] : String(r.file || '').replace(/\\/g, '/').split('/').slice(0, -1);
|
|
1260
1276
|
const body = _dropCommentLines(String(r.text || ''));
|
|
1261
1277
|
for (const m of body.matchAll(/(\.{1,2}(?:[/\\][\w.-]+)+\.(?:m?js|cjs|ts|sh))/g)) {
|
|
1262
1278
|
const segs = dir.slice();
|
|
@@ -1265,6 +1281,7 @@ function _detectOrphanGuards(input) {
|
|
|
1265
1281
|
if (s === '.' || s === '') continue;
|
|
1266
1282
|
if (s === '..') { if (!segs.length) { escaped = true; break; } segs.pop(); } else segs.push(s);
|
|
1267
1283
|
}
|
|
1284
|
+
// `scripts/./check.js` 처럼 중간 dot-segment 가 남아 있으면 여기서 이미 걸러진다(위 루프가 `.` 를 건너뛴다).
|
|
1268
1285
|
if (!escaped) _resolved.add(segs.join('/'));
|
|
1269
1286
|
}
|
|
1270
1287
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.120",
|
|
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
|
@@ -5088,8 +5088,10 @@ total++;
|
|
|
5088
5088
|
const j = JSON.parse(r.stdout); // 계약: stdout 전체가 단일 JSON 문서
|
|
5089
5089
|
const payloadOk = j.ok === true && j.pass === j.total && j.fail === 0 && j.total > 0;
|
|
5090
5090
|
const se = r.stderr || '';
|
|
5091
|
-
//
|
|
5092
|
-
|
|
5091
|
+
// 1.36.120: 잡음을 **열거**하는 대신 stderr 를 통째로 0 으로 고정한다. audit 이 `--json` 에서 stdout 만 막고
|
|
5092
|
+
// stderr 는 열어 둬서 사람용 경고가 샜고(309B), 그 문구가 **프로젝트 상태에 따라** 달라져
|
|
5093
|
+
// 같은 코드로 게이트가 통과/실패로 갈렸다(실측). 열거는 유계가 아니다 — 원인 단계에서 막았으니 여기선 0을 요구한다.
|
|
5094
|
+
const noiseFree = se.trim() === '';
|
|
5093
5095
|
ok = pureStdout && payloadOk && noiseFree && r.status === 0;
|
|
5094
5096
|
} catch {}
|
|
5095
5097
|
console.log(ok ? '✓ B(1.36.88) selftest --json 계약: 리포 루트 cwd stdout 순수 JSON + stderr 잡음 0' : '✗ selftest --json 계약 실패 (stdout 오염 또는 stderr 잡음)');
|
|
@@ -11242,6 +11244,21 @@ total++;
|
|
|
11242
11244
|
const rex = cp.spawnSync(process.execPath, [CLI, 'export', 'claude', '--path', dex, '--json'], { cwd: dex, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
11243
11245
|
let jex = null; try { jex = JSON.parse(String(rex.stdout || '').trim()); } catch {}
|
|
11244
11246
|
if (!jex || jex.code !== 'store_corrupt' || rex.status === 0) bad.push(`export별칭:손상코드아님(${jex && jex.code}/${rex.status})`);
|
|
11247
|
+
// (검수 P3) `prompt` 도 같은 별칭이다 — `export` 만 검사하면 `prompt` 를 가드 밖으로 빼는 변이가 산다.
|
|
11248
|
+
const dpr = mk(['--mode', 'minimal']);
|
|
11249
|
+
fs.writeFileSync(path.join(dpr, '.harness', 'manifest.json'), 'false');
|
|
11250
|
+
const rpr = cp.spawnSync(process.execPath, [CLI, 'prompt', 'claude', '--path', dpr, '--json'], { cwd: dpr, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
11251
|
+
let jpr = null; try { jpr = JSON.parse(String(rpr.stdout || '').trim()); } catch {}
|
|
11252
|
+
if (!jpr || jpr.code !== 'store_corrupt' || rpr.status === 0) bad.push(`prompt별칭:손상코드아님(${jpr && jpr.code}/${rpr.status})`);
|
|
11253
|
+
// (검수 P3) MCP 안내가 **MCP 결과에만** 결부되는지 — 지침 파일이 새로 생기는 경로에서 검사해야
|
|
11254
|
+
// `results` 로 되돌리는 변이가 죽는다(기존 픽스처는 모두 init 이 끝난 뒤라 우연히 같았다).
|
|
11255
|
+
const dmr = path.join(sb, 'mcponly'); fs.mkdirSync(dmr, { recursive: true });
|
|
11256
|
+
fs.writeFileSync(path.join(dmr, 'package.json'), '{"name":"m","version":"0.1.0"}');
|
|
11257
|
+
fs.writeFileSync(path.join(dmr, '.mcp.json'), JSON.stringify({ mcpServers: { leerness: { command: 'node', args: ['./mine.js'] } } }, null, 2));
|
|
11258
|
+
const rmr = cp.spawnSync(process.execPath, [CLI, 'adapter', 'claude', '--path', dmr], { cwd: dmr, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
11259
|
+
const omr = String(rmr.stdout || '') + String(rmr.stderr || '');
|
|
11260
|
+
if (/MCP 등록 —/.test(omr)) bad.push('MCP안내가_지침파일생성으로_발화');
|
|
11261
|
+
if (!/확인하지 않았습니다/.test(omr)) bad.push('MCP보존_중립문구없음');
|
|
11245
11262
|
// 판별력 — standard 프로젝트에서는 adapter 가 여전히 standard 를 유지해야 한다(등급 반영이 전부를 줄이면 안 된다)
|
|
11246
11263
|
const ds2 = mk();
|
|
11247
11264
|
const sBefore = fs.readFileSync(path.join(ds2, 'CLAUDE.md'), 'utf8').length;
|
|
@@ -11332,6 +11349,19 @@ total++;
|
|
|
11332
11349
|
cp.spawnSync(process.execPath, [CLI, 'adapter', 'cursor', '--path', dc3], { cwd: dc3, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
11333
11350
|
let jc3 = null; try { jc3 = JSON.parse(fs.readFileSync(path.join(dc3, '.cursor', 'mcp.json'), 'utf8')); } catch {}
|
|
11334
11351
|
if (!jc3 || jc3.mcpServers.leerness.command !== 'node' || !jc3.mcpServers.other) bad.push('cursor:사용자설정미보존');
|
|
11352
|
+
// (검수·자체 재현) cursor 는 MCP 설정이 **둘**이다. 한쪽만 우리가 썼는데 "이 도구가 직접 호출 가능" 이라고 하면
|
|
11353
|
+
// 정작 그 도구가 읽는 파일은 남의 설정일 수 있다 — 주장은 우리가 쓴 **파일 이름과 함께** 해야 한다.
|
|
11354
|
+
const dc4 = mk();
|
|
11355
|
+
fs.rmSync(path.join(dc4, '.mcp.json'), { force: true });
|
|
11356
|
+
fs.mkdirSync(path.join(dc4, '.cursor'), { recursive: true });
|
|
11357
|
+
fs.writeFileSync(path.join(dc4, '.cursor', 'mcp.json'), JSON.stringify({ mcpServers: { leerness: { command: 'node', args: ['./mine.js'] } } }, null, 2));
|
|
11358
|
+
const rc4 = cp.spawnSync(process.execPath, [CLI, 'adapter', 'cursor', '--path', dc4], { cwd: dc4, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
11359
|
+
const oc4 = String(rc4.stdout || '') + String(rc4.stderr || '');
|
|
11360
|
+
if (!/\.mcp\.json 에 MCP 등록/.test(oc4)) bad.push('cursor:쓴파일_범위표기없음');
|
|
11361
|
+
if (/\.cursor[\\/]mcp\.json 에 MCP 등록/.test(oc4)) bad.push('cursor:보존파일까지_호출가능주장');
|
|
11362
|
+
if (!/확인하지 않았습니다/.test(oc4)) bad.push('cursor:보존_중립문구없음');
|
|
11363
|
+
let jc4 = null; try { jc4 = JSON.parse(fs.readFileSync(path.join(dc4, '.cursor', 'mcp.json'), 'utf8')); } catch {}
|
|
11364
|
+
if (!jc4 || jc4.mcpServers.leerness.command !== 'node') bad.push('cursor:보존실패');
|
|
11335
11365
|
}
|
|
11336
11366
|
// ⑦ (검수 P1) `_ours` 경계 — 우리가 만든 **정확한 형태**만 갱신하고, 모르는 키가 하나라도 붙으면 보존한다.
|
|
11337
11367
|
// 그리고 명시적으로 적힌 falsy 값을 '없음' 으로 오인해 덮어쓰면 안 된다.
|
|
@@ -11799,6 +11829,33 @@ total++;
|
|
|
11799
11829
|
const remInp = AUD._collectGuardInputs(d6c);
|
|
11800
11830
|
if (remInp.runners.some(r => /phantom\.js$/.test(r.file))) bad.push('CMD주석에서_진입점_수집');
|
|
11801
11831
|
if (!PU._detectOrphanGuards(remInp).orphanScripts.includes('check:leaf')) bad.push('CMD주석이_고아를_덮음');
|
|
11832
|
+
// (검수 P3) `REM` 만 검사하면 `::`·`@REM` 변이가 산다 — 주석 표기별로 잰다.
|
|
11833
|
+
for (const [tag, mark] of [['::', '::'], ['@REM', '@REM']]) {
|
|
11834
|
+
fs.writeFileSync(path.join(d6c, '.github', 'workflows', 'ci.yml'),
|
|
11835
|
+
`jobs:\n a:\n steps:\n - shell: cmd\n run: |\n ${mark} debug: node .\\bin\\phantom.js\n echo harmless\n - run: npm test\n`);
|
|
11836
|
+
const ri = AUD._collectGuardInputs(d6c);
|
|
11837
|
+
if (ri.runners.some(r => /phantom\.js$/.test(r.file))) bad.push(`CMD주석(${tag})에서_진입점_수집`);
|
|
11838
|
+
}
|
|
11839
|
+
// (검수 P3) bare 진입점(`main: index.js`)의 설명 필터 — 픽스처가 없어 되돌리는 변이가 살았다.
|
|
11840
|
+
const dbe = path.join(sb4, 'bare');
|
|
11841
|
+
fs.mkdirSync(path.join(dbe, '.github', 'workflows'), { recursive: true });
|
|
11842
|
+
fs.writeFileSync(path.join(dbe, 'package.json'), JSON.stringify({ name: 'b', version: '0.1.0', main: 'index.js',
|
|
11843
|
+
scripts: { test: 'node t.js', 'check:leaf': 'node l.js' } }));
|
|
11844
|
+
fs.writeFileSync(path.join(dbe, '.github', 'workflows', 'ci.yml'), 'jobs:\n a:\n steps:\n - name: archive index.js\n run: npm test\n');
|
|
11845
|
+
fs.writeFileSync(path.join(dbe, 'index.js'), 'sh("npm run check:leaf")\n');
|
|
11846
|
+
const beInp = AUD._collectGuardInputs(dbe);
|
|
11847
|
+
if (beInp.runners.some(r => /index\.js$/.test(r.file))) bad.push('설명필드가_bare진입점을_승격');
|
|
11848
|
+
if (!PU._detectOrphanGuards(beInp).orphanScripts.includes('check:leaf')) bad.push('설명필드_승격이_고아를_덮음');
|
|
11849
|
+
// 판별력 — 실제 실행이면 수집돼야 한다
|
|
11850
|
+
fs.writeFileSync(path.join(dbe, '.github', 'workflows', 'ci.yml'), 'jobs:\n a:\n steps:\n - run: node index.js --guard\n');
|
|
11851
|
+
const beInp2 = AUD._collectGuardInputs(dbe);
|
|
11852
|
+
if (!beInp2.runners.some(r => /index\.js$/.test(r.file))) bad.push('실제실행인데_bare진입점_미수집');
|
|
11853
|
+
// (검수 P3) 0바이트 package.json 을 **실제로 수집**해서 계상되는지 — 지금까지는 readErrors 를 주입만 했다.
|
|
11854
|
+
const dz = path.join(sb4, 'zero'); fs.mkdirSync(dz, { recursive: true });
|
|
11855
|
+
fs.writeFileSync(path.join(dz, 'package.json'), '');
|
|
11856
|
+
const zInp = AUD._collectGuardInputs(dz);
|
|
11857
|
+
if (!zInp.readErrors) bad.push('0바이트_package.json_미계상');
|
|
11858
|
+
if (!/수집 불완전/.test(PU._detectOrphanGuards(zInp).reason || '')) bad.push('0바이트_근거미표기');
|
|
11802
11859
|
// (검수 P2) 상대 경로 해석: 주석 속 참조는 무시, 루트 밖은 내부 파일로 오인 금지, 역슬래시도 해석
|
|
11803
11860
|
const rel1 = PU._detectOrphanGuards({
|
|
11804
11861
|
packageScripts: { test: 'node scripts/run-a.js && node scripts/run-b.js' },
|
|
@@ -11816,6 +11873,56 @@ total++;
|
|
|
11816
11873
|
scriptFiles: ['scripts/check.js'],
|
|
11817
11874
|
});
|
|
11818
11875
|
if (!rel2.orphanFiles.includes('scripts/check.js')) bad.push('루트밖_참조가_내부파일을_덮음');
|
|
11876
|
+
// ⚠ 루트 이탈(`../../../…`) 가드는 **여기서 관측할 수 없다** — 측정해 보고 단언을 뺐다:
|
|
11877
|
+
// `allText` 의 전체 경로 비교가 부분 문자열이라, 이탈 경로도 우리 후보를 접미로 포함하면 어차피 덮인다.
|
|
11878
|
+
// 경계 판정을 전체 경로에까지 걸면 흔한 `./scripts/x.js` 표기가 깨지므로 그렇게 하지 않는다.
|
|
11879
|
+
// 가드 자체는 남긴다(해석 결과가 접미가 아닌 경우를 위한 방어) — 다만 공허한 단언을 두지는 않는다.
|
|
11880
|
+
const relIn = PU._detectOrphanGuards({
|
|
11881
|
+
packageScripts: { test: 'node scripts/a/run.js' },
|
|
11882
|
+
runners: [{ file: '.github/workflows/ci.yml', text: 'run: npm test', viaScript: null, viaScripts: [], unconditional: true },
|
|
11883
|
+
{ file: 'scripts/a/run.js', text: 'require("../../scripts/./check.js");\n', viaScript: 'test', viaScripts: ['test'] }],
|
|
11884
|
+
scriptFiles: ['scripts/check.js'],
|
|
11885
|
+
});
|
|
11886
|
+
if (relIn.orphanFiles.length) bad.push(`루트내_dot세그먼트_경로_오탐(${relIn.orphanFiles.join(',')})`);
|
|
11887
|
+
// (검수 P3) 역슬래시 상대경로 — 픽스처가 없어 `[/\\]` 를 `/` 로 줄이는 변이가 살았다.
|
|
11888
|
+
const relBs = PU._detectOrphanGuards({
|
|
11889
|
+
packageScripts: { test: 'node scripts/a/run.js' },
|
|
11890
|
+
runners: [{ file: '.github/workflows/ci.yml', text: 'run: npm test', viaScript: null, viaScripts: [], unconditional: true },
|
|
11891
|
+
{ file: 'scripts/a/run.js', text: 'require(".\\check.js");\n', viaScript: 'test', viaScripts: ['test'] }],
|
|
11892
|
+
scriptFiles: ['scripts/a/check.js', 'scripts/b/check.js'],
|
|
11893
|
+
});
|
|
11894
|
+
if (relBs.orphanFiles.includes('scripts/a/check.js')) bad.push('역슬래시_상대경로_미해석');
|
|
11895
|
+
// (검수) CI 설정의 명령은 **프로젝트 루트**에서 돈다 — 설정 파일 위치 기준으로 풀면 실제 호출을 놓친다.
|
|
11896
|
+
const relRoot = PU._detectOrphanGuards({
|
|
11897
|
+
packageScripts: { test: 'node t.js' },
|
|
11898
|
+
runners: [{ file: '.github/workflows/ci.yml', viaScript: null, viaScripts: [], unconditional: true,
|
|
11899
|
+
text: 'jobs:\n a:\n steps:\n - run: node ./scripts/deep/check.js\n - run: npm test\n' }],
|
|
11900
|
+
scriptFiles: ['scripts/deep/check.js'],
|
|
11901
|
+
});
|
|
11902
|
+
if (relRoot.orphanFiles.length) bad.push(`CI상대경로_루트기준아님(${relRoot.orphanFiles.join(',')})`);
|
|
11903
|
+
// dot-segment 가 낀 전체 경로도 정규화돼야 한다
|
|
11904
|
+
const relDot = PU._detectOrphanGuards({
|
|
11905
|
+
packageScripts: { test: 'node run.sh' },
|
|
11906
|
+
runners: [{ file: '.github/workflows/ci.yml', text: 'run: npm test', viaScript: null, viaScripts: [], unconditional: true },
|
|
11907
|
+
{ file: 'run.sh', text: 'node scripts/./check.js\n', viaScript: 'test', viaScripts: ['test'] }],
|
|
11908
|
+
scriptFiles: ['scripts/check.js'],
|
|
11909
|
+
});
|
|
11910
|
+
if (relDot.orphanFiles.length) bad.push(`dot세그먼트_경로_오탐(${relDot.orphanFiles.join(',')})`);
|
|
11911
|
+
// (자체 헌트) Windows CI 는 `node scripts\x\check.js` 로 적는다. 후보 경로는 `/` 표기라
|
|
11912
|
+
// 전체 경로가 안 맞고 basename 경계 판정도 앞의 `\` 때문에 걸러져 **정상 배선이 고아로 보고**됐다.
|
|
11913
|
+
for (const [label, ref] of [['posix', 'scripts/x/check.js'], ['win', 'scripts\\x\\check.js'], ['win-dot', '.\\scripts\\x\\check.js']]) {
|
|
11914
|
+
const rw = PU._detectOrphanGuards({ packageScripts: { test: 'node t.js' },
|
|
11915
|
+
runners: [{ file: '.github/workflows/ci.yml', viaScript: null, viaScripts: [], unconditional: true,
|
|
11916
|
+
text: `jobs:\n a:\n steps:\n - run: node ${ref}\n - run: npm test\n` }],
|
|
11917
|
+
scriptFiles: ['scripts/x/check.js'] });
|
|
11918
|
+
if (rw.orphanFiles.includes('scripts/x/check.js')) bad.push(`경로표기_오탐:${label}`);
|
|
11919
|
+
}
|
|
11920
|
+
// 판별력 — 루트의 동명 파일을 부르는 것은 하위 디렉터리 후보를 덮지 않는다(경계 판정의 목적)
|
|
11921
|
+
const rw2 = PU._detectOrphanGuards({ packageScripts: { test: 'node t.js' },
|
|
11922
|
+
runners: [{ file: '.github/workflows/ci.yml', viaScript: null, viaScripts: [], unconditional: true,
|
|
11923
|
+
text: 'jobs:\n a:\n steps:\n - run: node .\\check.js\n - run: npm test\n' }],
|
|
11924
|
+
scriptFiles: ['scripts/x/check.js', 'scripts/y/check.js'] });
|
|
11925
|
+
if (rw2.orphanFiles.length !== 2) bad.push(`경로경계_과다커버(${rw2.orphanFiles.join(',')})`);
|
|
11819
11926
|
} finally { try { fs.rmSync(sb4, { recursive: true, force: true }); } catch {} }
|
|
11820
11927
|
}
|
|
11821
11928
|
// ⑰ (검수 P2) 와일드카드가 있다고 실행은 아니다 — `echo "test:*"` 는 아무것도 안 돌린다.
|
|
@@ -11893,6 +12000,9 @@ total++;
|
|
|
11893
12000
|
// 이 저장소의 편향은 "오탐이 경고를 죽인다" 이므로 방향은 과소 보고다. 이 두 줄이 그 결정을 고정한다.
|
|
11894
12001
|
['matrix 변수명(실행키 아님)', 'jobs:\n a:\n strategy:\n matrix:\n target: [check:leaf]\n steps:\n - run: npm run ${{ matrix.target }}\n', false],
|
|
11895
12002
|
['Azure customCommand', 'steps:\n- task: Npm@1\n inputs:\n command: custom\n customCommand: run check:leaf\n', false],
|
|
12003
|
+
// (검수) `name:` 을 **무조건** 설명으로 보면 matrix 변수명이 `name` 인 정상 배선이 잘린다.
|
|
12004
|
+
// 값이 산문일 때만 설명이다 — 목록·단일 토큰은 데이터일 수 있으므로 남긴다.
|
|
12005
|
+
['matrix 변수명이 name', 'jobs:\n a:\n strategy:\n matrix:\n name: [check:leaf]\n steps:\n - run: npm run ${{ matrix.name }}\n', false],
|
|
11896
12006
|
]) {
|
|
11897
12007
|
const rr = PU._detectOrphanGuards({ packageScripts: { test: 'node t.js', 'check:leaf': 'node l.js' },
|
|
11898
12008
|
runners: [{ file: '.github/workflows/ci.yml', text, viaScript: null, viaScripts: [] }], scriptFiles: [] });
|