@dev-guard/cli 0.1.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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +79 -0
  3. package/dist/agent-strategies.d.ts +23 -0
  4. package/dist/agent-strategies.js +130 -0
  5. package/dist/agent-strategies.js.map +1 -0
  6. package/dist/ai-context.d.ts +10 -0
  7. package/dist/ai-context.js +143 -0
  8. package/dist/ai-context.js.map +1 -0
  9. package/dist/check.d.ts +6 -0
  10. package/dist/check.js +89 -0
  11. package/dist/check.js.map +1 -0
  12. package/dist/clipboard.d.ts +6 -0
  13. package/dist/clipboard.js +43 -0
  14. package/dist/clipboard.js.map +1 -0
  15. package/dist/codex-notify.d.ts +23 -0
  16. package/dist/codex-notify.js +146 -0
  17. package/dist/codex-notify.js.map +1 -0
  18. package/dist/command-targets.d.ts +10 -0
  19. package/dist/command-targets.js +124 -0
  20. package/dist/command-targets.js.map +1 -0
  21. package/dist/config.d.ts +22 -0
  22. package/dist/config.js +180 -0
  23. package/dist/config.js.map +1 -0
  24. package/dist/configure.d.ts +1 -0
  25. package/dist/configure.js +79 -0
  26. package/dist/configure.js.map +1 -0
  27. package/dist/doctor.d.ts +1 -0
  28. package/dist/doctor.js +326 -0
  29. package/dist/doctor.js.map +1 -0
  30. package/dist/drift-telemetry.d.ts +13 -0
  31. package/dist/drift-telemetry.js +64 -0
  32. package/dist/drift-telemetry.js.map +1 -0
  33. package/dist/effective-task.d.ts +44 -0
  34. package/dist/effective-task.js +355 -0
  35. package/dist/effective-task.js.map +1 -0
  36. package/dist/fs.d.ts +10 -0
  37. package/dist/fs.js +58 -0
  38. package/dist/fs.js.map +1 -0
  39. package/dist/git.d.ts +24 -0
  40. package/dist/git.js +235 -0
  41. package/dist/git.js.map +1 -0
  42. package/dist/hooks.d.ts +39 -0
  43. package/dist/hooks.js +513 -0
  44. package/dist/hooks.js.map +1 -0
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.js +555 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/infer-task.d.ts +1 -0
  49. package/dist/infer-task.js +120 -0
  50. package/dist/infer-task.js.map +1 -0
  51. package/dist/init.d.ts +1 -0
  52. package/dist/init.js +50 -0
  53. package/dist/init.js.map +1 -0
  54. package/dist/install-agent-instructions.d.ts +1 -0
  55. package/dist/install-agent-instructions.js +113 -0
  56. package/dist/install-agent-instructions.js.map +1 -0
  57. package/dist/migration.d.ts +8 -0
  58. package/dist/migration.js +43 -0
  59. package/dist/migration.js.map +1 -0
  60. package/dist/paths.d.ts +38 -0
  61. package/dist/paths.js +41 -0
  62. package/dist/paths.js.map +1 -0
  63. package/dist/project-detection.d.ts +10 -0
  64. package/dist/project-detection.js +144 -0
  65. package/dist/project-detection.js.map +1 -0
  66. package/dist/project-identity.d.ts +7 -0
  67. package/dist/project-identity.js +93 -0
  68. package/dist/project-identity.js.map +1 -0
  69. package/dist/project-memory.d.ts +4 -0
  70. package/dist/project-memory.js +32 -0
  71. package/dist/project-memory.js.map +1 -0
  72. package/dist/prompt.d.ts +13 -0
  73. package/dist/prompt.js +125 -0
  74. package/dist/prompt.js.map +1 -0
  75. package/dist/refresh.d.ts +15 -0
  76. package/dist/refresh.js +146 -0
  77. package/dist/refresh.js.map +1 -0
  78. package/dist/report.d.ts +1 -0
  79. package/dist/report.js +109 -0
  80. package/dist/report.js.map +1 -0
  81. package/dist/review.d.ts +2 -0
  82. package/dist/review.js +653 -0
  83. package/dist/review.js.map +1 -0
  84. package/dist/rule-filter.d.ts +8 -0
  85. package/dist/rule-filter.js +79 -0
  86. package/dist/rule-filter.js.map +1 -0
  87. package/dist/runs.d.ts +21 -0
  88. package/dist/runs.js +142 -0
  89. package/dist/runs.js.map +1 -0
  90. package/dist/runtime-state.d.ts +69 -0
  91. package/dist/runtime-state.js +1383 -0
  92. package/dist/runtime-state.js.map +1 -0
  93. package/dist/scan.d.ts +1 -0
  94. package/dist/scan.js +55 -0
  95. package/dist/scan.js.map +1 -0
  96. package/dist/self.d.ts +3 -0
  97. package/dist/self.js +235 -0
  98. package/dist/self.js.map +1 -0
  99. package/dist/task-ai.d.ts +1 -0
  100. package/dist/task-ai.js +643 -0
  101. package/dist/task-ai.js.map +1 -0
  102. package/dist/telemetry.d.ts +1 -0
  103. package/dist/telemetry.js +11 -0
  104. package/dist/telemetry.js.map +1 -0
  105. package/dist/update.d.ts +6 -0
  106. package/dist/update.js +154 -0
  107. package/dist/update.js.map +1 -0
  108. package/dist/watch.d.ts +1 -0
  109. package/dist/watch.js +303 -0
  110. package/dist/watch.js.map +1 -0
  111. package/package.json +31 -0
