candy-debug 0.0.0-stage → 1.0.0
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/LICENSE +21 -0
- package/README.md +73 -2
- package/dist/candy-debug.mjs +1061 -0
- package/package.json +28 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ryome
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,74 @@
|
|
|
1
|
-
#
|
|
1
|
+
# candy-debug
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
ロキシーがあなたのコードをレビューして、ノベルゲーム用の台本(JSON)を作る CLI です。
|
|
4
|
+
|
|
5
|
+
フォルダを渡すと、ローカルの [Claude Code](https://www.npmjs.com/package/@anthropic-ai/claude-code)(`claude` コマンド)が
|
|
6
|
+
コードを読み、指摘をロキシーの台詞にまとめます。出てきた JSON は candy-debug のクライアントアプリで、
|
|
7
|
+
音声つきのノベルとして再生できます。
|
|
8
|
+
|
|
9
|
+
## 必要なもの
|
|
10
|
+
|
|
11
|
+
- Node.js 20 以上
|
|
12
|
+
- ログイン済みの `claude` コマンド(`npm install -g @anthropic-ai/claude-code`)
|
|
13
|
+
|
|
14
|
+
## インストール
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install -g candy-debug
|
|
18
|
+
candy-debug --doctor # claude が使えるかを確かめる
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`cdbg` という短縮名でも同じコマンドを呼べます(`cdbg ./my-project -o review.json`)。
|
|
22
|
+
|
|
23
|
+
## 使い方
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
# 結果をファイルに書き出す
|
|
27
|
+
candy-debug ./my-project -o review.json
|
|
28
|
+
|
|
29
|
+
# 標準出力に JSON を出す(進捗は標準エラー)
|
|
30
|
+
candy-debug ./my-project > review.json
|
|
31
|
+
|
|
32
|
+
# AI を呼ばずに、対象になるファイルだけを確かめる
|
|
33
|
+
candy-debug ./my-project --scan-only
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| オプション | 内容 |
|
|
37
|
+
| ----------------------- | ----------------------------------------------------- |
|
|
38
|
+
| `-o, --output <file>` | 結果の JSON を書き出すファイル(省略時は標準出力) |
|
|
39
|
+
| `--max-findings <n>` | 指摘の最大件数(既定 6) |
|
|
40
|
+
| `--model <id>` | レビューに使うモデル(既定 `claude-sonnet-5`) |
|
|
41
|
+
| `--scenario-model <id>` | 台本づくりに使うモデル(既定は `--model` と同じ) |
|
|
42
|
+
| `--claude <command>` | `claude` コマンドの場所 |
|
|
43
|
+
| `--progress <mode>` | 進捗の出し方: `text` / `json` / `none`(既定 `text`) |
|
|
44
|
+
| `--scan-only` | 走査結果だけを JSON で出す |
|
|
45
|
+
| `--doctor [--json]` | `claude` の疎通確認 |
|
|
46
|
+
|
|
47
|
+
終了コード: `0` 成功 / `1` 失敗 / `2` 引数の誤り / `130` 中断
|
|
48
|
+
|
|
49
|
+
## 注意
|
|
50
|
+
|
|
51
|
+
- **対象のコードは `claude` コマンドを通じて Anthropic のサーバへ送られます。** 送ってよいコードかを確かめてから実行してください。
|
|
52
|
+
- 対象フォルダへは一切書き込みません(`-o` で明示した場所にだけ書きます)。
|
|
53
|
+
- 1 回の実行で `claude` を数回呼びます。目安は指摘 6 件で約 3 分・約 $0.3 です(2026-09-09 実測)。
|
|
54
|
+
|
|
55
|
+
## 出力の形
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
{
|
|
59
|
+
"format": "candy-debug/review",
|
|
60
|
+
"schemaVersion": 1,
|
|
61
|
+
"generator": { "name": "candy-debug", "version": "1.0.0" },
|
|
62
|
+
"project": { "name": "my-project", "dir": "/path/to/my-project" },
|
|
63
|
+
"findings": [/* 指摘 */],
|
|
64
|
+
"scenario": { "beats": [/* 台詞・表情・コード片・赤枠の位置 */] },
|
|
65
|
+
"warnings": [],
|
|
66
|
+
"costUsd": 0.29,
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
コード片は台本に埋め込まれているので、再生する側は対象プロジェクトを持っていなくても構いません。
|
|
71
|
+
|
|
72
|
+
## ライセンス
|
|
73
|
+
|
|
74
|
+
MIT
|
|
@@ -0,0 +1,1061 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
|
|
8
|
+
// package.json
|
|
9
|
+
var package_default = {
|
|
10
|
+
name: "candy-debug",
|
|
11
|
+
version: "1.0.0",
|
|
12
|
+
description: "\u30ED\u30AD\u30B7\u30FC\u304C\u30B3\u30FC\u30C9\u30EC\u30D3\u30E5\u30FC\u3092\u3057\u3066\u3001\u30CE\u30D9\u30EB\u30B2\u30FC\u30E0\u7528\u306E\u53F0\u672C\uFF08JSON\uFF09\u3092\u4F5C\u308B CLI",
|
|
13
|
+
type: "module",
|
|
14
|
+
bin: {
|
|
15
|
+
"candy-debug": "dist/candy-debug.mjs",
|
|
16
|
+
cdbg: "dist/candy-debug.mjs"
|
|
17
|
+
},
|
|
18
|
+
files: [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
engines: {
|
|
24
|
+
node: ">=20"
|
|
25
|
+
},
|
|
26
|
+
scripts: {
|
|
27
|
+
build: 'esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --loader:.csv=text --banner:js="#!/usr/bin/env node" --outfile=dist/candy-debug.mjs --log-level=warning',
|
|
28
|
+
prepublishOnly: "npm run build"
|
|
29
|
+
},
|
|
30
|
+
keywords: [
|
|
31
|
+
"code-review",
|
|
32
|
+
"claude",
|
|
33
|
+
"cli",
|
|
34
|
+
"visual-novel"
|
|
35
|
+
],
|
|
36
|
+
author: "ryome",
|
|
37
|
+
license: "MIT"
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/ai/claudeCli.ts
|
|
41
|
+
import { spawn } from "node:child_process";
|
|
42
|
+
function describeSpawnError(command, e) {
|
|
43
|
+
const code = e?.code;
|
|
44
|
+
const raw = e instanceof Error ? e.message : String(e);
|
|
45
|
+
switch (code) {
|
|
46
|
+
case "ENOENT":
|
|
47
|
+
return `\`${command}\` \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F\u3002Claude Code \u3092\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3057\u3001PATH \u306B\u542B\u307E\u308C\u3066\u3044\u308B\u304B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\uFF08${raw}\uFF09`;
|
|
48
|
+
case "ENOEXEC":
|
|
49
|
+
return `\`${command}\` \u306F\u898B\u3064\u304B\u308A\u307E\u3057\u305F\u304C\u3001\u5B9F\u884C\u3067\u304D\u308B\u5F62\u5F0F\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002Claude Code \u306E\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u304C\u58CA\u308C\u3066\u3044\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002\u30BF\u30FC\u30DF\u30CA\u30EB\u3067 \`${command} --version\` \u3092\u5B9F\u884C\u3057\u3066\u72B6\u6CC1\u3092\u78BA\u304B\u3081\u3001\u5165\u308C\u76F4\u3057\u3066\u304F\u3060\u3055\u3044\uFF08npm \u3067\u5165\u308C\u305F\u5834\u5408: npm install -g @anthropic-ai/claude-code\uFF09\uFF08${raw}\uFF09`;
|
|
50
|
+
case "EACCES":
|
|
51
|
+
return `\`${command}\` \u306B\u5B9F\u884C\u306E\u6A29\u9650\u304C\u3042\u308A\u307E\u305B\u3093\u3002Claude Code \u3092\u5165\u308C\u76F4\u3059\u304B\u3001\u5B9F\u884C\u6A29\u9650\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\uFF08${raw}\uFF09`;
|
|
52
|
+
default:
|
|
53
|
+
return `\`${command}\` \u3092\u8D77\u52D5\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\uFF08${raw}\uFF09`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
var DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
57
|
+
var ClaudeCliProvider = class {
|
|
58
|
+
id = "claude-cli";
|
|
59
|
+
displayName = "Claude\uFF08\u30ED\u30FC\u30AB\u30EB\u306E claude \u30B3\u30DE\u30F3\u30C9\uFF09";
|
|
60
|
+
command;
|
|
61
|
+
model;
|
|
62
|
+
scenarioModel;
|
|
63
|
+
timeoutMs;
|
|
64
|
+
constructor(opts = {}) {
|
|
65
|
+
this.command = opts.command ?? "claude";
|
|
66
|
+
this.model = opts.model ?? "claude-sonnet-5";
|
|
67
|
+
this.scenarioModel = opts.scenarioModel ?? this.model;
|
|
68
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
69
|
+
}
|
|
70
|
+
/** 子プロセスを起動し、stdin にプロンプトを流して stdout を集める */
|
|
71
|
+
run(args, stdin, timeoutMs, signal) {
|
|
72
|
+
return new Promise((resolve2, reject) => {
|
|
73
|
+
let child;
|
|
74
|
+
try {
|
|
75
|
+
child = spawn(this.command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
76
|
+
} catch (e) {
|
|
77
|
+
reject(new Error(describeSpawnError(this.command, e)));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
let stdout = "";
|
|
81
|
+
let stderr = "";
|
|
82
|
+
let settled = false;
|
|
83
|
+
const finish = (fn) => {
|
|
84
|
+
if (settled) return;
|
|
85
|
+
settled = true;
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
signal?.removeEventListener("abort", onAbort);
|
|
88
|
+
fn();
|
|
89
|
+
};
|
|
90
|
+
const timer = setTimeout(() => {
|
|
91
|
+
child.kill("SIGTERM");
|
|
92
|
+
finish(
|
|
93
|
+
() => reject(new Error(`AI \u306E\u5FDC\u7B54\u304C ${Math.round(timeoutMs / 1e3)} \u79D2\u4EE5\u5185\u306B\u8FD4\u308A\u307E\u305B\u3093\u3067\u3057\u305F`))
|
|
94
|
+
);
|
|
95
|
+
}, timeoutMs);
|
|
96
|
+
const onAbort = () => {
|
|
97
|
+
child.kill("SIGTERM");
|
|
98
|
+
finish(() => reject(new Error("\u4E2D\u65AD\u3057\u307E\u3057\u305F")));
|
|
99
|
+
};
|
|
100
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
101
|
+
child.stdout.on("data", (d) => stdout += d.toString());
|
|
102
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
103
|
+
child.on("error", (e) => {
|
|
104
|
+
finish(() => reject(new Error(describeSpawnError(this.command, e))));
|
|
105
|
+
});
|
|
106
|
+
child.on("close", (code) => finish(() => resolve2({ code, stdout, stderr })));
|
|
107
|
+
child.stdin.write(stdin);
|
|
108
|
+
child.stdin.end();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async ping(signal) {
|
|
112
|
+
try {
|
|
113
|
+
const r = await this.run(["--version"], "", 15e3, signal);
|
|
114
|
+
if (r.code !== 0) return { ok: false, detail: r.stderr.trim() || `\u7D42\u4E86\u30B3\u30FC\u30C9 ${r.code}` };
|
|
115
|
+
return { ok: true, detail: `${r.stdout.trim()} / \u30E2\u30C7\u30EB ${this.model}` };
|
|
116
|
+
} catch (e) {
|
|
117
|
+
return { ok: false, detail: e instanceof Error ? e.message : String(e) };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async complete(req, signal) {
|
|
121
|
+
const model = req.purpose === "scenario" ? this.scenarioModel : this.model;
|
|
122
|
+
const started = Date.now();
|
|
123
|
+
const r = await this.run(
|
|
124
|
+
[
|
|
125
|
+
"-p",
|
|
126
|
+
"--output-format",
|
|
127
|
+
"json",
|
|
128
|
+
// ツールは全て無効化する。コードはプロンプトで渡すので読ませる必要がない
|
|
129
|
+
"--allowed-tools",
|
|
130
|
+
"",
|
|
131
|
+
"--model",
|
|
132
|
+
model,
|
|
133
|
+
"--append-system-prompt",
|
|
134
|
+
req.system
|
|
135
|
+
],
|
|
136
|
+
req.user,
|
|
137
|
+
this.timeoutMs,
|
|
138
|
+
signal
|
|
139
|
+
);
|
|
140
|
+
if (r.code !== 0) {
|
|
141
|
+
const head = r.stderr.trim().split("\n")[0] ?? "";
|
|
142
|
+
throw new Error(
|
|
143
|
+
`AI \u306E\u547C\u3073\u51FA\u3057\u306B\u5931\u6557\u3057\u307E\u3057\u305F\uFF08\u7D42\u4E86\u30B3\u30FC\u30C9 ${r.code}\uFF09${head ? `: ${head}` : ""}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
let parsed;
|
|
147
|
+
try {
|
|
148
|
+
parsed = JSON.parse(r.stdout);
|
|
149
|
+
} catch {
|
|
150
|
+
throw new Error("AI \u306E\u5FDC\u7B54\u3092\u89E3\u91C8\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\uFF08JSON \u3068\u3057\u3066\u8AAD\u3081\u306A\u3044\uFF09");
|
|
151
|
+
}
|
|
152
|
+
if (parsed.is_error) throw new Error("AI \u304C\u30A8\u30E9\u30FC\u3092\u8FD4\u3057\u307E\u3057\u305F");
|
|
153
|
+
if (typeof parsed.result !== "string") throw new Error("AI \u306E\u5FDC\u7B54\u306B\u672C\u6587\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u305B\u3093");
|
|
154
|
+
return {
|
|
155
|
+
text: parsed.result,
|
|
156
|
+
usage: {
|
|
157
|
+
inputTokens: (parsed.usage?.input_tokens ?? 0) + (parsed.usage?.cache_read_input_tokens ?? 0) + (parsed.usage?.cache_creation_input_tokens ?? 0),
|
|
158
|
+
outputTokens: parsed.usage?.output_tokens,
|
|
159
|
+
costUsd: parsed.total_cost_usd
|
|
160
|
+
},
|
|
161
|
+
elapsedMs: Date.now() - started
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// ../doc/csv/characters.csv
|
|
167
|
+
var characters_default = 'character_id,name_ja,description,persona,tone,sample_lines,voice_engine,voice_speaker,voice_params,sprite_dir,status,notes\nchar_main,\u30ED\u30AD\u30B7\u30FC,\u8A9E\u308A\u624B\u3002\u9752\u3044\u4E09\u3064\u7DE8\u307F\u3068\u30B8\u30C8\u76EE\u306E\u9B54\u8853\u5E2B\u98A8\u3002\u6C34\u738B\u7D1A\u306E\u9B54\u8853\u5E2B\u306B\u3057\u3066\u30D7\u30ED\u30B0\u30E9\u30DF\u30F3\u30B0\u306E\u8B1B\u5E2B\u3068\u3044\u3046\u8AAD\u307F\u66FF\u3048,\u6709\u80FD\u3067\u4E01\u5BE7\u3001\u3067\u3082\u3069\u3053\u304B\u629C\u3051\u3066\u3044\u308B\u30AF\u30FC\u30EB\u306A\u30C9\u30B8\u3063\u5B50\u3002\u793C\u5100\u6B63\u3057\u3044\u52AA\u529B\u5BB6\u3067\u3001\u7406\u77E5\u7684\u306B\u843D\u3061\u7740\u3044\u3066\u8AAC\u660E\u3059\u308B\u6559\u5E2B\u3002\u5DEE\u5225\u3068\u5B64\u72EC\u3092\u7D4C\u9A13\u3057\u3066\u304D\u305F\u3076\u3093\u3001\u76F8\u624B\u3092\u898B\u4E0B\u3055\u305A\u3001\u66F8\u3044\u305F\u4EBA\u3092\u5426\u5B9A\u3057\u306A\u3044\u3002\u751F\u5F92\u3092\u5B89\u5FC3\u3055\u305B\u308B\u58F0\u304B\u3051\u3092\u3059\u308B\uFF08\u300C\u5927\u4E08\u592B\u3067\u3059\u3088\u3001\u9806\u756A\u306B\u898B\u3066\u3044\u304D\u307E\u3057\u3087\u3046\u300D\uFF09\u3002\u305F\u3060\u3057\u7269\u4E8B\u3092\u6700\u901F\u3067\u7D42\u308F\u3089\u305B\u3088\u3046\u3068\u3059\u308B\u6027\u8CEA\u304C\u707D\u3044\u3057\u3066\u3001\u809D\u5FC3\u306A\u3068\u3053\u308D\u3067\u9593\u306E\u60AA\u3044\u5931\u6557\u3092\u3059\u308B\u3002\u52D5\u63FA\u3057\u305F\u308A\u7167\u308C\u305F\u308A\u3059\u308B\u3068\u9014\u7AEF\u306B\u3057\u3069\u308D\u3082\u3069\u308D\u306B\u306A\u308B\u3002\u826F\u3044\u3068\u3053\u308D\u306F\u7D20\u76F4\u306B\u8912\u3081\u308B\u3002\u65AD\u5B9A\u3067\u304D\u306A\u3044\u3053\u3068\u306F\u65AD\u5B9A\u3057\u306A\u3044\u3002,\u4E00\u8CAB\u3057\u3066\u4E01\u5BE7\u8A9E\uFF08\u656C\u8A9E\uFF09\u3067\u8A71\u3059\u3002\u4E00\u4EBA\u79F0\u306F\u300C\u79C1\u300D\u3002\u3053\u308C\u306F\u5805\u82E6\u3057\u3055\u3067\u306F\u306A\u304F\u3001\u76F8\u624B\u3092\u4E0D\u5FEB\u306B\u3055\u305B\u306A\u3044\u305F\u3081\u306B\u610F\u8B58\u7684\u306B\u9078\u3093\u3067\u3044\u308B\u8A71\u3057\u65B9\u3002\u666E\u6BB5\u306F\u843D\u3061\u7740\u3044\u305F\u6559\u5E2B\u3089\u3057\u3044\u7406\u77E5\u7684\u306A\u53E3\u8ABF\u3002\u52D5\u63FA\u30FB\u7167\u308C\u30FB\u5931\u6557\u306E\u3068\u304D\u3060\u3051\u5D29\u308C\u3066\u3001\u3057\u3069\u308D\u3082\u3069\u308D\u306B\u306A\u308B\u3002\u7815\u3051\u305F\u8A9E\u5C3E\uFF08\u300C\u301C\u3060\u306D\u300D\u300C\u301C\u304B\u306A\u300D\u300C\u301C\u3060\u3088\u300D\uFF09\u306F\u4F7F\u308F\u306A\u3044\u3002,\u3053\u3053\u3001\u53D7\u3051\u53D6\u3063\u305F\u5024\u3092\u305D\u306E\u307E\u307E\u6587\u5B57\u5217\u306B\u6DF7\u305C\u3066\u3044\u307E\u3059\u306D\u3002\u5916\u304B\u3089\u6765\u305F\u6587\u5B57\u304C\u547D\u4EE4\u3068\u3057\u3066\u89E3\u91C8\u3055\u308C\u3066\u3057\u307E\u3044\u307E\u3059\u3002|\u5927\u4E08\u592B\u3067\u3059\u3088\u3002\u76F4\u3057\u65B9\u306F\u305D\u308C\u307B\u3069\u96E3\u3057\u304F\u3042\u308A\u307E\u305B\u3093\u304B\u3089\u3001\u9806\u756A\u306B\u898B\u3066\u3044\u304D\u307E\u3057\u3087\u3046\u3002|\u2026\u2026\u3042\u3063\u3001\u3059\u307F\u307E\u305B\u3093\u3002\u4ECA\u306E\u306F\u79C1\u306E\u52D8\u9055\u3044\u3067\u3059\u3002\u3048\u3048\u3068\u3001\u3082\u3046\u4E00\u5EA6\u8AAC\u660E\u3055\u305B\u3066\u304F\u3060\u3055\u3044\u3002,irodori,char_main,"{""speed"":1.0,""seed"":1234}",assets/characters/char_main,\u8A2D\u5B9A\u6E08\u307F,\u8A2D\u5B9A\u306E\u51FA\u5178: input/\u30ED\u30AD\u30B7\u30FC\u30FB\u30DF\u30B0\u30EB\u30C7\u30A3\u30A2_\u30AD\u30E3\u30E9\u30AF\u30BF\u30FC\u5206\u6790.md\uFF082026-09-09 \u53CD\u6620\uFF09\u3002**tone \u306B\u8A18\u5165\u4F8B\u306E\u30D7\u30EC\u30FC\u30B9\u30DB\u30EB\u30C0\u304C\u6B8B\u3063\u3066\u3044\u3066\u3001\u305D\u308C\u304C\u305D\u306E\u307E\u307E\u53F0\u672C\u751F\u6210\u30D7\u30ED\u30F3\u30D7\u30C8\u3078\u6CE8\u5165\u3055\u308C\u3066\u3044\u305F**\uFF08\u8AB2\u984C AI-04\uFF09\u3002persona\u30FBtone\u30FBsample_lines \u306F\u30D7\u30ED\u30F3\u30D7\u30C8\u306B\u76F4\u63A5\u5165\u308B\u3002\u7279\u306B sample_lines \u306F\u53E3\u8ABF\u306E\u624B\u672C\u3068\u3057\u3066\u52B9\u679C\u304C\u5927\u304D\u3044\u3002voice_speaker \u306F Irodori-TTS \u306E voice_id \u3068\u4E00\u81F4\u3055\u305B\u308B\u3053\u3068\u3002\u53C2\u7167\u97F3\u58F0\u306F resources.csv \u306E voice_ref_char_main \u884C\uFF08\u8AB2\u984C AUDIO-07\uFF09\n';
|
|
168
|
+
|
|
169
|
+
// src/character.ts
|
|
170
|
+
var FALLBACK = {
|
|
171
|
+
characterId: "char_main",
|
|
172
|
+
nameJa: "\u30ED\u30AD\u30B7\u30FC",
|
|
173
|
+
persona: "\u6709\u80FD\u3067\u4E01\u5BE7\u3001\u3067\u3082\u3069\u3053\u304B\u629C\u3051\u3066\u3044\u308B\u30AF\u30FC\u30EB\u306A\u30C9\u30B8\u3063\u5B50\u3002\u7406\u77E5\u7684\u306B\u843D\u3061\u7740\u3044\u3066\u8AAC\u660E\u3059\u308B\u6559\u5E2B\u3002\u66F8\u3044\u305F\u4EBA\u3092\u5426\u5B9A\u3057\u306A\u3044\u3002\u65AD\u5B9A\u3067\u304D\u306A\u3044\u3053\u3068\u306F\u65AD\u5B9A\u3057\u306A\u3044\u3002",
|
|
174
|
+
tone: "\u4E00\u8CAB\u3057\u3066\u4E01\u5BE7\u8A9E\u3067\u8A71\u3059\u3002\u4E00\u4EBA\u79F0\u306F\u300C\u79C1\u300D\u3002\u7815\u3051\u305F\u8A9E\u5C3E\u306F\u4F7F\u308F\u306A\u3044\u3002",
|
|
175
|
+
sampleLines: [
|
|
176
|
+
"\u3053\u3053\u3001\u53D7\u3051\u53D6\u3063\u305F\u5024\u3092\u305D\u306E\u307E\u307E\u6587\u5B57\u5217\u306B\u6DF7\u305C\u3066\u3044\u307E\u3059\u306D\u3002",
|
|
177
|
+
"\u5927\u4E08\u592B\u3067\u3059\u3088\u3002\u9806\u756A\u306B\u898B\u3066\u3044\u304D\u307E\u3057\u3087\u3046\u3002",
|
|
178
|
+
"\u2026\u2026\u3042\u3063\u3001\u3059\u307F\u307E\u305B\u3093\u3002\u4ECA\u306E\u306F\u79C1\u306E\u52D8\u9055\u3044\u3067\u3059\u3002"
|
|
179
|
+
]
|
|
180
|
+
};
|
|
181
|
+
function splitCsvLine(line) {
|
|
182
|
+
const out = [];
|
|
183
|
+
let cur = "";
|
|
184
|
+
let q = false;
|
|
185
|
+
for (let i = 0; i < line.length; i++) {
|
|
186
|
+
const c = line[i];
|
|
187
|
+
if (c === '"') {
|
|
188
|
+
if (q && line[i + 1] === '"') {
|
|
189
|
+
cur += '"';
|
|
190
|
+
i++;
|
|
191
|
+
} else q = !q;
|
|
192
|
+
} else if (c === "," && !q) {
|
|
193
|
+
out.push(cur);
|
|
194
|
+
cur = "";
|
|
195
|
+
} else cur += c;
|
|
196
|
+
}
|
|
197
|
+
out.push(cur);
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
function parseCharacterCsv(csv, characterId) {
|
|
201
|
+
try {
|
|
202
|
+
const [header, ...rows] = csv.trim().split("\n");
|
|
203
|
+
if (!header || rows.length === 0) return FALLBACK;
|
|
204
|
+
const h = splitCsvLine(header);
|
|
205
|
+
const parsed = rows.map(splitCsvLine);
|
|
206
|
+
const r = (characterId ? parsed.find((x) => x[h.indexOf("character_id")] === characterId) : null) ?? parsed[0];
|
|
207
|
+
const get = (k) => r[h.indexOf(k)] ?? "";
|
|
208
|
+
return {
|
|
209
|
+
characterId: get("character_id") || FALLBACK.characterId,
|
|
210
|
+
nameJa: get("name_ja") || FALLBACK.nameJa,
|
|
211
|
+
persona: get("persona") || FALLBACK.persona,
|
|
212
|
+
tone: get("tone") || FALLBACK.tone,
|
|
213
|
+
sampleLines: (get("sample_lines") || FALLBACK.sampleLines.join("|")).split("|").filter(Boolean)
|
|
214
|
+
};
|
|
215
|
+
} catch {
|
|
216
|
+
return FALLBACK;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function defaultCharacter() {
|
|
220
|
+
return parseCharacterCsv(characters_default);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/scenario/types.ts
|
|
224
|
+
var EMOTIONS = [
|
|
225
|
+
"normal",
|
|
226
|
+
"serious",
|
|
227
|
+
"thinking",
|
|
228
|
+
"proud",
|
|
229
|
+
"exasperated",
|
|
230
|
+
"flustered",
|
|
231
|
+
"sulking"
|
|
232
|
+
];
|
|
233
|
+
var MAX_TEXT_LENGTH = 100;
|
|
234
|
+
function validateScenario(s) {
|
|
235
|
+
const issues = [];
|
|
236
|
+
const beats = s.beats.map((b) => {
|
|
237
|
+
const fixed = { ...b };
|
|
238
|
+
if (!EMOTIONS.includes(fixed.emotion)) {
|
|
239
|
+
issues.push({ beatId: b.id, message: `\u672A\u77E5\u306E\u8868\u60C5 "${b.emotion}" \u2192 normal \u3067\u4EE3\u66FF` });
|
|
240
|
+
fixed.emotion = "normal";
|
|
241
|
+
}
|
|
242
|
+
if ([...fixed.text].length > MAX_TEXT_LENGTH) {
|
|
243
|
+
issues.push({
|
|
244
|
+
beatId: b.id,
|
|
245
|
+
message: `\u53F0\u8A5E\u304C ${[...fixed.text].length} \u6587\u5B57\uFF08\u4E0A\u9650 ${MAX_TEXT_LENGTH}\uFF09\u2192 \u672B\u5C3E\u3092\u5207\u308B`
|
|
246
|
+
});
|
|
247
|
+
fixed.text = [...fixed.text].slice(0, MAX_TEXT_LENGTH - 1).join("") + "\u2026";
|
|
248
|
+
}
|
|
249
|
+
return fixed;
|
|
250
|
+
});
|
|
251
|
+
return { scenario: { ...s, beats }, issues };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/contract.ts
|
|
255
|
+
var REVIEW_FILE_FORMAT = "candy-debug/review";
|
|
256
|
+
var REVIEW_FILE_VERSION = 1;
|
|
257
|
+
function summarizeScan(r) {
|
|
258
|
+
return {
|
|
259
|
+
projectDir: r.projectDir,
|
|
260
|
+
projectName: r.projectName,
|
|
261
|
+
fileCount: r.files.length,
|
|
262
|
+
files: r.files.map((f) => ({ relPath: f.relPath, lines: f.lines, language: f.language })),
|
|
263
|
+
excludedCount: r.excludedCount,
|
|
264
|
+
totalLines: r.totalLines,
|
|
265
|
+
estimatedTokens: r.estimatedTokens,
|
|
266
|
+
languageBreakdown: r.languageBreakdown,
|
|
267
|
+
warnings: r.warnings
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/ai/types.ts
|
|
272
|
+
function extractJson(text) {
|
|
273
|
+
const tryParse = (s) => {
|
|
274
|
+
try {
|
|
275
|
+
return JSON.parse(s);
|
|
276
|
+
} catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const direct = tryParse(text.trim());
|
|
281
|
+
if (direct !== null) return direct;
|
|
282
|
+
const fence = text.match(/```(?:json)?\s*\n([\s\S]*?)\n?```/);
|
|
283
|
+
if (fence) {
|
|
284
|
+
const inner = tryParse(fence[1].trim());
|
|
285
|
+
if (inner !== null) return inner;
|
|
286
|
+
}
|
|
287
|
+
for (const [open, close] of [
|
|
288
|
+
["{", "}"],
|
|
289
|
+
["[", "]"]
|
|
290
|
+
]) {
|
|
291
|
+
const from = text.indexOf(open);
|
|
292
|
+
const to = text.lastIndexOf(close);
|
|
293
|
+
if (from >= 0 && to > from) {
|
|
294
|
+
const cut = tryParse(text.slice(from, to + 1));
|
|
295
|
+
if (cut !== null) return cut;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/review/scanner.ts
|
|
302
|
+
import { promises as fs } from "node:fs";
|
|
303
|
+
import { join, relative, sep } from "node:path";
|
|
304
|
+
var ALWAYS_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
|
|
305
|
+
"node_modules",
|
|
306
|
+
".git",
|
|
307
|
+
"dist",
|
|
308
|
+
"out",
|
|
309
|
+
"build",
|
|
310
|
+
".next",
|
|
311
|
+
".cache",
|
|
312
|
+
"vendor",
|
|
313
|
+
"__pycache__",
|
|
314
|
+
".venv",
|
|
315
|
+
"venv",
|
|
316
|
+
"coverage",
|
|
317
|
+
".idea",
|
|
318
|
+
".vscode"
|
|
319
|
+
]);
|
|
320
|
+
var DEFAULT_EXTENSIONS = [
|
|
321
|
+
".ts",
|
|
322
|
+
".tsx",
|
|
323
|
+
".js",
|
|
324
|
+
".jsx",
|
|
325
|
+
".mjs",
|
|
326
|
+
".cjs",
|
|
327
|
+
".py",
|
|
328
|
+
".go",
|
|
329
|
+
".rb",
|
|
330
|
+
".rs",
|
|
331
|
+
".java",
|
|
332
|
+
".kt",
|
|
333
|
+
".swift",
|
|
334
|
+
".c",
|
|
335
|
+
".h",
|
|
336
|
+
".cpp",
|
|
337
|
+
".cs",
|
|
338
|
+
".php",
|
|
339
|
+
".css",
|
|
340
|
+
".scss",
|
|
341
|
+
".vue",
|
|
342
|
+
".svelte"
|
|
343
|
+
];
|
|
344
|
+
var LANG_BY_EXT = {
|
|
345
|
+
".ts": "typescript",
|
|
346
|
+
".tsx": "tsx",
|
|
347
|
+
".js": "javascript",
|
|
348
|
+
".jsx": "jsx",
|
|
349
|
+
".mjs": "javascript",
|
|
350
|
+
".cjs": "javascript",
|
|
351
|
+
".py": "python",
|
|
352
|
+
".json": "json",
|
|
353
|
+
".css": "css",
|
|
354
|
+
".scss": "css",
|
|
355
|
+
".html": "html"
|
|
356
|
+
};
|
|
357
|
+
function languageOf(relPath) {
|
|
358
|
+
const dot = relPath.lastIndexOf(".");
|
|
359
|
+
if (dot < 0) return "typescript";
|
|
360
|
+
return LANG_BY_EXT[relPath.slice(dot).toLowerCase()] ?? "typescript";
|
|
361
|
+
}
|
|
362
|
+
function estimateTokens(text) {
|
|
363
|
+
return Math.ceil(text.length / 3);
|
|
364
|
+
}
|
|
365
|
+
async function scanProject(dir, opts = {}) {
|
|
366
|
+
const exts = new Set(opts.includeExtensions ?? DEFAULT_EXTENSIONS);
|
|
367
|
+
const maxFileLines = opts.maxFileLines ?? 1500;
|
|
368
|
+
const maxFiles = opts.maxFiles ?? 40;
|
|
369
|
+
const files = [];
|
|
370
|
+
const warnings = [];
|
|
371
|
+
let excludedCount = 0;
|
|
372
|
+
async function walk(cur) {
|
|
373
|
+
let entries;
|
|
374
|
+
try {
|
|
375
|
+
entries = await fs.readdir(cur, { withFileTypes: true });
|
|
376
|
+
} catch {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
for (const e of entries) {
|
|
380
|
+
if (files.length >= maxFiles) return;
|
|
381
|
+
const full = join(cur, e.name);
|
|
382
|
+
if (e.isDirectory()) {
|
|
383
|
+
if (ALWAYS_EXCLUDE_DIRS.has(e.name) || e.name.startsWith(".")) {
|
|
384
|
+
excludedCount++;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
await walk(full);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (!e.isFile()) continue;
|
|
391
|
+
const dot = e.name.lastIndexOf(".");
|
|
392
|
+
const ext = dot >= 0 ? e.name.slice(dot).toLowerCase() : "";
|
|
393
|
+
if (!exts.has(ext)) {
|
|
394
|
+
excludedCount++;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
let content;
|
|
398
|
+
try {
|
|
399
|
+
content = await fs.readFile(full, "utf8");
|
|
400
|
+
} catch {
|
|
401
|
+
excludedCount++;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const lines = content.split("\n").length;
|
|
405
|
+
if (lines > maxFileLines) {
|
|
406
|
+
warnings.push(`${e.name} \u306F ${lines} \u884C\u3042\u308B\u305F\u3081\u5BFE\u8C61\u304B\u3089\u5916\u3057\u307E\u3057\u305F`);
|
|
407
|
+
excludedCount++;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
files.push({
|
|
411
|
+
relPath: relative(dir, full).split(sep).join("/"),
|
|
412
|
+
language: languageOf(e.name),
|
|
413
|
+
lines,
|
|
414
|
+
bytes: Buffer.byteLength(content, "utf8"),
|
|
415
|
+
content
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
await walk(dir);
|
|
420
|
+
files.sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
421
|
+
const languageBreakdown = {};
|
|
422
|
+
for (const f of files) languageBreakdown[f.language] = (languageBreakdown[f.language] ?? 0) + 1;
|
|
423
|
+
const projectName = dir.split(sep).filter(Boolean).pop() ?? dir;
|
|
424
|
+
return {
|
|
425
|
+
projectDir: dir,
|
|
426
|
+
projectName,
|
|
427
|
+
files,
|
|
428
|
+
excludedCount,
|
|
429
|
+
totalLines: files.reduce((n, f) => n + f.lines, 0),
|
|
430
|
+
estimatedTokens: files.reduce((n, f) => n + estimateTokens(f.content), 0),
|
|
431
|
+
languageBreakdown,
|
|
432
|
+
warnings
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/review/chunker.ts
|
|
437
|
+
var TAB_WIDTH = 2;
|
|
438
|
+
function expandTabs(text) {
|
|
439
|
+
return text.replace(/\t/g, " ".repeat(TAB_WIDTH));
|
|
440
|
+
}
|
|
441
|
+
function makeChunks(files, maxTokens = 24e3) {
|
|
442
|
+
const chunks = [];
|
|
443
|
+
let cur = [];
|
|
444
|
+
let curTokens = 0;
|
|
445
|
+
for (const f of files) {
|
|
446
|
+
const t = estimateTokens(f.content);
|
|
447
|
+
if (cur.length > 0 && curTokens + t > maxTokens) {
|
|
448
|
+
chunks.push({ files: cur, estimatedTokens: curTokens });
|
|
449
|
+
cur = [];
|
|
450
|
+
curTokens = 0;
|
|
451
|
+
}
|
|
452
|
+
cur.push(f);
|
|
453
|
+
curTokens += t;
|
|
454
|
+
}
|
|
455
|
+
if (cur.length > 0) chunks.push({ files: cur, estimatedTokens: curTokens });
|
|
456
|
+
return chunks;
|
|
457
|
+
}
|
|
458
|
+
function formatFile(f) {
|
|
459
|
+
const code = expandTabs(f.content);
|
|
460
|
+
const lines = code.split("\n");
|
|
461
|
+
const width = String(lines.length).length;
|
|
462
|
+
const body = lines.map((l, i) => `${String(i + 1).padStart(width)} | ${l}`).join("\n");
|
|
463
|
+
return `=== ${f.relPath} (${f.language}) ===
|
|
464
|
+
${body}`;
|
|
465
|
+
}
|
|
466
|
+
function formatChunk(chunk) {
|
|
467
|
+
return chunk.files.map(formatFile).join("\n\n");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/review/findings.ts
|
|
471
|
+
var SEVERITIES = ["critical", "warning", "info", "praise"];
|
|
472
|
+
var CONFIDENCES = ["high", "medium", "low"];
|
|
473
|
+
function asString(v) {
|
|
474
|
+
return typeof v === "string" && v.trim().length > 0 ? v.trim() : null;
|
|
475
|
+
}
|
|
476
|
+
function asInt(v) {
|
|
477
|
+
const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN;
|
|
478
|
+
return Number.isFinite(n) ? Math.round(n) : null;
|
|
479
|
+
}
|
|
480
|
+
function validateFindings(raw, files, idFrom = 1) {
|
|
481
|
+
const discarded = [];
|
|
482
|
+
if (!Array.isArray(raw)) {
|
|
483
|
+
return { findings: [], discarded: [{ reason: "\u914D\u5217\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3067\u3057\u305F" }] };
|
|
484
|
+
}
|
|
485
|
+
const byPath = new Map(files.map((f) => [f.relPath, f]));
|
|
486
|
+
const findings = [];
|
|
487
|
+
let seq = idFrom;
|
|
488
|
+
for (const item of raw) {
|
|
489
|
+
if (typeof item !== "object" || item === null) {
|
|
490
|
+
discarded.push({ reason: "\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u306F\u306A\u3044\u8981\u7D20" });
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const o = item;
|
|
494
|
+
const title = asString(o.title);
|
|
495
|
+
const problem = asString(o.problem);
|
|
496
|
+
const fileRaw = asString(o.file);
|
|
497
|
+
if (!title || !problem || !fileRaw) {
|
|
498
|
+
discarded.push({ reason: "title / problem / file \u306E\u3044\u305A\u308C\u304B\u304C\u7121\u3044" });
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const file = byPath.get(fileRaw)?.relPath ?? files.find((f) => f.relPath.endsWith(fileRaw) || fileRaw.endsWith(f.relPath))?.relPath;
|
|
502
|
+
if (!file) {
|
|
503
|
+
discarded.push({ reason: `\u5B58\u5728\u3057\u306A\u3044\u30D5\u30A1\u30A4\u30EB: ${fileRaw}` });
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
const target = byPath.get(file);
|
|
507
|
+
const lineCount = expandTabs(target.content).split("\n").length;
|
|
508
|
+
const startLine = asInt(o.startLine);
|
|
509
|
+
if (startLine === null || startLine < 1 || startLine > lineCount) {
|
|
510
|
+
discarded.push({ reason: `${file} \u306E\u884C\u756A\u53F7\u304C\u7BC4\u56F2\u5916: ${String(o.startLine)}` });
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
let endLine = asInt(o.endLine) ?? startLine;
|
|
514
|
+
if (endLine < startLine) endLine = startLine;
|
|
515
|
+
if (endLine > lineCount) endLine = lineCount;
|
|
516
|
+
const severity = SEVERITIES.includes(o.severity) ? o.severity : "info";
|
|
517
|
+
const confidence = CONFIDENCES.includes(o.confidence) ? o.confidence : "medium";
|
|
518
|
+
findings.push({
|
|
519
|
+
id: `F-${String(seq++).padStart(3, "0")}`,
|
|
520
|
+
severity,
|
|
521
|
+
category: asString(o.category) ?? "\u305D\u306E\u4ED6",
|
|
522
|
+
title,
|
|
523
|
+
file,
|
|
524
|
+
startLine,
|
|
525
|
+
endLine,
|
|
526
|
+
problem,
|
|
527
|
+
suggestion: asString(o.suggestion) ?? "",
|
|
528
|
+
highlightText: asString(o.highlightText) ?? void 0,
|
|
529
|
+
improvedCode: asString(o.improvedCode) ?? void 0,
|
|
530
|
+
confidence
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
return { findings, discarded };
|
|
534
|
+
}
|
|
535
|
+
function prioritize(findings, max) {
|
|
536
|
+
const order = { critical: 0, warning: 1, info: 2, praise: 3 };
|
|
537
|
+
const sorted = [...findings].sort((a, b) => order[a.severity] - order[b.severity]);
|
|
538
|
+
const praise = sorted.filter((f) => f.severity === "praise");
|
|
539
|
+
const rest = sorted.filter((f) => f.severity !== "praise");
|
|
540
|
+
const keepPraise = praise.slice(0, 1);
|
|
541
|
+
const keepRest = rest.slice(0, Math.max(0, max - keepPraise.length));
|
|
542
|
+
return [...keepRest, ...keepPraise];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/review/prompts.ts
|
|
546
|
+
var REVIEW_SYSTEM = `\u3042\u306A\u305F\u306F\u7D4C\u9A13\u8C4A\u5BCC\u306A\u30B3\u30FC\u30C9\u30EC\u30D3\u30E5\u30A2\u30FC\u3067\u3059\u3002\u65E5\u672C\u8A9E\u3067\u56DE\u7B54\u3057\u307E\u3059\u3002
|
|
547
|
+
|
|
548
|
+
# \u51FA\u529B\u5F62\u5F0F
|
|
549
|
+
JSON \u914D\u5217\u306E\u307F\u3092\u51FA\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u524D\u7F6E\u304D\u30FB\u5F8C\u66F8\u304D\u30FB\u30DE\u30FC\u30AF\u30C0\u30A6\u30F3\u306E\u30B3\u30FC\u30C9\u30D5\u30A7\u30F3\u30B9\u3092\u66F8\u3044\u3066\u306F\u3044\u3051\u307E\u305B\u3093\u3002
|
|
550
|
+
|
|
551
|
+
\u5404\u8981\u7D20\u306E\u5F62\u5F0F:
|
|
552
|
+
{
|
|
553
|
+
"severity": "critical" | "warning" | "info" | "praise",
|
|
554
|
+
"category": "\u30D0\u30B0" | "\u5B89\u5168\u6027" | "\u53EF\u8AAD\u6027" | "\u8A2D\u8A08" | "\u6027\u80FD" | "\u898F\u7D04" \u306E\u3044\u305A\u308C\u304B,
|
|
555
|
+
"title": "\u4E00\u884C\u8981\u7D04\uFF0840\u5B57\u4EE5\u5185\uFF09",
|
|
556
|
+
"file": "\u5165\u529B\u3067\u4E0E\u3048\u3089\u308C\u305F\u30D5\u30A1\u30A4\u30EB\u30D1\u30B9\u3068\u5B8C\u5168\u306B\u4E00\u81F4\u3059\u308B\u6587\u5B57\u5217",
|
|
557
|
+
"startLine": \u6307\u6458\u5BFE\u8C61\u306E\u958B\u59CB\u884C\u756A\u53F7\uFF08\u5165\u529B\u306B\u4ED8\u3044\u3066\u3044\u308B\u884C\u756A\u53F7\u3092\u305D\u306E\u307E\u307E\u4F7F\u3046\uFF09,
|
|
558
|
+
"endLine": \u6307\u6458\u5BFE\u8C61\u306E\u7D42\u4E86\u884C\u756A\u53F7,
|
|
559
|
+
"highlightText": "\u305D\u306E\u884C\u306B\u5B9F\u5728\u3059\u308B\u90E8\u5206\u6587\u5B57\u5217\u3002\u67A0\u3067\u56F2\u3093\u3067\u793A\u3059\u306E\u306B\u4F7F\u3046",
|
|
560
|
+
"problem": "\u4F55\u304C\u554F\u984C\u304B\u30022\u301C3\u6587",
|
|
561
|
+
"suggestion": "\u3069\u3046\u76F4\u3059\u304B\u30022\u301C3\u6587",
|
|
562
|
+
"improvedCode": "\u6539\u5584\u5F8C\u306E\u30B3\u30FC\u30C9\uFF08startLine\u301CendLine \u306E\u7BC4\u56F2\u3076\u3093\u3060\u3051\uFF09\u3002\u7121\u3051\u308C\u3070\u7701\u7565",
|
|
563
|
+
"confidence": "high" | "medium" | "low"
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
# \u5B88\u308B\u3053\u3068
|
|
567
|
+
1. file \u306F\u5FC5\u305A\u5165\u529B\u3067\u4E0E\u3048\u305F\u30D5\u30A1\u30A4\u30EB\u30D1\u30B9\u306E\u3044\u305A\u308C\u304B\u3068\u5B8C\u5168\u4E00\u81F4\u3055\u305B\u308B\u3002\u63A8\u6E2C\u3067\u66F8\u304B\u306A\u3044\u3002
|
|
568
|
+
2. startLine / endLine \u306F\u5165\u529B\u306B\u4ED8\u3044\u3066\u3044\u308B\u884C\u756A\u53F7\u3092\u305D\u306E\u307E\u307E\u4F7F\u3046\u3002\u6570\u3048\u76F4\u3055\u306A\u3044\u3002
|
|
569
|
+
3. highlightText \u306F\u305D\u306E\u884C\u306B**\u5B9F\u5728\u3059\u308B\u6587\u5B57\u5217**\u306B\u3059\u308B\u3002\u5B58\u5728\u3057\u306A\u3044\u6587\u5B57\u5217\u3092\u66F8\u304B\u306A\u3044\u3002
|
|
570
|
+
4. \u78BA\u4FE1\u304C\u6301\u3066\u306A\u3044\u3082\u306E\u306F confidence \u3092 "low" \u306B\u3059\u308B\u3002\u63A8\u6E2C\u3067\u65AD\u5B9A\u3057\u306A\u3044\u3002
|
|
571
|
+
5. \u4E00\u822C\u8AD6\u3092\u66F8\u304B\u306A\u3044\u3002\u300C\u5909\u6570\u540D\u306F\u5206\u304B\u308A\u3084\u3059\u304F\u300D\u3067\u306F\u306A\u304F\u3001\u305D\u306E\u30B3\u30FC\u30C9\u306E\u5177\u4F53\u7684\u306A\u7B87\u6240\u3092\u6307\u3059\u3002
|
|
572
|
+
6. **\u826F\u3044\u70B9\uFF08severity: "praise"\uFF09\u3092\u5FC5\u305A1\u4EF6\u4EE5\u4E0A\u542B\u3081\u308B\u3002**
|
|
573
|
+
7. \u30AD\u30E3\u30E9\u30AF\u30BF\u30FC\u3092\u6F14\u3058\u306A\u3044\u3002\u3053\u3053\u3067\u306F\u4E8B\u5B9F\u3060\u3051\u3092\u3001\u7D20\u306E\u65E5\u672C\u8A9E\u3067\u66F8\u304F\u3002`;
|
|
574
|
+
function reviewUserPrompt(formattedFiles, maxFindings) {
|
|
575
|
+
return `\u6B21\u306E\u30B3\u30FC\u30C9\u3092\u30EC\u30D3\u30E5\u30FC\u3057\u3001\u6307\u6458\u3092\u6700\u5927 ${maxFindings} \u4EF6\u3001JSON \u914D\u5217\u3067\u51FA\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002
|
|
576
|
+
\u91CD\u8981\u306A\u3082\u306E\u3092\u512A\u5148\u3057\u3001\u4E9B\u7D30\u306A\u3082\u306E\u306F\u7701\u3044\u3066\u304F\u3060\u3055\u3044\u3002
|
|
577
|
+
|
|
578
|
+
${formattedFiles}`;
|
|
579
|
+
}
|
|
580
|
+
function scenarioSystem(c) {
|
|
581
|
+
return `\u3042\u306A\u305F\u306F\u300C${c.nameJa}\u300D\u3068\u3044\u3046\u30AD\u30E3\u30E9\u30AF\u30BF\u30FC\u3068\u3057\u3066\u3001\u30B3\u30FC\u30C9\u30EC\u30D3\u30E5\u30FC\u306E\u5185\u5BB9\u3092\u8A9E\u308B\u53F0\u672C\u3092\u66F8\u304D\u307E\u3059\u3002
|
|
582
|
+
|
|
583
|
+
# \u30AD\u30E3\u30E9\u30AF\u30BF\u30FC
|
|
584
|
+
- \u540D\u524D: ${c.nameJa}
|
|
585
|
+
- \u4EBA\u7269\u50CF: ${c.persona}
|
|
586
|
+
- \u53E3\u8ABF: ${c.tone}
|
|
587
|
+
- \u53E3\u8ABF\u306E\u4F8B:
|
|
588
|
+
${c.sampleLines.map((l) => ` \u300C${l}\u300D`).join("\n")}
|
|
589
|
+
|
|
590
|
+
# \u51FA\u529B\u5F62\u5F0F
|
|
591
|
+
JSON \u914D\u5217\u306E\u307F\u3092\u51FA\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u524D\u7F6E\u304D\u30FB\u5F8C\u66F8\u304D\u30FB\u30B3\u30FC\u30C9\u30D5\u30A7\u30F3\u30B9\u3092\u66F8\u3044\u3066\u306F\u3044\u3051\u307E\u305B\u3093\u3002
|
|
592
|
+
|
|
593
|
+
\u5404\u8981\u7D20\u306E\u5F62\u5F0F:
|
|
594
|
+
{
|
|
595
|
+
"findingId": "\u5BFE\u8C61\u306E\u6307\u6458ID\uFF08\u30AA\u30FC\u30D7\u30CB\u30F3\u30B0\u3068\u30A8\u30F3\u30C7\u30A3\u30F3\u30B0\u306F\u7701\u7565\uFF09",
|
|
596
|
+
"role": "opening" | "present" | "problem" | "improve" | "summary" | "ending",
|
|
597
|
+
"text": "\u53F0\u8A5E",
|
|
598
|
+
"emotion": ${EMOTIONS.map((e) => `"${e}"`).join(" | ")}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
# \u5B88\u308B\u3053\u3068
|
|
602
|
+
1. **\u53F0\u8A5E\u306F\u5168\u89D2100\u6587\u5B57\u4EE5\u5185\u3002** 45\u301C80\u6587\u5B57\u304C\u6700\u3082\u8AAD\u307F\u3084\u3059\u3044\u3002\u6539\u884C\u306F \\n \u30671\u56DE\u307E\u3067\u3002
|
|
603
|
+
2. \u4E0A\u8A18\u306E\u30AD\u30E3\u30E9\u30AF\u30BF\u30FC\u306E\u53E3\u8ABF\u3067\u66F8\u304F\u3002\u53E3\u8ABF\u306E\u4F8B\u306B\u5BC4\u305B\u308B\u3002
|
|
604
|
+
3. **\u6280\u8853\u7684\u306A\u5185\u5BB9\u3092\u5909\u3048\u306A\u3044\u3002** \u6307\u6458\u306E\u610F\u5473\u306F\u5909\u3048\u305A\u3001\u8A00\u3044\u65B9\u3060\u3051\u3092\u30AD\u30E3\u30E9\u30AF\u30BF\u30FC\u306E\u3082\u306E\u306B\u3059\u308B\u3002
|
|
605
|
+
4. emotion \u306F\u4E0A\u306B\u6319\u3052\u305F\u3082\u306E\u304B\u3089\u306E\u307F\u9078\u3076\u3002
|
|
606
|
+
5. **\u53F0\u8A5E\u306B\u30D5\u30A1\u30A4\u30EB\u540D\u30FB\u884C\u756A\u53F7\u30FB\u8A18\u53F7\u3092\u66F8\u304B\u306A\u3044\u3002** \u97F3\u58F0\u3067\u8AAD\u307F\u4E0A\u3052\u308B\u305F\u3081\u3001
|
|
607
|
+
\u300C4\u884C\u76EE\u306E db.query \u304C\u300D\u306E\u3088\u3046\u306A\u66F8\u304D\u65B9\u3092\u3059\u308B\u3068\u805E\u304D\u82E6\u3057\u304F\u306A\u308B\u3002\u4F4D\u7F6E\u306F\u753B\u9762\u304C\u793A\u3059\u3002
|
|
608
|
+
6. confidence \u304C low \u306E\u6307\u6458\u306F\u3001\u65AD\u5B9A\u3092\u907F\u3051\u305F\u8A00\u3044\u56DE\u3057\u306B\u3059\u308B\u3002
|
|
609
|
+
|
|
610
|
+
# \u69CB\u6210
|
|
611
|
+
- \u6700\u521D\u306B role="opening" \u30921\u3064\u3002\u5168\u4F53\u3067\u4F55\u4EF6\u898B\u3064\u304B\u3063\u305F\u304B\u3092\u4F1D\u3048\u308B\u3002
|
|
612
|
+
- \u6307\u6458\u3054\u3068\u306B present \u2192 problem \u2192 improve \u2192 summary \u306E\u9806\u3067\u4E26\u3079\u308B\u3002
|
|
613
|
+
- severity \u304C "praise" \u306E\u6307\u6458\u306F problem \u3092\u4F5C\u3089\u306A\u3044\u3002
|
|
614
|
+
- improvedCode \u304C\u7121\u3044\u6307\u6458\u306F improve \u3092\u4F5C\u3089\u306A\u3044\u3002
|
|
615
|
+
- \u6700\u5F8C\u306B role="ending" \u30921\u3064\u3002\u826F\u3044\u70B9\u306B\u89E6\u308C\u3066\u7D42\u3048\u308B\u3002
|
|
616
|
+
|
|
617
|
+
# \u8868\u60C5\u306E\u4F7F\u3044\u5206\u3051
|
|
618
|
+
- normal: \u63D0\u793A\uFF08present\uFF09
|
|
619
|
+
- serious: \u91CD\u5927\u306A\u6307\u6458\uFF08critical\uFF09
|
|
620
|
+
- exasperated: \u96D1\u306A\u30B3\u30FC\u30C9\u3078\u306E\u6307\u6458
|
|
621
|
+
- thinking: \u6539\u5584\u6848\u306E\u63D0\u793A\uFF08improve\uFF09
|
|
622
|
+
- proud: \u307E\u3068\u3081\u30FB\u826F\u3044\u70B9\u30FB\u30A8\u30F3\u30C7\u30A3\u30F3\u30B0
|
|
623
|
+
- flustered: \u7279\u306B\u91CD\u5927\u306A\u767A\u898B
|
|
624
|
+
- sulking: \u4F7F\u7528\u983B\u5EA6\u306F\u4F4E\u3044`;
|
|
625
|
+
}
|
|
626
|
+
function scenarioUserPrompt(findings) {
|
|
627
|
+
const counts = findings.reduce((acc, f) => {
|
|
628
|
+
acc[f.severity] = (acc[f.severity] ?? 0) + 1;
|
|
629
|
+
return acc;
|
|
630
|
+
}, {});
|
|
631
|
+
const list = findings.map(
|
|
632
|
+
(f) => [
|
|
633
|
+
`## ${f.id}\uFF08${f.severity} / ${f.category} / \u78BA\u4FE1\u5EA6 ${f.confidence}\uFF09`,
|
|
634
|
+
`\u898B\u51FA\u3057: ${f.title}`,
|
|
635
|
+
`\u554F\u984C: ${f.problem}`,
|
|
636
|
+
f.suggestion ? `\u6539\u5584: ${f.suggestion}` : "",
|
|
637
|
+
f.improvedCode ? "\u6539\u5584\u5F8C\u306E\u30B3\u30FC\u30C9\u3042\u308A" : "\u6539\u5584\u5F8C\u306E\u30B3\u30FC\u30C9\u306A\u3057"
|
|
638
|
+
].filter(Boolean).join("\n")
|
|
639
|
+
).join("\n\n");
|
|
640
|
+
return `\u6B21\u306E\u30EC\u30D3\u30E5\u30FC\u7D50\u679C\u3092\u53F0\u672C\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002
|
|
641
|
+
|
|
642
|
+
\u5168\u4F53: ${findings.length}\u4EF6\uFF08${Object.entries(counts).map(([k, v]) => `${k} ${v}`).join(" / ")}\uFF09
|
|
643
|
+
|
|
644
|
+
${list}`;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/review/pipeline.ts
|
|
648
|
+
var SNIPPET_PAD = 2;
|
|
649
|
+
var SNIPPET_MAX_LINES = 12;
|
|
650
|
+
function buildCodeDisplay(f, file, source) {
|
|
651
|
+
const all = expandTabs(file.content).split("\n");
|
|
652
|
+
let startLine = Math.max(1, f.startLine - SNIPPET_PAD);
|
|
653
|
+
let endLine = Math.min(all.length, f.endLine + SNIPPET_PAD);
|
|
654
|
+
if (endLine - startLine + 1 > SNIPPET_MAX_LINES) {
|
|
655
|
+
const center = Math.round((f.startLine + f.endLine) / 2);
|
|
656
|
+
startLine = Math.max(1, center - Math.floor(SNIPPET_MAX_LINES / 2));
|
|
657
|
+
endLine = Math.min(all.length, startLine + SNIPPET_MAX_LINES - 1);
|
|
658
|
+
}
|
|
659
|
+
const highlights = [];
|
|
660
|
+
if (source === "improved") {
|
|
661
|
+
if (!f.improvedCode) return null;
|
|
662
|
+
const code2 = expandTabs(f.improvedCode);
|
|
663
|
+
const lines = code2.split("\n");
|
|
664
|
+
for (let i = 0; i < lines.length; i++) {
|
|
665
|
+
const t = lines[i];
|
|
666
|
+
if (t.trim().length === 0) continue;
|
|
667
|
+
const indent = t.length - t.trimStart().length;
|
|
668
|
+
highlights.push({
|
|
669
|
+
kind: "improvement",
|
|
670
|
+
startLine: startLine + i,
|
|
671
|
+
startColumn: indent + 1,
|
|
672
|
+
endLine: startLine + i,
|
|
673
|
+
endColumn: t.length + 1
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
return {
|
|
677
|
+
source,
|
|
678
|
+
snippet: { file: f.file, startLine, language: file.language, code: code2 },
|
|
679
|
+
// 行数が多いと枠だらけになるので、3 行までなら枠を出す
|
|
680
|
+
highlights: lines.length <= 3 ? highlights : []
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
const code = all.slice(startLine - 1, endLine).join("\n");
|
|
684
|
+
if (f.highlightText) {
|
|
685
|
+
const needle = expandTabs(f.highlightText);
|
|
686
|
+
for (let i = 0; i < endLine - startLine + 1; i++) {
|
|
687
|
+
const lineNo = startLine + i;
|
|
688
|
+
if (lineNo < f.startLine || lineNo > f.endLine) continue;
|
|
689
|
+
const at = all[lineNo - 1].indexOf(needle);
|
|
690
|
+
if (at < 0) continue;
|
|
691
|
+
highlights.push({
|
|
692
|
+
kind: "problem",
|
|
693
|
+
startLine: lineNo,
|
|
694
|
+
startColumn: at + 1,
|
|
695
|
+
endLine: lineNo,
|
|
696
|
+
endColumn: at + needle.length + 1
|
|
697
|
+
});
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (highlights.length === 0) {
|
|
702
|
+
const t = all[f.startLine - 1] ?? "";
|
|
703
|
+
if (t.trim().length > 0) {
|
|
704
|
+
const indent = t.length - t.trimStart().length;
|
|
705
|
+
highlights.push({
|
|
706
|
+
kind: "problem",
|
|
707
|
+
startLine: f.startLine,
|
|
708
|
+
startColumn: indent + 1,
|
|
709
|
+
endLine: f.startLine,
|
|
710
|
+
endColumn: t.length + 1
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return { source, snippet: { file: f.file, startLine, language: file.language, code }, highlights };
|
|
715
|
+
}
|
|
716
|
+
var ROLES = ["opening", "present", "problem", "improve", "summary", "ending"];
|
|
717
|
+
function assembleScenario(raw, findings, files, characterId, speaker, projectName) {
|
|
718
|
+
const warnings = [];
|
|
719
|
+
const byId = new Map(findings.map((f) => [f.id, f]));
|
|
720
|
+
const byPath = new Map(files.map((f) => [f.relPath, f]));
|
|
721
|
+
const beats = [];
|
|
722
|
+
let seq = 1;
|
|
723
|
+
if (!Array.isArray(raw)) {
|
|
724
|
+
warnings.push("\u53F0\u672C\u304C\u914D\u5217\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3067\u3057\u305F");
|
|
725
|
+
return {
|
|
726
|
+
scenario: {
|
|
727
|
+
schemaVersion: 1,
|
|
728
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
729
|
+
characterId,
|
|
730
|
+
title: projectName,
|
|
731
|
+
beats: []
|
|
732
|
+
},
|
|
733
|
+
warnings
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
for (const item of raw) {
|
|
737
|
+
const text = typeof item.text === "string" ? item.text.trim() : "";
|
|
738
|
+
if (!text) {
|
|
739
|
+
warnings.push("\u672C\u6587\u304C\u7121\u3044\u30D3\u30FC\u30C8\u3092\u98DB\u3070\u3057\u307E\u3057\u305F");
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const role = ROLES.includes(item.role) ? item.role : "present";
|
|
743
|
+
const emotion = EMOTIONS.includes(item.emotion) ? item.emotion : "normal";
|
|
744
|
+
const findingId = typeof item.findingId === "string" ? item.findingId : void 0;
|
|
745
|
+
const f = findingId ? byId.get(findingId) : void 0;
|
|
746
|
+
let stage = {};
|
|
747
|
+
if (role === "opening" || role === "ending") {
|
|
748
|
+
stage = { clearCode: true };
|
|
749
|
+
} else if (f) {
|
|
750
|
+
const file = byPath.get(f.file);
|
|
751
|
+
if (file) {
|
|
752
|
+
const src = role === "improve" ? "improved" : "original";
|
|
753
|
+
const disp = buildCodeDisplay(f, file, src);
|
|
754
|
+
if (disp) {
|
|
755
|
+
stage = {
|
|
756
|
+
code: role === "present" || role === "summary" ? { ...disp, highlights: [] } : disp
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
beats.push({
|
|
762
|
+
id: `B-${String(seq++).padStart(3, "0")}`,
|
|
763
|
+
findingId,
|
|
764
|
+
role,
|
|
765
|
+
speaker,
|
|
766
|
+
text,
|
|
767
|
+
emotion,
|
|
768
|
+
stage
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
const { scenario, issues } = validateScenario({
|
|
772
|
+
schemaVersion: 1,
|
|
773
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
774
|
+
characterId,
|
|
775
|
+
title: projectName,
|
|
776
|
+
beats
|
|
777
|
+
});
|
|
778
|
+
for (const i of issues) warnings.push(`${i.beatId}: ${i.message}`);
|
|
779
|
+
return { scenario, warnings };
|
|
780
|
+
}
|
|
781
|
+
async function runReview(dir, ai, character, opts = {}) {
|
|
782
|
+
const maxFindings = opts.maxFindings ?? 8;
|
|
783
|
+
const report = (p) => opts.onProgress?.(p);
|
|
784
|
+
const warnings = [];
|
|
785
|
+
let costUsd = 0;
|
|
786
|
+
report({ phase: "scan", done: 0, total: 1, message: "\u5BFE\u8C61\u306E\u30D5\u30A1\u30A4\u30EB\u3092\u6570\u3048\u3066\u3044\u307E\u3059" });
|
|
787
|
+
const scan = await scanProject(dir);
|
|
788
|
+
warnings.push(...scan.warnings);
|
|
789
|
+
if (scan.files.length === 0) {
|
|
790
|
+
throw new Error("\u30EC\u30D3\u30E5\u30FC\u3067\u304D\u308B\u30BD\u30FC\u30B9\u30B3\u30FC\u30C9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F");
|
|
791
|
+
}
|
|
792
|
+
const chunks = makeChunks(scan.files, opts.chunkTokens ?? 24e3);
|
|
793
|
+
const all = [];
|
|
794
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
795
|
+
if (opts.signal?.aborted) throw new Error("\u4E2D\u65AD\u3057\u307E\u3057\u305F");
|
|
796
|
+
report({
|
|
797
|
+
phase: "review",
|
|
798
|
+
done: i,
|
|
799
|
+
total: chunks.length,
|
|
800
|
+
// まとまりが 1 つなら実体を、複数なら**序数**で言う。
|
|
801
|
+
// 分母を出すと最後のまとまりで `3/3` になり、処理中なのに完了に見える。
|
|
802
|
+
message: chunks.length === 1 ? `${scan.files.length} \u30D5\u30A1\u30A4\u30EB\u30FB${scan.totalLines} \u884C\u3092\u8AAD\u3093\u3067\u3044\u307E\u3059` : `${i + 1} \u3064\u3081\u306E\u307E\u3068\u307E\u308A\u3092\u8AAD\u3093\u3067\u3044\u307E\u3059`
|
|
803
|
+
});
|
|
804
|
+
const res = await ai.complete(
|
|
805
|
+
{
|
|
806
|
+
purpose: "review",
|
|
807
|
+
system: REVIEW_SYSTEM,
|
|
808
|
+
user: reviewUserPrompt(formatChunk(chunks[i]), maxFindings)
|
|
809
|
+
},
|
|
810
|
+
opts.signal
|
|
811
|
+
);
|
|
812
|
+
costUsd += res.usage?.costUsd ?? 0;
|
|
813
|
+
const parsed = extractJson(res.text);
|
|
814
|
+
if (parsed === null) {
|
|
815
|
+
warnings.push(`${i + 1} \u756A\u76EE\u306E\u307E\u3068\u307E\u308A\u3067\u3001AI \u306E\u5FDC\u7B54\u3092\u89E3\u91C8\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F`);
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
const v = validateFindings(parsed, chunks[i].files, all.length + 1);
|
|
819
|
+
all.push(...v.findings);
|
|
820
|
+
if (v.discarded.length > 0) {
|
|
821
|
+
warnings.push(`${v.discarded.length} \u4EF6\u306E\u6307\u6458\u3092\u89E3\u91C8\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (all.length === 0) throw new Error("\u6307\u6458\u3092 1 \u4EF6\u3082\u53D6\u5F97\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F");
|
|
825
|
+
const findings = prioritize(all, maxFindings);
|
|
826
|
+
report({
|
|
827
|
+
phase: "scenario",
|
|
828
|
+
done: 0,
|
|
829
|
+
total: 1,
|
|
830
|
+
message: `\u6307\u6458 ${findings.length} \u4EF6\u3092\u7269\u8A9E\u306B\u7D44\u307F\u7ACB\u3066\u3066\u3044\u307E\u3059`
|
|
831
|
+
});
|
|
832
|
+
const sres = await ai.complete(
|
|
833
|
+
{
|
|
834
|
+
purpose: "scenario",
|
|
835
|
+
system: scenarioSystem(character),
|
|
836
|
+
user: scenarioUserPrompt(findings)
|
|
837
|
+
},
|
|
838
|
+
opts.signal
|
|
839
|
+
);
|
|
840
|
+
costUsd += sres.usage?.costUsd ?? 0;
|
|
841
|
+
const rawScenario = extractJson(sres.text);
|
|
842
|
+
const { scenario, warnings: sw } = assembleScenario(
|
|
843
|
+
rawScenario,
|
|
844
|
+
findings,
|
|
845
|
+
scan.files,
|
|
846
|
+
character.characterId,
|
|
847
|
+
character.nameJa,
|
|
848
|
+
scan.projectName
|
|
849
|
+
);
|
|
850
|
+
warnings.push(...sw);
|
|
851
|
+
if (scenario.beats.length === 0) throw new Error("\u53F0\u672C\u3092\u751F\u6210\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F");
|
|
852
|
+
report({
|
|
853
|
+
phase: "done",
|
|
854
|
+
done: 1,
|
|
855
|
+
total: 1,
|
|
856
|
+
message: `\u6307\u6458 ${findings.length} \u4EF6\u30FB${scenario.beats.length} \u53F0\u8A5E\u306E\u53F0\u672C\u304C\u3067\u304D\u307E\u3057\u305F`
|
|
857
|
+
});
|
|
858
|
+
return { scan, findings, scenario, warnings, costUsd };
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/index.ts
|
|
862
|
+
var DEFAULT_MODEL = "claude-sonnet-5";
|
|
863
|
+
var DEFAULT_MAX_FINDINGS = 6;
|
|
864
|
+
var HELP = `candy-debug ${package_default.version}
|
|
865
|
+
\u30ED\u30AD\u30B7\u30FC\u304C\u30B3\u30FC\u30C9\u30EC\u30D3\u30E5\u30FC\u3092\u3057\u3066\u3001\u30CE\u30D9\u30EB\u7528\u306E\u53F0\u672C\uFF08JSON\uFF09\u3092\u4F5C\u308A\u307E\u3059\u3002
|
|
866
|
+
|
|
867
|
+
\u4F7F\u3044\u65B9:
|
|
868
|
+
candy-debug <\u5BFE\u8C61\u30D5\u30A9\u30EB\u30C0> [\u30AA\u30D7\u30B7\u30E7\u30F3]
|
|
869
|
+
cdbg <\u5BFE\u8C61\u30D5\u30A9\u30EB\u30C0> [\u30AA\u30D7\u30B7\u30E7\u30F3] \uFF08\u77ED\u7E2E\u540D\u3002\u4E2D\u8EAB\u306F\u540C\u3058\uFF09
|
|
870
|
+
|
|
871
|
+
\u30AA\u30D7\u30B7\u30E7\u30F3:
|
|
872
|
+
-o, --output <file> \u7D50\u679C\u306E JSON \u3092\u66F8\u304D\u51FA\u3059\u30D5\u30A1\u30A4\u30EB\uFF08\u7701\u7565\u6642\u306F\u6A19\u6E96\u51FA\u529B\uFF09
|
|
873
|
+
--max-findings <n> \u6307\u6458\u306E\u6700\u5927\u4EF6\u6570\uFF08\u65E2\u5B9A ${DEFAULT_MAX_FINDINGS}\uFF09
|
|
874
|
+
--model <id> \u30EC\u30D3\u30E5\u30FC\u306B\u4F7F\u3046\u30E2\u30C7\u30EB\uFF08\u65E2\u5B9A ${DEFAULT_MODEL}\uFF09
|
|
875
|
+
--scenario-model <id> \u53F0\u672C\u3065\u304F\u308A\u306B\u4F7F\u3046\u30E2\u30C7\u30EB\uFF08\u65E2\u5B9A\u306F --model \u3068\u540C\u3058\uFF09
|
|
876
|
+
--claude <command> claude \u30B3\u30DE\u30F3\u30C9\u306E\u5834\u6240\uFF08\u65E2\u5B9A claude\uFF09
|
|
877
|
+
--progress <mode> \u9032\u6357\u306E\u51FA\u3057\u65B9: text | json | none\uFF08\u65E2\u5B9A text\uFF09
|
|
878
|
+
--scan-only AI \u3092\u547C\u3070\u305A\u3001\u5BFE\u8C61\u30D5\u30A1\u30A4\u30EB\u306E\u4E00\u89A7\u3068\u7D71\u8A08\u3060\u3051\u3092 JSON \u3067\u51FA\u3059
|
|
879
|
+
--doctor claude \u30B3\u30DE\u30F3\u30C9\u304C\u4F7F\u3048\u308B\u304B\u3092\u78BA\u304B\u3081\u308B
|
|
880
|
+
--json --doctor \u306E\u7D50\u679C\u3092 JSON \u3067\u51FA\u3059
|
|
881
|
+
-v, --version \u7248\u6570\u3092\u8868\u793A
|
|
882
|
+
-h, --help \u3053\u306E\u8AAC\u660E\u3092\u8868\u793A
|
|
883
|
+
|
|
884
|
+
\u6CE8\u610F:
|
|
885
|
+
\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9\u306F claude \u30B3\u30DE\u30F3\u30C9\u3092\u901A\u3058\u3066 Anthropic \u306E\u30B5\u30FC\u30D0\u3078\u9001\u3089\u308C\u307E\u3059\u3002
|
|
886
|
+
\u5BFE\u8C61\u30D5\u30A9\u30EB\u30C0\u3078\u306F\u4E00\u5207\u66F8\u304D\u8FBC\u307F\u307E\u305B\u3093\u3002
|
|
887
|
+
|
|
888
|
+
\u7D42\u4E86\u30B3\u30FC\u30C9: 0 \u6210\u529F / 1 \u5931\u6557 / 2 \u5F15\u6570\u306E\u8AA4\u308A / 130 \u4E2D\u65AD
|
|
889
|
+
`;
|
|
890
|
+
var UsageError = class extends Error {
|
|
891
|
+
};
|
|
892
|
+
var errorMode = "text";
|
|
893
|
+
function emit(mode, ev) {
|
|
894
|
+
if (mode === "json") process.stderr.write(JSON.stringify(ev) + "\n");
|
|
895
|
+
else if (mode === "text") {
|
|
896
|
+
if (ev.type === "progress") process.stderr.write(`[${ev.phase}] ${ev.message}
|
|
897
|
+
`);
|
|
898
|
+
else process.stderr.write(`\u30A8\u30E9\u30FC: ${ev.message}
|
|
899
|
+
`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
async function writeOutput(path, json) {
|
|
903
|
+
await mkdir(dirname(path), { recursive: true });
|
|
904
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
905
|
+
await writeFile(tmp, json, "utf8");
|
|
906
|
+
await rename(tmp, path);
|
|
907
|
+
}
|
|
908
|
+
function parseCli(argv) {
|
|
909
|
+
let parsed;
|
|
910
|
+
try {
|
|
911
|
+
parsed = parseArgs({
|
|
912
|
+
args: argv,
|
|
913
|
+
allowPositionals: true,
|
|
914
|
+
options: {
|
|
915
|
+
output: { type: "string", short: "o" },
|
|
916
|
+
"max-findings": { type: "string" },
|
|
917
|
+
model: { type: "string" },
|
|
918
|
+
"scenario-model": { type: "string" },
|
|
919
|
+
claude: { type: "string" },
|
|
920
|
+
progress: { type: "string" },
|
|
921
|
+
"scan-only": { type: "boolean" },
|
|
922
|
+
doctor: { type: "boolean" },
|
|
923
|
+
json: { type: "boolean" },
|
|
924
|
+
version: { type: "boolean", short: "v" },
|
|
925
|
+
help: { type: "boolean", short: "h" }
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
} catch (e) {
|
|
929
|
+
throw new UsageError(e instanceof Error ? e.message : String(e));
|
|
930
|
+
}
|
|
931
|
+
const v = parsed.values;
|
|
932
|
+
const progress = v.progress ?? "text";
|
|
933
|
+
if (!["text", "json", "none"].includes(progress)) {
|
|
934
|
+
throw new UsageError(`--progress \u306F text / json / none \u306E\u3044\u305A\u308C\u304B\u3067\u3059: ${v.progress}`);
|
|
935
|
+
}
|
|
936
|
+
let maxFindings = DEFAULT_MAX_FINDINGS;
|
|
937
|
+
if (v["max-findings"] !== void 0) {
|
|
938
|
+
maxFindings = Number(v["max-findings"]);
|
|
939
|
+
if (!Number.isInteger(maxFindings) || maxFindings < 1 || maxFindings > 30) {
|
|
940
|
+
throw new UsageError(`--max-findings \u306F 1\u301C30 \u306E\u6574\u6570\u3067\u3059: ${v["max-findings"]}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (parsed.positionals.length > 1) {
|
|
944
|
+
throw new UsageError(`\u5BFE\u8C61\u30D5\u30A9\u30EB\u30C0\u306F 1 \u3064\u3060\u3051\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044: ${parsed.positionals.join(" ")}`);
|
|
945
|
+
}
|
|
946
|
+
return {
|
|
947
|
+
dir: parsed.positionals[0],
|
|
948
|
+
output: v.output,
|
|
949
|
+
maxFindings,
|
|
950
|
+
model: v.model ?? DEFAULT_MODEL,
|
|
951
|
+
scenarioModel: v["scenario-model"],
|
|
952
|
+
claude: v.claude ?? "claude",
|
|
953
|
+
progress,
|
|
954
|
+
scanOnly: !!v["scan-only"],
|
|
955
|
+
doctor: !!v.doctor,
|
|
956
|
+
json: !!v.json,
|
|
957
|
+
version: !!v.version,
|
|
958
|
+
help: !!v.help
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
async function main(argv) {
|
|
962
|
+
const opts = parseCli(argv);
|
|
963
|
+
errorMode = opts.progress === "none" ? "text" : opts.progress;
|
|
964
|
+
if (opts.help) {
|
|
965
|
+
process.stdout.write(HELP);
|
|
966
|
+
return 0;
|
|
967
|
+
}
|
|
968
|
+
if (opts.version) {
|
|
969
|
+
process.stdout.write(`${package_default.version}
|
|
970
|
+
`);
|
|
971
|
+
return 0;
|
|
972
|
+
}
|
|
973
|
+
const ai = new ClaudeCliProvider({
|
|
974
|
+
command: opts.claude,
|
|
975
|
+
model: opts.model,
|
|
976
|
+
scenarioModel: opts.scenarioModel ?? opts.model
|
|
977
|
+
});
|
|
978
|
+
if (opts.doctor) {
|
|
979
|
+
const r = await ai.ping();
|
|
980
|
+
if (opts.json) process.stdout.write(JSON.stringify({ ...r, version: package_default.version }) + "\n");
|
|
981
|
+
else process.stdout.write(`${r.ok ? "OK" : "NG"}: ${r.detail}
|
|
982
|
+
`);
|
|
983
|
+
return r.ok ? 0 : 1;
|
|
984
|
+
}
|
|
985
|
+
if (!opts.dir) throw new UsageError("\u5BFE\u8C61\u30D5\u30A9\u30EB\u30C0\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF08candy-debug --help\uFF09");
|
|
986
|
+
const dir = resolve(opts.dir);
|
|
987
|
+
if (opts.scanOnly) {
|
|
988
|
+
const scan = await scanProject(dir);
|
|
989
|
+
const out = JSON.stringify(summarizeScan(scan), null, 2) + "\n";
|
|
990
|
+
if (opts.output) await writeOutput(resolve(opts.output), out);
|
|
991
|
+
else process.stdout.write(out);
|
|
992
|
+
return 0;
|
|
993
|
+
}
|
|
994
|
+
if (opts.progress === "text") {
|
|
995
|
+
process.stderr.write(
|
|
996
|
+
`\u5BFE\u8C61: ${dir}
|
|
997
|
+
\u30B3\u30FC\u30C9\u3092 claude \u30B3\u30DE\u30F3\u30C9\u3092\u901A\u3058\u3066 Anthropic \u306E\u30B5\u30FC\u30D0\u3078\u9001\u308A\u307E\u3059\u3002
|
|
998
|
+
`
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
const controller = new AbortController();
|
|
1002
|
+
const onSignal = () => controller.abort();
|
|
1003
|
+
process.once("SIGINT", onSignal);
|
|
1004
|
+
process.once("SIGTERM", onSignal);
|
|
1005
|
+
try {
|
|
1006
|
+
const outcome = await runReview(dir, ai, defaultCharacter(), {
|
|
1007
|
+
maxFindings: opts.maxFindings,
|
|
1008
|
+
signal: controller.signal,
|
|
1009
|
+
onProgress: (p) => emit(opts.progress, { type: "progress", ...p })
|
|
1010
|
+
});
|
|
1011
|
+
const file = {
|
|
1012
|
+
format: REVIEW_FILE_FORMAT,
|
|
1013
|
+
schemaVersion: REVIEW_FILE_VERSION,
|
|
1014
|
+
generator: { name: package_default.name, version: package_default.version },
|
|
1015
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1016
|
+
project: { name: outcome.scan.projectName, dir },
|
|
1017
|
+
findings: outcome.findings,
|
|
1018
|
+
scenario: outcome.scenario,
|
|
1019
|
+
warnings: outcome.warnings,
|
|
1020
|
+
costUsd: outcome.costUsd
|
|
1021
|
+
};
|
|
1022
|
+
const json = JSON.stringify(file, null, 2) + "\n";
|
|
1023
|
+
if (opts.output) {
|
|
1024
|
+
const out = resolve(opts.output);
|
|
1025
|
+
await writeOutput(out, json);
|
|
1026
|
+
if (opts.progress === "text") process.stderr.write(`\u66F8\u304D\u51FA\u3057\u307E\u3057\u305F: ${out}
|
|
1027
|
+
`);
|
|
1028
|
+
} else {
|
|
1029
|
+
process.stdout.write(json);
|
|
1030
|
+
}
|
|
1031
|
+
if (opts.progress === "text") {
|
|
1032
|
+
for (const w of outcome.warnings) process.stderr.write(`\u8B66\u544A: ${w}
|
|
1033
|
+
`);
|
|
1034
|
+
process.stderr.write(`\u8CBB\u7528\u306E\u76EE\u5B89: $${outcome.costUsd.toFixed(4)}
|
|
1035
|
+
`);
|
|
1036
|
+
}
|
|
1037
|
+
return 0;
|
|
1038
|
+
} catch (e) {
|
|
1039
|
+
if (controller.signal.aborted) {
|
|
1040
|
+
emit(opts.progress, { type: "error", message: "\u4E2D\u65AD\u3057\u307E\u3057\u305F" });
|
|
1041
|
+
return 130;
|
|
1042
|
+
}
|
|
1043
|
+
throw e;
|
|
1044
|
+
} finally {
|
|
1045
|
+
process.off("SIGINT", onSignal);
|
|
1046
|
+
process.off("SIGTERM", onSignal);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
main(process.argv.slice(2)).then(
|
|
1050
|
+
(code) => {
|
|
1051
|
+
process.exitCode = code;
|
|
1052
|
+
},
|
|
1053
|
+
(e) => {
|
|
1054
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1055
|
+
emit(errorMode, { type: "error", message });
|
|
1056
|
+
process.exitCode = e instanceof UsageError ? 2 : 1;
|
|
1057
|
+
}
|
|
1058
|
+
);
|
|
1059
|
+
export {
|
|
1060
|
+
main
|
|
1061
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "candy-debug",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ロキシーがコードレビューをして、ノベルゲーム用の台本(JSON)を作る CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"candy-debug": "dist/candy-debug.mjs",
|
|
8
|
+
"cdbg": "dist/candy-debug.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --loader:.csv=text --banner:js=\"#!/usr/bin/env node\" --outfile=dist/candy-debug.mjs --log-level=warning",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"code-review",
|
|
24
|
+
"claude",
|
|
25
|
+
"cli",
|
|
26
|
+
"visual-novel"
|
|
27
|
+
],
|
|
28
|
+
"author": "ryome",
|
|
29
|
+
"license": "MIT"
|
|
30
|
+
}
|