feat(core,cli): harden install engine and extend skill lint and interview eval

- update only touches the recorded project root; project installs do not
  follow symlinks or junctions; repeated --force rotates .orig backups;
  uninstall never deletes outside the recorded root
- run npm/npx without a shell on Windows and pin optional tool versions
- skip the update check in CI and non-interactive output
- lint: cross-document section pointers, preflight row ids, gate rule
  numbers, per-document size budgets, gate copy equality
- eval: scenarios S19-S21, route and read-cost rejudging, interpretation
  card judging, temp-dir previews and table-form cards no longer fail
This commit is contained in:
Yun Chan 2026-10-06 16:55:38 +09:00
parent ee3bef2ce5
commit 9bcb45cbd3
122 changed files with 5836 additions and 831 deletions

View file

@ -0,0 +1,36 @@
// 정적 판정 묶음. 기본 판정(judge)이 끝난 이벤트에 경로 판정(V-36)과 읽기 비용(V-40)을 얹는다.
// run.mjs(실행 직후)와 rejudge.mjs(기존 원본 스트림 재판정)가 같은 함수를 쓴다. 모델을 부르지 않는다.
import fs from "node:fs";
import path from "node:path";
import { detectSkillInvoked } from "./judge.mjs";
import { judgeRoute } from "./route.mjs";
import { readStats, judgeReads, formatReadStats } from "./read-stats.mjs";
/** 실행 디렉터리(`…/<runner>/<S6-1>/`)에서 평가한 스킬 사본을 찾는다. 없으면 null. */
export function findSkillCopy(runDir) {
for (const rel of [".claude/skills/designpaca", ".agents/skills/designpaca"]) {
const p = path.join(runDir, "work", rel);
if (fs.existsSync(path.join(p, "SKILL.md"))) return p;
}
return null;
}
/**
* @param {object} scenario scenarios.json 의 한 항목
* @param {Array} events normalizeClaude/normalizeCodex 결과
* @param {{skillDir?: string|null}} [opts]
* @returns {{route: object|null, reads: object, readsVerdict: object|null, readsLine: string}}
* route judgeRoute 결과(`expectedRoute` 가 없거나 스킬이 호출되지 않았으면 null)
* reads readStats 결과
* readsVerdict judgeReads 결과(스킬 문서를 읽지 않았으면 null)
*/
export function staticJudgements(scenario, events, opts = {}) {
const invoked = detectSkillInvoked(events);
const reads = readStats(events, { skillDir: opts.skillDir || undefined });
return {
route: invoked ? judgeRoute(scenario, events) : null,
reads,
readsVerdict: judgeReads(scenario, reads),
readsLine: formatReadStats(reads),
};
}