package/dist/review.js ADDED
@@ -0,0 +1,653 @@
1
+ import { defaultConfig, analyzeGeneratedDiffDrift, filterDiffTextForFiles, formatInferredDiffIntentClusters, inferredIntentToRequirement, inferredIntentToTaskType, scoreWorkflowQuality, filterDevGuardContextFiles, buildReviewFixPrompt, buildImpactHints, generateReviewResult, isAlwaysIgnoredContextPath, NoneAIProvider, OpenAIProvider } from "@dev-guard/core";
2
+ import { copyTextToClipboard } from "./clipboard.js";
3
+ import { loadConfig, readOpenAIApiKey } from "./config.js";
4
+ import { fromRoot, readJsonFile, readTextFile, writeTextFile } from "./fs.js";
5
+ import { getCommitGitChanges, getDiffForChangeFiles, getGitChanges, getStagedGitChanges } from "./git.js";
6
+ import { formatProjectIdentityWarning, loadCurrentProjectIdentity, readStoredProjectIdentity, sameProjectIdentity } from "./project-identity.js";
7
+ import { filterRelevantMarkdown } from "./rule-filter.js";
8
+ import { updateRunLog } from "./runs.js";
9
+ import { recordDriftTelemetry } from "./drift-telemetry.js";
10
+ import { formatEffectiveTaskBasis, formatEffectiveRunSummary, formatEffectiveTaskContext, resolveEffectiveTaskContext } from "./effective-task.js";
11
+ const untrackedFileLimit = 5;
12
+ const perFileCharacterLimit = 4000;
13
+ const totalUntrackedCharacterLimit = 12000;
14
+ export async function runReview(root, args) {
15
+ const options = parseReviewOptions(args);
16
+ const review = await executeReview(root, { ...options });
17
+ if (!review) {
18
+ return;
19
+ }
20
+ const fixPrompt = buildReviewFixPrompt({
21
+ review: review.result,
22
+ taskMarkdown: review.taskMarkdown,
23
+ changedFiles: review.changedFiles,
24
+ changeFiles: review.changeFiles
25
+ });
26
+ if (options.output) {
27
+ await writeTextFile(fromRoot(root, options.output), `${review.result.markdown}\n`);
28
+ console.error(`dev-guard review: wrote ${options.output}`);
29
+ }
30
+ if (options.copyFix) {
31
+ const copyResult = await copyTextToClipboard(review.result.fixPrompt);
32
+ if (copyResult.ok) {
33
+ console.error("dev-guard review: copied Codex 재수정 프롬프트 to clipboard.");
34
+ }
35
+ else {
36
+ console.error(`dev-guard review: clipboard copy failed (${copyResult.reason}).`);
37
+ }
38
+ }
39
+ if (options.fixPrompt && options.copy) {
40
+ const copyResult = await copyTextToClipboard(fixPrompt.promptText);
41
+ if (copyResult.ok) {
42
+ console.error("dev-guard review: copied generated fix prompt to clipboard.");
43
+ }
44
+ else {
45
+ console.error(`dev-guard review: fix prompt clipboard copy failed (${copyResult.reason}).`);
46
+ }
47
+ }
48
+ if (review.runLog) {
49
+ await updateRunLog(root, review.runLog.id, {
50
+ status: "reviewed",
51
+ reviewResult: review.result.markdown,
52
+ fixPrompt: fixPrompt.promptText,
53
+ targetFiles: review.changedFiles,
54
+ reason: review.result.status
55
+ });
56
+ }
57
+ console.log(review.result.markdown);
58
+ if (options.fixPrompt) {
59
+ console.log("\n---\n");
60
+ console.log(fixPrompt.promptText);
61
+ }
62
+ }
63
+ export async function runFixPrompt(root, args) {
64
+ const options = parseFixPromptOptions(args);
65
+ const review = await executeReview(root, {
66
+ includeContextFiles: options.includeContextFiles,
67
+ staged: options.staged,
68
+ commit: options.commit,
69
+ runId: options.runId,
70
+ noRun: options.noRun
71
+ });
72
+ if (!review) {
73
+ return;
74
+ }
75
+ const fixPrompt = buildReviewFixPrompt({
76
+ review: review.result,
77
+ taskMarkdown: review.taskMarkdown,
78
+ changedFiles: review.changedFiles,
79
+ changeFiles: review.changeFiles
80
+ });
81
+ if (options.output) {
82
+ await writeTextFile(fromRoot(root, options.output), `${fixPrompt.promptText}\n`);
83
+ console.error(`dev-guard fix-prompt: wrote ${options.output}`);
84
+ }
85
+ if (options.copy) {
86
+ const copyResult = await copyTextToClipboard(fixPrompt.promptText);
87
+ if (copyResult.ok) {
88
+ console.error("dev-guard fix-prompt: copied to clipboard.");
89
+ }
90
+ else {
91
+ console.error(`dev-guard fix-prompt: clipboard copy failed (${copyResult.reason}).`);
92
+ }
93
+ }
94
+ if (review.runLog) {
95
+ await updateRunLog(root, review.runLog.id, {
96
+ status: "reviewed",
97
+ reviewResult: review.result.markdown,
98
+ fixPrompt: fixPrompt.promptText,
99
+ targetFiles: review.changedFiles,
100
+ reason: review.result.status
101
+ });
102
+ }
103
+ console.log(fixPrompt.promptText);
104
+ }
105
+ async function executeReview(root, options) {
106
+ const rawGitChanges = await loadReviewGitChanges(root, options);
107
+ const reviewChangeFiles = filterDevGuardContextFiles(rawGitChanges.changeFiles, options.includeContextFiles);
108
+ const reviewChangedFiles = [...new Set(reviewChangeFiles.map((file) => file.path))].sort();
109
+ if (reviewChangeFiles.length === 0) {
110
+ console.log("dev-guard review");
111
+ console.log("");
112
+ console.log("리뷰할 변경 사항 없음");
113
+ if (rawGitChanges.changeFiles.length > 0 && !options.includeContextFiles) {
114
+ console.log("Context files changed only. Include them with --include-context-files.");
115
+ }
116
+ return undefined;
117
+ }
118
+ const resolvedConfig = await loadConfig(root);
119
+ const config = resolvedConfig.config;
120
+ const providerName = config.ai?.provider ?? defaultConfig.ai.provider ?? "none";
121
+ const model = config.ai?.model ?? defaultConfig.ai.model ?? "gpt-4o-mini";
122
+ const openAIApiKey = readOpenAIApiKey();
123
+ const currentIdentity = await loadCurrentProjectIdentity(root).catch(() => undefined);
124
+ const [taskMarkdown, rulesMarkdown, mistakesMarkdown, projectStateMarkdown, decisionsMarkdown, memory, diffText, codeGraph] = await Promise.all([
125
+ readTextFile(fromRoot(root, ".devguard/task.md")),
126
+ readTextFile(fromRoot(root, ".devguard/rules.md")),
127
+ readTextFile(fromRoot(root, ".devguard/mistakes.md")),
128
+ readTextFile(fromRoot(root, "docs/PROJECT_STATE.md")),
129
+ readTextFile(fromRoot(root, "docs/DECISIONS.md")),
130
+ loadReviewMemory(root, reviewChangedFiles, currentIdentity),
131
+ getDiffForChangeFiles(root, reviewChangeFiles, { stagedOnly: options.staged, commitRef: options.commit }),
132
+ readJsonFile(fromRoot(root, ".devguard/code-graph.json"), [])
133
+ ]);
134
+ const impactHints = buildImpactHints(reviewChangedFiles, codeGraph);
135
+ const effective = await resolveEffectiveTaskContext({
136
+ root,
137
+ taskMarkdown,
138
+ gitChanges: { diffText, changedFiles: reviewChangedFiles, changeFiles: reviewChangeFiles },
139
+ reviewChangeFiles,
140
+ changedFiles: reviewChangedFiles,
141
+ codeGraph,
142
+ currentIdentity,
143
+ options: {
144
+ forceTask: options.forceTask,
145
+ fromDiff: options.fromDiff,
146
+ noRun: options.noRun,
147
+ runId: options.runId
148
+ }
149
+ });
150
+ for (const line of formatEffectiveTaskContext("dev-guard review", effective)) {
151
+ console.error(line);
152
+ }
153
+ for (const line of formatEffectiveTaskBasis(effective)) {
154
+ console.error(`dev-guard review: ${line}`);
155
+ }
156
+ if (effective.runSelection.warning) {
157
+ console.error(`dev-guard review: warning: ${effective.runSelection.warning}`);
158
+ }
159
+ const inferredClusters = effective.inferredTask;
160
+ const inferredIntent = inferredClusters.primaryIntent;
161
+ const effectiveTaskMarkdown = effective.effectiveTaskMarkdown;
162
+ // Step 6: Filter rules against effective task
163
+ const ruleRelevanceText = [effectiveTaskMarkdown, reviewChangedFiles.join("\n"), projectStateMarkdown, decisionsMarkdown].join("\n");
164
+ const filteredRules = filterRelevantMarkdown(rulesMarkdown, ruleRelevanceText, currentIdentity);
165
+ const filteredMistakes = filterRelevantMarkdown(mistakesMarkdown, ruleRelevanceText, currentIdentity);
166
+ const untrackedFileContexts = await collectUntrackedFileContexts(root, reviewChangeFiles.filter((file) => file.source === "untracked").map((file) => file.path), extractReviewKeywords(effectiveTaskMarkdown));
167
+ // Effective run: excluded when diff-first to prevent old requirement contamination
168
+ const effectiveRunLog = effective.effectiveRunLog;
169
+ const effectiveRunSummary = formatEffectiveRunSummary(effective);
170
+ if (options.heuristic || providerName === "none") {
171
+ console.error("dev-guard review");
172
+ console.error("- review mode: heuristic");
173
+ console.error(`- provider: ${providerName}`);
174
+ console.error(`- model: ${model}`);
175
+ console.error(`- config source: ${resolvedConfig.source}`);
176
+ console.error(`- API key source: ${resolvedConfig.env.apiKey.selectedKey ?? "none"}`);
177
+ if (providerName === "none" && !options.heuristic) {
178
+ console.error("- note: AI provider is none; using local heuristic fallback. Use --heuristic to request this explicitly.");
179
+ }
180
+ const heuristicResult = generateHeuristicReview({
181
+ changedFiles: reviewChangedFiles,
182
+ changeFiles: reviewChangeFiles,
183
+ diffText,
184
+ taskMarkdown: effectiveTaskMarkdown,
185
+ inferredClusters,
186
+ taskAvailable: effective.useTaskMarkdown,
187
+ anchorStale: !effective.useTaskMarkdown && (effective.anchorStatus === "stale" || effective.mode === "diff-first_uncertain"),
188
+ anchorAbsent: !effective.useTaskMarkdown && effective.anchorStatus === "absent",
189
+ anchorMode: effective.mode,
190
+ untrackedFileContexts,
191
+ impactHints,
192
+ providerName
193
+ });
194
+ const drift = analyzeGeneratedDiffDrift({
195
+ requirementText: effective.useTaskMarkdown ? taskMarkdown : inferredIntentToRequirement(inferredIntent),
196
+ taskMarkdown: effective.useTaskMarkdown ? taskMarkdown : "",
197
+ diffText: filterDiffTextForFiles(diffText, inferredIntent.changedFiles),
198
+ changedFiles: inferredIntent.changedFiles,
199
+ changeFiles: reviewChangeFiles.filter((file) => inferredIntent.changedFiles.includes(file.path)),
200
+ taskType: effective.useTaskMarkdown ? undefined : inferredIntentToTaskType(inferredIntent)
201
+ });
202
+ await recordDriftTelemetry(root, {
203
+ result: drift,
204
+ source: "review:heuristic",
205
+ subtype: extractTaskSubtype(effective.useTaskMarkdown ? taskMarkdown : effectiveTaskMarkdown)
206
+ }).catch((error) => console.error(`dev-guard review: telemetry warning: ${errorMessage(error)}`));
207
+ return {
208
+ result: heuristicResult,
209
+ taskMarkdown: effectiveTaskMarkdown,
210
+ changedFiles: reviewChangedFiles,
211
+ changeFiles: reviewChangeFiles,
212
+ runLog: effectiveRunLog
213
+ };
214
+ }
215
+ if (providerName === "openai" && !openAIApiKey) {
216
+ if (options.heuristic) {
217
+ // Unreachable because heuristic returns above, but keeps the error path explicit.
218
+ throw new Error("OpenAI API key 환경변수가 없습니다. heuristic review에는 API key가 필요 없습니다.");
219
+ }
220
+ throw new Error("OpenAI API key 환경변수가 없습니다. API key는 config에 저장하지 말고 `DEV_GUARD_OPENAI_API_KEY` 또는 `OPENAI_API_KEY`로 설정해 주세요. 로컬 검토는 `dev-guard review --heuristic`를 사용하세요.");
221
+ }
222
+ const provider = providerName === "openai"
223
+ ? new OpenAIProvider({
224
+ apiKey: openAIApiKey ?? "",
225
+ model,
226
+ temperature: config.ai?.temperature,
227
+ maxTokens: config.ai?.maxTokens,
228
+ reasoningEffort: config.ai?.reasoningEffort,
229
+ baseURL: config.ai?.baseURL
230
+ })
231
+ : new NoneAIProvider();
232
+ const result = await generateReviewResult(provider, {
233
+ taskMarkdown: effectiveTaskMarkdown,
234
+ rulesMarkdown: filteredRules.filteredMarkdown,
235
+ mistakesMarkdown: filteredMistakes.filteredMarkdown,
236
+ projectStateMarkdown,
237
+ decisionsMarkdown,
238
+ changedFiles: reviewChangedFiles,
239
+ changeFiles: reviewChangeFiles,
240
+ diffText,
241
+ untrackedFileContexts,
242
+ memorySummaries: memory.summaries,
243
+ projectMapMarkdown: memory.projectMapMarkdown,
244
+ runLog: effectiveRunLog,
245
+ runSelectionSummary: effectiveRunSummary,
246
+ impactHints
247
+ }, model);
248
+ const drift = analyzeGeneratedDiffDrift({
249
+ requirementText: effective.useTaskMarkdown ? taskMarkdown : inferredIntentToRequirement(inferredIntent),
250
+ taskMarkdown: effective.useTaskMarkdown ? taskMarkdown : "",
251
+ diffText: filterDiffTextForFiles(diffText, inferredIntent.changedFiles),
252
+ changedFiles: inferredIntent.changedFiles,
253
+ changeFiles: reviewChangeFiles.filter((file) => inferredIntent.changedFiles.includes(file.path)),
254
+ taskType: effective.useTaskMarkdown ? undefined : inferredIntentToTaskType(inferredIntent)
255
+ });
256
+ await recordDriftTelemetry(root, {
257
+ result: drift,
258
+ source: "review:ai",
259
+ subtype: extractTaskSubtype(effective.useTaskMarkdown ? taskMarkdown : effectiveTaskMarkdown)
260
+ }).catch((error) => console.error(`dev-guard review: telemetry warning: ${errorMessage(error)}`));
261
+ return {
262
+ result,
263
+ taskMarkdown: effectiveTaskMarkdown,
264
+ changedFiles: reviewChangedFiles,
265
+ changeFiles: reviewChangeFiles,
266
+ runLog: effectiveRunLog
267
+ };
268
+ }
269
+ function parseReviewOptions(args) {
270
+ const output = readOption(args, "--output");
271
+ const copyFix = args.includes("--copy-fix");
272
+ const copy = args.includes("--copy");
273
+ const fixPrompt = args.includes("--fix-prompt");
274
+ const includeContextFiles = args.includes("--include-context-files");
275
+ const staged = args.includes("--staged");
276
+ const commit = readOption(args, "--commit");
277
+ const runId = readOption(args, "--run");
278
+ const noRun = args.includes("--no-run");
279
+ const heuristic = args.includes("--heuristic");
280
+ const forceTask = args.includes("--task");
281
+ const fromDiff = args.includes("--from-diff");
282
+ if (staged && commit) {
283
+ throw new Error("--staged와 --commit은 함께 사용할 수 없습니다.");
284
+ }
285
+ if (noRun && runId) {
286
+ throw new Error("--no-run과 --run은 함께 사용할 수 없습니다.");
287
+ }
288
+ if (forceTask && fromDiff) {
289
+ throw new Error("--task와 --from-diff는 함께 사용할 수 없습니다.");
290
+ }
291
+ return {
292
+ output,
293
+ copyFix,
294
+ copy,
295
+ fixPrompt,
296
+ includeContextFiles,
297
+ staged,
298
+ commit,
299
+ runId,
300
+ noRun,
301
+ heuristic,
302
+ forceTask,
303
+ fromDiff
304
+ };
305
+ }
306
+ function parseFixPromptOptions(args) {
307
+ const output = readOption(args, "--output");
308
+ const copy = args.includes("--copy");
309
+ const includeContextFiles = args.includes("--include-context-files");
310
+ const staged = args.includes("--staged");
311
+ const commit = readOption(args, "--commit");
312
+ const runId = readOption(args, "--run");
313
+ const noRun = args.includes("--no-run");
314
+ if (staged && commit) {
315
+ throw new Error("--staged와 --commit은 함께 사용할 수 없습니다.");
316
+ }
317
+ if (noRun && runId) {
318
+ throw new Error("--no-run과 --run은 함께 사용할 수 없습니다.");
319
+ }
320
+ return {
321
+ output,
322
+ copy,
323
+ includeContextFiles,
324
+ staged,
325
+ commit,
326
+ runId,
327
+ noRun
328
+ };
329
+ }
330
+ function generateHeuristicReview(input) {
331
+ const findings = [];
332
+ const primaryFiles = input.inferredClusters?.primaryIntent.changedFiles ?? input.changedFiles;
333
+ const primaryChangeFiles = input.changeFiles.filter((file) => primaryFiles.includes(file.path));
334
+ const primaryDiff = input.inferredClusters ? filterDiffTextForFiles(input.diffText, primaryFiles) : input.diffText;
335
+ const drift = analyzeGeneratedDiffDrift({
336
+ requirementText: input.taskAvailable || !input.inferredClusters ? input.taskMarkdown : inferredIntentToRequirement(input.inferredClusters.primaryIntent),
337
+ taskMarkdown: input.taskAvailable || !input.inferredClusters ? input.taskMarkdown : "",
338
+ diffText: primaryDiff,
339
+ changedFiles: primaryFiles,
340
+ changeFiles: primaryChangeFiles,
341
+ taskType: input.taskAvailable || !input.inferredClusters ? undefined : inferredIntentToTaskType(input.inferredClusters.primaryIntent)
342
+ });
343
+ const quality = scoreWorkflowQuality({
344
+ drift,
345
+ changedFiles: primaryFiles,
346
+ diffText: primaryDiff,
347
+ changeFiles: primaryChangeFiles
348
+ });
349
+ const addedLines = input.diffText
350
+ .split("\n")
351
+ .filter((line) => line.startsWith("+") && !line.startsWith("+++"))
352
+ .map((line) => line.slice(1));
353
+ const removedLines = input.diffText
354
+ .split("\n")
355
+ .filter((line) => line.startsWith("-") && !line.startsWith("---"))
356
+ .map((line) => line.slice(1));
357
+ const taskType = extractTaskType(input.taskMarkdown);
358
+ const largeChange = input.changedFiles.length >= 12 || input.diffText.length > 30000;
359
+ const hasMarkdownMarkerProblem = /dev-guard:update:start/.test(input.diffText) && !/dev-guard:update:end/.test(input.diffText);
360
+ const i18nMigration = taskType === "i18n" ? detectLocaleResourceMigration(input.changedFiles, input.diffText, addedLines, removedLines) : false;
361
+ const addedKoreanOutsideLocale = taskType === "i18n" &&
362
+ addedLines.some((line) => /[가-힣]/.test(line)) &&
363
+ !input.changedFiles.some((file) => /(^|\/)(messages|locales?|i18n)\//i.test(file)) &&
364
+ !i18nMigration;
365
+ const duplicateHeadings = detectDuplicateMarkdownHeadings([...addedLines, ...input.untrackedFileContexts.map((context) => context.excerpt).join("\n")]);
366
+ if (largeChange) {
367
+ findings.push({ severity: "warning", text: "변경 파일 수나 diff 크기가 큽니다. 이번 task 범위 안의 변경인지 확인하세요." });
368
+ }
369
+ if (input.changeFiles.some((file) => file.source === "untracked")) {
370
+ findings.push({ severity: "info", text: "untracked 파일이 포함되어 있습니다. 새 파일이 commit 대상인지 확인하세요." });
371
+ }
372
+ if (addedKoreanOutsideLocale) {
373
+ findings.push({ severity: "critical", text: "i18n 작업에서 locale resource 밖에 한국어 사용자 노출 문자열이 추가된 정황이 있습니다." });
374
+ }
375
+ if (i18nMigration) {
376
+ findings.push({ severity: "info", text: "Korean copy moved to locale resource; default locale preserved." });
377
+ }
378
+ if (hasMarkdownMarkerProblem) {
379
+ findings.push({ severity: "critical", text: "dev-guard managed markdown marker가 깨진 정황이 있습니다." });
380
+ }
381
+ if (duplicateHeadings.length > 0) {
382
+ findings.push({ severity: "warning", text: `중복 markdown heading 후보: ${duplicateHeadings.slice(0, 5).join(", ")}` });
383
+ }
384
+ if (removedLines.length > 120 && addedLines.length < 10) {
385
+ findings.push({ severity: "warning", text: "삭제 라인이 많고 대체 추가가 적습니다. 의도한 삭제인지 확인하세요." });
386
+ }
387
+ if (drift.severity !== "low") {
388
+ findings.push({
389
+ severity: drift.severity === "high" ? "critical" : "warning",
390
+ text: `Potential drift detected: ${drift.reasons.join("; ") || "domain mismatch"} (score ${drift.driftScore})`
391
+ });
392
+ }
393
+ const actionableMixedDetails = input.inferredClusters?.secondaryDetails.filter((detail) => detail.severity !== "info") ?? [];
394
+ if (input.inferredClusters && actionableMixedDetails.length > 0) {
395
+ findings.push({
396
+ severity: input.inferredClusters.mixedRisk === "high" ? "warning" : "info",
397
+ text: `mixed diff clusters: ${formatInferredDiffIntentClusters(input.inferredClusters)}`
398
+ });
399
+ }
400
+ for (const hint of input.impactHints.filter((item) => item.importedByCount >= 5)) {
401
+ findings.push({
402
+ severity: "warning",
403
+ text: `dependency impact: ${hint.file} is imported by ${hint.importedByCount} files (${hint.affectedAreas.join(", ") || "unknown"}).`
404
+ });
405
+ }
406
+ const status = findings.some((finding) => finding.severity === "critical")
407
+ ? "needs_changes"
408
+ : findings.some((finding) => finding.severity === "warning")
409
+ ? "warning"
410
+ : "pass";
411
+ const findingLines = findings.length > 0 ? findings.map((finding) => `- [${finding.severity}] ${finding.text}`).join("\n") : "- 로컬 휴리스틱에서 blocking 문제를 찾지 못했습니다.";
412
+ const fixPrompt = status === "pass"
413
+ ? "재수정 프롬프트 불필요: heuristic review status가 pass입니다."
414
+ : [
415
+ "현재 diff를 로컬 휴리스틱 지적 기준으로 다시 확인해 주세요.",
416
+ "원래 요구사항과 직접 관련 없는 변경만 되돌리고, 필요한 수정은 최소 범위로 유지하세요.",
417
+ `확인 파일: ${input.changedFiles.slice(0, 12).join(", ") || "none"}`,
418
+ "blocking/warning 항목을 먼저 해결한 뒤 pnpm run build를 실행하세요."
419
+ ].join("\n");
420
+ const taskSourceNote = input.inferredClusters
421
+ ? input.anchorAbsent
422
+ ? `- task anchor absent; diff intent 기준으로 검토했습니다: ${formatInferredDiffIntentClusters(input.inferredClusters)}`
423
+ : input.anchorMode === "diff-first_uncertain"
424
+ ? `- task.md uncertain; diff intent 기준으로 검토했습니다: ${formatInferredDiffIntentClusters(input.inferredClusters)}`
425
+ : input.anchorStale
426
+ ? `- task.md stale; diff intent 기준으로 검토했습니다: ${formatInferredDiffIntentClusters(input.inferredClusters)}`
427
+ : input.taskAvailable
428
+ ? `- task.md 기준으로 검토했습니다. diff clusters: ${formatInferredDiffIntentClusters(input.inferredClusters)}`
429
+ : `- 원본 task prompt 없이 diff intent를 기준으로 검토했습니다: ${formatInferredDiffIntentClusters(input.inferredClusters)}`
430
+ : "- task.md 기준으로 검토했습니다.";
431
+ return {
432
+ status,
433
+ fixPrompt,
434
+ markdown: `status: ${status}
435
+
436
+ ## 결론
437
+ ${status === "pass" ? "로컬 휴리스틱 기준으로 커밋을 막을 명확한 문제는 없습니다." : "로컬 휴리스틱 기준으로 확인할 문제가 있습니다."}
438
+
439
+ ## 요구사항 충족 여부
440
+ - AI semantic review는 실행하지 않았습니다.
441
+ - 현재 결과는 diff/static heuristic 기준입니다.
442
+ ${taskSourceNote}
443
+
444
+ ## 범위 초과 수정
445
+ ${largeChange ? "- broad diff입니다. task scope와 변경 파일을 다시 비교하세요." : "- 뚜렷한 broad scope 신호는 없습니다."}
446
+
447
+ ## 규칙 위반 가능성
448
+ ${findingLines}
449
+
450
+ ## Drift 분석
451
+ - severity: ${drift.severity}
452
+ - score: ${drift.driftScore}
453
+ - requirement domains: ${drift.requirementDomains.join(", ") || "none"}
454
+ - diff domains: ${drift.generatedDomains.join(", ") || "none"}
455
+ - requirement zones: ${drift.requirementZones.join(", ") || "none"}
456
+ - diff zones: ${drift.generatedZones.join(", ") || "none"}
457
+ - reasons: ${drift.reasons.join("; ") || "none"}
458
+
459
+ ## Impact 분석
460
+ ${formatReviewImpactHints(input.impactHints)}
461
+
462
+ ## Workflow Quality Score
463
+ - Requirement Alignment Score: ${quality.requirementAlignment}
464
+ - Drift Risk: ${quality.driftRisk}
465
+ - Scope Safety: ${quality.scopeSafety}
466
+ - Confidence: ${quality.confidence}
467
+
468
+ ## 반복 실수 가능성
469
+ - ${input.providerName === "none" ? "provider 없이 실행된 fallback review입니다. 의미 검토가 필요하면 provider 설정 후 AI review를 실행하세요." : "명시적으로 요청한 heuristic review입니다. AI semantic review가 필요하면 --heuristic 없이 실행하세요."}
470
+
471
+ ## 확인이 필요한 파일
472
+ ${input.changedFiles.length > 0 ? input.changedFiles.slice(0, 20).map((file) => `- ${file}`).join("\n") : "- 없음"}
473
+
474
+ ## Codex 재수정 프롬프트
475
+ ${fixPrompt}
476
+
477
+ ## 커밋 가능 여부
478
+ ${status === "pass" ? "- 가능: build/check 결과를 추가 확인하세요." : "- 보류: 위 항목을 확인하세요."}`
479
+ };
480
+ }
481
+ function detectLocaleResourceMigration(changedFiles, diffText, addedLines, removedLines) {
482
+ const hasKoResource = changedFiles.some((file) => /(^|\/)(ko|ko-KR)\.(json|ts|tsx|js|yaml|yml)$/i.test(file) || /(^|\/)(messages|locales?|translations|dictionaries)\/.*ko/i.test(file));
483
+ const hasEnResource = changedFiles.some((file) => /(^|\/)(en|en-US)\.(json|ts|tsx|js|yaml|yml)$/i.test(file) || /(^|\/)(messages|locales?|translations|dictionaries)\/.*en/i.test(file));
484
+ const removedKoreanCopy = removedLines.some((line) => /["'`][^"'`]*[가-힣][^"'`]*["'`]/.test(line));
485
+ const koResourceCopy = addedLines.some((line) => /["'][\w.-]+["']\s*:\s*["'][^"']*[가-힣]/.test(line) || /ko\s*[:=]/i.test(line) && /[가-힣]/.test(line));
486
+ const tCallAdded = addedLines.some((line) => /\bt\(\s*["'][\w.-]+["']\s*\)|useTranslations|useI18n|getMessage|translate\(/i.test(line));
487
+ const defaultKo = /defaultLocale\s*[:=]\s*["']ko(?:-KR)?["']|fallbackLocale\s*[:=]\s*["']ko(?:-KR)?["']|locale\s*\?\?\s*["']ko(?:-KR)?["']/i.test(diffText);
488
+ const defaultEnAdded = addedLines.some((line) => /defaultLocale\s*[:=]\s*["']en(?:-US)?["']|fallbackLocale\s*[:=]\s*["']en(?:-US)?["']|locale\s*\?\?\s*["']en(?:-US)?["']/i.test(line));
489
+ return hasKoResource && hasEnResource && removedKoreanCopy && koResourceCopy && tCallAdded && (defaultKo || !defaultEnAdded);
490
+ }
491
+ function extractTaskType(markdown) {
492
+ const match = markdown.match(/type:\s*([a-z_]+)/i);
493
+ return match?.[1] ?? "";
494
+ }
495
+ function extractTaskSubtype(markdown) {
496
+ return markdown.match(/subtype:\s*([a-z_.]+)/i)?.[1];
497
+ }
498
+ function detectDuplicateMarkdownHeadings(lines) {
499
+ const seen = new Set();
500
+ const duplicates = new Set();
501
+ for (const line of lines) {
502
+ const match = line.match(/^#{1,4}\s+(.+)$/);
503
+ if (!match) {
504
+ continue;
505
+ }
506
+ const heading = match[1].trim().toLowerCase();
507
+ if (seen.has(heading)) {
508
+ duplicates.add(match[1].trim());
509
+ }
510
+ seen.add(heading);
511
+ }
512
+ return [...duplicates];
513
+ }
514
+ function formatReviewImpactHints(impactHints) {
515
+ if (impactHints.length === 0) {
516
+ return "- dependency impact hint 없음";
517
+ }
518
+ return impactHints
519
+ .slice(0, 5)
520
+ .map((hint) => `- ${hint.file}: imported by ${hint.importedByCount}; affected areas: ${hint.affectedAreas.join(", ") || "unknown"}`)
521
+ .join("\n");
522
+ }
523
+ async function loadReviewGitChanges(root, options) {
524
+ if (options.commit) {
525
+ return getCommitGitChanges(root, options.commit);
526
+ }
527
+ if (options.staged) {
528
+ return getStagedGitChanges(root);
529
+ }
530
+ return getGitChanges(root);
531
+ }
532
+ async function loadReviewMemory(root, changedFiles, currentIdentity) {
533
+ const [summaries, projectMapMarkdown, storedIdentity] = await Promise.all([
534
+ readJsonFile(fromRoot(root, ".devguard/file-summaries.json"), []),
535
+ readTextFile(fromRoot(root, ".devguard/project-map.md")),
536
+ readStoredProjectIdentity(root)
537
+ ]);
538
+ if (summaries.length > 0 && currentIdentity && !sameProjectIdentity(currentIdentity, storedIdentity)) {
539
+ console.error(`dev-guard review: warning: ${formatProjectIdentityWarning("scan cache", currentIdentity, storedIdentity)}`);
540
+ return { summaries: [], projectMapMarkdown: "" };
541
+ }
542
+ const changedFileSet = new Set(changedFiles);
543
+ const relatedSummaries = summaries
544
+ .filter((summary) => !isAlwaysIgnoredContextPath(summary.path))
545
+ .filter((summary) => changedFileSet.has(summary.path) || summary.relatedFiles.some((file) => changedFileSet.has(file)))
546
+ .slice(0, 12)
547
+ .map((summary) => ({
548
+ path: summary.path,
549
+ role: summary.role,
550
+ keywords: summary.keywords,
551
+ features: summary.features
552
+ }));
553
+ return {
554
+ summaries: relatedSummaries,
555
+ projectMapMarkdown
556
+ };
557
+ }
558
+ async function collectUntrackedFileContexts(root, paths, keywords) {
559
+ const contexts = [];
560
+ let totalLength = 0;
561
+ for (const path of paths.filter(isTextReviewFile).slice(0, untrackedFileLimit)) {
562
+ if (totalLength >= totalUntrackedCharacterLimit) {
563
+ break;
564
+ }
565
+ const remaining = totalUntrackedCharacterLimit - totalLength;
566
+ const content = await readTextFile(fromRoot(root, path));
567
+ if (!content.trim()) {
568
+ continue;
569
+ }
570
+ const excerpt = buildReviewExcerpt(content, keywords, Math.min(perFileCharacterLimit, remaining));
571
+ totalLength += excerpt.length;
572
+ contexts.push({
573
+ path,
574
+ excerpt,
575
+ truncated: content.length > excerpt.length
576
+ });
577
+ }
578
+ return contexts;
579
+ }
580
+ function extractReviewKeywords(markdown) {
581
+ const tokens = markdown.toLowerCase().match(/[a-z0-9가-힣]{3,}/g) ?? [];
582
+ const routeTokens = markdown.match(/[a-z0-9_.-]+\/[a-z0-9_./*-]+/gi) ?? [];
583
+ return [...new Set([...routeTokens, ...tokens])].slice(0, 30);
584
+ }
585
+ function buildReviewExcerpt(content, keywords, maxCharacters) {
586
+ const lines = content.split("\n");
587
+ const selected = new Set();
588
+ const headLineCount = Math.min(lines.length, 50);
589
+ for (let index = 0; index < headLineCount; index += 1) {
590
+ selected.add(index);
591
+ }
592
+ for (const keyword of keywords) {
593
+ const normalizedKeyword = keyword.toLowerCase().replace(/\*\*?$/g, "");
594
+ if (normalizedKeyword.length < 3) {
595
+ continue;
596
+ }
597
+ lines.forEach((line, index) => {
598
+ if (!line.toLowerCase().includes(normalizedKeyword)) {
599
+ return;
600
+ }
601
+ for (let cursor = Math.max(0, index - 8); cursor <= Math.min(lines.length - 1, index + 12); cursor += 1) {
602
+ selected.add(cursor);
603
+ }
604
+ });
605
+ }
606
+ const chunks = [];
607
+ let previous = -2;
608
+ for (const lineIndex of [...selected].sort((a, b) => a - b)) {
609
+ if (lineIndex > previous + 1 && chunks.length > 0) {
610
+ chunks.push("...");
611
+ }
612
+ chunks.push(`${lineIndex + 1}: ${lines[lineIndex]}`);
613
+ previous = lineIndex;
614
+ if (chunks.join("\n").length >= maxCharacters) {
615
+ break;
616
+ }
617
+ }
618
+ return trimToLimit(chunks.join("\n"), maxCharacters);
619
+ }
620
+ function isTextReviewFile(path) {
621
+ if (path.startsWith("node_modules/") ||
622
+ path.startsWith(".next/") ||
623
+ path.startsWith("dist/") ||
624
+ path.startsWith("build/") ||
625
+ path.startsWith("coverage/")) {
626
+ return false;
627
+ }
628
+ if (/\.(png|jpe?g|gif|webp|avif|ico|svg|ttf|otf|woff2?|mp4|mov|mp3|wav|pdf|zip|gz)$/i.test(path)) {
629
+ return false;
630
+ }
631
+ return /\.(ts|tsx|js|jsx|mjs|cjs|json|md|css|scss|sass|sql|toml|ya?ml|txt)$/i.test(path);
632
+ }
633
+ function trimToLimit(content, maxCharacters) {
634
+ if (content.length <= maxCharacters) {
635
+ return content;
636
+ }
637
+ return `${content.slice(0, Math.max(0, maxCharacters - 80)).trimEnd()}\n... truncated by dev-guard review context limit ...`;
638
+ }
639
+ function readOption(args, name) {
640
+ const index = args.indexOf(name);
641
+ if (index < 0) {
642
+ return undefined;
643
+ }
644
+ const value = args[index + 1];
645
+ if (!value || value.startsWith("--")) {
646
+ throw new Error(`${name} 옵션에는 값이 필요합니다.`);
647
+ }
648
+ return value;
649
+ }
650
+ function errorMessage(error) {
651
+ return error instanceof Error ? error.message : String(error);
652
+ }
653
+ //# sourceMappingURL=review.js.map