codesummary 1.2.0 → 1.2.2

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,337 @@
1
+ export function buildSemanticClustersPrompt(context) {
2
+ const compactClusters = context.semanticClusters.map(cluster => ({
3
+ name: cluster.name,
4
+ files: cluster.files.slice(0, 12)
5
+ }));
6
+
7
+ return [
8
+ {
9
+ role: 'system',
10
+ content:
11
+ 'You are a software architecture assistant. ' +
12
+ 'Return only valid JSON. Do not include markdown fences.'
13
+ },
14
+ {
15
+ role: 'user',
16
+ content: JSON.stringify({
17
+ task: 'Refine semantic repository clusters',
18
+ constraints: {
19
+ keepClusterCountCloseToInput: true,
20
+ maxWordsPerDescription: 24,
21
+ preserveFilePaths: true,
22
+ outputFormat: {
23
+ clusters: [
24
+ {
25
+ name: 'string',
26
+ description: 'string',
27
+ files: ['path1', 'path2']
28
+ }
29
+ ]
30
+ }
31
+ },
32
+ input: {
33
+ projectName: context.projectName,
34
+ entrypoints: context.entrypoints.slice(0, 12),
35
+ coreModules: context.coreModules.slice(0, 12).map(item => item.path || item),
36
+ semanticClusters: compactClusters
37
+ }
38
+ })
39
+ }
40
+ ];
41
+ }
42
+
43
+ export function buildPdfProjectIntroPrompt(context) {
44
+ return [
45
+ {
46
+ role: 'system',
47
+ content:
48
+ 'You are a software architecture assistant. ' +
49
+ 'Return only valid JSON. Do not include markdown fences.'
50
+ },
51
+ {
52
+ role: 'user',
53
+ content: JSON.stringify({
54
+ task: 'Generate a concise project context block for a technical PDF report',
55
+ constraints: {
56
+ projectAgnostic: true,
57
+ evidenceOnly: true,
58
+ avoidDomainAssumptions: true,
59
+ fallbackWhenMissingEvidence: 'Not detected from available repository signals',
60
+ maxWordsPerField: 80,
61
+ tone: 'neutral technical',
62
+ avoidMarketingLanguage: true,
63
+ outputFormat: {
64
+ overview: 'string',
65
+ primaryPurpose: 'string',
66
+ keyComponents: ['string'],
67
+ suggestedReadingPath: ['string']
68
+ }
69
+ },
70
+ input: {
71
+ projectName: context.projectName,
72
+ totalFiles: context.totalFiles,
73
+ selectedExtensions: context.selectedExtensions,
74
+ topExtensions: context.topExtensions,
75
+ entrypoints: context.entrypoints || [],
76
+ coreModules: context.coreModules || [],
77
+ semanticClusters: context.semanticClusters || []
78
+ }
79
+ })
80
+ }
81
+ ];
82
+ }
83
+
84
+ export function buildPdfArchitectureInsightsPrompt(context) {
85
+ return [
86
+ {
87
+ role: 'system',
88
+ content:
89
+ 'You are a software architecture assistant. ' +
90
+ 'Return only valid JSON. Do not include markdown fences.'
91
+ },
92
+ {
93
+ role: 'user',
94
+ content: JSON.stringify({
95
+ task: 'Generate Architecture & Design Patterns insights for a technical PDF report',
96
+ constraints: {
97
+ projectAgnostic: true,
98
+ evidenceOnly: true,
99
+ avoidDomainAssumptions: true,
100
+ fallbackWhenMissingEvidence: 'Not detected from available repository signals',
101
+ language: 'English',
102
+ maxSentenceLength: 220,
103
+ maxItemsPerList: 8,
104
+ avoidSpeculationWithoutEvidence: true,
105
+ outputFormat: {
106
+ structuralPatterns: ['string'],
107
+ implementationParadigm: ['string'],
108
+ couplingPoints: ['string']
109
+ }
110
+ },
111
+ input: context
112
+ })
113
+ }
114
+ ];
115
+ }
116
+
117
+ export function buildPdfOperationsInsightsPrompt(context) {
118
+ return [
119
+ {
120
+ role: 'system',
121
+ content:
122
+ 'You are a software architecture assistant. ' +
123
+ 'Return only valid JSON. Do not include markdown fences.'
124
+ },
125
+ {
126
+ role: 'user',
127
+ content: JSON.stringify({
128
+ task: 'Generate Data/State lifecycle + configuration strategy + language breakdown insights for a technical PDF report',
129
+ constraints: {
130
+ projectAgnostic: true,
131
+ evidenceOnly: true,
132
+ avoidDomainAssumptions: true,
133
+ fallbackWhenMissingEvidence: 'Not detected from available repository signals',
134
+ language: 'English',
135
+ maxSentenceLength: 220,
136
+ maxItemsPerList: 10,
137
+ outputFormat: {
138
+ dataAndStateLifecycle: {
139
+ inboundOutbound: ['string'],
140
+ criticalTransformations: ['string'],
141
+ stateManagement: ['string']
142
+ },
143
+ configurationAndEnvironmentStrategy: {
144
+ configurationHierarchy: ['string'],
145
+ infrastructureDependencies: ['string']
146
+ },
147
+ languageSpecificBreakdown: {
148
+ responsibilityDistribution: ['string'],
149
+ interoperability: ['string']
150
+ }
151
+ }
152
+ },
153
+ input: context
154
+ })
155
+ }
156
+ ];
157
+ }
158
+
159
+ export function buildPdfMaintenanceAndSecurityPrompt(context) {
160
+ return [
161
+ {
162
+ role: 'system',
163
+ content:
164
+ 'You are a software architecture assistant. ' +
165
+ 'Return only valid JSON. Do not include markdown fences.'
166
+ },
167
+ {
168
+ role: 'user',
169
+ content: JSON.stringify({
170
+ task: 'Generate Onboarding/Maintenance guidance plus Security/Compliance insights for a technical PDF report',
171
+ constraints: {
172
+ projectAgnostic: true,
173
+ evidenceOnly: true,
174
+ avoidDomainAssumptions: true,
175
+ fallbackWhenMissingEvidence: 'Not detected from available repository signals',
176
+ language: 'English',
177
+ maxSentenceLength: 220,
178
+ maxItemsPerList: 10,
179
+ outputFormat: {
180
+ onboardingAndMaintenanceGuide: {
181
+ complexityHotspots: ['string'],
182
+ modificationGuide: ['string'],
183
+ domainGlossary: ['string']
184
+ },
185
+ securityAndComplianceSurface: {
186
+ exposureSurface: ['string'],
187
+ sensitiveDataHandling: ['string']
188
+ }
189
+ }
190
+ },
191
+ input: context
192
+ })
193
+ }
194
+ ];
195
+ }
196
+
197
+ export function buildPdfVisualInsightsPrompt(context) {
198
+ return [
199
+ {
200
+ role: 'system',
201
+ content:
202
+ 'You are a software architecture assistant. ' +
203
+ 'Return only valid JSON. Do not include markdown fences.'
204
+ },
205
+ {
206
+ role: 'user',
207
+ content: JSON.stringify({
208
+ task: 'Generate text-based visualisations for a technical PDF report',
209
+ constraints: {
210
+ projectAgnostic: true,
211
+ evidenceOnly: true,
212
+ avoidDomainAssumptions: true,
213
+ fallbackWhenMissingEvidence: 'Not detected from available repository signals',
214
+ language: 'English',
215
+ maxLinesPerBlock: 40,
216
+ outputFormat: {
217
+ semanticDirectoryTree: ['string'],
218
+ dependencyTextGraph: ['string']
219
+ }
220
+ },
221
+ input: context
222
+ })
223
+ }
224
+ ];
225
+ }
226
+
227
+ export function buildPdfApplicabilityPrompt(context) {
228
+ return [
229
+ {
230
+ role: 'system',
231
+ content:
232
+ 'You are a software architecture assistant. ' +
233
+ 'Return only valid JSON. Do not include markdown fences.'
234
+ },
235
+ {
236
+ role: 'user',
237
+ content: JSON.stringify({
238
+ task: 'Explain why the generated architecture analysis is useful across project categories',
239
+ constraints: {
240
+ projectAgnostic: true,
241
+ evidenceOnly: true,
242
+ avoidDomainAssumptions: true,
243
+ fallbackWhenMissingEvidence: 'Not detected from available repository signals',
244
+ language: 'English',
245
+ maxSentenceLength: 220,
246
+ maxItemsPerList: 8,
247
+ outputFormat: {
248
+ whyItHelpsAcrossProjects: {
249
+ infrastructureAsCode: ['string'],
250
+ dataPlatforms: ['string'],
251
+ frontendApplications: ['string'],
252
+ backendServices: ['string'],
253
+ genericTransferableValue: ['string']
254
+ }
255
+ }
256
+ },
257
+ input: context
258
+ })
259
+ }
260
+ ];
261
+ }
262
+
263
+ export function buildPdfExecutiveBriefPrompt(context) {
264
+ return [
265
+ {
266
+ role: 'system',
267
+ content:
268
+ 'You are a Principal Technical Writer, elite Software Architect, and editorial design director. ' +
269
+ 'Return only valid JSON. Do not include markdown fences.'
270
+ },
271
+ {
272
+ role: 'user',
273
+ content: JSON.stringify({
274
+ task: 'Generate a high-impact, infographic-ready technical executive brief for a PDF',
275
+ constraints: {
276
+ language: 'English',
277
+ projectAgnostic: true,
278
+ evidenceOnly: true,
279
+ avoidDomainAssumptions: true,
280
+ style: 'clinical, concise, high-signal',
281
+ paragraphLimitLines: 3,
282
+ maxItemsPerList: 5,
283
+ fallbackWhenMissingEvidence: 'Not detected from available repository signals',
284
+ sections: [
285
+ 'Executive Summary',
286
+ 'Architecture Blueprint',
287
+ 'Data Flow',
288
+ 'Onboarding Quick Guide',
289
+ 'Risk & Hotspots'
290
+ ],
291
+ outputFormat: {
292
+ executiveSummary: {
293
+ purpose: 'string (exactly 2 sentences)',
294
+ killerFeatures: ['string (exactly 3 items)']
295
+ },
296
+ architectureBlueprint: [
297
+ {
298
+ pattern: 'string',
299
+ primaryModule: 'string',
300
+ value: 'string'
301
+ }
302
+ ],
303
+ dataFlow: {
304
+ primaryFlow: 'string (A -> B -> C format)',
305
+ keyFlows: ['string']
306
+ },
307
+ onboardingQuickGuide: [
308
+ {
309
+ goal: 'string',
310
+ modify: 'string'
311
+ }
312
+ ],
313
+ riskHotspots: [
314
+ {
315
+ severity: 'string',
316
+ file: 'string',
317
+ reason: 'string'
318
+ }
319
+ ]
320
+ }
321
+ },
322
+ input: context
323
+ })
324
+ }
325
+ ];
326
+ }
327
+
328
+ export default {
329
+ buildSemanticClustersPrompt,
330
+ buildPdfProjectIntroPrompt,
331
+ buildPdfArchitectureInsightsPrompt,
332
+ buildPdfOperationsInsightsPrompt,
333
+ buildPdfMaintenanceAndSecurityPrompt,
334
+ buildPdfVisualInsightsPrompt,
335
+ buildPdfApplicabilityPrompt,
336
+ buildPdfExecutiveBriefPrompt
337
+ };
@@ -0,0 +1,81 @@
1
+ import OpenAiCompatibleProvider from './providers/openaiCompatible.js';
2
+ import OllamaProvider from './providers/ollama.js';
3
+ import { AiProviderError, normalizeAiError } from './errors.js';
4
+
5
+ export default class ProviderClient {
6
+ constructor(config = {}) {
7
+ this.config = config;
8
+ this.provider = this.createProvider(config);
9
+ }
10
+
11
+ createProvider(config) {
12
+ const providerName = (config.provider || 'openai-compatible').toLowerCase();
13
+ if (providerName === 'ollama') {
14
+ return new OllamaProvider(config);
15
+ }
16
+ return new OpenAiCompatibleProvider(config);
17
+ }
18
+
19
+ async chat(messages, options = {}) {
20
+ const maxRetries = Number.isInteger(options.maxRetries)
21
+ ? options.maxRetries
22
+ : (Number.isInteger(this.config.maxRetries) ? this.config.maxRetries : 2);
23
+ const initialBackoffMs = Number.isInteger(options.retryBackoffMs)
24
+ ? options.retryBackoffMs
25
+ : (Number.isInteger(this.config.retryBackoffMs) ? this.config.retryBackoffMs : 500);
26
+ const maxBackoffMs = Number.isInteger(options.maxBackoffMs)
27
+ ? options.maxBackoffMs
28
+ : (Number.isInteger(options.maxBackoffMaxMs)
29
+ ? options.maxBackoffMaxMs
30
+ : (Number.isInteger(this.config.maxBackoffMs) ? this.config.maxBackoffMs : 5000));
31
+
32
+ const totalAttempts = Math.max(1, maxRetries + 1);
33
+ let lastError = null;
34
+
35
+ for (let attempt = 1; attempt <= totalAttempts; attempt++) {
36
+ try {
37
+ const response = await this.provider.chat(messages, options);
38
+ return {
39
+ ...response,
40
+ attempts: attempt
41
+ };
42
+ } catch (error) {
43
+ const normalized = normalizeAiError(error, { provider: this.getModelInfo().provider });
44
+ lastError = normalized;
45
+ const canRetry = normalized.retryable && attempt < totalAttempts;
46
+
47
+ if (!canRetry) {
48
+ throw new AiProviderError(normalized.message, {
49
+ ...normalized,
50
+ details: {
51
+ ...(normalized.details || {}),
52
+ attempts: attempt
53
+ }
54
+ });
55
+ }
56
+
57
+ const delay = Math.min(maxBackoffMs, initialBackoffMs * Math.pow(2, attempt - 1));
58
+ const jitter = Math.floor(delay * 0.2 * Math.random());
59
+ await this.sleep(delay + jitter);
60
+ }
61
+ }
62
+
63
+ throw lastError || new AiProviderError('AI request failed', {
64
+ code: 'ai_error',
65
+ retryable: false,
66
+ provider: this.getModelInfo().provider
67
+ });
68
+ }
69
+
70
+ async healthCheck() {
71
+ return this.provider.healthCheck();
72
+ }
73
+
74
+ getModelInfo() {
75
+ return this.provider.getModelInfo();
76
+ }
77
+
78
+ sleep(ms) {
79
+ return new Promise(resolve => setTimeout(resolve, ms));
80
+ }
81
+ }
@@ -0,0 +1,92 @@
1
+ import { AiProviderError, createHttpAiError, normalizeAiError } from '../errors.js';
2
+
3
+ export default class OllamaProvider {
4
+ constructor(config) {
5
+ this.config = config;
6
+ this.providerName = 'ollama';
7
+ }
8
+
9
+ async chat(messages, options = {}) {
10
+ const timeoutMs = options.timeoutMs || this.config.timeoutMs || 30000;
11
+ const controller = new AbortController();
12
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
13
+ const baseUrl = (this.config.baseUrl || 'http://localhost:11434').replace(/\/+$/, '');
14
+ const url = `${baseUrl}/api/chat`;
15
+
16
+ try {
17
+ const response = await fetch(url, {
18
+ method: 'POST',
19
+ signal: controller.signal,
20
+ headers: {
21
+ 'Content-Type': 'application/json'
22
+ },
23
+ body: JSON.stringify({
24
+ model: options.model || this.config.model || 'llama3.1',
25
+ messages,
26
+ stream: false
27
+ })
28
+ });
29
+
30
+ if (!response.ok) {
31
+ const errorText = await response.text();
32
+ throw createHttpAiError(this.providerName, response.status, errorText);
33
+ }
34
+
35
+ const data = await response.json();
36
+ const content = data?.message?.content;
37
+ if (!content) {
38
+ throw new AiProviderError('Ollama response missing content', {
39
+ code: 'invalid_response',
40
+ retryable: false,
41
+ provider: this.providerName
42
+ });
43
+ }
44
+ return { content, raw: data };
45
+ } catch (error) {
46
+ throw normalizeAiError(error, { provider: this.providerName });
47
+ } finally {
48
+ clearTimeout(timer);
49
+ }
50
+ }
51
+
52
+ async healthCheck() {
53
+ const timeoutMs = Math.min(this.config.timeoutMs || 30000, 7000);
54
+ const controller = new AbortController();
55
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
56
+ const baseUrl = (this.config.baseUrl || 'http://localhost:11434').replace(/\/+$/, '');
57
+ const url = `${baseUrl}/api/tags`;
58
+
59
+ try {
60
+ const response = await fetch(url, {
61
+ method: 'GET',
62
+ signal: controller.signal
63
+ });
64
+
65
+ if (!response.ok) {
66
+ const text = await response.text();
67
+ return {
68
+ ok: false,
69
+ provider: this.providerName,
70
+ error: createHttpAiError(this.providerName, response.status, text)
71
+ };
72
+ }
73
+
74
+ return { ok: true, provider: this.providerName };
75
+ } catch (error) {
76
+ return {
77
+ ok: false,
78
+ provider: this.providerName,
79
+ error: normalizeAiError(error, { provider: this.providerName })
80
+ };
81
+ } finally {
82
+ clearTimeout(timer);
83
+ }
84
+ }
85
+
86
+ getModelInfo() {
87
+ return {
88
+ provider: 'ollama',
89
+ model: this.config.model || 'llama3.1'
90
+ };
91
+ }
92
+ }
@@ -0,0 +1,96 @@
1
+ import { AiProviderError, createHttpAiError, normalizeAiError } from '../errors.js';
2
+
3
+ export default class OpenAiCompatibleProvider {
4
+ constructor(config) {
5
+ this.config = config;
6
+ this.providerName = 'openai-compatible';
7
+ }
8
+
9
+ async chat(messages, options = {}) {
10
+ const timeoutMs = options.timeoutMs || this.config.timeoutMs || 30000;
11
+ const controller = new AbortController();
12
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
13
+ const baseUrl = (this.config.baseUrl || '').replace(/\/+$/, '');
14
+ const url = `${baseUrl}/chat/completions`;
15
+
16
+ try {
17
+ const response = await fetch(url, {
18
+ method: 'POST',
19
+ signal: controller.signal,
20
+ headers: {
21
+ 'Content-Type': 'application/json',
22
+ ...(this.config.apiKey ? { Authorization: `Bearer ${this.config.apiKey}` } : {})
23
+ },
24
+ body: JSON.stringify({
25
+ model: options.model || this.config.model,
26
+ messages,
27
+ temperature: options.temperature ?? 0.2
28
+ })
29
+ });
30
+
31
+ if (!response.ok) {
32
+ const errorText = await response.text();
33
+ throw createHttpAiError(this.providerName, response.status, errorText);
34
+ }
35
+
36
+ const data = await response.json();
37
+ const content = data?.choices?.[0]?.message?.content;
38
+ if (!content) {
39
+ throw new AiProviderError('OpenAI-compatible response missing content', {
40
+ code: 'invalid_response',
41
+ retryable: false,
42
+ provider: this.providerName
43
+ });
44
+ }
45
+ return { content, raw: data };
46
+ } catch (error) {
47
+ throw normalizeAiError(error, { provider: this.providerName });
48
+ } finally {
49
+ clearTimeout(timer);
50
+ }
51
+ }
52
+
53
+ async healthCheck() {
54
+ const timeoutMs = Math.min(this.config.timeoutMs || 30000, 7000);
55
+ const controller = new AbortController();
56
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
57
+ const baseUrl = (this.config.baseUrl || '').replace(/\/+$/, '');
58
+ const url = `${baseUrl}/models`;
59
+
60
+ try {
61
+ const response = await fetch(url, {
62
+ method: 'GET',
63
+ signal: controller.signal,
64
+ headers: {
65
+ ...(this.config.apiKey ? { Authorization: `Bearer ${this.config.apiKey}` } : {})
66
+ }
67
+ });
68
+
69
+ if (!response.ok) {
70
+ const text = await response.text();
71
+ return {
72
+ ok: false,
73
+ provider: this.providerName,
74
+ error: createHttpAiError(this.providerName, response.status, text)
75
+ };
76
+ }
77
+
78
+ return { ok: true, provider: this.providerName };
79
+ } catch (error) {
80
+ return {
81
+ ok: false,
82
+ provider: this.providerName,
83
+ error: normalizeAiError(error, { provider: this.providerName })
84
+ };
85
+ } finally {
86
+ clearTimeout(timer);
87
+ }
88
+ }
89
+
90
+ getModelInfo() {
91
+ return {
92
+ provider: 'openai-compatible',
93
+ model: this.config.model || 'unknown'
94
+ };
95
+ }
96
+ }