universal-ast-mapper 1.28.0 → 2.0.1

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.
@@ -0,0 +1,185 @@
1
+ import https from "node:https";
2
+ import fs from "node:fs";
3
+ // ─── Anthropic API ────────────────────────────────────────────────────────────
4
+ export async function callClaude(prompt, opts) {
5
+ const apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY;
6
+ if (!apiKey)
7
+ throw new Error("No Anthropic API key — set ANTHROPIC_API_KEY or pass --api-key");
8
+ const model = opts.model ?? "claude-sonnet-4-6";
9
+ const body = JSON.stringify({
10
+ model,
11
+ max_tokens: opts.maxTokens ?? 4096,
12
+ messages: [{ role: "user", content: prompt }],
13
+ });
14
+ return new Promise((resolve, reject) => {
15
+ const req = https.request({
16
+ hostname: "api.anthropic.com",
17
+ path: "/v1/messages",
18
+ method: "POST",
19
+ headers: {
20
+ "content-type": "application/json",
21
+ "x-api-key": apiKey,
22
+ "anthropic-version": "2023-06-01",
23
+ "content-length": Buffer.byteLength(body),
24
+ },
25
+ }, (res) => {
26
+ const chunks = [];
27
+ res.on("data", (c) => chunks.push(c));
28
+ res.on("end", () => {
29
+ const raw = Buffer.concat(chunks).toString("utf8");
30
+ try {
31
+ const parsed = JSON.parse(raw);
32
+ if (parsed.error)
33
+ reject(new Error(`Anthropic API: ${parsed.error.message}`));
34
+ else
35
+ resolve(parsed.content?.[0]?.text ?? "");
36
+ }
37
+ catch {
38
+ reject(new Error(`Unexpected API response: ${raw.slice(0, 300)}`));
39
+ }
40
+ });
41
+ });
42
+ req.on("error", reject);
43
+ req.write(body);
44
+ req.end();
45
+ });
46
+ }
47
+ // ─── Prompt builders ──────────────────────────────────────────────────────────
48
+ function smellPrompt(smell, sourceCode, language) {
49
+ const langFence = language === "typescript" ? "ts" : language === "javascript" ? "js" : language;
50
+ const symbol = smell.symbol ? ` for \`${smell.symbol}\`` : "";
51
+ return `You are an expert ${language} developer performing a refactoring.
52
+
53
+ ## Problem
54
+ Smell type: **${smell.smell}**${symbol}
55
+ Message: ${smell.message}
56
+ File: ${smell.file}${smell.line ? `, line ${smell.line}` : ""}
57
+
58
+ ## Source file
59
+ \`\`\`${langFence}
60
+ ${sourceCode}
61
+ \`\`\`
62
+
63
+ ## Your task
64
+ Refactor the code to eliminate the smell. Provide:
65
+ 1. The **minimal refactored code** — just the changed function/class (not the whole file unless necessary)
66
+ 2. A one-paragraph **explanation** of what you changed and why
67
+
68
+ Format your response EXACTLY as:
69
+ <before>
70
+ // paste the original problematic code block here
71
+ </before>
72
+ <after>
73
+ // paste the refactored code here
74
+ </after>
75
+ <explanation>
76
+ Your explanation here.
77
+ </explanation>`;
78
+ }
79
+ function securityPrompt(issue, sourceCode, language) {
80
+ const langFence = language === "typescript" ? "ts" : language === "javascript" ? "js" : language;
81
+ return `You are a security expert performing a code fix.
82
+
83
+ ## Security Issue
84
+ Rule: **${issue.rule}** (${issue.severity})
85
+ Message: ${issue.message}
86
+ File: ${issue.file}, line ${issue.line}
87
+ Snippet: \`${issue.snippet}\`
88
+
89
+ ## Source file
90
+ \`\`\`${langFence}
91
+ ${sourceCode}
92
+ \`\`\`
93
+
94
+ ## Your task
95
+ Fix the security vulnerability. Provide:
96
+ 1. The **minimal fixed code** — just the changed lines/block
97
+ 2. A one-paragraph **explanation** of the vulnerability and how the fix addresses it
98
+
99
+ Format your response EXACTLY as:
100
+ <before>
101
+ ${issue.snippet}
102
+ </before>
103
+ <after>
104
+ // fixed code here
105
+ </after>
106
+ <explanation>
107
+ Your explanation here.
108
+ </explanation>`;
109
+ }
110
+ // ─── Response parser ──────────────────────────────────────────────────────────
111
+ function parseResponse(raw) {
112
+ const extract = (tag) => {
113
+ const m = raw.match(new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`));
114
+ return m ? m[1].trim() : "";
115
+ };
116
+ return {
117
+ before: extract("before"),
118
+ after: extract("after"),
119
+ explanation: extract("explanation"),
120
+ };
121
+ }
122
+ // ─── Public API ───────────────────────────────────────────────────────────────
123
+ /** Send one refactor target to Claude and return a structured RefactorResult. */
124
+ export async function aiRefactor(target, opts = {}) {
125
+ const model = opts.model ?? "claude-sonnet-4-6";
126
+ let prompt;
127
+ let issue;
128
+ let symbol;
129
+ if (target.kind === "smell" && target.smell) {
130
+ prompt = smellPrompt(target.smell, target.sourceCode, target.language);
131
+ issue = `${target.smell.smell}${target.smell.symbol ? `: ${target.smell.symbol}` : ""}`;
132
+ symbol = target.smell.symbol;
133
+ }
134
+ else if (target.kind === "security" && target.security) {
135
+ prompt = securityPrompt(target.security, target.sourceCode, target.language);
136
+ issue = `${target.security.rule} (${target.security.severity})`;
137
+ }
138
+ else {
139
+ throw new Error("Invalid refactor target: must have smell or security");
140
+ }
141
+ const raw = await callClaude(prompt, opts);
142
+ const { before, after, explanation } = parseResponse(raw);
143
+ return {
144
+ filePath: target.filePath,
145
+ symbol,
146
+ issue,
147
+ before: before || "(see source)",
148
+ after: after || raw,
149
+ explanation: explanation || "(no explanation provided)",
150
+ model,
151
+ };
152
+ }
153
+ /**
154
+ * Batch-refactor: takes a list of targets and calls Claude once per target.
155
+ * Returns results in the same order; errors produce a result with `error` set.
156
+ */
157
+ export async function aiRefactorBatch(targets, opts = {}) {
158
+ const results = [];
159
+ for (const target of targets) {
160
+ try {
161
+ results.push(await aiRefactor(target, opts));
162
+ }
163
+ catch (e) {
164
+ results.push({
165
+ filePath: target.filePath,
166
+ issue: target.kind === "smell" ? target.smell?.smell ?? "smell" : target.security?.rule ?? "security",
167
+ before: "",
168
+ after: "",
169
+ explanation: "",
170
+ model: opts.model ?? "claude-sonnet-4-6",
171
+ error: e instanceof Error ? e.message : String(e),
172
+ });
173
+ }
174
+ }
175
+ return results;
176
+ }
177
+ /** Read source code for a file, returning empty string on error. */
178
+ export function readSource(filePath) {
179
+ try {
180
+ return fs.readFileSync(filePath, "utf8");
181
+ }
182
+ catch {
183
+ return "";
184
+ }
185
+ }
@@ -0,0 +1,105 @@
1
+ import https from "node:https";
2
+ async function callClaude(prompt, opts) {
3
+ const apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY;
4
+ if (!apiKey)
5
+ throw new Error("No Anthropic API key — set ANTHROPIC_API_KEY or pass --api-key");
6
+ const body = JSON.stringify({
7
+ model: opts.model ?? "claude-sonnet-4-6",
8
+ max_tokens: opts.maxTokens ?? 4096,
9
+ messages: [{ role: "user", content: prompt }],
10
+ });
11
+ return new Promise((resolve, reject) => {
12
+ const req = https.request({
13
+ hostname: "api.anthropic.com",
14
+ path: "/v1/messages",
15
+ method: "POST",
16
+ headers: {
17
+ "content-type": "application/json",
18
+ "x-api-key": apiKey,
19
+ "anthropic-version": "2023-06-01",
20
+ "content-length": Buffer.byteLength(body),
21
+ },
22
+ }, (res) => {
23
+ const chunks = [];
24
+ res.on("data", (c) => chunks.push(c));
25
+ res.on("end", () => {
26
+ const raw = Buffer.concat(chunks).toString("utf8");
27
+ try {
28
+ const parsed = JSON.parse(raw);
29
+ if (parsed.error) {
30
+ reject(new Error(`Anthropic API: ${parsed.error.message}`));
31
+ }
32
+ else {
33
+ resolve(parsed.content?.[0]?.text ?? "");
34
+ }
35
+ }
36
+ catch {
37
+ reject(new Error(`Unexpected API response: ${raw.slice(0, 300)}`));
38
+ }
39
+ });
40
+ });
41
+ req.on("error", reject);
42
+ req.write(body);
43
+ req.end();
44
+ });
45
+ }
46
+ // ─── Prompt builder ───────────────────────────────────────────────────────────
47
+ function buildPrompt(sourceFile, sourceCode, stubContent, framework, language) {
48
+ const langFence = language === "typescript" ? "ts" : language === "javascript" ? "js" : language;
49
+ return `You are an expert ${language} developer and TDD practitioner.
50
+
51
+ Your task: given a source file and its auto-generated test stubs, replace all TODO placeholders with real, meaningful assertions.
52
+
53
+ ## Source file: ${sourceFile}
54
+ \`\`\`${langFence}
55
+ ${sourceCode}
56
+ \`\`\`
57
+
58
+ ## Generated stubs to fill in:
59
+ \`\`\`${langFence}
60
+ ${stubContent}
61
+ \`\`\`
62
+
63
+ ## Rules:
64
+ - Test framework: **${framework}**
65
+ - Keep every test name and describe block from the stubs exactly as-is
66
+ - Replace "// TODO: arrange" with real setup code using the source implementation
67
+ - Replace generic assertions (toBeDefined, is not None, assertNotNull) with precise assertions that verify actual return values, side effects, or thrown errors
68
+ - For functions with clear deterministic behavior, use concrete expected values
69
+ - Cover happy path AND at least one edge case (empty/null/zero input) for each test
70
+ - For async functions, always use async/await
71
+ - Do NOT import anything extra beyond what the stubs already import
72
+ - Do NOT add tests beyond what is in the stubs
73
+ - Return ONLY the complete test file — no markdown fences, no explanation`;
74
+ }
75
+ // ─── Stripper ─────────────────────────────────────────────────────────────────
76
+ /** Remove leading/trailing markdown code fences if Claude adds them anyway. */
77
+ function stripFences(text) {
78
+ return text
79
+ .replace(/^```[\w]*\r?\n/m, "")
80
+ .replace(/\r?\n```$/m, "")
81
+ .trim();
82
+ }
83
+ // ─── Public API ───────────────────────────────────────────────────────────────
84
+ /**
85
+ * Enhance a stub-only TestGenResult by asking Claude to fill in real assertions.
86
+ * Falls back to the original stubs if the API is unavailable or `opts.apiKey` / the
87
+ * ANTHROPIC_API_KEY env var is not set.
88
+ */
89
+ export async function aiEnhanceTests(result, sourceCode, language, opts = {}) {
90
+ const enhanced = await callClaude(buildPrompt(result.sourceFile, sourceCode, result.content, result.framework, language), opts);
91
+ const cleaned = stripFences(enhanced);
92
+ return { ...result, content: cleaned, aiEnhanced: true };
93
+ }
94
+ /**
95
+ * Like `aiEnhanceTests` but never throws — returns original stubs if the API call
96
+ * fails, and sets `aiEnhanced: false` along with an `error` field for diagnostics.
97
+ */
98
+ export async function tryAiEnhanceTests(result, sourceCode, language, opts = {}) {
99
+ try {
100
+ return await aiEnhanceTests(result, sourceCode, language, opts);
101
+ }
102
+ catch (e) {
103
+ return { ...result, aiEnhanced: false, error: e instanceof Error ? e.message : String(e) };
104
+ }
105
+ }
@@ -0,0 +1,82 @@
1
+ function globToRegex(pattern) {
2
+ let result = "";
3
+ for (let i = 0; i < pattern.length; i++) {
4
+ const c = pattern[i];
5
+ if (c === "*" && pattern[i + 1] === "*") {
6
+ result += ".*";
7
+ i++;
8
+ }
9
+ else if (c === "*") {
10
+ result += "[^/]*";
11
+ }
12
+ else if (c === "?") {
13
+ result += "[^/]";
14
+ }
15
+ else if (/[.+^${}()|[\]\\]/.test(c)) {
16
+ result += "\\" + c;
17
+ }
18
+ else {
19
+ result += c;
20
+ }
21
+ }
22
+ return new RegExp("^" + result + "$");
23
+ }
24
+ function matchGlob(pattern, str) {
25
+ try {
26
+ return globToRegex(pattern).test(str);
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ export function loadArchRules(projectConfig) {
33
+ const arch = projectConfig.arch;
34
+ return arch?.rules ?? [];
35
+ }
36
+ export function checkArchRules(graph, rules) {
37
+ if (rules.length === 0)
38
+ return [];
39
+ const violations = [];
40
+ const fileImports = new Map();
41
+ for (const edge of graph.edges) {
42
+ if (edge.edgeType === "imports") {
43
+ const fromFile = edge.from.split("::")[0];
44
+ const toFile = edge.to.split("::")[0];
45
+ if (!fileImports.has(fromFile))
46
+ fileImports.set(fromFile, new Set());
47
+ fileImports.get(fromFile).add(toFile);
48
+ }
49
+ }
50
+ const allFiles = [...fileImports.keys()];
51
+ for (const rule of rules) {
52
+ const severity = rule.severity ?? "error";
53
+ const fromFiles = allFiles.filter(f => matchGlob(rule.from, f));
54
+ for (const file of fromFiles) {
55
+ const imports = fileImports.get(file) ?? new Set();
56
+ if (rule.forbidImport) {
57
+ for (const imp of imports) {
58
+ if (matchGlob(rule.forbidImport, imp)) {
59
+ violations.push({
60
+ rule: rule.name ?? `forbid: ${rule.from} → ${rule.forbidImport}`,
61
+ severity,
62
+ file,
63
+ message: rule.message ?? `"${file}" must not import "${imp}" (matches ${rule.forbidImport})`,
64
+ });
65
+ }
66
+ }
67
+ }
68
+ if (rule.requireImport) {
69
+ const hasRequired = [...imports].some(imp => matchGlob(rule.requireImport, imp));
70
+ if (!hasRequired) {
71
+ violations.push({
72
+ rule: rule.name ?? `require: ${rule.from} → ${rule.requireImport}`,
73
+ severity,
74
+ file,
75
+ message: rule.message ?? `"${file}" must import something matching "${rule.requireImport}"`,
76
+ });
77
+ }
78
+ }
79
+ }
80
+ }
81
+ return violations;
82
+ }