leerness 1.36.118 → 1.36.119
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 +16 -8
- package/lib/audit.js +10 -3
- package/lib/pure-utils.js +22 -16
- package/package.json +1 -1
- package/scripts/e2e.js +55 -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.119 하네스를 사용합니다. 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.119는 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.118는 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.119 릴리스 태그 이력** (수백 라운드) · _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.119: 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.119';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -4781,7 +4781,9 @@ function _selfTestCases() {
|
|
|
4781
4781
|
const wired = src.includes("if (cmd === 'export' || cmd === 'prompt') {")
|
|
4782
4782
|
&& src.includes("arg('--target', null)")
|
|
4783
4783
|
&& src.includes("'agents-md': 'codex'")
|
|
4784
|
-
|
|
4784
|
+
// 1.36.119: 별칭도 `adapter` 와 **같은 손상 가드**를 타야 한다 — 전엔 일반 error 로 떨어져
|
|
4785
|
+
// 자동화가 '손상' 과 '그 밖의 실패' 를 구분할 수 없었다(검수 P2).
|
|
4786
|
+
&& src.includes('_guardStore(has(\'--json\'), () => adapterCmd(arg(\'--path\', process.cwd()), _xTool))');
|
|
4785
4787
|
return wired;
|
|
4786
4788
|
} },
|
|
4787
4789
|
{ name: '12th 외부평가 Sonnet P3 (UR-0143): plan drop 은 plan.md(D-)만 기록, progress-tracker T- 행 미생성 (1.9.449)', run: () => {
|
|
@@ -23913,22 +23915,26 @@ function adapterCmd(root, tool, opts = {}) {
|
|
|
23913
23915
|
if (files[k] == null) continue;
|
|
23914
23916
|
results.push(writeIfSafe(root, k, files[k], { mergeManaged: managedOverwrite.has(k), lang: lang === 'en' ? 'en' : 'ko' })); // 1.36.60 (검수 2차 #2): 래퍼 언어 전달 (altTemplate 은 전환-감지 없는 이 경로에선 미적용 — High 회귀 방지)
|
|
23915
23917
|
}
|
|
23916
|
-
|
|
23918
|
+
// 1.36.119 (검수): 아래 안내 문구의 판정 대상을 **MCP 병합 결과로 한정**한다. 전에는 `results` 전체를 훑었고,
|
|
23919
|
+
// 지금 맞는 이유는 `writeIfSafe` 가 'merged'/'preserved' 만 돌려주기 때문이다 — 그 이름이 바뀌면
|
|
23920
|
+
// 지침 파일을 썼다는 이유로 "MCP 호출 가능" 이라고 말하게 된다(우연한 정합은 계약이 아니다).
|
|
23921
|
+
const mcpResults = [];
|
|
23922
|
+
if (a.mcp) { const _m = _mergeMcpJson(root); results.push(_m); mcpResults.push(_m); }
|
|
23917
23923
|
// 1.36.116 (검수 P1-D): Cursor 의 프로젝트별 MCP 설정은 `.cursor/mcp.json` 이다. 루트 `.mcp.json` 만 만들어 놓고
|
|
23918
23924
|
// "Cursor 도 인식한다" 고 말하던 것은 거짓 안내였다 — 이 어댑터의 목적 자체가 그 도구를 배선하는 것이다.
|
|
23919
23925
|
// 루트 파일도 그대로 둔다(다른 도구가 쓴다). 같은 병합기라 손상 파일은 여전히 보존/중단된다.
|
|
23920
|
-
if (tool === 'cursor') { _assertStoreParsable(path.join(root, '.cursor', 'mcp.json'), '.cursor/mcp.json');
|
|
23926
|
+
if (tool === 'cursor') { _assertStoreParsable(path.join(root, '.cursor', 'mcp.json'), '.cursor/mcp.json'); const _m2 = _mergeMcpJson(root, '.cursor/mcp.json'); results.push(_m2); mcpResults.push(_m2); }
|
|
23921
23927
|
if (json) { log(JSON.stringify({ adapter: tool, files: results }, null, 2)); return; }
|
|
23922
23928
|
ok(`adapter ${tool} (${a.label}) 적용 — ${results.length}개 파일`);
|
|
23923
23929
|
for (const r of results) log(` ${r.action}: ${r.file}`);
|
|
23924
23930
|
// 1.36.117 (검수 P1): 깨진 항목을 보존했을 때도 "직접 호출 가능" 이라고 말하고 있었다 — 그건 거짓 성공이다.
|
|
23925
|
-
const _brk =
|
|
23931
|
+
const _brk = mcpResults.filter(r => r.broken);
|
|
23926
23932
|
if (_brk.length) _brk.forEach(r => warn(`${r.file} 의 leerness 항목에 command 가 없어 서버가 뜨지 않습니다 — 사용자 설정이라 보존했습니다`));
|
|
23927
23933
|
// 1.36.118 (자체 헌트): **보존**했을 때도 "leerness MCP verb 를 직접 호출 가능" 이라고 말하고 있었다.
|
|
23928
23934
|
// 보존한 항목이 leerness 를 띄우는지는 우리가 확인하지 않았다(사용자가 다른 서버를 적어 뒀을 수도 있다) —
|
|
23929
23935
|
// 우리가 쓴 항목이 하나라도 있을 때만 그렇게 말한다. 그 밖에는 무엇을 보존했는지만 알린다.
|
|
23930
|
-
const _wrote =
|
|
23931
|
-
const _kept =
|
|
23936
|
+
const _wrote = mcpResults.filter(r => r.action === 'created' || r.action === 'updated');
|
|
23937
|
+
const _kept = mcpResults.filter(r => r.preserved && !r.broken);
|
|
23932
23938
|
if (a.mcp && _wrote.length) log(` ℹ MCP 등록 — 이 도구가 leerness MCP verb(state_show/start/record/verify/handoff)를 직접 호출 가능`);
|
|
23933
23939
|
_kept.forEach(r => log(` ℹ ${r.file} 의 기존 leerness 항목을 그대로 두었습니다 — 그 항목이 leerness MCP 를 띄우는지는 확인하지 않았습니다`));
|
|
23934
23940
|
// 보존은 안전하지만 **막다른 길이면 안 된다** — 우리 것이 망가졌을 때 되돌릴 방법을 알려 준다(자체 헌트).
|
|
@@ -27022,7 +27028,9 @@ async function main() {
|
|
|
27022
27028
|
const _ADAPTER_ALIAS = { 'agents-md': 'codex', 'agents': 'codex', 'agent': 'codex', 'agents.md': 'codex' };
|
|
27023
27029
|
let _xTool = arg('--target', null) || (args[1] && !args[1].startsWith('-') ? args[1] : 'list');
|
|
27024
27030
|
_xTool = _ADAPTER_ALIAS[String(_xTool).toLowerCase()] || _xTool;
|
|
27025
|
-
|
|
27031
|
+
// 1.36.119 (검수 P2): 같은 공개 명령의 **별칭**인데 `adapter` 만 손상을 구조화해 내고 여기는 일반 error 였다 —
|
|
27032
|
+
// 자동화가 '손상' 과 '그 밖의 실패' 를 구분할 수 없다. 같은 가드를 태운다.
|
|
27033
|
+
return _guardStore(has('--json'), () => adapterCmd(arg('--path', process.cwd()), _xTool));
|
|
27026
27034
|
}
|
|
27027
27035
|
// 1.9.245: API skill cache — 공식 문서·관련링크 자동 정리 (사용자 명시 UR-0015)
|
|
27028
27036
|
if (cmd === 'api-skill') return apiSkillCmd(arg('--path', process.cwd()), args[1] || 'help');
|
package/lib/audit.js
CHANGED
|
@@ -36,7 +36,10 @@ function _collectGuardInputs(root) {
|
|
|
36
36
|
let ownEntries = new Set();
|
|
37
37
|
let workspaces = false;
|
|
38
38
|
try {
|
|
39
|
-
|
|
39
|
+
// 0바이트 `package.json` 은 유효 JSON 이 아닌데 `|| '{}'` 가 그것을 '스크립트 없음' 으로 삼켰다(검수 P2).
|
|
40
|
+
const _pkgRaw = rd(path.join(root, 'package.json'));
|
|
41
|
+
if (_pkgRaw != null && !String(_pkgRaw).trim()) throw new Error('empty package.json');
|
|
42
|
+
const pkg = JSON.parse(_pkgRaw || '{}');
|
|
40
43
|
packageScripts = pkg.scripts || {};
|
|
41
44
|
workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces.length > 0 : !!(pkg.workspaces && pkg.workspaces.packages);
|
|
42
45
|
// 1.36.116 (실측): 패키지 **자신의 진입점**(bin/main)은 러너가 아니라 제품이다. leerness 에서 실제로 물렸다 —
|
|
@@ -87,15 +90,19 @@ function _collectGuardInputs(root) {
|
|
|
87
90
|
// 가드를 배선하고 있으면 그걸 못 봐서 정상 가드가 고아로 뒤집힌다. CI 가 부르면 그건 러너가 맞다.
|
|
88
91
|
// ⚠ 단 **주석 줄에서 수집하면 안 된다**: `# debug: node bin/cli.js` 한 줄로 제품 진입점이 러너가 되고
|
|
89
92
|
// 그 안의 문자열이 진짜 고아를 덮는다(검수 P2 — 자기참조가 주석 경로로 되살아난다).
|
|
90
|
-
|
|
93
|
+
// CMD 스텝의 `REM`·`::` 도 주석이다(검수 재현: `REM debug: node .\bin\phantom.js` 한 줄로 제품 진입점이 러너가 됐다).
|
|
94
|
+
const _noComment = (s) => String(s).split('\n').filter(l => !/^\s*(#|\/\/|--|;|::|REM\s)/i.test(l)).join('\n');
|
|
95
|
+
// 설명 필드(`- name: archive index.js`)는 실행 증거가 아니다 — bare 진입점 매처에도 같은 규칙을 건다.
|
|
96
|
+
const _noDescriptive = (s) => String(s).split('\n').filter(l => !/^\s*-?\s*(?:name|description|title|summary|comment|displayName|label)\s*:/i.test(l)).join('\n');
|
|
91
97
|
for (const r of runners.slice()) {
|
|
92
98
|
const txt = _noComment(r.text);
|
|
93
99
|
for (const m of txt.matchAll(_FILE_RE)) push(_norm(m[0]), null);
|
|
94
100
|
// 디렉터리 접두가 없는 진입점(`"main": "index.js"`)은 위 정규식이 못 잡는다 — 선언된 bin/main 만 이름으로 확인한다.
|
|
101
|
+
const bareTxt = _noDescriptive(txt);
|
|
95
102
|
for (const own of ownEntries) {
|
|
96
103
|
if (/[/\\]/.test(own)) continue;
|
|
97
104
|
const esc = own.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
98
|
-
if (new RegExp(`(^|[^\\w.\\-/\\\\])\\.?[/\\\\]?${esc}([^\\w.-]|$)`, 'm').test(
|
|
105
|
+
if (new RegExp(`(^|[^\\w.\\-/\\\\])\\.?[/\\\\]?${esc}([^\\w.-]|$)`, 'm').test(bareTxt)) push(own, null);
|
|
99
106
|
}
|
|
100
107
|
}
|
|
101
108
|
for (const [name, body] of Object.entries(packageScripts)) for (const m of String(body || '').matchAll(_FILE_RE)) {
|
package/lib/pure-utils.js
CHANGED
|
@@ -1209,17 +1209,15 @@ function _detectOrphanGuards(input) {
|
|
|
1209
1209
|
// 줄 필터는 **코드 파일**에만 건다 — 거기서만 이름이 데이터로 등장한다(우리 e2e.js 가 그랬다).
|
|
1210
1210
|
// ⚠ CI 설정을 전문으로 보되 **설명용 필드는 뺀다**: `- name: TODO check:leaf 를 붙이자`,
|
|
1211
1211
|
// `env: { FOO: check:x }`, artifact 이름 같은 것이 그것만으로 '호출됨' 이 됐다(자체 헌트 + 검수 재현).
|
|
1212
|
-
//
|
|
1213
|
-
// `
|
|
1214
|
-
// 실행
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
return _RUN_KEYS.test(m[1]) || _INVOKE.test(l);
|
|
1222
|
-
};
|
|
1212
|
+
// ⚠ 한때 **허용 목록**(실행 키만 본다)으로 뒤집었다가 되돌렸다. 그건 정상 배선을 버렸다(검수 재현):
|
|
1213
|
+
// `matrix: { target: [check:leaf] }` + `run: npm run ${{ matrix.target }}` 에서 `target` 은 실행 키가
|
|
1214
|
+
// 아니므로 잘려 나가고, 실행 줄에는 이름이 없어 **정상 가드가 고아로 보고**된다. Azure 의 `customCommand:` 도 같다.
|
|
1215
|
+
// 이 저장소의 편향은 명시적이다 — **오탐이 이 경고를 죽인다**. 그래서 방향은 과소 보고다:
|
|
1216
|
+
// 사람이 읽는 설명 필드만 빼고 나머지는 그대로 본다.
|
|
1217
|
+
// 그 대가(측정됨): `env: { TARGET: check:x }`·`key: check:x-hash` 처럼 **데이터 값**에 이름이 있으면
|
|
1218
|
+
// 그것을 호출로 보고 진짜 고아를 가린다. 그건 미탐이라 감수한다.
|
|
1219
|
+
const _DESCRIPTIVE = /^\s*-?\s*(?:name|description|title|summary|comment|displayName|label)\s*:/i;
|
|
1220
|
+
const _configLineOk = (l) => !_DESCRIPTIVE.test(l);
|
|
1223
1221
|
const _raw = _dropCommentLines(String(r.text || ''));
|
|
1224
1222
|
const t = _isConfigRunner(r.file)
|
|
1225
1223
|
? _raw.split('\n').filter(_configLineOk).join('\n')
|
|
@@ -1253,23 +1251,31 @@ function _detectOrphanGuards(input) {
|
|
|
1253
1251
|
// ⚠ 그런데 유일성만으로 끄면 **해석 가능한 상대 호출**까지 잃는다(검수 P2): `scripts/a/run.js` 안의
|
|
1254
1252
|
// `require('./check.js')` 는 `scripts/a/check.js` 를 가리키는데, 동명 파일이 있다는 이유로 그것까지 고아가 됐다.
|
|
1255
1253
|
// 참조한 러너의 디렉터리를 기준으로 상대 경로를 **해석해서** 덮인 집합에 넣는다.
|
|
1254
|
+
// ⚠ 원문을 그대로 훑으면 **주석 속 참조**도 실행으로 계상되고(검수 재현: `// require("./a/check.js")`),
|
|
1255
|
+
// `..` 가 루트 밖으로 나가면 남은 조각이 엉뚱한 루트 내부 파일로 해석된다. 둘 다 막는다.
|
|
1256
|
+
// 구분자는 `\` 도 받는다(`require(".\\a\\check.js")`).
|
|
1256
1257
|
const _resolved = new Set();
|
|
1257
1258
|
for (const r of runners.filter(_liveRunner)) {
|
|
1258
1259
|
const dir = String(r.file || '').replace(/\\/g, '/').split('/').slice(0, -1);
|
|
1259
|
-
|
|
1260
|
+
const body = _dropCommentLines(String(r.text || ''));
|
|
1261
|
+
for (const m of body.matchAll(/(\.{1,2}(?:[/\\][\w.-]+)+\.(?:m?js|cjs|ts|sh))/g)) {
|
|
1260
1262
|
const segs = dir.slice();
|
|
1261
|
-
|
|
1263
|
+
let escaped = false;
|
|
1264
|
+
for (const s of m[1].replace(/\\/g, '/').split('/')) {
|
|
1262
1265
|
if (s === '.' || s === '') continue;
|
|
1263
|
-
if (s === '..') segs.pop(); else segs.push(s);
|
|
1266
|
+
if (s === '..') { if (!segs.length) { escaped = true; break; } segs.pop(); } else segs.push(s);
|
|
1264
1267
|
}
|
|
1265
|
-
_resolved.add(segs.join('/'));
|
|
1268
|
+
if (!escaped) _resolved.add(segs.join('/'));
|
|
1266
1269
|
}
|
|
1267
1270
|
}
|
|
1268
1271
|
const orphanFiles = unknownOrchestrator ? [] : scriptFiles.filter(f => {
|
|
1269
1272
|
const full = String(f).replace(/\\/g, '/');
|
|
1270
1273
|
if (allText.includes(full) || _resolved.has(full)) return false;
|
|
1271
1274
|
const base = full.split('/').pop();
|
|
1272
|
-
|
|
1275
|
+
// basename 폴백은 **경로의 일부**를 잡으면 안 된다: `../../outside/check.js` 가 우리 `scripts/check.js` 를
|
|
1276
|
+
// 덮어 버렸다(자체 검증에서 실측). 앞에 경로 구분자가 없는 **단독 언급**일 때만 근거로 쓴다.
|
|
1277
|
+
const _bnRe = new RegExp(`(^|[^\\w./\\\\-])${base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\w.-]|$)`);
|
|
1278
|
+
if (_baseCount.get(base) === 1 && _bnRe.test(allText)) return false;
|
|
1273
1279
|
return true;
|
|
1274
1280
|
});
|
|
1275
1281
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.119",
|
|
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
|
@@ -11222,6 +11222,26 @@ total++;
|
|
|
11222
11222
|
cp.spawnSync(process.execPath, [CLI, 'adapter', 'claude', '--path', dm2], { cwd: dm2, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
11223
11223
|
const afterLen = fs.readFileSync(path.join(dm2, 'CLAUDE.md'), 'utf8').length;
|
|
11224
11224
|
if (afterLen > beforeLen * 1.5) bad.push(`adapter:등급무시(${beforeLen}→${afterLen})`);
|
|
11225
|
+
// (검수 P3) manifest **형상** 5종을 고정한다 — `_assertStoreParsable` 은 문법만 보므로
|
|
11226
|
+
// `null`·`[]`·`false`·`0`·문자열이 통과한 뒤 조용히 standard 로 되돌아갔다.
|
|
11227
|
+
for (const shape of ['null', '[]', 'false', '0', '"minimal"']) {
|
|
11228
|
+
const dsh = mk(['--mode', 'minimal']);
|
|
11229
|
+
const shLen = fs.readFileSync(path.join(dsh, 'CLAUDE.md'), 'utf8').length;
|
|
11230
|
+
fs.writeFileSync(path.join(dsh, '.harness', 'manifest.json'), shape);
|
|
11231
|
+
const rsh = cp.spawnSync(process.execPath, [CLI, 'adapter', 'claude', '--path', dsh], { cwd: dsh, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
11232
|
+
if (rsh.status === 0) bad.push(`manifest형상:${shape}에서_중단안함`);
|
|
11233
|
+
if (fs.readFileSync(path.join(dsh, 'CLAUDE.md'), 'utf8').length !== shLen) bad.push(`manifest형상:${shape}에서_등급뒤집힘`);
|
|
11234
|
+
}
|
|
11235
|
+
// 판별력 — 정상 객체(구형: mode 없음)는 통과해야 한다(형상 검사가 과하면 기존 설치가 깨진다)
|
|
11236
|
+
const dok = mk(['--mode', 'minimal']);
|
|
11237
|
+
fs.writeFileSync(path.join(dok, '.harness', 'manifest.json'), JSON.stringify({ installedAt: '2026-01-01' }, null, 2));
|
|
11238
|
+
if (cp.spawnSync(process.execPath, [CLI, 'adapter', 'claude', '--path', dok], { cwd: dok, encoding: 'utf8', timeout: 300000, env: ENV }).status !== 0) bad.push('manifest형상:정상객체를_거부');
|
|
11239
|
+
// (검수 P2) `export`/`prompt` 는 같은 명령의 별칭이다 — 손상을 구조화해 내야 자동화가 구분한다
|
|
11240
|
+
const dex = mk(['--mode', 'minimal']);
|
|
11241
|
+
fs.writeFileSync(path.join(dex, '.harness', 'manifest.json'), 'false');
|
|
11242
|
+
const rex = cp.spawnSync(process.execPath, [CLI, 'export', 'claude', '--path', dex, '--json'], { cwd: dex, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
11243
|
+
let jex = null; try { jex = JSON.parse(String(rex.stdout || '').trim()); } catch {}
|
|
11244
|
+
if (!jex || jex.code !== 'store_corrupt' || rex.status === 0) bad.push(`export별칭:손상코드아님(${jex && jex.code}/${rex.status})`);
|
|
11225
11245
|
// 판별력 — standard 프로젝트에서는 adapter 가 여전히 standard 를 유지해야 한다(등급 반영이 전부를 줄이면 안 된다)
|
|
11226
11246
|
const ds2 = mk();
|
|
11227
11247
|
const sBefore = fs.readFileSync(path.join(ds2, 'CLAUDE.md'), 'utf8').length;
|
|
@@ -11556,6 +11576,8 @@ total++;
|
|
|
11556
11576
|
if (f.severity !== 'warn') bad.push(`권고아님(${f.severity})`); // 차단으로 승격되면 안 된다
|
|
11557
11577
|
if (!(f.orphanScripts || []).includes('test:orphan-guard')) bad.push('audit_대상누락');
|
|
11558
11578
|
if (!f.reason) bad.push('audit_근거없음');
|
|
11579
|
+
// (검수 P3) 범위 메타데이터는 고아가 **있을 때도** 실려야 한다 — 0건 분기에만 넣으면 계약이 갈린다.
|
|
11580
|
+
if (!('monorepoOutOfScope' in f) || !('deferred' in f)) bad.push('audit_범위메타누락');
|
|
11559
11581
|
}
|
|
11560
11582
|
}
|
|
11561
11583
|
// 대조군 — 고아가 없으면 이 발견이 나오면 안 된다(오탐 0)
|
|
@@ -11765,6 +11787,35 @@ total++;
|
|
|
11765
11787
|
const cr = ci.runners.find(r => /tools[\\/]ci\.js$/.test(r.file)) || {};
|
|
11766
11788
|
if (!cr.unconditional) bad.push('CI직접호출_출처_유실');
|
|
11767
11789
|
if (PU._detectOrphanGuards(ci).orphanScripts.length) bad.push('CI직접호출인데_고아로_오탐');
|
|
11790
|
+
// (검수 P2) CMD 스텝의 `REM` 주석에서 제품 진입점을 러너로 승격시키면 자기참조가 되살아난다
|
|
11791
|
+
const d6c = path.join(sb4, 'rem');
|
|
11792
|
+
fs.mkdirSync(path.join(d6c, 'bin'), { recursive: true });
|
|
11793
|
+
fs.mkdirSync(path.join(d6c, '.github', 'workflows'), { recursive: true });
|
|
11794
|
+
fs.writeFileSync(path.join(d6c, 'package.json'), JSON.stringify({ name: 'r', version: '0.1.0', bin: { r: 'bin/phantom.js' },
|
|
11795
|
+
scripts: { test: 'node t.js', 'check:leaf': 'node l.js' } }));
|
|
11796
|
+
fs.writeFileSync(path.join(d6c, '.github', 'workflows', 'ci.yml'),
|
|
11797
|
+
'jobs:\n a:\n steps:\n - shell: cmd\n run: |\n REM debug only: node .\\bin\\phantom.js\n echo harmless\n - run: npm test\n');
|
|
11798
|
+
fs.writeFileSync(path.join(d6c, 'bin', 'phantom.js'), 'const x = "npm run check:leaf";\n');
|
|
11799
|
+
const remInp = AUD._collectGuardInputs(d6c);
|
|
11800
|
+
if (remInp.runners.some(r => /phantom\.js$/.test(r.file))) bad.push('CMD주석에서_진입점_수집');
|
|
11801
|
+
if (!PU._detectOrphanGuards(remInp).orphanScripts.includes('check:leaf')) bad.push('CMD주석이_고아를_덮음');
|
|
11802
|
+
// (검수 P2) 상대 경로 해석: 주석 속 참조는 무시, 루트 밖은 내부 파일로 오인 금지, 역슬래시도 해석
|
|
11803
|
+
const rel1 = PU._detectOrphanGuards({
|
|
11804
|
+
packageScripts: { test: 'node scripts/run-a.js && node scripts/run-b.js' },
|
|
11805
|
+
runners: [{ file: '.github/workflows/ci.yml', text: 'run: npm test', viaScript: null, viaScripts: [], unconditional: true },
|
|
11806
|
+
{ file: 'scripts/run-a.js', text: '// require("./a/check.js")\n', viaScript: 'test', viaScripts: ['test'] },
|
|
11807
|
+
{ file: 'scripts/run-b.js', text: 'require("./b/check.js");\n', viaScript: 'test', viaScripts: ['test'] }],
|
|
11808
|
+
scriptFiles: ['scripts/a/check.js', 'scripts/b/check.js'],
|
|
11809
|
+
});
|
|
11810
|
+
if (!rel1.orphanFiles.includes('scripts/a/check.js')) bad.push('주석속_상대참조를_실행으로');
|
|
11811
|
+
if (rel1.orphanFiles.includes('scripts/b/check.js')) bad.push('실제_상대참조를_고아로');
|
|
11812
|
+
const rel2 = PU._detectOrphanGuards({
|
|
11813
|
+
packageScripts: { test: 'node scripts/run.js' },
|
|
11814
|
+
runners: [{ file: '.github/workflows/ci.yml', text: 'run: npm test', viaScript: null, viaScripts: [], unconditional: true },
|
|
11815
|
+
{ file: 'scripts/run.js', text: 'require("../../outside/check.js");\n', viaScript: 'test', viaScripts: ['test'] }],
|
|
11816
|
+
scriptFiles: ['scripts/check.js'],
|
|
11817
|
+
});
|
|
11818
|
+
if (!rel2.orphanFiles.includes('scripts/check.js')) bad.push('루트밖_참조가_내부파일을_덮음');
|
|
11768
11819
|
} finally { try { fs.rmSync(sb4, { recursive: true, force: true }); } catch {} }
|
|
11769
11820
|
}
|
|
11770
11821
|
// ⑰ (검수 P2) 와일드카드가 있다고 실행은 아니다 — `echo "test:*"` 는 아무것도 안 돌린다.
|
|
@@ -11838,6 +11889,10 @@ total++;
|
|
|
11838
11889
|
['산문만', 'jobs:\n a:\n steps:\n - name: TODO check:leaf 붙이기\n run: echo skip\n', true],
|
|
11839
11890
|
['name 뒤 실제 run', 'jobs:\n a:\n steps:\n - name: Run check:leaf\n run: npm run check:leaf\n', false],
|
|
11840
11891
|
['matrix', 'jobs:\n a:\n strategy:\n matrix:\n script: [check:leaf]\n steps:\n - run: npm run ${{ matrix.script }}\n', false],
|
|
11892
|
+
// ⚠ 한때 **허용 목록**(실행 키만 본다)으로 뒤집었다가 되돌렸다 — 아래 두 케이스가 그때 고아로 뒤집혔다(오탐).
|
|
11893
|
+
// 이 저장소의 편향은 "오탐이 경고를 죽인다" 이므로 방향은 과소 보고다. 이 두 줄이 그 결정을 고정한다.
|
|
11894
|
+
['matrix 변수명(실행키 아님)', 'jobs:\n a:\n strategy:\n matrix:\n target: [check:leaf]\n steps:\n - run: npm run ${{ matrix.target }}\n', false],
|
|
11895
|
+
['Azure customCommand', 'steps:\n- task: Npm@1\n inputs:\n command: custom\n customCommand: run check:leaf\n', false],
|
|
11841
11896
|
]) {
|
|
11842
11897
|
const rr = PU._detectOrphanGuards({ packageScripts: { test: 'node t.js', 'check:leaf': 'node l.js' },
|
|
11843
11898
|
runners: [{ file: '.github/workflows/ci.yml', text, viaScript: null, viaScripts: [] }], scriptFiles: [] });
|