@dev-guard/core 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.
- package/LICENSE +21 -0
- package/dist/ai.d.ts +30 -0
- package/dist/ai.js +2050 -0
- package/dist/ai.js.map +1 -0
- package/dist/analyze.d.ts +2 -0
- package/dist/analyze.js +303 -0
- package/dist/analyze.js.map +1 -0
- package/dist/completion.d.ts +8 -0
- package/dist/completion.js +272 -0
- package/dist/completion.js.map +1 -0
- package/dist/context-files.d.ts +5 -0
- package/dist/context-files.js +44 -0
- package/dist/context-files.js.map +1 -0
- package/dist/defaults.d.ts +3 -0
- package/dist/defaults.js +46 -0
- package/dist/defaults.js.map +1 -0
- package/dist/diff-intent.d.ts +19 -0
- package/dist/diff-intent.js +517 -0
- package/dist/diff-intent.js.map +1 -0
- package/dist/drift.d.ts +19 -0
- package/dist/drift.js +264 -0
- package/dist/drift.js.map +1 -0
- package/dist/index-check.mjs +13 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/prompt.d.ts +2 -0
- package/dist/prompt.js +633 -0
- package/dist/prompt.js.map +1 -0
- package/dist/report.d.ts +2 -0
- package/dist/report.js +90 -0
- package/dist/report.js.map +1 -0
- package/dist/review.d.ts +4 -0
- package/dist/review.js +350 -0
- package/dist/review.js.map +1 -0
- package/dist/scan.d.ts +9 -0
- package/dist/scan.js +322 -0
- package/dist/scan.js.map +1 -0
- package/dist/task-anchor.d.ts +21 -0
- package/dist/task-anchor.js +126 -0
- package/dist/task-anchor.js.map +1 -0
- package/dist/task-router.d.ts +3 -0
- package/dist/task-router.js +340 -0
- package/dist/task-router.js.map +1 -0
- package/dist/templates.d.ts +20 -0
- package/dist/templates.js +56 -0
- package/dist/templates.js.map +1 -0
- package/dist/types.d.ts +366 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/update.d.ts +2 -0
- package/dist/update.js +250 -0
- package/dist/update.js.map +1 -0
- package/package.json +30 -0
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
import { filterDevGuardContextFiles } from "./context-files.js";
|
|
2
|
+
export function generateCodexPrompt(input) {
|
|
3
|
+
const changeFiles = normalizeChangeFiles(input.changeFiles, input.changedFiles);
|
|
4
|
+
const relatedChangeFiles = filterDevGuardContextFiles(changeFiles, input.includeContextFiles);
|
|
5
|
+
const relatedChangedFiles = normalizeFiles(relatedChangeFiles.map((file) => file.path));
|
|
6
|
+
const summary = buildSummary(relatedChangeFiles, input.diffText);
|
|
7
|
+
const density = resolveDensity(input, relatedChangeFiles);
|
|
8
|
+
if (density === "ultra") {
|
|
9
|
+
return buildUltraCompactPrompt(input, relatedChangeFiles, relatedChangedFiles, summary);
|
|
10
|
+
}
|
|
11
|
+
if (density === "compact" || density === "compact+") {
|
|
12
|
+
return buildCompactPrompt(input, relatedChangeFiles, relatedChangedFiles, summary, density);
|
|
13
|
+
}
|
|
14
|
+
return buildFullPrompt(input, relatedChangeFiles, relatedChangedFiles, summary);
|
|
15
|
+
}
|
|
16
|
+
function buildFullPrompt(input, changeFiles, changedFiles, summary) {
|
|
17
|
+
const taskSections = extractTaskSections(input.taskMarkdown);
|
|
18
|
+
const sections = [
|
|
19
|
+
["현재 작업 목표", fallbackMarkdown(input.taskMarkdown, "현재 작업 목표가 비어 있습니다.")],
|
|
20
|
+
["프로젝트 상태 요약", fallbackMarkdown(input.projectStateMarkdown, "프로젝트 상태 문서가 비어 있습니다.")],
|
|
21
|
+
["반드시 지켜야 할 규칙", fallbackMarkdown(input.rulesMarkdown, "규칙 문서가 비어 있습니다.")],
|
|
22
|
+
["반복하면 안 되는 실수", fallbackMarkdown(input.mistakesMarkdown, "반복 방지 문서가 비어 있습니다.")],
|
|
23
|
+
["최근 결정", fallbackMarkdown(input.decisionsMarkdown, "결정 문서가 비어 있습니다.")],
|
|
24
|
+
["핵심 Guard", formatSymbolicProtection(taskSections, input.rulesMarkdown, input.mistakesMarkdown)],
|
|
25
|
+
["현재 변경 파일", formatChangedFiles(changeFiles)],
|
|
26
|
+
["영향도 힌트", formatImpactHints(input.impactHints ?? [])],
|
|
27
|
+
["Codex 작업 지시", codexInstruction()],
|
|
28
|
+
["완료 조건", completionConditions()],
|
|
29
|
+
["검증 명령어", verificationCommands(changedFiles)]
|
|
30
|
+
];
|
|
31
|
+
const promptText = compressPrompt(renderPrompt("Codex 작업 프롬프트", `${summary}; density=verbose`, sections), input.maxPromptTokens);
|
|
32
|
+
return {
|
|
33
|
+
promptText: addTokenEstimate(promptText, "verbose"),
|
|
34
|
+
summary,
|
|
35
|
+
includedSections: sections.map(([title]) => title)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function buildCompactPrompt(input, changeFiles, changedFiles, summary, density) {
|
|
39
|
+
const taskSections = extractTaskSections(input.taskMarkdown);
|
|
40
|
+
const sections = [
|
|
41
|
+
["작업", compactTaskSummary(taskSections)],
|
|
42
|
+
["작업 유형", taskSections.taskType || "type 확인 필요"],
|
|
43
|
+
["관련 파일", formatCompactRelatedFiles(changeFiles, taskSections)],
|
|
44
|
+
["영향도", formatImpactHints(input.impactHints ?? [])],
|
|
45
|
+
["보호/금지", compactProtection(taskSections, input.rulesMarkdown, input.mistakesMarkdown)],
|
|
46
|
+
["완료 기준", taskSections.completionCriteria || taskSections.completionConditions || completionConditions()],
|
|
47
|
+
["검증 명령어", verificationCommands(changedFiles)]
|
|
48
|
+
];
|
|
49
|
+
return {
|
|
50
|
+
promptText: addTokenEstimate(compressPrompt(renderBudgetedPrompt("Codex 작업 프롬프트 compact", `${summary}; density=${density}`, sections, input.maxPromptTokens), input.maxPromptTokens), density),
|
|
51
|
+
summary,
|
|
52
|
+
includedSections: sections.map(([title]) => title)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function buildUltraCompactPrompt(input, changeFiles, changedFiles, summary) {
|
|
56
|
+
const taskSections = extractTaskSections(input.taskMarkdown);
|
|
57
|
+
const ultraIdeas = formatUltraIdeas(taskSections);
|
|
58
|
+
const sections = [
|
|
59
|
+
["TASK", oneLine([taskSections.goal, taskSections.problem].filter(Boolean).join(" | ")) || "Complete requested scoped task."],
|
|
60
|
+
["TYPE", oneLine(taskSections.taskType) || "unknown"],
|
|
61
|
+
["FILES", formatUltraFiles(changeFiles, taskSections)],
|
|
62
|
+
["IMPACT", formatUltraImpactHints(input.impactHints ?? [])],
|
|
63
|
+
["PROTECT", formatSymbolicProtection(taskSections, input.rulesMarkdown, input.mistakesMarkdown)],
|
|
64
|
+
["SUCCESS", formatUltraSuccess(taskSections)],
|
|
65
|
+
["VERIFY", formatUltraVerify(taskSections, changedFiles)]
|
|
66
|
+
];
|
|
67
|
+
if (ultraIdeas !== "none") {
|
|
68
|
+
const successIdx = sections.findIndex(([key]) => key === "SUCCESS");
|
|
69
|
+
sections.splice(successIdx, 0, ["IDEAS", ultraIdeas]);
|
|
70
|
+
}
|
|
71
|
+
const promptText = addTokenEstimate(renderBudgetedUltraPrompt(`${summary}; density=ultra`, sections, input.maxPromptTokens ?? 2500), "ultra");
|
|
72
|
+
return {
|
|
73
|
+
promptText,
|
|
74
|
+
summary,
|
|
75
|
+
includedSections: sections.map(([title]) => title)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function renderPrompt(title, summary, sections) {
|
|
79
|
+
const body = sections.map(([sectionTitle, content]) => `## ${sectionTitle}\n${content.trim()}`).join("\n\n");
|
|
80
|
+
return `# ${title}\n\n요약: ${summary}\n\n${body}\n`;
|
|
81
|
+
}
|
|
82
|
+
function renderBudgetedPrompt(title, summary, sections, maxPromptTokens) {
|
|
83
|
+
const priority = ["작업", "작업 유형", "완료 기준", "보호/금지", "검증 명령어", "관련 파일", "영향도"];
|
|
84
|
+
let active = [...sections];
|
|
85
|
+
let prompt = renderPrompt(title, summary, active);
|
|
86
|
+
while (maxPromptTokens && estimateTokens(prompt) > maxPromptTokens && active.length > 5) {
|
|
87
|
+
const removableIndex = [...active]
|
|
88
|
+
.map(([name], index) => ({ name, index, priority: priority.indexOf(name) >= 0 ? priority.indexOf(name) : 99 }))
|
|
89
|
+
.sort((a, b) => b.priority - a.priority)[0]?.index;
|
|
90
|
+
if (removableIndex === undefined || removableIndex < 0) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
active.splice(removableIndex, 1);
|
|
94
|
+
prompt = renderPrompt(title, `${summary}; budget_trimmed=true`, active);
|
|
95
|
+
}
|
|
96
|
+
return prompt;
|
|
97
|
+
}
|
|
98
|
+
function renderBudgetedUltraPrompt(summary, sections, maxPromptTokens) {
|
|
99
|
+
const priority = ["TASK", "TYPE", "SUCCESS", "IDEAS", "PROTECT", "VERIFY", "FILES", "IMPACT"];
|
|
100
|
+
let active = [...sections];
|
|
101
|
+
const build = (items, trimmed = false) => `# dev-guard ultra-compact\nSUMMARY: ${summary}${trimmed ? "; budget_trimmed=true" : ""}\n${items
|
|
102
|
+
.map(([title, content]) => `${title}: ${content.trim()}`)
|
|
103
|
+
.join("\n")}\n`;
|
|
104
|
+
let prompt = build(active);
|
|
105
|
+
while (estimateTokens(prompt) > maxPromptTokens && active.length > 5) {
|
|
106
|
+
const removableIndex = [...active]
|
|
107
|
+
.map(([name], index) => ({ name, index, priority: priority.indexOf(name) >= 0 ? priority.indexOf(name) : 99 }))
|
|
108
|
+
.sort((a, b) => b.priority - a.priority)[0]?.index;
|
|
109
|
+
if (removableIndex === undefined || removableIndex < 0) {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
active.splice(removableIndex, 1);
|
|
113
|
+
prompt = build(active, true);
|
|
114
|
+
}
|
|
115
|
+
return sectionSafeCompressPrompt(prompt, maxPromptTokens);
|
|
116
|
+
}
|
|
117
|
+
function normalizeChangeFiles(changeFiles, changedFiles) {
|
|
118
|
+
const source = changeFiles ?? changedFiles.map((path) => ({ path, status: "modified", source: "workingTree" }));
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
const normalized = [];
|
|
121
|
+
for (const file of source) {
|
|
122
|
+
const path = file.path.trim();
|
|
123
|
+
if (!path) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const key = `${file.source}:${file.status}:${file.oldPath ?? ""}:${path}`;
|
|
127
|
+
if (seen.has(key)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
seen.add(key);
|
|
131
|
+
normalized.push({ ...file, path });
|
|
132
|
+
}
|
|
133
|
+
return normalized.sort((a, b) => a.path.localeCompare(b.path) || a.source.localeCompare(b.source));
|
|
134
|
+
}
|
|
135
|
+
function resolveDensity(input, changeFiles) {
|
|
136
|
+
if (input.density) {
|
|
137
|
+
return input.density;
|
|
138
|
+
}
|
|
139
|
+
if (input.ultraCompact) {
|
|
140
|
+
return "ultra";
|
|
141
|
+
}
|
|
142
|
+
if (!input.compact) {
|
|
143
|
+
return "verbose";
|
|
144
|
+
}
|
|
145
|
+
const taskSections = extractTaskSections(input.taskMarkdown);
|
|
146
|
+
const task = inferTaskTypeName(taskSections, input.taskMarkdown);
|
|
147
|
+
const complex = estimateComplexity(input.taskMarkdown, changeFiles);
|
|
148
|
+
if (task.type === "ui_text_cleanup" || task.subtype === "bugfix.text_content") {
|
|
149
|
+
return "ultra";
|
|
150
|
+
}
|
|
151
|
+
if (task.type === "i18n") {
|
|
152
|
+
return "compact+";
|
|
153
|
+
}
|
|
154
|
+
if (task.type === "architecture" || task.type === "migration") {
|
|
155
|
+
return "verbose";
|
|
156
|
+
}
|
|
157
|
+
if (task.type === "ui_feature_polish") {
|
|
158
|
+
return "compact";
|
|
159
|
+
}
|
|
160
|
+
if (task.type === "bugfix") {
|
|
161
|
+
return complex >= 3 ? "compact+" : "compact";
|
|
162
|
+
}
|
|
163
|
+
return complex >= 4 ? "verbose" : "compact";
|
|
164
|
+
}
|
|
165
|
+
function inferTaskTypeName(taskSections, taskMarkdown = "") {
|
|
166
|
+
const source = [taskSections.taskType, taskMarkdown].filter(Boolean).join("\n");
|
|
167
|
+
return {
|
|
168
|
+
type: source.match(/type:\s*([a-z0-9_]+)/i)?.[1] ?? "",
|
|
169
|
+
subtype: source.match(/subtype:\s*([a-z0-9_.]+)/i)?.[1] ?? ""
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function estimateComplexity(taskMarkdown, changeFiles) {
|
|
173
|
+
let score = 0;
|
|
174
|
+
if (changeFiles.length > 10) {
|
|
175
|
+
score += 2;
|
|
176
|
+
}
|
|
177
|
+
else if (changeFiles.length > 4) {
|
|
178
|
+
score += 1;
|
|
179
|
+
}
|
|
180
|
+
if (/requires\s*phasing:\s*true|requiresPhasing:\s*true|##\s*(이번 단계|이후 단계|이번 작업에서 제외할 것)/i.test(taskMarkdown)) {
|
|
181
|
+
score += 2;
|
|
182
|
+
}
|
|
183
|
+
if (/subtype:\s*bugfix\.(navigation_state|data_persistence|api_error|build_error)/i.test(taskMarkdown)) {
|
|
184
|
+
score += 1;
|
|
185
|
+
}
|
|
186
|
+
if (/type:\s*(architecture|migration)/i.test(taskMarkdown)) {
|
|
187
|
+
score += 3;
|
|
188
|
+
}
|
|
189
|
+
if (/drift\s*(risk|score)|Potential drift|semantic drift/i.test(taskMarkdown)) {
|
|
190
|
+
score += 1;
|
|
191
|
+
}
|
|
192
|
+
return score;
|
|
193
|
+
}
|
|
194
|
+
function addTokenEstimate(promptText, density) {
|
|
195
|
+
return promptText.replace(/\n/, `\ndensity=${density}; estimated_tokens=~${estimateTokens(promptText)}\n`);
|
|
196
|
+
}
|
|
197
|
+
function estimateTokens(text) {
|
|
198
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
199
|
+
}
|
|
200
|
+
function normalizeFiles(files) {
|
|
201
|
+
return [...new Set(files.map((file) => file.trim()).filter(Boolean))].sort();
|
|
202
|
+
}
|
|
203
|
+
function buildSummary(changeFiles, diffText) {
|
|
204
|
+
if (changeFiles.length === 0) {
|
|
205
|
+
return "변경 파일 없음";
|
|
206
|
+
}
|
|
207
|
+
const workingTreeCount = changeFiles.filter((file) => file.source === "workingTree").length;
|
|
208
|
+
const stagedCount = changeFiles.filter((file) => file.source === "staged").length;
|
|
209
|
+
const untrackedCount = changeFiles.filter((file) => file.source === "untracked").length;
|
|
210
|
+
const diffLineCount = diffText.split("\n").filter((line) => line.startsWith("+") || line.startsWith("-")).length;
|
|
211
|
+
return `${changeFiles.length}개 파일 변경: working tree ${workingTreeCount}, staged ${stagedCount}, untracked ${untrackedCount}, diff line ${diffLineCount}`;
|
|
212
|
+
}
|
|
213
|
+
function fallbackMarkdown(markdown, fallback) {
|
|
214
|
+
const trimmed = markdown.trim();
|
|
215
|
+
return trimmed.length > 0 ? trimmed : fallback;
|
|
216
|
+
}
|
|
217
|
+
function summarizeMarkdown(markdown, fallback) {
|
|
218
|
+
const lines = markdown
|
|
219
|
+
.split("\n")
|
|
220
|
+
.map((line) => line.trim())
|
|
221
|
+
.filter((line) => line.length > 0)
|
|
222
|
+
.slice(0, 12);
|
|
223
|
+
return lines.length > 0 ? lines.join("\n") : fallback;
|
|
224
|
+
}
|
|
225
|
+
function formatChangedFiles(changeFiles) {
|
|
226
|
+
if (changeFiles.length === 0) {
|
|
227
|
+
return "- 현재 코드 변경 파일 없음\n- 수정 후보 파일은 task.md의 수정 범위를 참고";
|
|
228
|
+
}
|
|
229
|
+
const visible = changeFiles.slice(0, 20);
|
|
230
|
+
const lines = visible
|
|
231
|
+
.map((file) => {
|
|
232
|
+
const rename = file.oldPath ? ` from ${file.oldPath}` : "";
|
|
233
|
+
const state = file.source === file.status ? file.source : `${file.source}/${file.status}`;
|
|
234
|
+
const newFileNote = file.source === "untracked" ? " - 새 파일 생성됨" : "";
|
|
235
|
+
return `- ${file.path} (${state}${rename})${newFileNote}`;
|
|
236
|
+
});
|
|
237
|
+
if (changeFiles.length > visible.length) {
|
|
238
|
+
lines.push(`- ... ${changeFiles.length - visible.length} more files`);
|
|
239
|
+
}
|
|
240
|
+
return lines.join("\n");
|
|
241
|
+
}
|
|
242
|
+
function formatCompactRelatedFiles(changeFiles, taskSections) {
|
|
243
|
+
const changedFilesText = formatChangedFiles(changeFiles);
|
|
244
|
+
const taskFileSections = [
|
|
245
|
+
taskSections.targets ? `### 수정 대상\n${limitLines(taskSections.targets, 8)}` : "",
|
|
246
|
+
taskSections.references ? `### 참고 대상\n${limitLines(taskSections.references, 5)}` : ""
|
|
247
|
+
].filter(Boolean);
|
|
248
|
+
if (taskFileSections.length === 0) {
|
|
249
|
+
return changedFilesText;
|
|
250
|
+
}
|
|
251
|
+
if (changeFiles.length === 0) {
|
|
252
|
+
return [`- 현재 코드 변경 파일 없음`, ...taskFileSections].join("\n\n");
|
|
253
|
+
}
|
|
254
|
+
return [changedFilesText, ...taskFileSections].join("\n\n");
|
|
255
|
+
}
|
|
256
|
+
function formatUltraFiles(changeFiles, taskSections) {
|
|
257
|
+
if (/type:\s*product_strategy/i.test(taskSections.taskType)) {
|
|
258
|
+
const references = compactFileLines(taskSections.references, 3);
|
|
259
|
+
return references.length > 0 ? `none; REF: ${references.join(", ")}` : "none";
|
|
260
|
+
}
|
|
261
|
+
const targets = compactFileLines(taskSections.targets, 8);
|
|
262
|
+
if (targets.length > 0) {
|
|
263
|
+
return targets.join(", ");
|
|
264
|
+
}
|
|
265
|
+
if (changeFiles.length === 0) {
|
|
266
|
+
return "none; see task scope";
|
|
267
|
+
}
|
|
268
|
+
return groupFilePaths(changeFiles.map((file) => file.path)).slice(0, 8).join(", ");
|
|
269
|
+
}
|
|
270
|
+
function formatImpactHints(impactHints) {
|
|
271
|
+
if (impactHints.length === 0) {
|
|
272
|
+
return "- none";
|
|
273
|
+
}
|
|
274
|
+
return impactHints
|
|
275
|
+
.slice(0, 5)
|
|
276
|
+
.map((hint) => {
|
|
277
|
+
const usedBy = hint.importedBy.slice(0, 3).join(", ") || "unknown";
|
|
278
|
+
const areas = hint.affectedAreas.slice(0, 4).join(", ") || "unknown";
|
|
279
|
+
return `- ${hint.file}: imported by ${hint.importedByCount}; used by ${usedBy}; areas ${areas}`;
|
|
280
|
+
})
|
|
281
|
+
.join("\n");
|
|
282
|
+
}
|
|
283
|
+
function formatUltraImpactHints(impactHints) {
|
|
284
|
+
if (impactHints.length === 0) {
|
|
285
|
+
return "none";
|
|
286
|
+
}
|
|
287
|
+
return impactHints
|
|
288
|
+
.slice(0, 3)
|
|
289
|
+
.map((hint) => `${compactImpactPath(hint.file)} used_by=${hint.importedByCount}`)
|
|
290
|
+
.join("; ");
|
|
291
|
+
}
|
|
292
|
+
function compactImpactPath(path) {
|
|
293
|
+
if (path.length <= 56) {
|
|
294
|
+
return path;
|
|
295
|
+
}
|
|
296
|
+
const parts = path.split("/");
|
|
297
|
+
if (parts.length <= 2) {
|
|
298
|
+
return `...${path.slice(-53)}`;
|
|
299
|
+
}
|
|
300
|
+
return `${parts[0]}/.../${parts.at(-1)}`;
|
|
301
|
+
}
|
|
302
|
+
function formatUltraSuccess(taskSections) {
|
|
303
|
+
const source = [taskSections.goal, taskSections.problem, taskSections.taskType, taskSections.completionCriteria, taskSections.completionConditions]
|
|
304
|
+
.join("\n")
|
|
305
|
+
.toLowerCase();
|
|
306
|
+
const flags = new Set();
|
|
307
|
+
if (/cli_output_polish|명령|command|출력|output|요약|summary|preview|done|status|help/.test(source)) {
|
|
308
|
+
flags.add("cli_output_clear=true");
|
|
309
|
+
}
|
|
310
|
+
if (/ui_feature_polish|scoped-ui-change|memo_list_view|card_interaction|modal|bottom sheet|바텀시트|전체 보기|클릭/.test(source)) {
|
|
311
|
+
flags.add("wording_removed=true");
|
|
312
|
+
flags.add("memo_list_view=true");
|
|
313
|
+
flags.add("mobile_pc_ok=true");
|
|
314
|
+
flags.add("build=true");
|
|
315
|
+
}
|
|
316
|
+
if (/done/.test(source)) {
|
|
317
|
+
flags.add("done_output_clear=true");
|
|
318
|
+
}
|
|
319
|
+
if (/update\s*preview|preview|업데이트/.test(source)) {
|
|
320
|
+
flags.add("update_preview_summary_visible=true");
|
|
321
|
+
}
|
|
322
|
+
if (/update|preview|write|자동/.test(source)) {
|
|
323
|
+
flags.add("no_auto_write=true");
|
|
324
|
+
}
|
|
325
|
+
if (/i18n|locale|translation|영어|영문/.test(source)) {
|
|
326
|
+
flags.add("inventory_required=true");
|
|
327
|
+
flags.add("key_parity=true");
|
|
328
|
+
flags.add("hardcoded_text_check=true");
|
|
329
|
+
}
|
|
330
|
+
if (/architecture|migration|마이그레이션|아키텍처/.test(source)) {
|
|
331
|
+
flags.add("migration_plan_required=true");
|
|
332
|
+
flags.add("rollback_or_verification_required=true");
|
|
333
|
+
}
|
|
334
|
+
if (/product_strategy|discovery-first|공유하고 싶은 이유|구현 전 정의/.test(source)) {
|
|
335
|
+
flags.add("hook_defined=true");
|
|
336
|
+
flags.add("share_reason=true");
|
|
337
|
+
flags.add("fun_factor=true");
|
|
338
|
+
flags.add("implementation_scope=true");
|
|
339
|
+
}
|
|
340
|
+
if (flags.size > 0) {
|
|
341
|
+
return [...flags].slice(0, 6).join(", ");
|
|
342
|
+
}
|
|
343
|
+
const lines = (taskSections.completionCriteria || taskSections.completionConditions || completionConditions())
|
|
344
|
+
.split("\n")
|
|
345
|
+
.map((line) => line.trim())
|
|
346
|
+
.filter((line) => line.startsWith("- "))
|
|
347
|
+
.slice(0, 3);
|
|
348
|
+
return lines.length > 0 ? lines.join("; ") : "requested_outcome_verified=true";
|
|
349
|
+
}
|
|
350
|
+
function toUltraExperimentKey(idea) {
|
|
351
|
+
const mappings = [
|
|
352
|
+
// more specific patterns first
|
|
353
|
+
[/친구가.*본|타인.*시선|비교.*요소/, "friend_view_comparison"],
|
|
354
|
+
[/비교.*포맷|점수.*비교|친구.*점수|내.*점수.*vs|비교.*유도|친구.*비교.*유도/, "share_comparison"],
|
|
355
|
+
[/blur.*처리|전체.*공개|blur.*공유/, "blur_reveal_share"],
|
|
356
|
+
[/공유.*버튼|감정.*반응.*유발|버튼.*문구/, "emotional_share_cta"],
|
|
357
|
+
[/상위.*%.*수치|자랑.*동기|수치.*추가/, "social_proof_score"],
|
|
358
|
+
[/희소성|상위.*%만|여기까지/, "scarcity_copy"],
|
|
359
|
+
[/재시도.*훅|답.*바꾸면|결과.*달라질/, "replay_trigger"],
|
|
360
|
+
[/기대감|거의.*다.*왔|마지막.*문항|진행.*중.*카피/, "suspense_progression"],
|
|
361
|
+
[/재방문|결과가.*바뀌|일정.*기간/, "time_retest_hook"],
|
|
362
|
+
[/한.*번.*더|재시작|완료.*직후/, "restart_entry"],
|
|
363
|
+
[/밈.*문법|유행.*밈|밈.*스타일/, "meme_copy"],
|
|
364
|
+
[/공감.*유머|유머.*추가|대화체/, "empathy_humor_copy"],
|
|
365
|
+
[/과장|감탄사|이모지.*감정|감정.*반응.*강화/, "emotional_result_reaction"],
|
|
366
|
+
[/TikTok|Threads|짧고.*강한|짧은.*문장/, "short_copy_format"],
|
|
367
|
+
[/정체성.*라벨|X형.*인간|라벨링/, "identity_label"],
|
|
368
|
+
[/결과.*제목.*공격|공격적.*스타일|밈.*교체|제목.*더.*공격|제목.*변경/, "stronger_result_titles"],
|
|
369
|
+
[/스크린샷|비주얼.*구조/, "screenshot_ready_copy"],
|
|
370
|
+
[/진입.*카피|왜.*해야|동기.*유발/, "hook_entry_copy"],
|
|
371
|
+
[/공유.*시.*친구|친구.*공유|공유.*유도/, "share_comparison"]
|
|
372
|
+
];
|
|
373
|
+
for (const [pattern, key] of mappings) {
|
|
374
|
+
if (pattern.test(idea)) {
|
|
375
|
+
return key;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return idea
|
|
379
|
+
.replace(/^[-*]\s*/, "")
|
|
380
|
+
.replace(/['"''""\s]+/g, "_")
|
|
381
|
+
.replace(/[^a-z0-9_가-힣]/gi, "")
|
|
382
|
+
.slice(0, 28)
|
|
383
|
+
.toLowerCase();
|
|
384
|
+
}
|
|
385
|
+
function formatUltraIdeas(taskSections) {
|
|
386
|
+
if (!taskSections.experimentIdeas) {
|
|
387
|
+
return "none";
|
|
388
|
+
}
|
|
389
|
+
const keys = taskSections.experimentIdeas
|
|
390
|
+
.split("\n")
|
|
391
|
+
.map((line) => line.trim())
|
|
392
|
+
.filter((line) => line.startsWith("-") || line.startsWith("*"))
|
|
393
|
+
.map((line) => toUltraExperimentKey(line))
|
|
394
|
+
.filter(Boolean)
|
|
395
|
+
.slice(0, 5);
|
|
396
|
+
return keys.length > 0 ? keys.join(", ") : "none";
|
|
397
|
+
}
|
|
398
|
+
function formatUltraVerify(taskSections, changedFiles) {
|
|
399
|
+
if (/type:\s*product_strategy/i.test(taskSections.taskType)) {
|
|
400
|
+
return "no code change required unless scope approved";
|
|
401
|
+
}
|
|
402
|
+
return verificationCommands(changedFiles);
|
|
403
|
+
}
|
|
404
|
+
function compactFileLines(text, max) {
|
|
405
|
+
return text
|
|
406
|
+
.split("\n")
|
|
407
|
+
.map((line) => line.replace(/^[-*]\s*/, "").replace(/\s*\(.+?\)\s*$/, "").trim())
|
|
408
|
+
.filter(Boolean)
|
|
409
|
+
.slice(0, max);
|
|
410
|
+
}
|
|
411
|
+
function groupFilePaths(paths) {
|
|
412
|
+
const groups = new Map();
|
|
413
|
+
for (const path of paths) {
|
|
414
|
+
const parts = path.split("/");
|
|
415
|
+
const key = parts.length > 2 ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
416
|
+
groups.set(key, [...(groups.get(key) ?? []), path]);
|
|
417
|
+
}
|
|
418
|
+
return [...groups.entries()].map(([dir, files]) => (files.length >= 3 ? `${dir}/* (${files.length})` : files.join(", ")));
|
|
419
|
+
}
|
|
420
|
+
function limitLines(text, maxLines) {
|
|
421
|
+
const lines = text
|
|
422
|
+
.split("\n")
|
|
423
|
+
.map((line) => line.trimEnd())
|
|
424
|
+
.filter(Boolean);
|
|
425
|
+
const visible = lines.slice(0, maxLines);
|
|
426
|
+
if (lines.length > maxLines) {
|
|
427
|
+
visible.push(`- ... ${lines.length - maxLines} more`);
|
|
428
|
+
}
|
|
429
|
+
return visible.join("\n");
|
|
430
|
+
}
|
|
431
|
+
function extractTaskSections(taskMarkdown) {
|
|
432
|
+
return {
|
|
433
|
+
goal: cleanSectionContent(extractMarkdownSection(taskMarkdown, "목표")),
|
|
434
|
+
problem: cleanSectionContent(extractMarkdownSection(taskMarkdown, "현재 문제")),
|
|
435
|
+
taskType: cleanSectionContent(extractMarkdownSection(taskMarkdown, "작업 유형")),
|
|
436
|
+
scope: cleanSectionContent(extractMarkdownSection(taskMarkdown, "수정 범위")),
|
|
437
|
+
rules: cleanSectionContent(extractMarkdownSection(taskMarkdown, "반드시 지킬 규칙")),
|
|
438
|
+
targets: cleanSectionContent(extractMarkdownSection(taskMarkdown, "수정 대상")),
|
|
439
|
+
references: cleanSectionContent(extractMarkdownSection(taskMarkdown, "참고 대상")),
|
|
440
|
+
protectedTargets: cleanSectionContent(extractMarkdownSection(taskMarkdown, "보호 대상")),
|
|
441
|
+
forbidden: cleanSectionContent(extractMarkdownSection(taskMarkdown, "건드리면 안 되는 것")),
|
|
442
|
+
completionCriteria: cleanSectionContent(extractMarkdownSection(taskMarkdown, "완료 기준")),
|
|
443
|
+
completionConditions: cleanSectionContent(extractMarkdownSection(taskMarkdown, "완료 조건")),
|
|
444
|
+
cautions: cleanSectionContent(extractMarkdownSection(taskMarkdown, "Codex에게 전달할 주의사항")),
|
|
445
|
+
experimentIdeas: cleanSectionContent(extractMarkdownSection(taskMarkdown, "추천 실험 방향"))
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function compactTaskSummary(taskSections) {
|
|
449
|
+
const summary = [taskSections.goal, taskSections.problem ? `현재 문제: ${taskSections.problem}` : ""]
|
|
450
|
+
.filter(Boolean)
|
|
451
|
+
.map((text) => limitLines(text, 4))
|
|
452
|
+
.join("\n");
|
|
453
|
+
return summary || "요청된 작업을 범위 안에서 완료한다.";
|
|
454
|
+
}
|
|
455
|
+
function compactProtection(taskSections, rulesMarkdown, mistakesMarkdown) {
|
|
456
|
+
const primary = [
|
|
457
|
+
taskSections.protectedTargets ? `### 보호 대상\n${limitLines(taskSections.protectedTargets, 6)}` : "",
|
|
458
|
+
taskSections.forbidden ? `### 금지\n${limitLines(taskSections.forbidden, 5)}` : "",
|
|
459
|
+
taskSections.cautions ? `### 주의\n${limitLines(taskSections.cautions, 4)}` : ""
|
|
460
|
+
].filter(Boolean);
|
|
461
|
+
if (primary.length > 0) {
|
|
462
|
+
return primary.join("\n\n");
|
|
463
|
+
}
|
|
464
|
+
return summarizeMarkdown([rulesMarkdown, mistakesMarkdown].filter(Boolean).join("\n"), "관련 없는 파일 수정, 범위 확장, 검증 없는 완료 보고를 피한다.");
|
|
465
|
+
}
|
|
466
|
+
function formatSymbolicProtection(taskSections, rulesMarkdown, mistakesMarkdown) {
|
|
467
|
+
const text = [taskSections.protectedTargets, taskSections.forbidden, taskSections.cautions, rulesMarkdown, mistakesMarkdown]
|
|
468
|
+
.filter(Boolean)
|
|
469
|
+
.join("\n")
|
|
470
|
+
.toLowerCase();
|
|
471
|
+
const flags = new Set(["scope_lock=true", "preserve_behavior=true"]);
|
|
472
|
+
const task = inferTaskTypeName(taskSections);
|
|
473
|
+
if (task.type === "i18n" || /i18n|locale|translation|번역|영어|영문/.test(text)) {
|
|
474
|
+
for (const flag of [
|
|
475
|
+
"inventory_required=true",
|
|
476
|
+
"key_parity=true",
|
|
477
|
+
"hardcoded_text_check=true",
|
|
478
|
+
"metadata_aria_included=true",
|
|
479
|
+
"partial_translation_fail=true",
|
|
480
|
+
"preserve_source_locale=true"
|
|
481
|
+
]) {
|
|
482
|
+
flags.add(flag);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (task.type === "architecture" || task.type === "migration") {
|
|
486
|
+
for (const flag of [
|
|
487
|
+
"preserve_public_api=true",
|
|
488
|
+
"no_unrelated_refactor=true",
|
|
489
|
+
"migration_plan_required=true",
|
|
490
|
+
"rollback_or_verification_required=true"
|
|
491
|
+
]) {
|
|
492
|
+
flags.add(flag);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (task.type === "ui_feature_polish" || /scoped-ui-change|memo_list_view|card_interaction|modal|bottom sheet|바텀시트|inline expand/.test(text)) {
|
|
496
|
+
flags.add("allow_click_handler=true");
|
|
497
|
+
flags.add("allow_local_ui_state=true");
|
|
498
|
+
flags.add("allow_modal_or_sheet=true");
|
|
499
|
+
}
|
|
500
|
+
if (/ui|화면|layout|레이아웃|redesign|리디자인/.test(text)) {
|
|
501
|
+
flags.add("avoid_ui_redesign=true");
|
|
502
|
+
}
|
|
503
|
+
if (/logic|로직|state|상태|계산|routing|router|fetch|auth|인증/.test(text)) {
|
|
504
|
+
flags.add("protect_logic=true");
|
|
505
|
+
}
|
|
506
|
+
if (/data|데이터|fetch|api|supabase|저장/.test(text)) {
|
|
507
|
+
flags.add("protect_data=true");
|
|
508
|
+
}
|
|
509
|
+
return [...flags].join(", ");
|
|
510
|
+
}
|
|
511
|
+
function compressPrompt(prompt, maxPromptTokens = 2500) {
|
|
512
|
+
const replacements = [
|
|
513
|
+
[/기존 정상 동작을 보존/g, "preserve_behavior=true"],
|
|
514
|
+
[/관련 없는 파일 수정/g, "scope_lock=true"],
|
|
515
|
+
[/범위 확장/g, "scope_lock=true"],
|
|
516
|
+
[/불필요한 구조 변경/g, "avoid_restructure=true"],
|
|
517
|
+
[/UI redesign|UI 리디자인|리디자인/g, "avoid_ui_redesign=true"]
|
|
518
|
+
];
|
|
519
|
+
let next = prompt;
|
|
520
|
+
for (const [pattern, replacement] of replacements) {
|
|
521
|
+
next = next.replace(pattern, replacement);
|
|
522
|
+
}
|
|
523
|
+
next = dedupeSymbolicFlags(next);
|
|
524
|
+
const seen = new Set();
|
|
525
|
+
const lines = next
|
|
526
|
+
.split("\n")
|
|
527
|
+
.map((line) => line.trimEnd())
|
|
528
|
+
.filter((line) => {
|
|
529
|
+
const key = line.trim();
|
|
530
|
+
if (!key) {
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
if (seen.has(key)) {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
seen.add(key);
|
|
537
|
+
return true;
|
|
538
|
+
});
|
|
539
|
+
next = lines.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
540
|
+
return sectionSafeCompressPrompt(next, maxPromptTokens);
|
|
541
|
+
}
|
|
542
|
+
function sectionSafeCompressPrompt(prompt, maxPromptTokens = 2500) {
|
|
543
|
+
if (estimateTokens(prompt) <= maxPromptTokens) {
|
|
544
|
+
return prompt;
|
|
545
|
+
}
|
|
546
|
+
const lines = prompt.split("\n");
|
|
547
|
+
const protectedPrefixes = ["TASK:", "TYPE:", "SUCCESS:", "PROTECT:", "VERIFY:"];
|
|
548
|
+
const result = [];
|
|
549
|
+
let used = 0;
|
|
550
|
+
for (const line of lines) {
|
|
551
|
+
const required = protectedPrefixes.some((prefix) => line.startsWith(prefix)) || /^#|^density=|^SUMMARY:/.test(line);
|
|
552
|
+
const cost = estimateTokens(`${line}\n`);
|
|
553
|
+
if (!required && used + cost > maxPromptTokens) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
result.push(line);
|
|
557
|
+
used += cost;
|
|
558
|
+
}
|
|
559
|
+
const next = result.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
560
|
+
if (estimateTokens(next) <= maxPromptTokens || result.length === lines.length) {
|
|
561
|
+
return next;
|
|
562
|
+
}
|
|
563
|
+
return next;
|
|
564
|
+
}
|
|
565
|
+
function oneLine(text) {
|
|
566
|
+
return text.replace(/\s+/g, " ").trim();
|
|
567
|
+
}
|
|
568
|
+
function dedupeSymbolicFlags(text) {
|
|
569
|
+
return text
|
|
570
|
+
.split("\n")
|
|
571
|
+
.map((line) => {
|
|
572
|
+
const flags = line.match(/[a-z_]+=(?:true|false)/g);
|
|
573
|
+
if (!flags || flags.length < 2) {
|
|
574
|
+
return line;
|
|
575
|
+
}
|
|
576
|
+
const deduped = [...new Set(flags)];
|
|
577
|
+
return line.replace(flags.join(", "), deduped.join(", ")).replace(/,\s*,/g, ",");
|
|
578
|
+
})
|
|
579
|
+
.join("\n");
|
|
580
|
+
}
|
|
581
|
+
function extractMarkdownSection(taskMarkdown, title) {
|
|
582
|
+
const lines = taskMarkdown.split("\n");
|
|
583
|
+
const headingIndex = lines.findIndex((line) => new RegExp(`^##\\s+${escapeRegExp(title)}\\s*$`).test(line.trim()));
|
|
584
|
+
if (headingIndex < 0) {
|
|
585
|
+
return "";
|
|
586
|
+
}
|
|
587
|
+
const sectionLines = [];
|
|
588
|
+
for (const line of lines.slice(headingIndex + 1)) {
|
|
589
|
+
if (/^##\s+/.test(line.trim())) {
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
sectionLines.push(line);
|
|
593
|
+
}
|
|
594
|
+
return sectionLines.join("\n").trim();
|
|
595
|
+
}
|
|
596
|
+
function cleanSectionContent(content) {
|
|
597
|
+
return content
|
|
598
|
+
.split("\n")
|
|
599
|
+
.map((line) => line.trimEnd())
|
|
600
|
+
.filter((line, index, lines) => line.length > 0 || (index > 0 && index < lines.length - 1))
|
|
601
|
+
.join("\n")
|
|
602
|
+
.trim();
|
|
603
|
+
}
|
|
604
|
+
function escapeRegExp(value) {
|
|
605
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
606
|
+
}
|
|
607
|
+
function codexInstruction() {
|
|
608
|
+
return [
|
|
609
|
+
"- 위 컨텍스트를 기준으로 현재 작업만 수행한다.",
|
|
610
|
+
"- 관련 없는 파일 수정, 대규모 리팩토링, 불필요한 구조 변경을 피한다.",
|
|
611
|
+
"- 기존 정상 동작을 보존하고, 변경 이유가 명확한 최소 수정으로 해결한다.",
|
|
612
|
+
"- 파일을 수정했다면 변경 내용과 검증 결과를 짧게 보고한다."
|
|
613
|
+
].join("\n");
|
|
614
|
+
}
|
|
615
|
+
function completionConditions() {
|
|
616
|
+
return [
|
|
617
|
+
"- 요청된 기능 또는 수정이 동작한다.",
|
|
618
|
+
"- 변경 파일이 작업 범위를 벗어나지 않는다.",
|
|
619
|
+
"- 문서 업데이트가 필요한 경우 반영하거나 후보를 명확히 남긴다.",
|
|
620
|
+
"- 검증 명령어를 실행하고 결과를 확인한다."
|
|
621
|
+
].join("\n");
|
|
622
|
+
}
|
|
623
|
+
function verificationCommands(changedFiles) {
|
|
624
|
+
const commands = ["pnpm run build"];
|
|
625
|
+
if (changedFiles.some((file) => file.startsWith("packages/cli/"))) {
|
|
626
|
+
commands.push("pnpm cli check");
|
|
627
|
+
}
|
|
628
|
+
if (changedFiles.some((file) => file.startsWith("packages/core/") || file.startsWith("packages/cli/"))) {
|
|
629
|
+
commands.push("pnpm cli update");
|
|
630
|
+
}
|
|
631
|
+
return commands.map((command) => `- \`${command}\``).join("\n");
|
|
632
|
+
}
|
|
633
|
+
//# sourceMappingURL=prompt.js.map
|