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

@ -1,7 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { manifestPath } from "./paths.ts";
import { readIfExists, sha256, writeAtomic } from "./fsx.ts";
import { readIfExists, samePath, sha256, writeAtomic } from "./fsx.ts";
import { extractBlock } from "./marker.ts";
import type { DriftReport, InstallRecord, Manifest, Scope, TargetId } from "./types.ts";
@ -10,22 +9,34 @@ const EMPTY: Manifest = { schema: 1, installs: [] };
export async function readManifest(home?: string): Promise<Manifest> {
const raw = await readIfExists(manifestPath(home));
if (!raw) return structuredClone(EMPTY);
// 손상된 매니페스트로 설치 전체가 막히지 않도록 빈 것으로 되돌린다(원본은 writeManifest 가 보존한다)
return parseManifest(raw) ?? structuredClone(EMPTY);
}
function parseManifest(raw: string): Manifest | null {
try {
const parsed = JSON.parse(raw) as Manifest;
if (parsed.schema !== 1 || !Array.isArray(parsed.installs)) return structuredClone(EMPTY);
if (parsed.schema !== 1 || !Array.isArray(parsed.installs)) return null;
return parsed;
} catch {
// 손상된 매니페스트로 설치 전체가 막히지 않도록 빈 것으로 되돌린다
return structuredClone(EMPTY);
return null;
}
}
export async function writeManifest(m: Manifest, home?: string): Promise<void> {
await writeAtomic(manifestPath(home), JSON.stringify(m, null, 2) + "\n");
const file = manifestPath(home);
// 손상된 매니페스트를 빈 것으로 읽은 뒤 그대로 덮으면 다른 설치 기록이 영구 소실된다.
// 덮기 전에 원본을 manifest.json.corrupt-<시각> 으로 옮겨 복구 여지를 남긴다.
const prev = await readIfExists(file);
if (prev !== null && prev.trim().length > 0 && parseManifest(prev) === null) {
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
await fs.rename(file, `${file}.corrupt-${stamp}`).catch(() => undefined);
}
await writeAtomic(file, JSON.stringify(m, null, 2) + "\n");
}
function sameInstall(a: InstallRecord, target: TargetId, scope: Scope, root: string): boolean {
return a.target === target && a.scope === scope && path.resolve(a.root) === path.resolve(root);
return a.target === target && a.scope === scope && samePath(a.root, root);
}
export function findInstall(