Skip to content

Commit ec8a040

Browse files
lkhoonyclaude
andcommitted
fix: 복잡도 분석의 주석 귀속/유효성 판정 정확도 개선
멀티 풀이 파일에서 주석이 다른 풀이로 섞이거나, 주석이 없는데도 `hasUserAnnotation=true`로 판정되던 문제를 해결한다. - SYSTEM_PROMPT에 풀이 경계 정의, 엄격한 주석 귀속 규칙(빈 줄 중단 + 이전 풀이 경계 보호), 유효 복잡도 주석 정의(Big-O 리터럴 + 시간/공간 키워드 필수), 부정 예시, 멀티 풀이 few-shot 추가 - user prompt 각 라인에 `L{n}: ` prefix 부착해 모델이 귀속 범위를 수치로 판단 가능하게 함 - 응답 정규화에서 `userTime/userSpace`의 Big-O 리터럴 유무로 `hasUserAnnotation`과 `matches`를 코드가 재판정하여 모델 자기모순 응답을 차단 - 회귀 방지 테스트 5개 추가(정규화 4 + 라인 번호 prefix 1) Refs #30 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 0c97960 commit ec8a040

2 files changed

Lines changed: 361 additions & 28 deletions

File tree

handlers/complexity-analysis.js

Lines changed: 111 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,78 @@ const FILE_DELIMITER = "=====";
2121
const SYSTEM_PROMPT = `당신은 알고리즘 풀이의 시간/공간 복잡도를 분석하는 전문가입니다.
2222
2323
여러 문제의 솔루션 코드가 구분자(===== {문제명} =====)로 나뉘어 제공됩니다.
24+
각 파일은 라인 번호 prefix "L{n}: "와 함께 전달됩니다. 주석 귀속 판단에 이 라인 번호를 활용하세요.
2425
각 문제별로 독립적으로 분석하세요.
2526
27+
## 풀이(solution) 경계
2628
하나의 문제 안에 같은 문제를 여러 가지 방식으로 푼 풀이가 포함될 수 있습니다.
27-
각 문제마다 코드에서 독립된 풀이가 몇 개인지 판별하세요. (함수/클래스/메서드 단위로 구분)
28-
29-
각 풀이에 대해:
30-
1. name: 함수명 또는 식별 가능한 이름 (예: "twoSum_bruteForce", "Solution.maxArea")
31-
2. description: 접근 방식 한 줄 설명 (예: "이진 탐색", "HashMap 활용")
32-
3. 코드의 실제 시간/공간 복잡도를 Big-O 표기로 계산 (actualTime, actualSpace).
33-
4. 해당 풀이 바로 위/근처에 사용자가 남긴 시간복잡도/공간복잡도 주석을 찾으세요.
34-
주석은 자유 포맷이며 언어별 주석 스타일(//, #, /* */, --, """)과 한/영 키워드가 섞일 수 있습니다.
35-
예: "// TC: O(n)", "# 시간복잡도: O(n log n)", "/* Space: O(1) */", "// Time: O(n^2)"
36-
- 찾았으면 hasUserAnnotation=true, userTime/userSpace에 사용자 값 그대로.
37-
- 한쪽만 적혀 있으면 다른 쪽은 null.
38-
- 전혀 없으면 hasUserAnnotation=false, userTime=null, userSpace=null.
29+
풀이는 top-level 함수/메서드/클래스 선언 단위로 구분합니다. 선언이 시작된 라인을 "헤더 라인",
30+
본문이 끝난 라인을 "종료 라인"이라 합니다.
31+
- 언어별 헤더 예: JS의 \`function\`/\`const ... = (...) =>\`, Python의 \`def\`/\`class\`,
32+
Rust의 \`fn\`/\`impl { pub fn ... }\`, Go의 \`func\`, Java/Kotlin의 메서드 선언 등.
33+
- 파일 상단부터 순서대로 풀이 1..N으로 번호를 붙입니다.
34+
- 중첩 함수(inner helper)는 독립 풀이로 세지 않습니다.
35+
36+
## 주석 귀속 규칙 (엄격)
37+
풀이 k의 시간/공간 복잡도 주석은 다음 두 영역에서만 찾습니다.
38+
39+
1) 헤더 바로 위 영역
40+
- 풀이 k의 헤더 라인 바로 윗줄부터 위로 올라가면서 **빈 줄을 만나면 즉시 중단**합니다.
41+
- 풀이 k-1이 존재한다면 풀이 k-1의 종료 라인을 넘어가지 않습니다 (k=1이면 파일 시작이 하한).
42+
- 즉, 풀이 k의 헤더에 "붙어 있는" 연속된 주석 블록만 대상입니다.
43+
44+
2) 본문 첫 라인 영역
45+
- 풀이 k의 헤더 다음 라인에 붙어 있는 연속된 주석 블록(예: Python docstring, 함수 첫 줄 \`// ...\`).
46+
47+
위 두 영역 밖의 주석은 풀이 k의 주석이 **아닙니다**. 다른 풀이의 영역을 절대 침범하지 마세요.
48+
49+
## 유효한 복잡도 주석의 정의
50+
주석이 유효한 복잡도 주석으로 인정되려면 다음을 **모두** 만족해야 합니다.
51+
1. Big-O 리터럴 포함: \`O(...)\`, \`Θ(...)\`, \`Ω(...)\`, \`o(...)\`, \`ω(...)\` 중 하나.
52+
2. 시간/공간 중 어느 쪽인지를 가리키는 키워드와 같은 라인 또는 같은 주석 블록 안에 있을 것:
53+
시간복잡도 / 공간복잡도 / TC / SC / Time / Space / Complexity.
54+
3. 시간/공간 중 어느 쪽을 말하는지 판별 가능.
55+
56+
언어별 주석 스타일(\`//\`, \`#\`, \`/* */\`, \`--\`, \`"""\`)과 한/영 혼합을 허용합니다.
57+
예: \`// TC: O(n)\`, \`# 시간복잡도: O(n log n)\`, \`/* Space: O(1) */\`, \`// Time: O(n^2)\`.
58+
59+
판별 불가하거나 위 조건 중 하나라도 어긋나면 그 주석은 **무시**합니다.
60+
61+
## 부정 예시 (아래는 모두 "주석 없음"으로 처리)
62+
- \`// brute force 풀이\` — 접근 방식 설명일 뿐, 복잡도 측정치 아님
63+
- \`# 두 포인터 사용\` — 알고리즘 언급만
64+
- \`// 목표: O(n)으로 만들기\` — 목표/희망이지 측정치 아님
65+
- \`// 공간 O(1)만 써야 함 (문제 제약)\` — 문제 제약 언급
66+
- 풀이와 동떨어진 파일 상단의 문제 설명 주석(풀이 귀속 영역 밖)
67+
68+
풀이 k에 유효한 주석이 하나도 없으면:
69+
hasUserAnnotation = false, userTime = null, userSpace = null, matches.time = false, matches.space = false.
70+
71+
## 멀티 풀이 예시 (요약)
72+
입력:
73+
L1: // TC: O(n^4)
74+
L2: // SC: O(n)
75+
L3: const findMin_math = (nums) => Math.min(...nums);
76+
L4:
77+
L5: // TC: O(n^3)
78+
L6: // SC: O(1)
79+
L7: const findMin_naive = (nums) => { /* ... */ };
80+
L8:
81+
L9: const findMin = (nums) => { /* ... */ };
82+
83+
출력(요약):
84+
[
85+
{ name: "findMin_math", userTime: "O(n^4)", userSpace: "O(n)", hasUserAnnotation: true },
86+
{ name: "findMin_naive", userTime: "O(n^3)", userSpace: "O(1)", hasUserAnnotation: true },
87+
{ name: "findMin", userTime: null, userSpace: null, hasUserAnnotation: false }
88+
]
89+
90+
## 각 풀이에 대해 출력할 필드
91+
1. name: 함수명 또는 식별 가능한 이름 (예: "twoSum_bruteForce", "Solution.maxArea").
92+
2. description: 접근 방식 한 줄 설명 (예: "이진 탐색", "HashMap 활용").
93+
3. actualTime, actualSpace: 코드의 실제 시간/공간 복잡도를 Big-O 표기로 계산.
94+
4. hasUserAnnotation, userTime, userSpace: 위 "주석 귀속 규칙" + "유효한 복잡도 주석의 정의"에 따라 채웁니다.
95+
- 한쪽만 있으면 다른 쪽은 null.
3996
5. matches.time / matches.space:
4097
- hasUserAnnotation=false면 둘 다 false.
4198
- 사용자 값이 있는 항목만 actual과 비교하여 일치 여부를 boolean으로 반환.
@@ -71,11 +128,51 @@ const SYSTEM_PROMPT = `당신은 알고리즘 풀이의 시간/공간 복잡도
71128
]
72129
}`;
73130

131+
function addLineNumbers(content) {
132+
return content
133+
.split("\n")
134+
.map((line, i) => `L${i + 1}: ${line}`)
135+
.join("\n");
136+
}
137+
138+
const BIG_O_PATTERN = /[OΘΩoω]\s*\(/;
139+
140+
function normalizeSolution(s) {
141+
const userTime =
142+
typeof s.userTime === "string" && BIG_O_PATTERN.test(s.userTime)
143+
? s.userTime
144+
: null;
145+
const userSpace =
146+
typeof s.userSpace === "string" && BIG_O_PATTERN.test(s.userSpace)
147+
? s.userSpace
148+
: null;
149+
150+
const hasUserAnnotation = userTime !== null || userSpace !== null;
151+
152+
return {
153+
name: typeof s.name === "string" ? s.name : "unknown",
154+
description: typeof s.description === "string" ? s.description : "",
155+
hasUserAnnotation,
156+
userTime,
157+
userSpace,
158+
actualTime: typeof s.actualTime === "string" ? s.actualTime : "?",
159+
actualSpace: typeof s.actualSpace === "string" ? s.actualSpace : "?",
160+
matches: {
161+
time:
162+
hasUserAnnotation && userTime !== null && s.matches?.time === true,
163+
space:
164+
hasUserAnnotation && userSpace !== null && s.matches?.space === true,
165+
},
166+
feedback: typeof s.feedback === "string" ? s.feedback : "",
167+
suggestion: typeof s.suggestion === "string" ? s.suggestion : "",
168+
};
169+
}
170+
74171
async function callComplexityAnalysis(fileEntries, apiKey) {
75172
const userPrompt = fileEntries
76173
.map(
77174
(f) =>
78-
`${FILE_DELIMITER} ${f.problemName} ${FILE_DELIMITER}\n\`\`\`\n${f.content}\n\`\`\``
175+
`${FILE_DELIMITER} ${f.problemName} ${FILE_DELIMITER}\n\`\`\`\n${addLineNumbers(f.content)}\n\`\`\``
79176
)
80177
.join("\n\n");
81178

@@ -119,21 +216,7 @@ async function callComplexityAnalysis(fileEntries, apiKey) {
119216
problemName:
120217
typeof file.problemName === "string" ? file.problemName : "unknown",
121218
solutions: (Array.isArray(file.solutions) ? file.solutions : []).map(
122-
(s) => ({
123-
name: typeof s.name === "string" ? s.name : "unknown",
124-
description: typeof s.description === "string" ? s.description : "",
125-
hasUserAnnotation: s.hasUserAnnotation === true,
126-
userTime: typeof s.userTime === "string" ? s.userTime : null,
127-
userSpace: typeof s.userSpace === "string" ? s.userSpace : null,
128-
actualTime: typeof s.actualTime === "string" ? s.actualTime : "?",
129-
actualSpace: typeof s.actualSpace === "string" ? s.actualSpace : "?",
130-
matches: {
131-
time: s.matches?.time === true,
132-
space: s.matches?.space === true,
133-
},
134-
feedback: typeof s.feedback === "string" ? s.feedback : "",
135-
suggestion: typeof s.suggestion === "string" ? s.suggestion : "",
136-
})
219+
normalizeSolution
137220
),
138221
}));
139222
}

0 commit comments

Comments
 (0)