leerness 1.36.113 → 1.36.116
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 +222 -49
- package/lib/audit.js +535 -421
- package/lib/catalogs.js +505 -505
- package/lib/pure-utils.js +220 -2
- package/package.json +1 -1
- package/scripts/e2e.js +729 -1
package/lib/pure-utils.js
CHANGED
|
@@ -283,7 +283,9 @@ function _resolveNpmTag(explicit, env) {
|
|
|
283
283
|
|
|
284
284
|
// 1.9.283: .mcp.json 내용 (UR-0033) — leerness MCP 서버 등록.
|
|
285
285
|
function _mcpJsonContent() {
|
|
286
|
-
|
|
286
|
+
// 1.36.116 (검수 #13): `-y` 없으면 npx 가 설치 여부를 되묻고, GUI 클라이언트는 그 프롬프트에 답할 수 없다.
|
|
287
|
+
// `_mergeMcpJson` · `mcp install` 안내와 **같은 값**이어야 한다(사본 3벌 드리프트 방지).
|
|
288
|
+
return JSON.stringify({ mcpServers: { leerness: { command: 'npx', args: ['-y', 'leerness', 'mcp', 'serve'] } } }, null, 2) + '\n';
|
|
287
289
|
}
|
|
288
290
|
|
|
289
291
|
// 1.9.283: run 레코드 빌더 (UR-0032) — GPT-5.5 권고 14필드. startedAt 주입 가능(테스트).
|
|
@@ -1020,6 +1022,222 @@ function _managedMerge(file, next, previous, archiveRel, overwriteSet, opts = {}
|
|
|
1020
1022
|
// 그동안 **비용을 한 번도 재지 않았다**. 이 함수로 잰 실측(설치본 45개)에서 12개가 이월분을 갖고 합계 10,895 tok,
|
|
1021
1023
|
// 최악(이 저장소)은 지침 파일의 78.0%(6,048 tok)이고, minimal 등급으로 낮춰도 적재 7,242 tok 중 83% 가 그것이다.
|
|
1022
1024
|
// 여기서 하는 일은 **재는 것뿐**이다 — 지우지 않는다. 삭제를 막은 두 번의 판단(false-DROP 이 버그)은 그대로 유효하다.
|
|
1025
|
+
// 1.36.116 — "존재하지만 아무도 실행하지 않는 가드" 탐지 (순수 코어).
|
|
1026
|
+
// 왜: 실제 프로젝트(hive-analytics)의 CI 파일이 자기 사고를 이렇게 적어 놨다 —
|
|
1027
|
+
// "가드가 여럿 있는데 CI 에 하나도 연결돼 있지 않았다. 배포 전 검수가 세 번 연속 실질 결함을 찾아냈고,
|
|
1028
|
+
// 그중 여러 건이 '가드는 있는데 아무도 안 돌렸다' 에서 왔다. 사람이 돌려야만 유효한 검사는 결국 안 돌아간다."
|
|
1029
|
+
// leerness 는 프로젝트 위생을 감사하는 도구인데 이 클래스를 보지 않았다.
|
|
1030
|
+
//
|
|
1031
|
+
// ⚠ 설계에서 제일 중요한 것은 **오탐 방지**다. 잘 만든 프로젝트는 CI 에 검사 이름을 일일이 적지 않고
|
|
1032
|
+
// package.json 을 **동적으로 열거**한다(hive 의 `run-ci-checks.mjs` 가 그렇다). 그걸 못 알아보면
|
|
1033
|
+
// 가장 모범적인 저장소에서 수십 건 오탐이 나고, 그러면 사용자는 이 경고를 꺼 버린다.
|
|
1034
|
+
// 그래서 편향은 false-PASS 다: 동적 열거자가 하나라도 보이면 npm 스크립트는 전부 '덮임' 으로 본다.
|
|
1035
|
+
// 1.36.116 (검수 #3): `guard:*` 가 후보 어휘에 없었다 — 이 기능이 잡으려는 것의 **이름 그 자체**인데
|
|
1036
|
+
// `guard:firestore-parity` 같은 스크립트는 후보가 0개라 구조적으로 못 봤다. `validate` 도 접미 위치에 없었다.
|
|
1037
|
+
const _GUARD_NAME_RE = /^(?:pre|post)?(?:test|check|verify|validate|guard|gate|lint|typecheck|audit|scan|e2e|smoke)\b|[:-](?:test|check|verify|validate|guard|gate|lint|audit|scan|e2e|smoke)\b/i;
|
|
1038
|
+
// 동적 열거자 판정은 **두 신호를 독립적으로** 본다 — 한 파일이 package.json 을 읽고, scripts 를 열거한다.
|
|
1039
|
+
// ⚠ 근접 창(예: 400자)을 요구했다가 실측에서 깨졌다: 실제 열거자는 읽기(L42)와 열거(L98)가 56줄 떨어져 있었고,
|
|
1040
|
+
// 그 결과 가장 잘 만든 프로젝트에서 85건 오탐이 났다. 거리 가정을 쓰지 않는다.
|
|
1041
|
+
const _READS_PKG_RE = /package\.json/;
|
|
1042
|
+
const _ENUMS_SCRIPTS_RE = /\b(?:Object\.(?:keys|entries)\s*\(\s*[\w.?\s]*\bscripts\b|for\s*\(\s*const\s+\w+\s+in\s+[\w.]*\bscripts\b)/;
|
|
1043
|
+
// 열거자가 **무엇을 덮는지**도 뽑는다 — `startsWith("test:")` 처럼 접두를 거르면 그 밖은 안 도는 게 사실이다.
|
|
1044
|
+
// 1.36.116 (검수 #6): `/^test:/.test(name)` 형태를 못 뽑아 접두가 0개가 되고, 그러면 "전부 덮는다" 로 처리돼
|
|
1045
|
+
// 같은 파일의 `check:dead` 까지 실행된 것으로 계상됐다(미탐). 정규식 리터럴 + `.test(` 형태를 추가한다.
|
|
1046
|
+
const _ENUM_PREFIX_RE = /startsWith\s*\(\s*["'`]([\w:.-]+)["'`]\s*\)|\.match\s*\(\s*\/\^([\w:.-]+)|\/\^([\w:.-]+)[^/\n]*\/[a-z]*\s*\.test\s*\(/g;
|
|
1047
|
+
function _detectOrphanGuards(input) {
|
|
1048
|
+
const scripts = (input && input.packageScripts) || {}; // { name: body }
|
|
1049
|
+
const runners = (input && input.runners) || []; // [{ file, text }] — CI 워크플로 등
|
|
1050
|
+
const scriptFiles = (input && input.scriptFiles) || []; // ['scripts/check-x.js', ...]
|
|
1051
|
+
// ⚠ 관례상 **진입점**인 이름은 대상이 아니다 — `npm test` 는 사람도 CI 도 직접 부르는 표준 동사다.
|
|
1052
|
+
// 신규 프로젝트(스크립트 `test` 하나, CI 없음)에서 그걸 "아무도 안 부른다" 고 하면 그건 잡음이고,
|
|
1053
|
+
// 잡음은 이 경고를 죽인다. 우리가 잡으려는 것은 **네임스페이스 잎 가드**(`test:foo`·`check-bar`)다 —
|
|
1054
|
+
// 그건 누가 모아 주지 않으면 실제로 아무도 안 부른다.
|
|
1055
|
+
const ENTRYPOINTS = new Set(['test', 'start', 'build', 'lint', 'typecheck', 'check', 'verify', 'audit', 'e2e', 'prepare', 'prepublishOnly']);
|
|
1056
|
+
// npm 생명주기 훅(`preX`/`postX`)은 **npm 자신이 러너**다 — X 가 존재하면 자동으로 돈다.
|
|
1057
|
+
// 이걸 고아라고 하면 오탐이고, 오탐이 이 경고를 죽인다(작성 중 `pretest`/`posttest` 로 실제 발생).
|
|
1058
|
+
const NPM_LIFECYCLE = new Set(['test', 'start', 'restart', 'stop', 'install', 'publish', 'pack', 'version', 'prepare']);
|
|
1059
|
+
const isLifecycleHook = (n) => {
|
|
1060
|
+
const m = /^(pre|post)(.+)$/.exec(n);
|
|
1061
|
+
if (!m) return false;
|
|
1062
|
+
const base = m[2];
|
|
1063
|
+
return Object.prototype.hasOwnProperty.call(scripts, base) || NPM_LIFECYCLE.has(base);
|
|
1064
|
+
};
|
|
1065
|
+
const guardNames = Object.keys(scripts).filter(n => _GUARD_NAME_RE.test(n) && !ENTRYPOINTS.has(n) && !isLifecycleHook(n));
|
|
1066
|
+
|
|
1067
|
+
// ① 동적 열거자와 그것이 덮는 접두
|
|
1068
|
+
// ⚠ 알려진 한계: **문자열 리터럴 안의 코드 모양**도 열거자로 본다. 실제로 이 저장소의 e2e 픽스처가
|
|
1069
|
+
// 그렇게 잡혀 leerness 자신의 고아(`test:smoke`)가 가려진 적이 있다(자기참조 함정).
|
|
1070
|
+
// 따옴표 구간을 손으로 가려 보려다 **정상 입력에서 짝이 어긋나 더 나빠졌다** — 손으로 만든 JS 마스커는
|
|
1071
|
+
// 이 저장소가 이미 한 번 토끼굴에 빠졌던 자리다(1.36.x, 검수 7회). 그래서 마스킹을 쓰지 않는다.
|
|
1072
|
+
// 이 오인의 방향은 **과소 보고(false-PASS)** 이고, 그건 이 기능의 의도된 편향이다 —
|
|
1073
|
+
// 오탐이 나면 사용자가 경고 자체를 꺼 버리기 때문이다. 대신 픽스처 쪽에서 리터럴을 쪼개 자기참조를 없앤다.
|
|
1074
|
+
const dynamicRunners = [];
|
|
1075
|
+
const coveredPrefixes = new Set();
|
|
1076
|
+
const _scriptNames = Object.keys(scripts);
|
|
1077
|
+
// 뽑은 접두를 그대로 믿으면 안 된다 — 큰 소스 파일에서는 `--*`·`_*`·`.*`·`https:*` 같은 것이 수십 개 나오고,
|
|
1078
|
+
// 그중 하나만 스크립트 이름에 걸려도 전부가 '덮임' 이 돼 진짜 고아가 사라진다(leerness 자신에서 실측).
|
|
1079
|
+
// 접두로 인정하는 조건: **이 패키지의 스크립트 이름에 실제로 붙고**, 이름 전체가 아니며, 너무 짧지 않다.
|
|
1080
|
+
// ⚠ 길이 3 을 요구했다가 `startsWith("ci")` 같은 **정상 열거자**를 통째로 버렸다(검수 P2) — 그러면 그 열거자가
|
|
1081
|
+
// 실제로 돌리는 `ci-check`·`ci-guard` 가 고아로 나온다. 진짜 판별자는 길이가 아니라
|
|
1082
|
+
// "이 패키지의 스크립트 이름에 실제로 붙는가" 다. 길이는 1글자만 막는다.
|
|
1083
|
+
const _plausible = (p) => !!p && (/[:._-]$/.test(p) || p.length >= 2)
|
|
1084
|
+
&& _scriptNames.some(n => n !== p && n.startsWith(p));
|
|
1085
|
+
for (const r of runners) {
|
|
1086
|
+
const raw = String(r.text || '');
|
|
1087
|
+
const t = raw;
|
|
1088
|
+
if (!(_READS_PKG_RE.test(raw) && _ENUMS_SCRIPTS_RE.test(t))) continue;
|
|
1089
|
+
// 접두는 **원문**에서 뽑는다(가린 텍스트에는 문자열이 지워져 있다)
|
|
1090
|
+
let m; const re = new RegExp(_ENUM_PREFIX_RE.source, 'g');
|
|
1091
|
+
const rawFound = [], valid = [];
|
|
1092
|
+
while ((m = re.exec(raw))) { const p = m[1] || m[2] || m[3]; if (p) { rawFound.push(p); if (_plausible(p)) valid.push(p); } }
|
|
1093
|
+
// 접두 후보는 많은데 이 패키지의 스크립트와 하나도 안 맞으면, 그건 열거자의 필터가 아니라 남의 코드다 —
|
|
1094
|
+
// "전부 덮는다" 로 올리면 이 경고가 통째로 죽으므로 **열거자로 치지 않는다**.
|
|
1095
|
+
if (rawFound.length && !valid.length) continue;
|
|
1096
|
+
dynamicRunners.push(r.file);
|
|
1097
|
+
for (const p of valid) coveredPrefixes.add(p);
|
|
1098
|
+
}
|
|
1099
|
+
// 접두를 못 뽑았으면 "전부 덮는다" 로 본다 — 편향은 false-PASS 다(오탐이 이 경고를 죽인다).
|
|
1100
|
+
const enumCoversAll = dynamicRunners.length > 0 && coveredPrefixes.size === 0;
|
|
1101
|
+
|
|
1102
|
+
// ② 이름 언급 — 호출 문법을 파싱하지 않고 **이름이 러너 텍스트에 나오는가**로 본다.
|
|
1103
|
+
// `spawn("npm",["run","test:unit"])` 같은 배열 형태를 문법 파싱으로 잡으려다 실측에서 놓쳤다.
|
|
1104
|
+
// 느슨한 쪽(false-PASS)이 옳다 — 산문에 우연히 나와도 '덮임' 으로 보는 편이 오탐보다 낫다.
|
|
1105
|
+
const allRunnerText = runners.map(r => String(r.text || '')).join('\n') + '\n'
|
|
1106
|
+
+ Object.entries(scripts).map(([n, b]) => `${n} ${b}`).join('\n');
|
|
1107
|
+
const mentions = (name) => {
|
|
1108
|
+
const esc = String(name).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1109
|
+
return new RegExp(`(^|[^\\w:.-])${esc}([^\\w:.-]|$)`, 'm').test(allRunnerText);
|
|
1110
|
+
};
|
|
1111
|
+
const coveredByEnum = (n) => enumCoversAll || [...coveredPrefixes].some(p => n.startsWith(p));
|
|
1112
|
+
// **주석 줄의 언급은 실행이 아니다.** 이 저장소에서 실제로 겪었다 — 이 함정을 설명하려고 쓴 주석에
|
|
1113
|
+
// 스크립트 이름을 적었더니 그 이름이 '덮임' 으로 처리돼 진짜 고아가 사라졌다(자기참조 3회차).
|
|
1114
|
+
// 전면 문자열 마스킹은 앞서 정상 입력에서 깨졌으므로 쓰지 않는다 — 줄 시작이 주석인 줄만 뺀다(저위험).
|
|
1115
|
+
const _dropCommentLines = (s) => String(s).split('\n').filter(l => !/^\s*(\/\/|#|\*|--)/.test(l)).join('\n');
|
|
1116
|
+
const _has = (hay, name) => {
|
|
1117
|
+
const esc = String(name).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1118
|
+
return new RegExp(`(^|[^\\w:.-])${esc}([^\\w:.-]|$)`, 'm').test(hay);
|
|
1119
|
+
};
|
|
1120
|
+
const names = Object.keys(scripts);
|
|
1121
|
+
|
|
1122
|
+
// ②-a 우리가 그래프를 볼 수 없는 오케스트레이터가 있으면 **아무것도 보고하지 않는다**.
|
|
1123
|
+
// turbo/nx/lerna 는 배선이 자기 설정 파일(turbo.json 의 dependsOn 등)에 있어서 스크립트 본문만 봐선 안 보인다.
|
|
1124
|
+
// 못 보는 배선을 "없는 배선" 이라고 하면 정상 저장소에서 대량 오탐이 나고, 그러면 사용자는 이 경고를 끈다.
|
|
1125
|
+
// ⚠ 처음엔 `\b(turbo|nx|lerna)\b` 로 **모든 러너 텍스트**를 봤다가 leerness 자신에서 오발화했다:
|
|
1126
|
+
// 수집기가 27k 줄짜리 `bin/leerness.js` 도 러너로 담는데 거기 어딘가에 `nx` 가 있어서 판정이 통째로 보류되고
|
|
1127
|
+
// 진짜 고아(`test:smoke`)가 사라졌다. 짧은 단어를 큰 텍스트에 대면 반드시 걸린다 —
|
|
1128
|
+
// **호출 형태**를 요구하고, 러너는 CI 설정 파일로 한정한다.
|
|
1129
|
+
const _ORCH_RE = /(?:^|[\s"'`&|;(])(?:npx\s+|pnpm\s+|yarn\s+)?(?:turbo|nx|lerna)\s+(?:run|affected|exec|watch)\b/;
|
|
1130
|
+
const _isConfigRunner = (f) => /(?:\.ya?ml|Makefile|makefile|GNUmakefile|Jenkinsfile|justfile)$|\.husky\//.test(String(f || ''));
|
|
1131
|
+
|
|
1132
|
+
// ②-b **러너 루트에서의 도달성**으로 판정한다. 이전 구현은 "이름이 어디든 나오면 덮임" 이었고,
|
|
1133
|
+
// 그건 양방향으로 틀렸다(검수 재현 2건):
|
|
1134
|
+
// · 오탐 — `"test":"npm-run-all test:*"` 는 glob 이라 `test:unit` 이라는 문자열이 어디에도 없다 → 정상 배선이 고아.
|
|
1135
|
+
// · 미탐 — 아무도 안 부르는 `dead-wrapper` 가 `npm run check:schema` 를 적어 두면 leaf 가 '덮임'.
|
|
1136
|
+
// 고아끼리 순환(`check:a ⇄ check:b`)이면 둘 다 덮임이 된다.
|
|
1137
|
+
// 그래서 간선(스크립트→스크립트)을 만들고 루트에서 BFS 한다.
|
|
1138
|
+
const edges = new Map();
|
|
1139
|
+
for (const [a, bodyRaw] of Object.entries(scripts)) {
|
|
1140
|
+
const body = _dropCommentLines(String(bodyRaw || ''));
|
|
1141
|
+
const out = new Set();
|
|
1142
|
+
for (const b of names) if (b !== a && _has(body, b)) out.add(b);
|
|
1143
|
+
// glob 간선 — `npm-run-all test:*` / `run-s "check:*"` 는 이름을 적지 않고 접두로 태운다.
|
|
1144
|
+
// ⚠ 와일드카드가 있다고 다 실행은 아니다(검수 P2): `"test": "echo \"test:*\""` 는 아무것도 안 돌리는데
|
|
1145
|
+
// 간선을 만들어 진짜 고아를 숨겼다. **스크립트를 실행하는 문맥**일 때만 glob 을 간선으로 본다.
|
|
1146
|
+
if (/\b(?:npm-run-all|run-s|run-p|npm\s+run|yarn\s+run|pnpm\s+run)\b/.test(body)) {
|
|
1147
|
+
for (const m of body.matchAll(/([\w:.-]+)\*/g)) {
|
|
1148
|
+
for (const b of names) if (b !== a && b.startsWith(m[1])) out.add(b);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
edges.set(a, out);
|
|
1152
|
+
}
|
|
1153
|
+
// 루트 = ① CI 설정 러너 텍스트에 이름이 나오는 것 ② 관례상 사람이 직접 부르는 진입점 동사
|
|
1154
|
+
// ③ npm 자신이 돌리는 생명주기 스크립트 ④ **도달 가능한** 스크립트가 부르는 러너 파일에 나오는 것
|
|
1155
|
+
// ④ 때문에 루트 집합이 도달성에 의존한다 → 고정점까지 반복한다(검수 P2: 죽은 wrapper 가 참조하는
|
|
1156
|
+
// 러너 파일이 잎을 살려 주던 우회로를 닫는다).
|
|
1157
|
+
const NPM_SELF_RUN = ['prepare', 'prepack', 'postpack', 'prepublishOnly', 'postinstall', 'preinstall', 'install'];
|
|
1158
|
+
const seedRoots = new Set();
|
|
1159
|
+
for (const n of names) if (ENTRYPOINTS.has(n)) seedRoots.add(n);
|
|
1160
|
+
for (const n of names) if (NPM_SELF_RUN.includes(n) || isLifecycleHook(n)) seedRoots.add(n);
|
|
1161
|
+
const reachable = new Set();
|
|
1162
|
+
const usedRunners = new Set();
|
|
1163
|
+
const roots = new Set(seedRoots);
|
|
1164
|
+
const _bfs = () => {
|
|
1165
|
+
reachable.clear();
|
|
1166
|
+
const queue = [...roots];
|
|
1167
|
+
while (queue.length) {
|
|
1168
|
+
const n = queue.shift();
|
|
1169
|
+
if (reachable.has(n)) continue;
|
|
1170
|
+
reachable.add(n);
|
|
1171
|
+
for (const b of (edges.get(n) || [])) if (!reachable.has(b)) queue.push(b);
|
|
1172
|
+
// npm 생명주기 훅은 base 가 도는 순간 npm 이 같이 돌린다
|
|
1173
|
+
for (const p of ['pre' + n, 'post' + n]) if (Object.prototype.hasOwnProperty.call(scripts, p) && !reachable.has(p)) queue.push(p);
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
const _liveRunner = (r) => !r.viaScript || reachable.has(r.viaScript) || coveredByEnum(r.viaScript);
|
|
1177
|
+
for (let pass = 0; pass <= runners.length + 1; pass++) {
|
|
1178
|
+
_bfs();
|
|
1179
|
+
let grew = false;
|
|
1180
|
+
for (const r of runners) {
|
|
1181
|
+
if (usedRunners.has(r.file) || !_liveRunner(r)) continue;
|
|
1182
|
+
usedRunners.add(r.file);
|
|
1183
|
+
// ⚠ 러너 텍스트에 이름이 **나오기만** 하면 루트로 봤더니, 이 저장소의 테스트 파일(scripts/e2e.js)이
|
|
1184
|
+
// 러너로 수집되는 탓에 거기 데이터로 적힌 스크립트 이름이 전부 '호출됨' 이 됐다 —
|
|
1185
|
+
// 자기 기준값을 단언하려고 이름을 적자마자 그 기준값이 사라졌다(자기참조 6회차).
|
|
1186
|
+
// 리터럴을 또 쪼개는 것은 이 함정이 재발할 자리를 남기므로, **호출 문맥이 있는 줄**만 본다.
|
|
1187
|
+
// 줄 단위라 거리 가정(예전에 85건 오탐을 낸 근접 창)은 쓰지 않고, `spawn("npm",["run","x"])` 같은
|
|
1188
|
+
// 배열 형태도 같은 줄에 `npm` 이 있어 그대로 잡힌다.
|
|
1189
|
+
const t = _dropCommentLines(String(r.text || ''))
|
|
1190
|
+
.split('\n').filter(l => /\b(?:npm|npx|yarn|pnpm|make|run|exec)\b/.test(l)).join('\n');
|
|
1191
|
+
for (const n of names) if (!roots.has(n) && _has(t, n)) { roots.add(n); grew = true; }
|
|
1192
|
+
grew = true;
|
|
1193
|
+
}
|
|
1194
|
+
if (!grew) break;
|
|
1195
|
+
}
|
|
1196
|
+
_bfs();
|
|
1197
|
+
const isLive = (n) => reachable.has(n) || coveredByEnum(n);
|
|
1198
|
+
// 오케스트레이터 판정도 **실행되는 스크립트**에서만 본다 — 죽은 `"note":"echo turbo run test"` 한 줄로
|
|
1199
|
+
// 경고 전체를 끌 수 있으면 그건 우회로다(검수 P2).
|
|
1200
|
+
const unknownOrchestrator = Object.entries(scripts).some(([n, b]) => isLive(n) && _ORCH_RE.test(String(b || '')))
|
|
1201
|
+
|| runners.some(r => _isConfigRunner(r.file) && _ORCH_RE.test(String(r.text || '')));
|
|
1202
|
+
const orphanScripts = unknownOrchestrator ? [] : guardNames.filter(n => !isLive(n));
|
|
1203
|
+
void mentions;
|
|
1204
|
+
|
|
1205
|
+
// ③ scripts/ 아래 파일 가드 — 어디에서도 참조되지 않는 것
|
|
1206
|
+
// npm 스크립트 본문 · 러너 텍스트 어디에도 파일명이 안 나오면 사람이 기억해야만 도는 검사다.
|
|
1207
|
+
// 1.36.116: 참조하는 쪽이 **도달 가능해야** 덮은 것으로 본다 — 아무도 안 부르는 스크립트가 파일을
|
|
1208
|
+
// 언급해 봐야 그 파일도 안 도는 건 마찬가지다(스크립트 쪽 #7 과 같은 결함이 파일 쪽에도 있었다).
|
|
1209
|
+
const liveScripts = Object.entries(scripts).filter(([n]) => isLive(n));
|
|
1210
|
+
const allText = runners.filter(_liveRunner).map(r => String(r.text || '')).join('\n') + '\n'
|
|
1211
|
+
+ liveScripts.map(([, b]) => String(b || '')).join('\n');
|
|
1212
|
+
const orphanFiles = unknownOrchestrator ? [] : scriptFiles.filter(f => {
|
|
1213
|
+
const base = String(f).split(/[\\/]/).pop();
|
|
1214
|
+
return !allText.includes(base) && !allText.includes(String(f).replace(/\\/g, '/'));
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
// 1.36.116 (검수 #9): 수집이 불완전했으면 그 사실을 결과에 실어야 한다 — 읽기 실패는 곧 **오탐**이 되고
|
|
1218
|
+
// (그 러너가 부르던 정상 가드가 '아무도 안 부름' 이 된다), 사용자는 근거를 봐야 그 목록을 믿을지 정한다.
|
|
1219
|
+
const scanIncomplete = (Number(input && input.readErrors) || 0) + (Number(input && input.skippedLarge) || 0);
|
|
1220
|
+
const _incompleteNote = scanIncomplete
|
|
1221
|
+
? ` ⚠ 수집 불완전(읽기실패 ${Number(input.readErrors) || 0} · 대용량 제외 ${Number(input.skippedLarge) || 0}) — 이 목록은 과다보고일 수 있다`
|
|
1222
|
+
: '';
|
|
1223
|
+
return {
|
|
1224
|
+
guardCount: guardNames.length,
|
|
1225
|
+
dynamicRunners,
|
|
1226
|
+
enumPrefixes: [...coveredPrefixes],
|
|
1227
|
+
orphanScripts,
|
|
1228
|
+
orphanFiles,
|
|
1229
|
+
scanIncomplete,
|
|
1230
|
+
// 판정 근거를 함께 돌려준다 — 숫자만 보고 못 믿는 일이 없도록(사용자가 근거 없이 목록을 받으면 그냥 끈다)
|
|
1231
|
+
reason: (unknownOrchestrator
|
|
1232
|
+
? 'turbo/nx/lerna 배선은 설정 파일에 있어 스크립트 그래프로 볼 수 없다 — 판정 보류(보고 안 함)'
|
|
1233
|
+
: dynamicRunners.length
|
|
1234
|
+
? (enumCoversAll
|
|
1235
|
+
? `동적 열거자(${dynamicRunners.join(', ')})가 전체를 태운다`
|
|
1236
|
+
: `동적 열거자(${dynamicRunners.join(', ')})가 ${[...coveredPrefixes].map(p => p + '*').join(', ')} 만 태운다 — 그 밖은 별도 배선 필요`)
|
|
1237
|
+
: `러너 ${runners.length}개 + 진입점 ${[...roots].length}개에서 도달성 추적 (도달 ${reachable.size}/${names.length})`) + _incompleteNote,
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1023
1241
|
const _TAG_ESC = PRESERVED_TAG.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1024
1242
|
// 경계는 **생성기가 쓰는 opener 전체**로만 확정한다. 태그 하나 또는 제목 하나로 정하면 두 가지를 오인한다
|
|
1025
1243
|
// (검수 재현): (a) 사용자가 직접 쓴 `## Preserved previous content` 섹션 51B, (b) 문서에 인용된 태그 60B.
|
|
@@ -1481,7 +1699,7 @@ module.exports = { _decisionBlocksWithOffset, _blocksWithOffset, _lineOfOffset,
|
|
|
1481
1699
|
_matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd, GLOSSARY_START, GLOSSARY_END,
|
|
1482
1700
|
_isSecretKey, redactSecrets, hasCredentialMarker, compareVer, parseHarnessVersion,
|
|
1483
1701
|
_isPlaceholderSecret, _looksSecretLike,
|
|
1484
|
-
_mergeLines, _mergeEnvLines, _mergeReadmeSection, _managedMerge, _splitPreserved, PRESERVED_TAG, _parseSkillsValue,
|
|
1702
|
+
_mergeLines, _mergeEnvLines, _mergeReadmeSection, _managedMerge, _splitPreserved, PRESERVED_TAG, _detectOrphanGuards, _parseSkillsValue,
|
|
1485
1703
|
_parseArchiveBlocks, _parseSkillCatalog, _renderTeamsMd, _composeTeamPlan, _teamHandoffReminders, _cadenceAssessment, _teamDeployGate, _renderWorkspaceReferenceGuide, _memorySurface, _renderPulseLine,
|
|
1486
1704
|
_classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp,
|
|
1487
1705
|
// 1.9.283 (UR-0025 2단계)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.116",
|
|
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",
|