Skip to content

Commit e3c4a91

Browse files
Add max CCN
1 parent ebd5345 commit e3c4a91

File tree

7 files changed

+166
-22
lines changed

7 files changed

+166
-22
lines changed

prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ model ScoringConfig {
4242
id String @id @default(uuid())
4343
criterionWeights Json
4444
complexityThreshold Int @default(15)
45+
complexityMaxCcnThreshold Int @default(30)
4546
createdAt DateTime @default(now())
4647
updatedAt DateTime @updatedAt
4748
analyses RepoAnalysis[]

src/app/admin/page.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Link from "next/link";
22
import AdminWeightsForm from "@/components/AdminWeightsForm";
33
import {
4+
DEFAULT_COMPLEXITY_MAX_CCN_THRESHOLD,
45
DEFAULT_COMPLEXITY_THRESHOLD,
56
DEFAULT_CRITERION_CONFIG_BY_CHECK_ID,
67
getScoringConfig,
@@ -52,6 +53,12 @@ export default async function AdminPage() {
5253
defaultWeights={defaultWeights}
5354
initialComplexityThreshold={scoringConfig.complexityThreshold}
5455
defaultComplexityThreshold={DEFAULT_COMPLEXITY_THRESHOLD}
56+
initialComplexityMaxCcnThreshold={
57+
scoringConfig.complexityMaxCcnThreshold
58+
}
59+
defaultComplexityMaxCcnThreshold={
60+
DEFAULT_COMPLEXITY_MAX_CCN_THRESHOLD
61+
}
5562
/>
5663
</div>
5764
);

src/app/api/admin/scoring/route.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import {
3+
DEFAULT_COMPLEXITY_MAX_CCN_THRESHOLD,
34
DEFAULT_COMPLEXITY_THRESHOLD,
45
DEFAULT_CRITERION_CONFIG_BY_CHECK_ID,
56
getScoringConfig,
@@ -30,6 +31,16 @@ function extractComplexityThresholdFromPayload(payload: unknown): number | undef
3031
return value;
3132
}
3233

34+
function extractComplexityMaxCcnThresholdFromPayload(
35+
payload: unknown
36+
): number | undefined {
37+
if (!payload || typeof payload !== "object") return undefined;
38+
const raw = payload as Record<string, unknown>;
39+
const value = raw.complexityMaxCcnThreshold;
40+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
41+
return value;
42+
}
43+
3344
export async function GET() {
3445
try {
3546
const scoringConfig = await getScoringConfig();
@@ -42,13 +53,15 @@ export async function GET() {
4253
])
4354
),
4455
complexityThreshold: scoringConfig.complexityThreshold,
56+
complexityMaxCcnThreshold: scoringConfig.complexityMaxCcnThreshold,
4557
defaultCriterionWeights: Object.fromEntries(
4658
Object.entries(DEFAULT_CRITERION_CONFIG_BY_CHECK_ID).map(([checkId, config]) => [
4759
checkId,
4860
config.weight,
4961
])
5062
),
5163
defaultComplexityThreshold: DEFAULT_COMPLEXITY_THRESHOLD,
64+
defaultComplexityMaxCcnThreshold: DEFAULT_COMPLEXITY_MAX_CCN_THRESHOLD,
5265
});
5366
} catch (err: unknown) {
5467
const message = err instanceof Error ? err.message : "Unexpected error.";
@@ -61,18 +74,26 @@ export async function POST(req: NextRequest) {
6174
const body = await req.json();
6275
const reset = body?.reset === true;
6376
const incomingThreshold = extractComplexityThresholdFromPayload(body);
77+
const incomingMaxThreshold =
78+
extractComplexityMaxCcnThresholdFromPayload(body);
6479

6580
const activeConfig = reset
66-
? await saveCriterionWeights({}, DEFAULT_COMPLEXITY_THRESHOLD)
81+
? await saveCriterionWeights(
82+
{},
83+
DEFAULT_COMPLEXITY_THRESHOLD,
84+
DEFAULT_COMPLEXITY_MAX_CCN_THRESHOLD
85+
)
6786
: await saveCriterionWeights(
6887
extractWeightsFromPayload(body?.criterionWeights),
69-
incomingThreshold
88+
incomingThreshold,
89+
incomingMaxThreshold
7090
);
7191

7292
return NextResponse.json({
7393
ok: true,
7494
scoringConfigId: activeConfig.id,
7595
complexityThreshold: activeConfig.config.complexityThreshold,
96+
complexityMaxCcnThreshold: activeConfig.config.complexityMaxCcnThreshold,
7697
criterionWeights: Object.fromEntries(
7798
Object.entries(activeConfig.config.criterionConfigByCheckId).map(([checkId, config]) => [
7899
checkId,

src/components/AdminWeightsForm.tsx

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,18 +141,24 @@ interface AdminWeightsFormProps {
141141
defaultWeights: Record<string, number>;
142142
initialComplexityThreshold: number;
143143
defaultComplexityThreshold: number;
144+
initialComplexityMaxCcnThreshold: number;
145+
defaultComplexityMaxCcnThreshold: number;
144146
}
145147

146148
export default function AdminWeightsForm({
147149
initialWeights,
148150
defaultWeights,
149151
initialComplexityThreshold,
150152
defaultComplexityThreshold,
153+
initialComplexityMaxCcnThreshold,
154+
defaultComplexityMaxCcnThreshold,
151155
}: AdminWeightsFormProps) {
152156
const [weights, setWeights] = useState<Record<string, number>>(initialWeights);
153157
const [complexityThreshold, setComplexityThreshold] = useState<number>(
154158
initialComplexityThreshold
155159
);
160+
const [complexityMaxCcnThreshold, setComplexityMaxCcnThreshold] =
161+
useState<number>(initialComplexityMaxCcnThreshold);
156162
const [saving, setSaving] = useState(false);
157163
const [message, setMessage] = useState<string | null>(null);
158164
const [error, setError] = useState<string | null>(null);
@@ -164,8 +170,19 @@ export default function AdminWeightsForm({
164170
return current !== initial;
165171
});
166172

167-
return weightChanged || complexityThreshold !== initialComplexityThreshold;
168-
}, [weights, initialWeights, complexityThreshold, initialComplexityThreshold]);
173+
return (
174+
weightChanged ||
175+
complexityThreshold !== initialComplexityThreshold ||
176+
complexityMaxCcnThreshold !== initialComplexityMaxCcnThreshold
177+
);
178+
}, [
179+
weights,
180+
initialWeights,
181+
complexityThreshold,
182+
initialComplexityThreshold,
183+
complexityMaxCcnThreshold,
184+
initialComplexityMaxCcnThreshold,
185+
]);
169186

170187
async function save() {
171188
setSaving(true);
@@ -178,6 +195,7 @@ export default function AdminWeightsForm({
178195
body: JSON.stringify({
179196
criterionWeights: weights,
180197
complexityThreshold,
198+
complexityMaxCcnThreshold,
181199
}),
182200
});
183201
const data = await res.json();
@@ -191,6 +209,9 @@ export default function AdminWeightsForm({
191209
if (typeof data?.complexityThreshold === "number") {
192210
setComplexityThreshold(data.complexityThreshold);
193211
}
212+
if (typeof data?.complexityMaxCcnThreshold === "number") {
213+
setComplexityMaxCcnThreshold(data.complexityMaxCcnThreshold);
214+
}
194215
}
195216
} catch {
196217
setError("Network error while saving weights.");
@@ -202,6 +223,7 @@ export default function AdminWeightsForm({
202223
function resetToDefaults() {
203224
setWeights(defaultWeights);
204225
setComplexityThreshold(defaultComplexityThreshold);
226+
setComplexityMaxCcnThreshold(defaultComplexityMaxCcnThreshold);
205227
setMessage(null);
206228
setError(null);
207229
}
@@ -242,6 +264,27 @@ export default function AdminWeightsForm({
242264
className="w-24 rounded-md border border-gray-300 px-2 py-1 text-sm"
243265
/>
244266
</div>
267+
<div className="flex items-center gap-3">
268+
<label htmlFor="complexity-max-ccn-threshold" className="text-sm font-medium text-gray-700">
269+
Lizard threshold (Max CCN)
270+
</label>
271+
<input
272+
id="complexity-max-ccn-threshold"
273+
type="number"
274+
min={1}
275+
max={200}
276+
step={1}
277+
value={complexityMaxCcnThreshold}
278+
onChange={(event) => {
279+
const next = Number(event.target.value);
280+
if (!Number.isFinite(next)) return;
281+
setComplexityMaxCcnThreshold(
282+
Math.max(1, Math.min(200, Math.round(next)))
283+
);
284+
}}
285+
className="w-24 rounded-md border border-gray-300 px-2 py-1 text-sm"
286+
/>
287+
</div>
245288
</div>
246289

247290
<div className="space-y-4">

src/lib/checkers/complexity.ts

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ function extractThresholdBreaches(output: string): string[] {
7070
.slice(0, 10);
7171
}
7272

73-
function extractSummary(output: string): { averageCcn: number; functionCount: number } | null {
73+
function extractSummary(output: string): {
74+
averageCcn: number;
75+
maxCcn: number;
76+
functionCount: number;
77+
} | null {
7478
const lines = output
7579
.split(/\r?\n/)
7680
.map((line) => line.trim())
@@ -89,7 +93,7 @@ function extractSummary(output: string): { averageCcn: number; functionCount: nu
8993
const avgCcn = Number(numericParts[2]);
9094
const functionCount = Number(numericParts[4]);
9195
if (Number.isFinite(avgCcn) && Number.isFinite(functionCount)) {
92-
return { averageCcn: avgCcn, functionCount };
96+
return { averageCcn: avgCcn, maxCcn: avgCcn, functionCount };
9397
}
9498
}
9599
}
@@ -99,6 +103,7 @@ function extractSummary(output: string): { averageCcn: number; functionCount: nu
99103

100104
function extractAverageFromFunctionRows(output: string): {
101105
averageCcn: number;
106+
maxCcn: number;
102107
functionCount: number;
103108
} | null {
104109
const lines = output.split(/\r?\n/);
@@ -125,16 +130,19 @@ function extractAverageFromFunctionRows(output: string): {
125130
}
126131

127132
const total = ccnValues.reduce((sum, value) => sum + value, 0);
133+
const maxCcn = Math.max(...ccnValues);
128134
return {
129135
averageCcn: total / ccnValues.length,
136+
maxCcn,
130137
functionCount: ccnValues.length,
131138
};
132139
}
133140

134141
export async function checkComplexity(
135142
owner: string,
136143
repo: string,
137-
threshold: number
144+
averageThreshold: number,
145+
maxCcnThreshold: number
138146
): Promise<CheckResult> {
139147
const lizard = await detectLizardCommand();
140148

@@ -183,7 +191,8 @@ export async function checkComplexity(
183191
};
184192
}
185193

186-
const thresholdText = String(Math.max(1, Math.round(threshold)));
194+
const averageThresholdText = String(Math.max(1, Math.round(averageThreshold)));
195+
const maxCcnThresholdText = String(Math.max(1, Math.round(maxCcnThreshold)));
187196
const args = [
188197
...lizard.argsPrefix,
189198
"-x",
@@ -256,46 +265,62 @@ export async function checkComplexity(
256265
}
257266

258267
const averageCcn = summary.averageCcn;
268+
const maxCcn = summary.maxCcn;
269+
270+
const passesAverage = averageCcn <= Number(averageThresholdText);
271+
const passesMax = maxCcn <= Number(maxCcnThresholdText);
259272

260-
if (averageCcn <= Number(thresholdText)) {
273+
if (passesAverage && passesMax) {
261274
return {
262275
id: "complexity",
263276
title: "Cyclomatic complexity (Lizard)",
264277
description:
265278
"The repository should be analyzed with Lizard to track cyclomatic complexity across supported languages.",
266279
status: "pass",
267280
message:
268-
`Lizard analysis executed successfully. Average cyclomatic complexity is ${averageCcn.toFixed(
281+
`Lizard analysis executed successfully. AvgCCN ${averageCcn.toFixed(
269282
2
270-
)}, within threshold ${thresholdText}.`,
283+
)} <= ${averageThresholdText} and MaxCCN ${maxCcn.toFixed(
284+
2
285+
)} <= ${maxCcnThresholdText}.`,
271286
evidence: [
272287
`Analyzer: ${lizard.command} ${lizard.argsPrefix.join(" ")}`.trim(),
273288
...(analysis.code !== 0
274289
? [`Lizard returned non-zero exit code ${analysis.code}, but summary metrics were parsed.`]
275290
: []),
276-
`Average CCN: ${averageCcn.toFixed(2)} (threshold: ${thresholdText})`,
291+
`Average CCN: ${averageCcn.toFixed(2)} (threshold: ${averageThresholdText})`,
292+
`Max CCN: ${maxCcn.toFixed(2)} (threshold: ${maxCcnThresholdText})`,
277293
...evidence,
278294
].slice(0, 10),
279295
referenceUrl: "https://github.com/terryyin/lizard",
280296
};
281297
}
282298

299+
const exceeded: string[] = [];
300+
if (!passesAverage) {
301+
exceeded.push(
302+
`AvgCCN ${averageCcn.toFixed(2)} > ${averageThresholdText}`
303+
);
304+
}
305+
if (!passesMax) {
306+
exceeded.push(`MaxCCN ${maxCcn.toFixed(2)} > ${maxCcnThresholdText}`);
307+
}
308+
283309
return {
284310
id: "complexity",
285311
title: "Cyclomatic complexity (Lizard)",
286312
description:
287313
"The repository should be analyzed with Lizard to track cyclomatic complexity across supported languages.",
288314
status: "fail",
289315
message:
290-
`Lizard analysis ran. Average cyclomatic complexity is ${averageCcn.toFixed(
291-
2
292-
)}, above threshold ${thresholdText}.`,
316+
`Lizard analysis ran. Threshold exceeded: ${exceeded.join("; ")}.`,
293317
evidence: [
294318
`Analyzer: ${lizard.command} ${lizard.argsPrefix.join(" ")}`.trim(),
295319
...(analysis.code !== 0
296320
? [`Lizard returned non-zero exit code ${analysis.code}, but summary metrics were parsed.`]
297321
: []),
298-
`Average CCN: ${averageCcn.toFixed(2)} (threshold: ${thresholdText})`,
322+
`Average CCN: ${averageCcn.toFixed(2)} (threshold: ${averageThresholdText})`,
323+
`Max CCN: ${maxCcn.toFixed(2)} (threshold: ${maxCcnThresholdText})`,
299324
...(evidence.length > 0 ? evidence : ["See lizard output for details."]),
300325
].slice(0, 10),
301326
referenceUrl: "https://github.com/terryyin/lizard",

0 commit comments

Comments
 (0)