@kaydash9999/banker-plugins 0.26.0 → 0.26.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/codex/manifest.json +1 -1
- package/codex/transform-matrix.md +1 -1
- package/package.json +1 -1
- package/skills/make-evidence/SKILL.md +207 -131
- package/skills/make-evidence/scripts/evidence.mjs +162 -0
- package/skills/make-evidence/scripts/me-check.mjs +125 -0
- package/skills/make-evidence/scripts/me-commit.mjs +48 -0
- package/skills/make-evidence/scripts/me-git.mjs +63 -0
- package/skills/make-evidence/scripts/me-oracle.mjs +244 -0
- package/skills/make-evidence/scripts/me-precheck.mjs +122 -0
- package/skills/make-evidence/scripts/me-run.mjs +214 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* evidence.mjs: the helper the make-evidence skill runs. It records commands into logs, checks the numbers written
|
|
4
|
+
* in the evidence documents against those logs, gives the oracle writer a room outside the repository, runs the
|
|
5
|
+
* seeded bugs in a separate worktree, and commits only the evidence paths. Pure Node, no dependencies, no network.
|
|
6
|
+
*
|
|
7
|
+
* node evidence.mjs precheck [--repo <dir>]
|
|
8
|
+
* node evidence.mjs run --log <file> [--timeout <s>] [--cwd <dir>] [--shell] -- <command...>
|
|
9
|
+
* node evidence.mjs serve --log <file> --pid <file> --ready <url> [--wait <s>] [--cwd <dir>] [--shell] -- <command...>
|
|
10
|
+
* node evidence.mjs stop --pid <file>
|
|
11
|
+
* node evidence.mjs check [--repo <dir>] [--tracked]
|
|
12
|
+
* node evidence.mjs count --doc <trace.md>
|
|
13
|
+
* node evidence.mjs room [--repo <dir>] [--req <file>]...
|
|
14
|
+
* node evidence.mjs collect --room <dir> [--repo <dir>] [--replace]
|
|
15
|
+
* node evidence.mjs worktree [--repo <dir>] [--link <untracked dependency folder>]... | worktree --remove <dir>
|
|
16
|
+
* node evidence.mjs bug --worktree <dir> --name <name> [--kind <kind> --baseline <log>] [--timeout <s>] [--shell] -- <oracle command...>
|
|
17
|
+
* node evidence.mjs commit --message <text> [--paths <p>]... [--accept <file:line>]... [--repo <dir>]
|
|
18
|
+
*
|
|
19
|
+
* - A log name may hold {ts} or @TS@; it becomes a UTC time with milliseconds. An existing log is never overwritten.
|
|
20
|
+
* - A log starts with the command line (shell-quoted, secrets masked) and `# head= dirty= at= node= os= cwd=`, holds
|
|
21
|
+
* stdout and stderr as they come (secrets masked), and ends with `# redacted=<n>`, `exit=<code>` (or
|
|
22
|
+
* `exit=timeout(<s>s)`, `exit=interrupted(<signal>)`) and `METRIC run.exit=` plus the test counts it recognised.
|
|
23
|
+
* - Measured numbers go into a log as `METRIC <name.with.dots>=<value>`; a document cites them in backticks on a
|
|
24
|
+
* line that names the log, and check looks for that exact line there.
|
|
25
|
+
* - Oracle output lines are `ORACLE <constraint|objective|permission> <PASS|FAIL|ERROR> <case> [note]`.
|
|
26
|
+
*
|
|
27
|
+
* Exit status: 0 done, 1 a check failed (numbers, room, tree changed, commit refused, unknown trace status), 2
|
|
28
|
+
* usage error or a step that could not run, 3 precheck found something the user must decide, 124 run hit its
|
|
29
|
+
* time limit; run otherwise passes the command's own exit code through.
|
|
30
|
+
*/
|
|
31
|
+
import { readFileSync } from "node:fs";
|
|
32
|
+
import { resolve } from "node:path";
|
|
33
|
+
import { pathToFileURL } from "node:url";
|
|
34
|
+
import { OWN, ORACLE, Usage } from "./me-git.mjs";
|
|
35
|
+
import { runLogged, serve, stop } from "./me-run.mjs";
|
|
36
|
+
import { precheck } from "./me-precheck.mjs";
|
|
37
|
+
import { checkDocs, countTrace } from "./me-check.mjs";
|
|
38
|
+
import { addWorktree, collectRoom, makeRoom, removeWorktree, runBug } from "./me-oracle.mjs";
|
|
39
|
+
import { commitEvidence } from "./me-commit.mjs";
|
|
40
|
+
|
|
41
|
+
const USAGE = `make-evidence: 증거 문서용 도우미(기록 실행, 수치 대조, 판정기 방, 버그 작업 트리, 증거만 커밋)
|
|
42
|
+
사용법: node evidence.mjs <precheck|run|serve|stop|check|count|room|collect|worktree|bug|commit> [인자]
|
|
43
|
+
precheck [--repo <dir>] 스테이징, 미커밋 변경, 이 컴퓨터 밖 주소(설정, 환경 변수). 결정할 것이 있으면 종료 3
|
|
44
|
+
run --log <file> [--timeout <s>] [--cwd <dir>] [--shell] -- <명령...> 기본 제한 300초
|
|
45
|
+
serve --log <file> --pid <file> --ready <url> [--wait <s>] [--cwd <dir>] [--shell] -- <명령...>
|
|
46
|
+
stop --pid <file>
|
|
47
|
+
check [--repo <dir>] [--tracked] 문서의 \`이름.점=값\` 을 가리킨 로그의 METRIC 줄과 대조, 손 숫자 경고
|
|
48
|
+
count --doc <trace.md> trace 표의 상태 칸을 세어 METRIC 줄로 출력
|
|
49
|
+
room [--repo <dir>] [--req <file>]... 판정기 작성용 방(요구 문서와 owner-facts 만)
|
|
50
|
+
collect --room <dir> [--repo <dir>] [--replace] 방의 tests/oracle 을 저장소로 가져옴
|
|
51
|
+
worktree [--repo <dir>] [--link <폴더>]... | --remove <dir> HEAD 의 별도 작업 트리
|
|
52
|
+
bug --worktree <dir> --name <이름> [--kind <종류> --baseline <log>] [--timeout <s>] [--shell] -- <판정기 명령...>
|
|
53
|
+
commit --message <text> [--paths <p>]... [--accept <file:line>]... [--repo <dir>]
|
|
54
|
+
로그 이름의 {ts} 는 PowerShell 에서 따옴표로 감싸거나 @TS@ 로 씀`;
|
|
55
|
+
|
|
56
|
+
const FLAGS = new Set(["--shell", "--tracked", "--replace"]);
|
|
57
|
+
const MULTI = new Set(["--req", "--paths", "--link", "--accept"]);
|
|
58
|
+
|
|
59
|
+
export function parseArgs(argv) {
|
|
60
|
+
const a = { _: [], rest: [], multi: {} };
|
|
61
|
+
for (let i = 0; i < argv.length; i++) {
|
|
62
|
+
const t = argv[i];
|
|
63
|
+
if (t === "--") { a.rest = argv.slice(i + 1); break; }
|
|
64
|
+
if (FLAGS.has(t)) a[t.slice(2)] = true;
|
|
65
|
+
else if (t.startsWith("--")) {
|
|
66
|
+
if (i + 1 >= argv.length) throw new Usage(`${t} 에 값이 없음`);
|
|
67
|
+
const v = argv[++i];
|
|
68
|
+
if (MULTI.has(t)) (a.multi[t.slice(2)] ||= []).push(v);
|
|
69
|
+
else a[t.slice(2)] = v;
|
|
70
|
+
} else a._.push(t);
|
|
71
|
+
}
|
|
72
|
+
return a;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const need = (a, k) => { if (!a[k]) throw new Usage(`--${k} 가 필요함`); return a[k]; };
|
|
76
|
+
const seconds = (v, d) => { const n = v === undefined ? d : Number(v); if (!(n > 0)) throw new Usage(`시간 값이 잘못됨: ${v}`); return n; };
|
|
77
|
+
const say = (xs) => { for (const x of xs) console.log(x); };
|
|
78
|
+
|
|
79
|
+
async function cmdRun(a) {
|
|
80
|
+
const r = await runLogged({ log: need(a, "log"), argv: a.rest, cwd: resolve(a.cwd || "."), timeoutS: seconds(a.timeout, 300), shell: !!a.shell });
|
|
81
|
+
console.log(`run: exit=${r.exit} log=${r.file}`);
|
|
82
|
+
return r.code;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function cmdServe(a) {
|
|
86
|
+
const r = await serve({ log: need(a, "log"), pidFile: resolve(need(a, "pid")), ready: need(a, "ready"), waitS: seconds(a.wait, 60), argv: a.rest, cwd: resolve(a.cwd || "."), shell: !!a.shell });
|
|
87
|
+
console.log(`serve: ${r.ready ? "ready" : `${r.why}, stopped`} pid=${r.pid} log=${r.file}`);
|
|
88
|
+
return r.ready ? 0 : 2;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function cmdCheck(a) {
|
|
92
|
+
const c = checkDocs(a.repo || ".", { tracked: !!a.tracked });
|
|
93
|
+
say(c.errors.map((e) => `ERROR ${e}`));
|
|
94
|
+
say(c.warnings.map((w) => `WARN ${w}`));
|
|
95
|
+
say([`METRIC check.docs=${c.docs}`, `METRIC check.values=${c.tokens}`, `METRIC check.errors=${c.errors.length}`, `METRIC check.warnings=${c.warnings.length}`]);
|
|
96
|
+
return c.errors.length || c.warnings.length ? 1 : 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function cmdCount(a) {
|
|
100
|
+
const c = countTrace(readFileSync(need(a, "doc"), "utf8"));
|
|
101
|
+
say(c.unknownRows.map((n) => `ERROR 상태를 알 수 없는 행: ${a.doc}:${n}`));
|
|
102
|
+
say(["rows", "done", "partial", "notdone", "unverified", "unknown"].map((k) => `METRIC trace.${k}=${c[k]}`));
|
|
103
|
+
return c.unknown ? 1 : 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function cmdCollect(a) {
|
|
107
|
+
const r = collectRoom(resolve(need(a, "room")), a.repo, { replace: !!a.replace });
|
|
108
|
+
say(r.problems.map((p) => `ERROR ${p}`));
|
|
109
|
+
if (r.previous && !r.problems.length) console.log(`replaced ${ORACLE} 의 이전 커밋: ${r.previous}`);
|
|
110
|
+
say(r.made.map((f) => `oracle ${ORACLE}/${f}`));
|
|
111
|
+
return r.problems.length ? 1 : 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function cmdWorktree(a) {
|
|
115
|
+
if (a.remove) { removeWorktree(resolve(a.remove)); console.log(`removed ${resolve(a.remove)}`); return 0; }
|
|
116
|
+
console.log(addWorktree(a.repo || ".", a.multi.link || []));
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function cmdBug(a) {
|
|
121
|
+
const r = await runBug({ wt: resolve(need(a, "worktree")), name: need(a, "name"), kind: a.kind, baseline: a.baseline && resolve(a.baseline), timeoutS: seconds(a.timeout, 300), argv: a.rest, shell: !!a.shell });
|
|
122
|
+
say([`bug: log=${r.log}${r.patch ? ` patch=${r.patch}` : ""}`, ...r.toRepo.map((p) => `WARN 링크한 폴더가 원래 트리를 가리킴: ${p}`), ...r.metrics.map((m) => `METRIC ${m}`)]);
|
|
123
|
+
return r.unchanged ? 0 : 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function cmdCommit(a) {
|
|
127
|
+
const r = commitEvidence(a.repo || ".", need(a, "message"), a.multi.paths || OWN, a.multi.accept || []);
|
|
128
|
+
if (r.refused) { say(r.refused.map((x) => `ERROR ${x}`)); return 1; }
|
|
129
|
+
say([`commit: ${r.sha}`, ...r.files.map((f) => `file ${f}`), ...r.outside.map((f) => `ERROR 증거 밖의 파일이 커밋됨: ${f}`)]);
|
|
130
|
+
return r.outside.length ? 1 : 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const COMMANDS = {
|
|
134
|
+
precheck: (a) => { const r = precheck(a.repo || "."); say(r.out); return r.decide ? 3 : 0; },
|
|
135
|
+
run: cmdRun,
|
|
136
|
+
serve: cmdServe,
|
|
137
|
+
stop: (a) => { const r = stop({ pidFile: resolve(need(a, "pid")) }); console.log(`stop: pid=${r.pid} log=${r.file}`); return 0; },
|
|
138
|
+
check: cmdCheck,
|
|
139
|
+
count: cmdCount,
|
|
140
|
+
room: (a) => { const r = makeRoom(a.repo || ".", a.multi.req); say([`room: ${r.dir}`, ...r.files.map((f) => `file ${f}`)]); return 0; },
|
|
141
|
+
collect: cmdCollect,
|
|
142
|
+
worktree: cmdWorktree,
|
|
143
|
+
bug: cmdBug,
|
|
144
|
+
commit: cmdCommit,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export async function main(argv) {
|
|
148
|
+
const [sub, ...rest] = argv;
|
|
149
|
+
if (!sub || sub === "--help" || sub === "-h") { console.log(USAGE); return sub ? 0 : 2; }
|
|
150
|
+
const fn = COMMANDS[sub];
|
|
151
|
+
if (!fn) { console.error(`make-evidence: 모르는 명령: ${sub}\n${USAGE}`); return 2; }
|
|
152
|
+
try {
|
|
153
|
+
return await fn(parseArgs(rest));
|
|
154
|
+
} catch (e) {
|
|
155
|
+
console.error(`make-evidence: ${e.message}`);
|
|
156
|
+
return 2;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
161
|
+
main(process.argv.slice(2)).then((code) => { process.exitCode = code; });
|
|
162
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// make-evidence: checks the numbers in the evidence documents against the run logs, and counts trace statuses.
|
|
2
|
+
// Pure Node, no network.
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { join, relative } from "node:path";
|
|
5
|
+
import { EVIDENCE, gitOut, lines, topOf } from "./me-git.mjs";
|
|
6
|
+
|
|
7
|
+
// A cited value: `<name>=<value>` in backticks, the name with at least one dot (scale.x5.median_s), so plain
|
|
8
|
+
// code such as `exit=0` is not taken for one.
|
|
9
|
+
const TOKEN = /`([A-Za-z][\w-]*(?:\.[\w-]+)+)=([^`\s]+)`/g;
|
|
10
|
+
const LOGREF = /(?<=^|[\s|(\[`'"])(?:docs\/evidence\/)?logs\/[^\s`|)\]>"',]+/g;
|
|
11
|
+
const CHECK_LINE = /^수치 대조: (?:docs\/evidence\/)?logs\/\S+$/;
|
|
12
|
+
const WRITER = /^\$ (?:echo|printf|cat|type|tee|Write-Output|Set-Content|Out-File|Add-Content)\b/i;
|
|
13
|
+
const SKIP_DOCS = new Set(["owner-facts.md"]);
|
|
14
|
+
|
|
15
|
+
const logPath = (top, ref) => join(top, ref.startsWith("logs/") ? join(EVIDENCE, ref) : ref);
|
|
16
|
+
|
|
17
|
+
// A .log the helper wrote: `$ <command>` then `# head=`. The METRIC lines in it, or why it is not one. Other files
|
|
18
|
+
// under logs/ (patches, saved prompts) are checked for being there, and hold no citable numbers.
|
|
19
|
+
function readLog(cache, file) {
|
|
20
|
+
if (!cache.has(file)) {
|
|
21
|
+
let entry;
|
|
22
|
+
if (!existsSync(file)) entry = { missing: true };
|
|
23
|
+
else if (!file.endsWith(".log")) entry = { metrics: new Set(), other: true }; // a patch or a saved prompt: it only has to exist
|
|
24
|
+
else {
|
|
25
|
+
const all = readFileSync(file, "utf8").split(/\r?\n/);
|
|
26
|
+
if (!all[0]?.startsWith("$ ") || !all[1]?.startsWith("# head=")) entry = { bad: "run 이 만든 로그가 아님(첫 두 줄)" };
|
|
27
|
+
else if (WRITER.test(all[0])) entry = { bad: `글을 찍기만 하는 명령의 로그: ${all[0].slice(0, 60)}` };
|
|
28
|
+
else if (all[0].includes("METRIC ")) entry = { bad: "명령 줄에 METRIC 글이 들어 있음(상수를 찍는 명령)" };
|
|
29
|
+
else entry = { metrics: new Set(all.filter((l) => l.startsWith("METRIC ")).map((l) => l.trim())) };
|
|
30
|
+
}
|
|
31
|
+
cache.set(file, entry);
|
|
32
|
+
}
|
|
33
|
+
return cache.get(file);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Numbers that are not measurements: inline code, paths and file:line (a name with an extension), hashes (with a
|
|
37
|
+
// letter), dates, versions, ids (R1, F2, M-3, c1), the multiples the skill uses (1배, 5배, 10배, x5), line counts
|
|
38
|
+
// (10줄), steps (3단계), section and list numbers, a row number cell. Quoted text only where requirements are
|
|
39
|
+
// quoted (quotes: true, trace.md). Everything else with a digit is reported.
|
|
40
|
+
export function bareNumbers(line, { quotes = false } = {}) {
|
|
41
|
+
if (/^\s*\|[\s:|-]+\|\s*$/.test(line)) return false;
|
|
42
|
+
let rest = line.replace(/^\s*#+\s*(?:\d+(?:\.\d+)*\.\s+)?/, " ").replace(/`[^`]*`/g, " ").replace(LOGREF, " ");
|
|
43
|
+
if (quotes) rest = rest.replace(/"[^"]*"|“[^”]*”|「[^」]*」/g, " ");
|
|
44
|
+
rest = rest
|
|
45
|
+
.replace(/\b[a-z][\w+.-]*:\/\/\S+/gi, " ").replace(/(?<!\S)(?:\.{0,2}\/|~\/)\S*/g, " ")
|
|
46
|
+
.replace(/(?<!\S)[\w.-]+(?:[/\\][\w.-]+)+\.[A-Za-z][\w]*(?::\d+(?:-\d+)?)?(?=$|[\s,);])/g, " ")
|
|
47
|
+
.replace(/(?<!\S)[\w.-]+\.(?:[cm]?[jt]sx?|py|java|kt|go|rs|rb|php|cs|sql|md|json|ya?ml|toml|sh|ps1|html|css|vue|svelte):\d+(?:-\d+)?/g, " ")
|
|
48
|
+
.replace(/\b(?=[0-9a-f]*[a-f])[0-9a-f]{7,40}\b/g, " ")
|
|
49
|
+
.replace(/\b\d{4}-\d{2}-\d{2}(?:[T ][\d:.]+Z?)?\b/g, " ").replace(/\bv\d+(?:\.\d+)*\b|\b\d+\.\d+\.\d+(?:\.\d+)?\b/g, " ")
|
|
50
|
+
.replace(/\b[A-Z]{1,3}-?\d+\b|\b(?!x\d)[a-z_]+\d+\b/g, " ").replace(/(?<![\d.])(?:1|5|10)배(?=\s*(?:데이터|입력|사례))|\bx(?:1|5|10)\b/g, " ")
|
|
51
|
+
.replace(/(?<!\d)10줄|\d\s*(?:단계|번째)/g, " ").replace(/^\s*\|\s*(?:\d{1,3}|(?:1|5|10)배)\s*\|/, "|").replace(/^\s*(?:\d+\.|[-*])\s+/, " ");
|
|
52
|
+
return /\d/.test(rest);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function checkRefs(ctx, at, refs) {
|
|
56
|
+
for (const ref of refs) {
|
|
57
|
+
const p = logPath(ctx.top, ref);
|
|
58
|
+
const log = readLog(ctx.cache, p);
|
|
59
|
+
if (log.missing) ctx.errors.push(`${at} 로그가 없음: ${ref}`);
|
|
60
|
+
else if (log.bad) ctx.errors.push(`${at} ${log.bad}: ${ref}`);
|
|
61
|
+
else if (ctx.tracked && !ctx.tracked.has(relative(ctx.top, p).split("\\").join("/"))) ctx.errors.push(`${at} 로그가 git 에 없음: ${ref}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function checkTokens(ctx, at, refs, line) {
|
|
66
|
+
for (const m of line.matchAll(TOKEN)) {
|
|
67
|
+
ctx.tokens++;
|
|
68
|
+
const want = `METRIC ${m[1]}=${m[2]}`;
|
|
69
|
+
if (!refs.length) ctx.errors.push(`${at} \`${m[1]}=${m[2]}\` 줄에 로그 경로가 없음`);
|
|
70
|
+
else if (!refs.some((r) => readLog(ctx.cache, logPath(ctx.top, r)).metrics?.has(want))) ctx.errors.push(`${at} 가리킨 로그에 \`${want}\` 줄이 없음`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function checkDoc(ctx, name) {
|
|
75
|
+
let fenced = false;
|
|
76
|
+
readFileSync(join(ctx.top, EVIDENCE, name), "utf8").split(/\r?\n/).forEach((line, i) => {
|
|
77
|
+
const at = `${EVIDENCE}/${name}:${i + 1}`;
|
|
78
|
+
if (/^\s*(```|~~~)/.test(line)) { fenced = !fenced; return; }
|
|
79
|
+
if (fenced) return;
|
|
80
|
+
const all = line.match(LOGREF) || [];
|
|
81
|
+
if (CHECK_LINE.test(line.trim())) {
|
|
82
|
+
if (all.some((r) => /\{ts\}|@TS@|[<*]/.test(r))) ctx.errors.push(`${at} 수치 대조 줄이 실제 로그가 아니라 이름 꼴을 가리킴`);
|
|
83
|
+
checkRefs(ctx, at, all);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const refs = all.filter((r) => !/\{ts\}|@TS@|[<*]/.test(r)); // a name pattern in a command, not a log
|
|
87
|
+
checkRefs(ctx, at, refs);
|
|
88
|
+
checkTokens(ctx, at, refs, line);
|
|
89
|
+
if (bareNumbers(line, { quotes: name === "trace.md" })) ctx.warnings.push(`${at} \`이름=값\` 밖의 숫자: ${line.trim().slice(0, 120)}`);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function checkDocs(repo, { tracked = false } = {}) {
|
|
94
|
+
const top = topOf(repo);
|
|
95
|
+
const dir = join(top, EVIDENCE);
|
|
96
|
+
const ctx = { top, cache: new Map(), errors: [], warnings: [], tokens: 0, docs: 0 };
|
|
97
|
+
if (tracked) ctx.tracked = new Set(lines(gitOut(top, ["ls-files", "--", EVIDENCE])));
|
|
98
|
+
const names = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith(".md") && !SKIP_DOCS.has(f)).sort() : [];
|
|
99
|
+
for (const name of names) { ctx.docs++; checkDoc(ctx, name); }
|
|
100
|
+
return ctx;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---- trace counts ---------------------------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
const STATUS = [["완료", "done"], ["부분", "partial"], ["미구현", "notdone"], ["미검증", "unverified"]];
|
|
106
|
+
const cellsOf = (line) => line.trim().replace(/^\||\|$/g, "").split(/(?<!\\)\|/).map((s) => s.trim());
|
|
107
|
+
|
|
108
|
+
// Counts the 상태 column, found by its heading. No heading with 상태 is an error; rows whose status is none of
|
|
109
|
+
// the four are listed.
|
|
110
|
+
export function countTrace(text) {
|
|
111
|
+
const c = { rows: 0, done: 0, partial: 0, notdone: 0, unverified: 0, unknown: 0, unknownRows: [] };
|
|
112
|
+
let col = -1;
|
|
113
|
+
text.split(/\r?\n/).forEach((line, i) => {
|
|
114
|
+
if (!line.trimStart().startsWith("|") || /^\s*\|[\s:|-]+\|\s*$/.test(line)) return;
|
|
115
|
+
const cells = cellsOf(line);
|
|
116
|
+
if (col < 0) { col = cells.indexOf("상태"); return; }
|
|
117
|
+
if (cells.includes("상태")) return; // a second table's heading
|
|
118
|
+
c.rows++;
|
|
119
|
+
const hit = STATUS.find(([k]) => (cells[col] || "").startsWith(k));
|
|
120
|
+
if (hit) c[hit[1]]++;
|
|
121
|
+
else { c.unknown++; c.unknownRows.push(i + 1); }
|
|
122
|
+
});
|
|
123
|
+
if (col < 0) throw new Error("trace 표에 `상태` 칸이 있는 머리 줄이 없음");
|
|
124
|
+
return c;
|
|
125
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// make-evidence: commits only the evidence paths. Ignored files are forced in only for the run logs and patches
|
|
2
|
+
// under docs/evidence/logs; .env files, big files and text that still looks like a secret stop the commit.
|
|
3
|
+
// Commit hooks run. Pure Node, no network.
|
|
4
|
+
import { readFileSync, statSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { EVIDENCE, OWN, git, gitOut, headOf, lines, topOf } from "./me-git.mjs";
|
|
7
|
+
import { secretLines } from "./me-run.mjs";
|
|
8
|
+
|
|
9
|
+
const MAX_FILE = 5 * 1024 * 1024;
|
|
10
|
+
const FORCE = new RegExp(`^${EVIDENCE}/logs/[^/]+\\.(log|patch|md)$`);
|
|
11
|
+
const NEVER = /(^|\/)(\.env[^/]*|node_modules|__pycache__|\.venv)(\/|$)/;
|
|
12
|
+
const under = (f, paths) => paths.some((p) => f === p || f.startsWith(`${p}/`));
|
|
13
|
+
|
|
14
|
+
function candidates(top, paths) {
|
|
15
|
+
const normal = lines(gitOut(top, ["ls-files", "--modified", "--others", "--deleted", "--exclude-standard", "--", ...paths]));
|
|
16
|
+
const ignored = lines(gitOut(top, ["ls-files", "--others", "--ignored", "--exclude-standard", "--", ...paths]));
|
|
17
|
+
return { normal: [...new Set(normal)], forced: ignored.filter((f) => FORCE.test(f)) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// accept: exact file:line places the user confirmed are not secrets.
|
|
21
|
+
function refusals(top, files, accept = []) {
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const f of files) {
|
|
24
|
+
if (NEVER.test(f)) { out.push(`넣지 않는 파일: ${f}`); continue; }
|
|
25
|
+
let st;
|
|
26
|
+
try { st = statSync(join(top, f)); } catch { continue; } // a deletion
|
|
27
|
+
if (st.size > MAX_FILE) { out.push(`5MB 넘는 파일: ${f}`); continue; }
|
|
28
|
+
const hits = secretLines(readFileSync(join(top, f), "utf8")).filter((n) => !accept.includes(`${f}:${n}`));
|
|
29
|
+
for (const n of hits.slice(0, 5)) out.push(`비밀값 꼴이 남음: ${f}:${n} (값을 가리거나, 비밀값이 아니면 --accept ${f}:${n})`);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function commitEvidence(repo, message, paths = OWN, accept = []) {
|
|
35
|
+
const top = topOf(repo);
|
|
36
|
+
const foreign = lines(gitOut(top, ["diff", "--cached", "--name-only"])).filter((f) => !under(f, paths));
|
|
37
|
+
if (foreign.length) return { refused: [`증거 밖의 스테이징된 변경이 있음: ${foreign.join(", ")}`] };
|
|
38
|
+
const { normal, forced } = candidates(top, paths);
|
|
39
|
+
if (!normal.length && !forced.length) return { refused: [`커밋할 것이 없음: ${paths.join(", ")}`] };
|
|
40
|
+
const refused = refusals(top, [...normal, ...forced], accept);
|
|
41
|
+
if (refused.length) return { refused };
|
|
42
|
+
if (normal.length) gitOut(top, ["add", "-A", "--", ...normal]);
|
|
43
|
+
if (forced.length) gitOut(top, ["add", "-f", "--", ...forced]);
|
|
44
|
+
const r = git(top, ["commit", "-m", message, "--", ...new Set([...normal, ...forced])]);
|
|
45
|
+
if (r.status !== 0) return { refused: [`git commit 실패(훅은 우회하지 않음): ${String(r.stdout + r.stderr).trim().split("\n").slice(-3).join(" / ")}`] };
|
|
46
|
+
const files = lines(gitOut(top, ["show", "--name-only", "--format=", "HEAD"]));
|
|
47
|
+
return { sha: headOf(top), files, outside: files.filter((f) => !under(f, paths)) };
|
|
48
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// make-evidence: git calls, the evidence paths and a fingerprint of the user's work tree. Pure Node, no network.
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
5
|
+
import { homedir, tmpdir } from "node:os";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
|
+
|
|
8
|
+
export const EVIDENCE = "docs/evidence";
|
|
9
|
+
export const ORACLE = "tests/oracle";
|
|
10
|
+
export const OWN = [EVIDENCE, ORACLE];
|
|
11
|
+
export const EXCLUDE = OWN.map((p) => `:(exclude)${p}`);
|
|
12
|
+
export const KINDS = ["constraint", "objective", "permission"];
|
|
13
|
+
export const MAX_TEXT = 4 * 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
export class Usage extends Error {}
|
|
16
|
+
|
|
17
|
+
// Programs start from the home folder (Windows looks for a program in the working folder first); git gets the
|
|
18
|
+
// folder through -C.
|
|
19
|
+
export const runDir = () => (existsSync(homedir()) ? homedir() : tmpdir());
|
|
20
|
+
|
|
21
|
+
export function git(cwd, args, extra = {}) {
|
|
22
|
+
const r = spawnSync("git", ["-C", resolve(cwd), ...args], { cwd: runDir(), encoding: "utf8", maxBuffer: 256 * 1024 * 1024, windowsHide: true, ...extra });
|
|
23
|
+
if (r.error) throw new Error(`git 을 실행하지 못함: ${r.error.message}`);
|
|
24
|
+
return r;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function gitOut(cwd, args) {
|
|
28
|
+
const r = git(cwd, args);
|
|
29
|
+
if (r.status !== 0) throw new Error(`git ${args.filter((a) => !a.startsWith(":(")).join(" ")} 실패: ${String(r.stderr).trim().split("\n").pop()}`);
|
|
30
|
+
return r.stdout;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const lines = (s) => String(s).split(/\r?\n/).filter(Boolean);
|
|
34
|
+
export const topOf = (dir) => gitOut(dir, ["rev-parse", "--show-toplevel"]).trim();
|
|
35
|
+
export const headOf = (dir) => gitOut(dir, ["rev-parse", "HEAD"]).trim();
|
|
36
|
+
|
|
37
|
+
// What git sees changed outside the evidence paths (tracked and untracked).
|
|
38
|
+
export const otherStatus = (repo) => gitOut(repo, ["--no-optional-locks", "status", "--porcelain", "--untracked-files=all", "--", ".", ...EXCLUDE]);
|
|
39
|
+
|
|
40
|
+
export const sha256 = (data) => createHash("sha256").update(data).digest("hex");
|
|
41
|
+
|
|
42
|
+
function fileHash(file) {
|
|
43
|
+
try {
|
|
44
|
+
const st = lstatSync(file);
|
|
45
|
+
return st.isFile() ? sha256(readFileSync(file)) : `${st.isSymbolicLink() ? "link" : "other"}`;
|
|
46
|
+
} catch {
|
|
47
|
+
return "gone";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The user's work tree outside the evidence paths: the status lines, the content of tracked changes and of every
|
|
52
|
+
// untracked file. Two equal prints mean nothing there changed (ignored files are not covered).
|
|
53
|
+
export function treePrint(top) {
|
|
54
|
+
const status = otherStatus(top);
|
|
55
|
+
const diff = gitOut(top, ["--no-optional-locks", "diff", "HEAD", "--binary", "--", ".", ...EXCLUDE]);
|
|
56
|
+
const untracked = lines(gitOut(top, ["ls-files", "--others", "--exclude-standard", "--", ".", ...EXCLUDE]));
|
|
57
|
+
return sha256(`${status}\0${diff}\0${untracked.map((f) => `${f}:${fileHash(join(top, f))}`).join("\n")}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function stamp(d = new Date()) {
|
|
61
|
+
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
62
|
+
return `${d.getUTCFullYear()}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}-${p(d.getUTCHours())}${p(d.getUTCMinutes())}${p(d.getUTCSeconds())}-${p(d.getUTCMilliseconds(), 3)}Z`;
|
|
63
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// make-evidence: the oracle writer's room, and the seeded bugs run in a separate worktree of HEAD. The user's work
|
|
2
|
+
// tree is never written. Pure Node, no network.
|
|
3
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
+
import { EVIDENCE, EXCLUDE, KINDS, ORACLE, Usage, git, gitOut, headOf, lines, sha256, topOf, treePrint } from "./me-git.mjs";
|
|
7
|
+
import { claimLog, runLogged } from "./me-run.mjs";
|
|
8
|
+
|
|
9
|
+
// Inside means below, not equal; a path on another Windows drive is outside (relative() returns it absolute).
|
|
10
|
+
export const inside = (top, p) => { const r = relative(top, p); return r !== "" && !r.startsWith("..") && !isAbsolute(r); };
|
|
11
|
+
const slash = (p) => p.split("\\").join("/");
|
|
12
|
+
|
|
13
|
+
function walk(dir, base = dir) {
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
16
|
+
const p = join(dir, e.name);
|
|
17
|
+
if (e.isDirectory()) out.push(...walk(p, base));
|
|
18
|
+
else out.push(slash(relative(base, p)));
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---- room -----------------------------------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
// The room is <base>/room; its note (the repository path and the input list) is <base>/room.json, outside the
|
|
26
|
+
// room, so the oracle writer does not learn where the repository is.
|
|
27
|
+
const noteOf = (roomDir) => join(dirname(roomDir), "room.json");
|
|
28
|
+
|
|
29
|
+
export function makeRoom(repo, req = ["README.md"]) {
|
|
30
|
+
const top = topOf(repo);
|
|
31
|
+
const files = [...req, `${EVIDENCE}/owner-facts.md`];
|
|
32
|
+
const dir = join(mkdtempSync(join(tmpdir(), "make-evidence-room-")), "room");
|
|
33
|
+
if (inside(top, dir)) throw new Error(`임시 폴더가 저장소 안에 있음: ${dir}`);
|
|
34
|
+
mkdirSync(dir);
|
|
35
|
+
for (const f of files) {
|
|
36
|
+
const src = join(top, f);
|
|
37
|
+
if (!existsSync(src) || !lstatSync(src).isFile()) throw new Error(`방에 넣을 파일이 없음: ${f}`);
|
|
38
|
+
mkdirSync(dirname(join(dir, f)), { recursive: true });
|
|
39
|
+
cpSync(src, join(dir, f));
|
|
40
|
+
}
|
|
41
|
+
mkdirSync(join(dir, ORACLE), { recursive: true });
|
|
42
|
+
writeFileSync(noteOf(dir), JSON.stringify({ repo: top, files }, null, 2));
|
|
43
|
+
return { dir, files };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// What the room holds beside the oracle: only the input files, unchanged.
|
|
47
|
+
function roomProblems(roomDir, top, files) {
|
|
48
|
+
const problems = [];
|
|
49
|
+
for (const f of walk(roomDir)) {
|
|
50
|
+
if (f.startsWith(`${ORACLE}/`)) continue;
|
|
51
|
+
if (!files.includes(f)) problems.push(`방에 허용하지 않은 파일: ${f}`);
|
|
52
|
+
else if (!readFileSync(join(roomDir, f)).equals(readFileSync(join(top, f)))) problems.push(`입력 파일이 바뀜: ${f}`);
|
|
53
|
+
}
|
|
54
|
+
return problems;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const isFilled = (dir) => existsSync(dir) && readdirSync(dir).length > 0;
|
|
58
|
+
|
|
59
|
+
export function collectRoom(roomDir, repo, { replace = false } = {}) {
|
|
60
|
+
const note = JSON.parse(readFileSync(noteOf(roomDir), "utf8"));
|
|
61
|
+
const top = repo ? topOf(repo) : note.repo;
|
|
62
|
+
const problems = roomProblems(roomDir, top, note.files);
|
|
63
|
+
const made = existsSync(join(roomDir, ORACLE)) ? walk(join(roomDir, ORACLE)) : [];
|
|
64
|
+
if (!made.length) problems.push(`${ORACLE}/ 에 파일이 없음`);
|
|
65
|
+
const dest = join(top, ORACLE);
|
|
66
|
+
if (!replace && isFilled(dest)) problems.push(`${ORACLE}/ 가 이미 있음(판정기 빈틈을 고친 새 판정기일 때만 --replace)`);
|
|
67
|
+
const previous = lines(gitOut(top, ["log", "-1", "--format=%h %ad %s", "--date=iso", "--", ORACLE]))[0] || null;
|
|
68
|
+
if (problems.length) return { problems, made, previous };
|
|
69
|
+
rmSync(dest, { recursive: true, force: true });
|
|
70
|
+
cpSync(join(roomDir, ORACLE), dest, { recursive: true });
|
|
71
|
+
return { problems, made, previous };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---- worktree and links ---------------------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
const META = "meta.json";
|
|
77
|
+
const BUILD_DIR = /^(dist|build|out|target|\.next|\.nuxt|\.output|bin|obj|coverage|lib)$/i;
|
|
78
|
+
const metaOf = (wt) => JSON.parse(readFileSync(join(dirname(wt), META), "utf8"));
|
|
79
|
+
|
|
80
|
+
function checkLink(top, p) {
|
|
81
|
+
const src = resolve(top, p);
|
|
82
|
+
const rel = relative(top, src);
|
|
83
|
+
if (!inside(top, src)) throw new Usage(`--link 는 저장소 안의 경로만: ${p}`);
|
|
84
|
+
if (!existsSync(src)) throw new Usage(`--link 경로가 없음: ${p}`);
|
|
85
|
+
if (BUILD_DIR.test(basename(src))) throw new Usage(`--link 로 빌드 결과를 잇지 않음(버그를 넣은 빌드가 사용자 폴더에 써짐): ${p}. 작업 트리 안에서 새로 빌드함`);
|
|
86
|
+
if (/^\.env/i.test(basename(src))) throw new Usage(`--link 로 .env 를 잇지 않음: ${p}`);
|
|
87
|
+
if (lines(gitOut(top, ["ls-files", "--", rel])).length) throw new Usage(`--link 는 추적되지 않는 폴더만(추적되는 파일은 작업 트리에 이미 있음): ${p}`);
|
|
88
|
+
return rel;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// A linked folder's print (files, bytes, latest change) and the links in it that lead back into the main tree:
|
|
92
|
+
// symbolic links resolving there (workspace packages), .pth and egg-link files naming it (editable installs).
|
|
93
|
+
export function scanLinked(top, root) {
|
|
94
|
+
const s = { files: 0, bytes: 0, mtime: 0, toRepo: [] };
|
|
95
|
+
const real = realpathSync(top);
|
|
96
|
+
const visit = (dir) => {
|
|
97
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
98
|
+
const p = join(dir, e.name);
|
|
99
|
+
const st = lstatSync(p);
|
|
100
|
+
s.files++; s.bytes += st.size; s.mtime = Math.max(s.mtime, st.mtimeMs);
|
|
101
|
+
if (e.isSymbolicLink()) {
|
|
102
|
+
let to; try { to = realpathSync(p); } catch { to = resolve(dir, readlinkSync(p)); }
|
|
103
|
+
if (inside(real, to) && !inside(realpathSync(root), to)) s.toRepo.push(slash(relative(top, p)));
|
|
104
|
+
} else if (e.isDirectory()) visit(p);
|
|
105
|
+
else if (/\.(pth|egg-link)$|^__editable__/.test(e.name) && st.size < 65536 && readFileSync(p, "utf8").includes(real)) s.toRepo.push(slash(relative(top, p)));
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
visit(root);
|
|
109
|
+
return s;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function addWorktree(repo, links = []) {
|
|
113
|
+
const top = topOf(repo);
|
|
114
|
+
const rels = links.map((p) => checkLink(top, p));
|
|
115
|
+
const base = mkdtempSync(join(tmpdir(), "make-evidence-wt-"));
|
|
116
|
+
const wt = join(base, "wt");
|
|
117
|
+
gitOut(top, ["worktree", "add", "--detach", wt, "HEAD"]);
|
|
118
|
+
writeFileSync(join(base, META), JSON.stringify({ repo: top, links: rels }));
|
|
119
|
+
// The oracle and the evidence scripts as they are now, committed or not, so the run uses them.
|
|
120
|
+
for (const p of [ORACLE, `${EVIDENCE}/scripts`]) {
|
|
121
|
+
if (!existsSync(join(top, p))) continue;
|
|
122
|
+
rmSync(join(wt, p), { recursive: true, force: true });
|
|
123
|
+
cpSync(join(top, p), join(wt, p), { recursive: true });
|
|
124
|
+
}
|
|
125
|
+
for (const rel of rels) {
|
|
126
|
+
mkdirSync(dirname(join(wt, rel)), { recursive: true });
|
|
127
|
+
symlinkSync(join(top, rel), join(wt, rel), "junction");
|
|
128
|
+
}
|
|
129
|
+
return wt;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function removeWorktree(wt) {
|
|
133
|
+
const { repo, links } = metaOf(wt);
|
|
134
|
+
for (const rel of links) {
|
|
135
|
+
const at = join(wt, rel);
|
|
136
|
+
try { if (lstatSync(at).isSymbolicLink()) rmSync(at); } catch { /* not there */ } // the link, never its target
|
|
137
|
+
}
|
|
138
|
+
git(repo, ["worktree", "remove", "--force", wt]);
|
|
139
|
+
git(repo, ["worktree", "prune"]);
|
|
140
|
+
rmSync(dirname(wt), { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---- oracle lines and the verdict -----------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
const ORACLE_LINE = /^ORACLE (constraint|objective|permission) (PASS|FAIL|ERROR)\s+(\S+)/;
|
|
146
|
+
|
|
147
|
+
export function oracleResults(text) {
|
|
148
|
+
const r = Object.fromEntries(KINDS.map((k) => [k, { pass: new Set(), fail: new Set(), error: new Set() }]));
|
|
149
|
+
for (const line of text.split(/\r?\n/)) {
|
|
150
|
+
const m = line.trim().match(ORACLE_LINE);
|
|
151
|
+
if (m) r[m[1]][m[2].toLowerCase()].add(m[3]);
|
|
152
|
+
}
|
|
153
|
+
return r;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const casesOf = (r) => new Set(KINDS.flatMap((k) => [...r[k].pass, ...r[k].fail, ...r[k].error].map((c) => `${k}:${c}`)));
|
|
157
|
+
const sameSet = (a, b) => a.size === b.size && [...a].every((x) => b.has(x));
|
|
158
|
+
|
|
159
|
+
// caught: a FAIL of the bug's kind the baseline did not have, in a run that judged the same cases as the baseline
|
|
160
|
+
// and gave no ERROR. Anything that does not show the oracle judging the implementation is undecidable.
|
|
161
|
+
export function judge(text, kind, baseText, timedOut) {
|
|
162
|
+
const cur = oracleResults(text), base = oracleResults(baseText);
|
|
163
|
+
const collateral = KINDS.filter((k) => k !== kind).reduce((n, k) => n + [...cur[k].fail].filter((c) => !base[k].fail.has(c)).length, 0);
|
|
164
|
+
const undecided = (reason) => ({ result: "undecidable", reason, collateral });
|
|
165
|
+
if (timedOut) return undecided("timeout");
|
|
166
|
+
if (KINDS.some((k) => cur[k].error.size)) return undecided("oracle-error");
|
|
167
|
+
if (!base[kind].pass.size) return undecided("baseline-no-pass");
|
|
168
|
+
if (!sameSet(casesOf(cur), casesOf(base))) return undecided("cases-differ");
|
|
169
|
+
const caught = [...cur[kind].fail].some((c) => !base[kind].fail.has(c));
|
|
170
|
+
return { result: caught ? "caught" : "missed", reason: caught ? "new-fail" : "no-new-fail", collateral };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function oracleSha(wt) {
|
|
174
|
+
const dir = join(wt, ORACLE);
|
|
175
|
+
return existsSync(dir) ? sha256(walk(dir).sort().map((f) => `${f}:${sha256(readFileSync(join(dir, f)))}`).join("\n")).slice(0, 16) : "none";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---- the bug run ----------------------------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
const metricOf = (text, name) => (text.match(new RegExp(`^METRIC ${name.replace(/\./g, "\\.")}=(\\S+)$`, "m")) || [])[1];
|
|
181
|
+
|
|
182
|
+
function checkBugArgs(name, kind, baseline) {
|
|
183
|
+
if (!/^[\w-]+$/.test(name || "")) throw new Usage("--name 은 영문, 숫자, - 와 _ 만");
|
|
184
|
+
if (name === "baseline") return;
|
|
185
|
+
if (!KINDS.includes(kind)) throw new Usage(`--kind 는 ${KINDS.join(", ")} 가운데 하나`);
|
|
186
|
+
if (!baseline || !existsSync(baseline)) throw new Usage("--baseline <기준선 로그> 가 필요함(먼저 --name baseline 으로 돌림)");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// The baseline must be of this HEAD and this oracle.
|
|
190
|
+
function checkBaseline(wt, baseText) {
|
|
191
|
+
const head = (baseText.match(/^# head=(\S+)/m) || [])[1];
|
|
192
|
+
if (head !== headOf(wt)) throw new Usage(`기준선의 HEAD(${head})가 지금 HEAD 와 다름. 기준선을 다시 돌림`);
|
|
193
|
+
const sha = metricOf(baseText, "bug.baseline.oracle_sha");
|
|
194
|
+
if (sha !== oracleSha(wt)) throw new Usage("기준선의 판정기가 지금 판정기와 다름. 기준선을 다시 돌림");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function verdictMetrics(name, kind, run, text, baseText, linked) {
|
|
198
|
+
const m = [`bug.${name}.exit=${run.exit}`];
|
|
199
|
+
if (name === "baseline") {
|
|
200
|
+
const r = oracleResults(text);
|
|
201
|
+
for (const k of KINDS) m.push(`baseline.${k}.pass=${r[k].pass.size}`, `baseline.${k}.fail=${r[k].fail.size}`, `baseline.${k}.error=${r[k].error.size}`);
|
|
202
|
+
return m;
|
|
203
|
+
}
|
|
204
|
+
let v = judge(text, kind, baseText, run.code === 124);
|
|
205
|
+
if (linked.toRepo.length) v = { ...v, result: "undecidable", reason: "link-to-main" };
|
|
206
|
+
m.push(`bug.${name}.kind=${kind}`, `bug.${name}.result=${v.result}`, `bug.${name}.reason=${v.reason}`, `bug.${name}.collateral=${v.collateral}`);
|
|
207
|
+
return m;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function linkedPrints(top, links) {
|
|
211
|
+
const all = links.map((rel) => scanLinked(top, join(top, rel)));
|
|
212
|
+
return { key: JSON.stringify(all.map((s) => [s.files, s.bytes, s.mtime])), toRepo: all.flatMap((s) => s.toRepo) };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function bugBody({ wt, name, kind, baseText, timeoutS, argv, shell }) {
|
|
216
|
+
const { repo: top, links } = metaOf(wt);
|
|
217
|
+
const logs = join(top, EVIDENCE, "logs");
|
|
218
|
+
const diff = gitOut(wt, ["diff", "--", ".", ...EXCLUDE]);
|
|
219
|
+
if (name === "baseline" ? diff.trim() : !diff.trim()) throw new Error(name === "baseline" ? "기준선인데 작업 트리에 바뀐 것이 있음" : "작업 트리에 넣은 버그가 없음(추적되는 파일을 고침)");
|
|
220
|
+
if (name !== "baseline") checkBaseline(wt, baseText);
|
|
221
|
+
const before = { tree: treePrint(top), linked: linkedPrints(top, links) };
|
|
222
|
+
const patch = name === "baseline" ? null : claimLog(join(logs, `verify-bug-${name}-{ts}.patch`));
|
|
223
|
+
if (patch) writeFileSync(patch, diff);
|
|
224
|
+
const run = await runLogged({ log: join(logs, `verify-bug-${name}-{ts}.log`), argv, cwd: wt, timeoutS, shell });
|
|
225
|
+
const metrics = verdictMetrics(name, kind, run, readFileSync(run.file, "utf8"), baseText, before.linked);
|
|
226
|
+
metrics.push(`bug.${name}.oracle_sha=${oracleSha(wt)}`, `bug.${name}.links_to_repo=${before.linked.toRepo.length}`);
|
|
227
|
+
return { top, links, run, patch, metrics, before };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function runBug({ wt, name, kind, baseline, timeoutS = 300, argv, shell = false }) {
|
|
231
|
+
checkBugArgs(name, kind, baseline);
|
|
232
|
+
if (!argv.length) throw new Usage("판정기 명령이 없음(-- 뒤에 씀)");
|
|
233
|
+
let r;
|
|
234
|
+
try {
|
|
235
|
+
r = await bugBody({ wt, name, kind, baseText: baseline ? readFileSync(baseline, "utf8") : "", timeoutS, argv, shell });
|
|
236
|
+
} finally {
|
|
237
|
+
removeWorktree(wt);
|
|
238
|
+
}
|
|
239
|
+
const treeSame = treePrint(r.top) === r.before.tree;
|
|
240
|
+
const linkedSame = linkedPrints(r.top, r.links).key === r.before.linked.key;
|
|
241
|
+
r.metrics.push(`bug.${name}.tree_unchanged=${treeSame ? "yes" : "no"}`, `bug.${name}.links_unchanged=${linkedSame ? "yes" : "no"}`);
|
|
242
|
+
writeFileSync(r.run.file, `${readFileSync(r.run.file, "utf8")}${r.metrics.map((m) => `METRIC ${m}`).join("\n")}\n`);
|
|
243
|
+
return { log: r.run.file, patch: r.patch, metrics: r.metrics, unchanged: treeSame && linkedSame, toRepo: r.before.linked.toRepo };
|
|
244
|
+
}
|