@amrishkhan05/frankly 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/.claude-plugin/marketplace.json +27 -0
- package/.claude-plugin/plugin.json +11 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
- package/.github/ISSUE_TEMPLATE/documentation.md +15 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +20 -0
- package/.github/copilot-instructions.md +12 -0
- package/.github/plugin/marketplace.json +20 -0
- package/.github/plugin/plugin.json +16 -0
- package/.mcp.json +10 -0
- package/LICENSE +21 -0
- package/README.md +394 -0
- package/commands/frankly-help.toml +2 -0
- package/commands/frankly-plan.toml +2 -0
- package/commands/frankly-review.toml +2 -0
- package/commands/frankly-verify.toml +2 -0
- package/commands/frankly.toml +2 -0
- package/dist/analysis/change-classifier.d.ts +65 -0
- package/dist/analysis/change-classifier.d.ts.map +1 -0
- package/dist/analysis/change-classifier.js +241 -0
- package/dist/analysis/change-classifier.js.map +1 -0
- package/dist/analysis/repository-intelligence.d.ts +15 -0
- package/dist/analysis/repository-intelligence.d.ts.map +1 -0
- package/dist/analysis/repository-intelligence.js +179 -0
- package/dist/analysis/repository-intelligence.js.map +1 -0
- package/dist/core/config.d.ts +393 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +130 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/engine.d.ts +20 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +117 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/errors.d.ts +27 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +48 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +8 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/models.d.ts +155 -0
- package/dist/core/models.d.ts.map +1 -0
- package/dist/core/models.js +6 -0
- package/dist/core/models.js.map +1 -0
- package/dist/git/index.d.ts +7 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/index.js +6 -0
- package/dist/git/index.js.map +1 -0
- package/dist/git/repository.d.ts +44 -0
- package/dist/git/repository.d.ts.map +1 -0
- package/dist/git/repository.js +198 -0
- package/dist/git/repository.js.map +1 -0
- package/dist/graph/symbols.d.ts +45 -0
- package/dist/graph/symbols.d.ts.map +1 -0
- package/dist/graph/symbols.js +204 -0
- package/dist/graph/symbols.js.map +1 -0
- package/dist/integrations/claude/checkpoint.d.ts +2 -0
- package/dist/integrations/claude/checkpoint.d.ts.map +1 -0
- package/dist/integrations/claude/checkpoint.js +33 -0
- package/dist/integrations/claude/checkpoint.js.map +1 -0
- package/dist/integrations/cli/demo.d.ts +3 -0
- package/dist/integrations/cli/demo.d.ts.map +1 -0
- package/dist/integrations/cli/demo.js +31 -0
- package/dist/integrations/cli/demo.js.map +1 -0
- package/dist/integrations/cli/index.d.ts +7 -0
- package/dist/integrations/cli/index.d.ts.map +1 -0
- package/dist/integrations/cli/index.js +181 -0
- package/dist/integrations/cli/index.js.map +1 -0
- package/dist/integrations/mcp/index.d.ts +3 -0
- package/dist/integrations/mcp/index.d.ts.map +1 -0
- package/dist/integrations/mcp/index.js +7 -0
- package/dist/integrations/mcp/index.js.map +1 -0
- package/dist/integrations/mcp/server.d.ts +14 -0
- package/dist/integrations/mcp/server.d.ts.map +1 -0
- package/dist/integrations/mcp/server.js +232 -0
- package/dist/integrations/mcp/server.js.map +1 -0
- package/dist/project/detector.d.ts +70 -0
- package/dist/project/detector.d.ts.map +1 -0
- package/dist/project/detector.js +214 -0
- package/dist/project/detector.js.map +1 -0
- package/dist/report/json.d.ts +26 -0
- package/dist/report/json.d.ts.map +1 -0
- package/dist/report/json.js +148 -0
- package/dist/report/json.js.map +1 -0
- package/dist/review/red-ink-review.d.ts +26 -0
- package/dist/review/red-ink-review.d.ts.map +1 -0
- package/dist/review/red-ink-review.js +103 -0
- package/dist/review/red-ink-review.js.map +1 -0
- package/fixtures/retry-429/README.md +8 -0
- package/fixtures/retry-429/baseline/package.json +4 -0
- package/fixtures/retry-429/baseline/src/client.test.ts +8 -0
- package/fixtures/retry-429/baseline/src/client.ts +3 -0
- package/fixtures/retry-429/baseline/src/retry-policy.ts +1 -0
- package/fixtures/retry-429/baseline/tsconfig.json +7 -0
- package/fixtures/retry-429/candidate/src/client.ts +5 -0
- package/fixtures/retry-429/candidate/src/retry-strategy.ts +9 -0
- package/fixtures/retry-429/candidate/src/unrelated-format.ts +1 -0
- package/hooks/copilot-activate.js +9 -0
- package/hooks/copilot-hooks.json +13 -0
- package/hooks/hooks.json +13 -0
- package/package.json +70 -0
- package/scripts/run.cjs +32 -0
- package/skills/frankly/SKILL.md +12 -0
- package/src/analysis/change-classifier.ts +285 -0
- package/src/analysis/repository-intelligence.ts +236 -0
- package/src/core/config.ts +149 -0
- package/src/core/engine.ts +143 -0
- package/src/core/errors.ts +56 -0
- package/src/core/index.ts +8 -0
- package/src/core/models.ts +280 -0
- package/src/git/index.ts +7 -0
- package/src/git/repository.ts +207 -0
- package/src/graph/symbols.ts +254 -0
- package/src/integrations/claude/checkpoint.ts +40 -0
- package/src/integrations/cli/demo.ts +34 -0
- package/src/integrations/cli/index.ts +211 -0
- package/src/integrations/mcp/index.ts +7 -0
- package/src/integrations/mcp/server.ts +257 -0
- package/src/project/detector.ts +242 -0
- package/src/report/json.ts +175 -0
- package/src/review/red-ink-review.ts +125 -0
- package/src/tests/phase1.test.ts +57 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration types and loader for Frankly
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
|
|
9
|
+
export type FranklyTrigger = "manual" | "checkpoint" | "continuous" | "ci" | "off";
|
|
10
|
+
export type FranklyAction = "observe" | "advise" | "correct" | "enforce";
|
|
11
|
+
export type FranklyIntensity = "lite" | "full" | "ultra" | "off";
|
|
12
|
+
export type FranklyPersonality = "conservative" | "senior" | "witty";
|
|
13
|
+
|
|
14
|
+
const ConfigSchema = z.object({
|
|
15
|
+
trigger: z.enum(["manual", "checkpoint", "continuous", "ci", "off"]).default("checkpoint"),
|
|
16
|
+
action: z.enum(["observe", "advise", "correct", "enforce"]).default("correct"),
|
|
17
|
+
intensity: z.enum(["lite", "full", "ultra", "off"]).default("full"),
|
|
18
|
+
personality: z.enum(["conservative", "senior", "witty"]).default("senior"),
|
|
19
|
+
maxCorrectionPasses: z.number().int().min(0).max(5).default(1),
|
|
20
|
+
|
|
21
|
+
analysis: z
|
|
22
|
+
.object({
|
|
23
|
+
minimumSufficientChange: z.boolean().default(true),
|
|
24
|
+
taskAlignment: z.boolean().default(true),
|
|
25
|
+
reuseFirst: z.boolean().default(true),
|
|
26
|
+
scopeDrift: z.boolean().default(true),
|
|
27
|
+
refactorContamination: z.boolean().default(true),
|
|
28
|
+
unnecessaryAbstractions: z.boolean().default(true),
|
|
29
|
+
unnecessaryDependencies: z.boolean().default(true),
|
|
30
|
+
duplication: z.boolean().default(true),
|
|
31
|
+
blastRadius: z.boolean().default(true),
|
|
32
|
+
changeSurface: z.boolean().default(true),
|
|
33
|
+
changeNecessity: z.boolean().default(true),
|
|
34
|
+
reviewBurden: z.boolean().default(true),
|
|
35
|
+
contractChanges: z.boolean().default(true),
|
|
36
|
+
behavioralChanges: z.boolean().default(true),
|
|
37
|
+
testImpact: z.boolean().default(true),
|
|
38
|
+
missingRegressionTests: z.boolean().default(true),
|
|
39
|
+
})
|
|
40
|
+
.default({}),
|
|
41
|
+
|
|
42
|
+
restraint: z
|
|
43
|
+
.object({
|
|
44
|
+
enabled: z.boolean().default(true),
|
|
45
|
+
yagni: z.boolean().default(true),
|
|
46
|
+
reuseExistingCodeFirst: z.boolean().default(true),
|
|
47
|
+
stdlibFirst: z.boolean().default(true),
|
|
48
|
+
platformNativeFirst: z.boolean().default(true),
|
|
49
|
+
installedDependenciesFirst: z.boolean().default(true),
|
|
50
|
+
avoidNewDependencies: z.boolean().default(true),
|
|
51
|
+
avoidPrematureAbstraction: z.boolean().default(true),
|
|
52
|
+
avoidUnnecessaryGeneralization: z.boolean().default(true),
|
|
53
|
+
avoidUnrelatedRefactors: z.boolean().default(true),
|
|
54
|
+
preferLocalChange: z.boolean().default(true),
|
|
55
|
+
preferSmallestSafeDiff: z.boolean().default(true),
|
|
56
|
+
preserveSafetyGuardrails: z.boolean().default(true),
|
|
57
|
+
})
|
|
58
|
+
.default({}),
|
|
59
|
+
|
|
60
|
+
tests: z
|
|
61
|
+
.object({
|
|
62
|
+
predictAffected: z.boolean().default(true),
|
|
63
|
+
distinguishPredictedAndExecuted: z.boolean().default(true),
|
|
64
|
+
detectMissingRegressionCoverage: z.boolean().default(true),
|
|
65
|
+
riskBasedExecution: z.boolean().default(true),
|
|
66
|
+
adapters: z
|
|
67
|
+
.object({
|
|
68
|
+
jest: z.boolean().default(true),
|
|
69
|
+
vitest: z.boolean().default(true),
|
|
70
|
+
nx: z.boolean().default(true),
|
|
71
|
+
})
|
|
72
|
+
.default({}),
|
|
73
|
+
})
|
|
74
|
+
.default({}),
|
|
75
|
+
|
|
76
|
+
correction: z
|
|
77
|
+
.object({
|
|
78
|
+
enabled: z.boolean().default(true),
|
|
79
|
+
preserveRequestedBehavior: z.boolean().default(true),
|
|
80
|
+
preserveSecurity: z.boolean().default(true),
|
|
81
|
+
preserveValidation: z.boolean().default(true),
|
|
82
|
+
preserveAccessibility: z.boolean().default(true),
|
|
83
|
+
preserveDataIntegrity: z.boolean().default(true),
|
|
84
|
+
preserveErrorHandling: z.boolean().default(true),
|
|
85
|
+
preserveRegressionCoverage: z.boolean().default(true),
|
|
86
|
+
})
|
|
87
|
+
.default({}),
|
|
88
|
+
|
|
89
|
+
report: z
|
|
90
|
+
.object({
|
|
91
|
+
redInkReview: z.boolean().default(true),
|
|
92
|
+
showEvidence: z.boolean().default(true),
|
|
93
|
+
showConfidence: z.boolean().default(true),
|
|
94
|
+
showLowConfidenceFindings: z.boolean().default(false),
|
|
95
|
+
showPersonalityLine: z.boolean().default(true),
|
|
96
|
+
progressiveDisclosure: z.boolean().default(true),
|
|
97
|
+
})
|
|
98
|
+
.default({}),
|
|
99
|
+
|
|
100
|
+
privacy: z
|
|
101
|
+
.object({
|
|
102
|
+
localOnly: z.boolean().default(true),
|
|
103
|
+
telemetry: z.boolean().default(false),
|
|
104
|
+
externalLLMCalls: z.boolean().default(false),
|
|
105
|
+
embeddings: z.boolean().default(false),
|
|
106
|
+
remoteCodeUpload: z.boolean().default(false),
|
|
107
|
+
})
|
|
108
|
+
.default({}),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export type FranklyConfig = z.infer<typeof ConfigSchema>;
|
|
112
|
+
|
|
113
|
+
export function loadConfig(raw: unknown): FranklyConfig {
|
|
114
|
+
return ConfigSchema.parse(raw);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function getDefaultConfig(): FranklyConfig {
|
|
118
|
+
return ConfigSchema.parse({});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function loadRepositoryConfig(root: string): FranklyConfig {
|
|
122
|
+
const configPath = path.join(root, "frankly.config.json");
|
|
123
|
+
if (!fs.existsSync(configPath)) return getDefaultConfig();
|
|
124
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
|
|
125
|
+
delete raw.$schema;
|
|
126
|
+
return loadConfig(raw);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const INTENSITY_DESCRIPTIONS = {
|
|
130
|
+
lite: "Only obvious issues: unrelated changes, duplicate code, unnecessary dependency, obvious scope drift",
|
|
131
|
+
full: "Complete analysis: YAGNI, reuse, abstraction, scope, blast radius, tests, contracts, minimization",
|
|
132
|
+
ultra: "Aggressively challenge: premature generalization, new helpers, abstraction, unnecessary defensive complexity, dependency additions, avoidable files/symbols, unrelated refactoring",
|
|
133
|
+
off: "No analysis",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const TRIGGER_DESCRIPTIONS = {
|
|
137
|
+
manual: "Only run when explicitly requested",
|
|
138
|
+
checkpoint: "Run automatically at meaningful task completion points",
|
|
139
|
+
continuous: "Run on meaningful debounced edit batches",
|
|
140
|
+
ci: "Run on committed diff / CI / PR context",
|
|
141
|
+
off: "Disable Frankly",
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const ACTION_DESCRIPTIONS = {
|
|
145
|
+
observe: "Analyze only. Never instruct the agent to change the patch.",
|
|
146
|
+
advise: "Return recommendations to the coding agent.",
|
|
147
|
+
correct: "Give the coding agent exactly one automatic correction/simplification opportunity.",
|
|
148
|
+
enforce: "Block completion when configured policies fail.",
|
|
149
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import type { FranklyConfig } from "./config.js";
|
|
3
|
+
import { loadConfig, loadRepositoryConfig } from "./config.js";
|
|
4
|
+
import type { AnalysisResult, ChangedFile, ChangedSymbol, ExecutedTest, TaskPlan } from "./models.js";
|
|
5
|
+
import { countDiffLines, Repository } from "../git/repository.js";
|
|
6
|
+
import { ProjectDetector } from "../project/detector.js";
|
|
7
|
+
import { SymbolExtractor } from "../graph/symbols.js";
|
|
8
|
+
import { ChangeAnalyzer } from "../analysis/change-classifier.js";
|
|
9
|
+
import { inspectRepository } from "../analysis/repository-intelligence.js";
|
|
10
|
+
import { RedInkReviewBuilder } from "../review/red-ink-review.js";
|
|
11
|
+
|
|
12
|
+
export interface AnalysisEngine {
|
|
13
|
+
analyze(task: string, configOverrides?: Partial<FranklyConfig>, executedTests?: ExecutedTest[]): Promise<AnalysisResult>;
|
|
14
|
+
plan(task: string): Promise<TaskPlan>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class FranklyAnalysisEngine implements AnalysisEngine {
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly repository: Repository,
|
|
20
|
+
private readonly projectDetector: ProjectDetector,
|
|
21
|
+
private readonly symbolExtractor: SymbolExtractor,
|
|
22
|
+
private readonly config: FranklyConfig,
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
async plan(task: string): Promise<TaskPlan> {
|
|
26
|
+
const analyzer = new ChangeAnalyzer();
|
|
27
|
+
const keywords = analyzer.extractTaskKeywords(task);
|
|
28
|
+
const diff = await this.repository.getWorkingTreeDiff();
|
|
29
|
+
const intelligence = inspectRepository(this.repository.getRootPath(), keywords, [], []);
|
|
30
|
+
const project = await this.projectDetector.detectProject();
|
|
31
|
+
const expected = Math.max(1, Math.min(6, Math.ceil(Math.max(1, keywords.length) / 2)));
|
|
32
|
+
return {
|
|
33
|
+
task,
|
|
34
|
+
normalizedIntent: analyzer.normalizeTaskIntent(task),
|
|
35
|
+
keywords,
|
|
36
|
+
estimatedFiles: expected,
|
|
37
|
+
expectedSymbols: [1, Math.max(2, expected * 2)],
|
|
38
|
+
expectedTests: /fix|bug|retry|behavior|feature/i.test(task) ? 1 : 0,
|
|
39
|
+
expectsDependency: /dependency|package|library|sdk/i.test(task),
|
|
40
|
+
expectsPublicContract: /api|public|export|contract|interface/i.test(task),
|
|
41
|
+
likelyTouchedAreas: intelligence.reuseCandidates.slice(0, 5),
|
|
42
|
+
reuseCandidates: intelligence.reuseCandidates,
|
|
43
|
+
confidence: keywords.length > 1 ? "medium" : "low",
|
|
44
|
+
concerns: [
|
|
45
|
+
...(project.isWorkspace ? ["Confirm the change stays inside the affected workspace package."] : []),
|
|
46
|
+
...(keywords.length === 0 ? ["Task text is too vague for reliable intent matching."] : []),
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async analyze(
|
|
52
|
+
task: string,
|
|
53
|
+
configOverrides?: Partial<FranklyConfig>,
|
|
54
|
+
executedTests: ExecutedTest[] = [],
|
|
55
|
+
): Promise<AnalysisResult> {
|
|
56
|
+
const startTime = Date.now();
|
|
57
|
+
const config = loadConfig({ ...this.config, ...configOverrides });
|
|
58
|
+
const fileDiffs = await this.repository.getWorkingTreeDiff();
|
|
59
|
+
const analyzer = new ChangeAnalyzer();
|
|
60
|
+
const symbolMap = new Map<string, ReturnType<SymbolExtractor["extractSymbols"]>>();
|
|
61
|
+
const degradedFindings = [];
|
|
62
|
+
|
|
63
|
+
for (const file of fileDiffs) {
|
|
64
|
+
if (file.status === "deleted" || !/\.[cm]?[jt]sx?$/.test(file.path)) continue;
|
|
65
|
+
try {
|
|
66
|
+
symbolMap.set(file.path, this.symbolExtractor.extractSymbols(path.join(this.repository.getRootPath(), file.path)));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
symbolMap.set(file.path, []);
|
|
69
|
+
degradedFindings.push({
|
|
70
|
+
id: `semantic-fallback-${file.path}`,
|
|
71
|
+
severity: "warning" as const,
|
|
72
|
+
confidence: "high" as const,
|
|
73
|
+
title: "Semantic analysis unavailable",
|
|
74
|
+
description: `Semantic analysis unavailable for ${file.path}; using reduced-confidence diff evidence.`,
|
|
75
|
+
affectedSymbols: [file.path],
|
|
76
|
+
evidence: [],
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const classifications = analyzer.classifyChanges({ task, files: fileDiffs, symbols: symbolMap });
|
|
82
|
+
const symbols = classifications
|
|
83
|
+
.map((item) => item.symbol)
|
|
84
|
+
.filter((item): item is ChangedSymbol => "filePath" in item);
|
|
85
|
+
const files: ChangedFile[] = fileDiffs.map((file) => ({
|
|
86
|
+
path: file.path,
|
|
87
|
+
changeType: file.status,
|
|
88
|
+
...countDiffLines(file),
|
|
89
|
+
changes: file.hunks.length,
|
|
90
|
+
symbols: symbols.filter((symbol) => symbol.filePath === file.path),
|
|
91
|
+
}));
|
|
92
|
+
const intelligence = inspectRepository(
|
|
93
|
+
this.repository.getRootPath(),
|
|
94
|
+
analyzer.extractTaskKeywords(task),
|
|
95
|
+
fileDiffs,
|
|
96
|
+
symbols,
|
|
97
|
+
executedTests,
|
|
98
|
+
);
|
|
99
|
+
for (const symbol of symbols) {
|
|
100
|
+
symbol.callers = intelligence.directCallers.filter((file) => file !== symbol.filePath);
|
|
101
|
+
symbol.relatedTests = intelligence.predictedTests.filter((test) => test.path.includes(path.basename(symbol.filePath).replace(/\.[^.]+$/, ""))).map((test) => test.path);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const review = new RedInkReviewBuilder().build({
|
|
105
|
+
task,
|
|
106
|
+
files,
|
|
107
|
+
classifications,
|
|
108
|
+
scopeDrift: analyzer.detectScopeDrift(classifications.filter((item) => !("filePath" in item.symbol)), files.length),
|
|
109
|
+
fileCount: files.length,
|
|
110
|
+
symbolCount: symbols.length,
|
|
111
|
+
intelligence,
|
|
112
|
+
executedTests,
|
|
113
|
+
personality: config.personality,
|
|
114
|
+
action: config.action,
|
|
115
|
+
intensity: config.intensity,
|
|
116
|
+
disabled: config.trigger === "off" || config.intensity === "off",
|
|
117
|
+
maxCorrectionPasses: config.maxCorrectionPasses,
|
|
118
|
+
});
|
|
119
|
+
if (config.trigger !== "off" && config.intensity !== "off") review.findings.push(...degradedFindings);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
schemaVersion: "1",
|
|
123
|
+
repository: this.repository.getRootPath(),
|
|
124
|
+
task,
|
|
125
|
+
files,
|
|
126
|
+
symbols,
|
|
127
|
+
classifications,
|
|
128
|
+
findings: review.findings,
|
|
129
|
+
review,
|
|
130
|
+
timestamp: Date.now(),
|
|
131
|
+
duration: Date.now() - startTime,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function createAnalysisEngine(repositoryPath: string): Promise<FranklyAnalysisEngine> {
|
|
137
|
+
return new FranklyAnalysisEngine(
|
|
138
|
+
new Repository(repositoryPath),
|
|
139
|
+
new ProjectDetector(repositoryPath),
|
|
140
|
+
new SymbolExtractor(repositoryPath),
|
|
141
|
+
loadRepositoryConfig(repositoryPath),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core error types and utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class FranklyError extends Error {
|
|
6
|
+
constructor(
|
|
7
|
+
message: string,
|
|
8
|
+
public code: string,
|
|
9
|
+
public details?: Record<string, unknown>,
|
|
10
|
+
) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "FranklyError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class RepositoryError extends FranklyError {
|
|
17
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
18
|
+
super(message, "REPOSITORY_ERROR", details);
|
|
19
|
+
this.name = "RepositoryError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class GitError extends FranklyError {
|
|
24
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
25
|
+
super(message, "GIT_ERROR", details);
|
|
26
|
+
this.name = "GitError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class ProjectError extends FranklyError {
|
|
31
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
32
|
+
super(message, "PROJECT_ERROR", details);
|
|
33
|
+
this.name = "ProjectError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class SemanticError extends FranklyError {
|
|
38
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
39
|
+
super(message, "SEMANTIC_ERROR", details);
|
|
40
|
+
this.name = "SemanticError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class ConfigError extends FranklyError {
|
|
45
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
46
|
+
super(message, "CONFIG_ERROR", details);
|
|
47
|
+
this.name = "ConfigError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class AnalysisError extends FranklyError {
|
|
52
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
53
|
+
super(message, "ANALYSIS_ERROR", details);
|
|
54
|
+
this.name = "AnalysisError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core data models for Frankly
|
|
3
|
+
* Foundation types for all analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Change Symbols and Files
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
export type SymbolType =
|
|
11
|
+
| "function"
|
|
12
|
+
| "method"
|
|
13
|
+
| "class"
|
|
14
|
+
| "interface"
|
|
15
|
+
| "type"
|
|
16
|
+
| "enum"
|
|
17
|
+
| "variable"
|
|
18
|
+
| "constant"
|
|
19
|
+
| "export"
|
|
20
|
+
| "route"
|
|
21
|
+
| "controller"
|
|
22
|
+
| "service"
|
|
23
|
+
| "hook"
|
|
24
|
+
| "component"
|
|
25
|
+
| "schema"
|
|
26
|
+
| "configuration";
|
|
27
|
+
|
|
28
|
+
export type ChangeType = "added" | "modified" | "deleted" | "renamed";
|
|
29
|
+
|
|
30
|
+
export type Visibility = "public" | "private" | "protected" | "internal";
|
|
31
|
+
|
|
32
|
+
export interface ChangedSymbol {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
type: SymbolType;
|
|
36
|
+
filePath: string;
|
|
37
|
+
lineRange: [number, number]; // [start, end]
|
|
38
|
+
changeType: ChangeType;
|
|
39
|
+
visibility: Visibility;
|
|
40
|
+
isExported: boolean;
|
|
41
|
+
callers: string[];
|
|
42
|
+
callees: string[];
|
|
43
|
+
implementations: string[];
|
|
44
|
+
interfaces: string[];
|
|
45
|
+
referencedContracts: string[];
|
|
46
|
+
relatedTests: string[];
|
|
47
|
+
packageName?: string;
|
|
48
|
+
runtimeBoundary?: string;
|
|
49
|
+
evidence: Evidence[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ChangedFile {
|
|
53
|
+
path: string;
|
|
54
|
+
changeType: ChangeType;
|
|
55
|
+
additions: number;
|
|
56
|
+
deletions: number;
|
|
57
|
+
changes: number;
|
|
58
|
+
symbols: ChangedSymbol[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Evidence Model
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
export type EvidenceType =
|
|
66
|
+
| "TASK_MATCH"
|
|
67
|
+
| "SYMBOL_REFERENCE"
|
|
68
|
+
| "CALLER_RELATION"
|
|
69
|
+
| "CALLEE_RELATION"
|
|
70
|
+
| "INTERFACE_IMPLEMENTATION"
|
|
71
|
+
| "IMPORT_DEPENDENCY"
|
|
72
|
+
| "EXPORT_CONTRACT"
|
|
73
|
+
| "TEST_REFERENCE"
|
|
74
|
+
| "CONFIG_REFERENCE"
|
|
75
|
+
| "RUNTIME_BOUNDARY"
|
|
76
|
+
| "GIT_COCHANGE"
|
|
77
|
+
| "SIMILAR_IMPLEMENTATION"
|
|
78
|
+
| "EXISTING_UTILITY"
|
|
79
|
+
| "DEPENDENCY_USAGE"
|
|
80
|
+
| "TYPE_REQUIREMENT"
|
|
81
|
+
| "COMPILE_REQUIREMENT"
|
|
82
|
+
| "DIFF_DEPENDENT"
|
|
83
|
+
| "HISTORICAL_PATTERN";
|
|
84
|
+
|
|
85
|
+
export type Confidence = "high" | "medium" | "low";
|
|
86
|
+
|
|
87
|
+
export interface Evidence {
|
|
88
|
+
type: EvidenceType;
|
|
89
|
+
confidence: Confidence;
|
|
90
|
+
explanation: string;
|
|
91
|
+
reference?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// Change Classification
|
|
96
|
+
// ============================================================================
|
|
97
|
+
|
|
98
|
+
export type ChangeClassification =
|
|
99
|
+
| "REQUIRED"
|
|
100
|
+
| "SUPPORTING"
|
|
101
|
+
| "TEST"
|
|
102
|
+
| "REFACTOR"
|
|
103
|
+
| "INCIDENTAL"
|
|
104
|
+
| "GENERATED"
|
|
105
|
+
| "SUSPICIOUS"
|
|
106
|
+
| "UNEXPLAINED";
|
|
107
|
+
|
|
108
|
+
export interface ClassifiedChange {
|
|
109
|
+
symbol: ChangedSymbol | ChangedFile;
|
|
110
|
+
classification: ChangeClassification;
|
|
111
|
+
confidence: Confidence;
|
|
112
|
+
justification: string;
|
|
113
|
+
evidence: Evidence[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// Behavioral Changes
|
|
118
|
+
// ============================================================================
|
|
119
|
+
|
|
120
|
+
export type BehavioralChangeType =
|
|
121
|
+
| "API_BEHAVIOR"
|
|
122
|
+
| "CONTROL_FLOW"
|
|
123
|
+
| "ERROR_HANDLING"
|
|
124
|
+
| "STATE_MUTATION"
|
|
125
|
+
| "PERSISTENCE"
|
|
126
|
+
| "SECURITY"
|
|
127
|
+
| "AUTHORIZATION"
|
|
128
|
+
| "AUTHENTICATION"
|
|
129
|
+
| "CONCURRENCY"
|
|
130
|
+
| "RETRY"
|
|
131
|
+
| "CACHE"
|
|
132
|
+
| "CONFIGURATION"
|
|
133
|
+
| "DEPENDENCY"
|
|
134
|
+
| "DATABASE_SCHEMA"
|
|
135
|
+
| "PUBLIC_CONTRACT"
|
|
136
|
+
| "EVENT"
|
|
137
|
+
| "MESSAGE_SCHEMA"
|
|
138
|
+
| "TEST_ONLY"
|
|
139
|
+
| "REFACTOR_ONLY";
|
|
140
|
+
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// Contract Changes
|
|
143
|
+
// ============================================================================
|
|
144
|
+
|
|
145
|
+
export type ContractChangeType =
|
|
146
|
+
| "ADDED"
|
|
147
|
+
| "REMOVED"
|
|
148
|
+
| "WIDENED"
|
|
149
|
+
| "NARROWED"
|
|
150
|
+
| "CHANGED";
|
|
151
|
+
|
|
152
|
+
export interface ContractChange {
|
|
153
|
+
type: ContractChangeType;
|
|
154
|
+
location: string;
|
|
155
|
+
oldSignature?: string;
|
|
156
|
+
newSignature?: string;
|
|
157
|
+
severity: "breaking" | "potentially-breaking" | "compatible";
|
|
158
|
+
confidence: Confidence;
|
|
159
|
+
affectedCallers?: string[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// Findings and Recommendations
|
|
164
|
+
// ============================================================================
|
|
165
|
+
|
|
166
|
+
export type FindingSeverity = "error" | "warning" | "info";
|
|
167
|
+
|
|
168
|
+
export interface Finding {
|
|
169
|
+
id: string;
|
|
170
|
+
severity: FindingSeverity;
|
|
171
|
+
confidence: Confidence;
|
|
172
|
+
title: string;
|
|
173
|
+
description: string;
|
|
174
|
+
affectedSymbols: string[];
|
|
175
|
+
evidence: Evidence[];
|
|
176
|
+
recommendation?: string;
|
|
177
|
+
autoCorrectible?: boolean;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// Verdict and Review
|
|
182
|
+
// ============================================================================
|
|
183
|
+
|
|
184
|
+
export type Verdict = "CLEAN" | "REVIEW" | "SIMPLIFY" | "SPLIT" | "VERIFY" | "BLOCK";
|
|
185
|
+
|
|
186
|
+
export interface RedInkReview {
|
|
187
|
+
task: string;
|
|
188
|
+
verdict: Verdict;
|
|
189
|
+
taskAlignment: "high" | "medium" | "low";
|
|
190
|
+
changeNecessity: number; // 0-100
|
|
191
|
+
changeSurface: "LOW" | "MEDIUM" | "HIGH";
|
|
192
|
+
scopeDrift: number; // 0-1 (percentage)
|
|
193
|
+
findings: Finding[];
|
|
194
|
+
blastRadius: {
|
|
195
|
+
directCallers: number;
|
|
196
|
+
transitiveCallers: number;
|
|
197
|
+
affectedPackages: number;
|
|
198
|
+
callers: string[];
|
|
199
|
+
packages: string[];
|
|
200
|
+
};
|
|
201
|
+
testImpact: {
|
|
202
|
+
likelyAffected: number;
|
|
203
|
+
possiblyAffected: number;
|
|
204
|
+
executed: number;
|
|
205
|
+
passed: number;
|
|
206
|
+
predicted: PredictedTest[];
|
|
207
|
+
executedResults: ExecutedTest[];
|
|
208
|
+
};
|
|
209
|
+
contractChanges: ContractChange[];
|
|
210
|
+
behavioralChanges: BehavioralChangeType[];
|
|
211
|
+
reuseCandidates: string[];
|
|
212
|
+
correction?: CorrectionPlan;
|
|
213
|
+
recommendation: string;
|
|
214
|
+
personality?: string;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ============================================================================
|
|
218
|
+
// Analysis Result
|
|
219
|
+
// ============================================================================
|
|
220
|
+
|
|
221
|
+
export interface AnalysisResult {
|
|
222
|
+
schemaVersion: "1";
|
|
223
|
+
repository: string;
|
|
224
|
+
task: string;
|
|
225
|
+
files: ChangedFile[];
|
|
226
|
+
symbols: ChangedSymbol[];
|
|
227
|
+
classifications: ClassifiedChange[];
|
|
228
|
+
findings: Finding[];
|
|
229
|
+
review: RedInkReview;
|
|
230
|
+
timestamp: number;
|
|
231
|
+
duration: number; // ms
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface TaskPlan {
|
|
235
|
+
task: string;
|
|
236
|
+
normalizedIntent: string;
|
|
237
|
+
keywords: string[];
|
|
238
|
+
estimatedFiles: number;
|
|
239
|
+
expectedSymbols: [number, number];
|
|
240
|
+
expectedTests: number;
|
|
241
|
+
expectsDependency: boolean;
|
|
242
|
+
expectsPublicContract: boolean;
|
|
243
|
+
likelyTouchedAreas: string[];
|
|
244
|
+
reuseCandidates: string[];
|
|
245
|
+
confidence: "high" | "medium" | "low";
|
|
246
|
+
concerns: string[];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ============================================================================
|
|
250
|
+
// Test Impact
|
|
251
|
+
// ============================================================================
|
|
252
|
+
|
|
253
|
+
export type PredictedTestStatus = "LIKELY_AFFECTED" | "POSSIBLY_AFFECTED" | "UNLIKELY_AFFECTED";
|
|
254
|
+
export type ExecutedTestStatus = "PASSED" | "FAILED" | "SKIPPED" | "NOT_RUN";
|
|
255
|
+
|
|
256
|
+
export interface PredictedTest {
|
|
257
|
+
path: string;
|
|
258
|
+
name: string;
|
|
259
|
+
status: PredictedTestStatus;
|
|
260
|
+
evidence: Evidence[];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface ExecutedTest {
|
|
264
|
+
path: string;
|
|
265
|
+
name: string;
|
|
266
|
+
status: ExecutedTestStatus;
|
|
267
|
+
duration?: number;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// Correction Plan
|
|
272
|
+
// ============================================================================
|
|
273
|
+
|
|
274
|
+
export interface CorrectionPlan {
|
|
275
|
+
pass: number;
|
|
276
|
+
removals: string[]; // symbol IDs
|
|
277
|
+
simplifications: { symbol: string; suggestion: string }[];
|
|
278
|
+
evidence: Evidence[];
|
|
279
|
+
instruction: string;
|
|
280
|
+
}
|