@kaydash9999/banker-plugins 0.23.3 → 0.24.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 +56 -0
- package/README.md +14 -6
- package/codex/manifest.json +6 -2
- package/codex/skills/teamwork/scripts/teamwork.mjs +3 -3
- package/codex/transform-matrix.md +6 -2
- package/package.json +1 -1
- package/skills/build-testbox/SKILL.md +213 -0
- package/skills/build-testbox/scripts/args.mjs +156 -0
- package/skills/build-testbox/scripts/browser-cell.mjs +181 -0
- package/skills/build-testbox/scripts/browsers.mjs +164 -0
- package/skills/build-testbox/scripts/containers.mjs +237 -0
- package/skills/build-testbox/scripts/detect.mjs +166 -0
- package/skills/build-testbox/scripts/proc.mjs +91 -0
- package/skills/build-testbox/scripts/report.mjs +109 -0
- package/skills/build-testbox/scripts/setup.mjs +144 -0
- package/skills/build-testbox/scripts/testbox.mjs +148 -0
- package/skills/git-init/SKILL.md +152 -0
- package/skills/git-init/scripts/git-init.mjs +440 -0
- package/skills/git-init/scripts/marker-block.mjs +321 -0
- package/skills/git-init/scripts/payload-scan.mjs +186 -0
- package/skills/init-dir/SKILL.md +172 -0
- package/skills/init-dir/scripts/init-dir.mjs +308 -0
- package/skills/init-dir/scripts/marker-block.mjs +321 -0
- package/skills/init-dir/scripts/templates.mjs +100 -0
- package/skills/init-dir/templates/KB/README.md +15 -0
- package/skills/init-dir/templates/docs/README.md +14 -0
- package/skills/init-dir/templates/logs/README.md +14 -0
- package/skills/init-dir/templates/logs/gitignore +3 -0
- package/skills/init-dir/templates/mockup/README.md +7 -0
- package/skills/init-dir/templates/scripts/README.md +39 -0
- package/skills/init-dir/templates/scripts/lib/service.mjs +596 -0
- package/skills/init-dir/templates/scripts/restart.cmd +13 -0
- package/skills/init-dir/templates/scripts/restart.sh +10 -0
- package/skills/init-dir/templates/scripts/service.conf +5 -0
- package/skills/init-dir/templates/scripts/start.cmd +13 -0
- package/skills/init-dir/templates/scripts/start.sh +10 -0
- package/skills/init-dir/templates/scripts/status.cmd +13 -0
- package/skills/init-dir/templates/scripts/status.sh +10 -0
- package/skills/init-dir/templates/scripts/stop.cmd +13 -0
- package/skills/init-dir/templates/scripts/stop.sh +10 -0
- package/skills/init-dir/templates/tests/README.md +15 -0
- package/skills/init-dir/templates/tests/run.cmd +13 -0
- package/skills/init-dir/templates/tests/run.mjs +160 -0
- package/skills/init-dir/templates/tests/run.sh +10 -0
- package/skills/init-dir/templates/tests/tests.conf +7 -0
- package/skills/init-dir/templates/work/README.md +8 -0
- package/skills/progress-mon/README.md +11 -8
- package/skills/progress-mon/SKILL.md +53 -16
- package/skills/progress-mon/assets/fixture/README.md +1 -1
- package/skills/progress-mon/assets/fixture/adr.json +35 -0
- package/skills/progress-mon/assets/fixture/arch.json +1 -1
- package/skills/progress-mon/assets/fixture/flow.json +2 -2
- package/skills/progress-mon/assets/fixture/variants/arch-auto/arch.json +1 -1
- package/skills/progress-mon/assets/fixture/variants/legacy-init/rules.json +6 -0
- package/skills/progress-mon/assets/fixture/variants/legacy-init/state.json +5 -0
- package/skills/progress-mon/references/adr.md +184 -0
- package/skills/progress-mon/references/authoring.md +13 -5
- package/skills/progress-mon/scripts/pm-adr-view.mjs +101 -0
- package/skills/progress-mon/scripts/pm-adr.mjs +515 -0
- package/skills/progress-mon/scripts/pm-check.mjs +59 -22
- package/skills/progress-mon/scripts/pm-data.mjs +1 -1
- package/skills/progress-mon/scripts/pm-html.mjs +1 -0
- package/skills/progress-mon/scripts/pm-memory.mjs +113 -28
- package/skills/progress-mon/scripts/pm-render.mjs +6 -4
- package/skills/progress-mon/scripts/pm-server.mjs +2 -1
- package/skills/progress-mon/scripts/pm-sources.mjs +10 -0
- package/skills/progress-mon/scripts/progress-mon.mjs +3 -2
- package/skills/tune-tmux/SKILL.md +235 -0
- package/skills/tune-tmux/scripts/app-notes.mjs +38 -0
- package/skills/tune-tmux/scripts/marker-block.mjs +321 -0
- package/skills/tune-tmux/scripts/psmux-caps.json +71 -0
- package/skills/tune-tmux/scripts/tmux-config.mjs +251 -0
- package/skills/tune-tmux/scripts/tune-tmux.mjs +284 -0
- package/skills/ultra-chain/SKILL.md +52 -22
- package/skills/ultra-chain/references/chain-ledger.mjs +93 -11
- package/skills/ultra-chain/references/chain-report.mjs +13 -2
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// run.json (when, where and from which commit the run started) and events.jsonl (one JSON line per stage
|
|
4
4
|
// event). The time of every event is taken here, never written by the model. Pure Node, no dependencies.
|
|
5
5
|
//
|
|
6
|
-
// node chain-ledger.mjs init --base <folder> [--runtime claude|codex] [--exclude]
|
|
6
|
+
// node chain-ledger.mjs init --base <folder> [--runtime claude|codex] [--agents N] [--exclude]
|
|
7
7
|
// node chain-ledger.mjs event <run> --stage <id> --status <status> [--file <event.json>] [--round <n>]
|
|
8
8
|
// [--summary <text>] [--reason <text>]
|
|
9
9
|
// node chain-ledger.mjs boxes <run> --file <boxes.mjs report> [--stage <id>] [--round <n>]
|
|
@@ -61,7 +61,13 @@ const bool = () => (v) => (typeof v === "boolean" ? v : undefined);
|
|
|
61
61
|
const path = () => (v) => runPath(v) ?? undefined;
|
|
62
62
|
const COUNT = int(0, 10_000_000);
|
|
63
63
|
|
|
64
|
-
//
|
|
64
|
+
// The judges of one visual view (stage 7): every judge's score and its verdict after the score rule.
|
|
65
|
+
const JUDGES = { max: 20, fields: {
|
|
66
|
+
id: [text(60), true, "60자 이하 글"], score: [num(0, 100), false, "0~100 수"],
|
|
67
|
+
verdict: [oneOf("pass", "revise", "fail", "unverified"), true, "pass, revise, fail, unverified"] } };
|
|
68
|
+
|
|
69
|
+
// Each field: [check, required, what the check wants]. A check returns the cleaned value or undefined. `lists` are
|
|
70
|
+
// arrays of records inside one record.
|
|
65
71
|
const RECORDS = {
|
|
66
72
|
artifacts: { max: 50, fields: { label: [text(200), false, "200자 이하 글"], path: [path(), true, "실행 폴더 기준 상대 경로"] } },
|
|
67
73
|
tests: { max: 60, fields: {
|
|
@@ -73,7 +79,7 @@ const RECORDS = {
|
|
|
73
79
|
name: [text(100), true, "100자 이하 글"], mock: [path(), false, "실행 폴더 기준 상대 경로"],
|
|
74
80
|
actual: [path(), false, "실행 폴더 기준 상대 경로"], score: [num(0, 100), false, "0~100 수"],
|
|
75
81
|
verdict: [oneOf("pass", "revise", "fail", "unverified"), false, "pass, revise, fail, unverified"], intended: [bool(), false, "true, false"],
|
|
76
|
-
note: [text(500), false, "500자 이하 글"] } },
|
|
82
|
+
note: [text(500), false, "500자 이하 글"] }, lists: { judges: JUDGES } },
|
|
77
83
|
checklist: { max: 60, fields: {
|
|
78
84
|
item: [text(300), true, "300자 이하 글"], why: [text(500), false, "500자 이하 글"], ref: [path(), false, "실행 폴더 기준 상대 경로"] } },
|
|
79
85
|
seats: { max: 20, fields: {
|
|
@@ -109,9 +115,16 @@ function checkFields(obj, fields, where, extra = []) {
|
|
|
109
115
|
return out;
|
|
110
116
|
}
|
|
111
117
|
|
|
118
|
+
function checkItem(item, spec, where) {
|
|
119
|
+
const lists = spec.lists ?? {};
|
|
120
|
+
const out = checkFields(item, spec.fields, where, Object.keys(lists));
|
|
121
|
+
for (const [key, sub] of Object.entries(lists)) if (item[key] !== undefined) out[key] = checkList(item[key], sub, `${where}.${key}`);
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
112
125
|
function checkList(list, spec, where) {
|
|
113
126
|
if (!Array.isArray(list) || list.length > spec.max) throw new UsageError(`${where}: ${spec.max}개 이하 배열이어야 합니다`);
|
|
114
|
-
return list.map((item, i) =>
|
|
127
|
+
return list.map((item, i) => checkItem(item, spec, `${where}[${i}]`));
|
|
115
128
|
}
|
|
116
129
|
|
|
117
130
|
const BLOCKERS = { max: 50, fields: { item: [text(1000), true, "1000자 이하 글"] } };
|
|
@@ -137,6 +150,73 @@ export function checkEvent(raw) {
|
|
|
137
150
|
return event;
|
|
138
151
|
}
|
|
139
152
|
|
|
153
|
+
// ── reviewer count (--agents) ────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
// One count for every independent verdict of the chain: the ralph-qa seats of stages 3 and 6, the critic seats of
|
|
156
|
+
// stage 5 and the judges of stage 7. Under 3 is raised to 3 as ralph-qa does; over 17 is refused (the seat record
|
|
157
|
+
// holds 20, less the three outside seats).
|
|
158
|
+
const AGENTS_DEFAULT = 3;
|
|
159
|
+
const AGENTS_MAX = 17;
|
|
160
|
+
|
|
161
|
+
function agentsOf(value) {
|
|
162
|
+
if (value === undefined || value === null) return { agents: AGENTS_DEFAULT, agentsSource: "default" };
|
|
163
|
+
const n = typeof value === "number" ? value : /^\d+$/.test(String(value)) ? Number(value) : Number.NaN;
|
|
164
|
+
if (!Number.isInteger(n) || n < 0 || n > AGENTS_MAX) throw new UsageError(`--agents 는 0~${AGENTS_MAX} 정수입니다(좌석 기록 20 에서 외부 좌석 3 을 뺀 상한)`);
|
|
165
|
+
if (n < AGENTS_DEFAULT) return { agents: AGENTS_DEFAULT, agentsSource: "raised", agentsAsked: n };
|
|
166
|
+
return { agents: n, agentsSource: "flag" };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const seatIds = (prefix, n) => Array.from({ length: n }, (_, i) => `${prefix}#${i + 1}`);
|
|
170
|
+
const sameIds = (ids, prefix, n) => ids.length === n && [...ids].sort().join("\n") === seatIds(prefix, n).sort().join("\n");
|
|
171
|
+
|
|
172
|
+
// Stage 5's critic seat loop: an APPROVE or ITERATE round has exactly <agents> seats critic#1..critic#N, and APPROVE
|
|
173
|
+
// only when every seat approves. INCONCLUSIVE (a lost seat) and ralph's own verdict (by "ralph-reviewer") are not counted.
|
|
174
|
+
// A critic record takes only the results the loop can end in, so PASS or REJECT cannot slip past the seat count.
|
|
175
|
+
const CRITIC_RESULTS = ["APPROVE", "ITERATE", "INCONCLUSIVE", "STOPPED"];
|
|
176
|
+
function checkCriticSeats(event, n) {
|
|
177
|
+
const v = event.verdict;
|
|
178
|
+
if (event.stage !== "build" || !v || v.by !== "critic") return;
|
|
179
|
+
if (!CRITIC_RESULTS.includes(v.result)) throw new UsageError(`verdict.result: build 단계 critic 기록은 ${CRITIC_RESULTS.join(", ")} 만 됩니다(받은 값 ${v.result})`);
|
|
180
|
+
if (!["APPROVE", "ITERATE"].includes(v.result)) return;
|
|
181
|
+
const seats = v.seats ?? [];
|
|
182
|
+
if (!sameIds(seats.map((s) => s.id), "critic", n)) {
|
|
183
|
+
throw new UsageError(`verdict.seats: critic ${v.result} 기록은 좌석 ${n}개(${seatIds("critic", n).join(", ")})여야 합니다`);
|
|
184
|
+
}
|
|
185
|
+
if (v.result === "APPROVE" && seats.some((s) => s.verdict !== "APPROVE")) {
|
|
186
|
+
throw new UsageError("verdict.result: 모든 critic 좌석이 APPROVE 일 때만 APPROVE 입니다");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Stage 5 verdicts come from the critic seat loop or ralph's own reviewer only; any other `by` would slip past the
|
|
191
|
+
// seat count above.
|
|
192
|
+
const BUILD_JUDGES = ["critic", "ralph-reviewer"];
|
|
193
|
+
function checkBuildBy(event) {
|
|
194
|
+
if (event.stage !== "build" || !event.verdict || BUILD_JUDGES.includes(event.verdict.by)) return;
|
|
195
|
+
throw new UsageError(`verdict.by: build 단계 판정은 ${BUILD_JUDGES.join(", ")} 만 기록합니다(받은 값 ${event.verdict.by})`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Stage 7: a view passes only when all <agents> judges judge#1..judge#N pass with 90 or more, and its score is the
|
|
199
|
+
// lowest judge score.
|
|
200
|
+
function checkVisualJudges(event, n) {
|
|
201
|
+
(event.visual ?? []).forEach((v, i) => {
|
|
202
|
+
if (v.verdict !== "pass") return;
|
|
203
|
+
const where = `visual[${i}]`;
|
|
204
|
+
const judges = v.judges ?? [];
|
|
205
|
+
if (!sameIds(judges.map((j) => j.id), "judge", n)) throw new UsageError(`${where}.judges: pass 는 판정자 ${n}명(${seatIds("judge", n).join(", ")})이 있어야 합니다`);
|
|
206
|
+
if (judges.some((j) => j.verdict !== "pass" || !(j.score >= 90))) throw new UsageError(`${where}.judges: pass 는 모든 판정자가 pass 이고 90점 이상일 때만입니다`);
|
|
207
|
+
const low = Math.min(...judges.map((j) => j.score));
|
|
208
|
+
if (v.score !== low) throw new UsageError(`${where}.score: 판정자 점수의 최솟값 ${low} 이어야 합니다`);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Run folders made before --agents (no `agents` in run.json) are not checked, so an old run can still be resumed.
|
|
213
|
+
function checkQuorum(event, meta) {
|
|
214
|
+
if (!isObject(meta) || !Number.isInteger(meta.agents)) return;
|
|
215
|
+
checkBuildBy(event);
|
|
216
|
+
checkCriticSeats(event, meta.agents);
|
|
217
|
+
checkVisualJudges(event, meta.agents);
|
|
218
|
+
}
|
|
219
|
+
|
|
140
220
|
// ── run folder ───────────────────────────────────────────────────────────────
|
|
141
221
|
|
|
142
222
|
function git(cwd, args) {
|
|
@@ -191,19 +271,20 @@ function ignoreState(root, dir, baseDir, exclude) {
|
|
|
191
271
|
// Folders the stages write into, made up front so a shell redirect into them never fails.
|
|
192
272
|
export const RUN_FOLDERS = ["mock/expected", "actual", "visual", "qa", "boxes"];
|
|
193
273
|
|
|
194
|
-
// Creates <base>/<id>/ with run.json and an empty events.jsonl. opts: base, cwd, runtime, exclude, now, rand.
|
|
274
|
+
// Creates <base>/<id>/ with run.json and an empty events.jsonl. opts: base, cwd, runtime, agents, exclude, now, rand.
|
|
195
275
|
export function initRun(opts) {
|
|
196
276
|
const o = { cwd: process.cwd(), runtime: null, exclude: false, now: new Date(), rand: randomBytes(2).toString("hex"), ...opts };
|
|
197
277
|
if (!o.base) throw new UsageError("--base 가 필요합니다");
|
|
198
278
|
if (/[\u0000-\u001F]/.test(o.base)) throw new UsageError("--base 에 제어 문자가 있습니다");
|
|
199
279
|
if (o.runtime !== null && !["claude", "codex"].includes(o.runtime)) throw new UsageError("--runtime 은 claude, codex 중 하나입니다");
|
|
280
|
+
const agents = agentsOf(o.agents);
|
|
200
281
|
const info = repoInfo(o.cwd);
|
|
201
282
|
const baseDir = resolve(o.cwd, o.base);
|
|
202
283
|
const id = runId(o.now, o.rand);
|
|
203
284
|
const dir = join(baseDir, id);
|
|
204
285
|
if (existsSync(dir)) throw new Error(`이미 있는 실행 폴더입니다: ${dir}`);
|
|
205
286
|
mkdirSync(dir, { recursive: true });
|
|
206
|
-
const meta = { schema: 1, id, createdAt: o.now.toISOString(), runtime: o.runtime, ...info };
|
|
287
|
+
const meta = { schema: 1, id, createdAt: o.now.toISOString(), runtime: o.runtime, ...agents, ...info };
|
|
207
288
|
writeFileSync(join(dir, "run.json"), `${JSON.stringify(meta, null, 1)}\n`);
|
|
208
289
|
writeFileSync(join(dir, "events.jsonl"), "");
|
|
209
290
|
for (const f of RUN_FOLDERS) mkdirSync(join(dir, f), { recursive: true });
|
|
@@ -224,8 +305,9 @@ function needsNewline(file) {
|
|
|
224
305
|
}
|
|
225
306
|
|
|
226
307
|
export function appendEvent(dir, raw, now = new Date()) {
|
|
227
|
-
readRun(dir);
|
|
308
|
+
const meta = readRun(dir);
|
|
228
309
|
const event = { t: now.toISOString(), ...checkEvent(raw) };
|
|
310
|
+
checkQuorum(event, meta);
|
|
229
311
|
const file = join(dir, "events.jsonl");
|
|
230
312
|
appendFileSync(file, `${needsNewline(file) ? "\n" : ""}${JSON.stringify(event)}\n`);
|
|
231
313
|
return event;
|
|
@@ -446,7 +528,7 @@ export function gatesToTests(text, round, dir, gateDir) {
|
|
|
446
528
|
|
|
447
529
|
// ── command line ─────────────────────────────────────────────────────────────
|
|
448
530
|
|
|
449
|
-
const FLAGS = new Set(["--base", "--runtime", "--stage", "--status", "--file", "--round", "--summary", "--reason", "--unverified", "--why"]);
|
|
531
|
+
const FLAGS = new Set(["--base", "--runtime", "--agents", "--stage", "--status", "--file", "--round", "--summary", "--reason", "--unverified", "--why"]);
|
|
450
532
|
|
|
451
533
|
export function parseArgs(argv) {
|
|
452
534
|
const o = { _: [] };
|
|
@@ -533,7 +615,7 @@ export function progressLine(e) {
|
|
|
533
615
|
const withProgress = (e) => ({ ...e, progress: progressLine(e) });
|
|
534
616
|
|
|
535
617
|
const COMMANDS = {
|
|
536
|
-
init: (o) => initRun({ base: o.base, runtime: o.runtime ?? null, exclude: Boolean(o.exclude) }),
|
|
618
|
+
init: (o) => initRun({ base: o.base, runtime: o.runtime ?? null, agents: o.agents, exclude: Boolean(o.exclude) }),
|
|
537
619
|
event: (o) => withProgress(appendEvent(runDir(o), eventFrom(o))),
|
|
538
620
|
boxes: (o) => {
|
|
539
621
|
const dir = runDir(o);
|
|
@@ -566,10 +648,10 @@ const COMMANDS = {
|
|
|
566
648
|
// show is the first command of a resume, so it also re-makes the stage folders an older run may lack.
|
|
567
649
|
show: (o) => {
|
|
568
650
|
const dir = runDir(o);
|
|
569
|
-
const { id } = readRun(dir);
|
|
651
|
+
const { id, agents = null, agentsSource = null } = readRun(dir);
|
|
570
652
|
for (const f of RUN_FOLDERS) mkdirSync(join(dir, f), { recursive: true });
|
|
571
653
|
const { events, bad } = readEvents(dir);
|
|
572
|
-
return { id, dir, ...summarize(events), bad };
|
|
654
|
+
return { id, dir, agents, agentsSource, ...summarize(events), bad };
|
|
573
655
|
},
|
|
574
656
|
};
|
|
575
657
|
|
|
@@ -66,12 +66,19 @@ export function taskText(task) {
|
|
|
66
66
|
return lines.length > 1 && lines[1].trim() && lines[1].trim() === lines[0].trim() ? [lines[0], ...lines.slice(2)].join("\n") : String(task);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// How many reviewers each independent verdict used (--agents), and where the number came from.
|
|
70
|
+
export function agentsText(run) {
|
|
71
|
+
if (!Number.isInteger(run.agents)) return "기록 없음(옛 실행)";
|
|
72
|
+
if (run.agentsSource === "raised") return `${run.agents}(${run.agentsAsked ?? "?"} 에서 올림)`;
|
|
73
|
+
return `${run.agents}(${run.agentsSource === "flag" ? "--agents" : "기본"})`;
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
function renderHeader(run, task, sum, generatedAt) {
|
|
70
77
|
const total = sum.stages.reduce((a, s) => a + (s.ms ?? 0), 0);
|
|
71
78
|
const current = sum.current ? `${esc(LABEL[sum.current])} 단계` : "모든 단계 끝";
|
|
72
79
|
const facts = [
|
|
73
80
|
["실행", esc(run.id)], ["상태", `${badge(sum.overall)} ${current}`], ["시작", esc(when(run.createdAt))],
|
|
74
|
-
["단계 시간 합", esc(duration(total))], ["런타임", esc(run.runtime ?? "미상")],
|
|
81
|
+
["단계 시간 합", esc(duration(total))], ["런타임", esc(run.runtime ?? "미상")], ["검토자 수", esc(agentsText(run))],
|
|
75
82
|
["기준 커밋", esc((run.baseCommit ?? "").slice(0, 12))], ["브랜치", esc(run.branch ?? "")], ["보고서 생성", esc(when(generatedAt))],
|
|
76
83
|
];
|
|
77
84
|
const dl = facts.map(([k, v]) => `<div><dt>${esc(k)}</dt><dd>${v}</dd></div>`).join("");
|
|
@@ -123,12 +130,16 @@ function renderShot(p, caption, ctx) {
|
|
|
123
130
|
return `<figure class="shot"><a href="${esc(h)}"><img src="${esc(h)}" alt="${esc(caption)}" loading="lazy"></a><figcaption>${esc(caption)}</figcaption></figure>`;
|
|
124
131
|
}
|
|
125
132
|
|
|
133
|
+
const judgesTable = (judges) => (judges?.length
|
|
134
|
+
? table(["판정자", "점수", "판정"], judges.map((j) => [td(esc(j.id)), td(esc(j.score ?? "")), td(badge(j.verdict))])) : "");
|
|
135
|
+
|
|
126
136
|
function renderVisual(events, ctx) {
|
|
127
137
|
const cards = latestVisual(events).map((v) => {
|
|
128
138
|
const score = v.score === undefined ? "" : ` ${v.score}점`;
|
|
129
139
|
const intended = v.intended ? ' <span class="s s-note">의도한 차이</span>' : "";
|
|
130
140
|
return `<article class="card"><h3>${esc(v.name)}${esc(score)} ${v.verdict ? badge(v.verdict) : ""}${intended}</h3>`
|
|
131
|
-
+ `<div class="pair">${renderShot(v.mock, "목업", ctx)}${renderShot(v.actual, "실제", ctx)}</div>${
|
|
141
|
+
+ `<div class="pair">${renderShot(v.mock, "목업", ctx)}${renderShot(v.actual, "실제", ctx)}</div>${judgesTable(v.judges)}`
|
|
142
|
+
+ `${v.note ? `<p>${esc(v.note)}</p>` : ""}</article>`;
|
|
132
143
|
});
|
|
133
144
|
return `<section><h2>시각 검토</h2>${cards.length ? cards.join("\n") : '<p class="none">없음</p>'}</section>`;
|
|
134
145
|
}
|