mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
feat(ipuaro): add impact score to initial context
Add High Impact Files section to initial context showing which files are most critical based on percentage of codebase that depends on them. Changes: - Add impactScore field to FileMeta (0-100) - Add calculateImpactScore() helper function - Update MetaAnalyzer to compute impact scores - Add formatHighImpactFiles() to prompts.ts - Add includeHighImpactFiles config option (default: true) - 28 new tests (1826 total)
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { createFileMeta, isHubFile } from "../../../../src/domain/value-objects/FileMeta.js"
|
||||
import {
|
||||
calculateImpactScore,
|
||||
createFileMeta,
|
||||
isHubFile,
|
||||
} from "../../../../src/domain/value-objects/FileMeta.js"
|
||||
|
||||
describe("FileMeta", () => {
|
||||
describe("createFileMeta", () => {
|
||||
@@ -15,6 +19,7 @@ describe("FileMeta", () => {
|
||||
expect(meta.isHub).toBe(false)
|
||||
expect(meta.isEntryPoint).toBe(false)
|
||||
expect(meta.fileType).toBe("unknown")
|
||||
expect(meta.impactScore).toBe(0)
|
||||
})
|
||||
|
||||
it("should merge partial values", () => {
|
||||
@@ -42,4 +47,51 @@ describe("FileMeta", () => {
|
||||
expect(isHubFile(0)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("calculateImpactScore", () => {
|
||||
it("should return 0 for file with 0 dependents", () => {
|
||||
expect(calculateImpactScore(0, 10)).toBe(0)
|
||||
})
|
||||
|
||||
it("should return 0 when totalFiles is 0", () => {
|
||||
expect(calculateImpactScore(5, 0)).toBe(0)
|
||||
})
|
||||
|
||||
it("should return 0 when totalFiles is 1", () => {
|
||||
expect(calculateImpactScore(0, 1)).toBe(0)
|
||||
})
|
||||
|
||||
it("should calculate correct percentage", () => {
|
||||
// 5 dependents out of 10 files (excluding itself = 9 possible)
|
||||
// 5/9 * 100 = 55.56 → rounded to 56
|
||||
expect(calculateImpactScore(5, 10)).toBe(56)
|
||||
})
|
||||
|
||||
it("should return 100 when all other files depend on it", () => {
|
||||
// 9 dependents out of 10 files (9 possible dependents)
|
||||
expect(calculateImpactScore(9, 10)).toBe(100)
|
||||
})
|
||||
|
||||
it("should cap at 100", () => {
|
||||
// Edge case: more dependents than possible (shouldn't happen normally)
|
||||
expect(calculateImpactScore(20, 10)).toBe(100)
|
||||
})
|
||||
|
||||
it("should round the percentage", () => {
|
||||
// 1 dependent out of 3 files (2 possible)
|
||||
// 1/2 * 100 = 50
|
||||
expect(calculateImpactScore(1, 3)).toBe(50)
|
||||
})
|
||||
|
||||
it("should calculate impact for small projects", () => {
|
||||
// 1 dependent out of 2 files (1 possible)
|
||||
expect(calculateImpactScore(1, 2)).toBe(100)
|
||||
})
|
||||
|
||||
it("should calculate impact for larger projects", () => {
|
||||
// 50 dependents out of 100 files (99 possible)
|
||||
// 50/99 * 100 = 50.51 → rounded to 51
|
||||
expect(calculateImpactScore(50, 100)).toBe(51)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
truncateContext,
|
||||
formatDependencyGraph,
|
||||
formatCircularDeps,
|
||||
formatHighImpactFiles,
|
||||
type ProjectStructure,
|
||||
} from "../../../../src/infrastructure/llm/prompts.js"
|
||||
import type { FileAST } from "../../../../src/domain/value-objects/FileAST.js"
|
||||
@@ -2395,6 +2396,345 @@ describe("prompts", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("high impact files (0.29.0)", () => {
|
||||
describe("formatHighImpactFiles", () => {
|
||||
it("should return null for empty metas", () => {
|
||||
const metas = new Map<string, FileMeta>()
|
||||
|
||||
const result = formatHighImpactFiles(metas)
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it("should return null when no files have impact score >= minImpact", () => {
|
||||
const metas = new Map<string, FileMeta>([
|
||||
[
|
||||
"src/low.ts",
|
||||
{
|
||||
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||
dependencies: [],
|
||||
dependents: [],
|
||||
isHub: false,
|
||||
isEntryPoint: false,
|
||||
fileType: "source",
|
||||
impactScore: 2,
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
const result = formatHighImpactFiles(metas)
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it("should format file with high impact score", () => {
|
||||
const metas = new Map<string, FileMeta>([
|
||||
[
|
||||
"src/utils/validation.ts",
|
||||
{
|
||||
complexity: { loc: 50, nesting: 2, cyclomaticComplexity: 5, score: 30 },
|
||||
dependencies: [],
|
||||
dependents: [
|
||||
"a.ts",
|
||||
"b.ts",
|
||||
"c.ts",
|
||||
"d.ts",
|
||||
"e.ts",
|
||||
"f.ts",
|
||||
"g.ts",
|
||||
"h.ts",
|
||||
"i.ts",
|
||||
"j.ts",
|
||||
"k.ts",
|
||||
"l.ts",
|
||||
],
|
||||
isHub: true,
|
||||
isEntryPoint: false,
|
||||
fileType: "source",
|
||||
impactScore: 67,
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
const result = formatHighImpactFiles(metas)
|
||||
|
||||
expect(result).toContain("## High Impact Files")
|
||||
expect(result).toContain("| File | Impact | Dependents |")
|
||||
expect(result).toContain("| utils/validation | 67% | 12 files |")
|
||||
})
|
||||
|
||||
it("should sort by impact score descending", () => {
|
||||
const metas = new Map<string, FileMeta>([
|
||||
[
|
||||
"src/low.ts",
|
||||
{
|
||||
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||
dependencies: [],
|
||||
dependents: ["a.ts"],
|
||||
isHub: false,
|
||||
isEntryPoint: false,
|
||||
fileType: "source",
|
||||
impactScore: 10,
|
||||
},
|
||||
],
|
||||
[
|
||||
"src/high.ts",
|
||||
{
|
||||
complexity: { loc: 20, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||
dependencies: [],
|
||||
dependents: ["a.ts", "b.ts", "c.ts", "d.ts", "e.ts"],
|
||||
isHub: false,
|
||||
isEntryPoint: false,
|
||||
fileType: "source",
|
||||
impactScore: 50,
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
const result = formatHighImpactFiles(metas)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
const lines = result!.split("\n")
|
||||
const highIndex = lines.findIndex((l) => l.includes("high"))
|
||||
const lowIndex = lines.findIndex((l) => l.includes("low"))
|
||||
expect(highIndex).toBeLessThan(lowIndex)
|
||||
})
|
||||
|
||||
it("should limit to top N files", () => {
|
||||
const metas = new Map<string, FileMeta>()
|
||||
for (let i = 0; i < 20; i++) {
|
||||
metas.set(`src/file${String(i)}.ts`, {
|
||||
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||
dependencies: [],
|
||||
dependents: ["a.ts"],
|
||||
isHub: false,
|
||||
isEntryPoint: false,
|
||||
fileType: "source",
|
||||
impactScore: 10 + i,
|
||||
})
|
||||
}
|
||||
|
||||
const result = formatHighImpactFiles(metas, 5)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
const dataLines = result!
|
||||
.split("\n")
|
||||
.filter((l) => l.startsWith("| ") && l.includes("%"))
|
||||
expect(dataLines).toHaveLength(5)
|
||||
})
|
||||
|
||||
it("should filter by minImpact", () => {
|
||||
const metas = new Map<string, FileMeta>([
|
||||
[
|
||||
"src/high.ts",
|
||||
{
|
||||
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||
dependencies: [],
|
||||
dependents: ["a.ts", "b.ts", "c.ts"],
|
||||
isHub: false,
|
||||
isEntryPoint: false,
|
||||
fileType: "source",
|
||||
impactScore: 30,
|
||||
},
|
||||
],
|
||||
[
|
||||
"src/low.ts",
|
||||
{
|
||||
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||
dependencies: [],
|
||||
dependents: ["a.ts"],
|
||||
isHub: false,
|
||||
isEntryPoint: false,
|
||||
fileType: "source",
|
||||
impactScore: 5,
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
const result = formatHighImpactFiles(metas, 10, 20)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result).toContain("high")
|
||||
expect(result).not.toContain("low")
|
||||
})
|
||||
|
||||
it("should show singular 'file' for 1 dependent", () => {
|
||||
const metas = new Map<string, FileMeta>([
|
||||
[
|
||||
"src/single.ts",
|
||||
{
|
||||
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||
dependencies: [],
|
||||
dependents: ["a.ts"],
|
||||
isHub: false,
|
||||
isEntryPoint: false,
|
||||
fileType: "source",
|
||||
impactScore: 10,
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
const result = formatHighImpactFiles(metas)
|
||||
|
||||
expect(result).toContain("1 file")
|
||||
expect(result).not.toContain("1 files")
|
||||
})
|
||||
|
||||
it("should shorten src/ prefix", () => {
|
||||
const metas = new Map<string, FileMeta>([
|
||||
[
|
||||
"src/services/user.ts",
|
||||
{
|
||||
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||
dependencies: [],
|
||||
dependents: ["a.ts", "b.ts"],
|
||||
isHub: false,
|
||||
isEntryPoint: false,
|
||||
fileType: "source",
|
||||
impactScore: 20,
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
const result = formatHighImpactFiles(metas)
|
||||
|
||||
expect(result).toContain("services/user")
|
||||
expect(result).not.toContain("src/")
|
||||
})
|
||||
|
||||
it("should remove file extensions", () => {
|
||||
const metas = new Map<string, FileMeta>([
|
||||
[
|
||||
"lib/utils.ts",
|
||||
{
|
||||
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||
dependencies: [],
|
||||
dependents: ["a.ts", "b.ts"],
|
||||
isHub: false,
|
||||
isEntryPoint: false,
|
||||
fileType: "source",
|
||||
impactScore: 20,
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
const result = formatHighImpactFiles(metas)
|
||||
|
||||
expect(result).toContain("lib/utils")
|
||||
expect(result).not.toContain(".ts")
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildInitialContext with includeHighImpactFiles", () => {
|
||||
const structure: ProjectStructure = {
|
||||
name: "test-project",
|
||||
rootPath: "/test",
|
||||
files: ["src/index.ts"],
|
||||
directories: ["src"],
|
||||
}
|
||||
|
||||
const asts = new Map<string, FileAST>([
|
||||
[
|
||||
"src/index.ts",
|
||||
{
|
||||
imports: [],
|
||||
exports: [],
|
||||
functions: [],
|
||||
classes: [],
|
||||
interfaces: [],
|
||||
typeAliases: [],
|
||||
parseError: false,
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
it("should include high impact files by default", () => {
|
||||
const metas = new Map<string, FileMeta>([
|
||||
[
|
||||
"src/index.ts",
|
||||
{
|
||||
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||
dependencies: [],
|
||||
dependents: ["a.ts", "b.ts"],
|
||||
isHub: false,
|
||||
isEntryPoint: true,
|
||||
fileType: "source",
|
||||
impactScore: 20,
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
const context = buildInitialContext(structure, asts, metas)
|
||||
|
||||
expect(context).toContain("## High Impact Files")
|
||||
})
|
||||
|
||||
it("should exclude high impact files when includeHighImpactFiles is false", () => {
|
||||
const metas = new Map<string, FileMeta>([
|
||||
[
|
||||
"src/index.ts",
|
||||
{
|
||||
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||
dependencies: [],
|
||||
dependents: ["a.ts", "b.ts"],
|
||||
isHub: false,
|
||||
isEntryPoint: true,
|
||||
fileType: "source",
|
||||
impactScore: 20,
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
const context = buildInitialContext(structure, asts, metas, {
|
||||
includeHighImpactFiles: false,
|
||||
})
|
||||
|
||||
expect(context).not.toContain("## High Impact Files")
|
||||
})
|
||||
|
||||
it("should not include high impact files when metas is undefined", () => {
|
||||
const context = buildInitialContext(structure, asts, undefined, {
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
|
||||
expect(context).not.toContain("## High Impact Files")
|
||||
})
|
||||
|
||||
it("should not include high impact files when metas is empty", () => {
|
||||
const emptyMetas = new Map<string, FileMeta>()
|
||||
|
||||
const context = buildInitialContext(structure, asts, emptyMetas, {
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
|
||||
expect(context).not.toContain("## High Impact Files")
|
||||
})
|
||||
|
||||
it("should not include high impact files when no files have high impact", () => {
|
||||
const metas = new Map<string, FileMeta>([
|
||||
[
|
||||
"src/index.ts",
|
||||
{
|
||||
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||
dependencies: [],
|
||||
dependents: [],
|
||||
isHub: false,
|
||||
isEntryPoint: true,
|
||||
fileType: "source",
|
||||
impactScore: 0,
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
const context = buildInitialContext(structure, asts, metas, {
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
|
||||
expect(context).not.toContain("## High Impact Files")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("circular dependencies (0.28.0)", () => {
|
||||
describe("formatCircularDeps", () => {
|
||||
it("should return null for empty array", () => {
|
||||
|
||||
@@ -18,6 +18,7 @@ describe("ContextConfigSchema", () => {
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,6 +33,7 @@ describe("ContextConfigSchema", () => {
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -171,6 +173,7 @@ describe("ContextConfigSchema", () => {
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -187,6 +190,7 @@ describe("ContextConfigSchema", () => {
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -204,6 +208,7 @@ describe("ContextConfigSchema", () => {
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -218,6 +223,7 @@ describe("ContextConfigSchema", () => {
|
||||
includeSignatures: false,
|
||||
includeDepsGraph: false,
|
||||
includeCircularDeps: false,
|
||||
includeHighImpactFiles: false,
|
||||
}
|
||||
|
||||
const result = ContextConfigSchema.parse(config)
|
||||
@@ -233,6 +239,7 @@ describe("ContextConfigSchema", () => {
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: true,
|
||||
}
|
||||
|
||||
const result = ContextConfigSchema.parse(config)
|
||||
@@ -314,4 +321,29 @@ describe("ContextConfigSchema", () => {
|
||||
expect(() => ContextConfigSchema.parse({ includeCircularDeps: 1 })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("includeHighImpactFiles", () => {
|
||||
it("should accept true", () => {
|
||||
const result = ContextConfigSchema.parse({ includeHighImpactFiles: true })
|
||||
expect(result.includeHighImpactFiles).toBe(true)
|
||||
})
|
||||
|
||||
it("should accept false", () => {
|
||||
const result = ContextConfigSchema.parse({ includeHighImpactFiles: false })
|
||||
expect(result.includeHighImpactFiles).toBe(false)
|
||||
})
|
||||
|
||||
it("should default to true", () => {
|
||||
const result = ContextConfigSchema.parse({})
|
||||
expect(result.includeHighImpactFiles).toBe(true)
|
||||
})
|
||||
|
||||
it("should reject non-boolean", () => {
|
||||
expect(() => ContextConfigSchema.parse({ includeHighImpactFiles: "true" })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject number", () => {
|
||||
expect(() => ContextConfigSchema.parse({ includeHighImpactFiles: 1 })).toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user